Compare commits

...

55 Commits

Author SHA256 Message Date
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
Abhishek Mali
6608caf61d message 2025-11-13 13:05:17 +05:30
Utkarsh Khedkar
15497076ae Merge branch 'main' of http://103.248.30.24:3000/kent-logistics/Kent-logistics-Laravel 2025-11-13 10:12:31 +05:30
Utkarsh Khedkar
e760b1c45f account changes 2025-11-12 19:56:06 +05:30
Abhishek Mali
5f477c03d0 order list update 2025-11-12 19:44:04 +05:30
Abhishek Mali
47711478a6 B
Merge branch 'main' of http://103.248.30.24:3000/kent-logistics/Kent-logistics-Laravel
2025-11-12 11:56:58 +05:30
Abhishek Mali
6a4f1dd4e9 order list added 2025-11-12 11:56:43 +05:30
Utkarsh Khedkar
195f1bff16 my updates 2025-11-11 14:51:35 +05:30
Abhishek Mali
b7085f81ab Merge branch 'main' of http://103.248.30.24:3000/kent-logistics/Kent-logistics-Laravel 2025-11-07 17:36:11 +05:30
Abhishek Mali
7c7ac7683a user model is added login/logout 2025-11-07 17:34:56 +05:30
Utkarsh Khedkar
9dee1a4eed Resolved merge conflict in admin login 2025-11-07 14:04:02 +05:30
Utkarsh Khedkar
780138eddf Save my local work 2025-11-07 13:56:10 +05:30
125 changed files with 24977 additions and 737 deletions

View File

@@ -0,0 +1,98 @@
<?php
namespace App\Exports;
use App\Models\Order;
use Illuminate\Http\Request;
use Maatwebsite\Excel\Concerns\FromCollection;
use Maatwebsite\Excel\Concerns\WithHeadings;
class OrdersExport implements FromCollection, WithHeadings
{
protected $request;
public function __construct(Request $request)
{
$this->request = $request;
}
private function buildQuery()
{
$query = Order::with(['markList', 'invoice', 'shipments']);
if ($this->request->filled('search')) {
$search = $this->request->search;
$query->where(function($q) use ($search) {
$q->where('order_id', 'like', "%{$search}%")
->orWhereHas('markList', function($q2) use ($search) {
$q2->where('company_name', 'like', "%{$search}%")
->orWhere('customer_id', 'like', "%{$search}%");
})
->orWhereHas('invoice', function($q3) use ($search) {
$q3->where('invoice_number', 'like', "%{$search}%");
});
});
}
if ($this->request->filled('status')) {
$query->whereHas('invoice', function($q) {
$q->where('status', $this->request->status);
});
}
if ($this->request->filled('shipment')) {
$query->whereHas('shipments', function($q) {
$q->where('status', $this->request->shipment);
});
}
return $query->latest('id');
}
public function collection()
{
$orders = $this->buildQuery()->get();
// Map to simple array rows suitable for Excel
return $orders->map(function($order) {
$mark = $order->markList;
$invoice = $order->invoice;
$shipment = $order->shipments->first() ?? null;
return [
'Order ID' => $order->order_id,
'Shipment ID' => $shipment->shipment_id ?? '-',
'Customer ID' => $mark->customer_id ?? '-',
'Company' => $mark->company_name ?? '-',
'Origin' => $mark->origin ?? $order->origin ?? '-',
'Destination' => $mark->destination ?? $order->destination ?? '-',
'Order Date' => $order->created_at ? $order->created_at->format('d-m-Y') : '-',
'Invoice No' => $invoice->invoice_number ?? '-',
'Invoice Date' => $invoice?->invoice_date ? \Carbon\Carbon::parse($invoice->invoice_date)->format('d-m-Y') : '-',
'Amount' => $invoice?->final_amount ? number_format($invoice->final_amount, 2) : '-',
'Amount + GST' => $invoice?->final_amount_with_gst ? number_format($invoice->final_amount_with_gst, 2) : '-',
'Invoice Status' => $invoice->status ? ucfirst($invoice->status) : 'Pending',
'Shipment Status' => $shipment?->status ? ucfirst(str_replace('_', ' ', $shipment->status)) : 'Pending',
];
});
}
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,141 @@
<?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'])->orderBy('id', 'desc');
// SEARCH FILTER
if (!empty($search)) {
$query->where(function ($q) use ($search) {
$q->where('customer_name', 'like', "%$search%")
->orWhere('email', 'like', "%$search%")
->orWhere('mobile_no', 'like', "%$search%")
->orWhere('customer_id', 'like', "%$search%");
});
}
// STATUS FILTER
if (!empty($status) && in_array($status, ['active', 'inactive'])) {
$query->where('status', $status);
}
// Get all customers for statistics (without pagination)
$allCustomers = $query->get();
// Get paginated customers for the table (10 per page)
$customers = $query->paginate(10);
return view('admin.customers', compact('customers', 'allCustomers', 'search', 'status'));
}
// ---------------------------------------------------------
// 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'])->findOrFail($id);
$totalOrders = $customer->orders->count();
$totalAmount = $customer->orders->sum('ttl_amount');
$recentOrders = $customer->orders()->latest()->take(5)->get();
return view('admin.customers_view', compact(
'customer',
'totalOrders',
'totalAmount',
'recentOrders'
));
}
// ---------------------------------------------------------
// 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,227 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use App\Models\Invoice;
use App\Models\InvoiceItem;
use Mpdf\Mpdf;
use App\Models\InvoiceInstallment;
use Illuminate\Support\Facades\Log;
class AdminInvoiceController extends Controller
{
// -------------------------------------------------------------
// INVOICE LIST PAGE
// -------------------------------------------------------------
public function index()
{
$invoices = Invoice::with(['order.shipments'])->latest()->get();
return view('admin.invoice', compact('invoices'));
}
// -------------------------------------------------------------
// POPUP VIEW (AJAX)
// -------------------------------------------------------------
public function popup($id)
{
$invoice = Invoice::with(['items', 'order'])->findOrFail($id);
// Find actual Shipment record
$shipment = \App\Models\Shipment::whereHas('items', function ($q) use ($invoice) {
$q->where('order_id', $invoice->order_id);
})
->first();
return view('admin.popup_invoice', compact('invoice', 'shipment'));
}
// -------------------------------------------------------------
// EDIT INVOICE PAGE
// -------------------------------------------------------------
public function edit($id)
{
$invoice = Invoice::with(['order.shipments'])->findOrFail($id);
$shipment = $invoice->order?->shipments?->first();
return view('admin.invoice_edit', compact('invoice', 'shipment'));
}
// -------------------------------------------------------------
// UPDATE INVOICE
// -------------------------------------------------------------
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',
'final_amount' => 'required|numeric|min:0',
'tax_type' => 'required|in:gst,igst',
'tax_percent' => 'required|numeric|min:0|max:28',
'status' => 'required|in:pending,paid,overdue',
'notes' => 'nullable|string',
]);
Log::info("✅ Validated Invoice Update Data", $data);
$finalAmount = floatval($data['final_amount']);
$taxPercent = floatval($data['tax_percent']);
$taxAmount = 0;
if ($data['tax_type'] === 'gst') {
Log::info("🟢 GST Selected", compact('taxPercent'));
$data['cgst_percent'] = $taxPercent / 2;
$data['sgst_percent'] = $taxPercent / 2;
$data['igst_percent'] = 0;
} else {
Log::info("🔵 IGST Selected", compact('taxPercent'));
$data['cgst_percent'] = 0;
$data['sgst_percent'] = 0;
$data['igst_percent'] = $taxPercent;
}
$taxAmount = ($finalAmount * $taxPercent) / 100;
$data['gst_amount'] = $taxAmount;
$data['final_amount_with_gst'] = $finalAmount + $taxAmount;
$data['gst_percent'] = $taxPercent;
Log::info("📌 Final Calculated Invoice Values", [
'invoice_id' => $invoice->id,
'final_amount' => $finalAmount,
'gst_amount' => $data['gst_amount'],
'final_amount_with_gst' => $data['final_amount_with_gst'],
'tax_type' => $data['tax_type'],
'cgst_percent' => $data['cgst_percent'],
'sgst_percent' => $data['sgst_percent'],
'igst_percent' => $data['igst_percent'],
]);
$invoice->update($data);
Log::info("✅ Invoice Updated Successfully", [
'invoice_id' => $invoice->id
]);
// regenerate PDF
$this->generateInvoicePDF($invoice);
return redirect()
->route('admin.invoices.index')
->with('success', 'Invoice updated & PDF generated successfully.');
}
// -------------------------------------------------------------
// PDF GENERATION USING mPDF
// -------------------------------------------------------------
public function generateInvoicePDF($invoice)
{
$invoice->load(['items', 'order.shipments']);
$shipment = $invoice->order?->shipments?->first();
$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/DELETE)
// -------------------------------------------------------------
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);
$paidTotal = $invoice->installments()->sum('amount');
// Use GST-inclusive total for all calculations/checks
$remaining = $invoice->final_amount_with_gst - $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;
// Mark as 'paid' if GST-inclusive total is cleared
if ($newPaid >= $invoice->final_amount_with_gst) {
$invoice->update(['status' => 'paid']);
}
return response()->json([
'status' => 'success',
'message' => 'Installment added successfully.',
'installment' => $installment,
'totalPaid' => $newPaid,
'gstAmount' => $invoice->gst_amount,
'finalAmountWithGst' => $invoice->final_amount_with_gst,
'baseAmount' => $invoice->final_amount,
'remaining' => max(0, $invoice->final_amount_with_gst - $newPaid),
'isCompleted' => $newPaid >= $invoice->final_amount_with_gst
]);
}
public function deleteInstallment($id)
{
$installment = InvoiceInstallment::findOrFail($id);
$invoice = $installment->invoice;
$installment->delete();
$paidTotal = $invoice->installments()->sum('amount');
$remaining = $invoice->final_amount_with_gst - $paidTotal;
// Update status if not fully paid anymore
if ($remaining > 0 && $invoice->status === "paid") {
$invoice->update(['status' => 'pending']);
}
return response()->json([
'status' => 'success',
'message' => 'Installment deleted.',
'totalPaid' => $paidTotal,
'gstAmount' => $invoice->gst_amount,
'finalAmountWithGst' => $invoice->final_amount_with_gst,
'baseAmount' => $invoice->final_amount,
'remaining' => $remaining,
'isZero' => $paidTotal == 0
]);
}
}

View File

@@ -0,0 +1,34 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use App\Models\MarkList;
class AdminMarkListController extends Controller
{
/**
* Show all mark list entries in descending order (latest first)
*/
public function index()
{
$markList = MarkList::orderBy('id', 'desc')->get();
return view('admin.mark_list', compact('markList'));
}
/**
* Toggle status between Active and Inactive
*/
public function toggleStatus($id)
{
$mark = MarkList::findOrFail($id);
// Toggle logic
$mark->status = $mark->status === 'active' ? 'inactive' : 'active';
$mark->save();
return redirect()->back()->with('success', 'Status updated successfully!');
}
}

View File

