Compare commits

...

97 Commits

Author SHA256 Message Date
Abhishek Mali
9a6ca49ad7 user order controller update for show view data 2026-03-13 20:19:34 +05:30
Abhishek Mali
bf2689e62d container and invoice api done small api like view invoice traking in order section is remaning 2026-03-13 17:25:58 +05:30
Utkarsh Khedkar
c25b468c77 Status Updated paying 2026-03-12 18:20:09 +05:30
Utkarsh Khedkar
5d8a746876 Status Updated 2026-03-12 18:11:43 +05:30
Utkarsh Khedkar
bb2a361a97 Merge branch 'dev' of http://103.248.30.24:3000/kent-logistics/Kent-logistics-Laravel into dev 2026-03-12 12:34:49 +05:30
Utkarsh Khedkar
6b5876e08f Fetch Data For Customer Details 2026-03-12 12:34:27 +05:30
Abhishek Mali
43b1a64911 ajax update 2026-03-12 11:48:42 +05:30
Utkarsh Khedkar
ff4c006ca4 Gst Updates 2026-03-11 20:02:43 +05:30
Utkarsh Khedkar
d5e9113820 Merge branch 'dev' of http://103.248.30.24:3000/kent-logistics/Kent-logistics-Laravel into dev 2026-03-09 12:28:11 +05:30
Utkarsh Khedkar
bddbcf5c5f logo changes 2026-03-09 12:27:42 +05:30
Abhishek Mali
0c51ed1489 mark list and custumer data update 2026-03-09 12:04:08 +05:30
Utkarsh Khedkar
9cc6959396 Pdf Changes Done 2026-03-09 10:24:44 +05:30
Utkarsh Khedkar
c11467068c Frontend Changes 2026-02-28 11:00:48 +05:30
Abhishek Mali
599023166a update staff permissions 2026-02-27 12:59:20 +05:30
Utkarsh Khedkar
e188780329 All Kent Code Updated 2026-02-27 10:51:26 +05:30
Abhishek Mali
338425535e account 2025-12-23 22:15:45 +05:30
Abhishek Mali
f7856a6755 account 2025-12-23 21:11:53 +05:30
divya abdar
8f95091673 my chnages in customer and staff 2025-12-23 16:32:47 +05:30
divya abdar
7362ef6bdc staff chnages and customer chnages 2025-12-23 16:26:33 +05:30
Abhishek Mali
e872b83ea3 auto calculate 2025-12-23 14:15:03 +05:30
Abhishek Mali
6ccf2cf84e Merge branch 'dev' of http://103.248.30.24:3000/kent-logistics/Kent-logistics-Laravel into dev 2025-12-23 12:23:13 +05:30
Abhishek Mali
4637f0b189 excel import 2025-12-23 12:22:35 +05:30
divya abdar
952dd7eddd staff chnages 2025-12-23 12:22:21 +05:30
Abhishek Mali
451be1a533 status 2025-12-23 10:11:24 +05:30
Abhishek Mali
cd9a786ef4 Merge branch 'dev' of http://103.248.30.24:3000/kent-logistics/Kent-logistics-Laravel into dev 2025-12-23 09:51:35 +05:30
divya abdar
a6dd919d3f changes of invoice and shipment 2025-12-23 00:44:29 +05:30
Abhishek Mali
e0a8a5c69c status update 2025-12-23 00:36:15 +05:30
divya abdar
7fa03688aa changes of invoice and shipment 2025-12-23 00:30:18 +05:30
Utkarsh Khedkar
1885d3beef Merge branch 'dev' of http://103.248.30.24:3000/kent-logistics/Kent-logistics-Laravel into dev 2025-12-22 22:38:45 +05:30
Utkarsh Khedkar
72a81fa111 Dashboard Changes 2025-12-22 22:38:35 +05:30
divya abdar
044bfe5563 changes of shipment 2025-12-22 21:17:29 +05:30
divya abdar
8ca8f05b93 changes of shipment 2025-12-22 21:15:20 +05:30
Abhishek Mali
2741129740 Merge branch 'dev' of http://103.248.30.24:3000/kent-logistics/Kent-logistics-Laravel into dev 2025-12-22 19:22:12 +05:30
Abhishek Mali
1bce2be826 amount update 2025-12-22 19:22:01 +05:30
divya abdar
ea2532efc8 invoice pop up invoice edit file chnages 2025-12-22 17:30:47 +05:30
Utkarsh Khedkar
ccce02f43e Account Changes 2025-12-22 16:49:27 +05:30
Utkarsh Khedkar
cdb6cab57d Merge branch 'dev' of http://103.248.30.24:3000/kent-logistics/Kent-logistics-Laravel into dev 2025-12-19 17:28:07 +05:30
Utkarsh Khedkar
3941b06355 changes 2025-12-19 17:27:54 +05:30
divya abdar
d2730e78f6 order, report and dashboard changes 2025-12-19 17:08:53 +05:30
Utkarsh Khedkar
80c6e42e0c Merge branch 'dev' of http://103.248.30.24:3000/kent-logistics/Kent-logistics-Laravel into dev 2025-12-19 16:21:00 +05:30
Utkarsh Khedkar
e455c271c4 Chat UI Changes 2025-12-19 16:20:43 +05:30
divya abdar
48f7ab82ff minor changes in order and dashboard, records 2025-12-19 16:15:18 +05:30
Abhishek Mali
c4097ecbde employee update 2025-12-19 16:08:34 +05:30
Utkarsh Khedkar
8a0d122e2c Merge branch 'dev' of http://103.248.30.24:3000/kent-logistics/Kent-logistics-Laravel into dev 2025-12-19 11:24:28 +05:30
divya abdar
7ef28e06ae order chnages 2025-12-19 11:18:55 +05:30
divya abdar
752f5ee873 order section changes 2025-12-19 11:12:06 +05:30
Utkarsh Khedkar
84bf42f992 changes 2025-12-19 11:04:16 +05:30
Utkarsh Khedkar
fc9a401a8c changes 2025-12-19 11:00:34 +05:30
Abhishek Mali
3590e8f873 download option in invoide 2025-12-19 10:50:36 +05:30
Abhishek Mali
f6fb304b7a chat support download updated 2025-12-18 12:57:01 +05:30
Abhishek Mali
6b41a447bb chat support update 2025-12-17 19:49:14 +05:30
Abhishek Mali
5dc9fc7db4 chat support updates 2025-12-16 10:19:54 +05:30
Abhishek Mali
1aad6b231e chat support 2025-12-15 11:03:30 +05:30
Abhishek Mali
0a65d5f596 staff 2025-12-08 10:17:46 +05:30
Abhishek Mali
0a1d0a9c55 staff update 2025-12-05 17:16:02 +05:30
Abhishek Mali
409a854d7b error fix 2025-12-04 12:08:45 +05:30
Utkarsh Khedkar
4dab96b8d1 Account and Shipment Changes 2025-12-04 11:21:46 +05:30
Utkarsh Khedkar
e7fef314fc Merge branch 'dev' of http://103.248.30.24:3000/kent-logistics/Kent-logistics-Laravel into dev 2025-12-03 16:17:36 +05:30
Utkarsh Khedkar
5114357ff2 Account Changes 2025-12-03 16:17:14 +05:30
divya abdar
0afcb23511 conflict resolve 2025-12-03 16:13:37 +05:30
divya abdar
340c2b2132 Shipment dashboard changes 2025-12-03 15:36:04 +05:30
Abhishek Mali
44b8299b0e fixes 2025-12-03 15:31:00 +05:30
Utkarsh Khedkar
9b8c50fcec Merge branch 'dev' of http://103.248.30.24:3000/kent-logistics/Kent-logistics-Laravel into dev 2025-12-03 11:09:28 +05:30
Utkarsh Khedkar
7a814dff1d Dasshboard Changes 2025-12-03 11:09:12 +05:30
Abhishek Mali
f4730a81d8 download pdf and excel function created 2025-12-03 10:35:20 +05:30
Utkarsh Khedkar
3b24ee860a Merge branch 'dev' of http://103.248.30.24:3000/kent-logistics/Kent-logistics-Laravel into dev 2025-12-02 18:12:15 +05:30
Utkarsh Khedkar
2dcd9fe332 Account Changes 2025-12-02 18:11:58 +05:30
Abhishek Mali
922539844d api update 2025-12-02 18:07:15 +05:30
Abhishek Mali
3845972c5c error fix 2025-12-01 12:45:25 +05:30
Abhishek Mali
64d8939208 merge Conflict 2025-12-01 11:49:24 +05:30
Abhishek Mali
ec2a0baceb API changes 2025-12-01 11:44:43 +05:30
divya abdar
68bfd180ed merge resolve conflict 2025-12-01 11:42:47 +05:30
divya abdar
aa616fcf61 Frontend dashboard, shipment, invoice , customer 2025-12-01 10:38:52 +05:30
Utkarsh Khedkar
178fbb224c Account,Order&Request change 2025-12-01 10:34:27 +05:30
Utkarsh Khedkar
97db70c40e Account Section UI Changes 2025-11-27 19:39:36 +05:30
Utkarsh Khedkar
04b00c9db8 frontend Order Section Update 2025-11-26 23:07:12 +05:30
Abhishek Mali
bebe0711f4 gst 2025-11-25 13:14:53 +05:30
Utkarsh Khedkar
a14fe614e5 Resolve Conflict 2025-11-21 16:44:56 +05:30
Utkarsh Khedkar
4d44e7df25 UI Update Customer Section 2025-11-21 16:15:10 +05:30
Abhishek Mali
6e1ae8f380 account section 2025-11-21 16:07:43 +05:30
Utkarsh Khedkar
837f4fe566 Invoice Frontend 2025-11-18 14:35:58 +05:30
Abhishek Mali
63daef6a92 customer section 2025-11-18 10:01:59 +05:30
Utkarsh Khedkar
56a17cf1e0 Merge branch 'dev' of http://103.248.30.24:3000/kent-logistics/Kent-logistics-Laravel into dev 2025-11-17 10:36:00 +05:30
Utkarsh Khedkar
22be272067 Shipment Frontend 2025-11-17 10:35:42 +05:30
Abhishek Mali
df89031d36 invoice update 2025-11-17 10:33:11 +05:30
Utkarsh Khedkar
8f6e30554b Merge branch 'dev' of http://103.248.30.24:3000/kent-logistics/Kent-logistics-Laravel into dev 2025-11-15 10:22:36 +05:30
Utkarsh Khedkar
ca28409689 sidebar changes 2025-11-15 10:22:05 +05:30
Abhishek Mali
cfd128cf9b Merge branch 'dev' of http://103.248.30.24:3000/kent-logistics/Kent-logistics-Laravel into dev 2025-11-15 10:11:13 +05:30
Abhishek Mali
8b64515689 order popup 2025-11-15 10:08:43 +05:30
Utkarsh Khedkar
2703eff60f done 2025-11-14 14:35:10 +05:30
Abhishek Mali
59574fd664 test dev 2025-11-14 14:29:28 +05:30
Abhishek Mali
2f7eaf88a2 web.php 2025-11-14 14:06:23 +05:30
Abhishek Mali
e071a082e0 web.php 2025-11-14 14:00:36 +05:30
Abhishek Mali
b495a58d64 shipment 2025-11-14 13:55:01 +05:30
Utkarsh Khedkar
2c5d19779b Frontend(Create Order) 2025-11-14 13:47:01 +05:30
Utkarsh Khedkar
5d29c8accb Merge branch 'main' of http://103.248.30.24:3000/kent-logistics/Kent-logistics-Laravel 2025-11-13 13:12:29 +05:30
Utkarsh Khedkar
82bfcc5f33 Changes 2025-11-13 13:12:18 +05:30
197 changed files with 44118 additions and 2352 deletions

