Compare commits
14 Commits
dev
...
33571a5fd7
| Author | SHA256 | Date | |
|---|---|---|---|
|
|
33571a5fd7 | ||
|
|
94e211f87e | ||
|
|
8b6d3d5fad | ||
|
|
c89e5bdf7d | ||
|
|
10af713fa1 | ||
|
|
ebb263cd36 | ||
|
|
82d9c10130 | ||
|
|
cb24cf575b | ||
|
|
e4c07cb838 | ||
|
|
f38a5afdd7 | ||
|
|
9423c79c80 | ||
|
|
8a958b9c48 | ||
|
|
82882e859e | ||
|
|
2d28e7c1d5 |
@@ -1,6 +1,6 @@
|
||||
APP_NAME=Laravel
|
||||
APP_ENV=local
|
||||
APP_KEY=base64:/ulhRgiCOFjZV6xUDkXLfiR9X8iFRZ4QIiX3UJbdwY4=
|
||||
APP_KEY=
|
||||
APP_DEBUG=true
|
||||
APP_URL=http://localhost
|
||||
|
||||
|
||||
@@ -4,10 +4,10 @@ namespace App\Events;
|
||||
|
||||
use App\Models\ChatMessage;
|
||||
use Illuminate\Broadcasting\PrivateChannel;
|
||||
use Illuminate\Contracts\Broadcasting\ShouldBroadcastNow;
|
||||
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class NewChatMessage implements ShouldBroadcastNow
|
||||
class NewChatMessage implements ShouldBroadcast
|
||||
{
|
||||
use SerializesModels;
|
||||
|
||||
@@ -17,23 +17,30 @@ class NewChatMessage implements ShouldBroadcastNow
|
||||
* Create a new event instance.
|
||||
*/
|
||||
public function __construct(ChatMessage $message)
|
||||
{
|
||||
// Also load sender polymorphic relationship
|
||||
$message->load('sender');
|
||||
{
|
||||
// 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;
|
||||
}
|
||||
|
||||
$this->message = $message;
|
||||
}
|
||||
|
||||
/**
|
||||
* The channel the event should broadcast on.
|
||||
*/
|
||||
public function broadcastOn()
|
||||
{
|
||||
return [
|
||||
new PrivateChannel('ticket.' . $this->message->ticket_id),
|
||||
new PrivateChannel('admin.chat') // 👈 ADD THIS
|
||||
];
|
||||
|
||||
return new PrivateChannel('ticket.' . $this->message->ticket_id);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -41,40 +48,25 @@ class NewChatMessage implements ShouldBroadcastNow
|
||||
*/
|
||||
public function broadcastWith()
|
||||
{
|
||||
\Log::info('APP_URL USED IN EVENT', [
|
||||
'url' => config('app.url'),
|
||||
]);
|
||||
|
||||
\Log::info("DEBUG: NewChatMessage broadcasting on channel ticket.".$this->message->ticket_id);
|
||||
|
||||
\Log::info("EVENT BROADCAST FIRED", [
|
||||
'channel' => 'ticket.'.$this->message->ticket_id,
|
||||
'sender_type' => $this->message->sender_type,
|
||||
'sender_id' => $this->message->sender_id,
|
||||
]);
|
||||
|
||||
|
||||
return [
|
||||
'id' => $this->message->id,
|
||||
'ticket_id' => $this->message->ticket_id,
|
||||
'sender_id' => $this->message->sender_id,
|
||||
'sender_type' => $this->message->sender_type,
|
||||
'message' => $this->message->message,
|
||||
'client_id' => $this->message->client_id,
|
||||
|
||||
// ✅ relative path only
|
||||
'file_path' => $this->message->file_path ?? null,
|
||||
'file_type' => $this->message->file_type ?? null,
|
||||
|
||||
'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,
|
||||
'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(),
|
||||
];
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -92,10 +84,4 @@ class NewChatMessage implements ShouldBroadcastNow
|
||||
// Admin model has ->name
|
||||
return $sender->name ?? "Admin";
|
||||
}
|
||||
|
||||
public function broadcastAs()
|
||||
{
|
||||
return 'NewChatMessage';
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exports;
|
||||
|
||||
use Illuminate\Contracts\View\View;
|
||||
use Maatwebsite\Excel\Concerns\FromView;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class InvoicesExport implements FromView
|
||||
{
|
||||
protected $request;
|
||||
|
||||
public function __construct(Request $request)
|
||||
{
|
||||
$this->request = $request;
|
||||
}
|
||||
|
||||
public function view(): View
|
||||
{
|
||||
$request = $this->request;
|
||||
|
||||
$invoices = DB::table('invoices')
|
||||
->leftJoin('containers', 'containers.id', '=', 'invoices.container_id')
|
||||
->leftJoin('mark_list', 'mark_list.mark_no', '=', 'invoices.mark_no')
|
||||
->select(
|
||||
'invoices.invoice_number',
|
||||
'invoices.invoice_date',
|
||||
'invoices.mark_no',
|
||||
'containers.container_number',
|
||||
'containers.container_date',
|
||||
DB::raw('COALESCE(invoices.company_name, mark_list.company_name) as company_name'),
|
||||
DB::raw('COALESCE(invoices.customer_name, mark_list.customer_name) as customer_name'),
|
||||
'invoices.final_amount',
|
||||
'invoices.final_amount_with_gst',
|
||||
'invoices.status as invoice_status'
|
||||
)
|
||||
->when($request->filled('search'), function ($q) use ($request) {
|
||||
$search = trim($request->search);
|
||||
$q->where(function ($qq) use ($search) {
|
||||
$qq->where('invoices.invoice_number', 'like', "%{$search}%")
|
||||
->orWhere('invoices.mark_no', 'like', "%{$search}%")
|
||||
->orWhere('containers.container_number', 'like', "%{$search}%")
|
||||
->orWhere('mark_list.company_name', 'like', "%{$search}%")
|
||||
->orWhere('mark_list.customer_name', 'like', "%{$search}%");
|
||||
});
|
||||
})
|
||||
->when($request->filled('status'), function ($q) use ($request) {
|
||||
$q->where('invoices.status', $request->status);
|
||||
})
|
||||
->when($request->filled('from_date'), function ($q) use ($request) {
|
||||
$q->whereDate('invoices.invoice_date', '>=', $request->from_date);
|
||||
})
|
||||
->when($request->filled('to_date'), function ($q) use ($request) {
|
||||
$q->whereDate('invoices.invoice_date', '<=', $request->to_date);
|
||||
})
|
||||
->orderByDesc('containers.container_date')
|
||||
->orderByDesc('invoices.id')
|
||||
->get();
|
||||
|
||||
return view('admin.pdf.invoices_excel', compact('invoices'));
|
||||
}
|
||||
}
|
||||
@@ -5,10 +5,6 @@ namespace App\Http\Controllers\Admin;
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use App\Models\Container;
|
||||
use App\Models\Invoice;
|
||||
use App\Models\User;
|
||||
use App\Models\MarkList;
|
||||
|
||||
class AdminAuthController extends Controller
|
||||
{
|
||||
@@ -35,15 +31,16 @@ class AdminAuthController extends Controller
|
||||
}
|
||||
|
||||
$credentials = [
|
||||
$field => $loginInput,
|
||||
$field => $loginInput,
|
||||
'password' => $request->password,
|
||||
];
|
||||
|
||||
// attempt login
|
||||
if (Auth::guard('admin')->attempt($credentials)) {
|
||||
$request->session()->regenerate();
|
||||
$user = Auth::guard('admin')->user();
|
||||
return redirect()->route('admin.dashboard')
|
||||
->with('success', 'Welcome back, ' . $user->name . '!');
|
||||
|
||||
return redirect()->route('admin.dashboard')->with('success', 'Welcome back, ' . $user->name . '!');
|
||||
}
|
||||
|
||||
return back()->withErrors(['login' => 'Invalid login credentials.']);
|
||||
@@ -54,25 +51,6 @@ class AdminAuthController extends Controller
|
||||
Auth::guard('admin')->logout();
|
||||
$request->session()->invalidate();
|
||||
$request->session()->regenerateToken();
|
||||
return redirect()->route('admin.login')
|
||||
->with('success', 'Logged out successfully.');
|
||||
}
|
||||
|
||||
public function profile()
|
||||
{
|
||||
$user = Auth::guard('admin')->user();
|
||||
|
||||
// ── Real Stats ──
|
||||
$stats = [
|
||||
'total_containers' => Container::count(),
|
||||
'total_invoices' => Invoice::count(),
|
||||
'paid_invoices' => Invoice::where('status', 'paid')->count(),
|
||||
'pending_invoices' => Invoice::where('status', 'pending')->count(),
|
||||
'total_customers' => User::count(),
|
||||
'total_marklist' => MarkList::count(),
|
||||
'active_marklist' => MarkList::where('status', 'active')->count(),
|
||||
];
|
||||
|
||||
return view('admin.profile', compact('user', 'stats'));
|
||||
return redirect()->route('admin.login')->with('success', 'Logged out successfully.');
|
||||
}
|
||||
}
|
||||
@@ -3,14 +3,35 @@
|
||||
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()
|
||||
@@ -28,11 +49,26 @@ class AdminChatController extends Controller
|
||||
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);
|
||||
|
||||
@@ -53,6 +89,7 @@ class AdminChatController extends Controller
|
||||
|
||||
/**
|
||||
* Admin sends a message to the user
|
||||
>>>>>>> 8b6d3d5fadadda310ef45ec03c879b900bff4cb025f45d1bb5d188761d53e043
|
||||
*/
|
||||
public function sendMessage(Request $request, $ticketId)
|
||||
{
|
||||
@@ -69,9 +106,12 @@ class AdminChatController extends Controller
|
||||
'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
|
||||
@@ -85,11 +125,20 @@ class AdminChatController extends Controller
|
||||
$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));
|
||||
|
||||
@@ -97,6 +146,7 @@ class AdminChatController extends Controller
|
||||
'ticket_id' => $ticketId,
|
||||
'payload' => $request->all()
|
||||
]);
|
||||
>>>>>>> 8b6d3d5fadadda310ef45ec03c879b900bff4cb025f45d1bb5d188761d53e043
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => $message
|
||||
|
||||
@@ -6,8 +6,6 @@ use App\Http\Controllers\Controller;
|
||||
use App\Models\Invoice;
|
||||
use App\Models\InvoiceItem;
|
||||
use App\Models\InvoiceInstallment;
|
||||
use App\Models\InvoiceChargeGroup;
|
||||
use App\Models\InvoiceChargeGroupItem;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Mpdf\Mpdf;
|
||||
@@ -17,106 +15,49 @@ class AdminInvoiceController extends Controller
|
||||
// -------------------------------------------------------------
|
||||
// INVOICE LIST PAGE
|
||||
// -------------------------------------------------------------
|
||||
public function index(Request $request)
|
||||
public function index()
|
||||
{
|
||||
$query = Invoice::with(['items', 'customer', 'container']);
|
||||
|
||||
if ($request->filled('search')) {
|
||||
$search = $request->search;
|
||||
$query->where(function ($q) use ($search) {
|
||||
$q->where('invoice_number', 'like', "%{$search}%")
|
||||
->orWhere('customer_name', 'like', "%{$search}%");
|
||||
});
|
||||
}
|
||||
|
||||
if ($request->filled('status') && $request->status !== 'all') {
|
||||
$query->where('status', $request->status);
|
||||
}
|
||||
|
||||
if ($request->filled('start_date')) {
|
||||
$query->whereDate('invoice_date', '>=', $request->start_date);
|
||||
}
|
||||
|
||||
if ($request->filled('end_date')) {
|
||||
$query->whereDate('invoice_date', '<=', $request->end_date);
|
||||
}
|
||||
|
||||
$invoices = $query->latest()->get();
|
||||
$invoices = Invoice::with(['items', 'customer', 'container'])
|
||||
->latest()
|
||||
->get();
|
||||
|
||||
return view('admin.invoice', compact('invoices'));
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// POPUP VIEW
|
||||
// POPUP VIEW (AJAX)
|
||||
// -------------------------------------------------------------
|
||||
public function popup($id)
|
||||
{
|
||||
$invoice = Invoice::with([
|
||||
'items',
|
||||
'chargeGroups.items',
|
||||
])->findOrFail($id);
|
||||
{
|
||||
$invoice = Invoice::with(['items', 'customer', 'container'])->findOrFail($id);
|
||||
$shipment = null;
|
||||
|
||||
$shipment = null;
|
||||
|
||||
$groupedItemIds = $invoice->chargeGroups
|
||||
->flatMap(fn($group) => $group->items->pluck('invoice_item_id'))
|
||||
->unique()
|
||||
->values()
|
||||
->toArray();
|
||||
|
||||
return view('admin.popup_invoice', compact('invoice', 'shipment', 'groupedItemIds'));
|
||||
}
|
||||
return view('admin.popup_invoice', compact('invoice', 'shipment'));
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// EDIT INVOICE PAGE
|
||||
// -------------------------------------------------------------
|
||||
public function edit($id)
|
||||
{
|
||||
$invoice = Invoice::with([
|
||||
'items',
|
||||
'customer',
|
||||
'container',
|
||||
'chargeGroups.items',
|
||||
'installments',
|
||||
])->findOrFail($id);
|
||||
|
||||
// ✅ Customer details sync — जर test data आला असेल तर fix होईल
|
||||
if ($invoice->customer) {
|
||||
$needsUpdate = [];
|
||||
|
||||
if (empty($invoice->customer_email) || $invoice->customer_email === 'test@demo.com') {
|
||||
$needsUpdate['customer_email'] = $invoice->customer->email;
|
||||
}
|
||||
|
||||
if (empty($invoice->customer_address) || $invoice->customer_address === 'TEST ADDRESS') {
|
||||
$needsUpdate['customer_address'] = $invoice->customer->address;
|
||||
}
|
||||
|
||||
if (empty($invoice->pincode) || $invoice->pincode === '999999') {
|
||||
$needsUpdate['pincode'] = $invoice->customer->pincode;
|
||||
}
|
||||
|
||||
if (!empty($needsUpdate)) {
|
||||
$invoice->update($needsUpdate);
|
||||
$invoice->refresh();
|
||||
}
|
||||
}
|
||||
|
||||
$invoice = Invoice::with(['items', 'customer', 'container'])->findOrFail($id);
|
||||
$shipment = null;
|
||||
|
||||
$groupedItemIds = $invoice->chargeGroups
|
||||
->flatMap(function ($group) {
|
||||
return $group->items->pluck('invoice_item_id');
|
||||
})
|
||||
->unique()
|
||||
->values()
|
||||
->toArray();
|
||||
// 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');
|
||||
|
||||
return view('admin.invoice_edit', compact('invoice', 'shipment', 'groupedItemIds'));
|
||||
// Pass the new variable to the view
|
||||
return view('admin.invoice_edit', compact('invoice', 'shipment', 'customerTotalDue'));
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// UPDATE INVOICE (HEADER ONLY)
|
||||
// UPDATE INVOICE (HEADER LEVEL)
|
||||
// -------------------------------------------------------------
|
||||
// -------------------------------------------------------------
|
||||
// UPDATE INVOICE (HEADER LEVEL)
|
||||
// -------------------------------------------------------------
|
||||
public function update(Request $request, $id)
|
||||
{
|
||||
@@ -127,34 +68,85 @@ class AdminInvoiceController extends Controller
|
||||
|
||||
$invoice = Invoice::findOrFail($id);
|
||||
|
||||
// 1) VALIDATION
|
||||
$data = $request->validate([
|
||||
'invoice_date' => 'required|date',
|
||||
'due_date' => 'required|date|after_or_equal:invoice_date',
|
||||
'status' => 'required|in:pending,paying,paid,overdue',
|
||||
'final_amount' => 'required|numeric|min:0',
|
||||
'tax_type' => 'required|in:gst,igst',
|
||||
'tax_percent' => 'required|numeric|min:0|max:28',
|
||||
'status' => 'required|in:pending,paid,overdue',
|
||||
'notes' => 'nullable|string',
|
||||
]);
|
||||
|
||||
Log::info('✅ Validated Invoice Header Update Data', $data);
|
||||
Log::info('✅ Validated Invoice Update Data', $data);
|
||||
|
||||
$invoice->update($data);
|
||||
$invoice->refresh();
|
||||
// 2) CALCULATE GST / TOTALS
|
||||
$finalAmount = (float) $data['final_amount'];
|
||||
$taxPercent = (float) $data['tax_percent'];
|
||||
|
||||
Log::info('🔍 Invoice AFTER HEADER UPDATE', [
|
||||
'invoice_id' => $invoice->id,
|
||||
'charge_groups_total' => $invoice->charge_groups_total,
|
||||
'gst_amount' => $invoice->gst_amount,
|
||||
'grand_total_with_charges'=> $invoice->grand_total_with_charges,
|
||||
if ($data['tax_type'] === 'gst') {
|
||||
Log::info('🟢 GST Selected', compact('taxPercent'));
|
||||
|
||||
$data['cgst_percent'] = $taxPercent / 2;
|
||||
$data['sgst_percent'] = $taxPercent / 2;
|
||||
$data['igst_percent'] = 0;
|
||||
} else {
|
||||
Log::info('🔵 IGST Selected', compact('taxPercent'));
|
||||
|
||||
$data['cgst_percent'] = 0;
|
||||
$data['sgst_percent'] = 0;
|
||||
$data['igst_percent'] = $taxPercent;
|
||||
}
|
||||
|
||||
$gstAmount = ($finalAmount * $taxPercent) / 100;
|
||||
$data['gst_amount'] = $gstAmount;
|
||||
$data['final_amount_with_gst'] = $finalAmount + $gstAmount;
|
||||
$data['gst_percent'] = $taxPercent;
|
||||
|
||||
Log::info('📌 Final Calculated Invoice Values', [
|
||||
'invoice_id' => $invoice->id,
|
||||
'final_amount' => $finalAmount,
|
||||
'gst_amount' => $data['gst_amount'],
|
||||
'final_amount_with_gst' => $data['final_amount_with_gst'],
|
||||
'tax_type' => $data['tax_type'],
|
||||
'cgst_percent' => $data['cgst_percent'],
|
||||
'sgst_percent' => $data['sgst_percent'],
|
||||
'igst_percent' => $data['igst_percent'],
|
||||
]);
|
||||
|
||||
// 3) UPDATE DB
|
||||
$invoice->update($data);
|
||||
|
||||
Log::info('✅ Invoice Updated Successfully', [
|
||||
'invoice_id' => $invoice->id,
|
||||
]);
|
||||
|
||||
// 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);
|
||||
|
||||
return redirect()
|
||||
->route('admin.invoices.edit', $invoice->id)
|
||||
->with('success', 'Invoice updated & PDF generated successfully.');
|
||||
->route('admin.invoices.index')
|
||||
->with('success', 'Invoice updated & PDF generated successfully.');
|
||||
}
|
||||
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// UPDATE INVOICE ITEMS (फक्त items save)
|
||||
// 🔹 UPDATE INVOICE ITEMS (price + ttl_amount)
|
||||
// -------------------------------------------------------------
|
||||
public function updateItems(Request $request, Invoice $invoice)
|
||||
{
|
||||
@@ -169,7 +161,9 @@ class AdminInvoiceController extends Controller
|
||||
'items.*.ttl_amount' => ['required', 'numeric', 'min:0'],
|
||||
]);
|
||||
|
||||
foreach ($data['items'] as $itemId => $itemData) {
|
||||
$itemsInput = $data['items'];
|
||||
|
||||
foreach ($itemsInput as $itemId => $itemData) {
|
||||
$item = InvoiceItem::where('id', $itemId)
|
||||
->where('invoice_id', $invoice->id)
|
||||
->first();
|
||||
@@ -187,15 +181,46 @@ class AdminInvoiceController extends Controller
|
||||
$item->save();
|
||||
}
|
||||
|
||||
Log::info('✅ Invoice items updated (no totals recalculation)', [
|
||||
'invoice_id' => $invoice->id,
|
||||
$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
|
||||
// PDF GENERATION USING mPDF
|
||||
// -------------------------------------------------------------
|
||||
public function generateInvoicePDF($invoice)
|
||||
{
|
||||
@@ -232,6 +257,17 @@ class AdminInvoiceController extends Controller
|
||||
$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)
|
||||
// -------------------------------------------------------------
|
||||
@@ -244,12 +280,9 @@ class AdminInvoiceController extends Controller
|
||||
'amount' => 'required|numeric|min:1',
|
||||
]);
|
||||
|
||||
$invoice = Invoice::findOrFail($invoice_id);
|
||||
|
||||
$grandTotal = $invoice->grand_total_with_charges ?? 0;
|
||||
|
||||
$invoice = Invoice::findOrFail($invoice_id);
|
||||
$paidTotal = $invoice->installments()->sum('amount');
|
||||
$remaining = $grandTotal - $paidTotal;
|
||||
$remaining = $invoice->final_amount_with_gst - $paidTotal;
|
||||
|
||||
if ($request->amount > $remaining) {
|
||||
return response()->json([
|
||||
@@ -266,38 +299,24 @@ class AdminInvoiceController extends Controller
|
||||
'amount' => $request->amount,
|
||||
]);
|
||||
|
||||
$newPaid = $paidTotal + $request->amount;
|
||||
$remaining = max(0, $grandTotal - $newPaid);
|
||||
$newPaid = $paidTotal + $request->amount;
|
||||
|
||||
if ($newPaid >= $invoice->final_amount_with_gst) {
|
||||
$invoice->update(['status' => 'paid']);
|
||||
|
||||
|
||||
// Full payment logic (जर पूर्ण भरले तर status paid करणे, नाहीतर pending राहील)
|
||||
// if ($newPaid >= $grandTotal && $grandTotal > 0) {
|
||||
// $invoice->update([
|
||||
// 'payment_method' => $request->payment_method,
|
||||
// 'reference_no' => $request->reference_no,
|
||||
// 'status' => ($newPaid >= $grandTotal && $grandTotal > 0) ? 'paid' : $invoice->status,
|
||||
// ]);
|
||||
// }
|
||||
|
||||
// Partial payment status logic:
|
||||
$invoice->update([
|
||||
'payment_method' => $request->payment_method,
|
||||
'reference_no' => $request->reference_no,
|
||||
'status' => ($newPaid >= $grandTotal && $grandTotal > 0) ? 'paid' : $invoice->status,
|
||||
]);
|
||||
$this->generateInvoicePDF($invoice);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'status' => 'success',
|
||||
'message' => 'Installment added successfully.',
|
||||
'installment' => $installment,
|
||||
'chargeGroupsTotal' => $invoice->charge_groups_total ?? 0,
|
||||
'gstAmount' => $invoice->gst_amount ?? 0,
|
||||
'grandTotal' => $grandTotal,
|
||||
'totalPaid' => $newPaid,
|
||||
'remaining' => $remaining,
|
||||
'isCompleted' => $remaining <= 0,
|
||||
'isZero' => $newPaid == 0,
|
||||
'status' => 'success',
|
||||
'message' => 'Installment added successfully.',
|
||||
'installment' => $installment,
|
||||
'totalPaid' => $newPaid,
|
||||
'gstAmount' => $invoice->gst_amount,
|
||||
'finalAmountWithGst' => $invoice->final_amount_with_gst,
|
||||
'baseAmount' => $invoice->final_amount,
|
||||
'remaining' => max(0, $invoice->final_amount_with_gst - $newPaid),
|
||||
'isCompleted' => $newPaid >= $invoice->final_amount_with_gst,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -310,158 +329,25 @@ class AdminInvoiceController extends Controller
|
||||
$invoice = $installment->invoice;
|
||||
|
||||
$installment->delete();
|
||||
$invoice->refresh();
|
||||
|
||||
$grandTotal = $invoice->grand_total_with_charges ?? 0;
|
||||
|
||||
$paidTotal = $invoice->installments()->sum('amount');
|
||||
$remaining = max(0, $grandTotal - $paidTotal);
|
||||
$remaining = $invoice->final_amount_with_gst - $paidTotal;
|
||||
|
||||
if ($paidTotal <= 0 && $grandTotal > 0) {
|
||||
$invoice->update(['status' => 'pending']);
|
||||
} elseif ($paidTotal > 0 && $paidTotal < $grandTotal) {
|
||||
if ($remaining > 0 && $invoice->status === 'paid') {
|
||||
$invoice->update(['status' => 'pending']);
|
||||
|
||||
$this->generateInvoicePDF($invoice);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'status' => 'success',
|
||||
'message' => 'Installment deleted.',
|
||||
'chargeGroupsTotal' => $invoice->charge_groups_total ?? 0,
|
||||
'gstAmount' => $invoice->gst_amount ?? 0,
|
||||
'grandTotal' => $grandTotal,
|
||||
'totalPaid' => $paidTotal,
|
||||
'remaining' => $remaining,
|
||||
'isZero' => $paidTotal == 0,
|
||||
]);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// CHARGE GROUP SAVE
|
||||
// -------------------------------------------------------------
|
||||
public function storeChargeGroup(Request $request, $invoiceId)
|
||||
{
|
||||
Log::info('🟡 storeChargeGroup HIT', [
|
||||
'invoice_id' => $invoiceId,
|
||||
'payload' => $request->all(),
|
||||
]);
|
||||
|
||||
$invoice = Invoice::with('items', 'chargeGroups')->findOrFail($invoiceId);
|
||||
|
||||
$data = $request->validate([
|
||||
'groupname' => 'required|string|max:255',
|
||||
'basistype' => 'required|in:ttl_qty,amount,ttl_cbm,ttl_kg',
|
||||
'basisvalue' => 'required|numeric',
|
||||
'rate' => 'required|numeric|min:0.0001',
|
||||
'autototal' => 'required|numeric|min:0.01',
|
||||
'itemids' => 'required|array',
|
||||
'itemids.*' => 'integer|exists:invoice_items,id',
|
||||
|
||||
'tax_type' => 'nullable|in:none,gst,igst',
|
||||
'gst_percent' => 'nullable|numeric|min:0|max:28',
|
||||
'total_with_gst' => 'nullable|numeric|min:0',
|
||||
]);
|
||||
|
||||
Log::info('✅ storeChargeGroup VALIDATED', $data);
|
||||
|
||||
// duplicate name check
|
||||
$exists = InvoiceChargeGroup::where('invoice_id', $invoice->id)
|
||||
->where('group_name', $data['groupname'])
|
||||
->exists();
|
||||
|
||||
if ($exists) {
|
||||
return back()
|
||||
->withErrors(['groupname' => 'This group name is already used for this invoice.'])
|
||||
->withInput();
|
||||
}
|
||||
|
||||
$taxType = $data['tax_type'] ?? 'gst';
|
||||
$gstPercent = $data['gst_percent'] ?? 0;
|
||||
$baseTotal = $data['autototal'];
|
||||
|
||||
$totalWithGst = $data['total_with_gst'] ?? $baseTotal;
|
||||
if ($totalWithGst == 0 && $gstPercent > 0) {
|
||||
$gstAmount = ($baseTotal * $gstPercent) / 100;
|
||||
$totalWithGst = $baseTotal + $gstAmount;
|
||||
}
|
||||
|
||||
// 1) Group create
|
||||
$group = InvoiceChargeGroup::create([
|
||||
'invoice_id' => $invoice->id,
|
||||
'group_name' => $data['groupname'],
|
||||
'basis_type' => $data['basistype'],
|
||||
'basis_value' => $data['basisvalue'],
|
||||
'rate' => $data['rate'],
|
||||
'total_charge' => $baseTotal,
|
||||
'tax_type' => $taxType,
|
||||
'gst_percent' => $gstPercent,
|
||||
'total_with_gst' => $totalWithGst,
|
||||
]);
|
||||
|
||||
// 2) Items link
|
||||
foreach ($data['itemids'] as $itemId) {
|
||||
InvoiceChargeGroupItem::create([
|
||||
'group_id' => $group->id,
|
||||
'invoice_item_id' => $itemId,
|
||||
]);
|
||||
}
|
||||
|
||||
// 3) सर्व groups वरून invoice level totals
|
||||
$invoice->load('chargeGroups');
|
||||
|
||||
$chargeGroupsBase = $invoice->chargeGroups->sum('total_charge'); // base
|
||||
$chargeGroupsWithG = $invoice->chargeGroups->sum('total_with_gst'); // base + gst
|
||||
$chargeGroupsGst = $chargeGroupsWithG - $chargeGroupsBase; // gst only
|
||||
|
||||
$invoiceGstPercent = $group->gst_percent ?? 0;
|
||||
$invoiceTaxType = $group->tax_type ?? 'gst';
|
||||
|
||||
$cgstPercent = 0;
|
||||
$sgstPercent = 0;
|
||||
$igstPercent = 0;
|
||||
|
||||
if ($invoiceTaxType === 'gst') {
|
||||
$cgstPercent = $invoiceGstPercent / 2;
|
||||
$sgstPercent = $invoiceGstPercent / 2;
|
||||
} elseif ($invoiceTaxType === 'igst') {
|
||||
$igstPercent = $invoiceGstPercent;
|
||||
}
|
||||
|
||||
// 🔴 इथे main fix:
|
||||
// final_amount = base (total_charge sum)
|
||||
// final_amount_with_gst = base + gst (total_with_gst sum)
|
||||
// grand_total_with_charges = final_amount_with_gst (same)
|
||||
$invoice->update([
|
||||
'charge_groups_total' => $chargeGroupsBase,
|
||||
'gst_amount' => $chargeGroupsGst,
|
||||
'gst_percent' => $invoiceGstPercent,
|
||||
'tax_type' => $invoiceTaxType,
|
||||
'cgst_percent' => $cgstPercent,
|
||||
'sgst_percent' => $sgstPercent,
|
||||
'igst_percent' => $igstPercent,
|
||||
|
||||
'final_amount' => $chargeGroupsBase,
|
||||
'final_amount_with_gst' => $chargeGroupsWithG,
|
||||
'grand_total_with_charges' => $chargeGroupsWithG,
|
||||
]);
|
||||
|
||||
Log::info('✅ Invoice updated from Charge Group (CG total + GST)', [
|
||||
'invoice_id' => $invoice->id,
|
||||
'charge_groups_total' => $chargeGroupsBase,
|
||||
'gst_amount' => $chargeGroupsGst,
|
||||
'gst_percent' => $invoiceGstPercent,
|
||||
'tax_type' => $invoiceTaxType,
|
||||
'cgst_percent' => $cgstPercent,
|
||||
'sgst_percent' => $sgstPercent,
|
||||
'igst_percent' => $igstPercent,
|
||||
'final_amount' => $invoice->final_amount,
|
||||
'final_amount_with_gst' => $invoice->final_amount_with_gst,
|
||||
'grand_total_with_charges'=> $invoice->grand_total_with_charges,
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'Charge group saved successfully.',
|
||||
'group_id' => $group->id,
|
||||
'status' => 'success',
|
||||
'message' => 'Installment deleted.',
|
||||
'totalPaid' => $paidTotal,
|
||||
'gstAmount' => $invoice->gst_amount,
|
||||
'finalAmountWithGst' => $invoice->final_amount_with_gst,
|
||||
'baseAmount' => $invoice->final_amount,
|
||||
'remaining' => $remaining,
|
||||
'isZero' => $paidTotal == 0,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -10,118 +10,26 @@ use App\Models\MarkList;
|
||||
use App\Models\Invoice;
|
||||
use App\Models\InvoiceItem;
|
||||
use App\Models\User;
|
||||
use App\Models\Container;
|
||||
use App\Models\Admin;
|
||||
use App\Models\Shipment;
|
||||
use PDF;
|
||||
use Maatwebsite\Excel\Facades\Excel;
|
||||
use App\Exports\OrdersExport;
|
||||
use App\Imports\OrderItemsPreviewImport;
|
||||
|
||||
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use App\Exports\InvoicesExport;
|
||||
|
||||
|
||||
class AdminOrderController extends Controller
|
||||
{
|
||||
/* ---------------------------
|
||||
* DASHBOARD (old UI: stats + recent orders)
|
||||
* LIST / DASHBOARD
|
||||
* ---------------------------*/
|
||||
public function dashboard()
|
||||
public function index()
|
||||
{
|
||||
// ── Order counts (from Invoice Management / Orders table) ──
|
||||
$totalOrders = Order::count();
|
||||
|
||||
// "Pending" म्हणजे delivered नसलेले सर्व orders
|
||||
// Order तयार होतो तेव्हा status = 'order_placed' असतो, 'pending' नाही
|
||||
$deliveredStatuses = ['delivered'];
|
||||
$pendingOrders = Order::whereNotIn('status', $deliveredStatuses)->count();
|
||||
|
||||
// ── Invoice counts ──
|
||||
$totalContainers = Container::count();
|
||||
$totalInvoices = Invoice::count();
|
||||
$paidInvoices = Invoice::where('status', 'paid')->count();
|
||||
$pendingInvoices = Invoice::where('status', 'pending')->count();
|
||||
$overdueInvoices = Invoice::where('status', 'overdue')->count();
|
||||
$totalRevenue = Invoice::sum('final_amount_with_gst');
|
||||
|
||||
// ── User / Staff counts ──
|
||||
$activeCustomers = User::where('status', 'active')->count();
|
||||
$inactiveCustomers = User::where('status', 'inactive')->count();
|
||||
$totalStaff = Admin::where('type', 'staff')->count();
|
||||
|
||||
$markList = MarkList::where('status', 'active')->get();
|
||||
$orders = Order::latest()->get();
|
||||
$markList = MarkList::where('status', 'active')->get();
|
||||
|
||||
return view('admin.dashboard', compact(
|
||||
'totalOrders',
|
||||
'pendingOrders',
|
||||
'totalContainers',
|
||||
'totalInvoices',
|
||||
'paidInvoices',
|
||||
'pendingInvoices',
|
||||
'overdueInvoices',
|
||||
'totalRevenue',
|
||||
'activeCustomers',
|
||||
'inactiveCustomers',
|
||||
'totalStaff',
|
||||
'orders',
|
||||
'markList'
|
||||
));
|
||||
}
|
||||
|
||||
/* ---------------------------
|
||||
* LIST (new: Invoices Management for Orders page)
|
||||
* ---------------------------*/
|
||||
public function index(Request $request)
|
||||
{
|
||||
$invoices = DB::table('invoices')
|
||||
->leftJoin('containers', 'containers.id', '=', 'invoices.container_id')
|
||||
->leftJoin('mark_list', 'mark_list.mark_no', '=', 'invoices.mark_no')
|
||||
->select(
|
||||
'invoices.id',
|
||||
'invoices.invoice_number',
|
||||
'invoices.invoice_date',
|
||||
'invoices.final_amount',
|
||||
'invoices.final_amount_with_gst',
|
||||
'invoices.status as invoice_status',
|
||||
'invoices.mark_no',
|
||||
'invoices.container_id', // <<< हे नक्की घाल
|
||||
'containers.container_number',
|
||||
'containers.container_date',
|
||||
DB::raw('COALESCE(invoices.company_name, mark_list.company_name) as company_name'),
|
||||
DB::raw('COALESCE(invoices.customer_name, mark_list.customer_name) as customer_name')
|
||||
)
|
||||
|
||||
->when($request->filled('search'), function ($q) use ($request) {
|
||||
$search = trim($request->search);
|
||||
$q->where(function ($qq) use ($search) {
|
||||
$qq->where('invoices.invoice_number', 'like', "%{$search}%")
|
||||
->orWhere('containers.container_number', 'like', "%{$search}%")
|
||||
->orWhere('invoices.mark_no', 'like', "%{$search}%")
|
||||
->orWhere('mark_list.company_name', 'like', "%{$search}%")
|
||||
->orWhere('mark_list.customer_name', 'like', "%{$search}%");
|
||||
});
|
||||
})
|
||||
->when($request->filled('status'), function ($q) use ($request) {
|
||||
$q->where('invoices.status', $request->status);
|
||||
})
|
||||
->orderByDesc('invoices.invoice_date')
|
||||
->orderByDesc('invoices.id')
|
||||
->get();
|
||||
|
||||
// ── Real DB counts (filter-independent) ──
|
||||
$totalInvoices = \App\Models\Invoice::count();
|
||||
$paidInvoices = \App\Models\Invoice::where('status', 'paid')->count();
|
||||
$pendingInvoices = \App\Models\Invoice::where('status', 'pending')->count();
|
||||
$overdueInvoices = \App\Models\Invoice::where('status', 'overdue')->count();
|
||||
|
||||
return view('admin.orders', compact(
|
||||
'invoices',
|
||||
'totalInvoices',
|
||||
'paidInvoices',
|
||||
'pendingInvoices',
|
||||
'overdueInvoices'
|
||||
));
|
||||
return view('admin.dashboard', compact('orders', 'markList'));
|
||||
}
|
||||
|
||||
/* ---------------------------
|
||||
@@ -133,9 +41,52 @@ class AdminOrderController extends Controller
|
||||
return view('admin.orders_create', compact('markList'));
|
||||
}
|
||||
|
||||
/* ---------------------------
|
||||
* SHOW / POPUP
|
||||
* ---------------------------*/
|
||||
/**
|
||||
* Store a new order and optionally create initial invoice
|
||||
*/
|
||||
public function store(Request $request)
|
||||
{
|
||||
$data = $request->validate([
|
||||
'mark_no' => 'required|string',
|
||||
'origin' => 'nullable|string',
|
||||
'destination' => 'nullable|string',
|
||||
// totals optional when creating without items
|
||||
'ctn' => 'nullable|numeric',
|
||||
'qty' => 'nullable|numeric',
|
||||
'ttl_qty' => 'nullable|numeric',
|
||||
'ttl_amount' => 'nullable|numeric',
|
||||
'cbm' => 'nullable|numeric',
|
||||
'ttl_cbm' => 'nullable|numeric',
|
||||
'kg' => 'nullable|numeric',
|
||||
'ttl_kg' => 'nullable|numeric',
|
||||
]);
|
||||
|
||||
$order = Order::create([
|
||||
'order_id' => $this->generateOrderId(),
|
||||
'mark_no' => $data['mark_no'],
|
||||
'origin' => $data['origin'] ?? null,
|
||||
'destination' => $data['destination'] ?? null,
|
||||
'ctn' => $data['ctn'] ?? 0,
|
||||
'qty' => $data['qty'] ?? 0,
|
||||
'ttl_qty' => $data['ttl_qty'] ?? 0,
|
||||
'ttl_amount' => $data['ttl_amount'] ?? 0,
|
||||
'cbm' => $data['cbm'] ?? 0,
|
||||
'ttl_cbm' => $data['ttl_cbm'] ?? 0,
|
||||
'kg' => $data['kg'] ?? 0,
|
||||
'ttl_kg' => $data['ttl_kg'] ?? 0,
|
||||
'status' => 'pending',
|
||||
]);
|
||||
|
||||
//If you want to auto-create an invoice at order creation, uncomment:
|
||||
// $this->createInvoice($order);
|
||||
|
||||
return redirect()->route('admin.orders.show', $order->id)
|
||||
->with('success', 'Order created successfully.');
|
||||
}
|
||||
|
||||
// ---------------------------
|
||||
// SHOW / POPUP
|
||||
// ---------------------------
|
||||
public function show($id)
|
||||
{
|
||||
$order = Order::with('items', 'markList')->findOrFail($id);
|
||||
@@ -153,46 +104,57 @@ class AdminOrderController extends Controller
|
||||
$user = User::where('customer_id', $order->markList->customer_id)->first();
|
||||
}
|
||||
|
||||
return view('admin.popup', compact('order', 'user'));
|
||||
$data['order_id'] = $order->id;
|
||||
|
||||
OrderItem::create($data);
|
||||
|
||||
// recalc totals and save to order
|
||||
$this->recalcTotals($order);
|
||||
// $this->updateInvoiceFromOrder($order); // <-- NEW
|
||||
|
||||
return redirect()->back()->with('success', 'Item added and totals updated.');
|
||||
}
|
||||
|
||||
/* ---------------------------
|
||||
* ORDER ITEM MANAGEMENT (existing orders)
|
||||
* ---------------------------*/
|
||||
public function addItem(Request $request, $orderId)
|
||||
{
|
||||
$order = Order::findOrFail($orderId);
|
||||
{
|
||||
$order = Order::findOrFail($orderId);
|
||||
|
||||
$data = $request->validate([
|
||||
'description' => 'required|string',
|
||||
'ctn' => 'nullable|numeric',
|
||||
'qty' => 'nullable|numeric',
|
||||
'unit' => 'nullable|string',
|
||||
'price' => 'nullable|numeric',
|
||||
'cbm' => 'nullable|numeric',
|
||||
'kg' => 'nullable|numeric',
|
||||
'shop_no' => 'nullable|string',
|
||||
]);
|
||||
$data = $request->validate([
|
||||
'description' => 'required|string',
|
||||
'ctn' => 'nullable|numeric',
|
||||
'qty' => 'nullable|numeric',
|
||||
'unit' => 'nullable|string',
|
||||
'price' => 'nullable|numeric',
|
||||
'cbm' => 'nullable|numeric',
|
||||
'kg' => 'nullable|numeric',
|
||||
'shop_no' => 'nullable|string',
|
||||
]);
|
||||
|
||||
$ctn = (float) ($data['ctn'] ?? 0);
|
||||
$qty = (float) ($data['qty'] ?? 0);
|
||||
$price = (float) ($data['price'] ?? 0);
|
||||
$cbm = (float) ($data['cbm'] ?? 0);
|
||||
$kg = (float) ($data['kg'] ?? 0);
|
||||
// ✅ BACKEND CALCULATION
|
||||
$ctn = (float) ($data['ctn'] ?? 0);
|
||||
$qty = (float) ($data['qty'] ?? 0);
|
||||
$price = (float) ($data['price'] ?? 0);
|
||||
$cbm = (float) ($data['cbm'] ?? 0);
|
||||
$kg = (float) ($data['kg'] ?? 0);
|
||||
|
||||
$data['ttl_qty'] = $ctn * $qty;
|
||||
$data['ttl_amount'] = $data['ttl_qty'] * $price;
|
||||
$data['ttl_cbm'] = $cbm * $ctn;
|
||||
$data['ttl_kg'] = $ctn * $kg;
|
||||
$data['ttl_qty'] = $ctn * $qty;
|
||||
$data['ttl_amount'] = $data['ttl_qty'] * $price;
|
||||
$data['ttl_cbm'] = $cbm * $ctn;
|
||||
$data['ttl_kg'] = $ctn * $kg;
|
||||
|
||||
$data['order_id'] = $order->id;
|
||||
$data['order_id'] = $order->id;
|
||||
|
||||
OrderItem::create($data);
|
||||
OrderItem::create($data);
|
||||
|
||||
$this->recalcTotals($order);
|
||||
$this->recalcTotals($order);
|
||||
$this->updateInvoiceFromOrder($order);
|
||||
|
||||
return redirect()->back()->with('success', 'Item added and totals updated.');
|
||||
}
|
||||
|
||||
return redirect()->back()->with('success', 'Item added and totals updated.');
|
||||
}
|
||||
|
||||
public function deleteItem($id)
|
||||
{
|
||||
@@ -202,6 +164,7 @@ class AdminOrderController extends Controller
|
||||
$item->delete();
|
||||
|
||||
$this->recalcTotals($order);
|
||||
$this->updateInvoiceFromOrder($order);
|
||||
|
||||
return redirect()->back()->with('success', 'Item deleted and totals updated.');
|
||||
}
|
||||
@@ -214,6 +177,7 @@ class AdminOrderController extends Controller
|
||||
$item->restore();
|
||||
|
||||
$this->recalcTotals($order);
|
||||
$this->updateInvoiceFromOrder($order);
|
||||
|
||||
return redirect()->back()->with('success', 'Item restored and totals updated.');
|
||||
}
|
||||
@@ -262,14 +226,14 @@ class AdminOrderController extends Controller
|
||||
$items = $order->items()->get();
|
||||
|
||||
$order->update([
|
||||
'ctn' => (int) $items->sum(fn($i) => (int) ($i->ctn ?? 0)),
|
||||
'qty' => (int) $items->sum(fn($i) => (int) ($i->qty ?? 0)),
|
||||
'ttl_qty' => (int) $items->sum(fn($i) => (int) ($i->ttl_qty ?? 0)),
|
||||
'ttl_amount'=> (float) $items->sum(fn($i) => (float) ($i->ttl_amount ?? 0)),
|
||||
'cbm' => (float) $items->sum(fn($i) => (float) ($i->cbm ?? 0)),
|
||||
'ttl_cbm' => (float) $items->sum(fn($i) => (float) ($i->ttl_cbm ?? 0)),
|
||||
'kg' => (float) $items->sum(fn($i) => (float) ($i->kg ?? 0)),
|
||||
'ttl_kg' => (float) $items->sum(fn($i) => (float) ($i->ttl_kg ?? 0)),
|
||||
'ctn' => (int) $items->sum(fn($i) => (int) ($i->ctn ?? 0)),
|
||||
'qty' => (int) $items->sum(fn($i) => (int) ($i->qty ?? 0)),
|
||||
'ttl_qty' => (int) $items->sum(fn($i) => (int) ($i->ttl_qty ?? 0)),
|
||||
'ttl_amount' => (float) $items->sum(fn($i) => (float) ($i->ttl_amount ?? 0)),
|
||||
'cbm' => (float) $items->sum(fn($i) => (float) ($i->cbm ?? 0)),
|
||||
'ttl_cbm' => (float) $items->sum(fn($i) => (float) ($i->ttl_cbm ?? 0)),
|
||||
'kg' => (float) $items->sum(fn($i) => (float) ($i->kg ?? 0)),
|
||||
'ttl_kg' => (float) $items->sum(fn($i) => (float) ($i->ttl_kg ?? 0)),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -372,6 +336,7 @@ class AdminOrderController extends Controller
|
||||
$order = Order::with([
|
||||
'markList',
|
||||
'items',
|
||||
'invoice.items',
|
||||
'shipments' => function ($q) use ($id) {
|
||||
$q->whereHas('orders', function ($oq) use ($id) {
|
||||
$oq->where('orders.id', $id)
|
||||
@@ -390,14 +355,14 @@ class AdminOrderController extends Controller
|
||||
'order_id' => $order->order_id,
|
||||
'status' => $order->status,
|
||||
'totals' => [
|
||||
'ctn' => $order->ctn,
|
||||
'qty' => $order->qty,
|
||||
'ttl_qty' => $order->ttl_qty,
|
||||
'cbm' => $order->cbm,
|
||||
'ttl_cbm' => $order->ttl_cbm,
|
||||
'kg' => $order->kg,
|
||||
'ttl_kg' => $order->ttl_kg,
|
||||
'amount' => $order->ttl_amount,
|
||||
'ctn' => $order->ctn,
|
||||
'qty' => $order->qty,
|
||||
'ttl_qty' => $order->ttl_qty,
|
||||
'cbm' => $order->cbm,
|
||||
'ttl_cbm' => $order->ttl_cbm,
|
||||
'kg' => $order->kg,
|
||||
'ttl_kg' => $order->ttl_kg,
|
||||
'amount' => $order->ttl_amount,
|
||||
],
|
||||
'items' => $order->items,
|
||||
];
|
||||
@@ -451,6 +416,29 @@ class AdminOrderController extends Controller
|
||||
}
|
||||
|
||||
$invoiceData = null;
|
||||
if ($order->invoice) {
|
||||
$invoice = $order->invoice;
|
||||
$invoiceData = [
|
||||
'invoice_no' => $invoice->invoice_number,
|
||||
'status' => $invoice->status,
|
||||
'invoice_date' => $invoice->invoice_date,
|
||||
'due_date' => $invoice->due_date,
|
||||
'customer' => [
|
||||
'name' => $invoice->customer_name,
|
||||
'mobile' => $invoice->customer_mobile,
|
||||
'email' => $invoice->customer_email,
|
||||
'address' => $invoice->customer_address,
|
||||
'pincode' => $invoice->pincode,
|
||||
],
|
||||
'items' => $invoice->items,
|
||||
'summary' => [
|
||||
'amount' => $invoice->final_amount,
|
||||
'cgst' => 0,
|
||||
'sgst' => 0,
|
||||
'total' => $invoice->final_amount_with_gst,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
return view('admin.see_order', compact(
|
||||
'order',
|
||||
@@ -461,13 +449,14 @@ class AdminOrderController extends Controller
|
||||
}
|
||||
|
||||
/* ---------------------------
|
||||
* FILTERED LIST + EXPORTS (old orders listing)
|
||||
* FILTERED LIST + EXPORTS
|
||||
* ---------------------------*/
|
||||
public function orderShow()
|
||||
{
|
||||
$orders = Order::with([
|
||||
'markList',
|
||||
'shipments',
|
||||
'invoice'
|
||||
])->latest('id')->get();
|
||||
|
||||
return view('admin.orders', compact('orders'));
|
||||
@@ -476,7 +465,7 @@ class AdminOrderController extends Controller
|
||||
private function buildOrdersQueryFromRequest(Request $request)
|
||||
{
|
||||
$query = Order::query()
|
||||
->with(['markList', 'shipments']);
|
||||
->with(['markList', 'invoice', 'shipments']);
|
||||
|
||||
if ($request->filled('search')) {
|
||||
$search = trim($request->search);
|
||||
@@ -489,12 +478,23 @@ class AdminOrderController extends Controller
|
||||
->orWhere('origin', 'like', "%{$search}%")
|
||||
->orWhere('destination', 'like', "%{$search}%");
|
||||
})
|
||||
->orWhereHas('invoice', function ($q3) use ($search) {
|
||||
$q3->where('invoice_number', 'like', "%{$search}%");
|
||||
})
|
||||
->orWhereHas('shipments', function ($q4) use ($search) {
|
||||
$q4->where('shipments.shipment_id', 'like', "%{$search}%");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if ($request->filled('status')) {
|
||||
$query->where(function ($q) use ($request) {
|
||||
$q->whereHas('invoice', function ($q2) use ($request) {
|
||||
$q2->where('status', $request->status);
|
||||
})->orWhereDoesntHave('invoice');
|
||||
});
|
||||
}
|
||||
|
||||
if ($request->filled('shipment')) {
|
||||
$query->where(function ($q) use ($request) {
|
||||
$q->whereHas('shipments', function ($q2) use ($request) {
|
||||
@@ -516,83 +516,62 @@ class AdminOrderController extends Controller
|
||||
|
||||
public function downloadPdf(Request $request)
|
||||
{
|
||||
$invoices = DB::table('invoices')
|
||||
->leftJoin('containers', 'containers.id', '=', 'invoices.container_id')
|
||||
->leftJoin('mark_list', 'mark_list.mark_no', '=', 'invoices.mark_no')
|
||||
->select(
|
||||
'invoices.invoice_number',
|
||||
'invoices.invoice_date',
|
||||
'invoices.mark_no',
|
||||
'containers.container_number',
|
||||
'containers.container_date',
|
||||
DB::raw('COALESCE(invoices.company_name, mark_list.company_name) as company_name'),
|
||||
DB::raw('COALESCE(invoices.customer_name, mark_list.customer_name) as customer_name'),
|
||||
'invoices.final_amount',
|
||||
'invoices.final_amount_with_gst',
|
||||
'invoices.status as invoice_status'
|
||||
)
|
||||
->when($request->filled('search'), function ($q) use ($request) {
|
||||
$search = trim($request->search);
|
||||
$q->where(function ($qq) use ($search) {
|
||||
$qq->where('invoices.invoice_number', 'like', "%{$search}%")
|
||||
->orWhere('invoices.mark_no', 'like', "%{$search}%")
|
||||
->orWhere('containers.container_number', 'like', "%{$search}%")
|
||||
->orWhere('mark_list.company_name', 'like', "%{$search}%")
|
||||
->orWhere('mark_list.customer_name', 'like', "%{$search}%");
|
||||
});
|
||||
})
|
||||
->when($request->filled('status'), function ($q) use ($request) {
|
||||
$q->where('invoices.status', $request->status);
|
||||
})
|
||||
->when($request->filled('from_date'), function ($q) use ($request) {
|
||||
$q->whereDate('invoices.invoice_date', '>=', $request->from_date);
|
||||
})
|
||||
->when($request->filled('to_date'), function ($q) use ($request) {
|
||||
$q->whereDate('invoices.invoice_date', '<=', $request->to_date);
|
||||
})
|
||||
->orderByDesc('containers.container_date')
|
||||
->orderByDesc('invoices.id')
|
||||
->get();
|
||||
$orders = $this->buildOrdersQueryFromRequest($request)->get();
|
||||
$filters = [
|
||||
'search' => $request->search,
|
||||
'status' => $request->status,
|
||||
'shipment' => $request->shipment,
|
||||
'from' => $request->from_date,
|
||||
'to' => $request->to_date,
|
||||
];
|
||||
|
||||
$pdf = PDF::loadView('admin.pdf.invoices_report', compact('invoices'))
|
||||
$pdf = PDF::loadView('admin.orders.pdf', compact('orders', 'filters'))
|
||||
->setPaper('a4', 'landscape');
|
||||
|
||||
return $pdf->download(
|
||||
'invoices-report-' . now()->format('Y-m-d') . '.pdf'
|
||||
'orders-report-' . now()->format('Y-m-d') . '.pdf'
|
||||
);
|
||||
}
|
||||
|
||||
public function downloadExcel(Request $request)
|
||||
{
|
||||
return Excel::download(
|
||||
new InvoicesExport($request),
|
||||
'invoices-report-' . now()->format('Y-m-d') . '.xlsx'
|
||||
new OrdersExport($request),
|
||||
'orders-report-' . now()->format('Y-m-d') . '.xlsx'
|
||||
);
|
||||
}
|
||||
|
||||
/* --------------------------------------------------
|
||||
* NEW: Create Order + Invoice directly from popup
|
||||
* route: admin.orders.temp.add (Create New Order form)
|
||||
* --------------------------------------------------*/
|
||||
public function addTempItem(Request $request)
|
||||
{
|
||||
// 1) order-level fields
|
||||
$request->validate([
|
||||
'mark_no' => 'required',
|
||||
'origin' => 'nullable',
|
||||
'destination' => 'nullable',
|
||||
'origin' => 'required',
|
||||
'destination' => 'required',
|
||||
]);
|
||||
|
||||
// 2) multi-row items
|
||||
$items = $request->validate([
|
||||
'items' => 'required|array',
|
||||
'items.*.description' => 'required|string',
|
||||
'items.*.ctn' => 'nullable|numeric',
|
||||
'items.*.qty' => 'nullable|numeric',
|
||||
|
||||
'items.*.unit' => 'nullable|string',
|
||||
'items.*.price' => 'nullable|numeric',
|
||||
|
||||
'items.*.cbm' => 'nullable|numeric',
|
||||
|
||||
'items.*.kg' => 'nullable|numeric',
|
||||
|
||||
'items.*.shop_no' => 'nullable|string',
|
||||
])['items'];
|
||||
|
||||
// रिकामे rows काढा
|
||||
$items = array_filter($items, function ($row) {
|
||||
return trim($row['description'] ?? '') !== '';
|
||||
});
|
||||
@@ -601,31 +580,38 @@ class AdminOrderController extends Controller
|
||||
return back()->with('error', 'Add at least one item.');
|
||||
}
|
||||
|
||||
// ✅ BACKEND CALCULATION (DO NOT TRUST FRONTEND)
|
||||
foreach ($items as &$item) {
|
||||
|
||||
$ctn = (float) ($item['ctn'] ?? 0);
|
||||
$qty = (float) ($item['qty'] ?? 0);
|
||||
$price = (float) ($item['price'] ?? 0);
|
||||
$cbm = (float) ($item['cbm'] ?? 0);
|
||||
$kg = (float) ($item['kg'] ?? 0);
|
||||
|
||||
// Calculated fields
|
||||
$item['ttl_qty'] = $ctn * $qty;
|
||||
$item['ttl_amount'] = $item['ttl_qty'] * $price;
|
||||
$item['ttl_cbm'] = $cbm * $ctn;
|
||||
$item['ttl_kg'] = $ctn * $kg;
|
||||
}
|
||||
unset($item);
|
||||
unset($item); // VERY IMPORTANT
|
||||
|
||||
$total_ctn = array_sum(array_column($items, 'ctn'));
|
||||
$total_qty = array_sum(array_column($items, 'qty'));
|
||||
$total_ttl_qty = array_sum(array_column($items, 'ttl_qty'));
|
||||
$total_amount = array_sum(array_column($items, 'ttl_amount'));
|
||||
$total_cbm = array_sum(array_column($items, 'cbm'));
|
||||
$total_ttl_cbm = array_sum(array_column($items, 'ttl_cbm'));
|
||||
$total_kg = array_sum(array_column($items, 'kg'));
|
||||
$total_ttl_kg = array_sum(array_column($items, 'ttl_kg'));
|
||||
|
||||
// 3) totals
|
||||
$total_ctn = array_sum(array_column($items, 'ctn'));
|
||||
$total_qty = array_sum(array_column($items, 'qty'));
|
||||
$total_ttl_qty = array_sum(array_column($items, 'ttl_qty'));
|
||||
$total_amount = array_sum(array_column($items, 'ttl_amount'));
|
||||
$total_cbm = array_sum(array_column($items, 'cbm'));
|
||||
$total_ttl_cbm = array_sum(array_column($items, 'ttl_cbm'));
|
||||
$total_kg = array_sum(array_column($items, 'kg'));
|
||||
$total_ttl_kg = array_sum(array_column($items, 'ttl_kg'));
|
||||
|
||||
// 4) order id generate
|
||||
$orderId = $this->generateOrderId();
|
||||
|
||||
// 5) order create
|
||||
$order = Order::create([
|
||||
'order_id' => $orderId,
|
||||
'mark_no' => $request->mark_no,
|
||||
@@ -642,6 +628,7 @@ class AdminOrderController extends Controller
|
||||
'status' => 'order_placed',
|
||||
]);
|
||||
|
||||
// 6) order items
|
||||
foreach ($items as $item) {
|
||||
OrderItem::create([
|
||||
'order_id' => $order->id,
|
||||
@@ -660,37 +647,149 @@ class AdminOrderController extends Controller
|
||||
]);
|
||||
}
|
||||
|
||||
// 7) invoice number
|
||||
$invoiceNumber = $this->generateInvoiceNumber();
|
||||
|
||||
$markList = MarkList::where('mark_no', $order->mark_no)->first();
|
||||
$customer = null;
|
||||
if ($markList && $markList->customer_id) {
|
||||
$customer = User::where('customer_id', $markList->customer_id)->first();
|
||||
// 1. Auto-generate invoice number
|
||||
// $lastInvoice = \App\Models\Invoice::latest()->first();
|
||||
// $nextInvoice = $lastInvoice ? $lastInvoice->id + 1 : 1;
|
||||
// $invoiceNumber = 'INV-' . date('Y') . '-' . str_pad($nextInvoice, 6, '0', STR_PAD_LEFT);
|
||||
|
||||
// 2. Fetch customer (using mark list → customer_id)
|
||||
// $markList = MarkList::where('mark_no', $order->mark_no)->first();
|
||||
// $customer = null;
|
||||
|
||||
// if ($markList && $markList->customer_id) {
|
||||
// $customer = \App\Models\User::where('customer_id', $markList->customer_id)->first();
|
||||
// }
|
||||
|
||||
// 3. Create Invoice Record
|
||||
// $invoice = \App\Models\Invoice::create([
|
||||
// 'order_id' => $order->id,
|
||||
// 'customer_id' => $customer->id ?? null,
|
||||
// 'mark_no' => $order->mark_no,
|
||||
|
||||
// 'invoice_number' => $invoiceNumber,
|
||||
// 'invoice_date' => now(),
|
||||
// 'due_date' => now()->addDays(10),
|
||||
|
||||
// 'payment_method' => null,
|
||||
// 'reference_no' => null,
|
||||
// 'status' => 'pending',
|
||||
|
||||
// 'final_amount' => $total_amount,
|
||||
// 'gst_percent' => 0,
|
||||
// 'gst_amount' => 0,
|
||||
// 'final_amount_with_gst' => $total_amount,
|
||||
|
||||
// // snapshot customer fields
|
||||
// 'customer_name' => $customer->customer_name ?? null,
|
||||
// 'company_name' => $customer->company_name ?? null,
|
||||
// 'customer_email' => $customer->email ?? null,
|
||||
// 'customer_mobile' => $customer->mobile_no ?? null,
|
||||
// 'customer_address' => $customer->address ?? null,
|
||||
// 'pincode' => $customer->pincode ?? null,
|
||||
|
||||
// 'notes' => null,
|
||||
// ]);
|
||||
|
||||
// 4. Clone order items into invoice_items
|
||||
// foreach ($order->items as $item) {
|
||||
// \App\Models\InvoiceItem::create([
|
||||
// 'invoice_id' => $invoice->id,
|
||||
// 'description' => $item->description,
|
||||
// 'ctn' => $item->ctn,
|
||||
// 'qty' => $item->qty,
|
||||
// 'ttl_qty' => $item->ttl_qty,
|
||||
// 'unit' => $item->unit,
|
||||
// 'price' => $item->price,
|
||||
// 'ttl_amount' => $item->ttl_amount,
|
||||
// 'cbm' => $item->cbm,
|
||||
// 'ttl_cbm' => $item->ttl_cbm,
|
||||
// 'kg' => $item->kg,
|
||||
// 'ttl_kg' => $item->ttl_kg,
|
||||
// 'shop_no' => $item->shop_no,
|
||||
// ]);
|
||||
// }
|
||||
|
||||
// 5. TODO: PDF generation (I will add this later)
|
||||
// $invoice->pdf_path = null; // placeholder for now
|
||||
// $invoice->save();
|
||||
|
||||
// =======================
|
||||
// END INVOICE CREATION
|
||||
// =======================
|
||||
|
||||
// CLEAR TEMP DATA
|
||||
session()->forget(['temp_order_items', 'mark_no', 'origin', 'destination']);
|
||||
|
||||
return redirect()->route('admin.orders.index')
|
||||
->with('success', 'Order + Invoice created successfully.');
|
||||
}
|
||||
|
||||
/* ---------------------------
|
||||
* UPDATE ORDER ITEM (existing orders)
|
||||
* ---------------------------*/
|
||||
public function updateItem(Request $request, $id)
|
||||
{
|
||||
$item = OrderItem::findOrFail($id);
|
||||
$order = $item->order;
|
||||
|
||||
$request->validate([
|
||||
'description' => 'required|string',
|
||||
'ctn' => 'nullable|numeric',
|
||||
'qty' => 'nullable|numeric',
|
||||
'unit' => 'nullable|string',
|
||||
'price' => 'nullable|numeric',
|
||||
'cbm' => 'nullable|numeric',
|
||||
'kg' => 'nullable|numeric',
|
||||
'shop_no' => 'nullable|string',
|
||||
]);
|
||||
|
||||
// ✅ BACKEND CALCULATION
|
||||
$ctn = (float) ($request->ctn ?? 0);
|
||||
$qty = (float) ($request->qty ?? 0);
|
||||
$price = (float) ($request->price ?? 0);
|
||||
$cbm = (float) ($request->cbm ?? 0);
|
||||
$kg = (float) ($request->kg ?? 0);
|
||||
|
||||
$item->update([
|
||||
'description' => $request->description,
|
||||
'ctn' => $ctn,
|
||||
'qty' => $qty,
|
||||
'ttl_qty' => $ctn * $qty,
|
||||
'unit' => $request->unit,
|
||||
'price' => $price,
|
||||
'ttl_amount' => ($ctn * $qty) * $price,
|
||||
'cbm' => $cbm,
|
||||
'ttl_cbm' => $cbm * $ctn,
|
||||
'kg' => $kg,
|
||||
'ttl_kg' => $ctn * $kg,
|
||||
'shop_no' => $request->shop_no,
|
||||
]);
|
||||
|
||||
$this->recalcTotals($order);
|
||||
$this->updateInvoiceFromOrder($order);
|
||||
|
||||
return back()->with('success', 'Item updated successfully');
|
||||
}
|
||||
|
||||
|
||||
private function updateInvoiceFromOrder(Order $order)
|
||||
{
|
||||
$invoice = Invoice::where('order_id', $order->id)->first();
|
||||
|
||||
if (!$invoice) {
|
||||
return;
|
||||
}
|
||||
|
||||
$invoice = Invoice::create([
|
||||
'order_id' => $order->id,
|
||||
'customer_id' => $customer->id ?? null,
|
||||
'mark_no' => $order->mark_no,
|
||||
'invoice_number' => $invoiceNumber,
|
||||
'invoice_date' => now(),
|
||||
'due_date' => now()->addDays(10),
|
||||
'payment_method' => null,
|
||||
'reference_no' => null,
|
||||
'status' => 'pending',
|
||||
'final_amount' => $total_amount,
|
||||
'gst_percent' => 0,
|
||||
'gst_amount' => 0,
|
||||
'final_amount_with_gst' => $total_amount,
|
||||
'customer_name' => $customer->customer_name ?? null,
|
||||
'company_name' => $customer->company_name ?? null,
|
||||
'customer_email' => $customer->email ?? null,
|
||||
'customer_mobile' => $customer->mobile_no ?? null,
|
||||
'customer_address' => $customer->address ?? null,
|
||||
'pincode' => $customer->pincode ?? null,
|
||||
'notes' => null,
|
||||
'pdf_path' => null,
|
||||
]);
|
||||
$invoice->final_amount = $order->ttl_amount;
|
||||
$invoice->gst_percent = 0;
|
||||
$invoice->gst_amount = 0;
|
||||
$invoice->final_amount_with_gst = $order->ttl_amount;
|
||||
$invoice->save();
|
||||
|
||||
InvoiceItem::where('invoice_id', $invoice->id)->delete();
|
||||
|
||||
foreach ($order->items as $item) {
|
||||
InvoiceItem::create([
|
||||
@@ -709,81 +808,37 @@ class AdminOrderController extends Controller
|
||||
'shop_no' => $item->shop_no,
|
||||
]);
|
||||
}
|
||||
|
||||
return redirect()->route('admin.orders.index')
|
||||
->with('success', 'Order + Invoice created successfully.');
|
||||
}
|
||||
|
||||
/* ---------------------------
|
||||
* UPDATE ORDER ITEM (existing orders)
|
||||
* ---------------------------*/
|
||||
public function updateItem(Request $request, $id)
|
||||
{
|
||||
$item = OrderItem::findOrFail($id);
|
||||
$order = $item->order;
|
||||
|
||||
|
||||
public function uploadExcelPreview(Request $request)
|
||||
{
|
||||
try {
|
||||
$request->validate([
|
||||
'description' => 'required|string',
|
||||
'ctn' => 'nullable|numeric',
|
||||
'qty' => 'nullable|numeric',
|
||||
'unit' => 'nullable|string',
|
||||
'price' => 'nullable|numeric',
|
||||
'cbm' => 'nullable|numeric',
|
||||
'kg' => 'nullable|numeric',
|
||||
'shop_no' => 'nullable|string',
|
||||
'excel' => 'required|file|mimes:xlsx,xls'
|
||||
]);
|
||||
|
||||
$ctn = (float) ($request->ctn ?? 0);
|
||||
$qty = (float) ($request->qty ?? 0);
|
||||
$price = (float) ($request->price ?? 0);
|
||||
$cbm = (float) ($request->cbm ?? 0);
|
||||
$kg = (float) ($request->kg ?? 0);
|
||||
$import = new OrderItemsPreviewImport();
|
||||
Excel::import($import, $request->file('excel'));
|
||||
|
||||
$item->update([
|
||||
'description' => $request->description,
|
||||
'ctn' => $ctn,
|
||||
'qty' => $qty,
|
||||
'ttl_qty' => $ctn * $qty,
|
||||
'unit' => $request->unit,
|
||||
'price' => $price,
|
||||
'ttl_amount' => ($ctn * $qty) * $price,
|
||||
'cbm' => $cbm,
|
||||
'ttl_cbm' => $cbm * $ctn,
|
||||
'kg' => $kg,
|
||||
'ttl_kg' => $ctn * $kg,
|
||||
'shop_no' => $request->shop_no,
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'items' => $import->rows
|
||||
]);
|
||||
|
||||
$this->recalcTotals($order);
|
||||
|
||||
return back()->with('success', 'Item updated successfully');
|
||||
}
|
||||
|
||||
public function uploadExcelPreview(Request $request)
|
||||
{
|
||||
try {
|
||||
$request->validate([
|
||||
'excel' => 'required|file|mimes:xlsx,xls'
|
||||
]);
|
||||
|
||||
$import = new OrderItemsPreviewImport();
|
||||
Excel::import($import, $request->file('excel'));
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'items' => $import->rows
|
||||
]);
|
||||
} catch (ValidationException $e) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Invalid Excel file format'
|
||||
], 422);
|
||||
} catch (\Throwable $e) {
|
||||
\Log::error($e);
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Server error'
|
||||
], 500);
|
||||
}
|
||||
} catch (ValidationException $e) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Invalid Excel file format'
|
||||
], 422);
|
||||
} catch (\Throwable $e) {
|
||||
\Log::error($e);
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Server error'
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Order;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
@@ -11,98 +12,45 @@ class AdminReportController extends Controller
|
||||
/**
|
||||
* Display the reports page with joined data
|
||||
*/
|
||||
// public function index(Request $request)
|
||||
// {
|
||||
/*********************************************************
|
||||
* OLD FLOW (Order + Shipment + Invoice)
|
||||
* फक्त reference साठी ठेवलेला, वापरत नाही.
|
||||
*********************************************************/
|
||||
|
||||
/*
|
||||
public function index(Request $request)
|
||||
{
|
||||
// -------------------------------
|
||||
// FETCH REPORT DATA
|
||||
// ONLY orders that have BOTH:
|
||||
// 1. Invoice
|
||||
// 2. Shipment
|
||||
// -------------------------------
|
||||
$reports = DB::table('orders')
|
||||
->join('shipment_items', 'shipment_items.order_id', '=', 'orders.id')
|
||||
->join('shipments', 'shipments.id', '=', 'shipment_items.shipment_id')
|
||||
->join('invoices', 'invoices.order_id', '=', 'orders.id')
|
||||
->leftJoin('mark_list', 'mark_list.mark_no', '=', 'orders.mark_no')
|
||||
->leftJoin('users', 'users.customer_id', '=', 'mark_list.customer_id')
|
||||
->select(...)
|
||||
->orderBy('shipments.shipment_date', 'desc')
|
||||
->get();
|
||||
*/
|
||||
|
||||
/*********************************************************
|
||||
* NEW FLOW (Container + Invoice + MarkList)
|
||||
*********************************************************/
|
||||
|
||||
// $reports = DB::table('invoices')
|
||||
// ->join('containers', 'containers.id', '=', 'invoices.containerid')
|
||||
// ->leftJoin('mark_list', 'mark_list.markno', '=', 'invoices.markno')
|
||||
// ->select(
|
||||
// 'invoices.id as invoicepk',
|
||||
// 'invoices.invoicenumber',
|
||||
// 'invoices.invoicedate',
|
||||
// 'invoices.finalamount',
|
||||
// 'invoices.finalamountwithgst',
|
||||
// 'invoices.gstpercent',
|
||||
// 'invoices.gstamount',
|
||||
// 'invoices.status as invoicestatus',
|
||||
// 'invoices.markno',
|
||||
|
||||
// 'containers.id as containerpk',
|
||||
// 'containers.containernumber',
|
||||
// 'containers.containerdate',
|
||||
// 'containers.containername',
|
||||
|
||||
// 'mark_list.companyname',
|
||||
// 'mark_list.customername'
|
||||
// )
|
||||
// ->orderBy('containers.containerdate', 'desc')
|
||||
// ->get();
|
||||
|
||||
// return view('admin.reports', compact('reports'));
|
||||
// }
|
||||
|
||||
|
||||
|
||||
public function index(Request $request)
|
||||
{
|
||||
$reports = DB::table('invoices')
|
||||
->join('containers', 'containers.id', '=', 'invoices.container_id')
|
||||
->leftJoin('mark_list', 'mark_list.mark_no', '=', 'invoices.mark_no')
|
||||
->select(
|
||||
// INVOICE
|
||||
'invoices.id as invoicepk',
|
||||
'orders.id as order_pk',
|
||||
'orders.order_id',
|
||||
'orders.mark_no',
|
||||
'orders.origin',
|
||||
'orders.destination',
|
||||
|
||||
'shipments.id as shipment_pk',
|
||||
'shipments.shipment_id',
|
||||
'shipments.status as shipment_status',
|
||||
'shipments.shipment_date',
|
||||
|
||||
'invoices.invoice_number',
|
||||
'invoices.invoice_date',
|
||||
'invoices.final_amount',
|
||||
'invoices.final_amount_with_gst',
|
||||
'invoices.gst_percent',
|
||||
'invoices.gst_amount',
|
||||
'invoices.status as invoicestatus',
|
||||
'invoices.mark_no',
|
||||
'invoices.status as invoice_status',
|
||||
|
||||
// CONTAINER
|
||||
'containers.id as containerpk',
|
||||
'containers.container_number',
|
||||
'containers.container_date',
|
||||
'containers.container_name',
|
||||
|
||||
// RAW FIELDS (for reference/debug if needed)
|
||||
'invoices.company_name as inv_company_name',
|
||||
'invoices.customer_name as inv_customer_name',
|
||||
'mark_list.company_name as ml_company_name',
|
||||
'mark_list.customer_name as ml_customer_name',
|
||||
|
||||
// FINAL FIELDS (automatically pick invoice first, else mark_list)
|
||||
DB::raw('COALESCE(invoices.company_name, mark_list.company_name) as company_name'),
|
||||
DB::raw('COALESCE(invoices.customer_name, mark_list.customer_name) as customer_name')
|
||||
'mark_list.company_name',
|
||||
'mark_list.customer_name'
|
||||
)
|
||||
->orderBy('invoices.invoice_date', 'desc')
|
||||
->orderBy('invoices.id', 'desc')
|
||||
|
||||
->orderBy('shipments.shipment_date', 'desc')
|
||||
->get();
|
||||
|
||||
return view('admin.reports', compact('reports'));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -85,6 +85,7 @@ public function approveProfileUpdate($id)
|
||||
$req = \App\Models\UpdateRequest::findOrFail($id);
|
||||
$user = \App\Models\User::findOrFail($req->user_id);
|
||||
|
||||
// FIX: Ensure data is array
|
||||
$newData = is_array($req->data) ? $req->data : json_decode($req->data, true);
|
||||
|
||||
foreach ($newData as $key => $value) {
|
||||
@@ -95,18 +96,8 @@ public function approveProfileUpdate($id)
|
||||
}
|
||||
}
|
||||
|
||||
// Update user table
|
||||
$user->save();
|
||||
|
||||
// Update mark_list table
|
||||
\App\Models\MarkList::where('customer_id', $user->customer_id)
|
||||
->update([
|
||||
'customer_name' => $user->customer_name,
|
||||
'company_name' => $user->company_name,
|
||||
'mobile_no' => $user->mobile_no
|
||||
]);
|
||||
|
||||
// Update request status
|
||||
$req->status = 'approved';
|
||||
$req->admin_note = 'Approved by admin on ' . now();
|
||||
$req->save();
|
||||
|
||||
@@ -9,9 +9,6 @@ use App\Models\Invoice;
|
||||
use App\Models\InvoiceItem;
|
||||
use Illuminate\Http\Request;
|
||||
use Maatwebsite\Excel\Facades\Excel;
|
||||
use Carbon\Carbon;
|
||||
use Barryvdh\DomPDF\Facade\Pdf;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
class ContainerController extends Controller
|
||||
{
|
||||
@@ -193,7 +190,6 @@ class ContainerController extends Controller
|
||||
'kg_col' => null,
|
||||
'totalkg_col' => null,
|
||||
'itemno_col' => null,
|
||||
'shopno_col' => null,
|
||||
];
|
||||
|
||||
foreach ($header as $colIndex => $headingText) {
|
||||
@@ -234,11 +230,6 @@ class ContainerController extends Controller
|
||||
strpos($normalized, 'ITEM') !== false
|
||||
) {
|
||||
$essentialColumns['itemno_col'] = $colIndex;
|
||||
} elseif (
|
||||
strpos($normalized, 'SHOPNO') !== false ||
|
||||
strpos($normalized, 'SHOP') !== false
|
||||
) {
|
||||
$essentialColumns['shopno_col'] = $colIndex;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -261,7 +252,7 @@ class ContainerController extends Controller
|
||||
$rowText = strtoupper(implode(' ', $trimmedRow));
|
||||
if (
|
||||
stripos($rowText, 'TOTAL') !== false ||
|
||||
stripos($rowText, 'TTL') !== false ||
|
||||
stripos($rowText, 'TTL') !== false ||
|
||||
stripos($rowText, 'GRAND') !== false
|
||||
) {
|
||||
continue;
|
||||
@@ -282,96 +273,7 @@ class ContainerController extends Controller
|
||||
];
|
||||
}
|
||||
|
||||
if (empty($cleanedRows)) {
|
||||
return back()
|
||||
->withErrors(['excel_file' => 'No valid item rows found in Excel.'])
|
||||
->withInput();
|
||||
}
|
||||
|
||||
// FORMULA CHECK
|
||||
$cleanNumber = function ($value) {
|
||||
if (is_string($value)) {
|
||||
$value = str_replace(',', '', trim($value));
|
||||
}
|
||||
return is_numeric($value) ? (float)$value : 0;
|
||||
};
|
||||
|
||||
$formulaErrors = [];
|
||||
|
||||
foreach ($cleanedRows as $item) {
|
||||
$row = $item['row'];
|
||||
$offset = $item['offset'];
|
||||
|
||||
$ctn = $essentialColumns['ctn_col'] !== null ? $cleanNumber($row[$essentialColumns['ctn_col']] ?? 0) : 0;
|
||||
$qty = $essentialColumns['qty_col'] !== null ? $cleanNumber($row[$essentialColumns['qty_col']] ?? 0) : 0;
|
||||
$ttlQ = $essentialColumns['totalqty_col'] !== null ? $cleanNumber($row[$essentialColumns['totalqty_col']] ?? 0) : 0;
|
||||
$cbm = $essentialColumns['cbm_col'] !== null ? $cleanNumber($row[$essentialColumns['cbm_col']] ?? 0) : 0;
|
||||
$ttlC = $essentialColumns['totalcbm_col'] !== null ? $cleanNumber($row[$essentialColumns['totalcbm_col']] ?? 0) : 0;
|
||||
$kg = $essentialColumns['kg_col'] !== null ? $cleanNumber($row[$essentialColumns['kg_col']] ?? 0) : 0;
|
||||
$ttlK = $essentialColumns['totalkg_col'] !== null ? $cleanNumber($row[$essentialColumns['totalkg_col']] ?? 0) : 0;
|
||||
|
||||
$price = $essentialColumns['price_col'] !== null ? $cleanNumber($row[$essentialColumns['price_col']] ?? 0) : 0;
|
||||
$ttlAmount = $essentialColumns['amount_col'] !== null ? $cleanNumber($row[$essentialColumns['amount_col']] ?? 0) : 0;
|
||||
|
||||
$desc = $essentialColumns['desc_col'] !== null ? (string)($row[$essentialColumns['desc_col']] ?? '') : '';
|
||||
$mark = $essentialColumns['itemno_col'] !== null ? (string)($row[$essentialColumns['itemno_col']] ?? '') : '';
|
||||
|
||||
$expTtlQty = $qty * $ctn;
|
||||
$expTtlCbm = $cbm * $ctn;
|
||||
$expTtlKg = $kg * $ctn;
|
||||
$expTtlAmount = ($qty * $ctn) * $price;
|
||||
|
||||
$rowErrors = [];
|
||||
|
||||
if (abs($ttlQ - $expTtlQty) > 0.01) {
|
||||
$rowErrors['TOTAL QTY'] = [
|
||||
'actual' => $ttlQ,
|
||||
'expected' => $expTtlQty,
|
||||
];
|
||||
}
|
||||
|
||||
if (abs($ttlC - $expTtlCbm) > 0.0005) {
|
||||
$rowErrors['TOTAL CBM'] = [
|
||||
'actual' => $ttlC,
|
||||
'expected' => $expTtlCbm,
|
||||
];
|
||||
}
|
||||
|
||||
if (abs($ttlK - $expTtlKg) > 0.01) {
|
||||
$rowErrors['TOTAL KG'] = [
|
||||
'actual' => $ttlK,
|
||||
'expected' => $expTtlKg,
|
||||
];
|
||||
}
|
||||
|
||||
if ($essentialColumns['amount_col'] !== null && $essentialColumns['price_col'] !== null) {
|
||||
if (abs($ttlAmount - $expTtlAmount) > 0.01) {
|
||||
$rowErrors['TOTAL AMOUNT'] = [
|
||||
'actual' => $ttlAmount,
|
||||
'expected' => $expTtlAmount,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($rowErrors)) {
|
||||
$rowData = [];
|
||||
foreach ($header as $colIndex => $headingText) {
|
||||
$value = $row[$colIndex] ?? null;
|
||||
if (is_string($value)) $value = trim($value);
|
||||
$rowData[$headingText] = $value;
|
||||
}
|
||||
|
||||
$formulaErrors[] = [
|
||||
'excel_row' => $headerRowIndex + 1 + $offset,
|
||||
'mark_no' => $mark,
|
||||
'description' => $desc,
|
||||
'errors' => $rowErrors,
|
||||
'data' => $rowData,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// MARK CHECK
|
||||
// MARK CHECK: strict - collect ALL marks + unmatched rows
|
||||
$marksFromExcel = [];
|
||||
foreach ($cleanedRows as $item) {
|
||||
$row = $item['row'];
|
||||
@@ -397,12 +299,10 @@ class ContainerController extends Controller
|
||||
|
||||
$unmatchedMarks = array_values(array_diff($marksFromExcel, $validMarks));
|
||||
|
||||
$markErrors = [];
|
||||
|
||||
if (!empty($unmatchedMarks)) {
|
||||
foreach ($cleanedRows as $item) {
|
||||
$row = $item['row'];
|
||||
$offset = $item['offset'];
|
||||
$row = $item['row'];
|
||||
$offset = $item['offset'];
|
||||
$rowMark = trim((string)($row[$essentialColumns['itemno_col']] ?? ''));
|
||||
|
||||
if ($rowMark === '' || !in_array($rowMark, $unmatchedMarks)) {
|
||||
@@ -416,24 +316,21 @@ class ContainerController extends Controller
|
||||
$rowData[$headingText] = $value;
|
||||
}
|
||||
|
||||
$markErrors[] = [
|
||||
$unmatchedRowsData[] = [
|
||||
'excel_row' => $headerRowIndex + 1 + $offset,
|
||||
'mark_no' => $rowMark,
|
||||
'data' => $rowData,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($formulaErrors) || !empty($markErrors)) {
|
||||
return back()
|
||||
->withErrors(['excel_file' => 'Some mark numbers are not found in Mark List. Container not created.'])
|
||||
->withInput()
|
||||
->with([
|
||||
'formula_errors' => $formulaErrors,
|
||||
'mark_errors' => $markErrors,
|
||||
]);
|
||||
->with('unmatched_rows', $unmatchedRowsData);
|
||||
}
|
||||
|
||||
// STEP 1: Marks → customers mapping + grouping
|
||||
|
||||
$markRecords = MarkList::whereIn('mark_no', $marksFromExcel)
|
||||
->where('status', 'active')
|
||||
->get();
|
||||
@@ -481,6 +378,7 @@ class ContainerController extends Controller
|
||||
}
|
||||
|
||||
// STEP 2: Container + ContainerRows save
|
||||
|
||||
$container = Container::create([
|
||||
'container_name' => $request->container_name,
|
||||
'container_number' => $request->container_number,
|
||||
@@ -514,6 +412,7 @@ class ContainerController extends Controller
|
||||
}
|
||||
|
||||
// STEP 3: per-customer invoices + invoice items
|
||||
|
||||
$invoiceCount = 0;
|
||||
|
||||
foreach ($groupedByCustomer as $customerId => $rowsForCustomer) {
|
||||
@@ -524,20 +423,12 @@ class ContainerController extends Controller
|
||||
$firstMark = $rowsForCustomer[0]['mark'];
|
||||
$snap = $markToSnapshot[$firstMark] ?? null;
|
||||
|
||||
$customerUser = \App\Models\User::where('customer_id', $customerId)->first();
|
||||
|
||||
$invoice = new Invoice();
|
||||
$invoice->container_id = $container->id;
|
||||
$invoice->customer_id = $customerUser->id ?? null;
|
||||
$invoice->mark_no = $firstMark;
|
||||
|
||||
$invoice->container_id = $container->id;
|
||||
// $invoice->customer_id = $customerId;
|
||||
$invoice->invoice_number = $this->generateInvoiceNumber();
|
||||
|
||||
$invoice->invoice_date = $container->container_date;
|
||||
|
||||
$invoice->due_date = Carbon::parse($invoice->invoice_date)
|
||||
->addDays(10)
|
||||
->format('Y-m-d');
|
||||
$invoice->invoice_date = now()->toDateString();
|
||||
$invoice->due_date = null;
|
||||
|
||||
if ($snap) {
|
||||
$invoice->customer_name = $snap['customer_name'] ?? null;
|
||||
@@ -545,20 +436,19 @@ class ContainerController extends Controller
|
||||
$invoice->customer_mobile = $snap['mobile_no'] ?? null;
|
||||
}
|
||||
|
||||
if ($customerUser) {
|
||||
$invoice->customer_email = $customerUser->email ?? null;
|
||||
$invoice->customer_address = $customerUser->address ?? null;
|
||||
$invoice->pincode = $customerUser->pincode ?? null;
|
||||
}
|
||||
|
||||
$invoice->final_amount = 0;
|
||||
$invoice->gst_percent = 0;
|
||||
$invoice->gst_amount = 0;
|
||||
$invoice->final_amount_with_gst = 0;
|
||||
|
||||
$uniqueMarks = array_unique(array_column($rowsForCustomer, 'mark'));
|
||||
$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';
|
||||
|
||||
@@ -568,51 +458,42 @@ class ContainerController extends Controller
|
||||
$totalAmount = 0;
|
||||
|
||||
foreach ($rowsForCustomer as $item) {
|
||||
$row = $item['row'];
|
||||
$offset = $item['offset'];
|
||||
$mark = $item['mark']; // ✅ mark_no from Excel
|
||||
$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;
|
||||
|
||||
$shopNo = $essentialColumns['shopno_col'] !== null ? ($row[$essentialColumns['shopno_col']] ?? null) : null;
|
||||
|
||||
$rowIndex = $headerRowIndex + 1 + $offset;
|
||||
$description = $essentialColumns['desc_col'] !== null ? ($row[$essentialColumns['desc_col']] ?? null) : null;
|
||||
$ctn = $essentialColumns['ctn_col'] !== null ? (int)($row[$essentialColumns['ctn_col']] ?? 0) : 0;
|
||||
$qty = $essentialColumns['qty_col'] !== null ? (int)($row[$essentialColumns['qty_col']] ?? 0) : 0;
|
||||
$ttlQty = $essentialColumns['totalqty_col'] !== null ? (int)($row[$essentialColumns['totalqty_col']] ?? 0) : $qty;
|
||||
$unit = $essentialColumns['unit_col'] !== null ? ($row[$essentialColumns['unit_col']] ?? null) : null;
|
||||
$price = $essentialColumns['price_col'] !== null ? (float)($row[$essentialColumns['price_col']] ?? 0) : 0;
|
||||
$ttlAmount = $essentialColumns['amount_col'] !== null ? (float)($row[$essentialColumns['amount_col']] ?? 0) : 0;
|
||||
$cbm = $essentialColumns['cbm_col'] !== null ? (float)($row[$essentialColumns['cbm_col']] ?? 0) : 0;
|
||||
$ttlCbm = $essentialColumns['totalcbm_col'] !== null ? (float)($row[$essentialColumns['totalcbm_col']] ?? $cbm) : $cbm;
|
||||
$kg = $essentialColumns['kg_col'] !== null ? (float)($row[$essentialColumns['kg_col']] ?? 0) : 0;
|
||||
$ttlKg = $essentialColumns['totalkg_col'] !== null ? (float)($row[$essentialColumns['totalkg_col']] ?? $kg) : $kg;
|
||||
|
||||
InvoiceItem::create([
|
||||
'invoice_id' => $invoice->id,
|
||||
'container_id' => $container->id,
|
||||
'container_row_index' => $rowIndex,
|
||||
'description' => $description,
|
||||
'ctn' => $ctn,
|
||||
'qty' => $qty,
|
||||
'ttl_qty' => $ttlQty,
|
||||
'unit' => $unit,
|
||||
'price' => $price,
|
||||
'ttl_amount' => $ttlAmount,
|
||||
'cbm' => $cbm,
|
||||
'ttl_cbm' => $ttlCbm,
|
||||
'kg' => $kg,
|
||||
'ttl_kg' => $ttlKg,
|
||||
'shop_no' => $shopNo,
|
||||
'mark_no' => $mark, // ✅ save mark_no from Excel
|
||||
'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 = $totalAmount;
|
||||
$invoice->gst_percent = 0;
|
||||
$invoice->gst_amount = 0;
|
||||
$invoice->final_amount_with_gst = $totalAmount;
|
||||
|
||||
$invoice->save();
|
||||
@@ -625,17 +506,7 @@ class ContainerController extends Controller
|
||||
public function show(Container $container)
|
||||
{
|
||||
$container->load('rows');
|
||||
|
||||
$lockedRowIndexes = \App\Models\Invoice::whereIn('invoices.status', ['paid', 'paying'])
|
||||
->where('invoices.container_id', $container->id)
|
||||
->join('invoice_items', 'invoices.id', '=', 'invoice_items.invoice_id')
|
||||
->pluck('invoice_items.container_row_index')
|
||||
->filter()
|
||||
->unique()
|
||||
->values()
|
||||
->toArray();
|
||||
|
||||
return view('admin.container_show', compact('container', 'lockedRowIndexes'));
|
||||
return view('admin.container_show', compact('container'));
|
||||
}
|
||||
|
||||
public function updateRows(Request $request, Container $container)
|
||||
@@ -647,170 +518,17 @@ class ContainerController extends Controller
|
||||
->where('id', $rowId)
|
||||
->first();
|
||||
|
||||
if (!$row) {
|
||||
continue;
|
||||
}
|
||||
if (!$row) continue;
|
||||
|
||||
$data = $row->data ?? [];
|
||||
|
||||
foreach ($cols as $colHeader => $value) {
|
||||
$data[$colHeader] = $value;
|
||||
}
|
||||
$row->update(['data' => $data]);
|
||||
|
||||
$normalizedMap = [];
|
||||
foreach ($data as $key => $value) {
|
||||
if ($key === null || $key === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$normKey = strtoupper((string)$key);
|
||||
$normKey = str_replace([' ', '/', '-', '.'], '', $normKey);
|
||||
$normalizedMap[$normKey] = $value;
|
||||
}
|
||||
|
||||
$getFirstNumeric = function (array $map, array $possibleKeys) {
|
||||
foreach ($possibleKeys as $search) {
|
||||
$normSearch = strtoupper($search);
|
||||
$normSearch = str_replace([' ', '/', '-', '.'], '', $normSearch);
|
||||
|
||||
foreach ($map as $nKey => $value) {
|
||||
if (strpos($nKey, $normSearch) !== false) {
|
||||
if (is_numeric($value)) {
|
||||
return (float)$value;
|
||||
}
|
||||
if (is_string($value) && is_numeric(trim($value))) {
|
||||
return (float)trim($value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
$ctnKeys = ['CTN', 'CTNS'];
|
||||
$qtyKeys = ['QTY', 'PCS', 'PIECES'];
|
||||
$ttlQtyKeys = ['ITLQTY', 'TOTALQTY', 'TTLQTY'];
|
||||
$cbmKeys = ['TOTALCBM', 'TTLCBM', 'ITLCBM', 'CBM'];
|
||||
$kgKeys = ['TOTALKG', 'TTKG', 'KG', 'WEIGHT'];
|
||||
$amountKeys = ['AMOUNT', 'TTLAMOUNT', 'TOTALAMOUNT'];
|
||||
|
||||
$ctn = $getFirstNumeric($normalizedMap, $ctnKeys);
|
||||
|
||||
$qty = $getFirstNumeric($normalizedMap, $qtyKeys);
|
||||
|
||||
$ttlQ = $getFirstNumeric($normalizedMap, $ttlQtyKeys);
|
||||
|
||||
if ($ttlQ == 0 && $ctn && $qty) {
|
||||
$ttlQ = $ctn * $qty;
|
||||
}
|
||||
|
||||
$cbm = $getFirstNumeric($normalizedMap, ['CBM']);
|
||||
$ttlC = $getFirstNumeric($normalizedMap, ['TOTALCBM', 'TTLCBM', 'ITLCBM']);
|
||||
if ($ttlC == 0 && $cbm && $ctn) {
|
||||
$ttlC = $cbm * $ctn;
|
||||
}
|
||||
|
||||
$kg = $getFirstNumeric($normalizedMap, ['KG', 'WEIGHT']);
|
||||
$ttlK = $getFirstNumeric($normalizedMap, ['TOTALKG', 'TTKG']);
|
||||
if ($ttlK == 0 && $kg && $ctn) {
|
||||
$ttlK = $kg * $ctn;
|
||||
}
|
||||
|
||||
$price = $getFirstNumeric($normalizedMap, ['PRICE', 'RATE']);
|
||||
$amount = $getFirstNumeric($normalizedMap, $amountKeys);
|
||||
if ($amount == 0 && $price && $ttlQ) {
|
||||
$amount = $price * $ttlQ;
|
||||
}
|
||||
|
||||
$desc = null;
|
||||
foreach (['DESCRIPTION', 'DESC'] as $dKey) {
|
||||
$normD = str_replace([' ', '/', '-', '.'], '', strtoupper($dKey));
|
||||
foreach ($normalizedMap as $nKey => $v) {
|
||||
if (strpos($nKey, $normD) !== false) {
|
||||
$desc = is_string($v) ? trim($v) : $v;
|
||||
break 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$shopNo = null;
|
||||
foreach (['SHOPNO', 'SHOP'] as $sKey) {
|
||||
$normS = str_replace([' ', '/', '-', '.'], '', strtoupper($sKey));
|
||||
foreach ($normalizedMap as $nKey => $v) {
|
||||
if (strpos($nKey, $normS) !== false) {
|
||||
$shopNo = is_string($v) ? trim($v) : $v;
|
||||
break 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ Get mark_no
|
||||
$markNo = null;
|
||||
foreach (['MARKNO', 'MARK', 'ITEMNO', 'ITEM'] as $mKey) {
|
||||
$normM = str_replace([' ', '/', '-', '.'], '', strtoupper($mKey));
|
||||
foreach ($normalizedMap as $nKey => $v) {
|
||||
if (strpos($nKey, $normM) !== false) {
|
||||
$markNo = is_string($v) ? trim($v) : $v;
|
||||
break 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$rowIndex = $row->row_index;
|
||||
|
||||
$items = InvoiceItem::where('container_id', $container->id)
|
||||
->where('container_row_index', $rowIndex)
|
||||
->get();
|
||||
|
||||
if ($items->isEmpty() && $desc) {
|
||||
$items = InvoiceItem::where('container_id', $container->id)
|
||||
->whereNull('container_row_index')
|
||||
->where('description', $desc)
|
||||
->get();
|
||||
}
|
||||
|
||||
foreach ($items as $item) {
|
||||
$item->description = $desc;
|
||||
$item->ctn = $ctn;
|
||||
$item->qty = $qty;
|
||||
$item->ttl_qty = $ttlQ;
|
||||
$item->price = $price;
|
||||
$item->ttl_amount = $amount;
|
||||
$item->cbm = $cbm;
|
||||
$item->ttl_cbm = $ttlC;
|
||||
$item->kg = $kg;
|
||||
$item->ttl_kg = $ttlK;
|
||||
$item->shop_no = $shopNo;
|
||||
$item->mark_no = $markNo; // ✅ update mark_no
|
||||
$item->save();
|
||||
|
||||
$invoice = $item->invoice;
|
||||
if ($invoice) {
|
||||
$newBaseAmount = InvoiceItem::where('invoice_id', $invoice->id)
|
||||
->sum('ttl_amount');
|
||||
|
||||
$taxType = $invoice->tax_type;
|
||||
$cgstPercent = (float) ($invoice->cgst_percent ?? 0);
|
||||
$sgstPercent = (float) ($invoice->sgst_percent ?? 0);
|
||||
$igstPercent = (float) ($invoice->igst_percent ?? 0);
|
||||
|
||||
$gstPercent = 0;
|
||||
if ($taxType === 'gst') {
|
||||
$gstPercent = $cgstPercent + $sgstPercent;
|
||||
} elseif ($taxType === 'igst') {
|
||||
$gstPercent = $igstPercent;
|
||||
}
|
||||
|
||||
$gstAmount = $newBaseAmount * $gstPercent / 100;
|
||||
$finalWithGst = $newBaseAmount + $gstAmount;
|
||||
|
||||
$invoice->final_amount = $newBaseAmount;
|
||||
$invoice->gst_amount = $gstAmount;
|
||||
$invoice->final_amount_with_gst = $finalWithGst;
|
||||
$invoice->gst_percent = $gstPercent;
|
||||
$invoice->save();
|
||||
}
|
||||
}
|
||||
$row->update([
|
||||
'data' => $data,
|
||||
]);
|
||||
}
|
||||
|
||||
return redirect()
|
||||
@@ -820,37 +538,17 @@ class ContainerController extends Controller
|
||||
|
||||
public function updateStatus(Request $request, Container $container)
|
||||
{
|
||||
$request->validate([
|
||||
'status' => 'required|in:container-ready,export-custom,international-transit,arrived-at-india,import-custom,warehouse,domestic-distribution,out-for-delivery,delivered',
|
||||
]);
|
||||
$request->validate(['status' => 'required|in:pending,in-progress,completed,cancelled']);
|
||||
|
||||
$container->status = $request->status;
|
||||
$container->save();
|
||||
$container->update(['status' => $request->status]);
|
||||
|
||||
if ($request->wantsJson() || $request->ajax()) {
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'status' => $container->status,
|
||||
]);
|
||||
}
|
||||
|
||||
return back()->with('success', 'Container status updated.');
|
||||
return redirect()->route('containers.index')->with('success', 'Status updated.');
|
||||
}
|
||||
|
||||
public function destroy(Container $container)
|
||||
{
|
||||
$container->delete();
|
||||
|
||||
if (request()->wantsJson() || request()->ajax()) {
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'Container deleted',
|
||||
]);
|
||||
}
|
||||
|
||||
return redirect()
|
||||
->route('containers.index')
|
||||
->with('success', 'Container deleted.');
|
||||
return redirect()->route('containers.index')->with('success', 'Container deleted.');
|
||||
}
|
||||
|
||||
private function generateInvoiceNumber(): string
|
||||
@@ -876,100 +574,4 @@ class ContainerController extends Controller
|
||||
|
||||
return 'INV-' . $year . '-' . str_pad($nextSeq, 6, '0', STR_PAD_LEFT);
|
||||
}
|
||||
|
||||
public function downloadPdf(Container $container)
|
||||
{
|
||||
$container->load('rows');
|
||||
|
||||
$pdf = Pdf::loadView('admin.container_pdf', [
|
||||
'container' => $container,
|
||||
])->setPaper('a4', 'landscape');
|
||||
|
||||
$fileName = 'container-'.$container->container_number.'.pdf';
|
||||
|
||||
return $pdf->download($fileName);
|
||||
}
|
||||
|
||||
public function downloadExcel(Container $container)
|
||||
{
|
||||
if (!$container->excel_file) {
|
||||
abort(404, 'Excel file not found on record.');
|
||||
}
|
||||
|
||||
$path = $container->excel_file;
|
||||
|
||||
if (!Storage::exists($path)) {
|
||||
abort(404, 'Excel file missing on server.');
|
||||
}
|
||||
|
||||
$fileName = 'container-'.$container->container_number.'.xlsx';
|
||||
|
||||
return Storage::download($path, $fileName);
|
||||
}
|
||||
|
||||
public function popupPopup(Container $container)
|
||||
{
|
||||
$container->load('rows');
|
||||
|
||||
$rows = $container->rows ?? collect();
|
||||
|
||||
$totalCtn = 0;
|
||||
$totalQty = 0;
|
||||
$totalCbm = 0;
|
||||
$totalKg = 0;
|
||||
|
||||
$ctnKeys = ['CTN', 'CTNS'];
|
||||
$qtyKeys = ['ITLQTY', 'TOTALQTY', 'TTLQTY', 'QTY', 'PCS', 'PIECES'];
|
||||
$cbmKeys = ['TOTALCBM', 'TTLCBM', 'ITLCBM', 'CBM'];
|
||||
$kgKeys = ['TOTALKG', 'TTKG', 'KG', 'WEIGHT'];
|
||||
|
||||
$getFirstNumeric = function (array $data, array $possibleKeys) {
|
||||
$normalizedMap = [];
|
||||
foreach ($data as $key => $value) {
|
||||
if ($key === null) continue;
|
||||
$normKey = strtoupper((string)$key);
|
||||
$normKey = str_replace([' ', ',', '-', '.', "\n", "\r", "\t"], '', $normKey);
|
||||
$normalizedMap[$normKey] = $value;
|
||||
}
|
||||
|
||||
foreach ($possibleKeys as $search) {
|
||||
$normSearch = strtoupper($search);
|
||||
$normSearch = str_replace([' ', ',', '-', '.', "\n", "\r", "\t"], '', $normSearch);
|
||||
|
||||
foreach ($normalizedMap as $nKey => $value) {
|
||||
if (strpos($nKey, $normSearch) !== false) {
|
||||
if (is_numeric($value)) {
|
||||
return (float)$value;
|
||||
}
|
||||
if (is_string($value) && is_numeric(trim($value))) {
|
||||
return (float)trim($value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$data = $row->data ?? [];
|
||||
if (!is_array($data)) continue;
|
||||
|
||||
$totalCtn += $getFirstNumeric($data, $ctnKeys);
|
||||
$totalQty += $getFirstNumeric($data, $qtyKeys);
|
||||
$totalCbm += $getFirstNumeric($data, $cbmKeys);
|
||||
$totalKg += $getFirstNumeric($data, $kgKeys);
|
||||
}
|
||||
|
||||
$summary = [
|
||||
'total_ctn' => round($totalCtn, 2),
|
||||
'total_qty' => round($totalQty, 2),
|
||||
'total_cbm' => round($totalCbm, 3),
|
||||
'total_kg' => round($totalKg, 2),
|
||||
];
|
||||
|
||||
return view('admin.partials.container_popup_readonly', [
|
||||
'container' => $container,
|
||||
'summary' => $summary,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,9 +67,12 @@ class ChatController extends Controller
|
||||
'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
|
||||
@@ -86,7 +89,11 @@ class ChatController extends Controller
|
||||
$message->load('sender');
|
||||
|
||||
// Fire real-time event
|
||||
<<<<<<< HEAD
|
||||
broadcast(new NewChatMessage($message))->toOthers();
|
||||
=======
|
||||
broadcast(new NewChatMessage($message));
|
||||
>>>>>>> 8b6d3d5fadadda310ef45ec03c879b900bff4cb025f45d1bb5d188761d53e043
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
|
||||
@@ -21,33 +21,23 @@ class UserOrderController extends Controller
|
||||
}
|
||||
|
||||
// -------------------------------------
|
||||
// Get customer invoices with containers
|
||||
// Get all orders
|
||||
// -------------------------------------
|
||||
$invoices = $user->invoices()->with('container')->get();
|
||||
|
||||
// Unique containers for this customer
|
||||
$containers = $invoices->pluck('container')->filter()->unique('id');
|
||||
$orders = $user->orders()->with('invoice')->get();
|
||||
|
||||
// -------------------------------------
|
||||
// Counts based on container status
|
||||
// Counts
|
||||
// -------------------------------------
|
||||
$totalOrders = $containers->count();
|
||||
|
||||
$delivered = $containers->where('status', 'delivered')->count();
|
||||
|
||||
$inTransit = $containers->whereNotIn('status', [
|
||||
'delivered',
|
||||
'warehouse',
|
||||
'domestic-distribution'
|
||||
])->count();
|
||||
|
||||
$active = $totalOrders;
|
||||
$totalOrders = $orders->count();
|
||||
$delivered = $orders->where('status', 'delivered')->count();
|
||||
$inTransit = $orders->where('status', '!=', 'delivered')->count();
|
||||
$active = $totalOrders;
|
||||
|
||||
// -------------------------------------
|
||||
// Total Amount = sum of invoice totals
|
||||
// Total Amount = Invoice.total_with_gst
|
||||
// -------------------------------------
|
||||
$totalAmount = $invoices->sum(function ($invoice) {
|
||||
return $invoice->final_amount_with_gst ?? 0;
|
||||
$totalAmount = $orders->sum(function ($o) {
|
||||
return $o->invoice->final_amount_with_gst ?? 0;
|
||||
});
|
||||
|
||||
// Format total amount in K, L, Cr
|
||||
@@ -55,12 +45,13 @@ class UserOrderController extends Controller
|
||||
|
||||
return response()->json([
|
||||
'status' => true,
|
||||
|
||||
'summary' => [
|
||||
'active_orders' => $active,
|
||||
'in_transit_orders' => $inTransit,
|
||||
'delivered_orders' => $delivered,
|
||||
'total_value' => $formattedAmount,
|
||||
'total_raw' => $totalAmount
|
||||
'total_value' => $formattedAmount, // formatted value
|
||||
'total_raw' => $totalAmount // original value
|
||||
]
|
||||
]);
|
||||
}
|
||||
@@ -99,28 +90,20 @@ class UserOrderController extends Controller
|
||||
], 401);
|
||||
}
|
||||
|
||||
// Get invoices with containers for this customer
|
||||
$invoices = $user->invoices()
|
||||
->with('container')
|
||||
// Fetch orders for this user
|
||||
$orders = $user->orders()
|
||||
->with(['invoice', 'shipments'])
|
||||
->orderBy('id', 'desc')
|
||||
->get();
|
||||
|
||||
// Extract unique containers
|
||||
$containers = $invoices->pluck('container')
|
||||
->filter()
|
||||
->unique('id')
|
||||
->values();
|
||||
|
||||
$orders = $containers->map(function ($container) {
|
||||
|
||||
return [
|
||||
'order_id' => $container->id,
|
||||
'container_number' => $container->container_number,
|
||||
'status' => $container->status,
|
||||
'container_date' => $container->container_date,
|
||||
'created_at' => $container->created_at,
|
||||
];
|
||||
});
|
||||
->get()
|
||||
->map(function ($o) {
|
||||
return [
|
||||
'order_id' => $o->order_id,
|
||||
'status' => $o->status,
|
||||
'amount' => $o->ttl_amount,
|
||||
'description'=> "Order from {$o->origin} to {$o->destination}",
|
||||
'created_at' => $o->created_at,
|
||||
];
|
||||
});
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
@@ -132,73 +115,45 @@ public function orderDetails($order_id)
|
||||
{
|
||||
$user = JWTAuth::parseToken()->authenticate();
|
||||
|
||||
if (!$user) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Unauthorized'
|
||||
], 401);
|
||||
}
|
||||
|
||||
// Find container first
|
||||
$container = \App\Models\Container::find($order_id);
|
||||
|
||||
if (!$container) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Container not found'
|
||||
], 404);
|
||||
}
|
||||
|
||||
// Find invoice belonging to this user for this container
|
||||
$invoice = \App\Models\Invoice::where('customer_id', $user->id)
|
||||
->where('container_id', $container->id)
|
||||
$order = $user->orders()
|
||||
->with(['items'])
|
||||
->where('order_id', $order_id)
|
||||
->first();
|
||||
|
||||
if (!$invoice) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Order not found for this user'
|
||||
], 404);
|
||||
if (!$order) {
|
||||
return response()->json(['success' => false, 'message' => 'Order not found'], 404);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'order' => [
|
||||
'container_id' => $container->id,
|
||||
'container_number' => $container->container_number,
|
||||
'container_date' => $container->container_date,
|
||||
'status' => $container->status,
|
||||
'invoice_id' => $invoice->id,
|
||||
'items' => $invoice->items
|
||||
]
|
||||
'order' => $order
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
// public function orderShipment($order_id)
|
||||
// {
|
||||
// $user = JWTAuth::parseToken()->authenticate();
|
||||
public function orderShipment($order_id)
|
||||
{
|
||||
$user = JWTAuth::parseToken()->authenticate();
|
||||
|
||||
// // Get order
|
||||
// $order = $user->orders()->where('order_id', $order_id)->first();
|
||||
// Get order
|
||||
$order = $user->orders()->where('order_id', $order_id)->first();
|
||||
|
||||
// if (!$order) {
|
||||
// return response()->json(['success' => false, 'message' => 'Order not found'], 404);
|
||||
// }
|
||||
if (!$order) {
|
||||
return response()->json(['success' => false, 'message' => 'Order not found'], 404);
|
||||
}
|
||||
|
||||
// // Find shipment only for this order
|
||||
// $shipment = $order->shipments()
|
||||
// ->with(['items' => function ($q) use ($order) {
|
||||
// $q->where('order_id', $order->id);
|
||||
// }])
|
||||
// ->first();
|
||||
// Find shipment only for this order
|
||||
$shipment = $order->shipments()
|
||||
->with(['items' => function ($q) use ($order) {
|
||||
$q->where('order_id', $order->id);
|
||||
}])
|
||||
->first();
|
||||
|
||||
// return response()->json([
|
||||
// 'success' => true,
|
||||
// 'shipment' => $shipment
|
||||
// ]);
|
||||
// }
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'shipment' => $shipment
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
public function orderInvoice($order_id)
|
||||
@@ -224,35 +179,23 @@ public function trackOrder($order_id)
|
||||
{
|
||||
$user = JWTAuth::parseToken()->authenticate();
|
||||
|
||||
if (!$user) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Unauthorized'
|
||||
], 401);
|
||||
}
|
||||
|
||||
// Ensure the container belongs to this customer via invoice
|
||||
$invoice = \App\Models\Invoice::where('customer_id', $user->id)
|
||||
->where('container_id', $order_id)
|
||||
->with('container')
|
||||
$order = $user->orders()
|
||||
->with('shipments')
|
||||
->where('order_id', $order_id)
|
||||
->first();
|
||||
|
||||
if (!$invoice || !$invoice->container) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Order not found'
|
||||
], 404);
|
||||
if (!$order) {
|
||||
return response()->json(['success' => false, 'message' => 'Order not found'], 404);
|
||||
}
|
||||
|
||||
$container = $invoice->container;
|
||||
$shipment = $order->shipments()->first();
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'track' => [
|
||||
'order_id' => $container->id,
|
||||
'container_number' => $container->container_number,
|
||||
'status' => $container->status,
|
||||
'container_date' => $container->container_date,
|
||||
'order_id' => $order->order_id,
|
||||
'shipment_status' => $shipment->status ?? 'pending',
|
||||
'shipment_date' => $shipment->shipment_date ?? null,
|
||||
]
|
||||
]);
|
||||
}
|
||||
@@ -346,44 +289,44 @@ public function invoiceDetails($invoice_id)
|
||||
]);
|
||||
}
|
||||
|
||||
// public function confirmOrder($order_id)
|
||||
// {
|
||||
// $user = JWTAuth::parseToken()->authenticate();
|
||||
public function confirmOrder($order_id)
|
||||
{
|
||||
$user = JWTAuth::parseToken()->authenticate();
|
||||
|
||||
// if (! $user) {
|
||||
// return response()->json([
|
||||
// 'success' => false,
|
||||
// 'message' => 'Unauthorized'
|
||||
// ], 401);
|
||||
// }
|
||||
if (! $user) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Unauthorized'
|
||||
], 401);
|
||||
}
|
||||
|
||||
// $order = $user->orders()
|
||||
// ->where('order_id', $order_id)
|
||||
// ->first();
|
||||
$order = $user->orders()
|
||||
->where('order_id', $order_id)
|
||||
->first();
|
||||
|
||||
// if (! $order) {
|
||||
// return response()->json([
|
||||
// 'success' => false,
|
||||
// 'message' => 'Order not found'
|
||||
// ], 404);
|
||||
// }
|
||||
if (! $order) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Order not found'
|
||||
], 404);
|
||||
}
|
||||
|
||||
// // 🚫 Only allow confirm from order_placed
|
||||
// if ($order->status !== 'order_placed') {
|
||||
// return response()->json([
|
||||
// 'success' => false,
|
||||
// 'message' => 'Order cannot be confirmed'
|
||||
// ], 422);
|
||||
// }
|
||||
// 🚫 Only allow confirm from order_placed
|
||||
if ($order->status !== 'order_placed') {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Order cannot be confirmed'
|
||||
], 422);
|
||||
}
|
||||
|
||||
// $order->status = 'order_confirmed';
|
||||
// $order->save();
|
||||
$order->status = 'order_confirmed';
|
||||
$order->save();
|
||||
|
||||
// return response()->json([
|
||||
// 'success' => true,
|
||||
// 'message' => 'Order confirmed successfully'
|
||||
// ]);
|
||||
// }
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'Order confirmed successfully'
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -10,6 +10,15 @@ 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',
|
||||
@@ -20,6 +29,9 @@ class ChatMessage extends Model
|
||||
'read_by_user',
|
||||
'client_id',
|
||||
];
|
||||
|
||||
>>>>>>> 8b6d3d5fadadda310ef45ec03c879b900bff4cb025f45d1bb5d188761d53e043
|
||||
|
||||
/**
|
||||
* The ticket this message belongs to.
|
||||
*/
|
||||
|
||||
@@ -9,6 +9,7 @@ class Invoice extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
|
||||
protected $fillable = [
|
||||
'container_id',
|
||||
'customer_id',
|
||||
@@ -31,13 +32,6 @@ class Invoice extends Model
|
||||
'pincode',
|
||||
'pdf_path',
|
||||
'notes',
|
||||
// totals from charge groups
|
||||
'charge_groups_total',
|
||||
'grand_total_with_charges',
|
||||
'tax_type',
|
||||
'cgst_percent',
|
||||
'sgst_percent',
|
||||
'igst_percent',
|
||||
];
|
||||
|
||||
/****************************
|
||||
@@ -49,9 +43,16 @@ class Invoice extends Model
|
||||
return $this->hasMany(InvoiceItem::class)->orderBy('id', 'ASC');
|
||||
}
|
||||
|
||||
// public function container()
|
||||
// NEW: invoice आता container वर depend
|
||||
public function container()
|
||||
{
|
||||
return $this->belongsTo(Container::class);
|
||||
}
|
||||
|
||||
// OLD: order() relation काढले आहे
|
||||
// public function order()
|
||||
// {
|
||||
// return $this->belongsTo(Container::class);
|
||||
// return $this->belongsTo(Order::class);
|
||||
// }
|
||||
|
||||
public function customer()
|
||||
@@ -64,16 +65,11 @@ class Invoice extends Model
|
||||
return $this->hasMany(InvoiceInstallment::class);
|
||||
}
|
||||
|
||||
public function chargeGroups()
|
||||
{
|
||||
return $this->hasMany(InvoiceChargeGroup::class, 'invoice_id');
|
||||
}
|
||||
|
||||
/****************************
|
||||
* Helper Functions
|
||||
****************************/
|
||||
|
||||
// (Items based calculateTotals वापरणार नाहीस तरी ठेवू शकतोस)
|
||||
// Auto calculate GST fields (you can call this in controller before saving)
|
||||
public function calculateTotals()
|
||||
{
|
||||
$gst = ($this->final_amount * $this->gst_percent) / 100;
|
||||
@@ -81,53 +77,16 @@ class Invoice extends Model
|
||||
$this->final_amount_with_gst = $this->final_amount + $gst;
|
||||
}
|
||||
|
||||
// Check overdue status condition
|
||||
public function isOverdue()
|
||||
{
|
||||
return $this->status === 'pending' && now()->gt($this->due_date);
|
||||
}
|
||||
|
||||
// जर पुढे container → shipment relation असेल तर हा helper नंतर adjust करू
|
||||
public function getShipment()
|
||||
{
|
||||
// आधी order वरून shipment घेत होत; container flow मध्ये नंतर गरज पडल्यास बदलू
|
||||
return null;
|
||||
}
|
||||
|
||||
// ✅ Charge groups base total (WITHOUT GST)
|
||||
public function getChargeGroupsTotalAttribute()
|
||||
{
|
||||
// base = total_charge sum
|
||||
return (float) $this->chargeGroups->sum('total_charge');
|
||||
}
|
||||
|
||||
// ✅ Grand total: Charge groups base + GST (items ignore)
|
||||
public function getGrandTotalWithChargesAttribute()
|
||||
{
|
||||
$base = (float) ($this->charge_groups_total ?? 0);
|
||||
$gst = (float) ($this->gst_amount ?? 0);
|
||||
|
||||
return $base + $gst;
|
||||
}
|
||||
|
||||
public function totalPaid(): float
|
||||
{
|
||||
return (float) $this->installments()->sum('amount');
|
||||
}
|
||||
|
||||
public function remainingAmount(): float
|
||||
{
|
||||
$grand = (float) $this->grand_total_with_charges;
|
||||
$paid = (float) $this->totalPaid();
|
||||
|
||||
return max(0, $grand - $paid);
|
||||
}
|
||||
|
||||
public function isLockedForEdit(): bool
|
||||
{
|
||||
return $this->status === 'paid';
|
||||
}
|
||||
|
||||
public function container()
|
||||
{
|
||||
return $this->belongsTo(\App\Models\Container::class, 'container_id');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class InvoiceChargeGroup extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'invoice_id',
|
||||
'group_name',
|
||||
'basis_type',
|
||||
'basis_value',
|
||||
'rate',
|
||||
'total_charge',
|
||||
|
||||
'tax_type',
|
||||
'gst_percent',
|
||||
'total_with_gst',
|
||||
];
|
||||
|
||||
public function invoice()
|
||||
{
|
||||
return $this->belongsTo(Invoice::class);
|
||||
}
|
||||
|
||||
public function items()
|
||||
{
|
||||
return $this->hasMany(InvoiceChargeGroupItem::class, 'group_id');
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class InvoiceChargeGroupItem extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'group_id',
|
||||
'invoice_item_id',
|
||||
];
|
||||
|
||||
public function group()
|
||||
{
|
||||
return $this->belongsTo(InvoiceChargeGroup::class, 'group_id');
|
||||
}
|
||||
|
||||
public function item()
|
||||
{
|
||||
return $this->belongsTo(InvoiceItem::class, 'invoice_item_id');
|
||||
}
|
||||
}
|
||||
@@ -11,8 +11,6 @@ class InvoiceItem extends Model
|
||||
|
||||
protected $fillable = [
|
||||
'invoice_id',
|
||||
'container_id', // Container mapping
|
||||
'container_row_index', // Container row index
|
||||
|
||||
'description',
|
||||
'ctn',
|
||||
@@ -29,7 +27,6 @@ class InvoiceItem extends Model
|
||||
'ttl_kg',
|
||||
|
||||
'shop_no',
|
||||
'mark_no',
|
||||
];
|
||||
|
||||
/****************************
|
||||
@@ -40,79 +37,4 @@ class InvoiceItem extends Model
|
||||
{
|
||||
return $this->belongsTo(Invoice::class);
|
||||
}
|
||||
|
||||
public function chargeGroupItems()
|
||||
{
|
||||
return $this->hasMany(InvoiceChargeGroupItem::class, 'invoice_item_id');
|
||||
}
|
||||
|
||||
// हे helper: पहिला group fetch करून त्यावरून rate/total काढणे
|
||||
public function getChargeRateAttribute()
|
||||
{
|
||||
$pivot = $this->chargeGroupItems->first();
|
||||
if (!$pivot || !$pivot->group) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$group = $pivot->group;
|
||||
|
||||
// basis नुसार या item चा basis value
|
||||
$basis = 0;
|
||||
switch ($group->basis_type) {
|
||||
case 'ttl_qty':
|
||||
$basis = $this->ttl_qty;
|
||||
break;
|
||||
case 'amount':
|
||||
$basis = $this->ttl_amount;
|
||||
break;
|
||||
case 'ttl_cbm':
|
||||
$basis = $this->ttl_cbm;
|
||||
break;
|
||||
case 'ttl_kg':
|
||||
$basis = $this->ttl_kg;
|
||||
break;
|
||||
}
|
||||
|
||||
if ($basis <= 0 || ($group->basis_value ?? 0) <= 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// group चा rate field आधीच आहे, ते direct वापरू
|
||||
return (float) $group->rate;
|
||||
}
|
||||
|
||||
public function getChargeTotalAttribute()
|
||||
{
|
||||
$pivot = $this->chargeGroupItems->first();
|
||||
if (!$pivot || !$pivot->group) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$group = $pivot->group;
|
||||
|
||||
$basis = 0;
|
||||
switch ($group->basis_type) {
|
||||
case 'ttl_qty':
|
||||
$basis = $this->ttl_qty;
|
||||
break;
|
||||
case 'amount':
|
||||
$basis = $this->ttl_amount;
|
||||
break;
|
||||
case 'ttl_cbm':
|
||||
$basis = $this->ttl_cbm;
|
||||
break;
|
||||
case 'ttl_kg':
|
||||
$basis = $this->ttl_kg;
|
||||
break;
|
||||
}
|
||||
|
||||
if ($basis <= 0 || ($group->basis_value ?? 0) <= 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// per unit rate
|
||||
$rate = (float) $group->rate;
|
||||
// item total = basis * rate
|
||||
return $basis * $rate;
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -58,10 +58,10 @@ class Order extends Model
|
||||
return $this->belongsToMany(\App\Models\Shipment::class, 'shipment_items', 'order_id', 'shipment_id');
|
||||
}
|
||||
|
||||
// public function invoice()
|
||||
// {
|
||||
// return $this->hasOne(\App\Models\Invoice::class, 'order_id', 'id');
|
||||
// }
|
||||
public function invoice()
|
||||
{
|
||||
return $this->hasOne(\App\Models\Invoice::class, 'order_id', 'id');
|
||||
}
|
||||
|
||||
|
||||
const STATUS_LABELS = [
|
||||
|
||||
@@ -89,8 +89,10 @@ class User extends Authenticatable implements JWTSubject
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
|
||||
public function invoices()
|
||||
{
|
||||
return $this->hasMany(\App\Models\Invoice::class, 'customer_id', 'id');
|
||||
}
|
||||
|
||||
// App\Models\User.php
|
||||
|
||||
@@ -105,10 +107,6 @@ public function invoiceInstallments()
|
||||
);
|
||||
}
|
||||
|
||||
public function invoices()
|
||||
{
|
||||
return $this->hasMany(\App\Models\Invoice::class, 'customer_id', 'id');
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -85,7 +85,8 @@
|
||||
"allow-plugins": {
|
||||
"pestphp/pest-plugin": true,
|
||||
"php-http/discovery": true
|
||||
}
|
||||
},
|
||||
"platform-check": false
|
||||
},
|
||||
"minimum-stability": "stable",
|
||||
"prefer-stable": true
|
||||
|
||||
65
composer.lock
generated
65
composer.lock
generated
@@ -2880,6 +2880,18 @@
|
||||
},
|
||||
{
|
||||
"name": "maennchen/zipstream-php",
|
||||
<<<<<<< HEAD
|
||||
"version": "3.2.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/maennchen/ZipStream-PHP.git",
|
||||
"reference": "682f1098a8fddbaf43edac2306a691c7ad508ec5"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/maennchen/ZipStream-PHP/zipball/682f1098a8fddbaf43edac2306a691c7ad508ec5",
|
||||
"reference": "682f1098a8fddbaf43edac2306a691c7ad508ec5",
|
||||
=======
|
||||
"version": "3.1.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
@@ -2890,21 +2902,34 @@
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/maennchen/ZipStream-PHP/zipball/aeadcf5c412332eb426c0f9b4485f6accba2a99f",
|
||||
"reference": "aeadcf5c412332eb426c0f9b4485f6accba2a99f",
|
||||
>>>>>>> 8b6d3d5fadadda310ef45ec03c879b900bff4cb025f45d1bb5d188761d53e043
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-mbstring": "*",
|
||||
"ext-zlib": "*",
|
||||
<<<<<<< HEAD
|
||||
"php-64bit": "^8.3"
|
||||
=======
|
||||
"php-64bit": "^8.2"
|
||||
>>>>>>> 8b6d3d5fadadda310ef45ec03c879b900bff4cb025f45d1bb5d188761d53e043
|
||||
},
|
||||
"require-dev": {
|
||||
"brianium/paratest": "^7.7",
|
||||
"ext-zip": "*",
|
||||
<<<<<<< HEAD
|
||||
"friendsofphp/php-cs-fixer": "^3.86",
|
||||
"guzzlehttp/guzzle": "^7.5",
|
||||
"mikey179/vfsstream": "^1.6",
|
||||
"php-coveralls/php-coveralls": "^2.5",
|
||||
"phpunit/phpunit": "^12.0",
|
||||
=======
|
||||
"friendsofphp/php-cs-fixer": "^3.16",
|
||||
"guzzlehttp/guzzle": "^7.5",
|
||||
"mikey179/vfsstream": "^1.6",
|
||||
"php-coveralls/php-coveralls": "^2.5",
|
||||
"phpunit/phpunit": "^11.0",
|
||||
>>>>>>> 8b6d3d5fadadda310ef45ec03c879b900bff4cb025f45d1bb5d188761d53e043
|
||||
"vimeo/psalm": "^6.0"
|
||||
},
|
||||
"suggest": {
|
||||
@@ -2946,7 +2971,11 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/maennchen/ZipStream-PHP/issues",
|
||||
<<<<<<< HEAD
|
||||
"source": "https://github.com/maennchen/ZipStream-PHP/tree/3.2.1"
|
||||
=======
|
||||
"source": "https://github.com/maennchen/ZipStream-PHP/tree/3.1.2"
|
||||
>>>>>>> 8b6d3d5fadadda310ef45ec03c879b900bff4cb025f45d1bb5d188761d53e043
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -2954,7 +2983,11 @@
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
<<<<<<< HEAD
|
||||
"time": "2025-12-10T09:58:31+00:00"
|
||||
=======
|
||||
"time": "2025-01-27T12:07:53+00:00"
|
||||
>>>>>>> 8b6d3d5fadadda310ef45ec03c879b900bff4cb025f45d1bb5d188761d53e043
|
||||
},
|
||||
{
|
||||
"name": "markbaker/complex",
|
||||
@@ -4181,6 +4214,18 @@
|
||||
},
|
||||
{
|
||||
"name": "phpoffice/phpspreadsheet",
|
||||
<<<<<<< HEAD
|
||||
"version": "1.30.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/PHPOffice/PhpSpreadsheet.git",
|
||||
"reference": "09cdde5e2f078b9a3358dd217e2c8cb4dac84be2"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/PHPOffice/PhpSpreadsheet/zipball/09cdde5e2f078b9a3358dd217e2c8cb4dac84be2",
|
||||
"reference": "09cdde5e2f078b9a3358dd217e2c8cb4dac84be2",
|
||||
=======
|
||||
"version": "1.30.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
@@ -4191,6 +4236,7 @@
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/PHPOffice/PhpSpreadsheet/zipball/fa8257a579ec623473eabfe49731de5967306c4c",
|
||||
"reference": "fa8257a579ec623473eabfe49731de5967306c4c",
|
||||
>>>>>>> 8b6d3d5fadadda310ef45ec03c879b900bff4cb025f45d1bb5d188761d53e043
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -4213,12 +4259,19 @@
|
||||
"markbaker/complex": "^3.0",
|
||||
"markbaker/matrix": "^3.0",
|
||||
"php": ">=7.4.0 <8.5.0",
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
"psr/http-client": "^1.0",
|
||||
"psr/http-factory": "^1.0",
|
||||
>>>>>>> 8b6d3d5fadadda310ef45ec03c879b900bff4cb025f45d1bb5d188761d53e043
|
||||
"psr/simple-cache": "^1.0 || ^2.0 || ^3.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"dealerdirect/phpcodesniffer-composer-installer": "dev-main",
|
||||
<<<<<<< HEAD
|
||||
"doctrine/instantiator": "^1.5",
|
||||
=======
|
||||
>>>>>>> 8b6d3d5fadadda310ef45ec03c879b900bff4cb025f45d1bb5d188761d53e043
|
||||
"dompdf/dompdf": "^1.0 || ^2.0 || ^3.0",
|
||||
"friendsofphp/php-cs-fixer": "^3.2",
|
||||
"mitoteam/jpgraph": "^10.3",
|
||||
@@ -4265,6 +4318,12 @@
|
||||
},
|
||||
{
|
||||
"name": "Adrien Crivelli"
|
||||
<<<<<<< HEAD
|
||||
},
|
||||
{
|
||||
"name": "Owen Leibman"
|
||||
=======
|
||||
>>>>>>> 8b6d3d5fadadda310ef45ec03c879b900bff4cb025f45d1bb5d188761d53e043
|
||||
}
|
||||
],
|
||||
"description": "PHPSpreadsheet - Read, Create and Write Spreadsheet documents in PHP - Spreadsheet engine",
|
||||
@@ -4281,9 +4340,15 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/PHPOffice/PhpSpreadsheet/issues",
|
||||
<<<<<<< HEAD
|
||||
"source": "https://github.com/PHPOffice/PhpSpreadsheet/tree/1.30.2"
|
||||
},
|
||||
"time": "2026-01-11T05:58:24+00:00"
|
||||
=======
|
||||
"source": "https://github.com/PHPOffice/PhpSpreadsheet/tree/1.30.1"
|
||||
},
|
||||
"time": "2025-10-26T16:01:04+00:00"
|
||||
>>>>>>> 8b6d3d5fadadda310ef45ec03c879b900bff4cb025f45d1bb5d188761d53e043
|
||||
},
|
||||
{
|
||||
"name": "phpoption/phpoption",
|
||||
|
||||
@@ -6,11 +6,6 @@ return [
|
||||
|--------------------------------------------------------------------------
|
||||
| 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'),
|
||||
@@ -19,11 +14,6 @@ return [
|
||||
|--------------------------------------------------------------------------
|
||||
| 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'),
|
||||
@@ -32,11 +22,6 @@ return [
|
||||
|--------------------------------------------------------------------------
|
||||
| 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),
|
||||
@@ -45,11 +30,6 @@ return [
|
||||
|--------------------------------------------------------------------------
|
||||
| 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'),
|
||||
@@ -58,11 +38,6 @@ return [
|
||||
|--------------------------------------------------------------------------
|
||||
| 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',
|
||||
@@ -71,11 +46,6 @@ return [
|
||||
|--------------------------------------------------------------------------
|
||||
| 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'),
|
||||
@@ -88,11 +58,6 @@ return [
|
||||
|--------------------------------------------------------------------------
|
||||
| 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',
|
||||
@@ -109,13 +74,6 @@ return [
|
||||
|--------------------------------------------------------------------------
|
||||
| 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' => [
|
||||
@@ -123,4 +81,53 @@ return [
|
||||
'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,
|
||||
],
|
||||
|
||||
];
|
||||
|
||||
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,
|
||||
],
|
||||
];
|
||||
@@ -34,9 +34,6 @@ class CreateInvoiceItemsTable extends Migration
|
||||
|
||||
$table->timestamps();
|
||||
|
||||
$table->unsignedBigInteger('container_id')->nullable()->after('invoice_id');
|
||||
$table->integer('container_row_index')->nullable()->after('container_id');
|
||||
|
||||
// FK
|
||||
$table->foreign('invoice_id')->references('id')->on('invoices')->onDelete('cascade');
|
||||
});
|
||||
@@ -52,6 +49,4 @@ class CreateInvoiceItemsTable extends Migration
|
||||
});
|
||||
Schema::dropIfExists('invoice_items');
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ return new class extends Migration
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('containers', function (Blueprint $table) {
|
||||
$table->string('status', 21)
|
||||
$table->string('status', 20)
|
||||
->default('pending')
|
||||
->after('container_date');
|
||||
});
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('invoice_charge_groups', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('invoice_id');
|
||||
$table->string('group_name')->nullable(); // उदा. "FREIGHT", "HANDLING"
|
||||
$table->enum('basis_type', ['ttl_qty', 'amount', 'ttl_cbm', 'ttl_kg']);
|
||||
$table->decimal('basis_value', 15, 3)->default(0); // auto calculate केलेला total basis
|
||||
$table->decimal('rate', 15, 3)->default(0); // per basis rate (helper)
|
||||
$table->decimal('total_charge', 15, 2); // admin नी manually टाकलेला total
|
||||
$table->timestamps();
|
||||
|
||||
$table->foreign('invoice_id')
|
||||
->references('id')->on('invoices')
|
||||
->onDelete('cascade');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('invoice_charge_groups');
|
||||
}
|
||||
|
||||
|
||||
|
||||
};
|
||||
@@ -1,35 +0,0 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('invoice_charge_group_items', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('group_id');
|
||||
$table->unsignedBigInteger('invoice_item_id');
|
||||
$table->timestamps();
|
||||
|
||||
$table->foreign('group_id')
|
||||
->references('id')->on('invoice_charge_groups')
|
||||
->onDelete('cascade');
|
||||
|
||||
$table->foreign('invoice_item_id')
|
||||
->references('id')->on('invoice_items')
|
||||
->onDelete('cascade');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('invoice_charge_group_items');
|
||||
}
|
||||
|
||||
};
|
||||
@@ -1,27 +0,0 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::table('invoice_items', function (Blueprint $table) {
|
||||
$table->unsignedBigInteger('container_id')->nullable()->after('invoice_id');
|
||||
$table->integer('container_row_index')->nullable()->after('container_id');
|
||||
});
|
||||
}
|
||||
|
||||
public function down()
|
||||
{
|
||||
Schema::table('invoice_items', function (Blueprint $table) {
|
||||
$table->dropColumn(['container_id', 'container_row_index']);
|
||||
});
|
||||
}
|
||||
|
||||
};
|
||||
@@ -1,29 +0,0 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('invoices', function (Blueprint $table) {
|
||||
// column आधीच आहे का हे check करून, नसेल तरच add करायचा
|
||||
if (!Schema::hasColumn('invoices', 'due_date')) {
|
||||
$table->date('due_date')
|
||||
->nullable()
|
||||
->after('invoice_date');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('invoices', function (Blueprint $table) {
|
||||
if (Schema::hasColumn('invoices', 'due_date')) {
|
||||
$table->dropColumn('due_date');
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -1,26 +0,0 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('containers', function (Blueprint $table) {
|
||||
$table->string('status', 50)->change();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('containers', function (Blueprint $table) {
|
||||
$table->string('status', 20)->change();
|
||||
});
|
||||
}
|
||||
|
||||
};
|
||||
@@ -1,23 +0,0 @@
|
||||
<?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('invoice_charge_groups', function (Blueprint $table) {
|
||||
$table->string('tax_type')->nullable()->after('total_charge');
|
||||
$table->decimal('gst_percent', 5, 2)->default(0)->after('tax_type');
|
||||
$table->decimal('total_with_gst', 15, 2)->default(0)->after('gst_percent');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('invoice_charge_groups', function (Blueprint $table) {
|
||||
$table->dropColumn(['tax_type', 'gst_percent', 'total_with_gst']);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -1,37 +0,0 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class AddChargeColumnsToInvoicesTable extends Migration
|
||||
{
|
||||
public function up()
|
||||
{
|
||||
Schema::table('invoices', function (Blueprint $table) {
|
||||
if (!Schema::hasColumn('invoices', 'charge_groups_total')) {
|
||||
$table->decimal('charge_groups_total', 15, 2)
|
||||
->nullable()
|
||||
->after('final_amount_with_gst');
|
||||
}
|
||||
|
||||
if (!Schema::hasColumn('invoices', 'grand_total_with_charges')) {
|
||||
$table->decimal('grand_total_with_charges', 15, 2)
|
||||
->nullable()
|
||||
->after('charge_groups_total');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public function down()
|
||||
{
|
||||
Schema::table('invoices', function (Blueprint $table) {
|
||||
if (Schema::hasColumn('invoices', 'charge_groups_total')) {
|
||||
$table->dropColumn('charge_groups_total');
|
||||
}
|
||||
if (Schema::hasColumn('invoices', 'grand_total_with_charges')) {
|
||||
$table->dropColumn('grand_total_with_charges');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
<?php
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
return new class extends Migration {
|
||||
public function up(): void
|
||||
{
|
||||
DB::statement("
|
||||
ALTER TABLE `invoices`
|
||||
MODIFY `status` ENUM('pending','paying','paid','overdue')
|
||||
NOT NULL DEFAULT 'pending'
|
||||
");
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
DB::statement("
|
||||
ALTER TABLE `invoices`
|
||||
MODIFY `status` ENUM('pending','paid','overdue')
|
||||
NOT NULL DEFAULT 'pending'
|
||||
");
|
||||
}
|
||||
};
|
||||
@@ -1,27 +0,0 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::table('invoice_items', function (Blueprint $table) {
|
||||
$table->string('mark_no')->nullable(); // after() काहीही नको
|
||||
});
|
||||
}
|
||||
|
||||
public function down()
|
||||
{
|
||||
Schema::table('invoice_items', function (Blueprint $table) {
|
||||
$table->dropColumn('mark_no');
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
};
|
||||
@@ -25,12 +25,6 @@ class PermissionSeeder extends Seeder
|
||||
// EXTRA (ORDERS)
|
||||
'orders.view', // you added this separately
|
||||
|
||||
// CONTAINER
|
||||
'container.view',
|
||||
'container.create',
|
||||
'container.update',
|
||||
'container.delete',
|
||||
|
||||
// SHIPMENT
|
||||
'shipment.view',
|
||||
'shipment.create',
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 58 KiB |
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.
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.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,30 +1,31 @@
|
||||
import Echo from "laravel-echo";
|
||||
import Pusher from "pusher-js";
|
||||
import Echo from 'laravel-echo';
|
||||
import Pusher from 'pusher-js';
|
||||
|
||||
window.Pusher = Pusher;
|
||||
|
||||
console.log("[ECHO] Initializing Reverb...");
|
||||
|
||||
// Get CSRF token from meta tag
|
||||
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
|
||||
window.Echo = new Echo({
|
||||
broadcaster: "reverb",
|
||||
broadcaster: 'reverb',
|
||||
key: import.meta.env.VITE_REVERB_APP_KEY,
|
||||
|
||||
wsHost: import.meta.env.VITE_REVERB_HOST,
|
||||
wsPort: Number(import.meta.env.VITE_REVERB_PORT),
|
||||
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'],
|
||||
|
||||
forceTLS: false,
|
||||
disableStats: true,
|
||||
authEndpoint: '/admin/broadcasting/auth',
|
||||
|
||||
authEndpoint: "/broadcasting/auth",
|
||||
|
||||
// ⭐ MOST IMPORTANT ⭐
|
||||
withCredentials: true,
|
||||
|
||||
auth: {
|
||||
headers: {
|
||||
"X-CSRF-TOKEN": document
|
||||
.querySelector('meta[name="csrf-token"]')
|
||||
?.getAttribute("content"),
|
||||
"X-Requested-With": "XMLHttpRequest",
|
||||
},
|
||||
},
|
||||
'X-CSRF-TOKEN': csrfToken,
|
||||
'Accept': 'application/json',
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
console.log("[ECHO] Loaded Successfully!", window.Echo);
|
||||
console.log('%c[ECHO] Initialized!', 'color: green; font-weight: bold;', window.Echo);
|
||||
|
||||
@@ -1,102 +0,0 @@
|
||||
<div class="container-fluid py-2">
|
||||
|
||||
{{-- Top info cards (container / date / status) --}}
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-3 mb-2">
|
||||
<div class="card p-2">
|
||||
<small class="text-muted">Container Name</small>
|
||||
<div class="fw-semibold">{{ $container->container_name ?? '-' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3 mb-2">
|
||||
<div class="card p-2">
|
||||
<small class="text-muted">Container No</small>
|
||||
<div class="fw-semibold">{{ $container->container_number ?? '-' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3 mb-2">
|
||||
<div class="card p-2">
|
||||
<small class="text-muted">Container Date</small>
|
||||
<div class="fw-semibold">
|
||||
{{ $container->container_date ? \Carbon\Carbon::parse($container->container_date)->format('d-m-Y') : '-' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3 mb-2">
|
||||
<div class="card p-2">
|
||||
<small class="text-muted">Status</small>
|
||||
<div class="fw-semibold text-capitalize">{{ $container->status ?? '-' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Totals (CTN / Qty / CBM / KG) --}}
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-3 mb-2">
|
||||
<div class="card p-2">
|
||||
<small class="text-muted">Total CTN</small>
|
||||
<div class="fw-semibold">{{ $summary['total_ctn'] ?? '-' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3 mb-2">
|
||||
<div class="card p-2">
|
||||
<small class="text-muted">Total Qty</small>
|
||||
<div class="fw-semibold">{{ $summary['total_qty'] ?? '-' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3 mb-2">
|
||||
<div class="card p-2">
|
||||
<small class="text-muted">Total CBM</small>
|
||||
<div class="fw-semibold">{{ $summary['total_cbm'] ?? '-' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3 mb-2">
|
||||
<div class="card p-2">
|
||||
<small class="text-muted">Total KG</small>
|
||||
<div class="fw-semibold">{{ $summary['total_kg'] ?? '-' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Excel rows – same headings as container_show --}}
|
||||
@php
|
||||
$allHeadings = [];
|
||||
foreach ($container->rows as $row) {
|
||||
if (is_array($row->data)) {
|
||||
$allHeadings = array_unique(array_merge($allHeadings, array_keys($row->data)));
|
||||
}
|
||||
}
|
||||
@endphp
|
||||
|
||||
<div class="table-responsive" style="max-height: 500px; border-radius: 8px;">
|
||||
<table class="table table-sm table-bordered align-middle">
|
||||
<thead class="table-warning">
|
||||
<tr>
|
||||
<th style="width: 40px;">#</th>
|
||||
@foreach($allHeadings as $heading)
|
||||
<th>{{ $heading }}</th>
|
||||
@endforeach
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@forelse($container->rows as $index => $row)
|
||||
<tr>
|
||||
<td>{{ $index + 1 }}</td>
|
||||
@foreach($allHeadings as $heading)
|
||||
@php
|
||||
$val = is_array($row->data) ? ($row->data[$heading] ?? '') : '';
|
||||
@endphp
|
||||
<td>{{ $val }}</td>
|
||||
@endforeach
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="{{ count($allHeadings) + 1 }}" class="text-center text-muted py-3">
|
||||
No Excel rows for this container.
|
||||
</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
@@ -2603,12 +2603,11 @@ function renderPaymentTable(list){
|
||||
<td>${escapeHtml(entry.entry_date)}</td>
|
||||
<td>${escapeHtml(entry.description)}</td>
|
||||
|
||||
<!-- Order Quantity - Clickable number without box -->
|
||||
<td>
|
||||
<span onclick="openEntryOrdersModal('${escapeHtml(entry.entry_no)}')"
|
||||
style="cursor: pointer; color: #276dea; font-weight: 600; text-decoration: underline; text-decoration-color: #ccc;">
|
||||
<button type="button" class="entry-link"
|
||||
onclick="openEntryOrdersModal('${escapeHtml(entry.entry_no)}')">
|
||||
${entry.order_quantity ?? '-'}
|
||||
</span>
|
||||
</button>
|
||||
</td>
|
||||
|
||||
<td>${escapeHtml(entry.region)}</td>
|
||||
@@ -2681,6 +2680,7 @@ function renderPaymentTable(list){
|
||||
|
||||
|
||||
|
||||
|
||||
function cycleToggle(btn) {
|
||||
// वर्तमान position घेऊन पुढचा स्टेट कॅल्क्युलेट करा
|
||||
let pos = parseInt(btn.dataset.pos || '0', 10); // 0 = unpaid, 1 = pending, 2 = paid
|
||||
|
||||
@@ -1,548 +1,96 @@
|
||||
@extends('admin.layouts.app')
|
||||
|
||||
@section('page-title', 'Chat Support Dashboard')
|
||||
@section('page-title', 'Chat Support')
|
||||
|
||||
@section('content')
|
||||
<style>
|
||||
:root {
|
||||
--primary-gradient: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
--success-gradient: linear-gradient(135deg, #11998e 0%, #38ef7d 100%);
|
||||
--danger-gradient: linear-gradient(135deg, #ff416c 0%, #ff4b2b 100%);
|
||||
--warning-gradient: linear-gradient(135deg, #f7971e 0%, #ffd200 100%);
|
||||
--info-gradient: linear-gradient(135deg, #56ccf2 0%, #2f80ed 100%);
|
||||
--card-shadow: 0 3px 10px rgba(0,0,0,0.05);
|
||||
--card-shadow-hover: 0 6px 16px rgba(0,0,0,0.10);
|
||||
--border-radius: 8px;
|
||||
--transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
<div class="container py-4">
|
||||
|
||||
.chat-dashboard {
|
||||
min-height: 100vh;
|
||||
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
|
||||
padding: 0.75rem;
|
||||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
|
||||
}
|
||||
<h2 class="mb-4 fw-bold">Customer Support Chat</h2>
|
||||
|
||||
.dashboard-header {
|
||||
text-align: center;
|
||||
margin-bottom: 1rem;
|
||||
position: relative;
|
||||
}
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body p-0">
|
||||
|
||||
.dashboard-title {
|
||||
font-size: clamp(1.4rem, 2.5vw, 2rem);
|
||||
font-weight: 800;
|
||||
background: var(--primary-gradient);
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
margin: 0 0 0.4rem 0;
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
@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">
|
||||
|
||||
.dashboard-title::before {
|
||||
content: '💬';
|
||||
position: absolute;
|
||||
left: -1.6rem;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
font-size: 1.3rem;
|
||||
animation: bounce 2s infinite;
|
||||
}
|
||||
@foreach($tickets as $ticket)
|
||||
@php
|
||||
// Get last message
|
||||
$lastMsg = $ticket->messages()->latest()->first();
|
||||
@endphp
|
||||
|
||||
@keyframes bounce {
|
||||
0%, 20%, 50%, 80%, 100% { transform: translateY(-50%) translateY(0); }
|
||||
40% { transform: translateY(-50%) translateY(-5px); }
|
||||
60% { transform: translateY(-50%) translateY(-2px); }
|
||||
}
|
||||
<li class="list-group-item py-3">
|
||||
|
||||
.dashboard-subtitle {
|
||||
color: #64748b;
|
||||
font-size: 0.8rem;
|
||||
max-width: 380px;
|
||||
margin: 0 auto;
|
||||
line-height: 1.3;
|
||||
}
|
||||
<div class="d-flex align-items-center justify-content-between">
|
||||
|
||||
/* 🔔 GLOBAL NEW MESSAGE COUNTER */
|
||||
.global-notify {
|
||||
margin: 0 auto 0.75rem auto;
|
||||
max-width: 320px;
|
||||
padding: 0.35rem 0.7rem;
|
||||
border-radius: 999px;
|
||||
background: rgba(15,23,42,0.03);
|
||||
border: 1px dashed #cbd5f5;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.4rem;
|
||||
font-size: 0.78rem;
|
||||
color: #1e293b;
|
||||
}
|
||||
<!-- Left side: User info + last message -->
|
||||
<div class="d-flex align-items-center gap-3">
|
||||
|
||||
.global-notify-badge {
|
||||
background: #ef4444;
|
||||
color: #fff;
|
||||
min-width: 18px;
|
||||
padding: 0 0.35rem;
|
||||
height: 18px;
|
||||
border-radius: 999px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 0.7rem;
|
||||
font-weight: 700;
|
||||
box-shadow: 0 0 0 2px rgba(254, 226, 226, 0.8);
|
||||
}
|
||||
|
||||
.global-notify.d-none {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tickets-container {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
backdrop-filter: blur(12px);
|
||||
border-radius: var(--border-radius);
|
||||
padding: 0.75rem 0.9rem;
|
||||
box-shadow: var(--card-shadow);
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
/* max-height / overflow काढले, जेणेकरून बाहेरचा page scroll वापरला जाईल */
|
||||
display: block;
|
||||
}
|
||||
|
||||
.tickets-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 0.6rem;
|
||||
padding-bottom: 0.45rem;
|
||||
border-bottom: 1px solid #f1f5f9;
|
||||
}
|
||||
|
||||
.tickets-title {
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
color: #1e293b;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.3rem;
|
||||
}
|
||||
|
||||
.tickets-count {
|
||||
background: var(--primary-gradient);
|
||||
color: white;
|
||||
padding: 0.1rem 0.45rem;
|
||||
border-radius: 12px;
|
||||
font-size: 0.7rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.ticket-item {
|
||||
background: white;
|
||||
border-radius: 6px;
|
||||
padding: 0.4rem 0.6rem;
|
||||
margin-bottom: 0.3rem;
|
||||
border: 1px solid #e5e7eb;
|
||||
transition: var(--transition);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.ticket-item::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 2px;
|
||||
background: var(--primary-gradient);
|
||||
transform: scaleY(0);
|
||||
transition: var(--transition);
|
||||
}
|
||||
|
||||
.ticket-item:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: var(--card-shadow-hover);
|
||||
border-color: rgba(102, 126, 234, 0.35);
|
||||
}
|
||||
|
||||
.ticket-item:hover::before {
|
||||
transform: scaleY(1);
|
||||
}
|
||||
|
||||
.ticket-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.4rem;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.ticket-avatar {
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
border-radius: 6px;
|
||||
background: var(--info-gradient);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
color: white;
|
||||
flex-shrink: 0;
|
||||
box-shadow: 0 3px 8px rgba(86, 204, 242, 0.25);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.ticket-avatar::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.3), transparent);
|
||||
transition: left 0.4s;
|
||||
}
|
||||
|
||||
.ticket-item:hover .ticket-avatar::after {
|
||||
left: 100%;
|
||||
}
|
||||
|
||||
.ticket-content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.ticket-name {
|
||||
font-size: 0.8rem;
|
||||
font-weight: 700;
|
||||
color: #1e293b;
|
||||
margin: 0 0 0.08rem 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.unread-count {
|
||||
background: #ef4444;
|
||||
color: white;
|
||||
min-width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 999px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 0.6rem;
|
||||
font-weight: 700;
|
||||
box-shadow: 0 2px 6px rgba(239, 68, 68, 0.4);
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { transform: scale(1); }
|
||||
50% { transform: scale(1.05); }
|
||||
}
|
||||
|
||||
.ticket-preview {
|
||||
color: #64748b;
|
||||
font-size: 0.7rem;
|
||||
line-height: 1.25;
|
||||
margin-bottom: 0.1rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
max-height: 1.8em;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.ticket-time {
|
||||
font-size: 0.65rem;
|
||||
color: #94a3b8;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.18rem;
|
||||
}
|
||||
|
||||
.ticket-time svg {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.ticket-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding-top: 0.3rem;
|
||||
border-top: 1px dashed #e5e7eb;
|
||||
margin-top: 0.2rem;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
padding: 0.14rem 0.45rem;
|
||||
border-radius: 999px;
|
||||
font-size: 0.6rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.2px;
|
||||
}
|
||||
|
||||
.status-open { background: var(--success-gradient); color: white; }
|
||||
.status-closed{ background: var(--danger-gradient); color: white; }
|
||||
|
||||
.chat-btn {
|
||||
background: var(--primary-gradient);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0.25rem 0.6rem;
|
||||
border-radius: 999px;
|
||||
font-weight: 600;
|
||||
font-size: 0.7rem;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
text-decoration: none;
|
||||
transition: var(--transition);
|
||||
box-shadow: 0 3px 10px rgba(102, 126, 234, 0.25);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.chat-btn:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 6px 16px rgba(102, 126, 234, 0.35);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.chat-btn::after {
|
||||
content: '→';
|
||||
transition: margin-left 0.25s ease;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.chat-btn:hover::after {
|
||||
margin-left: 0.18rem;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 1.6rem 1rem;
|
||||
background: rgba(255, 255, 255, 0.6);
|
||||
border-radius: var(--border-radius);
|
||||
border: 2px dashed #e2e8f0;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
font-size: 2.8rem;
|
||||
margin-bottom: 0.9rem;
|
||||
display: block;
|
||||
animation: float 3s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes float {
|
||||
0%, 100% { transform: translateY(0px); }
|
||||
50% { transform: translateY(-8px); }
|
||||
}
|
||||
|
||||
.empty-title {
|
||||
font-size: 1.05rem;
|
||||
font-weight: 700;
|
||||
color: #1e293b;
|
||||
margin-bottom: 0.4rem;
|
||||
}
|
||||
|
||||
.empty-subtitle {
|
||||
color: #64748b;
|
||||
font-size: 0.85rem;
|
||||
margin-bottom: 1rem;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.ticket-id {
|
||||
background: rgba(102, 126, 234, 0.08);
|
||||
color: #667eea;
|
||||
padding: 0.12rem 0.5rem;
|
||||
border-radius: 999px;
|
||||
font-size: 0.65rem;
|
||||
font-weight: 600;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.18rem;
|
||||
}
|
||||
|
||||
.new-message-dot {
|
||||
position: absolute;
|
||||
top: 0.45rem;
|
||||
right: 0.5rem;
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
background: #ef4444;
|
||||
border-radius: 50%;
|
||||
border: 2px solid white;
|
||||
box-shadow: 0 0 0 2px rgba(239, 68, 68, 0.3);
|
||||
animation: blink 1.5s infinite;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
@keyframes blink {
|
||||
0%, 100% { opacity: 1; transform: scale(1); }
|
||||
50% { opacity: 0.5; transform: scale(1.1); }
|
||||
}
|
||||
|
||||
/* इथे आता inner scroll नाही */
|
||||
.tickets-list {
|
||||
/* flex: 1; काढला, overflow काढला, parent + body scroll वापरेल */
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
.tickets-list::-webkit-scrollbar {
|
||||
width: 3px;
|
||||
}
|
||||
|
||||
.tickets-list::-webkit-scrollbar-track {
|
||||
background: #f1f5f9;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.tickets-list::-webkit-scrollbar-thumb {
|
||||
background: var(--primary-gradient);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.chat-dashboard {
|
||||
padding: 0.6rem;
|
||||
}
|
||||
|
||||
.tickets-container {
|
||||
/* max-height काढलेले, mobile वरही outer scroll */
|
||||
}
|
||||
|
||||
.ticket-header {
|
||||
flex-direction: column;
|
||||
gap: 0.35rem;
|
||||
}
|
||||
|
||||
.ticket-footer {
|
||||
flex-direction: column;
|
||||
gap: 0.35rem;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.chat-btn {
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="chat-dashboard">
|
||||
<!-- Header -->
|
||||
<div class="dashboard-header">
|
||||
<h1 class="dashboard-title">Live Chat Dashboard</h1>
|
||||
<p class="dashboard-subtitle">
|
||||
Monitor customer conversations with real-time updates
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- 🔔 GLOBAL NEW MESSAGES NOTIFICATION -->
|
||||
<div id="globalNewMessageBox" class="global-notify d-none">
|
||||
<span>New messages:</span>
|
||||
<span id="globalNewMessageCount" class="global-notify-badge">0</span>
|
||||
</div>
|
||||
|
||||
<!-- Tickets Container -->
|
||||
<div class="tickets-container">
|
||||
<div class="tickets-header">
|
||||
<div>
|
||||
<h2 class="tickets-title">
|
||||
📋 Active Conversations
|
||||
<span class="tickets-count">{{ $tickets->count() }}</span>
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if($tickets->count() === 0)
|
||||
<!-- Empty State -->
|
||||
<div class="empty-state">
|
||||
<div class="empty-icon">💬</div>
|
||||
<h3 class="empty-title">No Active Conversations</h3>
|
||||
<p class="empty-subtitle">
|
||||
Customer conversations will appear here with real-time notifications.
|
||||
</p>
|
||||
<div class="ticket-id">Ready for support requests</div>
|
||||
</div>
|
||||
@else
|
||||
<!-- Tickets List -->
|
||||
<div class="tickets-list">
|
||||
@foreach($tickets as $ticket)
|
||||
<div class="ticket-item" data-ticket-id="{{ $ticket->id }}">
|
||||
@if($ticket->unread_count > 0)
|
||||
<div class="new-message-dot"></div>
|
||||
@endif
|
||||
|
||||
<div class="ticket-header">
|
||||
<div class="ticket-avatar">
|
||||
{{ strtoupper(substr($ticket->user->customer_name ?? $ticket->user->name, 0, 1)) }}
|
||||
</div>
|
||||
<div class="ticket-content">
|
||||
<div class="ticket-name">
|
||||
{{ $ticket->user->customer_name ?? $ticket->user->name }}
|
||||
@if($ticket->unread_count > 0)
|
||||
<span id="badge-{{ $ticket->id }}" class="unread-count">
|
||||
{{ $ticket->unread_count }}
|
||||
</span>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@php
|
||||
$lastMsg = $ticket->messages()->latest()->first();
|
||||
@endphp
|
||||
|
||||
<div class="ticket-preview">
|
||||
@if($lastMsg)
|
||||
@if($lastMsg->message)
|
||||
{{ Str::limit($lastMsg->message, 45) }}
|
||||
@elseif(Str::startsWith($lastMsg->file_type, 'image'))
|
||||
📷 Photo shared
|
||||
@else
|
||||
📎 File attached
|
||||
@endif
|
||||
@else
|
||||
<em>Conversation started</em>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@if($lastMsg)
|
||||
<div class="ticket-time">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path d="M8 3.5a.5.5 0 0 0-1 0V9a.5.5 0 0 0 .252.434l3.5 2a.5.5 0 0 0 .496-.868L8 8.71V3.5z"/>
|
||||
<path d="M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16zm7-8A7 7 0 1 1 1 8a7 7 0 0 1 14 0z"/>
|
||||
</svg>
|
||||
{{ $lastMsg->created_at->diffForHumans() }}
|
||||
<!-- 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>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ticket-footer">
|
||||
<span class="status-badge status-{{ $ticket->status }}">
|
||||
{{ ucfirst($ticket->status) }}
|
||||
</span>
|
||||
<a href="{{ route('admin.chat.open', $ticket->id) }}" class="chat-btn">
|
||||
Open Chat
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
<div>
|
||||
<!-- 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>
|
||||
|
||||
@endsection
|
||||
|
||||
@section('scripts')
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user