@@ -0,0 +1,741 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
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 PDF; // barryvdh/laravel-dompdf facade
use Maatwebsite\Excel\Facades\Excel;
use App\Exports\OrdersExport;
class AdminOrderController extends Controller
{
// ---------------------------
// LIST / DASHBOARD
// ---------------------------
public function index()
{
// raw list for admin dashboard (simple)
$orders = Order::latest()->get();
$markList = MarkList::where('status', 'active')->get();
return view('admin.dashboard', compact('orders', 'markList'));
}
/**
* Orders list (detailed)
*/
// public function orderShow()
// {
// $orders = Order::with(['markList', 'shipments', 'invoice'])
// ->latest('id')
// ->get();
// return view('admin.orders', compact('orders'));
// }
// ---------------------------
// CREATE NEW ORDER (simple DB flow)
// ---------------------------
/**
* Show create form (you can place create UI on separate view or dashboard)
*/
public function create()
{
// return a dedicated create view - create it at resources/views/admin/orders_create.blade.php
// If you prefer create UI on dashboard, change this to redirect route('admin.orders.index') etc.
$markList = MarkList::where('status', 'active')->get();
return view('admin.orders_create', compact('markList'));
}
/**
* Store a new order and optionally create initial invoice
*/
public function store(Request $request)
{
$data = $request->validate([
'mark_no' => 'required|string',
'origin' => 'nullable|string',
'destination' => 'nullable|string',
// totals optional when creating without items
'ctn' => 'nullable|numeric',
'qty' => 'nullable|numeric',
'ttl_qty' => 'nullable|numeric',
'ttl_amount' => 'nullable|numeric',
'cbm' => 'nullable|numeric',
'ttl_cbm' => 'nullable|numeric',
'kg' => 'nullable|numeric',
'ttl_kg' => 'nullable|numeric',
]);
$order = Order::create([
'order_id' => $this->generateOrderId(),
'mark_no' => $data['mark_no'],
'origin' => $data['origin'] ?? null,
'destination' => $data['destination'] ?? null,
'ctn' => $data['ctn'] ?? 0,
'qty' => $data['qty'] ?? 0,
'ttl_qty' => $data['ttl_qty'] ?? 0,
'ttl_amount' => $data['ttl_amount'] ?? 0,
'cbm' => $data['cbm'] ?? 0,
'ttl_cbm' => $data['ttl_cbm'] ?? 0,
'kg' => $data['kg'] ?? 0,
'ttl_kg' => $data['ttl_kg'] ?? 0,
'status' => 'pending',
]);
//If you want to auto-create an invoice at order creation, uncomment:
$this->createInvoice($order);
return redirect()->route('admin.orders.show', $order->id)
->with('success', 'Order created successfully.');
}
// ---------------------------
// SHOW / POPUP
// ---------------------------
public function show($id)
{
$order = Order::with('items', 'markList')->findOrFail($id);
$user = $this->getCustomerFromOrder($order);
return view('admin.orders_show', compact('order', 'user'));
}
// public function popup($id)
// {
// $order = Order::with(['items', 'markList'])->findOrFail($id);
// $user = $this->getCustomerFromOrder($order);
// return view('admin.popup', compact('order', 'user'));
// }
// ---------------------------
// ORDER ITEM MANAGEMENT (DB)
// ---------------------------
/**
* Add an item to an existing order
*/
public function addItem(Request $request, $orderId)
{
$order = Order::findOrFail($orderId);
$data = $request->validate([
'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',
]);
$data['order_id'] = $order->id;
OrderItem::create($data);
// recalc totals and save to order
$this->recalcTotals($order);
$this->updateInvoiceFromOrder($order); // <-- NEW
return redirect()->back()->with('success', 'Item added and totals updated.');
}
/**
* Soft-delete an order item and recalc totals
*/
public function deleteItem($id)
{
$item = OrderItem::findOrFail($id);
$order = $item->order;
$item->delete(); // soft delete
// recalc totals
$this->recalcTotals($order);
$this->updateInvoiceFromOrder($order);
return redirect()->back()->with('success', 'Item deleted and totals updated.');
}
/**
* Restore soft-deleted item and recalc totals
*/
public function restoreItem($id)
{
$item = OrderItem::withTrashed()->findOrFail($id);
$order = Order::findOrFail($item->order_id);
$item->restore();
// recalc totals
$this->recalcTotals($order);
$this->updateInvoiceFromOrder($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,
]);
// optionally recalc totals (not necessary unless you change item-level fields here)
$this->recalcTotals($order);
return redirect()->route('admin.orders.show', $order->id)
->with('success', 'Order updated successfully.');
}
/**
* Soft-delete whole order and its items (soft-delete items first then order)
*/
public function destroy($id)
{
$order = Order::findOrFail($id);
// soft-delete items first (so they show up in onlyTrashed for restore)
OrderItem::where('order_id', $order->id)->delete();
// then soft-delete order
$order->delete();
return redirect()->route('admin.orders.index')
->with('success', 'Order deleted successfully.');
}
// ---------------------------
// HELPERS
// ---------------------------
/**
* Recalculate totals for the order from current (non-deleted) items
*/
private function recalcTotals(Order $order)
{
// make sure we re-query live items (non-deleted)
$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)),
]);
}
/**
* Generate order id (keeps old format)
*/
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 (optional helper used by store/finish)
// ---------------------------
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,
]);
// clone order items into invoice items
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;
}
public function popup($id)
{
// Load order with items + markList
$order = Order::with(['items', 'markList'])->findOrFail($id);
// Fetch user based on markList customer_id (same as show method)
$user = null;
if ($order->markList && $order->markList->customer_id) {
$user = \App\Models\User::where('customer_id', $order->markList->customer_id)->first();
}
return view('admin.popup', compact('order', 'user'));
}
public function resetTemp()
{
session()->forget(['temp_order_items', 'mark_no', 'origin', 'destination']);
return redirect()->to(route('admin.orders.index') . '#createOrderForm')
->with('success', 'Order reset successfully.');
}
public function orderShow()
{
$orders = Order::with([
'markList', // company, customer, origin, destination, date
'shipments', // shipment_id, shipment_date, status
'invoice' // invoice number, dates, amounts, status
])
->latest('id') // show latest orders first
->get();
return view('admin.orders', compact('orders'));
}
// inside AdminOrderController
private function buildOrdersQueryFromRequest(Request $request)
{
$query = Order::with(['markList', 'invoice', 'shipments']);
// Search across order_id, markList.company_name, markList.customer_id, invoice.invoice_number
if ($request->filled('search')) {
$search = $request->search;
$query->where(function($q) use ($search) {
$q->where('order_id', 'like', "%{$search}%")
->orWhereHas('markList', function($q2) use ($search) {
$q2->where('company_name', 'like', "%{$search}%")
->orWhere('customer_id', 'like', "%{$search}%");
})
->orWhereHas('invoice', function($q3) use ($search) {
$q3->where('invoice_number', 'like', "%{$search}%");
});
});
}
// Invoice status filter
if ($request->filled('status')) {
$query->whereHas('invoice', function($q) use ($request) {
$q->where('status', $request->status);
});
}
// Shipment status filter
if ($request->filled('shipment')) {
$query->whereHas('shipments', function($q) use ($request) {
$q->where('status', $request->shipment);
});
}
// optional ordering
$query->latest('id');
return $query;
}
public function downloadPdf(Request $request)
{
// Build same filtered query used for table
$query = $this->buildOrdersQueryFromRequest($request);
$orders = $query->get();
// optional: pass filters to view for header
$filters = [
'search' => $request->search ?? null,
'status' => $request->status ?? null,
'shipment' => $request->shipment ?? null,
];
$pdf = PDF::loadView('admin.orders.pdf', compact('orders', 'filters'))
->setPaper('a4', 'landscape'); // adjust if needed
$fileName = 'orders-report'
. ($filters['status'] ? "-{$filters['status']}" : '')
. '-' . date('Y-m-d') . '.pdf';
return $pdf->download($fileName);
}
public function downloadExcel(Request $request)
{
// pass request to OrdersExport which will build Filtered query internally
return Excel::download(new OrdersExport($request), 'orders-report-' . date('Y-m-d') . '.xlsx');
}
public function addTempItem(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.');
}
// -------------------------------------------------------------------------
// 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 ORDER 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'],
]);
}
// =======================
// INVOICE CREATION START
// =======================
// 1. Auto-generate invoice number
$lastInvoice = \App\Models\Invoice::latest()->first();
$nextInvoice = $lastInvoice ? $lastInvoice->id + 1 : 1;
$invoiceNumber = 'INV-' . date('Y') . '-' . str_pad($nextInvoice, 6, '0', STR_PAD_LEFT);
// 2. Fetch customer (using mark list → customer_id)
$markList = MarkList::where('mark_no', $order->mark_no)->first();
$customer = null;
if ($markList && $markList->customer_id) {
$customer = \App\Models\User::where('customer_id', $markList->customer_id)->first();
}
// 3. Create Invoice Record
$invoice = \App\Models\Invoice::create([
'order_id' => $order->id,
'customer_id' => $customer->id ?? null,
'mark_no' => $order->mark_no,
'invoice_number' => $invoiceNumber,
'invoice_date' => now(),
'due_date' => now()->addDays(10),
'payment_method' => null,
'reference_no' => null,
'status' => 'pending',
'final_amount' => $total_amount,
'gst_percent' => 0,
'gst_amount' => 0,
'final_amount_with_gst' => $total_amount,
// snapshot customer fields
'customer_name' => $customer->customer_name ?? null,
'company_name' => $customer->company_name ?? null,
'customer_email' => $customer->email ?? null,
'customer_mobile' => $customer->mobile_no ?? null,
'customer_address' => $customer->address ?? null,
'pincode' => $customer->pincode ?? null,
'notes' => null,
]);
// 4. Clone order items into invoice_items
foreach ($order->items as $item) {
\App\Models\InvoiceItem::create([
'invoice_id' => $invoice->id,
'description' => $item->description,
'ctn' => $item->ctn,
'qty' => $item->qty,
'ttl_qty' => $item->ttl_qty,
'unit' => $item->unit,
'price' => $item->price,
'ttl_amount' => $item->ttl_amount,
'cbm' => $item->cbm,
'ttl_cbm' => $item->ttl_cbm,
'kg' => $item->kg,
'ttl_kg' => $item->ttl_kg,
'shop_no' => $item->shop_no,
]);
}
// 5. TODO: PDF generation (I will add this later)
$invoice->pdf_path = null; // placeholder for now
$invoice->save();
// =======================
// END INVOICE CREATION
// =======================
// CLEAR TEMP DATA
session()->forget(['temp_order_items', 'mark_no', 'origin', 'destination']);
return redirect()->route('admin.orders.index')
->with('success', 'Order + Invoice created successfully.');
}
// ---------------------------
// ORDER CRUD: update / destroy
// ---------------------------
public function updateItem(Request $request, $id)
{
$item = OrderItem::findOrFail($id);
$order = $item->order;
$item->update([
'description' => $request->description,
'ctn' => $request->ctn,
'qty' => $request->qty,
'ttl_qty' => $request->ttl_qty,
'unit' => $request->unit,
'price' => $request->price,
'ttl_amount' => $request->ttl_amount,
'cbm' => $request->cbm,
'ttl_cbm' => $request->ttl_cbm,
'kg' => $request->kg,
'ttl_kg' => $request->ttl_kg,
'shop_no' => $request->shop_no,
]);
$this->recalcTotals($order);
$this->updateInvoiceFromOrder($order); // <-- NEW
return back()->with('success', 'Item updated successfully');
}
private function updateInvoiceFromOrder(Order $order)
{
$invoice = Invoice::where('order_id', $order->id)->first();
if (!$invoice) {
return; // No invoice exists (should not happen normally)
}
// Update invoice totals
$invoice->final_amount = $order->ttl_amount;
$invoice->gst_percent = 0;
$invoice->gst_amount = 0;
$invoice->final_amount_with_gst = $order->ttl_amount;
$invoice->save();
// Delete old invoice items
InvoiceItem::where('invoice_id', $invoice->id)->delete();
// Re-create invoice items from updated order items
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,
]);
}
}
}