View File

@@ -1,6 +1,6 @@
APP_NAME=Laravel
APP_ENV=local
APP_KEY=
APP_KEY=base64:/ulhRgiCOFjZV6xUDkXLfiR9X8iFRZ4QIiX3UJbdwY4=
APP_DEBUG=true
APP_URL=http://localhost
@@ -20,12 +20,12 @@ LOG_STACK=single
LOG_DEPRECATIONS_CHANNEL=null
LOG_LEVEL=debug
DB_CONNECTION=sqlite
# DB_HOST=127.0.0.1
# DB_PORT=3306
# DB_DATABASE=laravel
# DB_USERNAME=root
# DB_PASSWORD=
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=kent_logistics6
DB_USERNAME=root
DB_PASSWORD=
SESSION_DRIVER=database
SESSION_LIFETIME=120

View 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';
}
}

View 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'));
}
}

View 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',
];
}
}

View 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);
}
}
}

View File

@@ -5,50 +5,52 @@ namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use App\Models\Admin;
class AdminAuthController extends Controller
{
/**
* Show the admin login page
*/
public function showLoginForm()
{
return view('admin.login');
}
/**
* Handle admin login
*/
public function login(Request $request)
{
$request->validate([
'email' => 'required|email',
'login' => 'required',
'password' => 'required|string|min:6',
]);
// Try to log in using the 'admin' guard
if (Auth::guard('admin')->attempt($request->only('email', 'password'))) {
return redirect()->route('admin.dashboard')->with('success', 'Welcome back, Admin!');
$loginInput = $request->input('login');
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)
{
Auth::guard('admin')->logout();
// Destroy the session completely
$request->session()->invalidate();
$request->session()->regenerateToken();
return redirect()->route('admin.login')->with('success', 'Logged out successfully.');
}
}

View 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
]);
}
}

