Compare commits
97 Commits
6608caf61d
...
qa
| Author | SHA256 | Date | |
|---|---|---|---|
|
|
9a6ca49ad7 | ||
|
|
bf2689e62d | ||
|
|
c25b468c77 | ||
|
|
5d8a746876 | ||
|
|
bb2a361a97 | ||
|
|
6b5876e08f | ||
|
|
43b1a64911 | ||
|
|
ff4c006ca4 | ||
|
|
d5e9113820 | ||
|
|
bddbcf5c5f | ||
|
|
0c51ed1489 | ||
|
|
9cc6959396 | ||
|
|
c11467068c | ||
|
|
599023166a | ||
|
|
e188780329 | ||
|
|
338425535e | ||
|
|
f7856a6755 | ||
|
|
8f95091673 | ||
|
|
7362ef6bdc | ||
|
|
e872b83ea3 | ||
|
|
6ccf2cf84e | ||
|
|
4637f0b189 | ||
|
|
952dd7eddd | ||
|
|
451be1a533 | ||
|
|
cd9a786ef4 | ||
|
|
a6dd919d3f | ||
|
|
e0a8a5c69c | ||
|
|
7fa03688aa | ||
|
|
1885d3beef | ||
|
|
72a81fa111 | ||
|
|
044bfe5563 | ||
|
|
8ca8f05b93 | ||
|
|
2741129740 | ||
|
|
1bce2be826 | ||
|
|
ea2532efc8 | ||
|
|
ccce02f43e | ||
|
|
cdb6cab57d | ||
|
|
3941b06355 | ||
|
|
d2730e78f6 | ||
|
|
80c6e42e0c | ||
|
|
e455c271c4 | ||
|
|
48f7ab82ff | ||
|
|
c4097ecbde | ||
|
|
8a0d122e2c | ||
|
|
7ef28e06ae | ||
|
|
752f5ee873 | ||
|
|
84bf42f992 | ||
|
|
fc9a401a8c | ||
|
|
3590e8f873 | ||
|
|
f6fb304b7a | ||
|
|
6b41a447bb | ||
|
|
5dc9fc7db4 | ||
|
|
1aad6b231e | ||
|
|
0a65d5f596 | ||
|
|
0a1d0a9c55 | ||
|
|
409a854d7b | ||
|
|
4dab96b8d1 | ||
|
|
e7fef314fc | ||
|
|
5114357ff2 | ||
|
|
0afcb23511 | ||
|
|
340c2b2132 | ||
|
|
44b8299b0e | ||
|
|
9b8c50fcec | ||
|
|
7a814dff1d | ||
|
|
f4730a81d8 | ||
|
|
3b24ee860a | ||
|
|
2dcd9fe332 | ||
|
|
922539844d | ||
|
|
3845972c5c | ||
|
|
64d8939208 | ||
|
|
ec2a0baceb | ||
|
|
68bfd180ed | ||
|
|
aa616fcf61 | ||
|
|
178fbb224c | ||
|
|
97db70c40e | ||
|
|
04b00c9db8 | ||
|
|
bebe0711f4 | ||
|
|
a14fe614e5 | ||
|
|
4d44e7df25 | ||
|
|
6e1ae8f380 | ||
|
|
837f4fe566 | ||
|
|
63daef6a92 | ||
|
|
56a17cf1e0 | ||
|
|
22be272067 | ||
|
|
df89031d36 | ||
|
|
8f6e30554b | ||
|
|
ca28409689 | ||
|
|
cfd128cf9b | ||
|
|
8b64515689 | ||
|
|
2703eff60f | ||
|
|
59574fd664 | ||
|
|
2f7eaf88a2 | ||
|
|
e071a082e0 | ||
|
|
b495a58d64 | ||
|
|
2c5d19779b | ||
|
|
5d29c8accb | ||
|
|
82bfcc5f33 |
14
.env.example
14
.env.example
@@ -1,6 +1,6 @@
|
|||||||
APP_NAME=Laravel
|
APP_NAME=Laravel
|
||||||
APP_ENV=local
|
APP_ENV=local
|
||||||
APP_KEY=
|
APP_KEY=base64:/ulhRgiCOFjZV6xUDkXLfiR9X8iFRZ4QIiX3UJbdwY4=
|
||||||
APP_DEBUG=true
|
APP_DEBUG=true
|
||||||
APP_URL=http://localhost
|
APP_URL=http://localhost
|
||||||
|
|
||||||
@@ -20,12 +20,12 @@ LOG_STACK=single
|
|||||||
LOG_DEPRECATIONS_CHANNEL=null
|
LOG_DEPRECATIONS_CHANNEL=null
|
||||||
LOG_LEVEL=debug
|
LOG_LEVEL=debug
|
||||||
|
|
||||||
DB_CONNECTION=sqlite
|
DB_CONNECTION=mysql
|
||||||
# DB_HOST=127.0.0.1
|
DB_HOST=127.0.0.1
|
||||||
# DB_PORT=3306
|
DB_PORT=3306
|
||||||
# DB_DATABASE=laravel
|
DB_DATABASE=kent_logistics6
|
||||||
# DB_USERNAME=root
|
DB_USERNAME=root
|
||||||
# DB_PASSWORD=
|
DB_PASSWORD=
|
||||||
|
|
||||||
SESSION_DRIVER=database
|
SESSION_DRIVER=database
|
||||||
SESSION_LIFETIME=120
|
SESSION_LIFETIME=120
|
||||||
|
|||||||
101
app/Events/NewChatMessage.php
Normal file
101
app/Events/NewChatMessage.php
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Events;
|
||||||
|
|
||||||
|
use App\Models\ChatMessage;
|
||||||
|
use Illuminate\Broadcasting\PrivateChannel;
|
||||||
|
use Illuminate\Contracts\Broadcasting\ShouldBroadcastNow;
|
||||||
|
use Illuminate\Queue\SerializesModels;
|
||||||
|
|
||||||
|
class NewChatMessage implements ShouldBroadcastNow
|
||||||
|
{
|
||||||
|
use SerializesModels;
|
||||||
|
|
||||||
|
public $message;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new event instance.
|
||||||
|
*/
|
||||||
|
public function __construct(ChatMessage $message)
|
||||||
|
{
|
||||||
|
// Also load sender polymorphic relationship
|
||||||
|
$message->load('sender');
|
||||||
|
|
||||||
|
$this->message = $message;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The channel the event should broadcast on.
|
||||||
|
*/
|
||||||
|
public function broadcastOn()
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
new PrivateChannel('ticket.' . $this->message->ticket_id),
|
||||||
|
new PrivateChannel('admin.chat') // 👈 ADD THIS
|
||||||
|
];
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data sent to frontend (Blade + Flutter)
|
||||||
|
*/
|
||||||
|
public function broadcastWith()
|
||||||
|
{
|
||||||
|
\Log::info('APP_URL USED IN EVENT', [
|
||||||
|
'url' => config('app.url'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
\Log::info("DEBUG: NewChatMessage broadcasting on channel ticket.".$this->message->ticket_id);
|
||||||
|
|
||||||
|
\Log::info("EVENT BROADCAST FIRED", [
|
||||||
|
'channel' => 'ticket.'.$this->message->ticket_id,
|
||||||
|
'sender_type' => $this->message->sender_type,
|
||||||
|
'sender_id' => $this->message->sender_id,
|
||||||
|
]);
|
||||||
|
|
||||||
|
|
||||||
|
return [
|
||||||
|
'id' => $this->message->id,
|
||||||
|
'ticket_id' => $this->message->ticket_id,
|
||||||
|
'sender_id' => $this->message->sender_id,
|
||||||
|
'sender_type' => $this->message->sender_type,
|
||||||
|
'message' => $this->message->message,
|
||||||
|
'client_id' => $this->message->client_id,
|
||||||
|
|
||||||
|
// ✅ relative path only
|
||||||
|
'file_path' => $this->message->file_path ?? null,
|
||||||
|
'file_type' => $this->message->file_type ?? null,
|
||||||
|
|
||||||
|
'sender' => [
|
||||||
|
'id' => $this->message->sender->id,
|
||||||
|
'name' => $this->getSenderName(),
|
||||||
|
'is_admin' => $this->message->sender_type === \App\Models\Admin::class,
|
||||||
|
],
|
||||||
|
|
||||||
|
'created_at' => $this->message->created_at->toDateTimeString(),
|
||||||
|
];
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper to extract sender name
|
||||||
|
*/
|
||||||
|
private function getSenderName()
|
||||||
|
{
|
||||||
|
$sender = $this->message->sender;
|
||||||
|
|
||||||
|
// User has customer_name (in your app)
|
||||||
|
if ($this->message->sender_type === \App\Models\User::class) {
|
||||||
|
return $sender->customer_name ?? $sender->name ?? "User";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Admin model has ->name
|
||||||
|
return $sender->name ?? "Admin";
|
||||||
|
}
|
||||||
|
|
||||||
|
public function broadcastAs()
|
||||||
|
{
|
||||||
|
return 'NewChatMessage';
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
63
app/Exports/InvoicesExport.php
Normal file
63
app/Exports/InvoicesExport.php
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Exports;
|
||||||
|
|
||||||
|
use Illuminate\Contracts\View\View;
|
||||||
|
use Maatwebsite\Excel\Concerns\FromView;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
|
class InvoicesExport implements FromView
|
||||||
|
{
|
||||||
|
protected $request;
|
||||||
|
|
||||||
|
public function __construct(Request $request)
|
||||||
|
{
|
||||||
|
$this->request = $request;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function view(): View
|
||||||
|
{
|
||||||
|
$request = $this->request;
|
||||||
|
|
||||||
|
$invoices = DB::table('invoices')
|
||||||
|
->leftJoin('containers', 'containers.id', '=', 'invoices.container_id')
|
||||||
|
->leftJoin('mark_list', 'mark_list.mark_no', '=', 'invoices.mark_no')
|
||||||
|
->select(
|
||||||
|
'invoices.invoice_number',
|
||||||
|
'invoices.invoice_date',
|
||||||
|
'invoices.mark_no',
|
||||||
|
'containers.container_number',
|
||||||
|
'containers.container_date',
|
||||||
|
DB::raw('COALESCE(invoices.company_name, mark_list.company_name) as company_name'),
|
||||||
|
DB::raw('COALESCE(invoices.customer_name, mark_list.customer_name) as customer_name'),
|
||||||
|
'invoices.final_amount',
|
||||||
|
'invoices.final_amount_with_gst',
|
||||||
|
'invoices.status as invoice_status'
|
||||||
|
)
|
||||||
|
->when($request->filled('search'), function ($q) use ($request) {
|
||||||
|
$search = trim($request->search);
|
||||||
|
$q->where(function ($qq) use ($search) {
|
||||||
|
$qq->where('invoices.invoice_number', 'like', "%{$search}%")
|
||||||
|
->orWhere('invoices.mark_no', 'like', "%{$search}%")
|
||||||
|
->orWhere('containers.container_number', 'like', "%{$search}%")
|
||||||
|
->orWhere('mark_list.company_name', 'like', "%{$search}%")
|
||||||
|
->orWhere('mark_list.customer_name', 'like', "%{$search}%");
|
||||||
|
});
|
||||||
|
})
|
||||||
|
->when($request->filled('status'), function ($q) use ($request) {
|
||||||
|
$q->where('invoices.status', $request->status);
|
||||||
|
})
|
||||||
|
->when($request->filled('from_date'), function ($q) use ($request) {
|
||||||
|
$q->whereDate('invoices.invoice_date', '>=', $request->from_date);
|
||||||
|
})
|
||||||
|
->when($request->filled('to_date'), function ($q) use ($request) {
|
||||||
|
$q->whereDate('invoices.invoice_date', '<=', $request->to_date);
|
||||||
|
})
|
||||||
|
->orderByDesc('containers.container_date')
|
||||||
|
->orderByDesc('invoices.id')
|
||||||
|
->get();
|
||||||
|
|
||||||
|
return view('admin.pdf.invoices_excel', compact('invoices'));
|
||||||
|
}
|
||||||
|
}
|
||||||
137
app/Exports/OrdersExport.php
Normal file
137
app/Exports/OrdersExport.php
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Exports;
|
||||||
|
|
||||||
|
use App\Models\Order;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Maatwebsite\Excel\Concerns\FromCollection;
|
||||||
|
use Maatwebsite\Excel\Concerns\WithHeadings;
|
||||||
|
use Carbon\Carbon;
|
||||||
|
|
||||||
|
class OrdersExport implements FromCollection, WithHeadings
|
||||||
|
{
|
||||||
|
protected Request $request;
|
||||||
|
|
||||||
|
public function __construct(Request $request)
|
||||||
|
{
|
||||||
|
$this->request = $request;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function buildQuery()
|
||||||
|
{
|
||||||
|
$query = Order::query()->with([
|
||||||
|
'markList',
|
||||||
|
'invoice',
|
||||||
|
'shipments',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// SEARCH
|
||||||
|
if ($this->request->filled('search')) {
|
||||||
|
$search = trim($this->request->search);
|
||||||
|
|
||||||
|
$query->where(function ($q) use ($search) {
|
||||||
|
$q->where('orders.order_id', 'like', "%{$search}%")
|
||||||
|
->orWhereHas('markList', function ($q2) use ($search) {
|
||||||
|
$q2->where('company_name', 'like', "%{$search}%")
|
||||||
|
->orWhere('customer_id', 'like', "%{$search}%")
|
||||||
|
->orWhere('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) {
|
||||||
|
// ✅ FIXED
|
||||||
|
$q4->where('shipments.shipment_id', 'like', "%{$search}%");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// INVOICE STATUS
|
||||||
|
// INVOICE STATUS (FIXED)
|
||||||
|
if ($this->request->filled('status')) {
|
||||||
|
$query->where(function ($q) {
|
||||||
|
$q->whereHas('invoice', function ($q2) {
|
||||||
|
$q2->where('status', $this->request->status);
|
||||||
|
})
|
||||||
|
->orWhereDoesntHave('invoice');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// SHIPMENT STATUS (FIXED)
|
||||||
|
if ($this->request->filled('shipment')) {
|
||||||
|
$query->where(function ($q) {
|
||||||
|
$q->whereHas('shipments', function ($q2) {
|
||||||
|
$q2->where('status', $this->request->shipment);
|
||||||
|
})
|
||||||
|
->orWhereDoesntHave('shipments');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// DATE RANGE
|
||||||
|
if ($this->request->filled('from_date')) {
|
||||||
|
$query->whereDate('orders.created_at', '>=', $this->request->from_date);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->request->filled('to_date')) {
|
||||||
|
$query->whereDate('orders.created_at', '<=', $this->request->to_date);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $query->latest('orders.id');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function collection()
|
||||||
|
{
|
||||||
|
return $this->buildQuery()->get()->map(function ($order) {
|
||||||
|
|
||||||
|
$mark = $order->markList;
|
||||||
|
$invoice = $order->invoice;
|
||||||
|
$shipment = $order->shipments->first();
|
||||||
|
|
||||||
|
return [
|
||||||
|
'Order ID' => $order->order_id ?? '-',
|
||||||
|
'Shipment ID' => $shipment?->shipment_id ?? '-',
|
||||||
|
'Customer ID' => $mark?->customer_id ?? '-',
|
||||||
|
'Company' => $mark?->company_name ?? '-',
|
||||||
|
'Origin' => $mark?->origin ?? $order->origin ?? '-',
|
||||||
|
'Destination' => $mark?->destination ?? $order->destination ?? '-',
|
||||||
|
'Order Date' => $order->created_at
|
||||||
|
? $order->created_at->format('d-m-Y')
|
||||||
|
: '-',
|
||||||
|
'Invoice No' => $invoice?->invoice_number ?? '-',
|
||||||
|
'Invoice Date' => $invoice?->invoice_date
|
||||||
|
? Carbon::parse($invoice->invoice_date)->format('d-m-Y')
|
||||||
|
: '-',
|
||||||
|
'Amount' => $invoice?->final_amount !== null
|
||||||
|
? number_format($invoice->final_amount, 2)
|
||||||
|
: '0.00',
|
||||||
|
'Amount + GST' => $invoice?->final_amount_with_gst !== null
|
||||||
|
? number_format($invoice->final_amount_with_gst, 2)
|
||||||
|
: '0.00',
|
||||||
|
'Invoice Status' => ucfirst($invoice?->status ?? 'pending'),
|
||||||
|
'Shipment Status' => ucfirst(str_replace('_', ' ', $shipment?->status ?? 'pending')),
|
||||||
|
];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function headings(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'Order ID',
|
||||||
|
'Shipment ID',
|
||||||
|
'Customer ID',
|
||||||
|
'Company',
|
||||||
|
'Origin',
|
||||||
|
'Destination',
|
||||||
|
'Order Date',
|
||||||
|
'Invoice No',
|
||||||
|
'Invoice Date',
|
||||||
|
'Amount',
|
||||||
|
'Amount + GST',
|
||||||
|
'Invoice Status',
|
||||||
|
'Shipment Status',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
399
app/Http/Controllers/Admin/AdminAccountController.php
Normal file
399
app/Http/Controllers/Admin/AdminAccountController.php
Normal file
@@ -0,0 +1,399 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Admin;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\Entry;
|
||||||
|
use App\Models\Order;
|
||||||
|
use App\Models\Installment;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
|
class AdminAccountController extends Controller
|
||||||
|
{
|
||||||
|
public function updateEntry(Request $request)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$data = $request->validate([
|
||||||
|
'entry_no' => 'required|exists:entries,entry_no',
|
||||||
|
'description' => 'required|string|max:255',
|
||||||
|
'order_quantity' => 'required|numeric|min:0',
|
||||||
|
'region' => 'required|string|max:50',
|
||||||
|
'amount' => 'required|numeric|min:0',
|
||||||
|
//'payment_status' => 'required|string|max:50',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$entry = Entry::where('entry_no', $data['entry_no'])->first();
|
||||||
|
|
||||||
|
if (!$entry) {
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Entry not found.',
|
||||||
|
], 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
$entry->description = $data['description'];
|
||||||
|
$entry->order_quantity = $data['order_quantity'];
|
||||||
|
$entry->region = $data['region'];
|
||||||
|
$entry->amount = $data['amount'];
|
||||||
|
//$entry->payment_status = $data['payment_status'];
|
||||||
|
|
||||||
|
$entry->save();
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'success' => true,
|
||||||
|
'message' => 'Entry updated successfully.',
|
||||||
|
'entry' => $entry,
|
||||||
|
]);
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Server error: '.$e->getMessage(),
|
||||||
|
], 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 🚀 1. Get dashboard entries
|
||||||
|
*/
|
||||||
|
public function getDashboardData()
|
||||||
|
{
|
||||||
|
$entries = Entry::withCount('installments')
|
||||||
|
->orderBy('id', 'desc')
|
||||||
|
->get();
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'success' => true,
|
||||||
|
'entries' => $entries
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 🚀 2. Get available consolidated orders
|
||||||
|
*/
|
||||||
|
public function getAvailableOrders()
|
||||||
|
{
|
||||||
|
|
||||||
|
$orders = Order::whereDoesntHave('entries')
|
||||||
|
->orderBy('id', 'desc')
|
||||||
|
->get();
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'success' => true,
|
||||||
|
'orders' => $orders,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 🚀 3. Create new entry
|
||||||
|
*/
|
||||||
|
public function accountCreateOrder(Request $request)
|
||||||
|
{
|
||||||
|
$data = $request->validate([
|
||||||
|
'description' => 'required|string|max:255',
|
||||||
|
'region' => 'required|string|max:50',
|
||||||
|
'amount' => 'required|numeric|min:1',
|
||||||
|
'entry_date' => 'nullable|date',
|
||||||
|
'selected_orders' => 'nullable|array',
|
||||||
|
'selected_orders.*'=> 'integer|exists:orders,id',
|
||||||
|
]);
|
||||||
|
|
||||||
|
return DB::transaction(function () use ($data) {
|
||||||
|
|
||||||
|
$entryDate = $data['entry_date'] ?? now()->toDateString();
|
||||||
|
|
||||||
|
// Count selected consolidated orders
|
||||||
|
$orderQuantity = !empty($data['selected_orders'])
|
||||||
|
? count($data['selected_orders'])
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
// Generate entry No: PAY-2025-001
|
||||||
|
$prefix = 'PAY-' . date('Y') . '-';
|
||||||
|
$last = Entry::where('entry_no', 'like', $prefix . '%')
|
||||||
|
->orderBy('id', 'desc')
|
||||||
|
->first();
|
||||||
|
|
||||||
|
$next = $last
|
||||||
|
? intval(substr($last->entry_no, strrpos($last->entry_no, '-') + 1)) + 1
|
||||||
|
: 1;
|
||||||
|
|
||||||
|
$entryNo = $prefix . str_pad($next, 7, '0', STR_PAD_LEFT);
|
||||||
|
|
||||||
|
// Create entry
|
||||||
|
$entry = Entry::create([
|
||||||
|
'entry_no' => $entryNo,
|
||||||
|
'description' => $data['description'],
|
||||||
|
'region' => $data['region'],
|
||||||
|
'order_quantity' => $orderQuantity,
|
||||||
|
'amount' => $data['amount'],
|
||||||
|
'pending_amount' => $data['amount'],
|
||||||
|
'entry_date' => $entryDate,
|
||||||
|
'payment_status' => 'unpaid',
|
||||||
|
'toggle_pos' => 0,
|
||||||
|
'dispatch_status' => 'pending',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Attach consolidated orders
|
||||||
|
if (!empty($data['selected_orders'])) {
|
||||||
|
$entry->orders()->attach($data['selected_orders']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$entry->load('orders');
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'success' => true,
|
||||||
|
'message' => 'Entry Created Successfully',
|
||||||
|
'entry' => $entry
|
||||||
|
], 201);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 🚀 4. Toggle payment switch
|
||||||
|
*/
|
||||||
|
public function togglePayment(Request $request)
|
||||||
|
{
|
||||||
|
$request->validate([
|
||||||
|
'entry_no' => 'required|string|exists:entries,entry_no',
|
||||||
|
'toggle_pos' => 'required|integer|in:0,1,2',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$entry = Entry::where('entry_no', $request->entry_no)->firstOrFail();
|
||||||
|
|
||||||
|
$map = [
|
||||||
|
0 => 'unpaid',
|
||||||
|
1 => 'pending',
|
||||||
|
2 => 'paid'
|
||||||
|
];
|
||||||
|
|
||||||
|
$entry->update([
|
||||||
|
'toggle_pos' => $request->toggle_pos,
|
||||||
|
'payment_status' => $map[$request->toggle_pos],
|
||||||
|
]);
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'success' => true,
|
||||||
|
'entry' => $entry
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 🚀 5. Add Installment
|
||||||
|
*/
|
||||||
|
public function addInstallment(Request $request)
|
||||||
|
{
|
||||||
|
$data = $request->validate([
|
||||||
|
'entry_no' => 'required|exists:entries,entry_no',
|
||||||
|
'proc_date' => 'nullable|date',
|
||||||
|
'amount' => 'required|numeric|min:1',
|
||||||
|
'status' => 'required|string'
|
||||||
|
]);
|
||||||
|
|
||||||
|
return DB::transaction(function () use ($data) {
|
||||||
|
|
||||||
|
$entry = Entry::where('entry_no', $data['entry_no'])
|
||||||
|
->lockForUpdate()
|
||||||
|
->firstOrFail();
|
||||||
|
|
||||||
|
$amount = floatval($data['amount']);
|
||||||
|
|
||||||
|
if ($amount > $entry->pending_amount) {
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Installment cannot exceed pending amount.'
|
||||||
|
], 422);
|
||||||
|
}
|
||||||
|
|
||||||
|
$installment = Installment::create([
|
||||||
|
'entry_id' => $entry->id,
|
||||||
|
'proc_date' => $data['proc_date'] ?? now()->toDateString(),
|
||||||
|
'amount' => $amount,
|
||||||
|
'description'=> $entry->description,
|
||||||
|
'region' => $entry->region,
|
||||||
|
'status' => $data['status']
|
||||||
|
]);
|
||||||
|
|
||||||
|
$entry->pending_amount -= $amount;
|
||||||
|
|
||||||
|
if ($entry->pending_amount <= 0.001) {
|
||||||
|
$entry->pending_amount = 0;
|
||||||
|
$entry->dispatch_status = 'dispatched';
|
||||||
|
}
|
||||||
|
|
||||||
|
$entry->save();
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'success' => true,
|
||||||
|
'entry' => $entry,
|
||||||
|
'installment' => $installment
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 🚀 6. Update Installment Status
|
||||||
|
*/
|
||||||
|
public function updateInstallmentStatus(Request $request)
|
||||||
|
{
|
||||||
|
$data = $request->validate([
|
||||||
|
'installment_id' => 'required|exists:installments,id',
|
||||||
|
'status' => 'required|string|max:50'
|
||||||
|
]);
|
||||||
|
|
||||||
|
return DB::transaction(function () use ($data) {
|
||||||
|
|
||||||
|
$installment = Installment::lockForUpdate()->findOrFail($data['installment_id']);
|
||||||
|
|
||||||
|
$installment->status = $data['status'];
|
||||||
|
$installment->save();
|
||||||
|
|
||||||
|
$entry = Entry::lockForUpdate()->find($installment->entry_id);
|
||||||
|
|
||||||
|
// If ANY installment is not delivered — entry is NOT delivered
|
||||||
|
if ($entry->installments()->where('status', '!=', 'Delivered')->exists()) {
|
||||||
|
// entry still in progress
|
||||||
|
$entry->dispatch_status = 'pending';
|
||||||
|
} else {
|
||||||
|
// all installments delivered
|
||||||
|
$entry->dispatch_status = 'delivered';
|
||||||
|
}
|
||||||
|
|
||||||
|
$entry->save();
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'success' => true,
|
||||||
|
'message' => 'Installment updated successfully',
|
||||||
|
'installment' => $installment,
|
||||||
|
'entry' => $entry
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 🚀 6. Entry Details (installment history)
|
||||||
|
*/
|
||||||
|
public function getEntryDetails($entry_no)
|
||||||
|
{
|
||||||
|
$entry = Entry::with('installments')
|
||||||
|
->where('entry_no', $entry_no)
|
||||||
|
->firstOrFail();
|
||||||
|
|
||||||
|
$totalProcessed = $entry->amount - $entry->pending_amount;
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'success' => true,
|
||||||
|
'entry' => $entry,
|
||||||
|
'total_processed' => $totalProcessed,
|
||||||
|
'pending' => $entry->pending_amount,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
//--------------------------
|
||||||
|
//add order Entry
|
||||||
|
//--------------------------
|
||||||
|
public function addOrdersToEntry(Request $request)
|
||||||
|
{
|
||||||
|
$data = $request->validate([
|
||||||
|
'entry_no' => 'required|exists:entries,entry_no',
|
||||||
|
'order_ids' => 'required|array',
|
||||||
|
'order_ids.*' => 'integer|exists:orders,id',
|
||||||
|
]);
|
||||||
|
|
||||||
|
return DB::transaction(function () use ($data) {
|
||||||
|
$entry = Entry::where('entry_no', $data['entry_no'])->firstOrFail();
|
||||||
|
|
||||||
|
|
||||||
|
$entry->orders()->syncWithoutDetaching($data['order_ids']);
|
||||||
|
|
||||||
|
$entry->order_quantity = $entry->orders()->count();
|
||||||
|
$entry->save();
|
||||||
|
|
||||||
|
$entry->load('orders');
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'success' => true,
|
||||||
|
'message' => 'Orders added successfully.',
|
||||||
|
'entry' => $entry,
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public function getEntryOrders($entry_no)
|
||||||
|
{
|
||||||
|
$entry = Entry::where('entry_no', $entry_no)
|
||||||
|
->with('orders')
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if (!$entry) {
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Entry not found.',
|
||||||
|
], 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'success' => true,
|
||||||
|
'orders' => $entry->orders,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function removeOrderFromEntry(Request $request)
|
||||||
|
{
|
||||||
|
$data = $request->validate([
|
||||||
|
'entry_no' => 'required|exists:entries,entry_no',
|
||||||
|
'order_id' => 'required|integer|exists:orders,id',
|
||||||
|
]);
|
||||||
|
|
||||||
|
return DB::transaction(function () use ($data) {
|
||||||
|
$entry = Entry::where('entry_no', $data['entry_no'])->firstOrFail();
|
||||||
|
|
||||||
|
// order detach करा
|
||||||
|
$entry->orders()->detach($data['order_id']);
|
||||||
|
|
||||||
|
// इथे quantity auto update
|
||||||
|
$entry->order_quantity = $entry->orders()->count();
|
||||||
|
$entry->save();
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'success' => true,
|
||||||
|
'message' => 'Order removed successfully.',
|
||||||
|
'entry' => $entry,
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
public function deleteEntry(Request $request)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$data = $request->validate([
|
||||||
|
'entry_no' => 'required|exists:entries,entry_no',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$entry = Entry::where('entry_no', $data['entry_no'])->first();
|
||||||
|
|
||||||
|
if (!$entry) {
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Entry not found.',
|
||||||
|
], 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
$entry->delete();
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'success' => true,
|
||||||
|
'message' => 'Entry deleted successfully.',
|
||||||
|
]);
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Server error: '.$e->getMessage(),
|
||||||
|
], 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -5,50 +5,52 @@ namespace App\Http\Controllers\Admin;
|
|||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\Auth;
|
use Illuminate\Support\Facades\Auth;
|
||||||
use Illuminate\Support\Facades\Hash;
|
|
||||||
use App\Models\Admin;
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class AdminAuthController extends Controller
|
class AdminAuthController extends Controller
|
||||||
{
|
{
|
||||||
/**
|
|
||||||
* Show the admin login page
|
|
||||||
*/
|
|
||||||
public function showLoginForm()
|
public function showLoginForm()
|
||||||
{
|
{
|
||||||
return view('admin.login');
|
return view('admin.login');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle admin login
|
|
||||||
*/
|
|
||||||
public function login(Request $request)
|
public function login(Request $request)
|
||||||
{
|
{
|
||||||
$request->validate([
|
$request->validate([
|
||||||
'email' => 'required|email',
|
'login' => 'required',
|
||||||
'password' => 'required|string|min:6',
|
'password' => 'required|string|min:6',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Try to log in using the 'admin' guard
|
$loginInput = $request->input('login');
|
||||||
if (Auth::guard('admin')->attempt($request->only('email', 'password'))) {
|
|
||||||
return redirect()->route('admin.dashboard')->with('success', 'Welcome back, Admin!');
|
if (filter_var($loginInput, FILTER_VALIDATE_EMAIL)) {
|
||||||
|
$field = 'email';
|
||||||
|
} elseif (preg_match('/^EMP\d+$/i', $loginInput)) {
|
||||||
|
$field = 'employee_id';
|
||||||
|
} else {
|
||||||
|
$field = 'username';
|
||||||
}
|
}
|
||||||
|
|
||||||
return back()->withErrors(['email' => 'Invalid email or password.']);
|
$credentials = [
|
||||||
|
$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 back()->withErrors(['login' => 'Invalid login credentials.']);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Logout admin
|
|
||||||
*/
|
|
||||||
public function logout(Request $request)
|
public function logout(Request $request)
|
||||||
{
|
{
|
||||||
Auth::guard('admin')->logout();
|
Auth::guard('admin')->logout();
|
||||||
|
|
||||||
// Destroy the session completely
|
|
||||||
$request->session()->invalidate();
|
$request->session()->invalidate();
|
||||||
$request->session()->regenerateToken();
|
$request->session()->regenerateToken();
|
||||||
|
|
||||||
return redirect()->route('admin.login')->with('success', 'Logged out successfully.');
|
return redirect()->route('admin.login')->with('success', 'Logged out successfully.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
105
app/Http/Controllers/Admin/AdminChatController.php
Normal file
105
app/Http/Controllers/Admin/AdminChatController.php
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Admin;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use App\Models\SupportTicket;
|
||||||
|
use App\Models\ChatMessage;
|
||||||
|
use App\Events\NewChatMessage;
|
||||||
|
|
||||||
|
class AdminChatController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Page 1: List all active user chats
|
||||||
|
*/
|
||||||
|
public function index()
|
||||||
|
{
|
||||||
|
$tickets = SupportTicket::with('user')
|
||||||
|
->withCount([
|
||||||
|
'messages as unread_count' => function ($q) {
|
||||||
|
$q->where('sender_type', \App\Models\User::class)
|
||||||
|
->where('read_by_admin', false);
|
||||||
|
}
|
||||||
|
])
|
||||||
|
->orderBy('updated_at', 'desc')
|
||||||
|
->get();
|
||||||
|
|
||||||
|
return view('admin.chat_support', compact('tickets'));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Page 2: Open chat window for a specific user
|
||||||
|
*/
|
||||||
|
public function openChat($ticketId)
|
||||||
|
{
|
||||||
|
$ticket = SupportTicket::with('user')->findOrFail($ticketId);
|
||||||
|
|
||||||
|
// ✅ MARK USER MESSAGES AS READ FOR ADMIN
|
||||||
|
ChatMessage::where('ticket_id', $ticketId)
|
||||||
|
->where('sender_type', \App\Models\User::class)
|
||||||
|
->where('read_by_admin', false)
|
||||||
|
->update(['read_by_admin' => true]);
|
||||||
|
|
||||||
|
$messages = ChatMessage::where('ticket_id', $ticketId)
|
||||||
|
->orderBy('created_at', 'asc')
|
||||||
|
->with('sender')
|
||||||
|
->get();
|
||||||
|
|
||||||
|
return view('admin.chat_window', compact('ticket', 'messages'));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Admin sends a message to the user
|
||||||
|
*/
|
||||||
|
public function sendMessage(Request $request, $ticketId)
|
||||||
|
{
|
||||||
|
$request->validate([
|
||||||
|
'message' => 'nullable|string',
|
||||||
|
'file' => 'nullable|file|max:20480', // 20 MB
|
||||||
|
]);
|
||||||
|
|
||||||
|
$ticket = SupportTicket::findOrFail($ticketId);
|
||||||
|
$admin = auth('admin')->user();
|
||||||
|
|
||||||
|
$data = [
|
||||||
|
'ticket_id' => $ticketId,
|
||||||
|
'sender_id' => $admin->id,
|
||||||
|
'sender_type' => \App\Models\Admin::class,
|
||||||
|
'message' => $request->message,
|
||||||
|
|
||||||
|
'read_by_admin' => true,
|
||||||
|
'read_by_user' => false,
|
||||||
|
];
|
||||||
|
|
||||||
|
// File Upload
|
||||||
|
if ($request->hasFile('file')) {
|
||||||
|
$path = $request->file('file')->store('chat', 'public');
|
||||||
|
$data['file_path'] = $path;
|
||||||
|
$data['file_type'] = $request->file('file')->getMimeType();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save message
|
||||||
|
$message = ChatMessage::create($data);
|
||||||
|
$message->load('sender');
|
||||||
|
|
||||||
|
\Log::info("DEBUG: ChatController sendMessage called", [
|
||||||
|
'ticket_id' => $ticketId,
|
||||||
|
'payload' => $request->all()
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Broadcast real-time
|
||||||
|
broadcast(new NewChatMessage($message));
|
||||||
|
|
||||||
|
\Log::info("DEBUG: ChatController sendMessage called 79", [
|
||||||
|
'ticket_id' => $ticketId,
|
||||||
|
'payload' => $request->all()
|
||||||
|
]);
|
||||||
|
return response()->json([
|
||||||
|
'success' => true,
|
||||||
|
'message' => $message
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
162
app/Http/Controllers/Admin/AdminCustomerController.php
Normal file
162
app/Http/Controllers/Admin/AdminCustomerController.php
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Admin;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Support\Facades\Hash;
|
||||||
|
|
||||||
|
class AdminCustomerController extends Controller
|
||||||
|
{
|
||||||
|
|
||||||
|
|
||||||
|
// ---------------------------------------------------------
|
||||||
|
// LIST CUSTOMERS (with search + status filter)
|
||||||
|
// ---------------------------------------------------------
|
||||||
|
public function index(Request $request)
|
||||||
|
{
|
||||||
|
$search = $request->search;
|
||||||
|
$status = $request->status;
|
||||||
|
|
||||||
|
$query = User::with([
|
||||||
|
'marks',
|
||||||
|
'orders',
|
||||||
|
'invoices.installments' // 🔥 IMPORTANT
|
||||||
|
])->orderBy('id', 'desc');
|
||||||
|
|
||||||
|
if (!empty($search)) {
|
||||||
|
$query->where(function ($q) use ($search) {
|
||||||
|
$q->where('customer_name', 'like', "%$search%")
|
||||||
|
->orWhere('email', 'like', "%$search%")
|
||||||
|
->orWhere('mobile_no', 'like', "%$search%")
|
||||||
|
->orWhere('customer_id', 'like', "%$search%");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($status) && in_array($status, ['active', 'inactive'])) {
|
||||||
|
$query->where('status', $status);
|
||||||
|
}
|
||||||
|
|
||||||
|
$allCustomers = $query->get();
|
||||||
|
$customers = $query->paginate(10);
|
||||||
|
|
||||||
|
return view('admin.customers', compact(
|
||||||
|
'customers',
|
||||||
|
'allCustomers',
|
||||||
|
'search',
|
||||||
|
'status'
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ---------------------------------------------------------
|
||||||
|
// SHOW ADD CUSTOMER FORM
|
||||||
|
// ---------------------------------------------------------
|
||||||
|
public function create()
|
||||||
|
{
|
||||||
|
return view('admin.customers_add');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------
|
||||||
|
// STORE NEW CUSTOMER
|
||||||
|
// ---------------------------------------------------------
|
||||||
|
public function store(Request $request)
|
||||||
|
{
|
||||||
|
$request->validate([
|
||||||
|
'customer_name' => 'required|string|max:255',
|
||||||
|
'company_name' => 'nullable|string|max:255',
|
||||||
|
'designation' => 'nullable|string|max:255',
|
||||||
|
'email' => 'required|email|unique:users,email',
|
||||||
|
'mobile_no' => 'required|string|max:20',
|
||||||
|
'address' => 'nullable|string',
|
||||||
|
'pincode' => 'nullable|string|max:10',
|
||||||
|
'customer_type' => 'required|in:regular,premium',
|
||||||
|
'status' => 'required|in:active,inactive',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// AUTO GENERATE CUSTOMER ID
|
||||||
|
$year = date('Y');
|
||||||
|
$prefix = "CID-$year-";
|
||||||
|
|
||||||
|
$lastCustomer = User::whereYear('created_at', $year)
|
||||||
|
->orderBy('id', 'DESC')
|
||||||
|
->first();
|
||||||
|
|
||||||
|
$next = $lastCustomer ? intval(substr($lastCustomer->customer_id, -6)) + 1 : 1;
|
||||||
|
$customerId = $prefix . str_pad($next, 6, '0', STR_PAD_LEFT);
|
||||||
|
|
||||||
|
// CREATE CUSTOMER
|
||||||
|
User::create([
|
||||||
|
'customer_id' => $customerId,
|
||||||
|
'customer_name' => $request->customer_name,
|
||||||
|
'company_name' => $request->company_name,
|
||||||
|
'designation' => $request->designation,
|
||||||
|
'email' => $request->email,
|
||||||
|
'mobile_no' => $request->mobile_no,
|
||||||
|
'address' => $request->address,
|
||||||
|
'pincode' => $request->pincode,
|
||||||
|
'date' => date('Y-m-d'),
|
||||||
|
'customer_type' => $request->customer_type,
|
||||||
|
'status' => $request->status,
|
||||||
|
'password' => Hash::make('123456'), // DEFAULT PASSWORD
|
||||||
|
]);
|
||||||
|
|
||||||
|
return redirect()
|
||||||
|
->route('admin.customers.index')
|
||||||
|
->with('success', 'Customer added successfully!');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------
|
||||||
|
// VIEW CUSTOMER FULL DETAILS
|
||||||
|
// ---------------------------------------------------------
|
||||||
|
public function view($id)
|
||||||
|
{
|
||||||
|
$customer = User::with([
|
||||||
|
'marks',
|
||||||
|
'orders',
|
||||||
|
'invoices.installments'
|
||||||
|
])->findOrFail($id);
|
||||||
|
|
||||||
|
// Orders
|
||||||
|
$totalOrders = $customer->orders->count();
|
||||||
|
$totalOrderAmount = $customer->orders->sum('ttl_amount');
|
||||||
|
|
||||||
|
// Invoices (PAYABLE)
|
||||||
|
$totalPayable = $customer->invoices->sum('final_amount_with_gst');
|
||||||
|
|
||||||
|
// Paid via installments
|
||||||
|
$totalPaid = $customer->invoiceInstallments->sum('amount');
|
||||||
|
|
||||||
|
// Remaining
|
||||||
|
$totalRemaining = max($totalPayable - $totalPaid, 0);
|
||||||
|
|
||||||
|
return view('admin.customers_view', compact(
|
||||||
|
'customer',
|
||||||
|
'totalOrders',
|
||||||
|
'totalOrderAmount',
|
||||||
|
'totalPayable',
|
||||||
|
'totalPaid',
|
||||||
|
'totalRemaining'
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ---------------------------------------------------------
|
||||||
|
// TOGGLE STATUS ACTIVE / INACTIVE
|
||||||
|
// ---------------------------------------------------------
|
||||||
|
public function toggleStatus($id)
|
||||||
|
{
|
||||||
|
$customer = User::findOrFail($id);
|
||||||
|
|
||||||
|
$customer->status = $customer->status === 'active'
|
||||||
|
? 'inactive'
|
||||||
|
: 'active';
|
||||||
|
|
||||||
|
$customer->save();
|
||||||
|
|
||||||
|
return back()->with('success', 'Customer status updated.');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
453
app/Http/Controllers/Admin/AdminInvoiceController.php
Normal file
453
app/Http/Controllers/Admin/AdminInvoiceController.php
Normal file
@@ -0,0 +1,453 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Admin;
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
class AdminInvoiceController extends Controller
|
||||||
|
{
|
||||||
|
// -------------------------------------------------------------
|
||||||
|
// INVOICE LIST PAGE
|
||||||
|
// -------------------------------------------------------------
|
||||||
|
public function index(Request $request)
|
||||||
|
{
|
||||||
|
$query = Invoice::with(['items', 'customer', 'container']);
|
||||||
|
|
||||||
|
if ($request->filled('search')) {
|
||||||
|
$search = $request->search;
|
||||||
|
$query->where(function ($q) use ($search) {
|
||||||
|
$q->where('invoice_number', 'like', "%{$search}%")
|
||||||
|
->orWhere('customer_name', 'like', "%{$search}%");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($request->filled('status') && $request->status !== 'all') {
|
||||||
|
$query->where('status', $request->status);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($request->filled('start_date')) {
|
||||||
|
$query->whereDate('invoice_date', '>=', $request->start_date);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($request->filled('end_date')) {
|
||||||
|
$query->whereDate('invoice_date', '<=', $request->end_date);
|
||||||
|
}
|
||||||
|
|
||||||
|
$invoices = $query->latest()->get();
|
||||||
|
|
||||||
|
return view('admin.invoice', compact('invoices'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------
|
||||||
|
// POPUP VIEW
|
||||||
|
// -------------------------------------------------------------
|
||||||
|
public function popup($id)
|
||||||
|
{
|
||||||
|
$invoice = Invoice::with([
|
||||||
|
'items',
|
||||||
|
'chargeGroups.items',
|
||||||
|
])->findOrFail($id);
|
||||||
|
|
||||||
|
$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'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------
|
||||||
|
// 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$shipment = null;
|
||||||
|
|
||||||
|
$groupedItemIds = $invoice->chargeGroups
|
||||||
|
->flatMap(function ($group) {
|
||||||
|
return $group->items->pluck('invoice_item_id');
|
||||||
|
})
|
||||||
|
->unique()
|
||||||
|
->values()
|
||||||
|
->toArray();
|
||||||
|
|
||||||
|
return view('admin.invoice_edit', compact('invoice', 'shipment', 'groupedItemIds'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------
|
||||||
|
// UPDATE INVOICE (HEADER ONLY)
|
||||||
|
// -------------------------------------------------------------
|
||||||
|
public function update(Request $request, $id)
|
||||||
|
{
|
||||||
|
Log::info('🟡 Invoice Update Request Received', [
|
||||||
|
'invoice_id' => $id,
|
||||||
|
'request' => $request->all(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$invoice = Invoice::findOrFail($id);
|
||||||
|
|
||||||
|
$data = $request->validate([
|
||||||
|
'invoice_date' => 'required|date',
|
||||||
|
'due_date' => 'required|date|after_or_equal:invoice_date',
|
||||||
|
'status' => 'required|in:pending,paying,paid,overdue',
|
||||||
|
'notes' => 'nullable|string',
|
||||||
|
]);
|
||||||
|
|
||||||
|
Log::info('✅ Validated Invoice Header Update Data', $data);
|
||||||
|
|
||||||
|
$invoice->update($data);
|
||||||
|
$invoice->refresh();
|
||||||
|
|
||||||
|
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,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->generateInvoicePDF($invoice);
|
||||||
|
|
||||||
|
return redirect()
|
||||||
|
->route('admin.invoices.edit', $invoice->id)
|
||||||
|
->with('success', 'Invoice updated & PDF generated successfully.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------
|
||||||
|
// UPDATE INVOICE ITEMS (फक्त items save)
|
||||||
|
// -------------------------------------------------------------
|
||||||
|
public function updateItems(Request $request, Invoice $invoice)
|
||||||
|
{
|
||||||
|
Log::info('🟡 Invoice Items Update Request', [
|
||||||
|
'invoice_id' => $invoice->id,
|
||||||
|
'payload' => $request->all(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$data = $request->validate([
|
||||||
|
'items' => ['required', 'array'],
|
||||||
|
'items.*.price' => ['required', 'numeric', 'min:0'],
|
||||||
|
'items.*.ttl_amount' => ['required', 'numeric', 'min:0'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
foreach ($data['items'] as $itemId => $itemData) {
|
||||||
|
$item = InvoiceItem::where('id', $itemId)
|
||||||
|
->where('invoice_id', $invoice->id)
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if (!$item) {
|
||||||
|
Log::warning('Invoice item not found or mismatched invoice', [
|
||||||
|
'invoice_id' => $invoice->id,
|
||||||
|
'item_id' => $itemId,
|
||||||
|
]);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$item->price = $itemData['price'];
|
||||||
|
$item->ttl_amount = $itemData['ttl_amount'];
|
||||||
|
$item->save();
|
||||||
|
}
|
||||||
|
|
||||||
|
Log::info('✅ Invoice items updated (no totals recalculation)', [
|
||||||
|
'invoice_id' => $invoice->id,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return back()->with('success', 'Invoice items updated successfully.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------
|
||||||
|
// PDF GENERATION
|
||||||
|
// -------------------------------------------------------------
|
||||||
|
public function generateInvoicePDF($invoice)
|
||||||
|
{
|
||||||
|
$invoice->load(['items', 'customer', 'container']);
|
||||||
|
$shipment = null;
|
||||||
|
|
||||||
|
$fileName = 'invoice-' . $invoice->invoice_number . '.pdf';
|
||||||
|
$folder = public_path('invoices/');
|
||||||
|
|
||||||
|
if (!file_exists($folder)) {
|
||||||
|
mkdir($folder, 0777, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
$filePath = $folder . $fileName;
|
||||||
|
|
||||||
|
if (file_exists($filePath)) {
|
||||||
|
unlink($filePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
$mpdf = new Mpdf([
|
||||||
|
'mode' => 'utf-8',
|
||||||
|
'format' => 'A4',
|
||||||
|
'default_font' => 'sans-serif',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$html = view('admin.pdf.invoice', [
|
||||||
|
'invoice' => $invoice,
|
||||||
|
'shipment' => $shipment,
|
||||||
|
])->render();
|
||||||
|
|
||||||
|
$mpdf->WriteHTML($html);
|
||||||
|
$mpdf->Output($filePath, 'F');
|
||||||
|
|
||||||
|
$invoice->update(['pdf_path' => 'invoices/' . $fileName]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------
|
||||||
|
// INSTALLMENTS (ADD)
|
||||||
|
// -------------------------------------------------------------
|
||||||
|
public function storeInstallment(Request $request, $invoice_id)
|
||||||
|
{
|
||||||
|
$request->validate([
|
||||||
|
'installment_date' => 'required|date',
|
||||||
|
'payment_method' => 'required|string',
|
||||||
|
'reference_no' => 'nullable|string',
|
||||||
|
'amount' => 'required|numeric|min:1',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$invoice = Invoice::findOrFail($invoice_id);
|
||||||
|
|
||||||
|
$grandTotal = $invoice->grand_total_with_charges ?? 0;
|
||||||
|
|
||||||
|
$paidTotal = $invoice->installments()->sum('amount');
|
||||||
|
$remaining = $grandTotal - $paidTotal;
|
||||||
|
|
||||||
|
if ($request->amount > $remaining) {
|
||||||
|
return response()->json([
|
||||||
|
'status' => 'error',
|
||||||
|
'message' => 'Installment amount exceeds remaining balance.',
|
||||||
|
], 422);
|
||||||
|
}
|
||||||
|
|
||||||
|
$installment = InvoiceInstallment::create([
|
||||||
|
'invoice_id' => $invoice_id,
|
||||||
|
'installment_date' => $request->installment_date,
|
||||||
|
'payment_method' => $request->payment_method,
|
||||||
|
'reference_no' => $request->reference_no,
|
||||||
|
'amount' => $request->amount,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$newPaid = $paidTotal + $request->amount;
|
||||||
|
$remaining = max(0, $grandTotal - $newPaid);
|
||||||
|
|
||||||
|
if ($newPaid >= $grandTotal && $grandTotal > 0) {
|
||||||
|
$invoice->update(['status' => 'paid']);
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------
|
||||||
|
// INSTALLMENTS (DELETE)
|
||||||
|
// -------------------------------------------------------------
|
||||||
|
public function deleteInstallment($id)
|
||||||
|
{
|
||||||
|
$installment = InvoiceInstallment::findOrFail($id);
|
||||||
|
$invoice = $installment->invoice;
|
||||||
|
|
||||||
|
$installment->delete();
|
||||||
|
$invoice->refresh();
|
||||||
|
|
||||||
|
$grandTotal = $invoice->grand_total_with_charges ?? 0;
|
||||||
|
|
||||||
|
$paidTotal = $invoice->installments()->sum('amount');
|
||||||
|
$remaining = max(0, $grandTotal - $paidTotal);
|
||||||
|
|
||||||
|
if ($paidTotal <= 0 && $grandTotal > 0) {
|
||||||
|
$invoice->update(['status' => 'pending']);
|
||||||
|
} elseif ($paidTotal > 0 && $paidTotal < $grandTotal) {
|
||||||
|
$invoice->update(['status' => 'pending']);
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,196 +7,756 @@ use Illuminate\Http\Request;
|
|||||||
use App\Models\Order;
|
use App\Models\Order;
|
||||||
use App\Models\OrderItem;
|
use App\Models\OrderItem;
|
||||||
use App\Models\MarkList;
|
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
|
class AdminOrderController extends Controller
|
||||||
{
|
{
|
||||||
public function index()
|
/* ---------------------------
|
||||||
|
* DASHBOARD (old UI: stats + recent orders)
|
||||||
|
* ---------------------------*/
|
||||||
|
public function dashboard()
|
||||||
{
|
{
|
||||||
$orders = Order::latest()->get();
|
$totalOrders = Order::count();
|
||||||
|
$pendingOrders = Order::where('status', 'pending')->count();
|
||||||
|
$totalShipments = Shipment::count();
|
||||||
|
$totalItems = OrderItem::count();
|
||||||
|
$totalRevenue = Invoice::sum('final_amount_with_gst');
|
||||||
|
$activeCustomers = User::where('status', 'active')->count();
|
||||||
|
$inactiveCustomers = User::where('status', 'inactive')->count();
|
||||||
|
$totalStaff = Admin::where('type', 'staff')->count();
|
||||||
|
|
||||||
$markList = MarkList::where('status', 'active')->get();
|
$markList = MarkList::where('status', 'active')->get();
|
||||||
|
$orders = Order::latest()->get();
|
||||||
|
|
||||||
return view('admin.dashboard', compact('orders', 'markList'));
|
return view('admin.dashboard', compact(
|
||||||
|
'totalOrders',
|
||||||
|
'pendingOrders',
|
||||||
|
'totalShipments',
|
||||||
|
'totalItems',
|
||||||
|
'totalRevenue',
|
||||||
|
'activeCustomers',
|
||||||
|
'inactiveCustomers',
|
||||||
|
'totalStaff',
|
||||||
|
'orders',
|
||||||
|
'markList'
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
/* ---------------------------
|
||||||
// STEP 1 : ADD TEMPORARY ITEM
|
* LIST (new: Invoices Management for Orders page)
|
||||||
// -------------------------------------------------------------------------
|
* ---------------------------*/
|
||||||
|
public function index(Request $request)
|
||||||
public function addTempItem(Request $request)
|
|
||||||
{
|
{
|
||||||
// Validate item fields
|
$invoices = DB::table('invoices')
|
||||||
$item = $request->validate([
|
->leftJoin('containers', 'containers.id', '=', 'invoices.container_id')
|
||||||
'mark_no' => 'required',
|
->leftJoin('mark_list', 'mark_list.mark_no', '=', 'invoices.mark_no')
|
||||||
'origin' => 'required',
|
->select(
|
||||||
'destination' => 'required',
|
'invoices.id',
|
||||||
'description' => 'required|string',
|
'invoices.invoice_number',
|
||||||
'ctn' => 'nullable|numeric',
|
'invoices.invoice_date',
|
||||||
'qty' => 'nullable|numeric',
|
'invoices.final_amount',
|
||||||
'ttl_qty' => 'nullable|numeric',
|
'invoices.final_amount_with_gst',
|
||||||
'unit' => 'nullable|string',
|
'invoices.status as invoice_status',
|
||||||
'price' => 'nullable|numeric',
|
'invoices.mark_no',
|
||||||
'ttl_amount' => 'nullable|numeric',
|
'invoices.container_id', // <<< हे नक्की घाल
|
||||||
'cbm' => 'nullable|numeric',
|
'containers.container_number',
|
||||||
'ttl_cbm' => 'nullable|numeric',
|
'containers.container_date',
|
||||||
'kg' => 'nullable|numeric',
|
DB::raw('COALESCE(invoices.company_name, mark_list.company_name) as company_name'),
|
||||||
'ttl_kg' => 'nullable|numeric',
|
DB::raw('COALESCE(invoices.customer_name, mark_list.customer_name) as customer_name')
|
||||||
'shop_no' => 'nullable|string',
|
)
|
||||||
]);
|
|
||||||
|
->when($request->filled('search'), function ($q) use ($request) {
|
||||||
// ❌ Prevent changing mark_no once first item added
|
$search = trim($request->search);
|
||||||
if (session()->has('mark_no') && session('mark_no') != $request->mark_no) {
|
$q->where(function ($qq) use ($search) {
|
||||||
return redirect()->to(route('admin.orders.index') . '#createOrderForm')
|
$qq->where('invoices.invoice_number', 'like', "%{$search}%")
|
||||||
->with('error', 'You must finish or clear the current order before changing Mark No.');
|
->orWhere('containers.container_number', 'like', "%{$search}%")
|
||||||
}
|
->orWhere('invoices.mark_no', 'like', "%{$search}%")
|
||||||
|
->orWhere('mark_list.company_name', 'like', "%{$search}%")
|
||||||
// Save mark, origin, destination ONLY ONCE
|
->orWhere('mark_list.customer_name', 'like', "%{$search}%");
|
||||||
if (!session()->has('mark_no')) {
|
});
|
||||||
session([
|
})
|
||||||
'mark_no' => $request->mark_no,
|
->when($request->filled('status'), function ($q) use ($request) {
|
||||||
'origin' => $request->origin,
|
$q->where('invoices.status', $request->status);
|
||||||
'destination' => $request->destination
|
})
|
||||||
]);
|
->orderByDesc('invoices.invoice_date') // इथे बदल
|
||||||
}
|
->orderByDesc('invoices.id') // same-date साठी tie-breaker
|
||||||
|
->get();
|
||||||
// ❌ DO NOT overwrite these values again
|
|
||||||
// session(['mark_no' => $request->mark_no]);
|
return view('admin.orders', compact('invoices'));
|
||||||
// session(['origin' => $request->origin]);
|
|
||||||
// session(['destination' => $request->destination]);
|
|
||||||
|
|
||||||
// Add new sub-item into session
|
|
||||||
session()->push('temp_order_items', $item);
|
|
||||||
|
|
||||||
return redirect()->to(route('admin.orders.index') . '#createOrderForm')
|
|
||||||
->with('success', 'Item added.');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
/* ---------------------------
|
||||||
// STEP 2 : DELETE TEMPORARY ITEM
|
* CREATE NEW ORDER (simple page)
|
||||||
// -------------------------------------------------------------------------
|
* ---------------------------*/
|
||||||
|
public function create()
|
||||||
public function deleteTempItem(Request $request)
|
|
||||||
{
|
{
|
||||||
$index = $request->index;
|
$markList = MarkList::where('status', 'active')->get();
|
||||||
|
return view('admin.orders_create', compact('markList'));
|
||||||
$items = session('temp_order_items', []);
|
|
||||||
|
|
||||||
if (isset($items[$index])) {
|
|
||||||
unset($items[$index]);
|
|
||||||
session(['temp_order_items' => array_values($items)]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// If no items left → reset mark_no lock
|
|
||||||
if (empty($items)) {
|
|
||||||
session()->forget(['mark_no', 'origin', 'destination']);
|
|
||||||
}
|
|
||||||
|
|
||||||
return redirect()->to(route('admin.orders.index') . '#createOrderForm')
|
|
||||||
->with('success', 'Item removed successfully.');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
/* ---------------------------
|
||||||
// STEP 3 : FINISH ORDER
|
* SHOW / POPUP
|
||||||
// -------------------------------------------------------------------------
|
* ---------------------------*/
|
||||||
|
|
||||||
public function finishOrder(Request $request)
|
|
||||||
{
|
|
||||||
$request->validate([
|
|
||||||
'mark_no' => 'required',
|
|
||||||
'origin' => 'required',
|
|
||||||
'destination' => 'required',
|
|
||||||
]);
|
|
||||||
|
|
||||||
$items = session('temp_order_items', []);
|
|
||||||
|
|
||||||
if (empty($items)) {
|
|
||||||
return redirect()->to(route('admin.orders.index') . '#createOrderForm')
|
|
||||||
->with('error', 'Add at least one item before finishing.');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate Order ID
|
|
||||||
$year = date('y');
|
|
||||||
$prefix = "KNT-$year-";
|
|
||||||
|
|
||||||
$lastOrder = Order::latest('id')->first();
|
|
||||||
$nextNumber = $lastOrder ? intval(substr($lastOrder->order_id, -8)) + 1 : 1;
|
|
||||||
|
|
||||||
$orderId = $prefix . str_pad($nextNumber, 8, '0', STR_PAD_LEFT);
|
|
||||||
|
|
||||||
// TOTAL SUMS
|
|
||||||
$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'));
|
|
||||||
|
|
||||||
// CREATE ORDER
|
|
||||||
$order = Order::create([
|
|
||||||
'order_id' => $orderId,
|
|
||||||
'mark_no' => $request->mark_no,
|
|
||||||
'origin' => $request->origin,
|
|
||||||
'destination' => $request->destination,
|
|
||||||
'ctn' => $total_ctn,
|
|
||||||
'qty' => $total_qty,
|
|
||||||
'ttl_qty' => $total_ttl_qty,
|
|
||||||
'ttl_amount' => $total_amount,
|
|
||||||
'cbm' => $total_cbm,
|
|
||||||
'ttl_cbm' => $total_ttl_cbm,
|
|
||||||
'kg' => $total_kg,
|
|
||||||
'ttl_kg' => $total_ttl_kg,
|
|
||||||
'status' => 'pending',
|
|
||||||
]);
|
|
||||||
|
|
||||||
// SAVE ALL SUB-ITEMS
|
|
||||||
foreach ($items as $item) {
|
|
||||||
OrderItem::create([
|
|
||||||
'order_id' => $order->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'],
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// CLEAR TEMP DATA
|
|
||||||
session()->forget(['temp_order_items', 'mark_no', 'origin', 'destination']);
|
|
||||||
|
|
||||||
return redirect()->route('admin.orders.index')
|
|
||||||
->with('success', 'Order saved successfully.');
|
|
||||||
}
|
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
|
||||||
// ORDER SHOW PAGE
|
|
||||||
// -------------------------------------------------------------------------
|
|
||||||
|
|
||||||
public function show($id)
|
public function show($id)
|
||||||
{
|
{
|
||||||
$order = Order::with('items', 'markList')->findOrFail($id);
|
$order = Order::with('items', 'markList')->findOrFail($id);
|
||||||
|
$user = $this->getCustomerFromOrder($order);
|
||||||
$user = null;
|
|
||||||
if ($order->markList && $order->markList->customer_id) {
|
|
||||||
$user = \App\Models\User::where('customer_id', $order->markList->customer_id)->first();
|
|
||||||
}
|
|
||||||
|
|
||||||
return view('admin.orders_show', compact('order', 'user'));
|
return view('admin.orders_show', compact('order', 'user'));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function resetTemp()
|
public function popup($id)
|
||||||
{
|
{
|
||||||
session()->forget(['temp_order_items', 'mark_no', 'origin', 'destination']);
|
$order = Order::with(['items', 'markList'])->findOrFail($id);
|
||||||
|
|
||||||
return redirect()->to(route('admin.orders.index') . '#createOrderForm')
|
$user = null;
|
||||||
->with('success', 'Order reset successfully.');
|
if ($order->markList && $order->markList->customer_id) {
|
||||||
|
$user = User::where('customer_id', $order->markList->customer_id)->first();
|
||||||
|
}
|
||||||
|
|
||||||
|
return view('admin.popup', compact('order', 'user'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---------------------------
|
||||||
|
* ORDER ITEM MANAGEMENT (existing orders)
|
||||||
|
* ---------------------------*/
|
||||||
|
public function addItem(Request $request, $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',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$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['order_id'] = $order->id;
|
||||||
|
|
||||||
|
OrderItem::create($data);
|
||||||
|
|
||||||
|
$this->recalcTotals($order);
|
||||||
|
|
||||||
|
return redirect()->back()->with('success', 'Item added and totals updated.');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function deleteItem($id)
|
||||||
|
{
|
||||||
|
$item = OrderItem::findOrFail($id);
|
||||||
|
$order = $item->order;
|
||||||
|
|
||||||
|
$item->delete();
|
||||||
|
|
||||||
|
$this->recalcTotals($order);
|
||||||
|
|
||||||
|
return redirect()->back()->with('success', 'Item deleted and totals updated.');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function restoreItem($id)
|
||||||
|
{
|
||||||
|
$item = OrderItem::withTrashed()->findOrFail($id);
|
||||||
|
$order = Order::findOrFail($item->order_id);
|
||||||
|
|
||||||
|
$item->restore();
|
||||||
|
|
||||||
|
$this->recalcTotals($order);
|
||||||
|
|
||||||
|
return redirect()->back()->with('success', 'Item restored and totals updated.');
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------------------------
|
||||||
|
* ORDER CRUD: update / destroy
|
||||||
|
* ---------------------------*/
|
||||||
|
public function update(Request $request, $id)
|
||||||
|
{
|
||||||
|
$order = Order::findOrFail($id);
|
||||||
|
|
||||||
|
$data = $request->validate([
|
||||||
|
'mark_no' => 'required|string',
|
||||||
|
'origin' => 'nullable|string',
|
||||||
|
'destination' => 'nullable|string',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$order->update([
|
||||||
|
'mark_no' => $data['mark_no'],
|
||||||
|
'origin' => $data['origin'] ?? null,
|
||||||
|
'destination' => $data['destination'] ?? null,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->recalcTotals($order);
|
||||||
|
|
||||||
|
return redirect()->route('admin.orders.show', $order->id)
|
||||||
|
->with('success', 'Order updated successfully.');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function destroy($id)
|
||||||
|
{
|
||||||
|
$order = Order::findOrFail($id);
|
||||||
|
|
||||||
|
OrderItem::where('order_id', $order->id)->delete();
|
||||||
|
$order->delete();
|
||||||
|
|
||||||
|
return redirect()->route('admin.orders.index')
|
||||||
|
->with('success', 'Order deleted successfully.');
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------------------------
|
||||||
|
* HELPERS
|
||||||
|
* ---------------------------*/
|
||||||
|
private function recalcTotals(Order $order)
|
||||||
|
{
|
||||||
|
$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)),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function generateOrderId()
|
||||||
|
{
|
||||||
|
$year = date('y');
|
||||||
|
$prefix = "KNT-$year-";
|
||||||
|
|
||||||
|
$lastOrder = Order::latest('id')->first();
|
||||||
|
$nextNumber = $lastOrder ? intval(substr($lastOrder->order_id, -8)) + 1 : 1;
|
||||||
|
|
||||||
|
return $prefix . str_pad($nextNumber, 8, '0', STR_PAD_LEFT);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------------------------
|
||||||
|
* INVOICE CREATION HELPERS
|
||||||
|
* ---------------------------*/
|
||||||
|
private function createInvoice(Order $order)
|
||||||
|
{
|
||||||
|
$invoiceNumber = $this->generateInvoiceNumber();
|
||||||
|
$customer = $this->getCustomerFromMarkList($order->mark_no);
|
||||||
|
$totalAmount = $order->ttl_amount;
|
||||||
|
|
||||||
|
$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' => $totalAmount,
|
||||||
|
'gst_percent' => 0,
|
||||||
|
'gst_amount' => 0,
|
||||||
|
'final_amount_with_gst' => $totalAmount,
|
||||||
|
'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,
|
||||||
|
]);
|
||||||
|
|
||||||
|
foreach ($order->items as $item) {
|
||||||
|
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,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function generateInvoiceNumber()
|
||||||
|
{
|
||||||
|
$lastInvoice = Invoice::latest()->first();
|
||||||
|
$nextInvoice = $lastInvoice ? $lastInvoice->id + 1 : 1;
|
||||||
|
|
||||||
|
return 'INV-' . date('Y') . '-' . str_pad($nextInvoice, 6, '0', STR_PAD_LEFT);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getCustomerFromMarkList($markNo)
|
||||||
|
{
|
||||||
|
$markList = MarkList::where('mark_no', $markNo)->first();
|
||||||
|
|
||||||
|
if ($markList && $markList->customer_id) {
|
||||||
|
return User::where('customer_id', $markList->customer_id)->first();
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getCustomerFromOrder($order)
|
||||||
|
{
|
||||||
|
if ($order->markList && $order->markList->customer_id) {
|
||||||
|
return User::where('customer_id', $order->markList->customer_id)->first();
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------------------------
|
||||||
|
* SEE (detailed)
|
||||||
|
* ---------------------------*/
|
||||||
|
public function see($id)
|
||||||
|
{
|
||||||
|
$order = Order::with([
|
||||||
|
'markList',
|
||||||
|
'items',
|
||||||
|
'shipments' => function ($q) use ($id) {
|
||||||
|
$q->whereHas('orders', function ($oq) use ($id) {
|
||||||
|
$oq->where('orders.id', $id)
|
||||||
|
->whereNull('orders.deleted_at');
|
||||||
|
})->with([
|
||||||
|
'orders' => function ($oq) use ($id) {
|
||||||
|
$oq->where('orders.id', $id)
|
||||||
|
->whereNull('orders.deleted_at')
|
||||||
|
->with('items');
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
])->findOrFail($id);
|
||||||
|
|
||||||
|
$orderData = [
|
||||||
|
'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,
|
||||||
|
],
|
||||||
|
'items' => $order->items,
|
||||||
|
];
|
||||||
|
|
||||||
|
$shipmentsData = [];
|
||||||
|
foreach ($order->shipments as $shipment) {
|
||||||
|
$shipmentOrders = [];
|
||||||
|
$totals = [
|
||||||
|
'ctn' => 0, 'qty' => 0, 'ttl_qty' => 0,
|
||||||
|
'cbm' => 0, 'ttl_cbm' => 0,
|
||||||
|
'kg' => 0, 'ttl_kg' => 0,
|
||||||
|
'amount' => 0,
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($shipment->orders as $shipOrder) {
|
||||||
|
foreach ($shipOrder->items as $item) {
|
||||||
|
$shipmentOrders[] = [
|
||||||
|
'order_id' => $shipOrder->order_id,
|
||||||
|
'origin' => $shipOrder->origin,
|
||||||
|
'destination' => $shipOrder->destination,
|
||||||
|
'description' => $item->description,
|
||||||
|
'ctn' => $item->ctn,
|
||||||
|
'qty' => $item->qty,
|
||||||
|
'ttl_qty' => $item->ttl_qty,
|
||||||
|
'amount' => $item->ttl_amount,
|
||||||
|
];
|
||||||
|
|
||||||
|
$totals['ctn'] += $item->ctn;
|
||||||
|
$totals['qty'] += $item->qty;
|
||||||
|
$totals['ttl_qty'] += $item->ttl_qty;
|
||||||
|
$totals['cbm'] += $item->cbm;
|
||||||
|
$totals['ttl_cbm'] += $item->ttl_cbm;
|
||||||
|
$totals['kg'] += $item->kg;
|
||||||
|
$totals['ttl_kg'] += $item->ttl_kg;
|
||||||
|
$totals['amount'] += $item->ttl_amount;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($shipmentOrders)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$shipmentsData[] = [
|
||||||
|
'shipment_id' => $shipment->shipment_id,
|
||||||
|
'status' => $shipment->status,
|
||||||
|
'date' => $shipment->shipment_date,
|
||||||
|
'total_orders' => 1,
|
||||||
|
'orders' => $shipmentOrders,
|
||||||
|
'totals' => $totals,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$invoiceData = null;
|
||||||
|
|
||||||
|
return view('admin.see_order', compact(
|
||||||
|
'order',
|
||||||
|
'orderData',
|
||||||
|
'shipmentsData',
|
||||||
|
'invoiceData'
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------------------------
|
||||||
|
* FILTERED LIST + EXPORTS (old orders listing)
|
||||||
|
* ---------------------------*/
|
||||||
|
public function orderShow()
|
||||||
|
{
|
||||||
|
$orders = Order::with([
|
||||||
|
'markList',
|
||||||
|
'shipments',
|
||||||
|
])->latest('id')->get();
|
||||||
|
|
||||||
|
return view('admin.orders', compact('orders'));
|
||||||
|
}
|
||||||
|
|
||||||
|
private function buildOrdersQueryFromRequest(Request $request)
|
||||||
|
{
|
||||||
|
$query = Order::query()
|
||||||
|
->with(['markList', 'shipments']);
|
||||||
|
|
||||||
|
if ($request->filled('search')) {
|
||||||
|
$search = trim($request->search);
|
||||||
|
|
||||||
|
$query->where(function ($q) use ($search) {
|
||||||
|
$q->where('orders.order_id', 'like', "%{$search}%")
|
||||||
|
->orWhereHas('markList', function ($q2) use ($search) {
|
||||||
|
$q2->where('company_name', 'like', "%{$search}%")
|
||||||
|
->orWhere('customer_id', 'like', "%{$search}%")
|
||||||
|
->orWhere('origin', 'like', "%{$search}%")
|
||||||
|
->orWhere('destination', 'like', "%{$search}%");
|
||||||
|
})
|
||||||
|
->orWhereHas('shipments', function ($q4) use ($search) {
|
||||||
|
$q4->where('shipments.shipment_id', 'like', "%{$search}%");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($request->filled('shipment')) {
|
||||||
|
$query->where(function ($q) use ($request) {
|
||||||
|
$q->whereHas('shipments', function ($q2) use ($request) {
|
||||||
|
$q2->where('status', $request->shipment);
|
||||||
|
})->orWhereDoesntHave('shipments');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($request->filled('from_date')) {
|
||||||
|
$query->whereDate('orders.created_at', '>=', $request->from_date);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($request->filled('to_date')) {
|
||||||
|
$query->whereDate('orders.created_at', '<=', $request->to_date);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $query->latest('orders.id');
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
|
||||||
|
$pdf = PDF::loadView('admin.pdf.invoices_report', compact('invoices'))
|
||||||
|
->setPaper('a4', 'landscape');
|
||||||
|
|
||||||
|
return $pdf->download(
|
||||||
|
'invoices-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: Create Order + Invoice directly from popup
|
||||||
|
* --------------------------------------------------*/
|
||||||
|
public function addTempItem(Request $request)
|
||||||
|
{
|
||||||
|
$request->validate([
|
||||||
|
'mark_no' => 'required',
|
||||||
|
'origin' => 'nullable',
|
||||||
|
'destination' => 'nullable',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$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'];
|
||||||
|
|
||||||
|
$items = array_filter($items, function ($row) {
|
||||||
|
return trim($row['description'] ?? '') !== '';
|
||||||
|
});
|
||||||
|
|
||||||
|
if (empty($items)) {
|
||||||
|
return back()->with('error', 'Add at least one item.');
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
$item['ttl_qty'] = $ctn * $qty;
|
||||||
|
$item['ttl_amount'] = $item['ttl_qty'] * $price;
|
||||||
|
$item['ttl_cbm'] = $cbm * $ctn;
|
||||||
|
$item['ttl_kg'] = $ctn * $kg;
|
||||||
|
}
|
||||||
|
unset($item);
|
||||||
|
|
||||||
|
$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'));
|
||||||
|
|
||||||
|
$orderId = $this->generateOrderId();
|
||||||
|
|
||||||
|
$order = Order::create([
|
||||||
|
'order_id' => $orderId,
|
||||||
|
'mark_no' => $request->mark_no,
|
||||||
|
'origin' => $request->origin,
|
||||||
|
'destination'=> $request->destination,
|
||||||
|
'ctn' => $total_ctn,
|
||||||
|
'qty' => $total_qty,
|
||||||
|
'ttl_qty' => $total_ttl_qty,
|
||||||
|
'ttl_amount' => $total_amount,
|
||||||
|
'cbm' => $total_cbm,
|
||||||
|
'ttl_cbm' => $total_ttl_cbm,
|
||||||
|
'kg' => $total_kg,
|
||||||
|
'ttl_kg' => $total_ttl_kg,
|
||||||
|
'status' => 'order_placed',
|
||||||
|
]);
|
||||||
|
|
||||||
|
foreach ($items as $item) {
|
||||||
|
OrderItem::create([
|
||||||
|
'order_id' => $order->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'],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$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();
|
||||||
|
}
|
||||||
|
|
||||||
|
$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,
|
||||||
|
]);
|
||||||
|
|
||||||
|
foreach ($order->items as $item) {
|
||||||
|
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,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
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',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$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);
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
108
app/Http/Controllers/Admin/AdminReportController.php
Normal file
108
app/Http/Controllers/Admin/AdminReportController.php
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Admin;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
|
class AdminReportController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Display the reports page with joined data
|
||||||
|
*/
|
||||||
|
// public function index(Request $request)
|
||||||
|
// {
|
||||||
|
/*********************************************************
|
||||||
|
* OLD FLOW (Order + Shipment + Invoice)
|
||||||
|
* फक्त reference साठी ठेवलेला, वापरत नाही.
|
||||||
|
*********************************************************/
|
||||||
|
|
||||||
|
/*
|
||||||
|
$reports = DB::table('orders')
|
||||||
|
->join('shipment_items', 'shipment_items.order_id', '=', 'orders.id')
|
||||||
|
->join('shipments', 'shipments.id', '=', 'shipment_items.shipment_id')
|
||||||
|
->join('invoices', 'invoices.order_id', '=', 'orders.id')
|
||||||
|
->leftJoin('mark_list', 'mark_list.mark_no', '=', 'orders.mark_no')
|
||||||
|
->leftJoin('users', 'users.customer_id', '=', 'mark_list.customer_id')
|
||||||
|
->select(...)
|
||||||
|
->orderBy('shipments.shipment_date', 'desc')
|
||||||
|
->get();
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*********************************************************
|
||||||
|
* NEW FLOW (Container + Invoice + MarkList)
|
||||||
|
*********************************************************/
|
||||||
|
|
||||||
|
// $reports = DB::table('invoices')
|
||||||
|
// ->join('containers', 'containers.id', '=', 'invoices.containerid')
|
||||||
|
// ->leftJoin('mark_list', 'mark_list.markno', '=', 'invoices.markno')
|
||||||
|
// ->select(
|
||||||
|
// 'invoices.id as invoicepk',
|
||||||
|
// 'invoices.invoicenumber',
|
||||||
|
// 'invoices.invoicedate',
|
||||||
|
// 'invoices.finalamount',
|
||||||
|
// 'invoices.finalamountwithgst',
|
||||||
|
// 'invoices.gstpercent',
|
||||||
|
// 'invoices.gstamount',
|
||||||
|
// 'invoices.status as invoicestatus',
|
||||||
|
// 'invoices.markno',
|
||||||
|
|
||||||
|
// 'containers.id as containerpk',
|
||||||
|
// 'containers.containernumber',
|
||||||
|
// 'containers.containerdate',
|
||||||
|
// 'containers.containername',
|
||||||
|
|
||||||
|
// 'mark_list.companyname',
|
||||||
|
// 'mark_list.customername'
|
||||||
|
// )
|
||||||
|
// ->orderBy('containers.containerdate', 'desc')
|
||||||
|
// ->get();
|
||||||
|
|
||||||
|
// return view('admin.reports', compact('reports'));
|
||||||
|
// }
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public function index(Request $request)
|
||||||
|
{
|
||||||
|
$reports = DB::table('invoices')
|
||||||
|
->join('containers', 'containers.id', '=', 'invoices.container_id')
|
||||||
|
->leftJoin('mark_list', 'mark_list.mark_no', '=', 'invoices.mark_no')
|
||||||
|
->select(
|
||||||
|
// INVOICE
|
||||||
|
'invoices.id as invoicepk',
|
||||||
|
'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',
|
||||||
|
|
||||||
|
// CONTAINER
|
||||||
|
'containers.id as containerpk',
|
||||||
|
'containers.container_number',
|
||||||
|
'containers.container_date',
|
||||||
|
'containers.container_name',
|
||||||
|
|
||||||
|
// RAW FIELDS (for reference/debug if needed)
|
||||||
|
'invoices.company_name as inv_company_name',
|
||||||
|
'invoices.customer_name as inv_customer_name',
|
||||||
|
'mark_list.company_name as ml_company_name',
|
||||||
|
'mark_list.customer_name as ml_customer_name',
|
||||||
|
|
||||||
|
// FINAL FIELDS (automatically pick invoice first, else mark_list)
|
||||||
|
DB::raw('COALESCE(invoices.company_name, mark_list.company_name) as company_name'),
|
||||||
|
DB::raw('COALESCE(invoices.customer_name, mark_list.customer_name) as customer_name')
|
||||||
|
)
|
||||||
|
->orderBy('invoices.invoice_date', 'desc')
|
||||||
|
->orderBy('invoices.id', 'desc')
|
||||||
|
|
||||||
|
->get();
|
||||||
|
|
||||||
|
return view('admin.reports', compact('reports'));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
191
app/Http/Controllers/Admin/AdminStaffController.php
Normal file
191
app/Http/Controllers/Admin/AdminStaffController.php
Normal file
@@ -0,0 +1,191 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Admin;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Hash;
|
||||||
|
use App\Models\Admin;
|
||||||
|
use Spatie\Permission\Models\Permission;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
|
class AdminStaffController extends Controller
|
||||||
|
{
|
||||||
|
public function index()
|
||||||
|
{
|
||||||
|
$staff = Admin::where('type', 'staff')->orderBy('id', 'DESC')->get();
|
||||||
|
return view('admin.staff.index', compact('staff'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function create()
|
||||||
|
{
|
||||||
|
$permissions = Permission::where('guard_name', 'admin')->get()->groupBy(function ($p) {
|
||||||
|
return explode('.', $p->name)[0];
|
||||||
|
});
|
||||||
|
|
||||||
|
return view('admin.staff.create', compact('permissions'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function store(Request $request)
|
||||||
|
{
|
||||||
|
$request->validate([
|
||||||
|
// Personal Info
|
||||||
|
'name' => 'required|string|max:255',
|
||||||
|
'email' => 'required|email|unique:admins,email',
|
||||||
|
'phone' => 'required|string|max:20',
|
||||||
|
'emergency_phone' => 'nullable|string|max:20',
|
||||||
|
'address' => 'nullable|string|max:255',
|
||||||
|
|
||||||
|
// Professional info
|
||||||
|
'role' => 'nullable|string|max:100',
|
||||||
|
'department' => 'nullable|string|max:100',
|
||||||
|
'designation' => 'nullable|string|max:100',
|
||||||
|
'joining_date' => 'nullable|date',
|
||||||
|
'status' => 'required|string|in:active,inactive',
|
||||||
|
'additional_info' => 'nullable|string',
|
||||||
|
|
||||||
|
// System access
|
||||||
|
'username' => 'nullable|string|unique:admins,username',
|
||||||
|
'password' => 'required|string|min:6',
|
||||||
|
|
||||||
|
// Permissions
|
||||||
|
'permissions' => 'nullable|array',
|
||||||
|
]);
|
||||||
|
|
||||||
|
DB::beginTransaction();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1️⃣ Create staff WITHOUT employee_id (ID not available yet)
|
||||||
|
$admin = Admin::create([
|
||||||
|
'name' => $request->name,
|
||||||
|
'email' => $request->email,
|
||||||
|
'phone' => $request->phone,
|
||||||
|
'emergency_phone' => $request->emergency_phone,
|
||||||
|
'address' => $request->address,
|
||||||
|
|
||||||
|
'role' => $request->role,
|
||||||
|
'department' => $request->department,
|
||||||
|
'designation' => $request->designation,
|
||||||
|
'joining_date' => $request->joining_date,
|
||||||
|
'status' => $request->status,
|
||||||
|
'additional_info' => $request->additional_info,
|
||||||
|
|
||||||
|
// username may be NULL here
|
||||||
|
'username' => $request->username ?: null,
|
||||||
|
'password' => Hash::make($request->password),
|
||||||
|
'type' => 'staff',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 2️⃣ Generate EMPLOYEE ID
|
||||||
|
$employeeId = 'EMP' . str_pad($admin->id, 4, '0', STR_PAD_LEFT);
|
||||||
|
|
||||||
|
// 3️⃣ Auto-generate username if left blank
|
||||||
|
$username = $request->username ?: strtolower($employeeId);
|
||||||
|
|
||||||
|
// 4️⃣ Update employee_id + username together
|
||||||
|
$admin->update([
|
||||||
|
'employee_id' => $employeeId,
|
||||||
|
'username' => $username,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 5️⃣ Assign permissions (if any)
|
||||||
|
if ($request->permissions) {
|
||||||
|
$admin->givePermissionTo($request->permissions);
|
||||||
|
}
|
||||||
|
|
||||||
|
DB::commit();
|
||||||
|
|
||||||
|
return redirect()
|
||||||
|
->route('admin.staff.index')
|
||||||
|
->with('success', 'Staff created successfully.');
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
DB::rollBack();
|
||||||
|
return back()->withErrors(['error' => $e->getMessage()]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public function edit($id)
|
||||||
|
{
|
||||||
|
$staff = Admin::where('type', 'staff')->findOrFail($id);
|
||||||
|
|
||||||
|
$permissions = Permission::where('guard_name', 'admin')->get()->groupBy(function ($p) {
|
||||||
|
return explode('.', $p->name)[0];
|
||||||
|
});
|
||||||
|
|
||||||
|
$staffPermissions = $staff->permissions->pluck('name')->toArray();
|
||||||
|
|
||||||
|
return view('admin.staff.edit', compact('staff', 'permissions', 'staffPermissions'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update(Request $request, $id)
|
||||||
|
{
|
||||||
|
$staff = Admin::where('type', 'staff')->findOrFail($id);
|
||||||
|
|
||||||
|
$request->validate([
|
||||||
|
'name' => 'required|string|max:255',
|
||||||
|
'email' => 'required|email|unique:admins,email,' . $staff->id,
|
||||||
|
'phone' => 'required|string|max:20',
|
||||||
|
'emergency_phone' => 'nullable|string|max:20',
|
||||||
|
'address' => 'nullable|string|max:255',
|
||||||
|
|
||||||
|
'role' => 'nullable|string|max:100',
|
||||||
|
'department' => 'nullable|string|max:100',
|
||||||
|
'designation' => 'nullable|string|max:100',
|
||||||
|
'joining_date' => 'nullable|date',
|
||||||
|
'status' => 'required|string|in:active,inactive',
|
||||||
|
'additional_info' => 'nullable|string',
|
||||||
|
|
||||||
|
'username' => 'nullable|string|unique:admins,username,' . $staff->id,
|
||||||
|
'password' => 'nullable|string|min:6',
|
||||||
|
|
||||||
|
'permissions' => 'nullable|array',
|
||||||
|
]);
|
||||||
|
|
||||||
|
DB::beginTransaction();
|
||||||
|
|
||||||
|
try {
|
||||||
|
$staff->update([
|
||||||
|
'name' => $request->name,
|
||||||
|
'email' => $request->email,
|
||||||
|
'phone' => $request->phone,
|
||||||
|
'emergency_phone' => $request->emergency_phone,
|
||||||
|
'address' => $request->address,
|
||||||
|
|
||||||
|
'role' => $request->role,
|
||||||
|
'department' => $request->department,
|
||||||
|
'designation' => $request->designation,
|
||||||
|
'joining_date' => $request->joining_date,
|
||||||
|
'status' => $request->status,
|
||||||
|
'additional_info' => $request->additional_info,
|
||||||
|
|
||||||
|
'username' => $request->username,
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($request->password) {
|
||||||
|
$staff->update(['password' => Hash::make($request->password)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$staff->syncPermissions($request->permissions ?? []);
|
||||||
|
|
||||||
|
DB::commit();
|
||||||
|
|
||||||
|
return redirect()->route('admin.staff.index')
|
||||||
|
->with('success', 'Staff updated successfully.');
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
DB::rollBack();
|
||||||
|
return back()->withErrors(['error' => $e->getMessage()]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function destroy($id)
|
||||||
|
{
|
||||||
|
$staff = Admin::where('type', 'staff')->findOrFail($id);
|
||||||
|
$staff->delete();
|
||||||
|
|
||||||
|
return redirect()->route('admin.staff.index')
|
||||||
|
->with('success', 'Staff removed successfully.');
|
||||||
|
}
|
||||||
|
}
|
||||||
338
app/Http/Controllers/Admin/ShipmentController.php
Normal file
338
app/Http/Controllers/Admin/ShipmentController.php
Normal file
@@ -0,0 +1,338 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Admin;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use App\Models\Shipment;
|
||||||
|
use App\Models\ShipmentItem;
|
||||||
|
use App\Models\Order;
|
||||||
|
use Carbon\Carbon;
|
||||||
|
|
||||||
|
class ShipmentController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Show shipment page (Create Shipment + Shipment List)
|
||||||
|
*/
|
||||||
|
public function index()
|
||||||
|
{
|
||||||
|
// 1) Get all used order IDs
|
||||||
|
$usedOrderIds = ShipmentItem::pluck('order_id')->toArray();
|
||||||
|
|
||||||
|
// 2) Load available orders (not used in any shipment)
|
||||||
|
$availableOrders = Order::whereNotIn('id', $usedOrderIds)
|
||||||
|
->where('status', '!=', 'order_placed')
|
||||||
|
->get();
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// 3) Load all shipments for listing
|
||||||
|
$shipments = Shipment::latest()->get();
|
||||||
|
|
||||||
|
// Return your file: resources/views/admin/shipment.blade.php
|
||||||
|
return view('admin.shipments', compact('availableOrders', 'shipments'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Store new shipment
|
||||||
|
*/
|
||||||
|
public function store(Request $request)
|
||||||
|
{
|
||||||
|
$request->validate([
|
||||||
|
'origin' => 'required|string',
|
||||||
|
'destination' => 'required|string',
|
||||||
|
'shipment_date' => 'required|date',
|
||||||
|
'order_ids' => 'required|array|min:1',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// -----------------------------
|
||||||
|
// PREVENT DUPLICATE ORDERS
|
||||||
|
// -----------------------------
|
||||||
|
foreach ($request->order_ids as $id) {
|
||||||
|
if (ShipmentItem::where('order_id', $id)->exists()) {
|
||||||
|
return back()->with('error', "Order ID $id is already assigned to a shipment.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------
|
||||||
|
// GENERATE UNIQUE SHIPMENT ID
|
||||||
|
// -----------------------------
|
||||||
|
$year = date('y');
|
||||||
|
$prefix = "SHIP-$year-";
|
||||||
|
|
||||||
|
$lastShipment = Shipment::latest('id')->first();
|
||||||
|
$nextNumber = $lastShipment ? intval(substr($lastShipment->shipment_id, -8)) + 1 : 1;
|
||||||
|
|
||||||
|
$newShipmentId = $prefix . str_pad($nextNumber, 8, '0', STR_PAD_LEFT);
|
||||||
|
|
||||||
|
// -----------------------------
|
||||||
|
// CALCULATE TOTALS
|
||||||
|
// -----------------------------
|
||||||
|
$orders = Order::whereIn('id', $request->order_ids)->get();
|
||||||
|
foreach ($orders as $order) {
|
||||||
|
if ($order->status === 'order_placed') {
|
||||||
|
return back()->with(
|
||||||
|
'error',
|
||||||
|
"Order {$order->order_id} is not ready for shipment"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
$total_ctn = $orders->sum('ctn');
|
||||||
|
$total_qty = $orders->sum('qty');
|
||||||
|
$total_ttl_qty = $orders->sum('ttl_qty');
|
||||||
|
$total_amount = $orders->sum('ttl_amount');
|
||||||
|
$total_cbm = $orders->sum('cbm');
|
||||||
|
$total_ttl_cbm = $orders->sum('ttl_cbm');
|
||||||
|
$total_kg = $orders->sum('kg');
|
||||||
|
$total_ttl_kg = $orders->sum('ttl_kg');
|
||||||
|
|
||||||
|
// -----------------------------
|
||||||
|
// CREATE SHIPMENT
|
||||||
|
//-------------------------------
|
||||||
|
$shipment = Shipment::create([
|
||||||
|
'shipment_id' => $newShipmentId,
|
||||||
|
'origin' => $request->origin,
|
||||||
|
'destination' => $request->destination,
|
||||||
|
'status' => Shipment::STATUS_SHIPMENT_READY,
|
||||||
|
'shipment_date' => $request->shipment_date,
|
||||||
|
|
||||||
|
'total_ctn' => $total_ctn,
|
||||||
|
'total_qty' => $total_qty,
|
||||||
|
'total_ttl_qty' => $total_ttl_qty,
|
||||||
|
'total_amount' => $total_amount,
|
||||||
|
'total_cbm' => $total_cbm,
|
||||||
|
'total_ttl_cbm' => $total_ttl_cbm,
|
||||||
|
'total_kg' => $total_kg,
|
||||||
|
'total_ttl_kg' => $total_ttl_kg,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// -----------------------------
|
||||||
|
// INSERT SHIPMENT ITEMS
|
||||||
|
// -----------------------------
|
||||||
|
foreach ($orders as $order) {
|
||||||
|
ShipmentItem::create([
|
||||||
|
'shipment_id' => $shipment->id,
|
||||||
|
'order_id' => $order->id,
|
||||||
|
'order_ctn' => $order->ctn,
|
||||||
|
'order_qty' => $order->qty,
|
||||||
|
'order_ttl_qty' => $order->ttl_qty,
|
||||||
|
'order_ttl_amount' => $order->ttl_amount,
|
||||||
|
'order_ttl_kg' => $order->ttl_kg,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return redirect()->back()->with('success', "Shipment $newShipmentId created successfully!");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show shipment details (for modal popup)
|
||||||
|
*/
|
||||||
|
public function show($id)
|
||||||
|
{
|
||||||
|
$shipment = Shipment::findOrFail($id);
|
||||||
|
|
||||||
|
// Load full order data from orders table
|
||||||
|
$orders = Order::whereIn('id',
|
||||||
|
ShipmentItem::where('shipment_id', $id)->pluck('order_id')
|
||||||
|
)->get();
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'shipment' => $shipment,
|
||||||
|
'orders' => $orders
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update Shipment status from action button
|
||||||
|
*/
|
||||||
|
public function updateStatus(Request $request)
|
||||||
|
{
|
||||||
|
$request->validate([
|
||||||
|
'shipment_id' => 'required|exists:shipments,id',
|
||||||
|
'status' => 'required|string'
|
||||||
|
]);
|
||||||
|
|
||||||
|
$shipment = Shipment::findOrFail($request->shipment_id);
|
||||||
|
$shipment->status = $request->status;
|
||||||
|
$shipment->save();
|
||||||
|
|
||||||
|
// ✅ Sync shipment status to orders ONLY after shipment exists
|
||||||
|
foreach ($shipment->orders as $order) {
|
||||||
|
|
||||||
|
// Prevent rollback or overwrite
|
||||||
|
if ($order->status === 'delivered') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$order->status = $shipment->status;
|
||||||
|
$order->save();
|
||||||
|
}
|
||||||
|
|
||||||
|
return redirect()->back()->with(
|
||||||
|
'success',
|
||||||
|
"Shipment status updated to {$shipment->statusLabel()}."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update shipment details
|
||||||
|
*/
|
||||||
|
public function update(Request $request, $id)
|
||||||
|
{
|
||||||
|
$shipment = Shipment::findOrFail($id);
|
||||||
|
|
||||||
|
$data = $request->validate([
|
||||||
|
'origin' => 'required|string',
|
||||||
|
'destination' => 'required|string',
|
||||||
|
'shipment_date' => 'required|date',
|
||||||
|
'status' => 'required|string',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$shipment->update($data);
|
||||||
|
|
||||||
|
// If it's an AJAX request, return JSON response
|
||||||
|
if ($request->ajax() || $request->wantsJson()) {
|
||||||
|
return response()->json([
|
||||||
|
'success' => true,
|
||||||
|
'message' => 'Shipment updated successfully.'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return redirect()->back()->with('success', 'Shipment updated successfully.');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete shipment permanently
|
||||||
|
*/
|
||||||
|
public function destroy($id, Request $request)
|
||||||
|
{
|
||||||
|
$shipment = Shipment::findOrFail($id);
|
||||||
|
|
||||||
|
// Delete shipment items
|
||||||
|
ShipmentItem::where('shipment_id', $shipment->id)->delete();
|
||||||
|
|
||||||
|
// Delete shipment itself
|
||||||
|
$shipment->delete();
|
||||||
|
|
||||||
|
// If it's an AJAX request, return JSON response
|
||||||
|
if ($request->ajax() || $request->wantsJson()) {
|
||||||
|
return response()->json([
|
||||||
|
'success' => true,
|
||||||
|
'message' => 'Shipment deleted successfully.'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return redirect()->route('admin.shipments')
|
||||||
|
->with('success', 'Shipment deleted successfully.');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function dummy($id)
|
||||||
|
{
|
||||||
|
// Load shipment
|
||||||
|
$shipment = Shipment::with('orders')->findOrFail($id);
|
||||||
|
|
||||||
|
// Dummy data (you can modify anytime)
|
||||||
|
$dummyData = [
|
||||||
|
'title' => 'Dummy Shipment Preview',
|
||||||
|
'generated_on' => now()->format('d M Y h:i A'),
|
||||||
|
'note' => 'This is dummy shipment information for testing page layout.'
|
||||||
|
];
|
||||||
|
|
||||||
|
return view('admin.view_shipment', compact('shipment', 'dummyData'));
|
||||||
|
}
|
||||||
|
// App\Models\Shipment.php
|
||||||
|
|
||||||
|
public function orders()
|
||||||
|
{
|
||||||
|
return $this->belongsToMany(\App\Models\Order::class, 'shipment_items', 'shipment_id', 'order_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function removeOrder(Shipment $shipment, Order $order)
|
||||||
|
{
|
||||||
|
// Remove row from pivot table shipment_items
|
||||||
|
ShipmentItem::where('shipment_id', $shipment->id)
|
||||||
|
->where('order_id', $order->id)
|
||||||
|
->delete(); // removes link shipment <-> order [web:41][web:45]
|
||||||
|
|
||||||
|
// Recalculate totals on this shipment (optional but recommended)
|
||||||
|
$orders = Order::whereIn(
|
||||||
|
'id',
|
||||||
|
ShipmentItem::where('shipment_id', $shipment->id)->pluck('order_id')
|
||||||
|
)->get();
|
||||||
|
|
||||||
|
$shipment->total_ctn = $orders->sum('ctn');
|
||||||
|
$shipment->total_qty = $orders->sum('qty');
|
||||||
|
$shipment->total_ttl_qty = $orders->sum('ttl_qty');
|
||||||
|
$shipment->total_cbm = $orders->sum('cbm');
|
||||||
|
$shipment->total_ttl_cbm = $orders->sum('ttl_cbm');
|
||||||
|
$shipment->total_kg = $orders->sum('kg');
|
||||||
|
$shipment->total_ttl_kg = $orders->sum('ttl_kg');
|
||||||
|
$shipment->total_amount = $orders->sum('ttl_amount');
|
||||||
|
$shipment->save();
|
||||||
|
|
||||||
|
// Redirect back to preview page where your blade is loaded
|
||||||
|
return redirect()
|
||||||
|
->route('admin.shipments.dummy', $shipment->id)
|
||||||
|
->with('success', 'Order removed from shipment successfully.');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function addOrders(Request $request, Shipment $shipment)
|
||||||
|
{
|
||||||
|
$request->validate([
|
||||||
|
'order_ids' => 'required|array|min:1',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$orders = Order::whereIn('id', $request->order_ids)->get();
|
||||||
|
|
||||||
|
foreach ($orders as $order) {
|
||||||
|
|
||||||
|
if ($order->status === 'order_placed') {
|
||||||
|
return back()->with(
|
||||||
|
'error',
|
||||||
|
"Order {$order->order_id} is not ready for shipment"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Prevent duplicates
|
||||||
|
if (ShipmentItem::where('order_id', $order->id)->exists()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
ShipmentItem::create([
|
||||||
|
'shipment_id' => $shipment->id,
|
||||||
|
'order_id' => $order->id,
|
||||||
|
'order_ctn' => $order->ctn,
|
||||||
|
'order_qty' => $order->qty,
|
||||||
|
'order_ttl_qty' => $order->ttl_qty,
|
||||||
|
'order_ttl_amount' => $order->ttl_amount,
|
||||||
|
'order_ttl_kg' => $order->ttl_kg,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recalculate totals
|
||||||
|
$orderIds = ShipmentItem::where('shipment_id', $shipment->id)->pluck('order_id');
|
||||||
|
$allOrders = Order::whereIn('id', $orderIds)->get();
|
||||||
|
|
||||||
|
$shipment->update([
|
||||||
|
'total_ctn' => $allOrders->sum('ctn'),
|
||||||
|
'total_qty' => $allOrders->sum('qty'),
|
||||||
|
'total_ttl_qty' => $allOrders->sum('ttl_qty'),
|
||||||
|
'total_cbm' => $allOrders->sum('cbm'),
|
||||||
|
'total_ttl_cbm' => $allOrders->sum('ttl_cbm'),
|
||||||
|
'total_kg' => $allOrders->sum('kg'),
|
||||||
|
'total_ttl_kg' => $allOrders->sum('ttl_kg'),
|
||||||
|
'total_amount' => $allOrders->sum('ttl_amount'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return redirect()
|
||||||
|
->route('admin.shipments.dummy', $shipment->id)
|
||||||
|
->with('success', 'Orders added to shipment successfully.');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -15,7 +15,12 @@ class UserRequestController extends Controller
|
|||||||
public function index()
|
public function index()
|
||||||
{
|
{
|
||||||
$requests = CustomerRequest::orderBy('id', 'desc')->get();
|
$requests = CustomerRequest::orderBy('id', 'desc')->get();
|
||||||
return view('admin.requests', compact('requests'));
|
$pendingProfileUpdates = \App\Models\UpdateRequest::where('status', 'pending')->count();
|
||||||
|
|
||||||
|
return view('admin.requests', compact(
|
||||||
|
'requests',
|
||||||
|
'pendingProfileUpdates'
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Approve user request
|
// Approve user request
|
||||||
@@ -65,4 +70,61 @@ class UserRequestController extends Controller
|
|||||||
|
|
||||||
return redirect()->back()->with('info', 'Request rejected successfully.');
|
return redirect()->back()->with('info', 'Request rejected successfully.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function profileUpdateRequests()
|
||||||
|
{
|
||||||
|
$requests = \App\Models\UpdateRequest::where('status', 'pending')
|
||||||
|
->orderBy('id', 'desc')
|
||||||
|
->get();
|
||||||
|
|
||||||
|
return view('admin.profile_update_requests', compact('requests'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function approveProfileUpdate($id)
|
||||||
|
{
|
||||||
|
$req = \App\Models\UpdateRequest::findOrFail($id);
|
||||||
|
$user = \App\Models\User::findOrFail($req->user_id);
|
||||||
|
|
||||||
|
$newData = is_array($req->data) ? $req->data : json_decode($req->data, true);
|
||||||
|
|
||||||
|
foreach ($newData as $key => $value) {
|
||||||
|
if ($value !== null && $value !== "") {
|
||||||
|
if (in_array($key, ['customer_name','company_name','designation','email','mobile_no','address','pincode'])) {
|
||||||
|
$user->$key = $value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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();
|
||||||
|
|
||||||
|
return back()->with('success', 'Profile updated successfully.');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public function rejectProfileUpdate($id)
|
||||||
|
{
|
||||||
|
$req = \App\Models\UpdateRequest::findOrFail($id);
|
||||||
|
$req->status = 'rejected';
|
||||||
|
$req->admin_note = 'Rejected by admin on ' . now();
|
||||||
|
$req->save();
|
||||||
|
|
||||||
|
return back()->with('info', 'Profile update request rejected.');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
959
app/Http/Controllers/ContainerController.php
Normal file
959
app/Http/Controllers/ContainerController.php
Normal file
@@ -0,0 +1,959 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Models\Container;
|
||||||
|
use App\Models\ContainerRow;
|
||||||
|
use App\Models\MarkList;
|
||||||
|
use App\Models\Invoice;
|
||||||
|
use App\Models\InvoiceItem;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Maatwebsite\Excel\Facades\Excel;
|
||||||
|
use Carbon\Carbon;
|
||||||
|
use Barryvdh\DomPDF\Facade\Pdf;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
|
||||||
|
class ContainerController extends Controller
|
||||||
|
{
|
||||||
|
public function index()
|
||||||
|
{
|
||||||
|
$containers = Container::with('rows')->latest()->get();
|
||||||
|
|
||||||
|
$containers->each(function ($container) {
|
||||||
|
$rows = $container->rows;
|
||||||
|
|
||||||
|
$totalCtn = 0;
|
||||||
|
$totalQty = 0;
|
||||||
|
$totalCbm = 0;
|
||||||
|
$totalKg = 0;
|
||||||
|
|
||||||
|
$ctnKeys = ['CTN', 'CTNS'];
|
||||||
|
$qtyKeys = ['ITLQTY', 'TOTALQTY', 'TTLQTY', 'QTY', 'PCS', 'PIECES'];
|
||||||
|
$cbmKeys = ['TOTALCBM', 'TTLCBM', 'ITLCBM', 'CBM'];
|
||||||
|
$kgKeys = ['TOTALKG', 'TTKG', 'KG', 'WEIGHT'];
|
||||||
|
|
||||||
|
$getFirstNumeric = function (array $data, array $possibleKeys) {
|
||||||
|
$normalizedMap = [];
|
||||||
|
|
||||||
|
foreach ($data as $key => $value) {
|
||||||
|
if ($key === null || $key === '') continue;
|
||||||
|
|
||||||
|
$normKey = strtoupper((string)$key);
|
||||||
|
$normKey = str_replace([' ', '/', '-', '.'], '', $normKey);
|
||||||
|
$normalizedMap[$normKey] = $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($possibleKeys as $search) {
|
||||||
|
$normSearch = strtoupper($search);
|
||||||
|
$normSearch = str_replace([' ', '/', '-', '.'], '', $normSearch);
|
||||||
|
|
||||||
|
foreach ($normalizedMap as $nKey => $value) {
|
||||||
|
if (
|
||||||
|
strpos($nKey, $normSearch) !== false &&
|
||||||
|
(is_numeric($value) || (is_string($value) && is_numeric(trim($value))))
|
||||||
|
) {
|
||||||
|
return (float) trim($value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
foreach ($rows as $row) {
|
||||||
|
$data = $row->data ?? [];
|
||||||
|
|
||||||
|
$totalCtn += $getFirstNumeric($data, $ctnKeys);
|
||||||
|
$totalQty += $getFirstNumeric($data, $qtyKeys);
|
||||||
|
$totalCbm += $getFirstNumeric($data, $cbmKeys);
|
||||||
|
$totalKg += $getFirstNumeric($data, $kgKeys);
|
||||||
|
}
|
||||||
|
|
||||||
|
$container->summary = [
|
||||||
|
'total_ctn' => round($totalCtn, 2),
|
||||||
|
'total_qty' => round($totalQty, 2),
|
||||||
|
'total_cbm' => round($totalCbm, 3),
|
||||||
|
'total_kg' => round($totalKg, 2),
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
return view('admin.container', compact('containers'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function create()
|
||||||
|
{
|
||||||
|
return view('admin.container_create');
|
||||||
|
}
|
||||||
|
|
||||||
|
private function isValidExcelFormat($rows, $header)
|
||||||
|
{
|
||||||
|
if (empty($header) || count($rows) < 2) return false;
|
||||||
|
|
||||||
|
$validKeywords = [
|
||||||
|
'MARK', 'DESCRIPTION', 'DESC', 'CTN', 'CTNS', 'QTY', 'TOTALQTY', 'ITLQTY', 'ITL QTY',
|
||||||
|
'UNIT', 'CBM', 'TOTAL CBM', 'KG', 'TOTAL KG',
|
||||||
|
'METAL BUCKLE', 'WATCH MOVEMENT', 'STEEL BOTTLE',
|
||||||
|
'MEHULPAID', 'ITEM NO', 'ITEM NO.', 'SAHILPAID', 'PINAKIN', 'GST',
|
||||||
|
'MOON LAMP', 'TRANSPARENT BOTTLE', 'PLASTIC FONDANT',
|
||||||
|
];
|
||||||
|
|
||||||
|
$headerText = implode(' ', array_map('strtoupper', $header));
|
||||||
|
$requiredHeaders = ['CTN', 'QTY', 'DESCRIPTION', 'DESC'];
|
||||||
|
|
||||||
|
$hasValidHeaders = false;
|
||||||
|
foreach ($requiredHeaders as $key) {
|
||||||
|
if (stripos($headerText, $key) !== false) {
|
||||||
|
$hasValidHeaders = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$hasValidHeaders) return false;
|
||||||
|
|
||||||
|
$dataPreview = '';
|
||||||
|
for ($i = 0; $i < min(5, count($rows)); $i++) {
|
||||||
|
$rowText = implode(' ', array_slice($rows[$i], 0, 10));
|
||||||
|
$dataPreview .= ' ' . strtoupper((string)$rowText);
|
||||||
|
}
|
||||||
|
|
||||||
|
$validMatches = 0;
|
||||||
|
foreach ($validKeywords as $keyword) {
|
||||||
|
if (stripos($headerText . $dataPreview, strtoupper($keyword)) !== false) {
|
||||||
|
$validMatches++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $validMatches >= 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function normalizeKey($value): string
|
||||||
|
{
|
||||||
|
$norm = strtoupper((string)$value);
|
||||||
|
return str_replace([' ', '/', '-', '.'], '', $norm);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function store(Request $request)
|
||||||
|
{
|
||||||
|
$request->validate([
|
||||||
|
'container_name' => 'required|string',
|
||||||
|
'container_number' => 'required|string|unique:containers,container_number',
|
||||||
|
'container_date' => 'required|date',
|
||||||
|
'excel_file' => 'required|file|mimes:xls,xlsx',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$file = $request->file('excel_file');
|
||||||
|
$sheets = Excel::toArray([], $file);
|
||||||
|
$rows = $sheets[0] ?? [];
|
||||||
|
|
||||||
|
if (count($rows) < 2) {
|
||||||
|
return back()
|
||||||
|
->withErrors(['excel_file' => 'Excel file is empty.'])
|
||||||
|
->withInput();
|
||||||
|
}
|
||||||
|
|
||||||
|
// HEADER DETECTION
|
||||||
|
$headerRowIndex = null;
|
||||||
|
$header = [];
|
||||||
|
|
||||||
|
foreach ($rows as $i => $row) {
|
||||||
|
$trimmed = array_map(fn($v) => trim((string)$v), $row);
|
||||||
|
$nonEmpty = array_filter($trimmed, fn($v) => $v !== '');
|
||||||
|
if (empty($nonEmpty)) continue;
|
||||||
|
|
||||||
|
if (count($nonEmpty) >= 4) {
|
||||||
|
$headerRowIndex = $i;
|
||||||
|
$header = $trimmed;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($headerRowIndex === null) {
|
||||||
|
return back()
|
||||||
|
->withErrors(['excel_file' => 'Header row not found in Excel.'])
|
||||||
|
->withInput();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$this->isValidExcelFormat($rows, $header)) {
|
||||||
|
return back()
|
||||||
|
->withErrors(['excel_file' => 'Only MEHUL / SAHIL / PINAKIN / GST loading list formats allowed.'])
|
||||||
|
->withInput();
|
||||||
|
}
|
||||||
|
|
||||||
|
// COLUMN INDEXES
|
||||||
|
$essentialColumns = [
|
||||||
|
'desc_col' => null,
|
||||||
|
'ctn_col' => null,
|
||||||
|
'qty_col' => null,
|
||||||
|
'totalqty_col' => null,
|
||||||
|
'unit_col' => null,
|
||||||
|
'price_col' => null,
|
||||||
|
'amount_col' => null,
|
||||||
|
'cbm_col' => null,
|
||||||
|
'totalcbm_col' => null,
|
||||||
|
'kg_col' => null,
|
||||||
|
'totalkg_col' => null,
|
||||||
|
'itemno_col' => null,
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($header as $colIndex => $headingText) {
|
||||||
|
if (empty($headingText)) continue;
|
||||||
|
|
||||||
|
$normalized = $this->normalizeKey($headingText);
|
||||||
|
|
||||||
|
if (strpos($normalized, 'DESCRIPTION') !== false || strpos($normalized, 'DESC') !== false) {
|
||||||
|
$essentialColumns['desc_col'] = $colIndex;
|
||||||
|
} elseif (strpos($normalized, 'CTN') !== false || strpos($normalized, 'CTNS') !== false) {
|
||||||
|
$essentialColumns['ctn_col'] = $colIndex;
|
||||||
|
} elseif (
|
||||||
|
strpos($normalized, 'ITLQTY') !== false ||
|
||||||
|
strpos($normalized, 'TOTALQTY') !== false ||
|
||||||
|
strpos($normalized, 'TTLQTY') !== false
|
||||||
|
) {
|
||||||
|
$essentialColumns['totalqty_col'] = $colIndex;
|
||||||
|
} elseif (strpos($normalized, 'QTY') !== false) {
|
||||||
|
$essentialColumns['qty_col'] = $colIndex;
|
||||||
|
} elseif (strpos($normalized, 'UNIT') !== false) {
|
||||||
|
$essentialColumns['unit_col'] = $colIndex;
|
||||||
|
} elseif (strpos($normalized, 'PRICE') !== false) {
|
||||||
|
$essentialColumns['price_col'] = $colIndex;
|
||||||
|
} elseif (strpos($normalized, 'AMOUNT') !== false) {
|
||||||
|
$essentialColumns['amount_col'] = $colIndex;
|
||||||
|
} elseif (strpos($normalized, 'TOTALCBM') !== false || strpos($normalized, 'ITLCBM') !== false) {
|
||||||
|
$essentialColumns['totalcbm_col'] = $colIndex;
|
||||||
|
} elseif (strpos($normalized, 'CBM') !== false) {
|
||||||
|
$essentialColumns['cbm_col'] = $colIndex;
|
||||||
|
} elseif (strpos($normalized, 'TOTALKG') !== false || strpos($normalized, 'TTKG') !== false) {
|
||||||
|
$essentialColumns['totalkg_col'] = $colIndex;
|
||||||
|
} elseif (strpos($normalized, 'KG') !== false) {
|
||||||
|
$essentialColumns['kg_col'] = $colIndex;
|
||||||
|
} elseif (
|
||||||
|
strpos($normalized, 'MARKNO') !== false ||
|
||||||
|
strpos($normalized, 'MARK') !== false ||
|
||||||
|
strpos($normalized, 'ITEMNO') !== false ||
|
||||||
|
strpos($normalized, 'ITEM') !== false
|
||||||
|
) {
|
||||||
|
$essentialColumns['itemno_col'] = $colIndex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_null($essentialColumns['itemno_col'])) {
|
||||||
|
return back()
|
||||||
|
->withErrors(['excel_file' => 'Mark / Item column not found in Excel (expected headers like MARK NO / Mark_No / Item_No).'])
|
||||||
|
->withInput();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ROWS CLEANING
|
||||||
|
$dataRows = array_slice($rows, $headerRowIndex + 1);
|
||||||
|
$cleanedRows = [];
|
||||||
|
$unmatchedRowsData = [];
|
||||||
|
|
||||||
|
foreach ($dataRows as $offset => $row) {
|
||||||
|
$trimmedRow = array_map(fn($v) => trim((string)$v), $row);
|
||||||
|
$nonEmptyCells = array_filter($trimmedRow, fn($v) => $v !== '');
|
||||||
|
if (count($nonEmptyCells) < 2) continue;
|
||||||
|
|
||||||
|
$rowText = strtoupper(implode(' ', $trimmedRow));
|
||||||
|
if (
|
||||||
|
stripos($rowText, 'TOTAL') !== false ||
|
||||||
|
stripos($rowText, 'TTL') !== false ||
|
||||||
|
stripos($rowText, 'GRAND') !== false
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$descValue = '';
|
||||||
|
if ($essentialColumns['desc_col'] !== null) {
|
||||||
|
$descValue = trim($row[$essentialColumns['desc_col']] ?? '');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($essentialColumns['desc_col'] !== null && $descValue === '' && count($nonEmptyCells) >= 1) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$cleanedRows[] = [
|
||||||
|
'row' => $row,
|
||||||
|
'offset' => $offset,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($cleanedRows)) {
|
||||||
|
return back()
|
||||||
|
->withErrors(['excel_file' => 'No valid item rows found in Excel.'])
|
||||||
|
->withInput();
|
||||||
|
}
|
||||||
|
|
||||||
|
// FORMULA CHECK
|
||||||
|
$cleanNumber = function ($value) {
|
||||||
|
if (is_string($value)) {
|
||||||
|
$value = str_replace(',', '', trim($value));
|
||||||
|
}
|
||||||
|
return is_numeric($value) ? (float)$value : 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
$formulaErrors = [];
|
||||||
|
|
||||||
|
foreach ($cleanedRows as $item) {
|
||||||
|
$row = $item['row'];
|
||||||
|
$offset = $item['offset'];
|
||||||
|
|
||||||
|
$ctn = $essentialColumns['ctn_col'] !== null ? $cleanNumber($row[$essentialColumns['ctn_col']] ?? 0) : 0;
|
||||||
|
$qty = $essentialColumns['qty_col'] !== null ? $cleanNumber($row[$essentialColumns['qty_col']] ?? 0) : 0;
|
||||||
|
$ttlQ = $essentialColumns['totalqty_col'] !== null ? $cleanNumber($row[$essentialColumns['totalqty_col']] ?? 0) : 0;
|
||||||
|
$cbm = $essentialColumns['cbm_col'] !== null ? $cleanNumber($row[$essentialColumns['cbm_col']] ?? 0) : 0;
|
||||||
|
$ttlC = $essentialColumns['totalcbm_col'] !== null ? $cleanNumber($row[$essentialColumns['totalcbm_col']] ?? 0) : 0;
|
||||||
|
$kg = $essentialColumns['kg_col'] !== null ? $cleanNumber($row[$essentialColumns['kg_col']] ?? 0) : 0;
|
||||||
|
$ttlK = $essentialColumns['totalkg_col'] !== null ? $cleanNumber($row[$essentialColumns['totalkg_col']] ?? 0) : 0;
|
||||||
|
|
||||||
|
$price = $essentialColumns['price_col'] !== null ? $cleanNumber($row[$essentialColumns['price_col']] ?? 0) : 0;
|
||||||
|
$ttlAmount = $essentialColumns['amount_col'] !== null ? $cleanNumber($row[$essentialColumns['amount_col']] ?? 0) : 0;
|
||||||
|
|
||||||
|
$desc = $essentialColumns['desc_col'] !== null ? (string)($row[$essentialColumns['desc_col']] ?? '') : '';
|
||||||
|
$mark = $essentialColumns['itemno_col'] !== null ? (string)($row[$essentialColumns['itemno_col']] ?? '') : '';
|
||||||
|
|
||||||
|
$expTtlQty = $qty * $ctn;
|
||||||
|
$expTtlCbm = $cbm * $ctn;
|
||||||
|
$expTtlKg = $kg * $ctn;
|
||||||
|
$expTtlAmount = ($qty * $ctn) * $price;
|
||||||
|
|
||||||
|
$rowErrors = [];
|
||||||
|
|
||||||
|
if (abs($ttlQ - $expTtlQty) > 0.01) {
|
||||||
|
$rowErrors['TOTAL QTY'] = [
|
||||||
|
'actual' => $ttlQ,
|
||||||
|
'expected' => $expTtlQty,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (abs($ttlC - $expTtlCbm) > 0.0005) {
|
||||||
|
$rowErrors['TOTAL CBM'] = [
|
||||||
|
'actual' => $ttlC,
|
||||||
|
'expected' => $expTtlCbm,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (abs($ttlK - $expTtlKg) > 0.01) {
|
||||||
|
$rowErrors['TOTAL KG'] = [
|
||||||
|
'actual' => $ttlK,
|
||||||
|
'expected' => $expTtlKg,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($essentialColumns['amount_col'] !== null && $essentialColumns['price_col'] !== null) {
|
||||||
|
if (abs($ttlAmount - $expTtlAmount) > 0.01) {
|
||||||
|
$rowErrors['TOTAL AMOUNT'] = [
|
||||||
|
'actual' => $ttlAmount,
|
||||||
|
'expected' => $expTtlAmount,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($rowErrors)) {
|
||||||
|
$rowData = [];
|
||||||
|
foreach ($header as $colIndex => $headingText) {
|
||||||
|
$value = $row[$colIndex] ?? null;
|
||||||
|
if (is_string($value)) $value = trim($value);
|
||||||
|
$rowData[$headingText] = $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
$formulaErrors[] = [
|
||||||
|
'excel_row' => $headerRowIndex + 1 + $offset,
|
||||||
|
'mark_no' => $mark,
|
||||||
|
'description' => $desc,
|
||||||
|
'errors' => $rowErrors,
|
||||||
|
'data' => $rowData,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK CHECK
|
||||||
|
$marksFromExcel = [];
|
||||||
|
foreach ($cleanedRows as $item) {
|
||||||
|
$row = $item['row'];
|
||||||
|
$rawMark = $row[$essentialColumns['itemno_col']] ?? null;
|
||||||
|
$mark = trim((string)($rawMark ?? ''));
|
||||||
|
if ($mark !== '') {
|
||||||
|
$marksFromExcel[] = $mark;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$marksFromExcel = array_values(array_unique($marksFromExcel));
|
||||||
|
|
||||||
|
if (empty($marksFromExcel)) {
|
||||||
|
return back()
|
||||||
|
->withErrors(['excel_file' => 'No mark numbers found in Excel file.'])
|
||||||
|
->withInput();
|
||||||
|
}
|
||||||
|
|
||||||
|
$validMarks = MarkList::whereIn('mark_no', $marksFromExcel)
|
||||||
|
->where('status', 'active')
|
||||||
|
->pluck('mark_no')
|
||||||
|
->toArray();
|
||||||
|
|
||||||
|
$unmatchedMarks = array_values(array_diff($marksFromExcel, $validMarks));
|
||||||
|
|
||||||
|
$markErrors = [];
|
||||||
|
|
||||||
|
if (!empty($unmatchedMarks)) {
|
||||||
|
foreach ($cleanedRows as $item) {
|
||||||
|
$row = $item['row'];
|
||||||
|
$offset = $item['offset'];
|
||||||
|
$rowMark = trim((string)($row[$essentialColumns['itemno_col']] ?? ''));
|
||||||
|
|
||||||
|
if ($rowMark === '' || !in_array($rowMark, $unmatchedMarks)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$rowData = [];
|
||||||
|
foreach ($header as $colIndex => $headingText) {
|
||||||
|
$value = $row[$colIndex] ?? null;
|
||||||
|
if (is_string($value)) $value = trim($value);
|
||||||
|
$rowData[$headingText] = $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
$markErrors[] = [
|
||||||
|
'excel_row' => $headerRowIndex + 1 + $offset,
|
||||||
|
'mark_no' => $rowMark,
|
||||||
|
'data' => $rowData,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($formulaErrors) || !empty($markErrors)) {
|
||||||
|
return back()
|
||||||
|
->withInput()
|
||||||
|
->with([
|
||||||
|
'formula_errors' => $formulaErrors,
|
||||||
|
'mark_errors' => $markErrors,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// STEP 1: Marks → customers mapping + grouping
|
||||||
|
$markRecords = MarkList::whereIn('mark_no', $marksFromExcel)
|
||||||
|
->where('status', 'active')
|
||||||
|
->get();
|
||||||
|
|
||||||
|
$markToCustomerId = [];
|
||||||
|
$markToSnapshot = [];
|
||||||
|
|
||||||
|
foreach ($markRecords as $mr) {
|
||||||
|
$markToCustomerId[$mr->mark_no] = $mr->customer_id;
|
||||||
|
|
||||||
|
$markToSnapshot[$mr->mark_no] = [
|
||||||
|
'customer_name' => $mr->customer_name,
|
||||||
|
'company_name' => $mr->company_name,
|
||||||
|
'mobile_no' => $mr->mobile_no,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$groupedByCustomer = [];
|
||||||
|
|
||||||
|
foreach ($cleanedRows as $item) {
|
||||||
|
$row = $item['row'];
|
||||||
|
$offset = $item['offset'];
|
||||||
|
|
||||||
|
$rawMark = $row[$essentialColumns['itemno_col']] ?? null;
|
||||||
|
$mark = trim((string)($rawMark ?? ''));
|
||||||
|
|
||||||
|
if ($mark === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$customerId = $markToCustomerId[$mark] ?? null;
|
||||||
|
if (!$customerId) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isset($groupedByCustomer[$customerId])) {
|
||||||
|
$groupedByCustomer[$customerId] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$groupedByCustomer[$customerId][] = [
|
||||||
|
'row' => $row,
|
||||||
|
'offset' => $offset,
|
||||||
|
'mark' => $mark,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// STEP 2: Container + ContainerRows save
|
||||||
|
$container = Container::create([
|
||||||
|
'container_name' => $request->container_name,
|
||||||
|
'container_number' => $request->container_number,
|
||||||
|
'container_date' => $request->container_date,
|
||||||
|
'status' => 'pending',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$path = $file->store('containers');
|
||||||
|
$container->update(['excel_file' => $path]);
|
||||||
|
|
||||||
|
$savedCount = 0;
|
||||||
|
|
||||||
|
foreach ($cleanedRows as $item) {
|
||||||
|
$row = $item['row'];
|
||||||
|
$offset = $item['offset'];
|
||||||
|
|
||||||
|
$data = [];
|
||||||
|
foreach ($header as $colIndex => $headingText) {
|
||||||
|
$value = $row[$colIndex] ?? null;
|
||||||
|
if (is_string($value)) $value = trim($value);
|
||||||
|
$data[$headingText] = $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
ContainerRow::create([
|
||||||
|
'container_id' => $container->id,
|
||||||
|
'row_index' => $headerRowIndex + 1 + $offset,
|
||||||
|
'data' => $data,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$savedCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// STEP 3: per-customer invoices + invoice items
|
||||||
|
$invoiceCount = 0;
|
||||||
|
|
||||||
|
foreach ($groupedByCustomer as $customerId => $rowsForCustomer) {
|
||||||
|
if (empty($rowsForCustomer)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$firstMark = $rowsForCustomer[0]['mark'];
|
||||||
|
$snap = $markToSnapshot[$firstMark] ?? null;
|
||||||
|
|
||||||
|
// ✅ Customer User model वरून fetch करा (customer_id string आहे जसे CID-2025-000001)
|
||||||
|
$customerUser = \App\Models\User::where('customer_id', $customerId)->first();
|
||||||
|
|
||||||
|
$invoice = new Invoice();
|
||||||
|
$invoice->container_id = $container->id;
|
||||||
|
$invoice->customer_id = $customerUser->id ?? null; // ✅ integer id store करतोय
|
||||||
|
$invoice->mark_no = $firstMark;
|
||||||
|
|
||||||
|
$invoice->invoice_number = $this->generateInvoiceNumber();
|
||||||
|
|
||||||
|
// invoice_date = container_date
|
||||||
|
$invoice->invoice_date = $container->container_date;
|
||||||
|
|
||||||
|
// due_date = container_date + 10 days
|
||||||
|
$invoice->due_date = Carbon::parse($invoice->invoice_date)
|
||||||
|
->addDays(10)
|
||||||
|
->format('Y-m-d');
|
||||||
|
|
||||||
|
// ✅ Snapshot data from MarkList (backward compatibility)
|
||||||
|
if ($snap) {
|
||||||
|
$invoice->customer_name = $snap['customer_name'] ?? null;
|
||||||
|
$invoice->company_name = $snap['company_name'] ?? null;
|
||||||
|
$invoice->customer_mobile = $snap['mobile_no'] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ User model वरून email, address, pincode घ्या
|
||||||
|
if ($customerUser) {
|
||||||
|
$invoice->customer_email = $customerUser->email ?? null;
|
||||||
|
$invoice->customer_address = $customerUser->address ?? null;
|
||||||
|
$invoice->pincode = $customerUser->pincode ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$invoice->final_amount = 0;
|
||||||
|
$invoice->gst_percent = 0;
|
||||||
|
$invoice->gst_amount = 0;
|
||||||
|
$invoice->final_amount_with_gst = 0;
|
||||||
|
|
||||||
|
$uniqueMarks = array_unique(array_column($rowsForCustomer, 'mark'));
|
||||||
|
$invoice->notes = 'Auto-created from Container ' . $container->container_number
|
||||||
|
. ' for Mark(s): ' . implode(', ', $uniqueMarks);
|
||||||
|
$invoice->pdf_path = null;
|
||||||
|
$invoice->status = 'pending';
|
||||||
|
|
||||||
|
$invoice->save();
|
||||||
|
$invoiceCount++;
|
||||||
|
|
||||||
|
$totalAmount = 0;
|
||||||
|
|
||||||
|
foreach ($rowsForCustomer as $item) {
|
||||||
|
$row = $item['row'];
|
||||||
|
$offset = $item['offset'];
|
||||||
|
|
||||||
|
$description = $essentialColumns['desc_col'] !== null ? ($row[$essentialColumns['desc_col']] ?? null) : null;
|
||||||
|
$ctn = $essentialColumns['ctn_col'] !== null ? (int) ($row[$essentialColumns['ctn_col']] ?? 0) : 0;
|
||||||
|
$qty = $essentialColumns['qty_col'] !== null ? (int) ($row[$essentialColumns['qty_col']] ?? 0) : 0;
|
||||||
|
$ttlQty = $essentialColumns['totalqty_col'] !== null ? (int) ($row[$essentialColumns['totalqty_col']] ?? 0) : $qty;
|
||||||
|
$unit = $essentialColumns['unit_col'] !== null ? ($row[$essentialColumns['unit_col']] ?? null) : null;
|
||||||
|
$price = $essentialColumns['price_col'] !== null ? (float) ($row[$essentialColumns['price_col']] ?? 0) : 0;
|
||||||
|
$ttlAmount = $essentialColumns['amount_col'] !== null ? (float) ($row[$essentialColumns['amount_col']] ?? 0) : 0;
|
||||||
|
$cbm = $essentialColumns['cbm_col'] !== null ? (float) ($row[$essentialColumns['cbm_col']] ?? 0) : 0;
|
||||||
|
$ttlCbm = $essentialColumns['totalcbm_col'] !== null ? (float) ($row[$essentialColumns['totalcbm_col']] ?? $cbm) : $cbm;
|
||||||
|
$kg = $essentialColumns['kg_col'] !== null ? (float) ($row[$essentialColumns['kg_col']] ?? 0) : 0;
|
||||||
|
$ttlKg = $essentialColumns['totalkg_col'] !== null ? (float) ($row[$essentialColumns['totalkg_col']] ?? $kg) : $kg;
|
||||||
|
|
||||||
|
$rowIndex = $headerRowIndex + 1 + $offset;
|
||||||
|
|
||||||
|
InvoiceItem::create([
|
||||||
|
'invoice_id' => $invoice->id,
|
||||||
|
'container_id' => $container->id,
|
||||||
|
'container_row_index' => $rowIndex,
|
||||||
|
'description' => $description,
|
||||||
|
'ctn' => $ctn,
|
||||||
|
'qty' => $qty,
|
||||||
|
'ttl_qty' => $ttlQty,
|
||||||
|
'unit' => $unit,
|
||||||
|
'price' => $price,
|
||||||
|
'ttl_amount' => $ttlAmount,
|
||||||
|
'cbm' => $cbm,
|
||||||
|
'ttl_cbm' => $ttlCbm,
|
||||||
|
'kg' => $kg,
|
||||||
|
'ttl_kg' => $ttlKg,
|
||||||
|
'shop_no' => null,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$totalAmount += $ttlAmount;
|
||||||
|
}
|
||||||
|
|
||||||
|
$invoice->final_amount = $totalAmount;
|
||||||
|
$invoice->gst_percent = 0;
|
||||||
|
$invoice->gst_amount = 0;
|
||||||
|
$invoice->final_amount_with_gst = $totalAmount;
|
||||||
|
|
||||||
|
$invoice->save();
|
||||||
|
}
|
||||||
|
|
||||||
|
$msg = "Container '{$container->container_number}' created with {$savedCount} rows and {$invoiceCount} customer invoice(s).";
|
||||||
|
return redirect()->route('containers.index')->with('success', $msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function show(Container $container)
|
||||||
|
{
|
||||||
|
$container->load('rows');
|
||||||
|
|
||||||
|
// paid / paying invoices च्या row indexes collect करा
|
||||||
|
$lockedRowIndexes = \App\Models\Invoice::whereIn('invoices.status', ['paid', 'paying'])
|
||||||
|
->where('invoices.container_id', $container->id)
|
||||||
|
->join('invoice_items', 'invoices.id', '=', 'invoice_items.invoice_id')
|
||||||
|
->pluck('invoice_items.container_row_index')
|
||||||
|
->filter()
|
||||||
|
->unique()
|
||||||
|
->values()
|
||||||
|
->toArray();
|
||||||
|
|
||||||
|
return view('admin.container_show', compact('container', 'lockedRowIndexes'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function updateRows(Request $request, Container $container)
|
||||||
|
{
|
||||||
|
$rowsInput = $request->input('rows', []);
|
||||||
|
|
||||||
|
foreach ($rowsInput as $rowId => $cols) {
|
||||||
|
$row = ContainerRow::where('container_id', $container->id)
|
||||||
|
->where('id', $rowId)
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if (!$row) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1) update container_rows.data
|
||||||
|
$data = $row->data ?? [];
|
||||||
|
foreach ($cols as $colHeader => $value) {
|
||||||
|
$data[$colHeader] = $value;
|
||||||
|
}
|
||||||
|
$row->update(['data' => $data]);
|
||||||
|
|
||||||
|
// 2) normalize keys
|
||||||
|
$normalizedMap = [];
|
||||||
|
foreach ($data as $key => $value) {
|
||||||
|
if ($key === null || $key === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$normKey = strtoupper((string)$key);
|
||||||
|
$normKey = str_replace([' ', '/', '-', '.'], '', $normKey);
|
||||||
|
$normalizedMap[$normKey] = $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// helper: get first numeric value from given keys
|
||||||
|
$getFirstNumeric = function (array $map, array $possibleKeys) {
|
||||||
|
foreach ($possibleKeys as $search) {
|
||||||
|
$normSearch = strtoupper($search);
|
||||||
|
$normSearch = str_replace([' ', '/', '-', '.'], '', $normSearch);
|
||||||
|
|
||||||
|
foreach ($map as $nKey => $value) {
|
||||||
|
if (strpos($nKey, $normSearch) !== false) {
|
||||||
|
if (is_numeric($value)) {
|
||||||
|
return (float)$value;
|
||||||
|
}
|
||||||
|
if (is_string($value) && is_numeric(trim($value))) {
|
||||||
|
return (float)trim($value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 3) read values – QTY vs TTLQTY separately
|
||||||
|
$ctnKeys = ['CTN', 'CTNS'];
|
||||||
|
$qtyKeys = ['QTY', 'PCS', 'PIECES']; // per-carton qty
|
||||||
|
$ttlQtyKeys = ['ITLQTY', 'TOTALQTY', 'TTLQTY']; // total qty
|
||||||
|
$cbmKeys = ['TOTALCBM', 'TTLCBM', 'ITLCBM', 'CBM'];
|
||||||
|
$kgKeys = ['TOTALKG', 'TTKG', 'KG', 'WEIGHT'];
|
||||||
|
$amountKeys = ['AMOUNT', 'TTLAMOUNT', 'TOTALAMOUNT'];
|
||||||
|
|
||||||
|
$ctn = $getFirstNumeric($normalizedMap, $ctnKeys);
|
||||||
|
|
||||||
|
// per carton qty
|
||||||
|
$qty = $getFirstNumeric($normalizedMap, $qtyKeys);
|
||||||
|
|
||||||
|
// total qty direct from TOTALQTY/TTLQTY/ITLQTY
|
||||||
|
$ttlQ = $getFirstNumeric($normalizedMap, $ttlQtyKeys);
|
||||||
|
|
||||||
|
// if total column is 0 then compute ctn * qty
|
||||||
|
if ($ttlQ == 0 && $ctn && $qty) {
|
||||||
|
$ttlQ = $ctn * $qty;
|
||||||
|
}
|
||||||
|
|
||||||
|
$cbm = $getFirstNumeric($normalizedMap, ['CBM']);
|
||||||
|
$ttlC = $getFirstNumeric($normalizedMap, ['TOTALCBM', 'TTLCBM', 'ITLCBM']);
|
||||||
|
if ($ttlC == 0 && $cbm && $ctn) {
|
||||||
|
$ttlC = $cbm * $ctn;
|
||||||
|
}
|
||||||
|
|
||||||
|
$kg = $getFirstNumeric($normalizedMap, ['KG', 'WEIGHT']);
|
||||||
|
$ttlK = $getFirstNumeric($normalizedMap, ['TOTALKG', 'TTKG']);
|
||||||
|
if ($ttlK == 0 && $kg && $ctn) {
|
||||||
|
$ttlK = $kg * $ctn;
|
||||||
|
}
|
||||||
|
|
||||||
|
$price = $getFirstNumeric($normalizedMap, ['PRICE', 'RATE']);
|
||||||
|
$amount = $getFirstNumeric($normalizedMap, $amountKeys);
|
||||||
|
if ($amount == 0 && $price && $ttlQ) {
|
||||||
|
$amount = $price * $ttlQ;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4) get description
|
||||||
|
$desc = null;
|
||||||
|
foreach (['DESCRIPTION', 'DESC'] as $dKey) {
|
||||||
|
$normD = str_replace([' ', '/', '-', '.'], '', strtoupper($dKey));
|
||||||
|
foreach ($normalizedMap as $nKey => $v) {
|
||||||
|
if (strpos($nKey, $normD) !== false) {
|
||||||
|
$desc = is_string($v) ? trim($v) : $v;
|
||||||
|
break 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$rowIndex = $row->row_index;
|
||||||
|
|
||||||
|
// 5) find linked invoice_items
|
||||||
|
$items = InvoiceItem::where('container_id', $container->id)
|
||||||
|
->where('container_row_index', $rowIndex)
|
||||||
|
->get();
|
||||||
|
|
||||||
|
if ($items->isEmpty() && $desc) {
|
||||||
|
$items = InvoiceItem::where('container_id', $container->id)
|
||||||
|
->whereNull('container_row_index')
|
||||||
|
->where('description', $desc)
|
||||||
|
->get();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6) update invoice_items + recalc invoice totals
|
||||||
|
foreach ($items as $item) {
|
||||||
|
$item->description = $desc;
|
||||||
|
$item->ctn = $ctn;
|
||||||
|
$item->qty = $qty; // per carton
|
||||||
|
$item->ttl_qty = $ttlQ; // total
|
||||||
|
$item->price = $price;
|
||||||
|
$item->ttl_amount = $amount;
|
||||||
|
$item->cbm = $cbm;
|
||||||
|
$item->ttl_cbm = $ttlC;
|
||||||
|
$item->kg = $kg;
|
||||||
|
$item->ttl_kg = $ttlK;
|
||||||
|
$item->save();
|
||||||
|
|
||||||
|
$invoice = $item->invoice;
|
||||||
|
if ($invoice) {
|
||||||
|
$newBaseAmount = InvoiceItem::where('invoice_id', $invoice->id)
|
||||||
|
->sum('ttl_amount');
|
||||||
|
|
||||||
|
$taxType = $invoice->tax_type;
|
||||||
|
$cgstPercent = (float) ($invoice->cgst_percent ?? 0);
|
||||||
|
$sgstPercent = (float) ($invoice->sgst_percent ?? 0);
|
||||||
|
$igstPercent = (float) ($invoice->igst_percent ?? 0);
|
||||||
|
|
||||||
|
$gstPercent = 0;
|
||||||
|
if ($taxType === 'gst') {
|
||||||
|
$gstPercent = $cgstPercent + $sgstPercent;
|
||||||
|
} elseif ($taxType === 'igst') {
|
||||||
|
$gstPercent = $igstPercent;
|
||||||
|
}
|
||||||
|
|
||||||
|
$gstAmount = $newBaseAmount * $gstPercent / 100;
|
||||||
|
$finalWithGst = $newBaseAmount + $gstAmount;
|
||||||
|
|
||||||
|
$invoice->final_amount = $newBaseAmount;
|
||||||
|
$invoice->gst_amount = $gstAmount;
|
||||||
|
$invoice->final_amount_with_gst = $finalWithGst;
|
||||||
|
$invoice->gst_percent = $gstPercent;
|
||||||
|
$invoice->save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return redirect()
|
||||||
|
->route('containers.show', $container->id)
|
||||||
|
->with('success', 'Excel rows updated successfully.');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function updateStatus(Request $request, Container $container)
|
||||||
|
{
|
||||||
|
$request->validate([
|
||||||
|
'status' => 'required|in:container-ready,export-custom,international-transit,arrived-at-india,import-custom,warehouse,domestic-distribution,out-for-delivery,delivered',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$container->status = $request->status;
|
||||||
|
$container->save();
|
||||||
|
|
||||||
|
if ($request->wantsJson() || $request->ajax()) {
|
||||||
|
return response()->json([
|
||||||
|
'success' => true,
|
||||||
|
'status' => $container->status,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return back()->with('success', 'Container status updated.');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function destroy(Container $container)
|
||||||
|
{
|
||||||
|
$container->delete();
|
||||||
|
|
||||||
|
if (request()->wantsJson() || request()->ajax()) {
|
||||||
|
return response()->json([
|
||||||
|
'success' => true,
|
||||||
|
'message' => 'Container deleted',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return redirect()
|
||||||
|
->route('containers.index')
|
||||||
|
->with('success', 'Container deleted.');
|
||||||
|
}
|
||||||
|
|
||||||
|
private function generateInvoiceNumber(): string
|
||||||
|
{
|
||||||
|
$year = now()->format('Y');
|
||||||
|
|
||||||
|
$last = Invoice::whereYear('created_at', $year)
|
||||||
|
->orderBy('id', 'desc')
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if ($last) {
|
||||||
|
$parts = explode('-', $last->invoice_number);
|
||||||
|
$seq = 0;
|
||||||
|
|
||||||
|
if (count($parts) === 3) {
|
||||||
|
$seq = (int) $parts[2];
|
||||||
|
}
|
||||||
|
|
||||||
|
$nextSeq = $seq + 1;
|
||||||
|
} else {
|
||||||
|
$nextSeq = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'INV-' . $year . '-' . str_pad($nextSeq, 6, '0', STR_PAD_LEFT);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function downloadPdf(Container $container)
|
||||||
|
{
|
||||||
|
$container->load('rows');
|
||||||
|
|
||||||
|
$pdf = Pdf::loadView('admin.container_pdf', [
|
||||||
|
'container' => $container,
|
||||||
|
])->setPaper('a4', 'landscape');
|
||||||
|
|
||||||
|
$fileName = 'container-'.$container->container_number.'.pdf';
|
||||||
|
|
||||||
|
return $pdf->download($fileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function downloadExcel(Container $container)
|
||||||
|
{
|
||||||
|
if (!$container->excel_file) {
|
||||||
|
abort(404, 'Excel file not found on record.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stored path like "containers/abc.xlsx"
|
||||||
|
$path = $container->excel_file;
|
||||||
|
|
||||||
|
if (!Storage::exists($path)) {
|
||||||
|
abort(404, 'Excel file missing on server.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$fileName = 'container-'.$container->container_number.'.xlsx';
|
||||||
|
|
||||||
|
return Storage::download($path, $fileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function popupPopup(Container $container)
|
||||||
|
{
|
||||||
|
// existing show सारखाच data वापरू
|
||||||
|
$container->load('rows');
|
||||||
|
|
||||||
|
// summary आधीपासून index() मध्ये जसा काढतोस तसाच logic reuse
|
||||||
|
$rows = $container->rows ?? collect();
|
||||||
|
|
||||||
|
$totalCtn = 0;
|
||||||
|
$totalQty = 0;
|
||||||
|
$totalCbm = 0;
|
||||||
|
$totalKg = 0;
|
||||||
|
|
||||||
|
$ctnKeys = ['CTN', 'CTNS'];
|
||||||
|
$qtyKeys = ['ITLQTY', 'TOTALQTY', 'TTLQTY', 'QTY', 'PCS', 'PIECES'];
|
||||||
|
$cbmKeys = ['TOTALCBM', 'TTLCBM', 'ITLCBM', 'CBM'];
|
||||||
|
$kgKeys = ['TOTALKG', 'TTKG', 'KG', 'WEIGHT'];
|
||||||
|
|
||||||
|
$getFirstNumeric = function (array $data, array $possibleKeys) {
|
||||||
|
$normalizedMap = [];
|
||||||
|
foreach ($data as $key => $value) {
|
||||||
|
if ($key === null) continue;
|
||||||
|
$normKey = strtoupper((string)$key);
|
||||||
|
$normKey = str_replace([' ', ',', '-', '.', "\n", "\r", "\t"], '', $normKey);
|
||||||
|
$normalizedMap[$normKey] = $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($possibleKeys as $search) {
|
||||||
|
$normSearch = strtoupper($search);
|
||||||
|
$normSearch = str_replace([' ', ',', '-', '.', "\n", "\r", "\t"], '', $normSearch);
|
||||||
|
|
||||||
|
foreach ($normalizedMap as $nKey => $value) {
|
||||||
|
if (strpos($nKey, $normSearch) !== false) {
|
||||||
|
if (is_numeric($value)) {
|
||||||
|
return (float)$value;
|
||||||
|
}
|
||||||
|
if (is_string($value) && is_numeric(trim($value))) {
|
||||||
|
return (float)trim($value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
foreach ($rows as $row) {
|
||||||
|
$data = $row->data ?? [];
|
||||||
|
if (!is_array($data)) continue;
|
||||||
|
|
||||||
|
$totalCtn += $getFirstNumeric($data, $ctnKeys);
|
||||||
|
$totalQty += $getFirstNumeric($data, $qtyKeys);
|
||||||
|
$totalCbm += $getFirstNumeric($data, $cbmKeys);
|
||||||
|
$totalKg += $getFirstNumeric($data, $kgKeys);
|
||||||
|
}
|
||||||
|
|
||||||
|
$summary = [
|
||||||
|
'total_ctn' => round($totalCtn, 2),
|
||||||
|
'total_qty' => round($totalQty, 2),
|
||||||
|
'total_cbm' => round($totalCbm, 3),
|
||||||
|
'total_kg' => round($totalKg, 2),
|
||||||
|
];
|
||||||
|
|
||||||
|
return view('admin.partials.container_popup_readonly', [
|
||||||
|
'container' => $container,
|
||||||
|
'summary' => $summary,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -44,6 +44,8 @@ class RequestController extends Controller
|
|||||||
'pincode' => $request->pincode,
|
'pincode' => $request->pincode,
|
||||||
'date' => Carbon::now()->toDateString(), // Auto current date
|
'date' => Carbon::now()->toDateString(), // Auto current date
|
||||||
'status' => 'pending', // Default status
|
'status' => 'pending', // Default status
|
||||||
|
|
||||||
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// ✅ Response
|
// ✅ Response
|
||||||
@@ -53,4 +55,6 @@ class RequestController extends Controller
|
|||||||
'data' => $newRequest
|
'data' => $newRequest
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,9 +6,49 @@ use Illuminate\Http\Request;
|
|||||||
use Illuminate\Support\Facades\Hash;
|
use Illuminate\Support\Facades\Hash;
|
||||||
use PHPOpenSourceSaver\JWTAuth\Facades\JWTAuth;
|
use PHPOpenSourceSaver\JWTAuth\Facades\JWTAuth;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
|
||||||
class UserAuthController extends Controller
|
class UserAuthController extends Controller
|
||||||
{
|
{
|
||||||
|
|
||||||
|
public function refreshToken()
|
||||||
|
{
|
||||||
|
Log::info('🔄 [JWT-REFRESH] called');
|
||||||
|
|
||||||
|
try {
|
||||||
|
$newToken = JWTAuth::parseToken()->refresh();
|
||||||
|
|
||||||
|
Log::info('✅ [JWT-REFRESH] Token refreshed');
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'success' => true,
|
||||||
|
'token' => $newToken,
|
||||||
|
]);
|
||||||
|
|
||||||
|
} catch (\PHPOpenSourceSaver\JWTAuth\Exceptions\TokenExpiredException $e) {
|
||||||
|
Log::warning('⛔ [JWT-REFRESH] Refresh TTL expired');
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Refresh expired. Please login again.',
|
||||||
|
], 401);
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Log::error('🔥 [JWT-REFRESH] Exception', [
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Unable to refresh token.',
|
||||||
|
], 401);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* User Login
|
* User Login
|
||||||
*/
|
*/
|
||||||
@@ -60,6 +100,8 @@ class UserAuthController extends Controller
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* User Logout
|
* User Logout
|
||||||
*/
|
*/
|
||||||
|
|||||||
96
app/Http/Controllers/user/ChatController.php
Normal file
96
app/Http/Controllers/user/ChatController.php
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\User;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use App\Models\SupportTicket;
|
||||||
|
use App\Models\ChatMessage;
|
||||||
|
use App\Events\NewChatMessage;
|
||||||
|
|
||||||
|
class ChatController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Start chat or return existing ticket for this user
|
||||||
|
*/
|
||||||
|
public function startChat()
|
||||||
|
{
|
||||||
|
// One chat ticket per user
|
||||||
|
$ticket = SupportTicket::firstOrCreate([
|
||||||
|
'user_id' => auth()->id(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'success' => true,
|
||||||
|
'ticket' => $ticket
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load all messages for this ticket
|
||||||
|
*/
|
||||||
|
public function getMessages($ticketId)
|
||||||
|
{
|
||||||
|
// Ensure this ticket belongs to the logged-in user
|
||||||
|
$ticket = SupportTicket::where('id', $ticketId)
|
||||||
|
->where('user_id', auth()->id())
|
||||||
|
->firstOrFail();
|
||||||
|
|
||||||
|
$messages = ChatMessage::where('ticket_id', $ticketId)
|
||||||
|
->orderBy('created_at', 'asc')
|
||||||
|
->with('sender')
|
||||||
|
->get();
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'success' => true,
|
||||||
|
'messages' => $messages
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send text or file message from user → admin/staff
|
||||||
|
*/
|
||||||
|
public function sendMessage(Request $request, $ticketId)
|
||||||
|
{
|
||||||
|
$request->validate([
|
||||||
|
'message' => 'nullable|string',
|
||||||
|
'file' => 'nullable|file|max:20480', // 20MB limit
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Validate ticket ownership
|
||||||
|
$ticket = SupportTicket::where('id', $ticketId)
|
||||||
|
->where('user_id', auth()->id())
|
||||||
|
->firstOrFail();
|
||||||
|
|
||||||
|
$data = [
|
||||||
|
'ticket_id' => $ticketId,
|
||||||
|
'sender_id' => auth()->id(),
|
||||||
|
'sender_type' => \App\Models\User::class,
|
||||||
|
'message' => $request->message,
|
||||||
|
'client_id' => $request->client_id, // ✅ ADD
|
||||||
|
'read_by_admin' => false,
|
||||||
|
'read_by_user' => true,
|
||||||
|
];
|
||||||
|
|
||||||
|
// Handle file upload
|
||||||
|
if ($request->hasFile('file')) {
|
||||||
|
$path = $request->file('file')->store('chat', 'public');
|
||||||
|
$data['file_path'] = $path;
|
||||||
|
$data['file_type'] = $request->file('file')->getMimeType();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save message
|
||||||
|
$message = ChatMessage::create($data);
|
||||||
|
|
||||||
|
// Load sender info for broadcast
|
||||||
|
$message->load('sender');
|
||||||
|
|
||||||
|
// Fire real-time event
|
||||||
|
broadcast(new NewChatMessage($message));
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'success' => true,
|
||||||
|
'message' => $message
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
391
app/Http/Controllers/user/UserOrderController.php
Normal file
391
app/Http/Controllers/user/UserOrderController.php
Normal file
@@ -0,0 +1,391 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\User;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use PHPOpenSourceSaver\JWTAuth\Facades\JWTAuth;
|
||||||
|
|
||||||
|
class UserOrderController extends Controller
|
||||||
|
{
|
||||||
|
public function orderSummary()
|
||||||
|
{
|
||||||
|
// Authenticate user via JWT
|
||||||
|
$user = JWTAuth::parseToken()->authenticate();
|
||||||
|
|
||||||
|
if (!$user) {
|
||||||
|
return response()->json([
|
||||||
|
'status' => false,
|
||||||
|
'message' => 'Unauthorized'
|
||||||
|
], 401);
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------
|
||||||
|
// Get customer invoices with containers
|
||||||
|
// -------------------------------------
|
||||||
|
$invoices = $user->invoices()->with('container')->get();
|
||||||
|
|
||||||
|
// Unique containers for this customer
|
||||||
|
$containers = $invoices->pluck('container')->filter()->unique('id');
|
||||||
|
|
||||||
|
// -------------------------------------
|
||||||
|
// Counts based on container status
|
||||||
|
// -------------------------------------
|
||||||
|
$totalOrders = $containers->count();
|
||||||
|
|
||||||
|
$delivered = $containers->where('status', 'delivered')->count();
|
||||||
|
|
||||||
|
$inTransit = $containers->whereNotIn('status', [
|
||||||
|
'delivered',
|
||||||
|
'warehouse',
|
||||||
|
'domestic-distribution'
|
||||||
|
])->count();
|
||||||
|
|
||||||
|
$active = $totalOrders;
|
||||||
|
|
||||||
|
// -------------------------------------
|
||||||
|
// Total Amount = sum of invoice totals
|
||||||
|
// -------------------------------------
|
||||||
|
$totalAmount = $invoices->sum(function ($invoice) {
|
||||||
|
return $invoice->final_amount_with_gst ?? 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Format total amount in K, L, Cr
|
||||||
|
$formattedAmount = $this->formatIndianNumber($totalAmount);
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'status' => true,
|
||||||
|
'summary' => [
|
||||||
|
'active_orders' => $active,
|
||||||
|
'in_transit_orders' => $inTransit,
|
||||||
|
'delivered_orders' => $delivered,
|
||||||
|
'total_value' => $formattedAmount,
|
||||||
|
'total_raw' => $totalAmount
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert number into Indian Format:
|
||||||
|
* 1000 -> 1K
|
||||||
|
* 100000 -> 1L
|
||||||
|
* 10000000 -> 1Cr
|
||||||
|
*/
|
||||||
|
private function formatIndianNumber($num)
|
||||||
|
{
|
||||||
|
if ($num >= 10000000) {
|
||||||
|
return round($num / 10000000, 1) . 'Cr';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($num >= 100000) {
|
||||||
|
return round($num / 100000, 1) . 'L';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($num >= 1000) {
|
||||||
|
return round($num / 1000, 1) . 'K';
|
||||||
|
}
|
||||||
|
|
||||||
|
return (string)$num;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function allOrders()
|
||||||
|
{
|
||||||
|
$user = JWTAuth::parseToken()->authenticate();
|
||||||
|
|
||||||
|
if (!$user) {
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Unauthorized'
|
||||||
|
], 401);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get invoices with containers for this customer
|
||||||
|
$invoices = $user->invoices()
|
||||||
|
->with('container')
|
||||||
|
->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,
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'success' => true,
|
||||||
|
'orders' => $orders
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
->with(['items'])
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if (!$invoice) {
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Order not found for this user'
|
||||||
|
], 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
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// public function orderShipment($order_id)
|
||||||
|
// {
|
||||||
|
// $user = JWTAuth::parseToken()->authenticate();
|
||||||
|
|
||||||
|
// // Get order
|
||||||
|
// $order = $user->orders()->where('order_id', $order_id)->first();
|
||||||
|
|
||||||
|
// 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();
|
||||||
|
|
||||||
|
// return response()->json([
|
||||||
|
// 'success' => true,
|
||||||
|
// 'shipment' => $shipment
|
||||||
|
// ]);
|
||||||
|
// }
|
||||||
|
|
||||||
|
|
||||||
|
public function orderInvoice($order_id)
|
||||||
|
{
|
||||||
|
$user = JWTAuth::parseToken()->authenticate();
|
||||||
|
|
||||||
|
$order = $user->orders()
|
||||||
|
->with('invoice.items')
|
||||||
|
->where('order_id', $order_id)
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if (!$order) {
|
||||||
|
return response()->json(['success' => false, 'message' => 'Order not found'], 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'success' => true,
|
||||||
|
'invoice' => $order->invoice
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
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')
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if (!$invoice || !$invoice->container) {
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Order not found'
|
||||||
|
], 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
$container = $invoice->container;
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'success' => true,
|
||||||
|
'track' => [
|
||||||
|
'order_id' => $container->id,
|
||||||
|
'container_number' => $container->container_number,
|
||||||
|
'status' => $container->status,
|
||||||
|
'container_date' => $container->container_date,
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function allInvoices()
|
||||||
|
{
|
||||||
|
$user = JWTAuth::parseToken()->authenticate();
|
||||||
|
|
||||||
|
if (!$user) {
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Unauthorized'
|
||||||
|
], 401);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch all invoices of customer
|
||||||
|
$invoices = $user->invoices()
|
||||||
|
->withCount('installments')
|
||||||
|
->orderBy('id', 'desc')
|
||||||
|
->get()
|
||||||
|
->map(function ($invoice) {
|
||||||
|
return [
|
||||||
|
'invoice_id' => $invoice->id,
|
||||||
|
'invoice_number' => $invoice->invoice_number,
|
||||||
|
'invoice_date' => $invoice->invoice_date,
|
||||||
|
'status' => $invoice->status,
|
||||||
|
'amount' => $invoice->final_amount_with_gst,
|
||||||
|
'formatted_amount' => $this->formatIndianNumber($invoice->final_amount_with_gst),
|
||||||
|
'pdf_url' => $invoice->pdf_path ? url($invoice->pdf_path) : null,
|
||||||
|
'installment_count' => $invoice->installments_count,
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'success' => true,
|
||||||
|
'invoices' => $invoices
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function invoiceInstallmentsById($invoice_id)
|
||||||
|
{
|
||||||
|
$user = \PHPOpenSourceSaver\JWTAuth\Facades\JWTAuth::parseToken()->authenticate();
|
||||||
|
|
||||||
|
if (! $user) {
|
||||||
|
return response()->json(['success' => false, 'message' => 'Unauthorized'], 401);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find invoice by numeric id and ensure it belongs to logged-in user (invoice.customer_id = user.id)
|
||||||
|
$invoice = \App\Models\Invoice::where('id', (int)$invoice_id)
|
||||||
|
->where('customer_id', $user->id)
|
||||||
|
->with(['installments' => function($q){
|
||||||
|
$q->orderBy('installment_date', 'ASC')->orderBy('id', 'ASC');
|
||||||
|
}])
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if (! $invoice) {
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Invoice not found for this customer'
|
||||||
|
], 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'success' => true,
|
||||||
|
'invoice_id' => $invoice->id,
|
||||||
|
'invoice_number' => $invoice->invoice_number,
|
||||||
|
'installments' => $invoice->installments
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function invoiceDetails($invoice_id)
|
||||||
|
{
|
||||||
|
$user = JWTAuth::parseToken()->authenticate();
|
||||||
|
|
||||||
|
if (! $user) {
|
||||||
|
return response()->json(['success' => false, 'message' => 'Unauthorized'], 401);
|
||||||
|
}
|
||||||
|
|
||||||
|
$invoice = \App\Models\Invoice::where('id', $invoice_id)
|
||||||
|
->where('customer_id', $user->id)
|
||||||
|
->with('items')
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if (! $invoice) {
|
||||||
|
return response()->json(['success' => false, 'message' => 'Invoice not found'], 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'success' => true,
|
||||||
|
'invoice' => $invoice
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// public function confirmOrder($order_id)
|
||||||
|
// {
|
||||||
|
// $user = JWTAuth::parseToken()->authenticate();
|
||||||
|
|
||||||
|
// if (! $user) {
|
||||||
|
// return response()->json([
|
||||||
|
// 'success' => false,
|
||||||
|
// 'message' => 'Unauthorized'
|
||||||
|
// ], 401);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// $order = $user->orders()
|
||||||
|
// ->where('order_id', $order_id)
|
||||||
|
// ->first();
|
||||||
|
|
||||||
|
// if (! $order) {
|
||||||
|
// return response()->json([
|
||||||
|
// 'success' => false,
|
||||||
|
// 'message' => 'Order not found'
|
||||||
|
// ], 404);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // 🚫 Only allow confirm from order_placed
|
||||||
|
// if ($order->status !== 'order_placed') {
|
||||||
|
// return response()->json([
|
||||||
|
// 'success' => false,
|
||||||
|
// 'message' => 'Order cannot be confirmed'
|
||||||
|
// ], 422);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// $order->status = 'order_confirmed';
|
||||||
|
// $order->save();
|
||||||
|
|
||||||
|
// return response()->json([
|
||||||
|
// 'success' => true,
|
||||||
|
// 'message' => 'Order confirmed successfully'
|
||||||
|
// ]);
|
||||||
|
// }
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
149
app/Http/Controllers/user/UserProfileController.php
Normal file
149
app/Http/Controllers/user/UserProfileController.php
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\User;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use App\Models\UpdateRequest;
|
||||||
|
use PHPOpenSourceSaver\JWTAuth\Facades\JWTAuth;
|
||||||
|
|
||||||
|
class UserProfileController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Get user profile
|
||||||
|
*/
|
||||||
|
public function profile()
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$user = JWTAuth::parseToken()->authenticate();
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Token invalid or expired',
|
||||||
|
], 401);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! $user) {
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Unauthorized'
|
||||||
|
], 401);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'success' => true,
|
||||||
|
'data' => [
|
||||||
|
'customer_id' => $user->customer_id,
|
||||||
|
'customer_name' => $user->customer_name,
|
||||||
|
'company_name' => $user->company_name,
|
||||||
|
'designation' => $user->designation,
|
||||||
|
'email' => $user->email,
|
||||||
|
'mobile' => $user->mobile_no,
|
||||||
|
'address' => $user->address,
|
||||||
|
'pincode' => $user->pincode,
|
||||||
|
'status' => $user->status,
|
||||||
|
'customer_type' => $user->customer_type,
|
||||||
|
'profile_image' => $user->profile_image ? url($user->profile_image) : null,
|
||||||
|
'date' => $user->date,
|
||||||
|
'created_at' => $user->created_at,
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update profile IMAGE only (no admin approval)
|
||||||
|
*/
|
||||||
|
public function updateProfileImage(Request $request)
|
||||||
|
{
|
||||||
|
$user = JWTAuth::parseToken()->authenticate();
|
||||||
|
|
||||||
|
if (! $user) {
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Unauthorized'
|
||||||
|
], 401);
|
||||||
|
}
|
||||||
|
|
||||||
|
$request->validate([
|
||||||
|
'profile_image' => 'required|image|mimes:jpg,jpeg,png|max:2048'
|
||||||
|
]);
|
||||||
|
|
||||||
|
// DELETE OLD IMAGE
|
||||||
|
if ($user->profile_image && file_exists(public_path($user->profile_image))) {
|
||||||
|
@unlink(public_path($user->profile_image));
|
||||||
|
}
|
||||||
|
|
||||||
|
// SAVE NEW IMAGE
|
||||||
|
$file = $request->file('profile_image');
|
||||||
|
$filename = 'profile_' . time() . '.' . $file->getClientOriginalExtension();
|
||||||
|
$folder = 'profile_upload/';
|
||||||
|
$file->move(public_path($folder), $filename);
|
||||||
|
|
||||||
|
$user->profile_image = $folder . $filename;
|
||||||
|
$user->save();
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'success' => true,
|
||||||
|
'message' => 'Profile image updated successfully',
|
||||||
|
'data' => [
|
||||||
|
'customer_id' => $user->customer_id,
|
||||||
|
'customer_name' => $user->customer_name,
|
||||||
|
'company_name' => $user->company_name,
|
||||||
|
'designation' => $user->designation,
|
||||||
|
'email' => $user->email,
|
||||||
|
'mobile' => $user->mobile_no,
|
||||||
|
'address' => $user->address,
|
||||||
|
'pincode' => $user->pincode,
|
||||||
|
'status' => $user->status,
|
||||||
|
'customer_type' => $user->customer_type,
|
||||||
|
'profile_image' => url($user->profile_image),
|
||||||
|
'date' => $user->date,
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Submit profile update request (requires admin approval)
|
||||||
|
*/
|
||||||
|
public function updateProfileRequest(Request $request)
|
||||||
|
{
|
||||||
|
$user = JWTAuth::parseToken()->authenticate();
|
||||||
|
|
||||||
|
if (! $user) {
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Unauthorized'
|
||||||
|
], 401);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate input
|
||||||
|
$request->validate([
|
||||||
|
'customer_name' => 'nullable|string|max:255',
|
||||||
|
'company_name' => 'nullable|string|max:255',
|
||||||
|
'designation' => 'nullable|string|max:255',
|
||||||
|
'email' => 'nullable|email',
|
||||||
|
'mobile_no' => 'nullable|string|max:15',
|
||||||
|
'address' => 'nullable|string',
|
||||||
|
'pincode' => 'nullable|string|max:10'
|
||||||
|
]);
|
||||||
|
|
||||||
|
// SAVE AS ARRAY (NOT JSON STRING!)
|
||||||
|
$updateReq = \App\Models\UpdateRequest::create([
|
||||||
|
'user_id' => $user->id,
|
||||||
|
'data' => $request->all(), // <---- FIXED
|
||||||
|
'status' => 'pending',
|
||||||
|
]);
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'success' => true,
|
||||||
|
'message' => 'Profile update request submitted. Waiting for admin approval.',
|
||||||
|
'request_id' => $updateReq->id
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
37
app/Http/Middleware/JwtRefreshMiddleware.php
Normal file
37
app/Http/Middleware/JwtRefreshMiddleware.php
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Middleware;
|
||||||
|
|
||||||
|
use Closure;
|
||||||
|
use Tymon\JWTAuth\Facades\JWTAuth;
|
||||||
|
use Tymon\JWTAuth\Exceptions\TokenExpiredException;
|
||||||
|
use Tymon\JWTAuth\Exceptions\TokenInvalidException;
|
||||||
|
use Tymon\JWTAuth\Exceptions\JWTException;
|
||||||
|
|
||||||
|
class JwtRefreshMiddleware
|
||||||
|
{
|
||||||
|
public function handle($request, Closure $next)
|
||||||
|
{
|
||||||
|
|
||||||
|
try {
|
||||||
|
JWTAuth::parseToken()->authenticate();
|
||||||
|
} catch (TokenExpiredException $e) {
|
||||||
|
try {
|
||||||
|
$newToken = JWTAuth::refresh(JWTAuth::getToken());
|
||||||
|
auth()->setToken($newToken);
|
||||||
|
|
||||||
|
$response = $next($request);
|
||||||
|
|
||||||
|
return $response->header('Authorization', 'Bearer ' . $newToken);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return response()->json(['message' => 'Session expired, please login again'], 401);
|
||||||
|
}
|
||||||
|
} catch (TokenInvalidException $e) {
|
||||||
|
return response()->json(['message' => 'Invalid token'], 401);
|
||||||
|
} catch (JWTException $e) {
|
||||||
|
return response()->json(['message' => 'Token missing'], 401);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $next($request);
|
||||||
|
}
|
||||||
|
}
|
||||||
26
app/Imports/OrderItemsPreviewImport.php
Normal file
26
app/Imports/OrderItemsPreviewImport.php
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
<?php
|
||||||
|
namespace App\Imports;
|
||||||
|
|
||||||
|
use Maatwebsite\Excel\Concerns\ToCollection;
|
||||||
|
use Illuminate\Support\Collection;
|
||||||
|
|
||||||
|
class OrderItemsPreviewImport implements ToCollection
|
||||||
|
{
|
||||||
|
public array $rows = [];
|
||||||
|
|
||||||
|
public function collection(Collection $collection)
|
||||||
|
{
|
||||||
|
$header = $collection->first()->map(fn ($h) => strtolower(trim($h)))->toArray();
|
||||||
|
|
||||||
|
foreach ($collection->skip(1) as $row) {
|
||||||
|
$item = [];
|
||||||
|
foreach ($header as $i => $key) {
|
||||||
|
$item[$key] = $row[$i] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($item['description'])) {
|
||||||
|
$this->rows[] = $item;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,22 +1,43 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
// app/Models/Admin.php
|
|
||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||||
use Illuminate\Notifications\Notifiable;
|
use Illuminate\Notifications\Notifiable;
|
||||||
|
use Spatie\Permission\Traits\HasRoles;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Support\Facades\Hash;
|
||||||
|
|
||||||
class Admin extends Authenticatable
|
class Admin extends Authenticatable
|
||||||
{
|
{
|
||||||
use Notifiable;
|
use HasFactory, Notifiable, HasRoles;
|
||||||
|
|
||||||
protected $guard = 'admin';
|
protected $guard_name = 'admin';
|
||||||
|
|
||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
'name', 'email', 'password', 'role',
|
'name', 'email', 'password', 'username',
|
||||||
|
'phone', 'emergency_phone', 'address',
|
||||||
|
'role', 'department', 'designation', 'joining_date',
|
||||||
|
'status', 'additional_info', 'type','employee_id', // admin/staff indicator
|
||||||
];
|
];
|
||||||
|
|
||||||
protected $hidden = [
|
protected $hidden = [
|
||||||
'password', 'remember_token',
|
'password', 'remember_token',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
public function setPasswordAttribute($value)
|
||||||
|
{
|
||||||
|
if (!$value) return;
|
||||||
|
|
||||||
|
if (Hash::needsRehash($value)) {
|
||||||
|
$this->attributes['password'] = Hash::make($value);
|
||||||
|
} else {
|
||||||
|
$this->attributes['password'] = $value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDisplayNameAttribute()
|
||||||
|
{
|
||||||
|
return "{$this->name}";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
38
app/Models/ChatMessage.php
Normal file
38
app/Models/ChatMessage.php
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
|
||||||
|
class ChatMessage extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'ticket_id',
|
||||||
|
'sender_id',
|
||||||
|
'sender_type',
|
||||||
|
'message',
|
||||||
|
'file_path',
|
||||||
|
'file_type',
|
||||||
|
'read_by_admin',
|
||||||
|
'read_by_user',
|
||||||
|
'client_id',
|
||||||
|
];
|
||||||
|
/**
|
||||||
|
* The ticket this message belongs to.
|
||||||
|
*/
|
||||||
|
public function ticket()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(SupportTicket::class, 'ticket_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Polymorphic sender (User or Admin)
|
||||||
|
*/
|
||||||
|
public function sender()
|
||||||
|
{
|
||||||
|
return $this->morphTo();
|
||||||
|
}
|
||||||
|
}
|
||||||
30
app/Models/Container.php
Normal file
30
app/Models/Container.php
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class Container extends Model
|
||||||
|
{
|
||||||
|
protected $fillable = [
|
||||||
|
'container_name',
|
||||||
|
'container_number',
|
||||||
|
'container_date',
|
||||||
|
'status',
|
||||||
|
'excel_file',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'container_date' => 'date',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function rows()
|
||||||
|
{
|
||||||
|
return $this->hasMany(ContainerRow::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function invoices()
|
||||||
|
{
|
||||||
|
return $this->hasMany(Invoice::class);
|
||||||
|
}
|
||||||
|
}
|
||||||
23
app/Models/ContainerRow.php
Normal file
23
app/Models/ContainerRow.php
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class ContainerRow extends Model
|
||||||
|
{
|
||||||
|
protected $fillable = [
|
||||||
|
'container_id',
|
||||||
|
'row_index',
|
||||||
|
'data',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'data' => 'array',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function container()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Container::class);
|
||||||
|
}
|
||||||
|
}
|
||||||
37
app/Models/Entry.php
Normal file
37
app/Models/Entry.php
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class Entry extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'entry_no',
|
||||||
|
'description',
|
||||||
|
'region',
|
||||||
|
'order_quantity',
|
||||||
|
'amount',
|
||||||
|
'pending_amount',
|
||||||
|
'entry_date',
|
||||||
|
'payment_status',
|
||||||
|
'toggle_pos',
|
||||||
|
'dispatch_status',
|
||||||
|
];
|
||||||
|
|
||||||
|
// An entry can have multiple installments
|
||||||
|
public function installments()
|
||||||
|
{
|
||||||
|
return $this->hasMany(Installment::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
// An entry can have multiple orders (consolidated orders)
|
||||||
|
public function orders()
|
||||||
|
{
|
||||||
|
return $this->belongsToMany(Order::class, 'entry_order', 'entry_id', 'order_id')
|
||||||
|
->withTimestamps();
|
||||||
|
}
|
||||||
|
}
|
||||||
28
app/Models/EntryOrder.php
Normal file
28
app/Models/EntryOrder.php
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class EntryOrder extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $table = 'entry_order';
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'entry_id',
|
||||||
|
'order_id'
|
||||||
|
];
|
||||||
|
|
||||||
|
public function entry()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Entry::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function order()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Order::class);
|
||||||
|
}
|
||||||
|
}
|
||||||
25
app/Models/Installment.php
Normal file
25
app/Models/Installment.php
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class Installment extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'entry_id',
|
||||||
|
'proc_date',
|
||||||
|
'amount',
|
||||||
|
'description',
|
||||||
|
'region',
|
||||||
|
'status'
|
||||||
|
];
|
||||||
|
|
||||||
|
public function entry()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Entry::class);
|
||||||
|
}
|
||||||
|
}
|
||||||
133
app/Models/Invoice.php
Normal file
133
app/Models/Invoice.php
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class Invoice extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'container_id',
|
||||||
|
'customer_id',
|
||||||
|
'mark_no',
|
||||||
|
'invoice_number',
|
||||||
|
'invoice_date',
|
||||||
|
'due_date',
|
||||||
|
'payment_method',
|
||||||
|
'reference_no',
|
||||||
|
'status',
|
||||||
|
'final_amount',
|
||||||
|
'gst_percent',
|
||||||
|
'gst_amount',
|
||||||
|
'final_amount_with_gst',
|
||||||
|
'customer_name',
|
||||||
|
'company_name',
|
||||||
|
'customer_email',
|
||||||
|
'customer_mobile',
|
||||||
|
'customer_address',
|
||||||
|
'pincode',
|
||||||
|
'pdf_path',
|
||||||
|
'notes',
|
||||||
|
// totals from charge groups
|
||||||
|
'charge_groups_total',
|
||||||
|
'grand_total_with_charges',
|
||||||
|
'tax_type',
|
||||||
|
'cgst_percent',
|
||||||
|
'sgst_percent',
|
||||||
|
'igst_percent',
|
||||||
|
];
|
||||||
|
|
||||||
|
/****************************
|
||||||
|
* Relationships
|
||||||
|
****************************/
|
||||||
|
|
||||||
|
public function items()
|
||||||
|
{
|
||||||
|
return $this->hasMany(InvoiceItem::class)->orderBy('id', 'ASC');
|
||||||
|
}
|
||||||
|
|
||||||
|
// public function container()
|
||||||
|
// {
|
||||||
|
// return $this->belongsTo(Container::class);
|
||||||
|
// }
|
||||||
|
|
||||||
|
public function customer()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class, 'customer_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function installments()
|
||||||
|
{
|
||||||
|
return $this->hasMany(InvoiceInstallment::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function chargeGroups()
|
||||||
|
{
|
||||||
|
return $this->hasMany(InvoiceChargeGroup::class, 'invoice_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
/****************************
|
||||||
|
* Helper Functions
|
||||||
|
****************************/
|
||||||
|
|
||||||
|
// (Items based calculateTotals वापरणार नाहीस तरी ठेवू शकतोस)
|
||||||
|
public function calculateTotals()
|
||||||
|
{
|
||||||
|
$gst = ($this->final_amount * $this->gst_percent) / 100;
|
||||||
|
$this->gst_amount = $gst;
|
||||||
|
$this->final_amount_with_gst = $this->final_amount + $gst;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isOverdue()
|
||||||
|
{
|
||||||
|
return $this->status === 'pending' && now()->gt($this->due_date);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getShipment()
|
||||||
|
{
|
||||||
|
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');
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
31
app/Models/InvoiceChargeGroup.php
Normal file
31
app/Models/InvoiceChargeGroup.php
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
<?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');
|
||||||
|
}
|
||||||
|
}
|
||||||
23
app/Models/InvoiceChargeGroupItem.php
Normal file
23
app/Models/InvoiceChargeGroupItem.php
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class InvoiceChargeGroupItem extends Model
|
||||||
|
{
|
||||||
|
protected $fillable = [
|
||||||
|
'group_id',
|
||||||
|
'invoice_item_id',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function group()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(InvoiceChargeGroup::class, 'group_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function item()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(InvoiceItem::class, 'invoice_item_id');
|
||||||
|
}
|
||||||
|
}
|
||||||
24
app/Models/InvoiceInstallment.php
Normal file
24
app/Models/InvoiceInstallment.php
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class InvoiceInstallment extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'invoice_id',
|
||||||
|
'installment_date',
|
||||||
|
'payment_method',
|
||||||
|
'reference_no',
|
||||||
|
'amount',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function invoice()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Invoice::class);
|
||||||
|
}
|
||||||
|
}
|
||||||
117
app/Models/InvoiceItem.php
Normal file
117
app/Models/InvoiceItem.php
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class InvoiceItem extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'invoice_id',
|
||||||
|
'container_id', // Container mapping
|
||||||
|
'container_row_index', // Container row index
|
||||||
|
|
||||||
|
'description',
|
||||||
|
'ctn',
|
||||||
|
'qty',
|
||||||
|
'ttl_qty',
|
||||||
|
'unit',
|
||||||
|
'price',
|
||||||
|
'ttl_amount',
|
||||||
|
|
||||||
|
'cbm',
|
||||||
|
'ttl_cbm',
|
||||||
|
|
||||||
|
'kg',
|
||||||
|
'ttl_kg',
|
||||||
|
|
||||||
|
'shop_no',
|
||||||
|
];
|
||||||
|
|
||||||
|
/****************************
|
||||||
|
* Relationships
|
||||||
|
****************************/
|
||||||
|
|
||||||
|
public function invoice()
|
||||||
|
{
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,11 +4,13 @@ namespace App\Models;
|
|||||||
|
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||||
|
|
||||||
|
|
||||||
class Order extends Model
|
class Order extends Model
|
||||||
{
|
{
|
||||||
use HasFactory;
|
use HasFactory,SoftDeletes;
|
||||||
|
|
||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
'order_id',
|
'order_id',
|
||||||
'mark_no',
|
'mark_no',
|
||||||
@@ -39,4 +41,48 @@ class Order extends Model
|
|||||||
{
|
{
|
||||||
return $this->hasOne(MarkList::class, 'mark_no', 'mark_no');
|
return $this->hasOne(MarkList::class, 'mark_no', 'mark_no');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function entries()
|
||||||
|
{
|
||||||
|
return $this->belongsToMany(Entry::class, 'entry_order', 'order_id', 'entry_id')
|
||||||
|
->withTimestamps();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function shipmentItems()
|
||||||
|
{
|
||||||
|
return $this->hasMany(\App\Models\ShipmentItem::class, 'order_id', 'id');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function shipments()
|
||||||
|
{
|
||||||
|
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');
|
||||||
|
// }
|
||||||
|
|
||||||
|
|
||||||
|
const STATUS_LABELS = [
|
||||||
|
'order_placed' => 'Order Placed',
|
||||||
|
'order_confirmed' => 'Order Confirmed',
|
||||||
|
'supplier_warehouse' => 'Supplier Warehouse',
|
||||||
|
'consolidate_warehouse'=> 'Consolidate Warehouse',
|
||||||
|
'export_custom' => 'Export Custom',
|
||||||
|
'international_transit'=> 'International Transit',
|
||||||
|
'arrived_india' => 'Arrived at India',
|
||||||
|
'import_custom' => 'Import Custom',
|
||||||
|
'warehouse' => 'Warehouse',
|
||||||
|
'domestic_distribution'=> 'Domestic Distribution',
|
||||||
|
'out_for_delivery' => 'Out for Delivery',
|
||||||
|
'delivered' => 'Delivered',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function getStatusLabelAttribute()
|
||||||
|
{
|
||||||
|
return self::STATUS_LABELS[$this->status]
|
||||||
|
?? ucfirst(str_replace('_', ' ', $this->status));
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,10 +4,11 @@ namespace App\Models;
|
|||||||
|
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||||
|
|
||||||
class OrderItem extends Model
|
class OrderItem extends Model
|
||||||
{
|
{
|
||||||
use HasFactory;
|
use HasFactory, SoftDeletes;
|
||||||
|
|
||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
'order_id',
|
'order_id',
|
||||||
|
|||||||
91
app/Models/Shipment.php
Normal file
91
app/Models/Shipment.php
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class Shipment extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'shipment_id',
|
||||||
|
'origin',
|
||||||
|
'destination',
|
||||||
|
'total_ctn',
|
||||||
|
'total_qty',
|
||||||
|
'total_ttl_qty',
|
||||||
|
'total_amount',
|
||||||
|
'total_cbm',
|
||||||
|
'total_ttl_cbm',
|
||||||
|
'total_kg',
|
||||||
|
'total_ttl_kg',
|
||||||
|
'status',
|
||||||
|
'shipment_date',
|
||||||
|
'meta',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'meta' => 'array',
|
||||||
|
'shipment_date' => 'date',
|
||||||
|
];
|
||||||
|
|
||||||
|
// ---------------------------
|
||||||
|
// RELATIONSHIPS
|
||||||
|
// ---------------------------
|
||||||
|
|
||||||
|
public function items()
|
||||||
|
{
|
||||||
|
return $this->hasMany(ShipmentItem::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function orders()
|
||||||
|
{
|
||||||
|
return $this->belongsToMany(Order::class, 'shipment_items', 'shipment_id', 'order_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------
|
||||||
|
// HELPERS
|
||||||
|
// ---------------------------
|
||||||
|
|
||||||
|
public function totalOrdersCount()
|
||||||
|
{
|
||||||
|
return $this->items()->count();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------
|
||||||
|
// STATUS CONSTANTS (LOGISTICS FLOW)
|
||||||
|
// ---------------------------
|
||||||
|
const STATUS_SHIPMENT_READY = 'shipment_ready';
|
||||||
|
const STATUS_EXPORT_CUSTOM = 'export_custom';
|
||||||
|
const STATUS_INTERNATIONAL_TRANSIT= 'international_transit';
|
||||||
|
const STATUS_ARRIVED_INDIA = 'arrived_india';
|
||||||
|
const STATUS_IMPORT_CUSTOM = 'import_custom';
|
||||||
|
const STATUS_WAREHOUSE = 'warehouse';
|
||||||
|
const STATUS_DOMESTIC_DISTRIBUTION= 'domestic_distribution';
|
||||||
|
const STATUS_OUT_FOR_DELIVERY = 'out_for_delivery';
|
||||||
|
const STATUS_DELIVERED = 'delivered';
|
||||||
|
|
||||||
|
public static function statusOptions()
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
self::STATUS_SHIPMENT_READY => 'Shipment Ready',
|
||||||
|
self::STATUS_EXPORT_CUSTOM => 'Export Custom',
|
||||||
|
self::STATUS_INTERNATIONAL_TRANSIT => 'International Transit',
|
||||||
|
self::STATUS_ARRIVED_INDIA => 'Arrived at India',
|
||||||
|
self::STATUS_IMPORT_CUSTOM => 'Import Custom',
|
||||||
|
self::STATUS_WAREHOUSE => 'Warehouse',
|
||||||
|
self::STATUS_DOMESTIC_DISTRIBUTION => 'Domestic Distribution',
|
||||||
|
self::STATUS_OUT_FOR_DELIVERY => 'Out for Delivery',
|
||||||
|
self::STATUS_DELIVERED => 'Delivered',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function statusLabel()
|
||||||
|
{
|
||||||
|
return self::statusOptions()[$this->status]
|
||||||
|
?? ucfirst(str_replace('_', ' ', $this->status));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
61
app/Models/ShipmentItem.php
Normal file
61
app/Models/ShipmentItem.php
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class ShipmentItem extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'shipment_id',
|
||||||
|
'order_id',
|
||||||
|
|
||||||
|
// OLD fields (keep them if old data exists)
|
||||||
|
'order_ctn',
|
||||||
|
'order_qty',
|
||||||
|
'order_ttl_qty',
|
||||||
|
'order_ttl_amount',
|
||||||
|
'order_ttl_kg',
|
||||||
|
|
||||||
|
// NEW fields (added for correct shipments)
|
||||||
|
'ctn',
|
||||||
|
'qty',
|
||||||
|
'ttl_qty',
|
||||||
|
|
||||||
|
'cbm',
|
||||||
|
'ttl_cbm',
|
||||||
|
|
||||||
|
'kg',
|
||||||
|
'ttl_kg',
|
||||||
|
|
||||||
|
'ttl_amount',
|
||||||
|
];
|
||||||
|
|
||||||
|
// ---------------------------
|
||||||
|
// RELATIONSHIPS
|
||||||
|
// ---------------------------
|
||||||
|
|
||||||
|
public function shipment()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Shipment::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function order()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Order::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper: return order data with fallback to snapshot
|
||||||
|
public function getDisplayQty()
|
||||||
|
{
|
||||||
|
return $this->qty;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDisplayAmount()
|
||||||
|
{
|
||||||
|
return $this->ttl_amount;
|
||||||
|
}
|
||||||
|
}
|
||||||
81
app/Models/Staff.php
Normal file
81
app/Models/Staff.php
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||||
|
use Illuminate\Notifications\Notifiable;
|
||||||
|
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||||
|
use Spatie\Permission\Traits\HasRoles;
|
||||||
|
use Illuminate\Support\Facades\Hash;
|
||||||
|
|
||||||
|
class Staff extends Authenticatable
|
||||||
|
{
|
||||||
|
use Notifiable, HasRoles, SoftDeletes;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The guard name used by Spatie.
|
||||||
|
* Make sure this matches the guard you'll use for admin/staff auth (usually 'web' or 'admin').
|
||||||
|
*/
|
||||||
|
protected $guard_name = 'admin';
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The attributes that are mass assignable.
|
||||||
|
*/
|
||||||
|
protected $fillable = [
|
||||||
|
'employee_id',
|
||||||
|
'name',
|
||||||
|
'email',
|
||||||
|
'phone',
|
||||||
|
'emergency_phone',
|
||||||
|
'address',
|
||||||
|
'role', // business role/title (not Spatie role)
|
||||||
|
'department',
|
||||||
|
'designation',
|
||||||
|
'joining_date',
|
||||||
|
'status',
|
||||||
|
'additional_info',
|
||||||
|
'username',
|
||||||
|
'password',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hidden attributes (not returned in arrays / JSON).
|
||||||
|
*/
|
||||||
|
protected $hidden = [
|
||||||
|
'password',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Casts
|
||||||
|
*/
|
||||||
|
protected $casts = [
|
||||||
|
'joining_date' => 'date',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mutator: automatically hash password when set.
|
||||||
|
* Accepts plain text and hashes it with bcrypt.
|
||||||
|
*/
|
||||||
|
public function setPasswordAttribute($value)
|
||||||
|
{
|
||||||
|
if (empty($value)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If already hashed (starts with $2y$), don't double-hash
|
||||||
|
if (Hash::needsRehash($value)) {
|
||||||
|
$this->attributes['password'] = Hash::make($value);
|
||||||
|
} else {
|
||||||
|
$this->attributes['password'] = $value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optional helper to get display name (useful in views/logs).
|
||||||
|
*/
|
||||||
|
public function getDisplayNameAttribute()
|
||||||
|
{
|
||||||
|
return $this->name . ' (' . $this->employee_id . ')';
|
||||||
|
}
|
||||||
|
}
|
||||||
32
app/Models/SupportTicket.php
Normal file
32
app/Models/SupportTicket.php
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
|
||||||
|
class SupportTicket extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'user_id',
|
||||||
|
'status',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The user (customer) who owns this ticket.
|
||||||
|
*/
|
||||||
|
public function user()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* All chat messages for this ticket.
|
||||||
|
*/
|
||||||
|
public function messages()
|
||||||
|
{
|
||||||
|
return $this->hasMany(ChatMessage::class, 'ticket_id')->orderBy('created_at', 'asc');
|
||||||
|
}
|
||||||
|
}
|
||||||
30
app/Models/UpdateRequest.php
Normal file
30
app/Models/UpdateRequest.php
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class UpdateRequest extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $table = 'update_requests';
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'user_id',
|
||||||
|
'data',
|
||||||
|
'status',
|
||||||
|
'admin_note',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'data' => 'array', // converts JSON to array automatically
|
||||||
|
];
|
||||||
|
|
||||||
|
// Relationship: request belongs to a user
|
||||||
|
public function user()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,7 +15,7 @@ class User extends Authenticatable implements JWTSubject
|
|||||||
* The attributes that are mass assignable.
|
* The attributes that are mass assignable.
|
||||||
*/
|
*/
|
||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
'customer_id', // CID-2025-000001 format
|
'customer_id',
|
||||||
'customer_name',
|
'customer_name',
|
||||||
'company_name',
|
'company_name',
|
||||||
'designation',
|
'designation',
|
||||||
@@ -25,10 +25,15 @@ class User extends Authenticatable implements JWTSubject
|
|||||||
'pincode',
|
'pincode',
|
||||||
'date',
|
'date',
|
||||||
'password',
|
'password',
|
||||||
|
|
||||||
|
// newly added customer fields
|
||||||
|
'status', // active / inactive
|
||||||
|
'customer_type', // premium / regular
|
||||||
|
'profile_image', // optional image
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The attributes that should be hidden for arrays.
|
* Attributes that should be hidden.
|
||||||
*/
|
*/
|
||||||
protected $hidden = [
|
protected $hidden = [
|
||||||
'password',
|
'password',
|
||||||
@@ -36,7 +41,7 @@ class User extends Authenticatable implements JWTSubject
|
|||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The attributes that should be cast.
|
* Attribute casting.
|
||||||
*/
|
*/
|
||||||
protected function casts(): array
|
protected function casts(): array
|
||||||
{
|
{
|
||||||
@@ -47,7 +52,30 @@ class User extends Authenticatable implements JWTSubject
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* JWT Identifier.
|
* Relationship: User → MarkList (Many)
|
||||||
|
*/
|
||||||
|
public function marks()
|
||||||
|
{
|
||||||
|
return $this->hasMany(\App\Models\MarkList::class, 'customer_id', 'customer_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Relationship: User → Orders (Through MarkList)
|
||||||
|
*/
|
||||||
|
public function orders()
|
||||||
|
{
|
||||||
|
return $this->hasManyThrough(
|
||||||
|
\App\Models\Order::class,
|
||||||
|
\App\Models\MarkList::class,
|
||||||
|
'customer_id', // MarkList.customer_id
|
||||||
|
'mark_no', // Orders.mark_no
|
||||||
|
'customer_id', // Users.customer_id
|
||||||
|
'mark_no' // MarkList.mark_no
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JWT Identifier
|
||||||
*/
|
*/
|
||||||
public function getJWTIdentifier()
|
public function getJWTIdentifier()
|
||||||
{
|
{
|
||||||
@@ -55,10 +83,32 @@ class User extends Authenticatable implements JWTSubject
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* JWT Custom Claims.
|
* JWT Custom Claims
|
||||||
*/
|
*/
|
||||||
public function getJWTCustomClaims()
|
public function getJWTCustomClaims()
|
||||||
{
|
{
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// App\Models\User.php
|
||||||
|
|
||||||
|
|
||||||
|
public function invoiceInstallments()
|
||||||
|
{
|
||||||
|
return $this->hasManyThrough(
|
||||||
|
InvoiceInstallment::class,
|
||||||
|
Invoice::class,
|
||||||
|
'customer_id', // FK on invoices
|
||||||
|
'invoice_id' // FK on installments
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function invoices()
|
||||||
|
{
|
||||||
|
return $this->hasMany(\App\Models\Invoice::class, 'customer_id', 'id');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
40
app/Providers/AuthServiceProvider.php
Normal file
40
app/Providers/AuthServiceProvider.php
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Providers;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
|
||||||
|
use Illuminate\Support\Facades\Gate;
|
||||||
|
|
||||||
|
class AuthServiceProvider extends ServiceProvider
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The policy mappings for the application.
|
||||||
|
*
|
||||||
|
* @var array<class-string, class-string>
|
||||||
|
*/
|
||||||
|
protected $policies = [
|
||||||
|
// 'App\Models\Model' => 'App\Policies\ModelPolicy',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register any authentication / authorization services.
|
||||||
|
*/
|
||||||
|
public function boot()
|
||||||
|
{
|
||||||
|
$this->registerPolicies();
|
||||||
|
|
||||||
|
// SUPER ADMIN bypass
|
||||||
|
Gate::before(function ($user, $ability) {
|
||||||
|
if ($user->hasRole('super-admin')) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ADMIN bypass
|
||||||
|
Gate::before(function ($user, $ability) {
|
||||||
|
if ($user->hasRole('admin')) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
22
app/Providers/BroadcastServiceProvider.php
Normal file
22
app/Providers/BroadcastServiceProvider.php
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Providers;
|
||||||
|
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use Illuminate\Support\Facades\Broadcast;
|
||||||
|
use Illuminate\Support\ServiceProvider;
|
||||||
|
|
||||||
|
class BroadcastServiceProvider extends ServiceProvider
|
||||||
|
{
|
||||||
|
public function boot(): void
|
||||||
|
{
|
||||||
|
Broadcast::routes([
|
||||||
|
'middleware' => ['web'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 👇 FORCE admin guard for broadcasting
|
||||||
|
Auth::shouldUse('admin');
|
||||||
|
|
||||||
|
require base_path('routes/channels.php');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,13 +6,18 @@ use Illuminate\Foundation\Configuration\Middleware;
|
|||||||
|
|
||||||
return Application::configure(basePath: dirname(__DIR__))
|
return Application::configure(basePath: dirname(__DIR__))
|
||||||
->withRouting(
|
->withRouting(
|
||||||
web: __DIR__.'/../routes/web.php',
|
web: [
|
||||||
|
__DIR__.'/../routes/web.php',
|
||||||
|
__DIR__.'/../routes/channels.php',
|
||||||
|
],
|
||||||
|
api: __DIR__.'/../routes/api.php',
|
||||||
commands: __DIR__.'/../routes/console.php',
|
commands: __DIR__.'/../routes/console.php',
|
||||||
health: '/up',
|
health: '/up',
|
||||||
)
|
)
|
||||||
->withMiddleware(function (Middleware $middleware): void {
|
->withMiddleware(function (Middleware $middleware) {
|
||||||
//
|
//
|
||||||
})
|
})
|
||||||
->withExceptions(function (Exceptions $exceptions): void {
|
->withExceptions(function (Exceptions $exceptions) {
|
||||||
//
|
//
|
||||||
})->create();
|
})
|
||||||
|
->create();
|
||||||
|
|||||||
@@ -3,4 +3,7 @@
|
|||||||
return [
|
return [
|
||||||
App\Providers\AppServiceProvider::class,
|
App\Providers\AppServiceProvider::class,
|
||||||
App\Providers\RouteServiceProvider::class,
|
App\Providers\RouteServiceProvider::class,
|
||||||
|
App\Providers\AuthServiceProvider::class,
|
||||||
|
App\Providers\BroadcastServiceProvider::class,
|
||||||
|
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -7,9 +7,14 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"require": {
|
"require": {
|
||||||
"php": "^8.2",
|
"php": "^8.2",
|
||||||
|
"barryvdh/laravel-dompdf": "^3.1",
|
||||||
"laravel/framework": "^12.0",
|
"laravel/framework": "^12.0",
|
||||||
|
"laravel/reverb": "^1.6",
|
||||||
"laravel/tinker": "^2.10.1",
|
"laravel/tinker": "^2.10.1",
|
||||||
"php-open-source-saver/jwt-auth": "2.8"
|
"maatwebsite/excel": "^3.1",
|
||||||
|
"mpdf/mpdf": "^8.2",
|
||||||
|
"php-open-source-saver/jwt-auth": "2.8",
|
||||||
|
"spatie/laravel-permission": "^6.23"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"fakerphp/faker": "^1.23",
|
"fakerphp/faker": "^1.23",
|
||||||
|
|||||||
3218
composer.lock
generated
3218
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -85,6 +85,13 @@ return [
|
|||||||
// 'driver' => 'database',
|
// 'driver' => 'database',
|
||||||
// 'table' => 'users',
|
// 'table' => 'users',
|
||||||
// ],
|
// ],
|
||||||
|
'staff' => [
|
||||||
|
'driver' => 'eloquent',
|
||||||
|
'model' => App\Models\Staff::class,
|
||||||
|
],
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
],
|
],
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|||||||
31
config/broadcasting.php
Normal file
31
config/broadcasting.php
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
'default' => env('BROADCAST_DRIVER', 'null'),
|
||||||
|
|
||||||
|
'connections' => [
|
||||||
|
|
||||||
|
'reverb' => [
|
||||||
|
'driver' => 'reverb',
|
||||||
|
'key' => env('REVERB_APP_KEY'),
|
||||||
|
'secret' => env('REVERB_APP_SECRET'),
|
||||||
|
'app_id' => env('REVERB_APP_ID'),
|
||||||
|
'options' => [
|
||||||
|
'host' => env('REVERB_HOST'),
|
||||||
|
'port' => env('REVERB_PORT'),
|
||||||
|
'scheme' => env('REVERB_SCHEME'),
|
||||||
|
'useTLS' => false,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
|
||||||
|
'log' => [
|
||||||
|
'driver' => 'log',
|
||||||
|
],
|
||||||
|
|
||||||
|
'null' => [
|
||||||
|
'driver' => 'null',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
];
|
||||||
@@ -89,7 +89,7 @@ return [
|
|||||||
|
|
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
'ttl' => (int) env('JWT_TTL', 60),
|
'ttl' => (int) env('JWT_TTL', 1440),
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
@@ -108,7 +108,7 @@ return [
|
|||||||
|
|
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
'refresh_ttl' => (int) env('JWT_REFRESH_TTL', 20160),
|
'refresh_ttl' => (int) env('JWT_REFRESH_TTL', 64800),
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
|
|||||||
202
config/permission.php
Normal file
202
config/permission.php
Normal file
@@ -0,0 +1,202 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
'models' => [
|
||||||
|
|
||||||
|
/*
|
||||||
|
* When using the "HasPermissions" trait from this package, we need to know which
|
||||||
|
* Eloquent model should be used to retrieve your permissions. Of course, it
|
||||||
|
* is often just the "Permission" model but you may use whatever you like.
|
||||||
|
*
|
||||||
|
* The model you want to use as a Permission model needs to implement the
|
||||||
|
* `Spatie\Permission\Contracts\Permission` contract.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'permission' => Spatie\Permission\Models\Permission::class,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* When using the "HasRoles" trait from this package, we need to know which
|
||||||
|
* Eloquent model should be used to retrieve your roles. Of course, it
|
||||||
|
* is often just the "Role" model but you may use whatever you like.
|
||||||
|
*
|
||||||
|
* The model you want to use as a Role model needs to implement the
|
||||||
|
* `Spatie\Permission\Contracts\Role` contract.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'role' => Spatie\Permission\Models\Role::class,
|
||||||
|
|
||||||
|
],
|
||||||
|
|
||||||
|
'table_names' => [
|
||||||
|
|
||||||
|
/*
|
||||||
|
* When using the "HasRoles" trait from this package, we need to know which
|
||||||
|
* table should be used to retrieve your roles. We have chosen a basic
|
||||||
|
* default value but you may easily change it to any table you like.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'roles' => 'roles',
|
||||||
|
|
||||||
|
/*
|
||||||
|
* When using the "HasPermissions" trait from this package, we need to know which
|
||||||
|
* table should be used to retrieve your permissions. We have chosen a basic
|
||||||
|
* default value but you may easily change it to any table you like.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'permissions' => 'permissions',
|
||||||
|
|
||||||
|
/*
|
||||||
|
* When using the "HasPermissions" trait from this package, we need to know which
|
||||||
|
* table should be used to retrieve your models permissions. We have chosen a
|
||||||
|
* basic default value but you may easily change it to any table you like.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'model_has_permissions' => 'model_has_permissions',
|
||||||
|
|
||||||
|
/*
|
||||||
|
* When using the "HasRoles" trait from this package, we need to know which
|
||||||
|
* table should be used to retrieve your models roles. We have chosen a
|
||||||
|
* basic default value but you may easily change it to any table you like.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'model_has_roles' => 'model_has_roles',
|
||||||
|
|
||||||
|
/*
|
||||||
|
* When using the "HasRoles" trait from this package, we need to know which
|
||||||
|
* table should be used to retrieve your roles permissions. We have chosen a
|
||||||
|
* basic default value but you may easily change it to any table you like.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'role_has_permissions' => 'role_has_permissions',
|
||||||
|
],
|
||||||
|
|
||||||
|
'column_names' => [
|
||||||
|
/*
|
||||||
|
* Change this if you want to name the related pivots other than defaults
|
||||||
|
*/
|
||||||
|
'role_pivot_key' => null, // default 'role_id',
|
||||||
|
'permission_pivot_key' => null, // default 'permission_id',
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Change this if you want to name the related model primary key other than
|
||||||
|
* `model_id`.
|
||||||
|
*
|
||||||
|
* For example, this would be nice if your primary keys are all UUIDs. In
|
||||||
|
* that case, name this `model_uuid`.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'model_morph_key' => 'model_id',
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Change this if you want to use the teams feature and your related model's
|
||||||
|
* foreign key is other than `team_id`.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'team_foreign_key' => 'team_id',
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
* When set to true, the method for checking permissions will be registered on the gate.
|
||||||
|
* Set this to false if you want to implement custom logic for checking permissions.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'register_permission_check_method' => true,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* When set to true, Laravel\Octane\Events\OperationTerminated event listener will be registered
|
||||||
|
* this will refresh permissions on every TickTerminated, TaskTerminated and RequestTerminated
|
||||||
|
* NOTE: This should not be needed in most cases, but an Octane/Vapor combination benefited from it.
|
||||||
|
*/
|
||||||
|
'register_octane_reset_listener' => false,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Events will fire when a role or permission is assigned/unassigned:
|
||||||
|
* \Spatie\Permission\Events\RoleAttached
|
||||||
|
* \Spatie\Permission\Events\RoleDetached
|
||||||
|
* \Spatie\Permission\Events\PermissionAttached
|
||||||
|
* \Spatie\Permission\Events\PermissionDetached
|
||||||
|
*
|
||||||
|
* To enable, set to true, and then create listeners to watch these events.
|
||||||
|
*/
|
||||||
|
'events_enabled' => false,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Teams Feature.
|
||||||
|
* When set to true the package implements teams using the 'team_foreign_key'.
|
||||||
|
* If you want the migrations to register the 'team_foreign_key', you must
|
||||||
|
* set this to true before doing the migration.
|
||||||
|
* If you already did the migration then you must make a new migration to also
|
||||||
|
* add 'team_foreign_key' to 'roles', 'model_has_roles', and 'model_has_permissions'
|
||||||
|
* (view the latest version of this package's migration file)
|
||||||
|
*/
|
||||||
|
|
||||||
|
'teams' => false,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The class to use to resolve the permissions team id
|
||||||
|
*/
|
||||||
|
'team_resolver' => \Spatie\Permission\DefaultTeamResolver::class,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Passport Client Credentials Grant
|
||||||
|
* When set to true the package will use Passports Client to check permissions
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use_passport_client_credentials' => false,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* When set to true, the required permission names are added to exception messages.
|
||||||
|
* This could be considered an information leak in some contexts, so the default
|
||||||
|
* setting is false here for optimum safety.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'display_permission_in_exception' => false,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* When set to true, the required role names are added to exception messages.
|
||||||
|
* This could be considered an information leak in some contexts, so the default
|
||||||
|
* setting is false here for optimum safety.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'display_role_in_exception' => false,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* By default wildcard permission lookups are disabled.
|
||||||
|
* See documentation to understand supported syntax.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'enable_wildcard_permission' => false,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The class to use for interpreting wildcard permissions.
|
||||||
|
* If you need to modify delimiters, override the class and specify its name here.
|
||||||
|
*/
|
||||||
|
// 'wildcard_permission' => Spatie\Permission\WildcardPermission::class,
|
||||||
|
|
||||||
|
/* Cache-specific settings */
|
||||||
|
|
||||||
|
'cache' => [
|
||||||
|
|
||||||
|
/*
|
||||||
|
* By default all permissions are cached for 24 hours to speed up performance.
|
||||||
|
* When permissions or roles are updated the cache is flushed automatically.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'expiration_time' => \DateInterval::createFromDateString('24 hours'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The cache key used to store all permissions.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'key' => 'spatie.permission.cache',
|
||||||
|
|
||||||
|
/*
|
||||||
|
* You may optionally indicate a specific cache driver to use for permission and
|
||||||
|
* role caching using any of the `store` drivers listed in the cache.php config
|
||||||
|
* file. Using 'default' here means to use the `default` set in cache.php.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'store' => 'default',
|
||||||
|
],
|
||||||
|
];
|
||||||
96
config/reverb.php
Normal file
96
config/reverb.php
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Default Reverb Server
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
'default' => env('REVERB_SERVER', 'reverb'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Reverb Servers
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
'servers' => [
|
||||||
|
|
||||||
|
'reverb' => [
|
||||||
|
'host' => env('REVERB_SERVER_HOST', '0.0.0.0'), // WebSocket listens here
|
||||||
|
'port' => env('REVERB_SERVER_PORT', 8080), // WebSocket port
|
||||||
|
'path' => env('REVERB_SERVER_PATH', ''),
|
||||||
|
|
||||||
|
// Used for Echo client hostname
|
||||||
|
'hostname' => env('REVERB_HOST', 'localhost'),
|
||||||
|
|
||||||
|
'options' => [
|
||||||
|
'tls' => [], // No TLS for localhost
|
||||||
|
],
|
||||||
|
|
||||||
|
'max_request_size' => env('REVERB_MAX_REQUEST_SIZE', 10000),
|
||||||
|
|
||||||
|
'scaling' => [
|
||||||
|
'enabled' => env('REVERB_SCALING_ENABLED', false),
|
||||||
|
],
|
||||||
|
|
||||||
|
'pulse_ingest_interval' => env('REVERB_PULSE_INGEST_INTERVAL', 15),
|
||||||
|
'telescope_ingest_interval' => env('REVERB_TELESCOPE_INGEST_INTERVAL', 15),
|
||||||
|
],
|
||||||
|
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Reverb Applications
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
'apps' => [
|
||||||
|
|
||||||
|
'provider' => 'config',
|
||||||
|
|
||||||
|
'apps' => [
|
||||||
|
[
|
||||||
|
'key' => env('REVERB_APP_KEY'),
|
||||||
|
'secret' => env('REVERB_APP_SECRET'),
|
||||||
|
'app_id' => env('REVERB_APP_ID'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Echo + Flutter Client Options
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
*/
|
||||||
|
'options' => [
|
||||||
|
'host' => env('REVERB_HOST', 'localhost'), // for client connections
|
||||||
|
'port' => env('REVERB_PORT', 8080), // SAME as WebSocket server port
|
||||||
|
'scheme' => env('REVERB_SCHEME', 'http'),
|
||||||
|
'useTLS' => false,
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Allowed Origins (Important)
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| "*" allows all origins:
|
||||||
|
| - Flutter (Android/iOS/Web)
|
||||||
|
| - Admin Panel
|
||||||
|
| - Localhost
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
'allowed_origins' => ['*'],
|
||||||
|
|
||||||
|
'ping_interval' => env('REVERB_APP_PING_INTERVAL', 60),
|
||||||
|
'activity_timeout' => env('REVERB_APP_ACTIVITY_TIMEOUT', 30),
|
||||||
|
'max_connections' => env('REVERB_APP_MAX_CONNECTIONS'),
|
||||||
|
'max_message_size' => env('REVERB_APP_MAX_MESSAGE_SIZE', 10000),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
],
|
||||||
|
|
||||||
|
];
|
||||||
|
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('entries', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
|
||||||
|
$table->string('entry_no')->unique(); // PAY-2024-001
|
||||||
|
$table->string('description');
|
||||||
|
$table->string('region');
|
||||||
|
|
||||||
|
$table->unsignedInteger('order_quantity')->default(0); // selected consolidated order count
|
||||||
|
|
||||||
|
$table->decimal('amount', 12, 2);
|
||||||
|
$table->decimal('pending_amount', 12, 2); // always <= amount
|
||||||
|
|
||||||
|
$table->date('entry_date'); // auto-today default by controller
|
||||||
|
|
||||||
|
// Toggle-based payment states
|
||||||
|
$table->enum('payment_status', ['unpaid', 'pending', 'paid'])->default('unpaid');
|
||||||
|
$table->tinyInteger('toggle_pos')->default(0); // 0 left, 1 middle, 2 right
|
||||||
|
|
||||||
|
// Dispatch state (for second table)
|
||||||
|
$table->enum('dispatch_status', [
|
||||||
|
'pending',
|
||||||
|
'loading',
|
||||||
|
'packed',
|
||||||
|
'dispatched',
|
||||||
|
'delivered'
|
||||||
|
])->default('pending');
|
||||||
|
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('entries');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('installments', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
|
||||||
|
$table->foreignId('entry_id')
|
||||||
|
->constrained('entries')
|
||||||
|
->onDelete('cascade');
|
||||||
|
|
||||||
|
$table->date('proc_date'); // processing date
|
||||||
|
$table->decimal('amount', 12, 2);
|
||||||
|
|
||||||
|
$table->string('description')->nullable();
|
||||||
|
$table->string('region')->nullable();
|
||||||
|
|
||||||
|
$table->enum('status', [
|
||||||
|
'Pending',
|
||||||
|
'Loading',
|
||||||
|
'Packed',
|
||||||
|
'Dispatched',
|
||||||
|
'Delivered'
|
||||||
|
])->default('Pending');
|
||||||
|
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('installments');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('entry_order', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
|
||||||
|
$table->foreignId('entry_id')
|
||||||
|
->constrained('entries')
|
||||||
|
->onDelete('cascade');
|
||||||
|
|
||||||
|
$table->foreignId('order_id')
|
||||||
|
->constrained('orders')
|
||||||
|
->onDelete('cascade');
|
||||||
|
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('entry_order');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
class CreateShipmentsTable extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function up()
|
||||||
|
{
|
||||||
|
Schema::create('shipments', function (Blueprint $table) {
|
||||||
|
$table->bigIncrements('id');
|
||||||
|
|
||||||
|
// Human-friendly auto-generated shipment id (e.g. SHIP-25-00000001)
|
||||||
|
$table->string('shipment_id')->unique();
|
||||||
|
|
||||||
|
// Basic details
|
||||||
|
$table->string('origin')->nullable();
|
||||||
|
$table->string('destination')->nullable();
|
||||||
|
|
||||||
|
// Totals (calculated when creating shipment)
|
||||||
|
$table->integer('total_ctn')->default(0)->comment('sum of CTN of selected orders');
|
||||||
|
$table->integer('total_qty')->default(0)->comment('sum of qty of selected orders');
|
||||||
|
$table->integer('total_ttl_qty')->default(0)->comment('sum of ttl_qty of selected orders');
|
||||||
|
$table->decimal('total_amount', 16, 2)->default(0.00)->comment('sum of ttl_amount of selected orders');
|
||||||
|
$table->decimal('total_cbm', 14, 3)->default(0.000)->comment('sum cbm');
|
||||||
|
$table->decimal('total_ttl_cbm', 14, 3)->default(0.000)->comment('sum ttl cbm');
|
||||||
|
$table->decimal('total_kg', 14, 3)->default(0.000)->comment('sum kg');
|
||||||
|
$table->decimal('total_ttl_kg', 14, 3)->default(0.000)->comment('sum ttl kg');
|
||||||
|
|
||||||
|
// status: pending (default), in_transit, dispatched, delivered, cancelled, etc.
|
||||||
|
$table->string('status')->default('pending');
|
||||||
|
|
||||||
|
// shipment date (admin can change)
|
||||||
|
$table->date('shipment_date')->nullable();
|
||||||
|
|
||||||
|
// optional meta (vehicle, driver etc)
|
||||||
|
$table->json('meta')->nullable();
|
||||||
|
|
||||||
|
$table->timestamps();
|
||||||
|
|
||||||
|
// Indexes for fast filtering
|
||||||
|
$table->index('shipment_id');
|
||||||
|
$table->index('status');
|
||||||
|
$table->index('shipment_date');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function down()
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('shipments');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
class CreateShipmentItemsTable extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function up()
|
||||||
|
{
|
||||||
|
Schema::create('shipment_items', function (Blueprint $table) {
|
||||||
|
$table->bigIncrements('id');
|
||||||
|
|
||||||
|
// Link to shipments
|
||||||
|
$table->foreignId('shipment_id')->constrained('shipments')->onDelete('cascade');
|
||||||
|
|
||||||
|
// Link to orders. assuming orders.id is bigIncrements
|
||||||
|
$table->foreignId('order_id')->constrained('orders')->onDelete('restrict');
|
||||||
|
|
||||||
|
// Snapshots (optional) — store basic order totals at time of assignment
|
||||||
|
$table->integer('order_ctn')->nullable()->default(0);
|
||||||
|
$table->integer('order_qty')->nullable()->default(0);
|
||||||
|
$table->integer('order_ttl_qty')->nullable()->default(0);
|
||||||
|
$table->decimal('order_ttl_amount', 16, 2)->nullable()->default(0.00);
|
||||||
|
$table->decimal('order_ttl_kg', 14, 3)->nullable()->default(0.000);
|
||||||
|
|
||||||
|
$table->timestamps();
|
||||||
|
|
||||||
|
// Prevent duplicate assignment of same order to the same shipment
|
||||||
|
$table->unique(['shipment_id', 'order_id']);
|
||||||
|
|
||||||
|
// We will check order_id uniqueness across shipments in app logic (see below)
|
||||||
|
$table->index('order_id');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function down()
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('shipment_items');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
class CreateInvoicesTable extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('invoices', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
|
||||||
|
// Links
|
||||||
|
$table->unsignedBigInteger('order_id')->index();
|
||||||
|
$table->unsignedBigInteger('customer_id')->nullable()->index(); // snapshot link if available
|
||||||
|
$table->string('mark_no')->nullable()->index();
|
||||||
|
|
||||||
|
// Invoice identity
|
||||||
|
$table->string('invoice_number')->unique();
|
||||||
|
$table->date('invoice_date')->nullable();
|
||||||
|
$table->date('due_date')->nullable();
|
||||||
|
|
||||||
|
// Payment / status
|
||||||
|
$table->string('payment_method')->nullable();
|
||||||
|
$table->string('reference_no')->nullable();
|
||||||
|
$table->enum('status', ['pending','paid','overdue'])->default('pending');
|
||||||
|
|
||||||
|
// Amounts
|
||||||
|
$table->decimal('final_amount', 14, 2)->default(0.00); // editable by user
|
||||||
|
$table->decimal('gst_percent', 5, 2)->default(0.00); // editable by user
|
||||||
|
$table->decimal('gst_amount', 14, 2)->default(0.00); // auto-calculated
|
||||||
|
$table->decimal('final_amount_with_gst', 14, 2)->default(0.00); // auto-calculated
|
||||||
|
|
||||||
|
// Customer snapshot (immutable fields)
|
||||||
|
$table->string('customer_name')->nullable();
|
||||||
|
$table->string('company_name')->nullable();
|
||||||
|
$table->string('customer_email')->nullable();
|
||||||
|
$table->string('customer_mobile')->nullable();
|
||||||
|
$table->text('customer_address')->nullable();
|
||||||
|
$table->string('pincode')->nullable();
|
||||||
|
|
||||||
|
// PDF / notes
|
||||||
|
$table->string('pdf_path')->nullable();
|
||||||
|
$table->text('notes')->nullable();
|
||||||
|
|
||||||
|
$table->timestamps();
|
||||||
|
|
||||||
|
// Foreign keys (optional — adjust table names/namespaces if yours are different)
|
||||||
|
$table->foreign('order_id')->references('id')->on('orders')->onDelete('cascade');
|
||||||
|
// customer_id may reference users table, keep nullable to avoid migration order issues
|
||||||
|
$table->foreign('customer_id')->references('id')->on('users')->onDelete('set null');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('invoices', function (Blueprint $table) {
|
||||||
|
$table->dropForeign(['order_id']);
|
||||||
|
$table->dropForeign(['customer_id']);
|
||||||
|
});
|
||||||
|
Schema::dropIfExists('invoices');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
class CreateInvoiceItemsTable extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('invoice_items', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->unsignedBigInteger('invoice_id')->index();
|
||||||
|
|
||||||
|
// Snapshot of order item fields (not editable)
|
||||||
|
$table->text('description')->nullable();
|
||||||
|
$table->integer('ctn')->default(0);
|
||||||
|
$table->integer('qty')->default(0);
|
||||||
|
$table->integer('ttl_qty')->default(0);
|
||||||
|
$table->string('unit')->nullable();
|
||||||
|
$table->decimal('price', 14, 2)->default(0.00);
|
||||||
|
$table->decimal('ttl_amount', 14, 2)->default(0.00);
|
||||||
|
|
||||||
|
$table->decimal('cbm', 12, 3)->default(0.000);
|
||||||
|
$table->decimal('ttl_cbm', 12, 3)->default(0.000);
|
||||||
|
|
||||||
|
$table->decimal('kg', 12, 3)->default(0.000);
|
||||||
|
$table->decimal('ttl_kg', 12, 3)->default(0.000);
|
||||||
|
|
||||||
|
$table->string('shop_no')->nullable();
|
||||||
|
|
||||||
|
$table->timestamps();
|
||||||
|
|
||||||
|
$table->unsignedBigInteger('container_id')->nullable()->after('invoice_id');
|
||||||
|
$table->integer('container_row_index')->nullable()->after('container_id');
|
||||||
|
|
||||||
|
// FK
|
||||||
|
$table->foreign('invoice_id')->references('id')->on('invoices')->onDelete('cascade');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('invoice_items', function (Blueprint $table) {
|
||||||
|
$table->dropForeign(['invoice_id']);
|
||||||
|
});
|
||||||
|
Schema::dropIfExists('invoice_items');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up()
|
||||||
|
{
|
||||||
|
Schema::table('users', function (Blueprint $table) {
|
||||||
|
|
||||||
|
// status: active / inactive
|
||||||
|
$table->enum('status', ['active', 'inactive'])->default('active')->after('pincode');
|
||||||
|
|
||||||
|
// premium / regular
|
||||||
|
$table->enum('customer_type', ['regular', 'premium'])->default('regular')->after('status');
|
||||||
|
|
||||||
|
// optional: profile image path
|
||||||
|
$table->string('profile_image')->nullable()->after('customer_type');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down()
|
||||||
|
{
|
||||||
|
Schema::table('users', function (Blueprint $table) {
|
||||||
|
$table->dropColumn(['status', 'customer_type', 'profile_image']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
<?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) {
|
||||||
|
|
||||||
|
// GST type — gst or igst
|
||||||
|
$table->enum('tax_type', ['gst', 'igst'])
|
||||||
|
->default('gst')
|
||||||
|
->after('final_amount');
|
||||||
|
|
||||||
|
// Old gst_percent becomes optional
|
||||||
|
$table->decimal('gst_percent', 5, 2)
|
||||||
|
->nullable()
|
||||||
|
->change();
|
||||||
|
|
||||||
|
// Split GST %
|
||||||
|
$table->decimal('cgst_percent', 5, 2)
|
||||||
|
->nullable()
|
||||||
|
->after('gst_percent');
|
||||||
|
|
||||||
|
$table->decimal('sgst_percent', 5, 2)
|
||||||
|
->nullable()
|
||||||
|
->after('cgst_percent');
|
||||||
|
|
||||||
|
// IGST %
|
||||||
|
$table->decimal('igst_percent', 5, 2)
|
||||||
|
->nullable()
|
||||||
|
->after('sgst_percent');
|
||||||
|
|
||||||
|
// Tax amount recalculation is the same
|
||||||
|
// gst_amount and final_amount_with_gst already exist
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('invoices', function (Blueprint $table) {
|
||||||
|
$table->dropColumn([
|
||||||
|
'tax_type',
|
||||||
|
'cgst_percent',
|
||||||
|
'sgst_percent',
|
||||||
|
'igst_percent',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
<?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
|
||||||
|
{
|
||||||
|
// Table already exists. Add updates here if needed.
|
||||||
|
Schema::table('invoice_installments', function (Blueprint $table) {
|
||||||
|
// nothing to update
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('invoice_installments', function (Blueprint $table) {
|
||||||
|
// nothing to rollback
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
<?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('order_items', function (Blueprint $table) {
|
||||||
|
$table->softDeletes();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('order_items', function (Blueprint $table) {
|
||||||
|
$table->dropSoftDeletes();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('order_items', function (Blueprint $table) {
|
||||||
|
if (! Schema::hasColumn('order_items', 'deleted_at')) {
|
||||||
|
$table->softDeletes(); // adds deleted_at (nullable timestamp)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('order_items', function (Blueprint $table) {
|
||||||
|
if (Schema::hasColumn('order_items', 'deleted_at')) {
|
||||||
|
$table->dropSoftDeletes(); // drops deleted_at
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up()
|
||||||
|
{
|
||||||
|
Schema::table('orders', function (Blueprint $table) {
|
||||||
|
$table->softDeletes(); // creates deleted_at column
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down()
|
||||||
|
{
|
||||||
|
Schema::table('orders', function (Blueprint $table) {
|
||||||
|
$table->dropColumn('deleted_at');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
class CreateUpdateRequestsTable extends Migration
|
||||||
|
{
|
||||||
|
public function up()
|
||||||
|
{
|
||||||
|
Schema::create('update_requests', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
|
||||||
|
// The user who is requesting profile update
|
||||||
|
$table->unsignedBigInteger('user_id');
|
||||||
|
|
||||||
|
// JSON data of the requested profile changes
|
||||||
|
$table->json('data')->nullable();
|
||||||
|
|
||||||
|
// pending / approved / rejected
|
||||||
|
$table->enum('status', ['pending', 'approved', 'rejected'])->default('pending');
|
||||||
|
|
||||||
|
// Optional message (admin notes)
|
||||||
|
$table->text('admin_note')->nullable();
|
||||||
|
|
||||||
|
$table->timestamps();
|
||||||
|
|
||||||
|
// Foreign key constraint
|
||||||
|
$table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down()
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('update_requests');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
<?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('shipment_items', function (Blueprint $table) {
|
||||||
|
|
||||||
|
if (!Schema::hasColumn('shipment_items', 'ctn')) {
|
||||||
|
$table->integer('ctn')->default(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Schema::hasColumn('shipment_items', 'qty')) {
|
||||||
|
$table->integer('qty')->default(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Schema::hasColumn('shipment_items', 'ttl_qty')) {
|
||||||
|
$table->integer('ttl_qty')->default(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Schema::hasColumn('shipment_items', 'cbm')) {
|
||||||
|
$table->double('cbm')->default(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Schema::hasColumn('shipment_items', 'ttl_cbm')) {
|
||||||
|
$table->double('ttl_cbm')->default(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Schema::hasColumn('shipment_items', 'kg')) {
|
||||||
|
$table->double('kg')->default(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Schema::hasColumn('shipment_items', 'ttl_kg')) {
|
||||||
|
$table->double('ttl_kg')->default(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Schema::hasColumn('shipment_items', 'ttl_amount')) {
|
||||||
|
$table->double('ttl_amount')->default(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('shipment_items', function (Blueprint $table) {
|
||||||
|
// safely remove columns (optional)
|
||||||
|
$columns = [
|
||||||
|
'ctn', 'qty', 'ttl_qty', 'cbm',
|
||||||
|
'ttl_cbm', 'kg', 'ttl_kg', 'ttl_amount'
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($columns as $col) {
|
||||||
|
if (Schema::hasColumn('shipment_items', $col)) {
|
||||||
|
$table->dropColumn($col);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
<?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
|
||||||
|
{
|
||||||
|
$teams = config('permission.teams');
|
||||||
|
$tableNames = config('permission.table_names');
|
||||||
|
$columnNames = config('permission.column_names');
|
||||||
|
$pivotRole = $columnNames['role_pivot_key'] ?? 'role_id';
|
||||||
|
$pivotPermission = $columnNames['permission_pivot_key'] ?? 'permission_id';
|
||||||
|
|
||||||
|
throw_if(empty($tableNames), Exception::class, 'Error: config/permission.php not loaded. Run [php artisan config:clear] and try again.');
|
||||||
|
throw_if($teams && empty($columnNames['team_foreign_key'] ?? null), Exception::class, 'Error: team_foreign_key on config/permission.php not loaded. Run [php artisan config:clear] and try again.');
|
||||||
|
|
||||||
|
Schema::create($tableNames['permissions'], static function (Blueprint $table) {
|
||||||
|
// $table->engine('InnoDB');
|
||||||
|
$table->bigIncrements('id'); // permission id
|
||||||
|
$table->string('name'); // For MyISAM use string('name', 225); // (or 166 for InnoDB with Redundant/Compact row format)
|
||||||
|
$table->string('guard_name'); // For MyISAM use string('guard_name', 25);
|
||||||
|
$table->timestamps();
|
||||||
|
|
||||||
|
$table->unique(['name', 'guard_name']);
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::create($tableNames['roles'], static function (Blueprint $table) use ($teams, $columnNames) {
|
||||||
|
// $table->engine('InnoDB');
|
||||||
|
$table->bigIncrements('id'); // role id
|
||||||
|
if ($teams || config('permission.testing')) { // permission.testing is a fix for sqlite testing
|
||||||
|
$table->unsignedBigInteger($columnNames['team_foreign_key'])->nullable();
|
||||||
|
$table->index($columnNames['team_foreign_key'], 'roles_team_foreign_key_index');
|
||||||
|
}
|
||||||
|
$table->string('name'); // For MyISAM use string('name', 225); // (or 166 for InnoDB with Redundant/Compact row format)
|
||||||
|
$table->string('guard_name'); // For MyISAM use string('guard_name', 25);
|
||||||
|
$table->timestamps();
|
||||||
|
if ($teams || config('permission.testing')) {
|
||||||
|
$table->unique([$columnNames['team_foreign_key'], 'name', 'guard_name']);
|
||||||
|
} else {
|
||||||
|
$table->unique(['name', 'guard_name']);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::create($tableNames['model_has_permissions'], static function (Blueprint $table) use ($tableNames, $columnNames, $pivotPermission, $teams) {
|
||||||
|
$table->unsignedBigInteger($pivotPermission);
|
||||||
|
|
||||||
|
$table->string('model_type');
|
||||||
|
$table->unsignedBigInteger($columnNames['model_morph_key']);
|
||||||
|
$table->index([$columnNames['model_morph_key'], 'model_type'], 'model_has_permissions_model_id_model_type_index');
|
||||||
|
|
||||||
|
$table->foreign($pivotPermission)
|
||||||
|
->references('id') // permission id
|
||||||
|
->on($tableNames['permissions'])
|
||||||
|
->onDelete('cascade');
|
||||||
|
if ($teams) {
|
||||||
|
$table->unsignedBigInteger($columnNames['team_foreign_key']);
|
||||||
|
$table->index($columnNames['team_foreign_key'], 'model_has_permissions_team_foreign_key_index');
|
||||||
|
|
||||||
|
$table->primary([$columnNames['team_foreign_key'], $pivotPermission, $columnNames['model_morph_key'], 'model_type'],
|
||||||
|
'model_has_permissions_permission_model_type_primary');
|
||||||
|
} else {
|
||||||
|
$table->primary([$pivotPermission, $columnNames['model_morph_key'], 'model_type'],
|
||||||
|
'model_has_permissions_permission_model_type_primary');
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::create($tableNames['model_has_roles'], static function (Blueprint $table) use ($tableNames, $columnNames, $pivotRole, $teams) {
|
||||||
|
$table->unsignedBigInteger($pivotRole);
|
||||||
|
|
||||||
|
$table->string('model_type');
|
||||||
|
$table->unsignedBigInteger($columnNames['model_morph_key']);
|
||||||
|
$table->index([$columnNames['model_morph_key'], 'model_type'], 'model_has_roles_model_id_model_type_index');
|
||||||
|
|
||||||
|
$table->foreign($pivotRole)
|
||||||
|
->references('id') // role id
|
||||||
|
->on($tableNames['roles'])
|
||||||
|
->onDelete('cascade');
|
||||||
|
if ($teams) {
|
||||||
|
$table->unsignedBigInteger($columnNames['team_foreign_key']);
|
||||||
|
$table->index($columnNames['team_foreign_key'], 'model_has_roles_team_foreign_key_index');
|
||||||
|
|
||||||
|
$table->primary([$columnNames['team_foreign_key'], $pivotRole, $columnNames['model_morph_key'], 'model_type'],
|
||||||
|
'model_has_roles_role_model_type_primary');
|
||||||
|
} else {
|
||||||
|
$table->primary([$pivotRole, $columnNames['model_morph_key'], 'model_type'],
|
||||||
|
'model_has_roles_role_model_type_primary');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::create($tableNames['role_has_permissions'], static function (Blueprint $table) use ($tableNames, $pivotRole, $pivotPermission) {
|
||||||
|
$table->unsignedBigInteger($pivotPermission);
|
||||||
|
$table->unsignedBigInteger($pivotRole);
|
||||||
|
|
||||||
|
$table->foreign($pivotPermission)
|
||||||
|
->references('id') // permission id
|
||||||
|
->on($tableNames['permissions'])
|
||||||
|
->onDelete('cascade');
|
||||||
|
|
||||||
|
$table->foreign($pivotRole)
|
||||||
|
->references('id') // role id
|
||||||
|
->on($tableNames['roles'])
|
||||||
|
->onDelete('cascade');
|
||||||
|
|
||||||
|
$table->primary([$pivotPermission, $pivotRole], 'role_has_permissions_permission_id_role_id_primary');
|
||||||
|
});
|
||||||
|
|
||||||
|
app('cache')
|
||||||
|
->store(config('permission.cache.store') != 'default' ? config('permission.cache.store') : null)
|
||||||
|
->forget(config('permission.cache.key'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
$tableNames = config('permission.table_names');
|
||||||
|
|
||||||
|
throw_if(empty($tableNames), Exception::class, 'Error: config/permission.php not found and defaults could not be merged. Please publish the package configuration before proceeding, or drop the tables manually.');
|
||||||
|
|
||||||
|
Schema::drop($tableNames['role_has_permissions']);
|
||||||
|
Schema::drop($tableNames['model_has_roles']);
|
||||||
|
Schema::drop($tableNames['model_has_permissions']);
|
||||||
|
Schema::drop($tableNames['roles']);
|
||||||
|
Schema::drop($tableNames['permissions']);
|
||||||
|
}
|
||||||
|
};
|
||||||
50
database/migrations/2025_12_04_071300_create_staff_table.php
Normal file
50
database/migrations/2025_12_04_071300_create_staff_table.php
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
<?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::create('staff', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
|
||||||
|
// Personal Information
|
||||||
|
$table->string('employee_id')->unique();
|
||||||
|
$table->string('name');
|
||||||
|
$table->string('email')->unique();
|
||||||
|
$table->string('phone');
|
||||||
|
$table->string('emergency_phone')->nullable();
|
||||||
|
$table->text('address')->nullable();
|
||||||
|
|
||||||
|
// Professional Information
|
||||||
|
$table->string('role')->nullable(); // Job title
|
||||||
|
$table->string('department')->nullable();
|
||||||
|
$table->string('designation')->nullable();
|
||||||
|
$table->date('joining_date')->nullable();
|
||||||
|
$table->string('status')->default('active'); // active/inactive
|
||||||
|
$table->text('additional_info')->nullable();
|
||||||
|
|
||||||
|
// System Access
|
||||||
|
$table->string('username')->unique();
|
||||||
|
$table->string('password');
|
||||||
|
|
||||||
|
$table->softDeletes();
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('staff');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
<?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('admins', function (Blueprint $table) {
|
||||||
|
$table->string('employee_id')->unique()->nullable();
|
||||||
|
$table->string('phone')->nullable();
|
||||||
|
$table->string('emergency_phone')->nullable();
|
||||||
|
$table->text('address')->nullable();
|
||||||
|
|
||||||
|
$table->string('department')->nullable();
|
||||||
|
$table->string('designation')->nullable();
|
||||||
|
$table->date('joining_date')->nullable();
|
||||||
|
$table->enum('status', ['active','inactive'])->default('active');
|
||||||
|
$table->text('additional_info')->nullable();
|
||||||
|
|
||||||
|
$table->string('username')->unique()->nullable();
|
||||||
|
$table->enum('type', ['admin','staff'])->default('staff');
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('admins', function (Blueprint $table) {
|
||||||
|
//
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<?php
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration {
|
||||||
|
public function up()
|
||||||
|
{
|
||||||
|
Schema::table('admins', function (Blueprint $table) {
|
||||||
|
$table->string('role')->nullable()->change(); // <-- Fix problem
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down()
|
||||||
|
{
|
||||||
|
Schema::table('admins', function (Blueprint $table) {
|
||||||
|
$table->enum('role', ['admin', 'super-admin'])->nullable()->change();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
<?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('support_tickets', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->unsignedBigInteger('user_id'); // user who owns the chat
|
||||||
|
$table->string('status')->default('open'); // open / closed
|
||||||
|
$table->timestamps();
|
||||||
|
|
||||||
|
// foreign key constraint (optional but recommended)
|
||||||
|
$table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('support_tickets');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
<?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('chat_messages', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
|
||||||
|
// Chat belongs to a ticket
|
||||||
|
$table->unsignedBigInteger('ticket_id');
|
||||||
|
|
||||||
|
// POLYMORPHIC sender (User OR Admin)
|
||||||
|
$table->unsignedBigInteger('sender_id');
|
||||||
|
$table->string('sender_type');
|
||||||
|
// Example values:
|
||||||
|
// - "App\Models\User"
|
||||||
|
// - "App\Models\Admin"
|
||||||
|
|
||||||
|
// Content
|
||||||
|
$table->text('message')->nullable();
|
||||||
|
$table->string('file_path')->nullable(); // storage/app/public/chat/...
|
||||||
|
$table->string('file_type')->default('text'); // text / image / video / pdf
|
||||||
|
|
||||||
|
$table->timestamps();
|
||||||
|
|
||||||
|
// FK to tickets table
|
||||||
|
$table->foreign('ticket_id')
|
||||||
|
->references('id')->on('support_tickets')
|
||||||
|
->onDelete('cascade');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('chat_messages');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('chat_messages', function (Blueprint $table) {
|
||||||
|
$table->boolean('read_by_admin')->default(false)->after('file_type');
|
||||||
|
$table->boolean('read_by_user')->default(false)->after('read_by_admin');
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('chat_messages', function (Blueprint $table) {
|
||||||
|
//
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('chat_messages', function (Blueprint $table) {
|
||||||
|
$table->string('client_id')
|
||||||
|
->nullable()
|
||||||
|
->after('sender_type')
|
||||||
|
->index();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('chat_messages', function (Blueprint $table) {
|
||||||
|
$table->dropColumn('client_id');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up()
|
||||||
|
{
|
||||||
|
Schema::create('containers', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->string('container_name');
|
||||||
|
$table->string('container_number')->unique();
|
||||||
|
$table->date('container_date');
|
||||||
|
$table->string('excel_file')->nullable();
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down()
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('containers');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up()
|
||||||
|
{
|
||||||
|
Schema::create('loading_list_items', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->foreignId('container_id')
|
||||||
|
->constrained('containers')
|
||||||
|
->onDelete('cascade');
|
||||||
|
|
||||||
|
$table->string('mark')->nullable(); // MARK / ITEM NO
|
||||||
|
$table->string('description')->nullable();
|
||||||
|
$table->integer('ctn')->nullable();
|
||||||
|
$table->integer('qty')->nullable();
|
||||||
|
$table->integer('total_qty')->nullable();
|
||||||
|
$table->string('unit')->nullable();
|
||||||
|
$table->decimal('price', 15, 3)->nullable(); // SAHIL format साठी
|
||||||
|
$table->decimal('cbm', 15, 5)->nullable();
|
||||||
|
$table->decimal('total_cbm', 15, 5)->nullable();
|
||||||
|
$table->decimal('kg', 15, 3)->nullable();
|
||||||
|
$table->decimal('total_kg', 15, 3)->nullable();
|
||||||
|
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down()
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('loading_list_items');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('container_rows', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->foreignId('container_id')
|
||||||
|
->constrained('containers')
|
||||||
|
->onDelete('cascade');
|
||||||
|
|
||||||
|
// Excel मधल्या row क्रमांकासाठी (optional)
|
||||||
|
$table->unsignedInteger('row_index')->nullable();
|
||||||
|
|
||||||
|
// या row चा full data: "heading text" => "cell value"
|
||||||
|
$table->json('data');
|
||||||
|
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('container_rows');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('containers', function (Blueprint $table) {
|
||||||
|
$table->string('status', 21)
|
||||||
|
->default('pending')
|
||||||
|
->after('container_date');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('containers', function (Blueprint $table) {
|
||||||
|
$table->dropColumn('status');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('invoices', function (Blueprint $table) {
|
||||||
|
// 1) order_id foreign key काढा
|
||||||
|
$table->dropForeign(['order_id']);
|
||||||
|
|
||||||
|
// 2) order_id column काढा
|
||||||
|
$table->dropColumn('order_id');
|
||||||
|
|
||||||
|
// 3) container_id add करा
|
||||||
|
$table->unsignedBigInteger('container_id')->nullable()->after('id');
|
||||||
|
|
||||||
|
// 4) container_id FK
|
||||||
|
$table->foreign('container_id')
|
||||||
|
->references('id')
|
||||||
|
->on('containers')
|
||||||
|
->onDelete('cascade');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('invoices', function (Blueprint $table) {
|
||||||
|
// rollback: container_id काढून order_id परत add
|
||||||
|
$table->dropForeign(['container_id']);
|
||||||
|
$table->dropColumn('container_id');
|
||||||
|
|
||||||
|
$table->unsignedBigInteger('order_id')->index();
|
||||||
|
$table->foreign('order_id')
|
||||||
|
->references('id')
|
||||||
|
->on('orders')
|
||||||
|
->onDelete('cascade');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('invoice_charge_groups', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->unsignedBigInteger('invoice_id');
|
||||||
|
$table->string('group_name')->nullable(); // उदा. "FREIGHT", "HANDLING"
|
||||||
|
$table->enum('basis_type', ['ttl_qty', 'amount', 'ttl_cbm', 'ttl_kg']);
|
||||||
|
$table->decimal('basis_value', 15, 3)->default(0); // auto calculate केलेला total basis
|
||||||
|
$table->decimal('rate', 15, 3)->default(0); // per basis rate (helper)
|
||||||
|
$table->decimal('total_charge', 15, 2); // admin नी manually टाकलेला total
|
||||||
|
$table->timestamps();
|
||||||
|
|
||||||
|
$table->foreign('invoice_id')
|
||||||
|
->references('id')->on('invoices')
|
||||||
|
->onDelete('cascade');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('invoice_charge_groups');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
};
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('invoice_charge_group_items', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->unsignedBigInteger('group_id');
|
||||||
|
$table->unsignedBigInteger('invoice_item_id');
|
||||||
|
$table->timestamps();
|
||||||
|
|
||||||
|
$table->foreign('group_id')
|
||||||
|
->references('id')->on('invoice_charge_groups')
|
||||||
|
->onDelete('cascade');
|
||||||
|
|
||||||
|
$table->foreign('invoice_item_id')
|
||||||
|
->references('id')->on('invoice_items')
|
||||||
|
->onDelete('cascade');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('invoice_charge_group_items');
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up()
|
||||||
|
{
|
||||||
|
Schema::table('invoice_items', function (Blueprint $table) {
|
||||||
|
$table->unsignedBigInteger('container_id')->nullable()->after('invoice_id');
|
||||||
|
$table->integer('container_row_index')->nullable()->after('container_id');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down()
|
||||||
|
{
|
||||||
|
Schema::table('invoice_items', function (Blueprint $table) {
|
||||||
|
$table->dropColumn(['container_id', 'container_row_index']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('invoices', function (Blueprint $table) {
|
||||||
|
// column आधीच आहे का हे check करून, नसेल तरच add करायचा
|
||||||
|
if (!Schema::hasColumn('invoices', 'due_date')) {
|
||||||
|
$table->date('due_date')
|
||||||
|
->nullable()
|
||||||
|
->after('invoice_date');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('invoices', function (Blueprint $table) {
|
||||||
|
if (Schema::hasColumn('invoices', 'due_date')) {
|
||||||
|
$table->dropColumn('due_date');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('containers', function (Blueprint $table) {
|
||||||
|
$table->string('status', 50)->change();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('containers', function (Blueprint $table) {
|
||||||
|
$table->string('status', 20)->change();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
<?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']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
<?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');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
<?php
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
|
return new class extends Migration {
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
DB::statement("
|
||||||
|
ALTER TABLE `invoices`
|
||||||
|
MODIFY `status` ENUM('pending','paying','paid','overdue')
|
||||||
|
NOT NULL DEFAULT 'pending'
|
||||||
|
");
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
DB::statement("
|
||||||
|
ALTER TABLE `invoices`
|
||||||
|
MODIFY `status` ENUM('pending','paid','overdue')
|
||||||
|
NOT NULL DEFAULT 'pending'
|
||||||
|
");
|
||||||
|
}
|
||||||
|
};
|
||||||
109
database/seeders/PermissionSeeder.php
Normal file
109
database/seeders/PermissionSeeder.php
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Database\Seeders;
|
||||||
|
|
||||||
|
use Illuminate\Database\Seeder;
|
||||||
|
use Spatie\Permission\Models\Permission;
|
||||||
|
use Spatie\Permission\Models\Role;
|
||||||
|
|
||||||
|
class PermissionSeeder extends Seeder
|
||||||
|
{
|
||||||
|
public function run()
|
||||||
|
{
|
||||||
|
// ------------------------------------------------------
|
||||||
|
// FINAL PERMISSION LIST (YOUR DATA)
|
||||||
|
// ------------------------------------------------------
|
||||||
|
|
||||||
|
$permissions = [
|
||||||
|
|
||||||
|
// ORDER
|
||||||
|
'order.view',
|
||||||
|
'order.create',
|
||||||
|
'order.edit',
|
||||||
|
'order.delete',
|
||||||
|
|
||||||
|
// EXTRA (ORDERS)
|
||||||
|
'orders.view', // you added this separately
|
||||||
|
|
||||||
|
// CONTAINER
|
||||||
|
'container.view',
|
||||||
|
'container.create',
|
||||||
|
'container.update',
|
||||||
|
'container.delete',
|
||||||
|
|
||||||
|
// SHIPMENT
|
||||||
|
'shipment.view',
|
||||||
|
'shipment.create',
|
||||||
|
'shipment.delete',
|
||||||
|
|
||||||
|
// INVOICE
|
||||||
|
'invoice.view',
|
||||||
|
'invoice.edit',
|
||||||
|
'invoice.add_installment',
|
||||||
|
|
||||||
|
// CUSTOMER
|
||||||
|
'customer.view',
|
||||||
|
'customer.create',
|
||||||
|
|
||||||
|
// REQUEST
|
||||||
|
'request.view',
|
||||||
|
'request.update_profile',
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// @can('')
|
||||||
|
// @endcan
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// ACCOUNT
|
||||||
|
'account.view',
|
||||||
|
'account.create_order',
|
||||||
|
'account.edit_order',
|
||||||
|
'account.delete_order',
|
||||||
|
'account.toggle_payment_status',
|
||||||
|
'account.add_installment',
|
||||||
|
'account.view_installments',
|
||||||
|
|
||||||
|
// REPORT
|
||||||
|
'report.view',
|
||||||
|
|
||||||
|
// MARK LIST
|
||||||
|
'mark_list.view',
|
||||||
|
];
|
||||||
|
|
||||||
|
// ------------------------------------------------------
|
||||||
|
// CREATE PERMISSIONS
|
||||||
|
// ------------------------------------------------------
|
||||||
|
|
||||||
|
foreach ($permissions as $permission) {
|
||||||
|
Permission::firstOrCreate(
|
||||||
|
['name' => $permission, 'guard_name' => 'admin']
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------
|
||||||
|
// ROLES
|
||||||
|
// ------------------------------------------------------
|
||||||
|
|
||||||
|
// Create super-admin role
|
||||||
|
$superAdminRole = Role::firstOrCreate(
|
||||||
|
['name' => 'super-admin', 'guard_name' => 'admin']
|
||||||
|
);
|
||||||
|
|
||||||
|
// Create admin role
|
||||||
|
$adminRole = Role::firstOrCreate(
|
||||||
|
['name' => 'admin', 'guard_name' => 'admin']
|
||||||
|
);
|
||||||
|
|
||||||
|
// ------------------------------------------------------
|
||||||
|
// ASSIGN ALL PERMISSIONS TO BOTH ROLES
|
||||||
|
// ------------------------------------------------------
|
||||||
|
|
||||||
|
$allPermissions = Permission::where('guard_name', 'admin')->get();
|
||||||
|
|
||||||
|
$superAdminRole->syncPermissions($allPermissions);
|
||||||
|
$adminRole->syncPermissions($allPermissions);
|
||||||
|
}
|
||||||
|
}
|
||||||
2525
package-lock.json
generated
Normal file
2525
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -13,5 +13,9 @@
|
|||||||
"laravel-vite-plugin": "^2.0.0",
|
"laravel-vite-plugin": "^2.0.0",
|
||||||
"tailwindcss": "^4.0.0",
|
"tailwindcss": "^4.0.0",
|
||||||
"vite": "^7.0.7"
|
"vite": "^7.0.7"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"laravel-echo": "^2.2.6",
|
||||||
|
"pusher-js": "^8.4.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
public/images/kentlogo1.png
Normal file
BIN
public/images/kentlogo1.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 58 KiB |
@@ -18,3 +18,4 @@ require __DIR__.'/../vendor/autoload.php';
|
|||||||
$app = require_once __DIR__.'/../bootstrap/app.php';
|
$app = require_once __DIR__.'/../bootstrap/app.php';
|
||||||
|
|
||||||
$app->handleRequest(Request::capture());
|
$app->handleRequest(Request::capture());
|
||||||
|
|
||||||
|
|||||||
BIN
public/invoices/invoice-INV-2025-000001.pdf
Normal file
BIN
public/invoices/invoice-INV-2025-000001.pdf
Normal file
Binary file not shown.
BIN
public/invoices/invoice-INV-2025-000002.pdf
Normal file
BIN
public/invoices/invoice-INV-2025-000002.pdf
Normal file
Binary file not shown.
BIN
public/invoices/invoice-INV-2025-000003.pdf
Normal file
BIN
public/invoices/invoice-INV-2025-000003.pdf
Normal file
Binary file not shown.
BIN
public/invoices/invoice-INV-2025-000004.pdf
Normal file
BIN
public/invoices/invoice-INV-2025-000004.pdf
Normal file
Binary file not shown.
BIN
public/invoices/invoice-INV-2025-000005.pdf
Normal file
BIN
public/invoices/invoice-INV-2025-000005.pdf
Normal file
Binary file not shown.
BIN
public/invoices/invoice-INV-2025-000006.pdf
Normal file
BIN
public/invoices/invoice-INV-2025-000006.pdf
Normal file
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user