View File

@@ -0,0 +1,56 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\Order;
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)
{
// -------------------------------
// FETCH REPORT DATA
// ONLY orders that have BOTH:
// 1. Invoice
// 2. Shipment
// -------------------------------
$reports = DB::table('orders')
->join('shipment_items', 'shipment_items.order_id', '=', 'orders.id')
->join('shipments', 'shipments.id', '=', 'shipment_items.shipment_id')
->join('invoices', 'invoices.order_id', '=', 'orders.id')
->leftJoin('mark_list', 'mark_list.mark_no', '=', 'orders.mark_no')
->leftJoin('users', 'users.customer_id', '=', 'mark_list.customer_id')
->select(
'orders.id as order_pk',
'orders.order_id',
'orders.mark_no',
'orders.origin',
'orders.destination',
'shipments.id as shipment_pk',
'shipments.shipment_id',
'shipments.status as shipment_status',
'shipments.shipment_date',
'invoices.invoice_number',
'invoices.invoice_date',
'invoices.final_amount',
'invoices.status as invoice_status',
'mark_list.company_name',
'mark_list.customer_name'
)
->orderBy('shipments.shipment_date', 'desc')
->get();
return view('admin.reports', compact('reports'));
}
}

View File

@@ -0,0 +1,179 @@
<?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 {
$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' => $request->username,
'password' => Hash::make($request->password),
'type' => 'staff',
]);
// Generate EMPLOYEE ID using admin ID (safe)
$employeeId = 'EMP' . str_pad($admin->id, 4, '0', STR_PAD_LEFT);
$admin->update(['employee_id' => $employeeId]);
// 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,228 @@
<?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)->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();
$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_PENDING,
'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'
]);
// 1) Update shipment status
$shipment = Shipment::findOrFail($request->shipment_id);
$shipment->status = $request->status;
$shipment->save();
// 2) Update ALL related orders' status
foreach ($shipment->orders as $order) {
$order->status = $shipment->status; // status is string: pending, in_transit, dispatched, delivered
$order->save();
}
return redirect()->back()->with(
'success',
"Shipment status updated to {$shipment->statusLabel()} and related orders updated."
);
}
/**
* 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'));
}
}

View File

@@ -65,4 +65,52 @@ 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);
// FIX: Ensure data is array
$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;
}
}
}
$user->save();
$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,95 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Models\MarkList;
use App\Models\User;
use Carbon\Carbon;
use PHPOpenSourceSaver\JWTAuth\Facades\JWTAuth;
class MarkListController extends Controller
{
/**
* Add new Mark No (entered by user)
*/
public function addmarkno(Request $request)
{
try {
// Validate input
$request->validate([
'mark_no' => 'required|string|max:255|unique:mark_list,mark_no',
'origin' => 'required|string|max:255',
'destination' => 'required|string|max:255',
]);
} catch (\Illuminate\Validation\ValidationException $e) {
// Check if the mark_no uniqueness failed
if (isset($e->validator->failed()['mark_no']['Unique'])) {
return response()->json([
'success' => false,
'message' => 'Mark No already taken by another customer.'
], 400);
}
// Default validation error
return response()->json([
'success' => false,
'message' => $e->getMessage()
], 400);
}
// Authenticate user via JWT
$user = JWTAuth::parseToken()->authenticate();
if (!$user) {
return response()->json([
'success' => false,
'message' => 'Unauthorized'
], 401);
}
// Create mark record
$mark = MarkList::create([
'mark_no' => $request->mark_no,
'origin' => $request->origin,
'destination' => $request->destination,
'customer_id' => $user->customer_id,
'customer_name' => $user->customer_name,
'company_name' => $user->company_name,
'mobile_no' => $user->mobile_no,
'date' => Carbon::now()->toDateString(),
'status' => 'active',
]);
return response()->json([
'success' => true,
'message' => 'Mark No added successfully.',
'data' => $mark
], 200);
}
/**
* Show all marks for the logged-in user
*/
public function showmarklist()
{
$user = JWTAuth::parseToken()->authenticate();
if (!$user) {
return response()->json([
'success' => false,
'message' => 'Unauthorized'
], 401);
}
$marks = MarkList::where('customer_id', $user->customer_id)
->orderBy('id', 'desc')
->get();
return response()->json([
'success' => true,
'data' => $marks
]);
}
}

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

@@ -9,6 +9,74 @@ use App\Models\User;
class UserAuthController extends Controller
{
public function refreshToken()
{
\Log::info('🔄 refreshToken() called');
try {
// Get current token
$currentToken = JWTAuth::getToken();
if (!$currentToken) {
\Log::warning('⚠ No token provided in refreshToken()');
return response()->json([
'success' => false,
'message' => 'Token not provided',
], 401);
}
\Log::info('📥 Current Token:', ['token' => (string) $currentToken]);
// Try refreshing token
$newToken = JWTAuth::refresh($currentToken);
\Log::info('✅ Token refreshed successfully', ['new_token' => $newToken]);
return response()->json([
'success' => true,
'token' => $newToken,
]);
} catch (\Tymon\JWTAuth\Exceptions\TokenExpiredException $e) {
\Log::error('❌ TokenExpiredException in refreshToken()', [
'message' => $e->getMessage(),
]);
return response()->json([
'success' => false,
'message' => 'Token expired, cannot refresh.',
], 401);
} catch (\Tymon\JWTAuth\Exceptions\TokenInvalidException $e) {
\Log::error('❌ TokenInvalidException in refreshToken()', [
'message' => $e->getMessage(),
]);
return response()->json([
'success' => false,
'message' => 'Invalid token.',
], 401);
} catch (\Tymon\JWTAuth\Exceptions\JWTException $e) {
\Log::error('❌ JWTException in refreshToken()', [
'message' => $e->getMessage(),
]);
return response()->json([
'success' => false,
'message' => 'Could not refresh token.',
], 401);
} catch (\Exception $e) {
\Log::error('❌ General Exception in refreshToken()', [
'message' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
]);
return response()->json([
'success' => false,
'message' => 'Unexpected error while refreshing token.',
], 500);
}
}
/**
* User Login
*/
@@ -60,6 +128,8 @@ class UserAuthController extends Controller
]);
}
/**
* User Logout
*/

View File

@@ -0,0 +1,296 @@
<?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 all orders
// -------------------------------------
$orders = $user->orders()->with('invoice')->get();
// -------------------------------------
// Counts
// -------------------------------------
$totalOrders = $orders->count();
$delivered = $orders->where('status', 'delivered')->count();
$inTransit = $orders->where('status', '!=', 'delivered')->count();
$active = $totalOrders;
// -------------------------------------
// Total Amount = Invoice.total_with_gst
// -------------------------------------
$totalAmount = $orders->sum(function ($o) {
return $o->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, // formatted value
'total_raw' => $totalAmount // original value
]
]);
}
/**
* 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);
}
// Fetch orders for this user
$orders = $user->orders()
->with(['invoice', 'shipments'])
->orderBy('id', 'desc')
->get()
->map(function ($o) {
return [
'order_id' => $o->order_id,
'status' => $o->status,
'amount' => $o->ttl_amount,
'description'=> "Order from {$o->origin} to {$o->destination}",
'created_at' => $o->created_at,
];
});
return response()->json([
'success' => true,
'orders' => $orders
]);
}
public function orderDetails($order_id)
{
$user = JWTAuth::parseToken()->authenticate();
$order = $user->orders()
->with(['items'])
->where('order_id', $order_id)
->first();
if (!$order) {
return response()->json(['success' => false, 'message' => 'Order not found'], 404);
}
return response()->json([
'success' => true,
'order' => $order
]);
}
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();
$order = $user->orders()
->with('shipments')
->where('order_id', $order_id)
->first();
if (!$order) {
return response()->json(['success' => false, 'message' => 'Order not found'], 404);
}
$shipment = $order->shipments()->first();
return response()->json([
'success' => true,
'track' => [
'order_id' => $order->order_id,
'shipment_status' => $shipment->status ?? 'pending',
'shipment_date' => $shipment->shipment_date ?? null,
]
]);
}
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
]);
}
}

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,36 @@
<?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

@@ -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', // 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}";
}
}

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

96
app/Models/Invoice.php Normal file
View File

@@ -0,0 +1,96 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Invoice extends Model
{
use HasFactory;
protected $fillable = [
'order_id',
'customer_id',
'mark_no',
'invoice_number',
'invoice_date',
'due_date',
'payment_method',
'reference_no',
'status',
'final_amount', // without tax
'tax_type', // gst / igst
'gst_percent', // only used for gst UI input
'cgst_percent',
'sgst_percent',
'igst_percent',
'gst_amount', // total tax amount
'final_amount_with_gst',
'customer_name',
'company_name',
'customer_email',
'customer_mobile',
'customer_address',
'pincode',
'pdf_path',
'notes',
];
/****************************
* Relationships
****************************/
public function items()
{
return $this->hasMany(InvoiceItem::class)->orderBy('id', 'ASC');
}
public function order()
{
return $this->belongsTo(Order::class);
}
public function customer()
{
return $this->belongsTo(User::class, 'customer_id');
}
/****************************
* Helper Functions
****************************/
// Auto calculate GST fields (you can call this in controller before saving)
public function calculateTotals()
{
$gst = ($this->final_amount * $this->gst_percent) / 100;
$this->gst_amount = $gst;
$this->final_amount_with_gst = $this->final_amount + $gst;
}
// Check overdue status condition
public function isOverdue()
{
return $this->status === 'pending' && now()->gt($this->due_date);
}
public function getShipment()
{
return $this->order?->shipments?->first();
}
public function installments()
{
return $this->hasMany(InvoiceInstallment::class);
}
}

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

View File

@@ -0,0 +1,40 @@
<?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',
'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);
}
}

View File

@@ -2,28 +2,24 @@
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class MarkList extends Model
{
protected $table = 'mark_lists';
use HasFactory;
protected $table = 'mark_list';
protected $fillable = [
'mark_no', // e.g., MARK-2025-000001
'mark_no',
'origin',
'destination',
'customer_name',
'mobile_no',
'customer_id',
'customer_name',
'company_name',
'mobile_no',
'date',
'status',
'status'
];
/**
* Relationship: each mark list belongs to a customer (user)
*/
public function customer()
{
return $this->belongsTo(User::class, 'customer_id');
}
}

68
app/Models/Order.php Normal file
View File

@@ -0,0 +1,68 @@
<?php
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,SoftDeletes;
protected $fillable = [
'order_id',
'mark_no',
'origin',
'destination',
// totals only
'ctn',
'qty',
'ttl_qty',
'ttl_amount',
'cbm',
'ttl_cbm',
'kg',
'ttl_kg',
'status'
];
// One order has many items
public function items()
{
return $this->hasMany(OrderItem::class);
}
// Link using mark_no (optional)
public function markList()
{
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');
}
}

39
app/Models/OrderItem.php Normal file
View File

@@ -0,0 +1,39 @@
<?php
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, SoftDeletes;
protected $fillable = [
'order_id',
'description',
'ctn',
'qty',
'ttl_qty',
'unit',
'price',
'ttl_amount',
'cbm',
'ttl_cbm',
'kg',
'ttl_kg',
'shop_no',
'meta',
];
protected $casts = [
'meta' => 'array',
];
// Link to parent order
public function order()
{
return $this->belongsTo(Order::class);
}
}