View 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.');
}
}

View 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,
]);
}
}

View File

@@ -7,196 +7,756 @@ use Illuminate\Http\Request;
use App\Models\Order;
use App\Models\OrderItem;
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
{
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();
$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
// -------------------------------------------------------------------------
public function addTempItem(Request $request)
/* ---------------------------
* LIST (new: Invoices Management for Orders page)
* ---------------------------*/
public function index(Request $request)
{
// Validate item fields
$item = $request->validate([
'mark_no' => 'required',
'origin' => 'required',
'destination' => 'required',
'description' => 'required|string',
'ctn' => 'nullable|numeric',
'qty' => 'nullable|numeric',
'ttl_qty' => 'nullable|numeric',
'unit' => 'nullable|string',
'price' => 'nullable|numeric',
'ttl_amount' => 'nullable|numeric',
'cbm' => 'nullable|numeric',
'ttl_cbm' => 'nullable|numeric',
'kg' => 'nullable|numeric',
'ttl_kg' => 'nullable|numeric',
'shop_no' => 'nullable|string',
]);
// ❌ Prevent changing mark_no once first item added
if (session()->has('mark_no') && session('mark_no') != $request->mark_no) {
return redirect()->to(route('admin.orders.index') . '#createOrderForm')
->with('error', 'You must finish or clear the current order before changing Mark No.');
}
// Save mark, origin, destination ONLY ONCE
if (!session()->has('mark_no')) {
session([
'mark_no' => $request->mark_no,
'origin' => $request->origin,
'destination' => $request->destination
]);
}
// ❌ DO NOT overwrite these values again
// session(['mark_no' => $request->mark_no]);
// 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.');
$invoices = DB::table('invoices')
->leftJoin('containers', 'containers.id', '=', 'invoices.container_id')
->leftJoin('mark_list', 'mark_list.mark_no', '=', 'invoices.mark_no')
->select(
'invoices.id',
'invoices.invoice_number',
'invoices.invoice_date',
'invoices.final_amount',
'invoices.final_amount_with_gst',
'invoices.status as invoice_status',
'invoices.mark_no',
'invoices.container_id', // <<< हे नक्की घाल
'containers.container_number',
'containers.container_date',
DB::raw('COALESCE(invoices.company_name, mark_list.company_name) as company_name'),
DB::raw('COALESCE(invoices.customer_name, mark_list.customer_name) as customer_name')
)
->when($request->filled('search'), function ($q) use ($request) {
$search = trim($request->search);
$q->where(function ($qq) use ($search) {
$qq->where('invoices.invoice_number', 'like', "%{$search}%")
->orWhere('containers.container_number', 'like', "%{$search}%")
->orWhere('invoices.mark_no', 'like', "%{$search}%")
->orWhere('mark_list.company_name', 'like', "%{$search}%")
->orWhere('mark_list.customer_name', 'like', "%{$search}%");
});
})
->when($request->filled('status'), function ($q) use ($request) {
$q->where('invoices.status', $request->status);
})
->orderByDesc('invoices.invoice_date') // इथे बदल
->orderByDesc('invoices.id') // same-date साठी tie-breaker
->get();
return view('admin.orders', compact('invoices'));
}
// -------------------------------------------------------------------------
// STEP 2 : DELETE TEMPORARY ITEM
// -------------------------------------------------------------------------
public function deleteTempItem(Request $request)
/* ---------------------------
* CREATE NEW ORDER (simple page)
* ---------------------------*/
public function create()
{
$index = $request->index;
$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.');
$markList = MarkList::where('status', 'active')->get();
return view('admin.orders_create', compact('markList'));
}
// -------------------------------------------------------------------------
// STEP 3 : FINISH ORDER
// -------------------------------------------------------------------------
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
// -------------------------------------------------------------------------
/* ---------------------------
* SHOW / POPUP
* ---------------------------*/
public function show($id)
{
$order = Order::with('items', 'markList')->findOrFail($id);
$user = null;
if ($order->markList && $order->markList->customer_id) {
$user = \App\Models\User::where('customer_id', $order->markList->customer_id)->first();
}
$user = $this->getCustomerFromOrder($order);
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')
->with('success', 'Order reset successfully.');
$user = null;
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);
}
}
}