80
app/Models/Shipment.php Normal file
View File

@@ -0,0 +1,80 @@
<?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');
}
// ---------------------------
// STATUS CONSTANTS
// ---------------------------
const STATUS_PENDING = 'pending';
const STATUS_IN_TRANSIT = 'in_transit';
const STATUS_DISPATCHED = 'dispatched';
const STATUS_DELIVERED = 'delivered';
public static function statusOptions()
{
return [
self::STATUS_PENDING => 'Pending',
self::STATUS_IN_TRANSIT => 'In Transit',
self::STATUS_DISPATCHED => 'Dispatched',
self::STATUS_DELIVERED => 'Delivered',
];
}
// ---------------------------
// HELPERS
// ---------------------------
public function totalOrdersCount()
{
return $this->items()->count();
}
public function statusLabel()
{
return self::statusOptions()[$this->status] ?? ucfirst($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,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,17 @@ class User extends Authenticatable implements JWTSubject
}
/**
* JWT Custom Claims.
* JWT Custom Claims
*/
public function getJWTCustomClaims()
{
return [];
}
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

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

View File

@@ -7,9 +7,13 @@
"license": "MIT",
"require": {
"php": "^8.2",
"barryvdh/laravel-dompdf": "^3.1",
"laravel/framework": "^12.0",
"laravel/tinker": "^2.10.1",
"php-open-source-saver/jwt-auth": "2.8"
"maatwebsite/excel": "^1.1",
"mpdf/mpdf": "^8.2",
"php-open-source-saver/jwt-auth": "2.8",
"spatie/laravel-permission": "^6.23"
},
"require-dev": {
"fakerphp/faker": "^1.23",

1737
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

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

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

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

@@ -1,45 +0,0 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up()
{
Schema::create('mark_lists', function (Blueprint $table) {
$table->id();
// Order as requested:
$table->string('mark_no');
$table->string('origin');
$table->string('destination');
$table->string('customer_name');
$table->string('mobile_no');
$table->unsignedBigInteger('customer_id');
$table->date('date')->nullable();
$table->enum('status', ['active', 'inactive'])->default('active');
$table->timestamps();
// Foreign key constraint
$table->foreign('customer_id')
->references('id')
->on('users')
->onDelete('cascade');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('mark_lists');
}
};

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()
{
Schema::create('mark_list', function (Blueprint $table) {
$table->id();
$table->string('mark_no')->unique();
$table->string('origin');
$table->string('destination');
$table->string('customer_id');
$table->string('customer_name');
$table->string('company_name')->nullable();
$table->string('mobile_no');
$table->date('date')->nullable();
$table->enum('status', ['active', 'inactive'])->default('active');
$table->timestamps();
});
}
public function down()
{
Schema::dropIfExists('mark_list');
}
};

View File

@@ -0,0 +1,36 @@
<?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('orders', function (Blueprint $table) {
$table->id();
$table->string('order_id')->unique(); // Example: KNT-25-00000001
$table->string('mark_no'); // linked to mark_lists.mark_no
$table->string('description')->nullable();
$table->string('origin')->nullable();
$table->string('destination')->nullable();
$table->integer('ctn')->nullable();
$table->integer('qty')->nullable();
$table->integer('ttl_qty')->nullable();
$table->string('unit')->nullable();
$table->decimal('price', 10, 2)->nullable();
$table->decimal('ttl_amount', 10, 2)->nullable();
$table->decimal('cbm', 10, 3)->nullable();
$table->decimal('ttl_cbm', 10, 3)->nullable();
$table->decimal('kg', 10, 3)->nullable();
$table->decimal('ttl_kg', 10, 3)->nullable();
$table->string('shop_no')->nullable();
$table->string('status')->default('in_transit'); // in_transit, dispatched, delivered
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('orders');
}
};

View File

@@ -0,0 +1,64 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateOrderItemsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('order_items', function (Blueprint $table) {
$table->bigIncrements('id');
// Link to orders table (parent order)
$table->foreignId('order_id')->constrained('orders')->onDelete('cascade');
// Sub-order / line item fields
$table->string('description')->nullable();
$table->integer('ctn')->nullable()->default(0);
$table->integer('qty')->nullable()->default(0);
$table->integer('ttl_qty')->nullable()->default(0);
$table->string('unit')->nullable();
// financials & measurements
$table->decimal('price', 14, 2)->nullable()->default(0.00);
$table->decimal('ttl_amount', 16, 2)->nullable()->default(0.00);
$table->decimal('cbm', 12, 3)->nullable()->default(0.000);
$table->decimal('ttl_cbm', 14, 3)->nullable()->default(0.000);
$table->decimal('kg', 12, 3)->nullable()->default(0.000);
$table->decimal('ttl_kg', 14, 3)->nullable()->default(0.000);
$table->string('shop_no')->nullable();
// optional extra data (json for extensibility)
$table->json('meta')->nullable();
$table->timestamps();
// Indexes for common queries
$table->index('order_id');
$table->index('ctn');
$table->index('qty');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('order_items');
}
}

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,52 @@
<?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();
// 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,36 @@
<?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();
$table->unsignedBigInteger('ticket_id'); // support ticket ID
$table->unsignedBigInteger('sender_id'); // user or admin/staff
$table->text('message')->nullable(); // message content
$table->string('file_path')->nullable(); // image/pdf/video
$table->string('file_type')->default('text'); // text/image/pdf/video
$table->timestamps();
// foreign keys
$table->foreign('ticket_id')->references('id')->on('support_tickets')->onDelete('cascade');
$table->foreign('sender_id')->references('id')->on('users')->onDelete('cascade'); // admin also stored in users table? If admin separate, change later.
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('chat_messages');
}
};

View File

@@ -0,0 +1,103 @@
<?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
// 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);
}
}

BIN
public/images/kent-logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 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.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 209 KiB

File diff suppressed because it is too large Load Diff

View File