View 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'));
}
}

View 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.');
}
}

View 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.');
}
}

View File

@@ -15,7 +15,12 @@ class UserRequestController extends Controller
public function index()
{
$requests = CustomerRequest::orderBy('id', 'desc')->get();
return view('admin.requests', compact('requests'));
$pendingProfileUpdates = \App\Models\UpdateRequest::where('status', 'pending')->count();
return view('admin.requests', compact(
'requests',
'pendingProfileUpdates'
));
}
// Approve user request
@@ -65,4 +70,61 @@ class UserRequestController extends Controller
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.');
}
}

View 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,
]);
}
}

View File

@@ -44,6 +44,8 @@ class RequestController extends Controller
'pincode' => $request->pincode,
'date' => Carbon::now()->toDateString(), // Auto current date
'status' => 'pending', // Default status
]);
// ✅ Response
@@ -53,4 +55,6 @@ class RequestController extends Controller
'data' => $newRequest
]);
}
}

View File

@@ -6,9 +6,49 @@ use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use PHPOpenSourceSaver\JWTAuth\Facades\JWTAuth;
use App\Models\User;
use Illuminate\Support\Facades\Log;
class UserAuthController extends Controller
{
public function refreshToken()
{
Log::info('🔄 [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
*/
@@ -60,6 +100,8 @@ class UserAuthController extends Controller
]);
}
/**
* User Logout
*/

View 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
]);
}
}

View 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'
// ]);
// }
}

View 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
]);
}
}

View 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);
}
}

View 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;
}
}
}
}

View File

@@ -1,22 +1,43 @@
<?php
// app/Models/Admin.php
namespace App\Models;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Spatie\Permission\Traits\HasRoles;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Support\Facades\Hash;
class Admin extends Authenticatable
{
use Notifiable;
use HasFactory, Notifiable, HasRoles;
protected $guard = 'admin';
protected $guard_name = 'admin';
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 = [
'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}";
}
}

View 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
View 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);
}
}

View 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
View 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
View 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);
}
}

View 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
View 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');
}
}

View 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');
}
}

View 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');
}
}

View 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
View 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;
}
}

View File

@@ -4,11 +4,13 @@ namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
class Order extends Model
{
use HasFactory;
use HasFactory,SoftDeletes;
protected $fillable = [
'order_id',
'mark_no',
@@ -39,4 +41,48 @@ class Order extends Model
{
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));
}
}

View File

@@ -4,10 +4,11 @@ namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
class OrderItem extends Model
{
use HasFactory;
use HasFactory, SoftDeletes;
protected $fillable = [
'order_id',

91
app/Models/Shipment.php Normal file
View 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));
}
}

View 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
View 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 . ')';
}
}

View 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');
}
}

View 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);
}
}

View File