@@ -1,12 +1,979 @@
@extends('admin.layouts.app')
@section('page-title', 'Dashboard')
@section('page-title', 'Customers')
@section('content')
<div class="card shadow-sm">
<div class="card-body">
<h4>Welcome to the Admin customer page</h4>
<p>Here you can manage all system modules.</p>
</div>
<style>
/* Import Inter font */
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
body {
font-family: 'Inter', sans-serif;
overflow-x: hidden; /* Prevent horizontal scroll on body */
}
.glass-card {
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(20px);
border-radius: 15px;
border: 1px solid rgba(255, 255, 255, 0.2);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
overflow: hidden;
}
/* New Stats Container */
.stats-container {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
gap: 12px;
margin: 20px 0;
}
.stat-card {
background: linear-gradient(135deg, #f8f9ff 0%, #f0f4ff 100%);
padding: 16px;
border-radius: 12px;
border-left: 4px solid #4f46e5;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
transition: transform 0.3s ease;
display: flex;
align-items: center;
gap: 12px;
min-height: 70px;
}
.stat-card:hover {
transform: translateY(-2px);
}
.stat-card.warning {
border-left-color: #f59e0b;
background: linear-gradient(135deg, #fffbeb 0%, #fef3c7 100%);
}
.stat-card.success {
border-left-color: #10b981;
background: linear-gradient(135deg, #ecfdf5 0%, #d1fae5 100%);
}
.stat-card.danger {
border-left-color: #ef4444;
background: linear-gradient(135deg, #fef2f2 0%, #fee2e2 100%);
}
.stat-card.info {
border-left-color: #3b82f6;
background: linear-gradient(135deg, #eff6ff 0%, #dbeafe 100%);
}
.stat-card.secondary {
border-left-color: #8b5cf6;
background: linear-gradient(135deg, #faf5ff 0%, #f3e8ff 100%);
}
.stat-icon {
width: 45px;
height: 45px;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
background: rgba(79, 70, 229, 0.1);
flex-shrink: 0;
}
.stat-icon i {
font-size: 18px;
color: #4f46e5;
}
.stat-card.warning .stat-icon {
background: rgba(245, 158, 11, 0.1);
}
.stat-card.warning .stat-icon i {
color: #f59e0b;
}
.stat-card.success .stat-icon {
background: rgba(16, 185, 129, 0.1);
}
.stat-card.success .stat-icon i {
color: #10b981;
}
.stat-card.danger .stat-icon {
background: rgba(239, 68, 68, 0.1);
}
.stat-card.danger .stat-icon i {
color: #ef4444;
}
.stat-card.info .stat-icon {
background: rgba(59, 130, 246, 0.1);
}
.stat-card.info .stat-icon i {
color: #3b82f6;
}
.stat-card.secondary .stat-icon {
background: rgba(139, 92, 246, 0.1);
}
.stat-card.secondary .stat-icon i {
color: #8b5cf6;
}
.stat-content {
flex: 1;
}
.stat-value {
font-size: 22px;
font-weight: 700;
color: #1a202c;
line-height: 1.2;
margin-bottom: 2px;
}
.stat-label {
font-size: 12px;
color: #718096;
font-weight: 500;
line-height: 1.3;
}
/* Updated Search Container - Wider with icon on left */
.search-container {
background: rgba(255, 255, 255, 0.9);
border-radius: 10px;
padding: 6px 12px;
border: 1px solid rgba(0, 0, 0, 0.1);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
width: 350px; /* Increased width */
display: flex;
align-items: center;
}
.search-input {
border: none;
background: transparent;
padding: 4px 8px;
width: 100%;
font-size: 13px;
outline: none;
font-family: 'Inter', sans-serif;
}
.search-input::placeholder {
color: #9ca3af;
font-size: 13px;
}
.filter-btn {
background: rgba(102, 126, 234, 0.1);
border: 2px solid transparent;
color: #667eea;
padding: 5px 12px;
border-radius: 8px;
font-weight: 600;
transition: all 0.3s ease;
margin: 0 3px;
text-decoration: none;
display: inline-block;
font-size: 0.75rem;
font-family: 'Inter', sans-serif;
}
.filter-btn.active {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border-color: #667eea;
}
.add-customer-btn {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border: none;
border-radius: 8px;
padding: 6px 16px;
color: white;
font-weight: 600;
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4);
transition: all 0.3s ease;
text-decoration: none;
display: inline-block;
font-size: 0.75rem;
font-family: 'Inter', sans-serif;
}
/* Updated Table Styles - Fixed horizontal scroll */
.table-glass {
background: rgba(255, 255, 255, 0.9);
border-radius: 10px;
overflow: hidden;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.05);
font-family: 'Inter', sans-serif;
}
/* Single gradient for entire header - Blue to Purple */
.table thead {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important;
}
.table-header {
color: white !important;
font-weight: 600;
font-size: 0.85rem;
padding: 14px 12px !important;
border: none;
font-family: 'Inter', sans-serif;
position: relative;
background: linear-gradient(135deg, #667eea 0%);;
}
/* Remove individual curved borders */
.table-header:first-child {
border-top-left-radius: 0;
}
.table-header:last-child {
border-top-right-radius: 0;
}
/* Apply rounded corners to the entire header container */
.table-container thead tr:first-child th:first-child {
border-top-left-radius: 10px;
}
.table-container thead tr:first-child th:last-child {
border-top-right-radius: 10px;
}
/* Updated Table Column Alignment */
.table > :not(caption) > * > * {
padding: 14px 12px;
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
font-size: 14px;
font-family: 'Inter', sans-serif;
vertical-align: middle;
}
/* Center align specific columns */
.table > :not(caption) > * > *:nth-child(2), /* Customer ID */
.table > :not(caption) > * > *:nth-child(3), /* Orders */
.table > :not(caption) > * > *:nth-child(4), /* Total */
.table > :not(caption) > * > *:nth-child(5) { /* Create Date */
text-align: center;
}
/* Customer Info column should remain left-aligned */
.table > :not(caption) > * > *:first-child {
text-align: left;
}
/* Status and Actions columns should remain as is */
.table > :not(caption) > * > *:nth-child(6), /* Status */
.table > :not(caption) > * > *:nth-child(7) { /* Actions */
text-align: center;
}
/* Updated header alignment to match */
.table-header:nth-child(2),
.table-header:nth-child(3),
.table-header:nth-child(4),
.table-header:nth-child(5) {
text-align: center;
}
/* Customer Info header stays left */
.table-header:first-child {
text-align: Center;
}
/* Status and Actions headers stay centered */
.table-header:nth-child(6),
.table-header:nth-child(7) {
text-align: center;
}
.customer-avatar {
width: 40px;
height: 40px;
border-radius: 50%;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
display: flex;
align-items: center;
justify-content: center;
color: white;
font-weight: 600;
font-size: 0.9rem;
box-shadow: 0 2px 8px rgba(102, 126, 234, 0.3);
}
.premium-badge {
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
color: white;
padding: 3px 8px;
border-radius: 8px;
font-size: 0.7rem;
font-weight: 600;
}
.regular-badge {
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
color: white;
padding: 3px 8px;
border-radius: 8px;
font-size: 0.7rem;
font-weight: 600;
}
.status-badge {
padding: 5px 10px;
border-radius: 8px;
font-size: 0.75rem;
font-weight: 600;
}
.active-status {
background: linear-gradient(135deg, #4cd964 0%, #5ac8fa 100%);
color: white;
}
.inactive-status {
background: linear-gradient(135deg, #ff6b6b 0%, #ee5a24 100%);
color: white;
}
.action-btn {
width: 30px;
height: 30px;
border-radius: 6px;
border: none;
background: rgba(102, 126, 234, 0.1);
color: #667eea;
display: inline-flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
margin: 0 2px;
text-decoration: none;
font-size: 0.8rem;
}
.action-btn:hover {
background: #667eea;
color: white;
transform: scale(1.05);
}
.customer-info-column {
min-width: 220px;
max-width: 220px; /* Added max-width to prevent overflow */
}
.table tbody tr {
transition: all 0.3s ease;
}
.table tbody tr:hover {
background: rgba(102, 126, 234, 0.03);
transform: translateX(5px);
}
.customer-details {
font-size: 0.8rem;
color: #6c757d;
}
/* Remove customer-stats since we're adding columns */
/* Enhanced table styling - Fixed horizontal scroll */
.table-container {
border-radius: 10px;
overflow: hidden;
width: 100%; /* Ensure container takes full width */
}
/* Fix table responsiveness */
.table-responsive {
overflow-x: auto;
-webkit-overflow-scrolling: touch;
width: 100%;
}
/* Ensure table doesn't exceed container */
.table {
width: 100%;
max-width: 100%;
margin-bottom: 0;
table-layout: auto; /* Changed to auto for better column distribution */
}
/* Fix for search and filter section */
.search-filter-container {
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: nowrap;
gap: 15px;
width: 100%;
}
.search-section {
display: flex;
align-items: center;
flex-shrink: 0;
}
.filter-section {
display: flex;
align-items: center;
flex-wrap: nowrap;
gap: 6px;
flex-shrink: 0;
}
/* New columns styling */
.orders-column, .total-column, .customer-id-column, .create-date-column {
text-align: center;
font-weight: 500;
min-width: 80px; /* Added minimum widths for consistency */
}
.orders-count {
font-size: 14px;
color: #1a202c;
font-weight: 600;
}
.total-amount {
font-size: 14px;
color: #10b981;
font-weight: 600;
}
/* ---------- Pagination Styles ---------- */
.pagination-container {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 15px;
padding: 12px 0;
border-top: 1px solid #eef3fb;
font-family: 'Inter', sans-serif;
width: 100%;
}
.pagination-info {
font-size: 13px;
color: #9ba5bb;
font-weight: 600;
flex-shrink: 0;
}
.pagination-controls {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
justify-content: flex-end;
}
.pagination-btn {
background: #fff;
border: 1px solid #e3eaf6;
color: #1a2951;
padding: 8px 12px;
border-radius: 6px;
font-size: 13px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
min-width: 40px;
height: 32px;
}
.pagination-btn:hover:not(:disabled) {
background: #1a2951;
color: white;
border-color: #1a2951;
}
.pagination-btn:disabled {
background: #f8fafc;
color: #cbd5e0;
border-color: #e2e8f0;
cursor: not-allowed;
opacity: 0.6;
}
.pagination-page-btn {
background: #fff;
border: 1px solid #e3eaf6;
color: #1a2951;
padding: 6px 12px;
border-radius: 6px;
font-size: 13px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
min-width: 36px;
text-align: center;
text-decoration: none;
display: inline-block;
}
.pagination-page-btn:hover {
background: #1a2951;
color: white;
border-color: #1a2951;
}
.pagination-page-btn.active {
background: #1a2951;
color: white;
border-color: #1a2951;
}
.pagination-pages {
display: flex;
gap: 4px;
align-items: center;
flex-wrap: wrap;
}
.pagination-ellipsis {
color: #9ba5bb;
font-size: 13px;
padding: 0 4px;
}
/* Image-based pagination buttons */
.pagination-img-btn {
background: #fff;
border: 1px solid #e3eaf6;
border-radius: 6px;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
min-width: 40px;
height: 32px;
padding: 0;
}
.pagination-img-btn:hover:not(:disabled) {
background: #1a2951;
border-color: #1a2951;
}
.pagination-img-btn:disabled {
background: #f8fafc;
border-color: #e2e8f0;
cursor: not-allowed;
opacity: 0.5;
}
.pagination-img-btn img {
width: 16px;
height: 16px;
filter: brightness(0) saturate(100%) invert(26%) sepia(89%) saturate(748%) hue-rotate(201deg) brightness(93%) contrast(89%);
transition: filter 0.3s ease;
}
.pagination-img-btn:hover:not(:disabled) img {
filter: brightness(0) saturate(100%) invert(100%) sepia(100%) saturate(0%) hue-rotate(288deg) brightness(106%) contrast(101%);
}
.pagination-img-btn:disabled img {
filter: brightness(0) saturate(100%) invert(84%) sepia(8%) saturate(165%) hue-rotate(179deg) brightness(89%) contrast(86%);
}
/* Mobile responsive fixes */
@media (max-width: 1200px) {
.table > :not(caption) > * > * {
padding: 12px 8px;
font-size: 13px;
}
.customer-info-column {
min-width: 180px;
max-width: 180px;
}
}
@media (max-width: 992px) {
.search-container {
width: 280px;
}
.stats-container {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 768px) {
.stats-container {
grid-template-columns: repeat(2, 1fr);
}
.search-filter-container {
flex-direction: column;
align-items: stretch;
gap: 10px;
}
.search-section {
justify-content: center;
}
.search-container {
width: 100%;
}
.filter-section {
justify-content: center;
flex-wrap: wrap;
}
.pagination-container {
flex-direction: column;
gap: 10px;
align-items: stretch;
}
.pagination-controls {
justify-content: center;
}
.table > :not(caption) > * > * {
padding: 10px 6px;
font-size: 12px;
}
.customer-info-column {
min-width: 150px;
max-width: 150px;
}
.customer-avatar {
width: 32px;
height: 32px;
font-size: 0.8rem;
}
.action-btn {
width: 26px;
height: 26px;
font-size: 0.7rem;
}
}
@media (max-width: 576px) {
.stats-container {
grid-template-columns: 1fr;
}
.table-responsive {
font-size: 12px;
}
.customer-info-column {
min-width: 120px;
max-width: 120px;
}
.premium-badge,
.regular-badge,
.status-badge {
font-size: 0.6rem;
padding: 2px 6px;
}
}
</style>
<div class="container-fluid">
<!-- Header - Removed gradient -->
<div class="d-flex justify-content-between align-items-center mb-3">
<h4 style="color: #2c3e50; font-weight: 700; font-family: 'Inter', sans-serif;">Customer List</h4>
</div>
<!-- Stats Cards with NEW DESIGN -->
<div class="stats-container">
<!-- Total Customers -->
<div class="stat-card">
<div class="stat-icon">
<i class="bi bi-people-fill"></i>
</div>
<div class="stat-content">
<div class="stat-value">{{ $allCustomers->count() }}</div>
<div class="stat-label">Total Customers</div>
</div>
</div>
<!-- New This Month -->
<div class="stat-card warning">
<div class="stat-icon">
<i class="bi bi-person-plus"></i>
</div>
<div class="stat-content">
<div class="stat-value">
@php
$newThisMonth = $allCustomers->filter(function($customer) {
return $customer->created_at->format('Y-m') === now()->format('Y-m');
})->count();
@endphp
{{ $newThisMonth }}
</div>
<div class="stat-label">New This Month</div>
</div>
</div>
<!-- Active Customers -->
<div class="stat-card success">
<div class="stat-icon">
<i class="bi bi-activity"></i>
</div>
<div class="stat-content">
<div class="stat-value">
@php
$activeCustomers = $allCustomers->where('status', 'active')->count();
@endphp
{{ $activeCustomers }}
</div>
<div class="stat-label">Active Customers</div>
</div>
</div>
<!-- Premium Customers -->
<div class="stat-card secondary">
<div class="stat-icon">
<i class="bi bi-award-fill"></i>
</div>
<div class="stat-content">
<div class="stat-value">
@php
$premiumCount = $allCustomers->where('customer_type', 'premium')->count();
@endphp
{{ $premiumCount }}
</div>
<div class="stat-label">Premium Customers</div>
</div>
</div>
</div>
<!-- Search and Filter Section -->
<div class="glass-card p-3 mb-3">
<div class="search-filter-container">
<!-- Search Section - Wider with icon on left -->
<div class="search-section">
<form method="GET" action="{{ route('admin.customers.index') }}" class="d-flex align-items-center">
<div class="search-container">
<i class="bi bi-search text-muted me-2"></i>
<input type="text"
name="search"
value="{{ $search ?? '' }}"
class="search-input"
placeholder="Search customers by name, email, or phone...">
@if(!empty($status))
<input type="hidden" name="status" value="{{ $status }}">
@endif
</div>
</form>
</div>
<!-- Filter Section -->
<div class="filter-section">
<a href="{{ route('admin.customers.index', ['status'=>'active', 'search'=>$search ?? '']) }}"
class="filter-btn {{ ($status ?? '') == 'active' ? 'active' : '' }}">
Active
</a>
<a href="{{ route('admin.customers.index', ['status'=>'inactive', 'search'=>$search ?? '']) }}"
class="filter-btn {{ ($status ?? '') == 'inactive' ? 'active' : '' }}">
Inactive
</a>
<a href="{{ route('admin.customers.index') }}"
class="filter-btn {{ empty($status) ? 'active' : '' }}">
All
</a>
@can('customer.create')
<a href="{{ route('admin.customers.add') }}" class="add-customer-btn">
<i class="bi bi-plus-circle me-1"></i>Add Customer
</a>
@endcan
</div>
</div>
</div>
<!-- Customer List Table -->
<div class="table-container">
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead class="gradient-table-header">
<tr>
<th class="table-header">Customer Info</th>
<th class="table-header">Customer ID</th>
<th class="table-header">Orders</th>
<th class="table-header">Total</th>
<th class="table-header">Create Date</th>
<th class="table-header">Status</th>
<th class="table-header" width="100">Actions</th>
</tr>
</thead>
<tbody id="customersTableBody">
@forelse($customers as $c)
<tr>
<!-- Customer Info Column -->
<td class="customer-info-column">
<div class="d-flex align-items-start">
<div class="customer-avatar me-3">
{{ strtoupper(substr($c->customer_name,0,1)) }}
</div>
<div>
<div class="fw-bold">{{ $c->customer_name }}</div>
@if($c->customer_type == 'premium')
<span class="premium-badge">Premium Customer</span>
@else
<span class="regular-badge">Regular Customer</span>
@endif
<div class="customer-details mt-1">
{{ $c->email }}<br>
{{ $c->mobile_no }}
</div>
</div>
</div>
</td>
<!-- Customer ID -->
<td class="customer-id-column">
<span class="fw-bold text-primary">{{ $c->customer_id }}</span>
</td>
<!-- Orders Column -->
<td class="orders-column">
<span class="orders-count">{{ $c->orders->count() }}</span>
</td>
<!-- Total Column -->
<td class="total-column">
<span class="total-amount">{{ number_format($c->orders->sum('ttl_amount'), 2) }}</span>
</td>
<!-- Create Date -->
<td class="create-date-column">
<span class="text-muted">{{ $c->created_at ? $c->created_at->format('d-m-Y') : '-' }}</span>
</td>
<!-- Status -->
<td>
@if($c->status === 'active')
<span class="status-badge active-status">Active</span>
@else
<span class="status-badge inactive-status">Inactive</span>
@endif
</td>
<!-- Actions -->
<td>
<div class="d-flex justify-content-center">
<a href="{{ route('admin.customers.view', $c->id) }}"
class="action-btn" title="View">
<i class="bi bi-eye"></i>
</a>
<form action="{{ route('admin.customers.status', $c->id) }}"
method="POST" style="display:inline-block;">
@csrf
<button class="action-btn" title="Toggle Status" type="submit">
<i class="bi bi-power"></i>
</button>
</form>
</div>
</td>
</tr>
@empty
<tr>
<td colspan="7" class="text-center py-4">
<i class="bi bi-people display-4 text-muted d-block mb-2"></i>
<span class="text-muted">No customers found.</span>
</td>
</tr>
@endforelse
</tbody>
</table>
</div>
</div>
<!-- Pagination Controls -->
<div class="pagination-container">
<div class="pagination-info" id="pageInfo">
Showing {{ $customers->firstItem() ?? 0 }} to {{ $customers->lastItem() ?? 0 }} of {{ $customers->total() }} entries
</div>
<div class="pagination-controls">
<button class="pagination-img-btn" id="prevPageBtn" title="Previous page" {{ $customers->onFirstPage() ? 'disabled' : '' }}>
<!-- Left arrow SVG -->
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10 12L6 8L10 4" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</button>
<div class="pagination-pages" id="paginationPages">
@for ($i = 1; $i <= $customers->lastPage(); $i++)
<a href="{{ $customers->url($i) }}"
class="pagination-page-btn {{ $customers->currentPage() == $i ? 'active' : '' }}">
{{ $i }}
</a>
@endfor
</div>
<button class="pagination-img-btn" id="nextPageBtn" title="Next page" {{ $customers->hasMorePages() ? '' : 'disabled' }}>
<!-- Right arrow SVG -->
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6 4L10 8L6 12" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</button>
</div>
</div>
</div>
@endsection
<script>
document.addEventListener('DOMContentLoaded', function() {
// Add hover effects to table rows
const tableRows = document.querySelectorAll('.table tbody tr');
tableRows.forEach(row => {
row.addEventListener('mouseenter', function() {
this.style.transform = 'translateX(5px)';
});
row.addEventListener('mouseleave', function() {
this.style.transform = 'translateX(0)';
});
});
// Pagination button handlers
document.getElementById('prevPageBtn').addEventListener('click', function() {
@if(!$customers->onFirstPage())
window.location.href = '{{ $customers->previousPageUrl() }}';
@endif
});
document.getElementById('nextPageBtn').addEventListener('click', function() {
@if($customers->hasMorePages())
window.location.href = '{{ $customers->nextPageUrl() }}';
@endif
});
});
</script>
@endsection

View File

@@ -0,0 +1,542 @@
@extends('admin.layouts.app')
@section('page-title', 'Add Customer')
@section('content')
<style>
:root {
--primary-gradient: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
--success-gradient: linear-gradient(135deg, #10b981 0%, #059669 100%);
--glass-bg: #ffffff;
--glass-border: rgba(255, 255, 255, 0.2);
--shadow-soft: 0 8px 32px rgba(0, 0, 0, 0.1);
--shadow-medium: 0 12px 48px rgba(0, 0, 0, 0.15);
--shadow-strong: 0 20px 60px rgba(0, 0, 0, 0.2);
}
/* Enhanced Card - No Blur - Original Width */
.card {
background: var(--glass-bg);
border-radius: 24px;
box-shadow: var(--shadow-strong);
border: 1px solid #e4e6ef;
animation: cardEntrance 0.8s cubic-bezier(0.25, 0.46, 0.45, 0.94);
position: relative;
overflow: hidden;
/* Maintaining original width */
width: 100%;
max-width: none;
}
.card::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(45deg,
rgba(102, 126, 234, 0.03) 0%,
rgba(118, 75, 162, 0.03) 50%,
rgba(16, 185, 129, 0.03) 100%);
pointer-events: none;
}
@keyframes cardEntrance {
0% {
opacity: 0;
transform: translateY(30px) scale(0.95);
}
100% {
opacity: 1;
transform: translateY(0) scale(1);
}
}
/* Premium Card Header */
.card-header {
background: var(--primary-gradient);
color: white;
border-bottom: none;
padding: 25px 30px;
position: relative;
overflow: hidden;
}
.card-header::before {
content: '';
position: absolute;
top: -50%;
left: -50%;
width: 200%;
height: 200%;
background: linear-gradient(45deg,
transparent 0%,
rgba(255, 255, 255, 0.1) 50%,
transparent 100%);
animation: headerShimmer 6s infinite linear;
transform: rotate(45deg);
}
@keyframes headerShimmer {
0% { transform: translateX(-100%) rotate(45deg); }
100% { transform: translateX(100%) rotate(45deg); }
}
.card-header h4 {
margin: 0;
font-weight: 800;
font-size: 1.5rem;
position: relative;
display: flex;
align-items: center;
gap: 12px;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.card-header h4::before {
content: '✨';
font-size: 1.3rem;
filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.2));
}
.card-body {
padding: 30px;
background: #f8fafc;
position: relative;
}
/* World-Class Form Elements - No Blur */
.form-label {
font-weight: 700;
color: #1e293b;
margin-bottom: 8px;
font-size: 0.9rem;
display: flex;
align-items: center;
gap: 6px;
}
.form-label::before {
content: '';
width: 3px;
height: 14px;
background: var(--primary-gradient);
border-radius: 2px;
display: inline-block;
}
.form-control, .form-select {
border: 2px solid #e2e8f0;
border-radius: 12px;
padding: 12px 16px;
font-size: 0.95rem;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
background: #ffffff;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
position: relative;
}
.form-control:focus, .form-select:focus {
border-color: #667eea;
box-shadow:
0 0 0 3px rgba(102, 126, 234, 0.15),
0 6px 20px rgba(102, 126, 234, 0.15),
inset 0 1px 0 rgba(255, 255, 255, 0.8);
background: #ffffff;
transform: translateY(-2px);
}
.form-control:hover, .form-select:hover {
border-color: #cbd5e1;
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
}
/* Enhanced Grid System - Original Layout */
.row.g-3 {
margin: -12px;
}
.row.g-3 > [class*="col-"] {
padding: 12px;
animation: formElementEntrance 0.6s cubic-bezier(0.25, 0.46, 0.45, 0.94) both;
}
@keyframes formElementEntrance {
0% {
opacity: 0;
transform: translateY(20px);
}
100% {
opacity: 1;
transform: translateY(0);
}
}
/* Staggered Animation Delays */
.row.g-3 > [class*="col-"]:nth-child(1) { animation-delay: 0.1s; }
.row.g-3 > [class*="col-"]:nth-child(2) { animation-delay: 0.15s; }
.row.g-3 > [class*="col-"]:nth-child(3) { animation-delay: 0.2s; }
.row.g-3 > [class*="col-"]:nth-child(4) { animation-delay: 0.25s; }
.row.g-3 > [class*="col-"]:nth-child(5) { animation-delay: 0.3s; }
.row.g-3 > [class*="col-"]:nth-child(6) { animation-delay: 0.35s; }
/* Premium Textarea */
textarea.form-control {
resize: vertical;
min-height: 100px;
line-height: 1.5;
background: #ffffff;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
textarea.form-control:focus {
transform: translateY(-2px);
box-shadow:
0 0 0 3px rgba(102, 126, 234, 0.15),
0 8px 24px rgba(102, 126, 234, 0.15);
}
/* World-Class Button Design */
.btn-success {
background: var(--success-gradient);
border: none;
padding: 14px 35px;
border-radius: 12px;
color: white;
font-weight: 700;
font-size: 1rem;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow: var(--shadow-medium);
position: relative;
overflow: hidden;
}
.btn-success::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg,
transparent,
rgba(255, 255, 255, 0.3),
transparent);
transition: left 0.5s ease;
}
.btn-success:hover::before {
left: 100%;
}
.btn-success:hover {
transform: translateY(-3px);
box-shadow:
0 12px 30px rgba(16, 185, 129, 0.35),
0 0 0 1px rgba(255, 255, 255, 0.1);
}
.btn-success:active {
transform: translateY(-1px);
transition: all 0.1s ease;
}
/* Enhanced Select Styling */
.form-select {
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%23667eea' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M6 8l4 4 4-4'/%3e%3c/svg%3e");
background-position: right 16px center;
background-repeat: no-repeat;
background-size: 16px;
padding-right: 45px;
cursor: pointer;
appearance: none;
}
/* Loading Animation */
.btn-success.loading {
pointer-events: none;
padding-right: 50px;
}
.btn-success.loading::after {
content: '';
position: absolute;
right: 16px;
top: 50%;
width: 16px;
height: 16px;
margin-top: -8px;
border: 2px solid transparent;
border-top: 2px solid white;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* Micro-interactions for Enhanced UX */
.form-control:valid {
border-left: 2px solid #10b981;
}
.form-control:invalid:not(:focus):not(:placeholder-shown) {
border-left: 2px solid #ef4444;
animation: shake 0.4s ease-in-out;
}
@keyframes shake {
0%, 100% { transform: translateX(0); }
25% { transform: translateX(-3px); }
75% { transform: translateX(3px); }
}
/* Secondary Button */
.btn-secondary {
background: #64748b;
border: none;
padding: 14px 30px;
border-radius: 12px;
color: white;
font-weight: 700;
font-size: 1rem;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow: var(--shadow-soft);
}
.btn-secondary:hover {
background: #475569;
transform: translateY(-2px);
box-shadow: var(--shadow-medium);
}
/* Required Field Indicator */
.required-field::after {
content: '*';
color: #ef4444;
margin-left: 4px;
font-weight: 800;
}
/* Success Animation */
@keyframes successPulse {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.02); }
}
.success-animation {
animation: successPulse 0.5s ease-in-out;
}
/* Input Hints */
.input-hint {
font-size: 0.8rem;
color: #64748b;
margin-top: 4px;
display: flex;
align-items: center;
gap: 4px;
}
.input-hint::before {
content: '💡';
font-size: 0.7rem;
}
/* Performance optimized animations */
@media (prefers-reduced-motion: reduce) {
* {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
</style>
<div class="container py-4">
<!-- Maintaining original container structure -->
<div class="card shadow-sm" style="border: none; background: transparent; box-shadow: none !important;">
<div class="card" style="margin: 0;">
<!-- Premium Card Header -->
<div class="card-header">
<h4>
<i class="bi bi-person-plus-fill me-2"></i>
Add New Customer
</h4>
</div>
<!-- Card Body - Original Structure -->
<div class="card-body">
<form action="{{ route('admin.customers.store') }}" method="POST" id="customerForm">
@csrf
<div class="row g-3">
<!-- Customer Name -->
<div class="col-md-6">
<label class="form-label required-field">Customer Name</label>
<input type="text"
name="customer_name"
class="form-control"
placeholder="Enter full name"
required
pattern="[A-Za-z\s]{2,}">
<div class="input-hint">Minimum 2 characters, letters only</div>
</div>
<!-- Company Name -->
<div class="col-md-6">
<label class="form-label">Company Name</label>
<input type="text"
name="company_name"
class="form-control"
placeholder="Enter company name">
</div>
<!-- Designation -->
<div class="col-md-6">
<label class="form-label">Designation</label>
<input type="text"
name="designation"
class="form-control"
placeholder="Enter job title">
</div>
<!-- Email -->
<div class="col-md-6">
<label class="form-label required-field">Email Address</label>
<input type="email"
name="email"
class="form-control"
placeholder="Enter email address"
required
pattern="[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}$">
<div class="input-hint">Valid email format required</div>
</div>
<!-- Mobile -->
<div class="col-md-6">
<label class="form-label required-field">Mobile Number</label>
<input type="tel"
name="mobile_no"
class="form-control"
placeholder="Enter mobile number"
required
pattern="[0-9]{10}">
<div class="input-hint">10 digits without spaces</div>
</div>
<!-- Pincode -->
<div class="col-md-6">
<label class="form-label">Pincode</label>
<input type="text"
name="pincode"
class="form-control"
placeholder="Enter pincode"
pattern="[0-9]{6}">
<div class="input-hint">6-digit pincode</div>
</div>
<!-- Address -->
<div class="col-12">
<label class="form-label">Address</label>
<textarea name="address"
class="form-control"
rows="3"
placeholder="Enter complete address"></textarea>
</div>
<!-- Customer Type -->
<div class="col-md-6">
<label class="form-label required-field">Customer Type</label>
<select name="customer_type" class="form-select" required>
<option value="regular">Regular</option>
<option value="premium">Premium</option>
</select>
<div class="input-hint">Premium customers get special benefits</div>
</div>
<!-- Status -->
<div class="col-md-6">
<label class="form-label required-field">Status</label>
<select name="status" class="form-select" required>
<option value="active">Active</option>
<option value="inactive">Inactive</option>
</select>
<div class="input-hint">Active customers can place orders</div>
</div>
</div>
<!-- Form Actions - Original Layout -->
<div class="text-end mt-4">
<a href="{{ route('admin.customers.index') }}" class="btn btn-secondary me-3">
<i class="bi bi-arrow-left me-2"></i>
Cancel
</a>
<button type="submit" class="btn-success" id="submitBtn">
<i class="bi bi-person-plus me-2"></i>
Create Customer
</button>
</div>
</form>
</div>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
const form = document.getElementById('customerForm');
const submitBtn = document.getElementById('submitBtn');
form.addEventListener('submit', function(e) {
// Add loading state
submitBtn.classList.add('loading');
submitBtn.innerHTML = '<i class="bi bi-hourglass-split me-2"></i>Creating...';
// Simulate form processing
setTimeout(() => {
submitBtn.classList.remove('loading');
submitBtn.classList.add('success-animation');
submitBtn.innerHTML = '<i class="bi bi-check-circle me-2"></i>Customer Created!';
setTimeout(() => {
submitBtn.classList.remove('success-animation');
submitBtn.innerHTML = '<i class="bi bi-person-plus me-2"></i>Create Customer';
}, 1500);
}, 1000);
});
// Real-time validation
const inputs = form.querySelectorAll('input[required]');
inputs.forEach(input => {
input.addEventListener('blur', function() {
if (this.value && this.checkValidity()) {
this.style.borderLeft = '2px solid #10b981';
} else if (this.value && !this.checkValidity()) {
this.style.borderLeft = '2px solid #ef4444';
} else {
this.style.borderLeft = '';
}
});
});
// Enhanced input interactions
const formControls = form.querySelectorAll('.form-control, .form-select');
formControls.forEach(control => {
control.addEventListener('focus', function() {
this.style.transform = 'translateY(-2px)';
});
control.addEventListener('blur', function() {
this.style.transform = 'translateY(0)';
});
});
});
</script>
@endsection

View File

@@ -0,0 +1,784 @@
@extends('admin.layouts.app')
@section('page-title', 'Customer Details')
@section('content')
<style>
:root {
--primary-gradient: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
--success-gradient: linear-gradient(135deg, #10b981 0%, #059669 100%);
--warning-gradient: linear-gradient(135deg, #f59e0b 0%, #d97706 100%);
--info-gradient: linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%);
--glass-bg: rgba(255, 255, 255, 0.95);
--shadow-strong: 0 20px 60px rgba(0, 0, 0, 0.15);
--shadow-medium: 0 12px 40px rgba(0, 0, 0, 0.1);
}
/* Main Card Animations */
.customer-card {
background: var(--glass-bg);
border-radius: 24px;
box-shadow: var(--shadow-strong);
border: 1px solid rgba(255, 255, 255, 0.3);
animation: cardEntrance 0.8s cubic-bezier(0.25, 0.46, 0.45, 0.94);
position: relative;
overflow: hidden;
}
.customer-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(45deg,
rgba(102, 126, 234, 0.03) 0%,
rgba(118, 75, 162, 0.03) 50%,
rgba(16, 185, 129, 0.03) 100%);
pointer-events: none;
}
@keyframes cardEntrance {
0% {
opacity: 0;
transform: translateY(30px) scale(0.95);
}
100% {
opacity: 1;
transform: translateY(0) scale(1);
}
}
/* Header Section */
.page-header {
background: var(--primary-gradient);
border-radius: 20px;
padding: 30px;
color: white;
margin-bottom: 30px;
position: relative;
overflow: hidden;
animation: headerSlide 0.6s ease-out;
}
@keyframes headerSlide {
0% {
opacity: 0;
transform: translateX(-30px);
}
100% {
opacity: 1;
transform: translateX(0);
}
}
.page-header::before {
content: '';
position: absolute;
top: -50%;
left: -50%;
width: 200%;
height: 200%;
background: linear-gradient(45deg,
transparent 0%,
rgba(255, 255, 255, 0.1) 50%,
transparent 100%);
animation: headerShimmer 8s infinite linear;
transform: rotate(45deg);
}
@keyframes headerShimmer {
0% { transform: translateX(-100%) rotate(45deg); }
100% { transform: translateX(100%) rotate(45deg); }
}
/* Customer Profile Section */
.profile-header {
padding: 40px 35px 30px;
position: relative;
}
.avatar-container {
position: relative;
display: inline-block;
}
.customer-avatar {
width: 100px;
height: 100px;
border-radius: 50%;
background: var(--primary-gradient);
display: flex;
align-items: center;
justify-content: center;
color: white;
font-weight: 800;
font-size: 2.5rem;
box-shadow: 0 12px 30px rgba(102, 126, 234, 0.4);
border: 4px solid white;
position: relative;
z-index: 2;
animation: avatarPulse 2s ease-in-out infinite;
}
@keyframes avatarPulse {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.05); }
}
.avatar-status {
position: absolute;
bottom: 8px;
right: 8px;
width: 24px;
height: 24px;
border-radius: 50%;
border: 3px solid white;
z-index: 3;
}
.status-active { background: var(--success-gradient); }
.status-inactive { background: #ef4444; }
/* Info Cards */
.info-card {
background: white;
border-radius: 20px;
padding: 25px;
box-shadow: var(--shadow-medium);
border: 1px solid #f1f5f9;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
height: 100%;
position: relative;
overflow: hidden;
}
.info-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 5px;
height: 100%;
background: var(--primary-gradient);
}
.info-card:hover {
transform: translateY(-5px);
box-shadow: 0 25px 50px rgba(0, 0, 0, 0.15);
}
.info-card h6 {
color: #64748b;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.5px;
font-size: 0.8rem;
margin-bottom: 15px;
display: flex;
align-items: center;
gap: 8px;
}
.info-card h6::before {
content: '';
width: 3px;
height: 12px;
background: var(--primary-gradient);
border-radius: 2px;
display: inline-block;
}
/* Stats Cards */
.stats-card {
background: white;
border-radius: 20px;
padding: 25px;
text-align: center;
box-shadow: var(--shadow-medium);
border: 1px solid #f1f5f9;
transition: all 0.3s ease;
position: relative;
overflow: hidden;
}
.stats-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 4px;
background: var(--primary-gradient);
}
.stats-card:hover {
transform: translateY(-5px);
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.12);
}
.stats-card.orders { --card-color: #3b82f6; }
.stats-card.amount { --card-color: #10b981; }
.stats-card.marks { --card-color: #8b5cf6; }
.stats-icon {
width: 60px;
height: 60px;
border-radius: 15px;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto 15px;
font-size: 1.5rem;
background: linear-gradient(135deg, var(--card-color), var(--card-color));
color: white;
box-shadow: 0 8px 20px rgba(59, 130, 246, 0.3);
}
.stats-card.orders .stats-icon { background: var(--info-gradient); }
.stats-card.amount .stats-icon { background: var(--success-gradient); }
.stats-card.marks .stats-icon { background: var(--primary-gradient); }
.stats-value {
font-size: 2.2rem;
font-weight: 800;
color: #1e293b;
margin-bottom: 5px;
line-height: 1;
}
.stats-label {
color: #64748b;
font-weight: 600;
font-size: 0.9rem;
}
/* Mark Numbers List */
.marks-section {
background: white;
border-radius: 20px;
box-shadow: var(--shadow-medium);
border: 1px solid #f1f5f9;
overflow: hidden;
}
.section-header {
background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
padding: 25px 30px;
border-bottom: 1px solid #e2e8f0;
}
.section-header h5 {
margin: 0;
color: #1e293b;
font-weight: 800;
display: flex;
align-items: center;
gap: 10px;
}
.section-header h5::before {
content: '';
width: 4px;
height: 20px;
background: var(--primary-gradient);
border-radius: 2px;
display: inline-block;
}
.mark-item {
display: flex;
align-items: center;
padding: 20px 30px;
border-bottom: 1px solid #f1f5f9;
transition: all 0.3s ease;
animation: itemEntrance 0.5s ease-out both;
}
@keyframes itemEntrance {
0% {
opacity: 0;
transform: translateX(-20px);
}
100% {
opacity: 1;
transform: translateX(0);
}
}
.mark-item:last-child {
border-bottom: none;
}
.mark-item:hover {
background: #f8fafc;
transform: translateX(5px);
}
.mark-badge {
background: var(--primary-gradient);
color: white;
padding: 8px 16px;
border-radius: 12px;
font-weight: 700;
font-size: 0.9rem;
margin-right: 15px;
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
}
.mark-route {
color: #64748b;
font-weight: 500;
}
/* Buttons - FIXED POSITIONING */
.btn-back {
background: rgba(255, 255, 255, 0.2);
border: 2px solid rgba(255, 255, 255, 0.3);
border-radius: 12px;
padding: 12px 25px;
color: white;
font-weight: 700;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
overflow: hidden;
backdrop-filter: blur(10px);
text-decoration: none;
display: inline-flex;
align-items: center;
gap: 8px;
}
.btn-back::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg,
transparent,
rgba(255, 255, 255, 0.2),
transparent);
transition: left 0.6s ease;
}
.btn-back:hover::before {
left: 100%;
}
.btn-back:hover {
background: rgba(255, 255, 255, 0.3);
border-color: rgba(255, 255, 255, 0.5);
transform: translateY(-2px);
color: white;
text-decoration: none;
}
.btn-premium {
background: var(--primary-gradient);
border: none;
border-radius: 12px;
padding: 12px 25px;
color: white;
font-weight: 700;
box-shadow: 0 8px 25px rgba(102, 126, 234, 0.4);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
overflow: hidden;
}
.btn-premium::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg,
transparent,
rgba(255, 255, 255, 0.2),
transparent);
transition: left 0.6s ease;
}
.btn-premium:hover::before {
left: 100%;
}
.btn-premium:hover {
transform: translateY(-3px);
box-shadow: 0 12px 35px rgba(102, 126, 234, 0.5);
}
.btn-outline-secondary {
border: 2px solid #64748b;
border-radius: 12px;
padding: 12px 25px;
color: #64748b;
font-weight: 700;
background: transparent;
transition: all 0.3s ease;
text-decoration: none;
display: inline-flex;
align-items: center;
gap: 8px;
}
.btn-outline-secondary:hover {
border-color: #475569;
color: #475569;
background: rgba(100, 116, 139, 0.05);
transform: translateY(-2px);
text-decoration: none;
}
/* Badges */
.type-badge {
padding: 8px 16px;
border-radius: 12px;
font-weight: 700;
font-size: 0.8rem;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.badge-premium {
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
color: white;
box-shadow: 0 4px 15px rgba(240, 147, 251, 0.4);
}
.badge-regular {
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
color: white;
box-shadow: 0 4px 15px rgba(79, 172, 254, 0.4);
}
.status-badge {
padding: 8px 16px;
border-radius: 12px;
font-weight: 700;
font-size: 0.8rem;
}
.badge-active {
background: var(--success-gradient);
color: white;
box-shadow: 0 4px 15px rgba(16, 185, 129, 0.4);
}
.badge-inactive {
background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
color: white;
box-shadow: 0 4px 15px rgba(239, 68, 68, 0.4);
}
/* Animations */
.animate-fade-in {
animation: fadeInUp 0.6s ease-out both;
}
@keyframes fadeInUp {
0% {
opacity: 0;
transform: translateY(20px);
}
100% {
opacity: 1;
transform: translateY(0);
}
}
.animation-delay-1 { animation-delay: 0.1s; }
.animation-delay-2 { animation-delay: 0.2s; }
.animation-delay-3 { animation-delay: 0.3s; }
.animation-delay-4 { animation-delay: 0.4s; }
/* Header Button Container - FIXED */
.header-actions {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 15px;
}
/* Responsive */
@media (max-width: 768px) {
.profile-header {
padding: 25px 20px;
text-align: center;
}
.customer-avatar {
width: 80px;
height: 80px;
font-size: 2rem;
}
.page-header {
padding: 20px;
}
.stats-card {
padding: 20px;
}
.stats-value {
font-size: 1.8rem;
}
.header-actions {
justify-content: center;
margin-top: 15px;
}
.page-header .row {
text-align: center;
}
}
@media (max-width: 576px) {
.btn-back, .btn-outline-secondary {
padding: 10px 20px;
font-size: 0.9rem;
}
.header-actions {
flex-direction: column;
gap: 10px;
width: 100%;
}
.header-actions a {
width: 100%;
justify-content: center;
}
}
</style>
<div class="container py-4">
{{-- HEADER - FIXED BUTTON POSITION --}}
<div class="page-header animate-fade-in">
<div class="row align-items-center">
<div class="col-md-8">
<h1 class="fw-bold mb-2">Customer Details</h1>
<p class="mb-0 opacity-90">Complete customer information and analytics</p>
</div>
<div class="col-md-4">
<div class="header-actions">
<a href="{{ route('admin.customers.index') }}" class="btn-back">
<i class="bi bi-arrow-left"></i>
Back to List
</a>
</div>
</div>
</div>
</div>
{{-- CUSTOMER PROFILE CARD --}}
<div class="customer-card mb-4">
<div class="profile-header">
<div class="row align-items-center">
<div class="col-auto">
<div class="avatar-container">
<div class="customer-avatar">
{{ strtoupper(substr($customer->customer_name,0,1)) }}
</div>
<div class="avatar-status {{ $customer->status == 'active' ? 'status-active' : 'status-inactive' }}"></div>
</div>
</div>
<div class="col">
<div class="d-flex align-items-center mb-2">
<h2 class="fw-bold mb-0 me-3">{{ $customer->customer_name }}</h2>
@if($customer->customer_type == 'premium')
<span class="type-badge badge-premium">
<i class="bi bi-award me-1"></i>Premium
</span>
@else
<span class="type-badge badge-regular">
<i class="bi bi-person me-1"></i>Regular
</span>
@endif
</div>
<p class="text-muted mb-2">
<i class="bi bi-building me-2"></i>
{{ $customer->company_name ?? 'No company specified' }}
</p>
<div class="d-flex align-items-center gap-3">
<span class="status-badge {{ $customer->status == 'active' ? 'badge-active' : 'badge-inactive' }}">
<i class="bi bi-circle-fill me-1" style="font-size: 0.6rem;"></i>
{{ ucfirst($customer->status) }}
</span>
<span class="text-muted">
<i class="bi bi-calendar me-1"></i>
Joined {{ $customer->created_at ? $customer->created_at->format('M d, Y') : 'N/A' }}
</span>
</div>
</div>
</div>
</div>
{{-- CUSTOMER INFORMATION --}}
<div class="row g-4 p-4">
{{-- Contact Information --}}
<div class="col-md-6 animate-fade-in animation-delay-1">
<div class="info-card">
<h6><i class="bi bi-telephone me-2"></i>Contact Information</h6>
<div class="mb-3">
<div class="d-flex align-items-center mb-2">
<i class="bi bi-envelope text-primary me-2"></i>
<strong class="me-2">Email:</strong>
<span>{{ $customer->email }}</span>
</div>
<div class="d-flex align-items-center mb-2">
<i class="bi bi-phone text-primary me-2"></i>
<strong class="me-2">Mobile:</strong>
<span>{{ $customer->mobile_no }}</span>
</div>
<div class="d-flex align-items-center mb-2">
<i class="bi bi-geo-alt text-primary me-2"></i>
<strong class="me-2">Address:</strong>
<span>{{ $customer->address ?? 'N/A' }}</span>
</div>
<div class="d-flex align-items-center">
<i class="bi bi-pin-map text-primary me-2"></i>
<strong class="me-2">Pincode:</strong>
<span>{{ $customer->pincode ?? 'N/A' }}</span>
</div>
</div>
</div>
</div>
{{-- Account Information --}}
<div class="col-md-6 animate-fade-in animation-delay-2">
<div class="info-card">
<h6><i class="bi bi-person-badge me-2"></i>Account Information</h6>
<div class="mb-3">
<div class="d-flex align-items-center mb-2">
<i class="bi bi-id-card text-primary me-2"></i>
<strong class="me-2">Customer ID:</strong>
<code class="bg-light px-2 py-1 rounded">{{ $customer->customer_id }}</code>
</div>
<div class="d-flex align-items-center mb-2">
<i class="bi bi-calendar-check text-primary me-2"></i>
<strong class="me-2">Registered On:</strong>
<span>{{ $customer->created_at ? $customer->created_at->format('d M, Y') : '-' }}</span>
</div>
<div class="d-flex align-items-center">
<i class="bi bi-briefcase text-primary me-2"></i>
<strong class="me-2">Designation:</strong>
<span>{{ $customer->designation ?? 'N/A' }}</span>
</div>
</div>
</div>
</div>
</div>
</div>
{{-- STATISTICS --}}
<div class="row g-4 mb-4">
{{-- Total Orders --}}
<div class="col-md-4 animate-fade-in animation-delay-1">
<div class="stats-card orders">
<div class="stats-icon">
<i class="bi bi-cart-check"></i>
</div>
<div class="stats-value">{{ $totalOrders }}</div>
<div class="stats-label">Total Orders</div>
</div>
</div>
{{-- Total Amount --}}
<div class="col-md-4 animate-fade-in animation-delay-2">
<div class="stats-card amount">
<div class="stats-icon">
<i class="bi bi-currency-rupee"></i>
</div>
<div class="stats-value">{{ number_format($totalAmount, 2) }}</div>
<div class="stats-label">Total Amount Spent</div>
</div>
</div>
{{-- Mark Count --}}
<div class="col-md-4 animate-fade-in animation-delay-3">
<div class="stats-card marks">
<div class="stats-icon">
<i class="bi bi-hash"></i>
</div>
<div class="stats-value">{{ $customer->marks->count() }}</div>
<div class="stats-label">Mark Numbers</div>
</div>
</div>
</div>
{{-- MARK NUMBERS SECTION --}}
<div class="marks-section animate-fade-in animation-delay-4">
<div class="section-header">
<h5>
<i class="bi bi-hash"></i>
Customer Mark Numbers
<span class="badge bg-primary ms-2">{{ $customer->marks->count() }}</span>
</h5>
</div>
<div class="section-body">
@if($customer->marks->count() == 0)
<div class="text-center py-5">
<i class="bi bi-inbox display-4 text-muted mb-3"></i>
<p class="text-muted mb-0">No mark numbers found for this customer.</p>
</div>
@else
@foreach($customer->marks as $index => $mark)
<div class="mark-item" style="animation-delay: {{ $index * 0.1 }}s">
<div class="mark-badge">
<i class="bi bi-tag me-1"></i>{{ $mark->mark_no }}
</div>
<div class="mark-route">
<i class="bi bi-arrow-right me-1"></i>
{{ $mark->origin }} {{ $mark->destination }}
</div>
</div>
@endforeach
@endif
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
// Add hover effects to interactive elements
const interactiveElements = document.querySelectorAll('.info-card, .stats-card, .mark-item');
interactiveElements.forEach(element => {
element.addEventListener('mouseenter', function() {
this.style.transform = this.classList.contains('mark-item') ? 'translateX(5px)' : 'translateY(-5px)';
});
element.addEventListener('mouseleave', function() {
this.style.transform = 'translateY(0)';
});
});
// Add loading animation to stats cards
const statsValues = document.querySelectorAll('.stats-value');
statsValues.forEach(value => {
const originalText = value.textContent;
value.textContent = '0';
setTimeout(() => {
value.textContent = originalText;
value.style.transform = 'scale(1.1)';
setTimeout(() => {
value.style.transform = 'scale(1)';
}, 300);
}, 500);
});
});
</script>
@endsection

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