@@ -15,7 +15,7 @@ class User extends Authenticatable implements JWTSubject
* The attributes that are mass assignable.
*/
protected $fillable = [
'customer_id', // CID-2025-000001 format
'customer_id',
'customer_name',
'company_name',
'designation',
@@ -25,10 +25,15 @@ class User extends Authenticatable implements JWTSubject
'pincode',
'date',
'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 = [
'password',
@@ -36,7 +41,7 @@ class User extends Authenticatable implements JWTSubject
];
/**
* The attributes that should be cast.
* Attribute casting.
*/
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()
{
@@ -55,10 +83,32 @@ class User extends Authenticatable implements JWTSubject
}
/**
* JWT Custom Claims.
* JWT Custom Claims
*/
public function getJWTCustomClaims()
{
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');
}
}

View 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;
}
});
}
}

View 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');
}
}

View File

@@ -6,13 +6,18 @@ use Illuminate\Foundation\Configuration\Middleware;
return Application::configure(basePath: dirname(__DIR__))
->withRouting(
web: __DIR__.'/../routes/web.php',
web: [
__DIR__.'/../routes/web.php',
__DIR__.'/../routes/channels.php',
],
api: __DIR__.'/../routes/api.php',
commands: __DIR__.'/../routes/console.php',
health: '/up',
)
->withMiddleware(function (Middleware $middleware): void {
->withMiddleware(function (Middleware $middleware) {
//
})
->withExceptions(function (Exceptions $exceptions): void {
->withExceptions(function (Exceptions $exceptions) {
//
})->create();
})
->create();

View File

@@ -3,4 +3,7 @@
return [
App\Providers\AppServiceProvider::class,
App\Providers\RouteServiceProvider::class,
App\Providers\AuthServiceProvider::class,
App\Providers\BroadcastServiceProvider::class,
];

View File

@@ -7,9 +7,14 @@
"license": "MIT",
"require": {
"php": "^8.2",
"barryvdh/laravel-dompdf": "^3.1",
"laravel/framework": "^12.0",
"laravel/reverb": "^1.6",
"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": {
"fakerphp/faker": "^1.23",

3218
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -85,6 +85,13 @@ return [
// 'driver' => 'database',
// 'table' => 'users',
// ],
'staff' => [
'driver' => 'eloquent',
'model' => App\Models\Staff::class,
],
],
/*

31
config/broadcasting.php Normal file
View 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',
],
],
];

View File

@@ -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
View 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
View 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),
],
],
],
];

View File

@@ -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');
}
};

View File

@@ -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');
}
};

View File

@@ -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');
}
};

View File

@@ -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');
}
}

View File

@@ -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');
}
}

View File

@@ -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');
}
}

View File

@@ -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');
}
}

View File

@@ -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']);
});
}
};

View File

@@ -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',
]);
});
}
};

View File

@@ -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
});
}
};

View File

@@ -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();
});
}
};

View File

@@ -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
}
});
}
};

View File

@@ -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');
});
}
};

View File

@@ -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');
}
}

View File

@@ -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);
}
}
});
}
};

View File

@@ -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']);
}
};

View 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');
}
};

View File

@@ -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) {
//
});
}
};

View File

@@ -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();
});
}
};

View File

@@ -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');
}
};

View File

@@ -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');
}
};

View File

@@ -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) {
//
});
}
};

View File

@@ -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');
});
}
};

View File

@@ -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');
}
};

View File

@@ -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');
}
};

View File

@@ -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');
}
};

View File

@@ -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');
});
}
};

View File

@@ -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');
});
}
};

View File

@@ -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');
}
};

View File

@@ -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');
}
};

View File

@@ -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']);
});
}
};

View File

@@ -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');
}
});
}
};

View File

@@ -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();
});
}
};

View File

@@ -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']);
});
}
};

View File

@@ -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');
}
});
}
}

View File

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

View 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

File diff suppressed because it is too large Load Diff

View File

@@ -13,5 +13,9 @@
"laravel-vite-plugin": "^2.0.0",
"tailwindcss": "^4.0.0",
"vite": "^7.0.7"
},
"dependencies": {
"laravel-echo": "^2.2.6",
"pusher-js": "^8.4.0"
}
}

BIN
public/images/kentlogo1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

View File

@@ -18,3 +18,4 @@ require __DIR__.'/../vendor/autoload.php';
$app = require_once __DIR__.'/../bootstrap/app.php';
$app->handleRequest(Request::capture());

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Some files were not shown because too many files have changed in this diff Show More