Compare commits
47 Commits
5f477c03d0
...
main
| Author | SHA256 | Date | |
|---|---|---|---|
|
|
0a65d5f596 | ||
|
|
0a1d0a9c55 | ||
|
|
409a854d7b | ||
|
|
4dab96b8d1 | ||
|
|
e7fef314fc | ||
|
|
5114357ff2 | ||
|
|
0afcb23511 | ||
|
|
340c2b2132 | ||
|
|
44b8299b0e | ||
|
|
9b8c50fcec | ||
|
|
7a814dff1d | ||
|
|
f4730a81d8 | ||
|
|
3b24ee860a | ||
|
|
2dcd9fe332 | ||
|
|
922539844d | ||
|
|
3845972c5c | ||
|
|
64d8939208 | ||
|
|
ec2a0baceb | ||
|
|
68bfd180ed | ||
|
|
aa616fcf61 | ||
|
|
178fbb224c | ||
|
|
97db70c40e | ||
|
|
04b00c9db8 | ||
|
|
bebe0711f4 | ||
|
|
a14fe614e5 | ||
|
|
4d44e7df25 | ||
|
|
6e1ae8f380 | ||
|
|
837f4fe566 | ||
|
|
63daef6a92 | ||
|
|
56a17cf1e0 | ||
|
|
22be272067 | ||
|
|
df89031d36 | ||
|
|
8f6e30554b | ||
|
|
ca28409689 | ||
|
|
cfd128cf9b | ||
|
|
8b64515689 | ||
|
|
2703eff60f | ||
|
|
59574fd664 | ||
|
|
2f7eaf88a2 | ||
|
|
e071a082e0 | ||
|
|
b495a58d64 | ||
|
|
2c5d19779b | ||
|
|
5d29c8accb | ||
|
|
82bfcc5f33 | ||
|
|
6608caf61d | ||
|
|
15497076ae | ||
|
|
e760b1c45f |
98
app/Exports/OrdersExport.php
Normal file
98
app/Exports/OrdersExport.php
Normal 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',
|
||||
];
|
||||
}
|
||||
}
|
||||
399
app/Http/Controllers/Admin/AdminAccountController.php
Normal file
399
app/Http/Controllers/Admin/AdminAccountController.php
Normal file
@@ -0,0 +1,399 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Entry;
|
||||
use App\Models\Order;
|
||||
use App\Models\Installment;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class AdminAccountController extends Controller
|
||||
{
|
||||
public function updateEntry(Request $request)
|
||||
{
|
||||
try {
|
||||
$data = $request->validate([
|
||||
'entry_no' => 'required|exists:entries,entry_no',
|
||||
'description' => 'required|string|max:255',
|
||||
'order_quantity' => 'required|numeric|min:0',
|
||||
'region' => 'required|string|max:50',
|
||||
'amount' => 'required|numeric|min:0',
|
||||
//'payment_status' => 'required|string|max:50',
|
||||
]);
|
||||
|
||||
$entry = Entry::where('entry_no', $data['entry_no'])->first();
|
||||
|
||||
if (!$entry) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Entry not found.',
|
||||
], 404);
|
||||
}
|
||||
|
||||
$entry->description = $data['description'];
|
||||
$entry->order_quantity = $data['order_quantity'];
|
||||
$entry->region = $data['region'];
|
||||
$entry->amount = $data['amount'];
|
||||
//$entry->payment_status = $data['payment_status'];
|
||||
|
||||
$entry->save();
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'Entry updated successfully.',
|
||||
'entry' => $entry,
|
||||
]);
|
||||
} catch (\Throwable $e) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Server error: '.$e->getMessage(),
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 🚀 1. Get dashboard entries
|
||||
*/
|
||||
public function getDashboardData()
|
||||
{
|
||||
$entries = Entry::withCount('installments')
|
||||
->orderBy('id', 'desc')
|
||||
->get();
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'entries' => $entries
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 🚀 2. Get available consolidated orders
|
||||
*/
|
||||
public function getAvailableOrders()
|
||||
{
|
||||
|
||||
$orders = Order::whereDoesntHave('entries')
|
||||
->orderBy('id', 'desc')
|
||||
->get();
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'orders' => $orders,
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 🚀 3. Create new entry
|
||||
*/
|
||||
public function accountCreateOrder(Request $request)
|
||||
{
|
||||
$data = $request->validate([
|
||||
'description' => 'required|string|max:255',
|
||||
'region' => 'required|string|max:50',
|
||||
'amount' => 'required|numeric|min:1',
|
||||
'entry_date' => 'nullable|date',
|
||||
'selected_orders' => 'nullable|array',
|
||||
'selected_orders.*'=> 'integer|exists:orders,id',
|
||||
]);
|
||||
|
||||
return DB::transaction(function () use ($data) {
|
||||
|
||||
$entryDate = $data['entry_date'] ?? now()->toDateString();
|
||||
|
||||
// Count selected consolidated orders
|
||||
$orderQuantity = !empty($data['selected_orders'])
|
||||
? count($data['selected_orders'])
|
||||
: 0;
|
||||
|
||||
// Generate entry No: PAY-2025-001
|
||||
$prefix = 'PAY-' . date('Y') . '-';
|
||||
$last = Entry::where('entry_no', 'like', $prefix . '%')
|
||||
->orderBy('id', 'desc')
|
||||
->first();
|
||||
|
||||
$next = $last
|
||||
? intval(substr($last->entry_no, strrpos($last->entry_no, '-') + 1)) + 1
|
||||
: 1;
|
||||
|
||||
$entryNo = $prefix . str_pad($next, 7, '0', STR_PAD_LEFT);
|
||||
|
||||
// Create entry
|
||||
$entry = Entry::create([
|
||||
'entry_no' => $entryNo,
|
||||
'description' => $data['description'],
|
||||
'region' => $data['region'],
|
||||
'order_quantity' => $orderQuantity,
|
||||
'amount' => $data['amount'],
|
||||
'pending_amount' => $data['amount'],
|
||||
'entry_date' => $entryDate,
|
||||
'payment_status' => 'unpaid',
|
||||
'toggle_pos' => 0,
|
||||
'dispatch_status' => 'pending',
|
||||
]);
|
||||
|
||||
// Attach consolidated orders
|
||||
if (!empty($data['selected_orders'])) {
|
||||
$entry->orders()->attach($data['selected_orders']);
|
||||
}
|
||||
|
||||
$entry->load('orders');
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'Entry Created Successfully',
|
||||
'entry' => $entry
|
||||
], 201);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 🚀 4. Toggle payment switch
|
||||
*/
|
||||
public function togglePayment(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'entry_no' => 'required|string|exists:entries,entry_no',
|
||||
'toggle_pos' => 'required|integer|in:0,1,2',
|
||||
]);
|
||||
|
||||
$entry = Entry::where('entry_no', $request->entry_no)->firstOrFail();
|
||||
|
||||
$map = [
|
||||
0 => 'unpaid',
|
||||
1 => 'pending',
|
||||
2 => 'paid'
|
||||
];
|
||||
|
||||
$entry->update([
|
||||
'toggle_pos' => $request->toggle_pos,
|
||||
'payment_status' => $map[$request->toggle_pos],
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'entry' => $entry
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 🚀 5. Add Installment
|
||||
*/
|
||||
public function addInstallment(Request $request)
|
||||
{
|
||||
$data = $request->validate([
|
||||
'entry_no' => 'required|exists:entries,entry_no',
|
||||
'proc_date' => 'nullable|date',
|
||||
'amount' => 'required|numeric|min:1',
|
||||
'status' => 'required|string'
|
||||
]);
|
||||
|
||||
return DB::transaction(function () use ($data) {
|
||||
|
||||
$entry = Entry::where('entry_no', $data['entry_no'])
|
||||
->lockForUpdate()
|
||||
->firstOrFail();
|
||||
|
||||
$amount = floatval($data['amount']);
|
||||
|
||||
if ($amount > $entry->pending_amount) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Installment cannot exceed pending amount.'
|
||||
], 422);
|
||||
}
|
||||
|
||||
$installment = Installment::create([
|
||||
'entry_id' => $entry->id,
|
||||
'proc_date' => $data['proc_date'] ?? now()->toDateString(),
|
||||
'amount' => $amount,
|
||||
'description'=> $entry->description,
|
||||
'region' => $entry->region,
|
||||
'status' => $data['status']
|
||||
]);
|
||||
|
||||
$entry->pending_amount -= $amount;
|
||||
|
||||
if ($entry->pending_amount <= 0.001) {
|
||||
$entry->pending_amount = 0;
|
||||
$entry->dispatch_status = 'dispatched';
|
||||
}
|
||||
|
||||
$entry->save();
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'entry' => $entry,
|
||||
'installment' => $installment
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 🚀 6. Update Installment Status
|
||||
*/
|
||||
public function updateInstallmentStatus(Request $request)
|
||||
{
|
||||
$data = $request->validate([
|
||||
'installment_id' => 'required|exists:installments,id',
|
||||
'status' => 'required|string|max:50'
|
||||
]);
|
||||
|
||||
return DB::transaction(function () use ($data) {
|
||||
|
||||
$installment = Installment::lockForUpdate()->findOrFail($data['installment_id']);
|
||||
|
||||
$installment->status = $data['status'];
|
||||
$installment->save();
|
||||
|
||||
$entry = Entry::lockForUpdate()->find($installment->entry_id);
|
||||
|
||||
// If ANY installment is not delivered — entry is NOT delivered
|
||||
if ($entry->installments()->where('status', '!=', 'Delivered')->exists()) {
|
||||
// entry still in progress
|
||||
$entry->dispatch_status = 'pending';
|
||||
} else {
|
||||
// all installments delivered
|
||||
$entry->dispatch_status = 'delivered';
|
||||
}
|
||||
|
||||
$entry->save();
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'Installment updated successfully',
|
||||
'installment' => $installment,
|
||||
'entry' => $entry
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 🚀 6. Entry Details (installment history)
|
||||
*/
|
||||
public function getEntryDetails($entry_no)
|
||||
{
|
||||
$entry = Entry::with('installments')
|
||||
->where('entry_no', $entry_no)
|
||||
->firstOrFail();
|
||||
|
||||
$totalProcessed = $entry->amount - $entry->pending_amount;
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'entry' => $entry,
|
||||
'total_processed' => $totalProcessed,
|
||||
'pending' => $entry->pending_amount,
|
||||
]);
|
||||
}
|
||||
|
||||
//--------------------------
|
||||
//add order Entry
|
||||
//--------------------------
|
||||
public function addOrdersToEntry(Request $request)
|
||||
{
|
||||
$data = $request->validate([
|
||||
'entry_no' => 'required|exists:entries,entry_no',
|
||||
'order_ids' => 'required|array',
|
||||
'order_ids.*' => 'integer|exists:orders,id',
|
||||
]);
|
||||
|
||||
return DB::transaction(function () use ($data) {
|
||||
$entry = Entry::where('entry_no', $data['entry_no'])->firstOrFail();
|
||||
|
||||
|
||||
$entry->orders()->syncWithoutDetaching($data['order_ids']);
|
||||
|
||||
$entry->order_quantity = $entry->orders()->count();
|
||||
$entry->save();
|
||||
|
||||
$entry->load('orders');
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'Orders added successfully.',
|
||||
'entry' => $entry,
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
public function getEntryOrders($entry_no)
|
||||
{
|
||||
$entry = Entry::where('entry_no', $entry_no)
|
||||
->with('orders')
|
||||
->first();
|
||||
|
||||
if (!$entry) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Entry not found.',
|
||||
], 404);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'orders' => $entry->orders,
|
||||
]);
|
||||
}
|
||||
|
||||
public function removeOrderFromEntry(Request $request)
|
||||
{
|
||||
$data = $request->validate([
|
||||
'entry_no' => 'required|exists:entries,entry_no',
|
||||
'order_id' => 'required|integer|exists:orders,id',
|
||||
]);
|
||||
|
||||
return DB::transaction(function () use ($data) {
|
||||
$entry = Entry::where('entry_no', $data['entry_no'])->firstOrFail();
|
||||
|
||||
// order detach करा
|
||||
$entry->orders()->detach($data['order_id']);
|
||||
|
||||
// इथे quantity auto update
|
||||
$entry->order_quantity = $entry->orders()->count();
|
||||
$entry->save();
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'Order removed successfully.',
|
||||
'entry' => $entry,
|
||||
]);
|
||||
});
|
||||
}
|
||||
public function deleteEntry(Request $request)
|
||||
{
|
||||
try {
|
||||
$data = $request->validate([
|
||||
'entry_no' => 'required|exists:entries,entry_no',
|
||||
]);
|
||||
|
||||
$entry = Entry::where('entry_no', $data['entry_no'])->first();
|
||||
|
||||
if (!$entry) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Entry not found.',
|
||||
], 404);
|
||||
}
|
||||
|
||||
$entry->delete();
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'Entry deleted successfully.',
|
||||
]);
|
||||
} catch (\Throwable $e) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Server error: '.$e->getMessage(),
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -5,50 +5,52 @@ namespace App\Http\Controllers\Admin;
|
||||
use App\Http\Controllers\Controller;
|
||||
use 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.');
|
||||
}
|
||||
}
|
||||
|
||||
141
app/Http/Controllers/Admin/AdminCustomerController.php
Normal file
141
app/Http/Controllers/Admin/AdminCustomerController.php
Normal 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.');
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
227
app/Http/Controllers/Admin/AdminInvoiceController.php
Normal file
227
app/Http/Controllers/Admin/AdminInvoiceController.php
Normal 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
|
||||
]);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -5,74 +5,737 @@ namespace App\Http\Controllers\Admin;
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Models\Order;
|
||||
use App\Models\MarkList; // ✅ Correct model
|
||||
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(); // ✅ Correct usage
|
||||
$markList = MarkList::where('status', 'active')->get();
|
||||
|
||||
return view('admin.dashboard', compact('orders', 'markList'));
|
||||
}
|
||||
|
||||
public function store(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'mark_no' => 'required',
|
||||
'description' => 'required',
|
||||
]);
|
||||
/**
|
||||
* Orders list (detailed)
|
||||
*/
|
||||
// public function orderShow()
|
||||
// {
|
||||
// $orders = Order::with(['markList', 'shipments', 'invoice'])
|
||||
// ->latest('id')
|
||||
// ->get();
|
||||
|
||||
// ✅ Generate custom order_id like KNT-25-00000001
|
||||
$year = date('y');
|
||||
$prefix = "KNT-$year-";
|
||||
// return view('admin.orders', compact('orders'));
|
||||
// }
|
||||
|
||||
// Get the last order to increment number
|
||||
$lastOrder = Order::latest('id')->first();
|
||||
$nextNumber = $lastOrder ? intval(substr($lastOrder->order_id, -8)) + 1 : 1;
|
||||
|
||||
// Format number with leading zeros (8 digits)
|
||||
$newOrderId = $prefix . str_pad($nextNumber, 8, '0', STR_PAD_LEFT);
|
||||
|
||||
// ✅ Create order
|
||||
$order = new Order();
|
||||
$order->order_id = $newOrderId; // ✅ set this field
|
||||
$order->mark_no = $request->mark_no;
|
||||
$order->origin = $request->origin;
|
||||
$order->destination = $request->destination;
|
||||
$order->description = $request->description;
|
||||
$order->ctn = $request->ctn;
|
||||
$order->qty = $request->qty;
|
||||
$order->ttl_qty = $request->ttl_qty;
|
||||
$order->unit = $request->unit;
|
||||
$order->price = $request->price;
|
||||
$order->ttl_amount = $request->ttl_amount;
|
||||
$order->cbm = $request->cbm;
|
||||
$order->ttl_cbm = $request->ttl_cbm;
|
||||
$order->kg = $request->kg;
|
||||
$order->ttl_kg = $request->ttl_kg;
|
||||
$order->shop_no = $request->shop_no;
|
||||
$order->status = 'pending';
|
||||
$order->save();
|
||||
|
||||
return redirect()->back()->with('success', 'Order created successfully with ID: ' . $newOrderId);
|
||||
}
|
||||
|
||||
public function show($id)
|
||||
// ---------------------------
|
||||
// CREATE NEW ORDER (simple DB flow)
|
||||
// ---------------------------
|
||||
/**
|
||||
* Show create form (you can place create UI on separate view or dashboard)
|
||||
*/
|
||||
public function create()
|
||||
{
|
||||
$order = Order::with('markList')->findOrFail($id);
|
||||
|
||||
// Get the mark list associated with this order
|
||||
$markList = $order->markList;
|
||||
|
||||
// Fetch the user using the customer_id from mark list
|
||||
$user = null;
|
||||
if ($markList && $markList->customer_id) {
|
||||
$user = \App\Models\User::where('customer_id', $markList->customer_id)->first();
|
||||
}
|
||||
|
||||
return view('admin.orders_show', compact('order', 'markList', 'user'));
|
||||
// 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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
56
app/Http/Controllers/Admin/AdminReportController.php
Normal file
56
app/Http/Controllers/Admin/AdminReportController.php
Normal 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'));
|
||||
}
|
||||
}
|
||||
179
app/Http/Controllers/Admin/AdminStaffController.php
Normal file
179
app/Http/Controllers/Admin/AdminStaffController.php
Normal 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.');
|
||||
}
|
||||
}
|
||||
228
app/Http/Controllers/Admin/ShipmentController.php
Normal file
228
app/Http/Controllers/Admin/ShipmentController.php
Normal 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'));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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.');
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
296
app/Http/Controllers/user/UserOrderController.php
Normal file
296
app/Http/Controllers/user/UserOrderController.php
Normal 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
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
149
app/Http/Controllers/user/UserProfileController.php
Normal file
149
app/Http/Controllers/user/UserProfileController.php
Normal file
@@ -0,0 +1,149 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\User;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Models\UpdateRequest;
|
||||
use PHPOpenSourceSaver\JWTAuth\Facades\JWTAuth;
|
||||
|
||||
class UserProfileController extends Controller
|
||||
{
|
||||
/**
|
||||
* Get user profile
|
||||
*/
|
||||
public function profile()
|
||||
{
|
||||
try {
|
||||
$user = JWTAuth::parseToken()->authenticate();
|
||||
} catch (\Exception $e) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Token invalid or expired',
|
||||
], 401);
|
||||
}
|
||||
|
||||
if (! $user) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Unauthorized'
|
||||
], 401);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'data' => [
|
||||
'customer_id' => $user->customer_id,
|
||||
'customer_name' => $user->customer_name,
|
||||
'company_name' => $user->company_name,
|
||||
'designation' => $user->designation,
|
||||
'email' => $user->email,
|
||||
'mobile' => $user->mobile_no,
|
||||
'address' => $user->address,
|
||||
'pincode' => $user->pincode,
|
||||
'status' => $user->status,
|
||||
'customer_type' => $user->customer_type,
|
||||
'profile_image' => $user->profile_image ? url($user->profile_image) : null,
|
||||
'date' => $user->date,
|
||||
'created_at' => $user->created_at,
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Update profile IMAGE only (no admin approval)
|
||||
*/
|
||||
public function updateProfileImage(Request $request)
|
||||
{
|
||||
$user = JWTAuth::parseToken()->authenticate();
|
||||
|
||||
if (! $user) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Unauthorized'
|
||||
], 401);
|
||||
}
|
||||
|
||||
$request->validate([
|
||||
'profile_image' => 'required|image|mimes:jpg,jpeg,png|max:2048'
|
||||
]);
|
||||
|
||||
// DELETE OLD IMAGE
|
||||
if ($user->profile_image && file_exists(public_path($user->profile_image))) {
|
||||
@unlink(public_path($user->profile_image));
|
||||
}
|
||||
|
||||
// SAVE NEW IMAGE
|
||||
$file = $request->file('profile_image');
|
||||
$filename = 'profile_' . time() . '.' . $file->getClientOriginalExtension();
|
||||
$folder = 'profile_upload/';
|
||||
$file->move(public_path($folder), $filename);
|
||||
|
||||
$user->profile_image = $folder . $filename;
|
||||
$user->save();
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'Profile image updated successfully',
|
||||
'data' => [
|
||||
'customer_id' => $user->customer_id,
|
||||
'customer_name' => $user->customer_name,
|
||||
'company_name' => $user->company_name,
|
||||
'designation' => $user->designation,
|
||||
'email' => $user->email,
|
||||
'mobile' => $user->mobile_no,
|
||||
'address' => $user->address,
|
||||
'pincode' => $user->pincode,
|
||||
'status' => $user->status,
|
||||
'customer_type' => $user->customer_type,
|
||||
'profile_image' => url($user->profile_image),
|
||||
'date' => $user->date,
|
||||
]
|
||||
]);
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Submit profile update request (requires admin approval)
|
||||
*/
|
||||
public function updateProfileRequest(Request $request)
|
||||
{
|
||||
$user = JWTAuth::parseToken()->authenticate();
|
||||
|
||||
if (! $user) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Unauthorized'
|
||||
], 401);
|
||||
}
|
||||
|
||||
// Validate input
|
||||
$request->validate([
|
||||
'customer_name' => 'nullable|string|max:255',
|
||||
'company_name' => 'nullable|string|max:255',
|
||||
'designation' => 'nullable|string|max:255',
|
||||
'email' => 'nullable|email',
|
||||
'mobile_no' => 'nullable|string|max:15',
|
||||
'address' => 'nullable|string',
|
||||
'pincode' => 'nullable|string|max:10'
|
||||
]);
|
||||
|
||||
// SAVE AS ARRAY (NOT JSON STRING!)
|
||||
$updateReq = \App\Models\UpdateRequest::create([
|
||||
'user_id' => $user->id,
|
||||
'data' => $request->all(), // <---- FIXED
|
||||
'status' => 'pending',
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'Profile update request submitted. Waiting for admin approval.',
|
||||
'request_id' => $updateReq->id
|
||||
]);
|
||||
}
|
||||
|
||||
}
|
||||
36
app/Http/Middleware/JwtRefreshMiddleware.php
Normal file
36
app/Http/Middleware/JwtRefreshMiddleware.php
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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
37
app/Models/Entry.php
Normal file
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Entry extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'entry_no',
|
||||
'description',
|
||||
'region',
|
||||
'order_quantity',
|
||||
'amount',
|
||||
'pending_amount',
|
||||
'entry_date',
|
||||
'payment_status',
|
||||
'toggle_pos',
|
||||
'dispatch_status',
|
||||
];
|
||||
|
||||
// An entry can have multiple installments
|
||||
public function installments()
|
||||
{
|
||||
return $this->hasMany(Installment::class);
|
||||
}
|
||||
|
||||
// An entry can have multiple orders (consolidated orders)
|
||||
public function orders()
|
||||
{
|
||||
return $this->belongsToMany(Order::class, 'entry_order', 'entry_id', 'order_id')
|
||||
->withTimestamps();
|
||||
}
|
||||
}
|
||||
28
app/Models/EntryOrder.php
Normal file
28
app/Models/EntryOrder.php
Normal file
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class EntryOrder extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $table = 'entry_order';
|
||||
|
||||
protected $fillable = [
|
||||
'entry_id',
|
||||
'order_id'
|
||||
];
|
||||
|
||||
public function entry()
|
||||
{
|
||||
return $this->belongsTo(Entry::class);
|
||||
}
|
||||
|
||||
public function order()
|
||||
{
|
||||
return $this->belongsTo(Order::class);
|
||||
}
|
||||
}
|
||||
25
app/Models/Installment.php
Normal file
25
app/Models/Installment.php
Normal file
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Installment extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'entry_id',
|
||||
'proc_date',
|
||||
'amount',
|
||||
'description',
|
||||
'region',
|
||||
'status'
|
||||
];
|
||||
|
||||
public function entry()
|
||||
{
|
||||
return $this->belongsTo(Entry::class);
|
||||
}
|
||||
}
|
||||
96
app/Models/Invoice.php
Normal file
96
app/Models/Invoice.php
Normal 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);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
24
app/Models/InvoiceInstallment.php
Normal file
24
app/Models/InvoiceInstallment.php
Normal file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class InvoiceInstallment extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'invoice_id',
|
||||
'installment_date',
|
||||
'payment_method',
|
||||
'reference_no',
|
||||
'amount',
|
||||
];
|
||||
|
||||
public function invoice()
|
||||
{
|
||||
return $this->belongsTo(Invoice::class);
|
||||
}
|
||||
}
|
||||
40
app/Models/InvoiceItem.php
Normal file
40
app/Models/InvoiceItem.php
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -4,21 +4,65 @@ namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use App\Models\MarkList;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
|
||||
class Order extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
use HasFactory,SoftDeletes;
|
||||
|
||||
protected $fillable = [
|
||||
'order_id', 'mark_no', 'description', 'origin', 'destination',
|
||||
'ctn', 'qty', 'ttl_qty', 'unit', 'price', 'ttl_amount',
|
||||
'cbm', 'ttl_cbm', 'kg', 'ttl_kg', 'shop_no', 'status'
|
||||
'order_id',
|
||||
'mark_no',
|
||||
'origin',
|
||||
'destination',
|
||||
|
||||
// totals only
|
||||
'ctn',
|
||||
'qty',
|
||||
'ttl_qty',
|
||||
'ttl_amount',
|
||||
'cbm',
|
||||
'ttl_cbm',
|
||||
'kg',
|
||||
'ttl_kg',
|
||||
|
||||
'status'
|
||||
];
|
||||
|
||||
// Relation using mark_no instead of id
|
||||
// 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
39
app/Models/OrderItem.php
Normal 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
80
app/Models/Shipment.php
Normal 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);
|
||||
}
|
||||
}
|
||||
61
app/Models/ShipmentItem.php
Normal file
61
app/Models/ShipmentItem.php
Normal file
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class ShipmentItem extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'shipment_id',
|
||||
'order_id',
|
||||
|
||||
// OLD fields (keep them if old data exists)
|
||||
'order_ctn',
|
||||
'order_qty',
|
||||
'order_ttl_qty',
|
||||
'order_ttl_amount',
|
||||
'order_ttl_kg',
|
||||
|
||||
// NEW fields (added for correct shipments)
|
||||
'ctn',
|
||||
'qty',
|
||||
'ttl_qty',
|
||||
|
||||
'cbm',
|
||||
'ttl_cbm',
|
||||
|
||||
'kg',
|
||||
'ttl_kg',
|
||||
|
||||
'ttl_amount',
|
||||
];
|
||||
|
||||
// ---------------------------
|
||||
// RELATIONSHIPS
|
||||
// ---------------------------
|
||||
|
||||
public function shipment()
|
||||
{
|
||||
return $this->belongsTo(Shipment::class);
|
||||
}
|
||||
|
||||
public function order()
|
||||
{
|
||||
return $this->belongsTo(Order::class);
|
||||
}
|
||||
|
||||
// Helper: return order data with fallback to snapshot
|
||||
public function getDisplayQty()
|
||||
{
|
||||
return $this->qty;
|
||||
}
|
||||
|
||||
public function getDisplayAmount()
|
||||
{
|
||||
return $this->ttl_amount;
|
||||
}
|
||||
}
|
||||
81
app/Models/Staff.php
Normal file
81
app/Models/Staff.php
Normal file
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Spatie\Permission\Traits\HasRoles;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
|
||||
class Staff extends Authenticatable
|
||||
{
|
||||
use Notifiable, HasRoles, SoftDeletes;
|
||||
|
||||
/**
|
||||
* The guard name used by Spatie.
|
||||
* Make sure this matches the guard you'll use for admin/staff auth (usually 'web' or 'admin').
|
||||
*/
|
||||
protected $guard_name = 'admin';
|
||||
|
||||
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
*/
|
||||
protected $fillable = [
|
||||
'employee_id',
|
||||
'name',
|
||||
'email',
|
||||
'phone',
|
||||
'emergency_phone',
|
||||
'address',
|
||||
'role', // business role/title (not Spatie role)
|
||||
'department',
|
||||
'designation',
|
||||
'joining_date',
|
||||
'status',
|
||||
'additional_info',
|
||||
'username',
|
||||
'password',
|
||||
];
|
||||
|
||||
/**
|
||||
* Hidden attributes (not returned in arrays / JSON).
|
||||
*/
|
||||
protected $hidden = [
|
||||
'password',
|
||||
];
|
||||
|
||||
/**
|
||||
* Casts
|
||||
*/
|
||||
protected $casts = [
|
||||
'joining_date' => 'date',
|
||||
];
|
||||
|
||||
/**
|
||||
* Mutator: automatically hash password when set.
|
||||
* Accepts plain text and hashes it with bcrypt.
|
||||
*/
|
||||
public function setPasswordAttribute($value)
|
||||
{
|
||||
if (empty($value)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If already hashed (starts with $2y$), don't double-hash
|
||||
if (Hash::needsRehash($value)) {
|
||||
$this->attributes['password'] = Hash::make($value);
|
||||
} else {
|
||||
$this->attributes['password'] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Optional helper to get display name (useful in views/logs).
|
||||
*/
|
||||
public function getDisplayNameAttribute()
|
||||
{
|
||||
return $this->name . ' (' . $this->employee_id . ')';
|
||||
}
|
||||
}
|
||||
30
app/Models/UpdateRequest.php
Normal file
30
app/Models/UpdateRequest.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class UpdateRequest extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $table = 'update_requests';
|
||||
|
||||
protected $fillable = [
|
||||
'user_id',
|
||||
'data',
|
||||
'status',
|
||||
'admin_note',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'data' => 'array', // converts JSON to array automatically
|
||||
];
|
||||
|
||||
// Relationship: request belongs to a user
|
||||
public function user()
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,7 @@ class User extends Authenticatable implements JWTSubject
|
||||
* The attributes that are mass assignable.
|
||||
*/
|
||||
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');
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
40
app/Providers/AuthServiceProvider.php
Normal file
40
app/Providers/AuthServiceProvider.php
Normal file
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
|
||||
class AuthServiceProvider extends ServiceProvider
|
||||
{
|
||||
/**
|
||||
* The policy mappings for the application.
|
||||
*
|
||||
* @var array<class-string, class-string>
|
||||
*/
|
||||
protected $policies = [
|
||||
// 'App\Models\Model' => 'App\Policies\ModelPolicy',
|
||||
];
|
||||
|
||||
/**
|
||||
* Register any authentication / authorization services.
|
||||
*/
|
||||
public function boot()
|
||||
{
|
||||
$this->registerPolicies();
|
||||
|
||||
// SUPER ADMIN bypass
|
||||
Gate::before(function ($user, $ability) {
|
||||
if ($user->hasRole('super-admin')) {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
// ADMIN bypass
|
||||
Gate::before(function ($user, $ability) {
|
||||
if ($user->hasRole('admin')) {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -3,4 +3,5 @@
|
||||
return [
|
||||
App\Providers\AppServiceProvider::class,
|
||||
App\Providers\RouteServiceProvider::class,
|
||||
App\Providers\AuthServiceProvider::class,
|
||||
];
|
||||
|
||||
@@ -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
1737
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -85,6 +85,11 @@ return [
|
||||
// 'driver' => 'database',
|
||||
// 'table' => 'users',
|
||||
// ],
|
||||
'staff' => [
|
||||
'driver' => 'eloquent',
|
||||
'model' => App\Models\Staff::class,
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
/*
|
||||
|
||||
202
config/permission.php
Normal file
202
config/permission.php
Normal file
@@ -0,0 +1,202 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
'models' => [
|
||||
|
||||
/*
|
||||
* When using the "HasPermissions" trait from this package, we need to know which
|
||||
* Eloquent model should be used to retrieve your permissions. Of course, it
|
||||
* is often just the "Permission" model but you may use whatever you like.
|
||||
*
|
||||
* The model you want to use as a Permission model needs to implement the
|
||||
* `Spatie\Permission\Contracts\Permission` contract.
|
||||
*/
|
||||
|
||||
'permission' => Spatie\Permission\Models\Permission::class,
|
||||
|
||||
/*
|
||||
* When using the "HasRoles" trait from this package, we need to know which
|
||||
* Eloquent model should be used to retrieve your roles. Of course, it
|
||||
* is often just the "Role" model but you may use whatever you like.
|
||||
*
|
||||
* The model you want to use as a Role model needs to implement the
|
||||
* `Spatie\Permission\Contracts\Role` contract.
|
||||
*/
|
||||
|
||||
'role' => Spatie\Permission\Models\Role::class,
|
||||
|
||||
],
|
||||
|
||||
'table_names' => [
|
||||
|
||||
/*
|
||||
* When using the "HasRoles" trait from this package, we need to know which
|
||||
* table should be used to retrieve your roles. We have chosen a basic
|
||||
* default value but you may easily change it to any table you like.
|
||||
*/
|
||||
|
||||
'roles' => 'roles',
|
||||
|
||||
/*
|
||||
* When using the "HasPermissions" trait from this package, we need to know which
|
||||
* table should be used to retrieve your permissions. We have chosen a basic
|
||||
* default value but you may easily change it to any table you like.
|
||||
*/
|
||||
|
||||
'permissions' => 'permissions',
|
||||
|
||||
/*
|
||||
* When using the "HasPermissions" trait from this package, we need to know which
|
||||
* table should be used to retrieve your models permissions. We have chosen a
|
||||
* basic default value but you may easily change it to any table you like.
|
||||
*/
|
||||
|
||||
'model_has_permissions' => 'model_has_permissions',
|
||||
|
||||
/*
|
||||
* When using the "HasRoles" trait from this package, we need to know which
|
||||
* table should be used to retrieve your models roles. We have chosen a
|
||||
* basic default value but you may easily change it to any table you like.
|
||||
*/
|
||||
|
||||
'model_has_roles' => 'model_has_roles',
|
||||
|
||||
/*
|
||||
* When using the "HasRoles" trait from this package, we need to know which
|
||||
* table should be used to retrieve your roles permissions. We have chosen a
|
||||
* basic default value but you may easily change it to any table you like.
|
||||
*/
|
||||
|
||||
'role_has_permissions' => 'role_has_permissions',
|
||||
],
|
||||
|
||||
'column_names' => [
|
||||
/*
|
||||
* Change this if you want to name the related pivots other than defaults
|
||||
*/
|
||||
'role_pivot_key' => null, // default 'role_id',
|
||||
'permission_pivot_key' => null, // default 'permission_id',
|
||||
|
||||
/*
|
||||
* Change this if you want to name the related model primary key other than
|
||||
* `model_id`.
|
||||
*
|
||||
* For example, this would be nice if your primary keys are all UUIDs. In
|
||||
* that case, name this `model_uuid`.
|
||||
*/
|
||||
|
||||
'model_morph_key' => 'model_id',
|
||||
|
||||
/*
|
||||
* Change this if you want to use the teams feature and your related model's
|
||||
* foreign key is other than `team_id`.
|
||||
*/
|
||||
|
||||
'team_foreign_key' => 'team_id',
|
||||
],
|
||||
|
||||
/*
|
||||
* When set to true, the method for checking permissions will be registered on the gate.
|
||||
* Set this to false if you want to implement custom logic for checking permissions.
|
||||
*/
|
||||
|
||||
'register_permission_check_method' => true,
|
||||
|
||||
/*
|
||||
* When set to true, Laravel\Octane\Events\OperationTerminated event listener will be registered
|
||||
* this will refresh permissions on every TickTerminated, TaskTerminated and RequestTerminated
|
||||
* NOTE: This should not be needed in most cases, but an Octane/Vapor combination benefited from it.
|
||||
*/
|
||||
'register_octane_reset_listener' => false,
|
||||
|
||||
/*
|
||||
* Events will fire when a role or permission is assigned/unassigned:
|
||||
* \Spatie\Permission\Events\RoleAttached
|
||||
* \Spatie\Permission\Events\RoleDetached
|
||||
* \Spatie\Permission\Events\PermissionAttached
|
||||
* \Spatie\Permission\Events\PermissionDetached
|
||||
*
|
||||
* To enable, set to true, and then create listeners to watch these events.
|
||||
*/
|
||||
'events_enabled' => false,
|
||||
|
||||
/*
|
||||
* Teams Feature.
|
||||
* When set to true the package implements teams using the 'team_foreign_key'.
|
||||
* If you want the migrations to register the 'team_foreign_key', you must
|
||||
* set this to true before doing the migration.
|
||||
* If you already did the migration then you must make a new migration to also
|
||||
* add 'team_foreign_key' to 'roles', 'model_has_roles', and 'model_has_permissions'
|
||||
* (view the latest version of this package's migration file)
|
||||
*/
|
||||
|
||||
'teams' => false,
|
||||
|
||||
/*
|
||||
* The class to use to resolve the permissions team id
|
||||
*/
|
||||
'team_resolver' => \Spatie\Permission\DefaultTeamResolver::class,
|
||||
|
||||
/*
|
||||
* Passport Client Credentials Grant
|
||||
* When set to true the package will use Passports Client to check permissions
|
||||
*/
|
||||
|
||||
'use_passport_client_credentials' => false,
|
||||
|
||||
/*
|
||||
* When set to true, the required permission names are added to exception messages.
|
||||
* This could be considered an information leak in some contexts, so the default
|
||||
* setting is false here for optimum safety.
|
||||
*/
|
||||
|
||||
'display_permission_in_exception' => false,
|
||||
|
||||
/*
|
||||
* When set to true, the required role names are added to exception messages.
|
||||
* This could be considered an information leak in some contexts, so the default
|
||||
* setting is false here for optimum safety.
|
||||
*/
|
||||
|
||||
'display_role_in_exception' => false,
|
||||
|
||||
/*
|
||||
* By default wildcard permission lookups are disabled.
|
||||
* See documentation to understand supported syntax.
|
||||
*/
|
||||
|
||||
'enable_wildcard_permission' => false,
|
||||
|
||||
/*
|
||||
* The class to use for interpreting wildcard permissions.
|
||||
* If you need to modify delimiters, override the class and specify its name here.
|
||||
*/
|
||||
// 'wildcard_permission' => Spatie\Permission\WildcardPermission::class,
|
||||
|
||||
/* Cache-specific settings */
|
||||
|
||||
'cache' => [
|
||||
|
||||
/*
|
||||
* By default all permissions are cached for 24 hours to speed up performance.
|
||||
* When permissions or roles are updated the cache is flushed automatically.
|
||||
*/
|
||||
|
||||
'expiration_time' => \DateInterval::createFromDateString('24 hours'),
|
||||
|
||||
/*
|
||||
* The cache key used to store all permissions.
|
||||
*/
|
||||
|
||||
'key' => 'spatie.permission.cache',
|
||||
|
||||
/*
|
||||
* You may optionally indicate a specific cache driver to use for permission and
|
||||
* role caching using any of the `store` drivers listed in the cache.php config
|
||||
* file. Using 'default' here means to use the `default` set in cache.php.
|
||||
*/
|
||||
|
||||
'store' => 'default',
|
||||
],
|
||||
];
|
||||
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('entries', function (Blueprint $table) {
|
||||
$table->id();
|
||||
|
||||
$table->string('entry_no')->unique(); // PAY-2024-001
|
||||
$table->string('description');
|
||||
$table->string('region');
|
||||
|
||||
$table->unsignedInteger('order_quantity')->default(0); // selected consolidated order count
|
||||
|
||||
$table->decimal('amount', 12, 2);
|
||||
$table->decimal('pending_amount', 12, 2); // always <= amount
|
||||
|
||||
$table->date('entry_date'); // auto-today default by controller
|
||||
|
||||
// Toggle-based payment states
|
||||
$table->enum('payment_status', ['unpaid', 'pending', 'paid'])->default('unpaid');
|
||||
$table->tinyInteger('toggle_pos')->default(0); // 0 left, 1 middle, 2 right
|
||||
|
||||
// Dispatch state (for second table)
|
||||
$table->enum('dispatch_status', [
|
||||
'pending',
|
||||
'loading',
|
||||
'packed',
|
||||
'dispatched',
|
||||
'delivered'
|
||||
])->default('pending');
|
||||
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('entries');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('installments', function (Blueprint $table) {
|
||||
$table->id();
|
||||
|
||||
$table->foreignId('entry_id')
|
||||
->constrained('entries')
|
||||
->onDelete('cascade');
|
||||
|
||||
$table->date('proc_date'); // processing date
|
||||
$table->decimal('amount', 12, 2);
|
||||
|
||||
$table->string('description')->nullable();
|
||||
$table->string('region')->nullable();
|
||||
|
||||
$table->enum('status', [
|
||||
'Pending',
|
||||
'Loading',
|
||||
'Packed',
|
||||
'Dispatched',
|
||||
'Delivered'
|
||||
])->default('Pending');
|
||||
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('installments');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('entry_order', function (Blueprint $table) {
|
||||
$table->id();
|
||||
|
||||
$table->foreignId('entry_id')
|
||||
->constrained('entries')
|
||||
->onDelete('cascade');
|
||||
|
||||
$table->foreignId('order_id')
|
||||
->constrained('orders')
|
||||
->onDelete('cascade');
|
||||
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('entry_order');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,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');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class CreateShipmentsTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::create('shipments', function (Blueprint $table) {
|
||||
$table->bigIncrements('id');
|
||||
|
||||
// Human-friendly auto-generated shipment id (e.g. SHIP-25-00000001)
|
||||
$table->string('shipment_id')->unique();
|
||||
|
||||
// Basic details
|
||||
$table->string('origin')->nullable();
|
||||
$table->string('destination')->nullable();
|
||||
|
||||
// Totals (calculated when creating shipment)
|
||||
$table->integer('total_ctn')->default(0)->comment('sum of CTN of selected orders');
|
||||
$table->integer('total_qty')->default(0)->comment('sum of qty of selected orders');
|
||||
$table->integer('total_ttl_qty')->default(0)->comment('sum of ttl_qty of selected orders');
|
||||
$table->decimal('total_amount', 16, 2)->default(0.00)->comment('sum of ttl_amount of selected orders');
|
||||
$table->decimal('total_cbm', 14, 3)->default(0.000)->comment('sum cbm');
|
||||
$table->decimal('total_ttl_cbm', 14, 3)->default(0.000)->comment('sum ttl cbm');
|
||||
$table->decimal('total_kg', 14, 3)->default(0.000)->comment('sum kg');
|
||||
$table->decimal('total_ttl_kg', 14, 3)->default(0.000)->comment('sum ttl kg');
|
||||
|
||||
// status: pending (default), in_transit, dispatched, delivered, cancelled, etc.
|
||||
$table->string('status')->default('pending');
|
||||
|
||||
// shipment date (admin can change)
|
||||
$table->date('shipment_date')->nullable();
|
||||
|
||||
// optional meta (vehicle, driver etc)
|
||||
$table->json('meta')->nullable();
|
||||
|
||||
$table->timestamps();
|
||||
|
||||
// Indexes for fast filtering
|
||||
$table->index('shipment_id');
|
||||
$table->index('status');
|
||||
$table->index('shipment_date');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('shipments');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class CreateShipmentItemsTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::create('shipment_items', function (Blueprint $table) {
|
||||
$table->bigIncrements('id');
|
||||
|
||||
// Link to shipments
|
||||
$table->foreignId('shipment_id')->constrained('shipments')->onDelete('cascade');
|
||||
|
||||
// Link to orders. assuming orders.id is bigIncrements
|
||||
$table->foreignId('order_id')->constrained('orders')->onDelete('restrict');
|
||||
|
||||
// Snapshots (optional) — store basic order totals at time of assignment
|
||||
$table->integer('order_ctn')->nullable()->default(0);
|
||||
$table->integer('order_qty')->nullable()->default(0);
|
||||
$table->integer('order_ttl_qty')->nullable()->default(0);
|
||||
$table->decimal('order_ttl_amount', 16, 2)->nullable()->default(0.00);
|
||||
$table->decimal('order_ttl_kg', 14, 3)->nullable()->default(0.000);
|
||||
|
||||
$table->timestamps();
|
||||
|
||||
// Prevent duplicate assignment of same order to the same shipment
|
||||
$table->unique(['shipment_id', 'order_id']);
|
||||
|
||||
// We will check order_id uniqueness across shipments in app logic (see below)
|
||||
$table->index('order_id');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('shipment_items');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class CreateInvoicesTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('invoices', function (Blueprint $table) {
|
||||
$table->id();
|
||||
|
||||
// Links
|
||||
$table->unsignedBigInteger('order_id')->index();
|
||||
$table->unsignedBigInteger('customer_id')->nullable()->index(); // snapshot link if available
|
||||
$table->string('mark_no')->nullable()->index();
|
||||
|
||||
// Invoice identity
|
||||
$table->string('invoice_number')->unique();
|
||||
$table->date('invoice_date')->nullable();
|
||||
$table->date('due_date')->nullable();
|
||||
|
||||
// Payment / status
|
||||
$table->string('payment_method')->nullable();
|
||||
$table->string('reference_no')->nullable();
|
||||
$table->enum('status', ['pending','paid','overdue'])->default('pending');
|
||||
|
||||
// Amounts
|
||||
$table->decimal('final_amount', 14, 2)->default(0.00); // editable by user
|
||||
$table->decimal('gst_percent', 5, 2)->default(0.00); // editable by user
|
||||
$table->decimal('gst_amount', 14, 2)->default(0.00); // auto-calculated
|
||||
$table->decimal('final_amount_with_gst', 14, 2)->default(0.00); // auto-calculated
|
||||
|
||||
// Customer snapshot (immutable fields)
|
||||
$table->string('customer_name')->nullable();
|
||||
$table->string('company_name')->nullable();
|
||||
$table->string('customer_email')->nullable();
|
||||
$table->string('customer_mobile')->nullable();
|
||||
$table->text('customer_address')->nullable();
|
||||
$table->string('pincode')->nullable();
|
||||
|
||||
// PDF / notes
|
||||
$table->string('pdf_path')->nullable();
|
||||
$table->text('notes')->nullable();
|
||||
|
||||
$table->timestamps();
|
||||
|
||||
// Foreign keys (optional — adjust table names/namespaces if yours are different)
|
||||
$table->foreign('order_id')->references('id')->on('orders')->onDelete('cascade');
|
||||
// customer_id may reference users table, keep nullable to avoid migration order issues
|
||||
$table->foreign('customer_id')->references('id')->on('users')->onDelete('set null');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('invoices', function (Blueprint $table) {
|
||||
$table->dropForeign(['order_id']);
|
||||
$table->dropForeign(['customer_id']);
|
||||
});
|
||||
Schema::dropIfExists('invoices');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,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');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up()
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
|
||||
// status: active / inactive
|
||||
$table->enum('status', ['active', 'inactive'])->default('active')->after('pincode');
|
||||
|
||||
// premium / regular
|
||||
$table->enum('customer_type', ['regular', 'premium'])->default('regular')->after('status');
|
||||
|
||||
// optional: profile image path
|
||||
$table->string('profile_image')->nullable()->after('customer_type');
|
||||
});
|
||||
}
|
||||
|
||||
public function down()
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->dropColumn(['status', 'customer_type', 'profile_image']);
|
||||
});
|
||||
}
|
||||
|
||||
};
|
||||
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('invoices', function (Blueprint $table) {
|
||||
|
||||
// GST type — gst or igst
|
||||
$table->enum('tax_type', ['gst', 'igst'])
|
||||
->default('gst')
|
||||
->after('final_amount');
|
||||
|
||||
// Old gst_percent becomes optional
|
||||
$table->decimal('gst_percent', 5, 2)
|
||||
->nullable()
|
||||
->change();
|
||||
|
||||
// Split GST %
|
||||
$table->decimal('cgst_percent', 5, 2)
|
||||
->nullable()
|
||||
->after('gst_percent');
|
||||
|
||||
$table->decimal('sgst_percent', 5, 2)
|
||||
->nullable()
|
||||
->after('cgst_percent');
|
||||
|
||||
// IGST %
|
||||
$table->decimal('igst_percent', 5, 2)
|
||||
->nullable()
|
||||
->after('sgst_percent');
|
||||
|
||||
// Tax amount recalculation is the same
|
||||
// gst_amount and final_amount_with_gst already exist
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('invoices', function (Blueprint $table) {
|
||||
$table->dropColumn([
|
||||
'tax_type',
|
||||
'cgst_percent',
|
||||
'sgst_percent',
|
||||
'igst_percent',
|
||||
]);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
// Table already exists. Add updates here if needed.
|
||||
Schema::table('invoice_installments', function (Blueprint $table) {
|
||||
// nothing to update
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('invoice_installments', function (Blueprint $table) {
|
||||
// nothing to rollback
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('order_items', function (Blueprint $table) {
|
||||
$table->softDeletes();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('order_items', function (Blueprint $table) {
|
||||
$table->dropSoftDeletes();
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('order_items', function (Blueprint $table) {
|
||||
if (! Schema::hasColumn('order_items', 'deleted_at')) {
|
||||
$table->softDeletes(); // adds deleted_at (nullable timestamp)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('order_items', function (Blueprint $table) {
|
||||
if (Schema::hasColumn('order_items', 'deleted_at')) {
|
||||
$table->dropSoftDeletes(); // drops deleted_at
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::table('orders', function (Blueprint $table) {
|
||||
$table->softDeletes(); // creates deleted_at column
|
||||
});
|
||||
}
|
||||
|
||||
public function down()
|
||||
{
|
||||
Schema::table('orders', function (Blueprint $table) {
|
||||
$table->dropColumn('deleted_at');
|
||||
});
|
||||
}
|
||||
|
||||
};
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class CreateUpdateRequestsTable extends Migration
|
||||
{
|
||||
public function up()
|
||||
{
|
||||
Schema::create('update_requests', function (Blueprint $table) {
|
||||
$table->id();
|
||||
|
||||
// The user who is requesting profile update
|
||||
$table->unsignedBigInteger('user_id');
|
||||
|
||||
// JSON data of the requested profile changes
|
||||
$table->json('data')->nullable();
|
||||
|
||||
// pending / approved / rejected
|
||||
$table->enum('status', ['pending', 'approved', 'rejected'])->default('pending');
|
||||
|
||||
// Optional message (admin notes)
|
||||
$table->text('admin_note')->nullable();
|
||||
|
||||
$table->timestamps();
|
||||
|
||||
// Foreign key constraint
|
||||
$table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
|
||||
});
|
||||
}
|
||||
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('update_requests');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('shipment_items', function (Blueprint $table) {
|
||||
|
||||
if (!Schema::hasColumn('shipment_items', 'ctn')) {
|
||||
$table->integer('ctn')->default(0);
|
||||
}
|
||||
|
||||
if (!Schema::hasColumn('shipment_items', 'qty')) {
|
||||
$table->integer('qty')->default(0);
|
||||
}
|
||||
|
||||
if (!Schema::hasColumn('shipment_items', 'ttl_qty')) {
|
||||
$table->integer('ttl_qty')->default(0);
|
||||
}
|
||||
|
||||
if (!Schema::hasColumn('shipment_items', 'cbm')) {
|
||||
$table->double('cbm')->default(0);
|
||||
}
|
||||
|
||||
if (!Schema::hasColumn('shipment_items', 'ttl_cbm')) {
|
||||
$table->double('ttl_cbm')->default(0);
|
||||
}
|
||||
|
||||
if (!Schema::hasColumn('shipment_items', 'kg')) {
|
||||
$table->double('kg')->default(0);
|
||||
}
|
||||
|
||||
if (!Schema::hasColumn('shipment_items', 'ttl_kg')) {
|
||||
$table->double('ttl_kg')->default(0);
|
||||
}
|
||||
|
||||
if (!Schema::hasColumn('shipment_items', 'ttl_amount')) {
|
||||
$table->double('ttl_amount')->default(0);
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('shipment_items', function (Blueprint $table) {
|
||||
// safely remove columns (optional)
|
||||
$columns = [
|
||||
'ctn', 'qty', 'ttl_qty', 'cbm',
|
||||
'ttl_cbm', 'kg', 'ttl_kg', 'ttl_amount'
|
||||
];
|
||||
|
||||
foreach ($columns as $col) {
|
||||
if (Schema::hasColumn('shipment_items', $col)) {
|
||||
$table->dropColumn($col);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,134 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
$teams = config('permission.teams');
|
||||
$tableNames = config('permission.table_names');
|
||||
$columnNames = config('permission.column_names');
|
||||
$pivotRole = $columnNames['role_pivot_key'] ?? 'role_id';
|
||||
$pivotPermission = $columnNames['permission_pivot_key'] ?? 'permission_id';
|
||||
|
||||
throw_if(empty($tableNames), Exception::class, 'Error: config/permission.php not loaded. Run [php artisan config:clear] and try again.');
|
||||
throw_if($teams && empty($columnNames['team_foreign_key'] ?? null), Exception::class, 'Error: team_foreign_key on config/permission.php not loaded. Run [php artisan config:clear] and try again.');
|
||||
|
||||
Schema::create($tableNames['permissions'], static function (Blueprint $table) {
|
||||
// $table->engine('InnoDB');
|
||||
$table->bigIncrements('id'); // permission id
|
||||
$table->string('name'); // For MyISAM use string('name', 225); // (or 166 for InnoDB with Redundant/Compact row format)
|
||||
$table->string('guard_name'); // For MyISAM use string('guard_name', 25);
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique(['name', 'guard_name']);
|
||||
});
|
||||
|
||||
Schema::create($tableNames['roles'], static function (Blueprint $table) use ($teams, $columnNames) {
|
||||
// $table->engine('InnoDB');
|
||||
$table->bigIncrements('id'); // role id
|
||||
if ($teams || config('permission.testing')) { // permission.testing is a fix for sqlite testing
|
||||
$table->unsignedBigInteger($columnNames['team_foreign_key'])->nullable();
|
||||
$table->index($columnNames['team_foreign_key'], 'roles_team_foreign_key_index');
|
||||
}
|
||||
$table->string('name'); // For MyISAM use string('name', 225); // (or 166 for InnoDB with Redundant/Compact row format)
|
||||
$table->string('guard_name'); // For MyISAM use string('guard_name', 25);
|
||||
$table->timestamps();
|
||||
if ($teams || config('permission.testing')) {
|
||||
$table->unique([$columnNames['team_foreign_key'], 'name', 'guard_name']);
|
||||
} else {
|
||||
$table->unique(['name', 'guard_name']);
|
||||
}
|
||||
});
|
||||
|
||||
Schema::create($tableNames['model_has_permissions'], static function (Blueprint $table) use ($tableNames, $columnNames, $pivotPermission, $teams) {
|
||||
$table->unsignedBigInteger($pivotPermission);
|
||||
|
||||
$table->string('model_type');
|
||||
$table->unsignedBigInteger($columnNames['model_morph_key']);
|
||||
$table->index([$columnNames['model_morph_key'], 'model_type'], 'model_has_permissions_model_id_model_type_index');
|
||||
|
||||
$table->foreign($pivotPermission)
|
||||
->references('id') // permission id
|
||||
->on($tableNames['permissions'])
|
||||
->onDelete('cascade');
|
||||
if ($teams) {
|
||||
$table->unsignedBigInteger($columnNames['team_foreign_key']);
|
||||
$table->index($columnNames['team_foreign_key'], 'model_has_permissions_team_foreign_key_index');
|
||||
|
||||
$table->primary([$columnNames['team_foreign_key'], $pivotPermission, $columnNames['model_morph_key'], 'model_type'],
|
||||
'model_has_permissions_permission_model_type_primary');
|
||||
} else {
|
||||
$table->primary([$pivotPermission, $columnNames['model_morph_key'], 'model_type'],
|
||||
'model_has_permissions_permission_model_type_primary');
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
Schema::create($tableNames['model_has_roles'], static function (Blueprint $table) use ($tableNames, $columnNames, $pivotRole, $teams) {
|
||||
$table->unsignedBigInteger($pivotRole);
|
||||
|
||||
$table->string('model_type');
|
||||
$table->unsignedBigInteger($columnNames['model_morph_key']);
|
||||
$table->index([$columnNames['model_morph_key'], 'model_type'], 'model_has_roles_model_id_model_type_index');
|
||||
|
||||
$table->foreign($pivotRole)
|
||||
->references('id') // role id
|
||||
->on($tableNames['roles'])
|
||||
->onDelete('cascade');
|
||||
if ($teams) {
|
||||
$table->unsignedBigInteger($columnNames['team_foreign_key']);
|
||||
$table->index($columnNames['team_foreign_key'], 'model_has_roles_team_foreign_key_index');
|
||||
|
||||
$table->primary([$columnNames['team_foreign_key'], $pivotRole, $columnNames['model_morph_key'], 'model_type'],
|
||||
'model_has_roles_role_model_type_primary');
|
||||
} else {
|
||||
$table->primary([$pivotRole, $columnNames['model_morph_key'], 'model_type'],
|
||||
'model_has_roles_role_model_type_primary');
|
||||
}
|
||||
});
|
||||
|
||||
Schema::create($tableNames['role_has_permissions'], static function (Blueprint $table) use ($tableNames, $pivotRole, $pivotPermission) {
|
||||
$table->unsignedBigInteger($pivotPermission);
|
||||
$table->unsignedBigInteger($pivotRole);
|
||||
|
||||
$table->foreign($pivotPermission)
|
||||
->references('id') // permission id
|
||||
->on($tableNames['permissions'])
|
||||
->onDelete('cascade');
|
||||
|
||||
$table->foreign($pivotRole)
|
||||
->references('id') // role id
|
||||
->on($tableNames['roles'])
|
||||
->onDelete('cascade');
|
||||
|
||||
$table->primary([$pivotPermission, $pivotRole], 'role_has_permissions_permission_id_role_id_primary');
|
||||
});
|
||||
|
||||
app('cache')
|
||||
->store(config('permission.cache.store') != 'default' ? config('permission.cache.store') : null)
|
||||
->forget(config('permission.cache.key'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
$tableNames = config('permission.table_names');
|
||||
|
||||
throw_if(empty($tableNames), Exception::class, 'Error: config/permission.php not found and defaults could not be merged. Please publish the package configuration before proceeding, or drop the tables manually.');
|
||||
|
||||
Schema::drop($tableNames['role_has_permissions']);
|
||||
Schema::drop($tableNames['model_has_roles']);
|
||||
Schema::drop($tableNames['model_has_permissions']);
|
||||
Schema::drop($tableNames['roles']);
|
||||
Schema::drop($tableNames['permissions']);
|
||||
}
|
||||
};
|
||||
50
database/migrations/2025_12_04_071300_create_staff_table.php
Normal file
50
database/migrations/2025_12_04_071300_create_staff_table.php
Normal file
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::create('staff', function (Blueprint $table) {
|
||||
$table->id();
|
||||
|
||||
// Personal Information
|
||||
$table->string('employee_id')->unique();
|
||||
$table->string('name');
|
||||
$table->string('email')->unique();
|
||||
$table->string('phone');
|
||||
$table->string('emergency_phone')->nullable();
|
||||
$table->text('address')->nullable();
|
||||
|
||||
// Professional Information
|
||||
$table->string('role')->nullable(); // Job title
|
||||
$table->string('department')->nullable();
|
||||
$table->string('designation')->nullable();
|
||||
$table->date('joining_date')->nullable();
|
||||
$table->string('status')->default('active'); // active/inactive
|
||||
$table->text('additional_info')->nullable();
|
||||
|
||||
// System Access
|
||||
$table->string('username')->unique();
|
||||
$table->string('password');
|
||||
|
||||
$table->softDeletes();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('staff');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('admins', function (Blueprint $table) {
|
||||
$table->string('employee_id')->unique()->nullable();
|
||||
$table->string('phone')->nullable();
|
||||
$table->string('emergency_phone')->nullable();
|
||||
$table->text('address')->nullable();
|
||||
|
||||
$table->string('department')->nullable();
|
||||
$table->string('designation')->nullable();
|
||||
$table->date('joining_date')->nullable();
|
||||
$table->enum('status', ['active','inactive'])->default('active');
|
||||
$table->text('additional_info')->nullable();
|
||||
|
||||
$table->string('username')->unique()->nullable();
|
||||
$table->enum('type', ['admin','staff'])->default('staff');
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('admins', function (Blueprint $table) {
|
||||
//
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration {
|
||||
public function up()
|
||||
{
|
||||
Schema::table('admins', function (Blueprint $table) {
|
||||
$table->string('role')->nullable()->change(); // <-- Fix problem
|
||||
});
|
||||
}
|
||||
|
||||
public function down()
|
||||
{
|
||||
Schema::table('admins', function (Blueprint $table) {
|
||||
$table->enum('role', ['admin', 'super-admin'])->nullable()->change();
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('support_tickets', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('user_id'); // user who owns the chat
|
||||
$table->string('status')->default('open'); // open / closed
|
||||
$table->timestamps();
|
||||
|
||||
// foreign key constraint (optional but recommended)
|
||||
$table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('support_tickets');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,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');
|
||||
}
|
||||
};
|
||||
103
database/seeders/PermissionSeeder.php
Normal file
103
database/seeders/PermissionSeeder.php
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -18,3 +18,4 @@ require __DIR__.'/../vendor/autoload.php';
|
||||
$app = require_once __DIR__.'/../bootstrap/app.php';
|
||||
|
||||
$app->handleRequest(Request::capture());
|
||||
|
||||
|
||||
BIN
public/invoices/invoice-INV-2025-000001.pdf
Normal file
BIN
public/invoices/invoice-INV-2025-000001.pdf
Normal file
Binary file not shown.
BIN
public/invoices/invoice-INV-2025-000002.pdf
Normal file
BIN
public/invoices/invoice-INV-2025-000002.pdf
Normal file
Binary file not shown.
BIN
public/invoices/invoice-INV-2025-000003.pdf
Normal file
BIN
public/invoices/invoice-INV-2025-000003.pdf
Normal file
Binary file not shown.
BIN
public/invoices/invoice-INV-2025-000004.pdf
Normal file
BIN
public/invoices/invoice-INV-2025-000004.pdf
Normal file
Binary file not shown.
BIN
public/invoices/invoice-INV-2025-000005.pdf
Normal file
BIN
public/invoices/invoice-INV-2025-000005.pdf
Normal file
Binary file not shown.
BIN
public/invoices/invoice-INV-2025-000006.pdf
Normal file
BIN
public/invoices/invoice-INV-2025-000006.pdf
Normal file
Binary file not shown.
BIN
public/invoices/invoice-INV-2025-000007.pdf
Normal file
BIN
public/invoices/invoice-INV-2025-000007.pdf
Normal file
Binary file not shown.
BIN
public/invoices/invoice-INV-2025-000008.pdf
Normal file
BIN
public/invoices/invoice-INV-2025-000008.pdf
Normal file
Binary file not shown.
BIN
public/invoices/invoice-INV-2025-000009.pdf
Normal file
BIN
public/invoices/invoice-INV-2025-000009.pdf
Normal file
Binary file not shown.
BIN
public/invoices/invoice-INV-2025-000010.pdf
Normal file
BIN
public/invoices/invoice-INV-2025-000010.pdf
Normal file
Binary file not shown.
BIN
public/invoices/invoice-INV-2025-000011.pdf
Normal file
BIN
public/invoices/invoice-INV-2025-000011.pdf
Normal file
Binary file not shown.
BIN
public/invoices/invoice-INV-2025-000012.pdf
Normal file
BIN
public/invoices/invoice-INV-2025-000012.pdf
Normal file
Binary file not shown.
BIN
public/invoices/invoice-INV-2025-000013.pdf
Normal file
BIN
public/invoices/invoice-INV-2025-000013.pdf
Normal file
Binary file not shown.
BIN
public/invoices/invoice-INV-2025-000014.pdf
Normal file
BIN
public/invoices/invoice-INV-2025-000014.pdf
Normal file
Binary file not shown.
BIN
public/invoices/invoice-INV-2025-000017.pdf
Normal file
BIN
public/invoices/invoice-INV-2025-000017.pdf
Normal file
Binary file not shown.
BIN
public/invoices/invoice-INV-2025-000019.pdf
Normal file
BIN
public/invoices/invoice-INV-2025-000019.pdf
Normal file
Binary file not shown.
BIN
public/invoices/invoice-INV-2025-000020.pdf
Normal file
BIN
public/invoices/invoice-INV-2025-000020.pdf
Normal file
Binary file not shown.
BIN
public/invoices/invoice-INV-2025-000022.pdf
Normal file
BIN
public/invoices/invoice-INV-2025-000022.pdf
Normal file
Binary file not shown.
BIN
public/invoices/invoice-INV-2025-000023.pdf
Normal file
BIN
public/invoices/invoice-INV-2025-000023.pdf
Normal file
Binary file not shown.
BIN
public/invoices/invoice-INV-2025-000024.pdf
Normal file
BIN
public/invoices/invoice-INV-2025-000024.pdf
Normal file
Binary file not shown.
BIN
public/invoices/invoice-INV-2025-000025.pdf
Normal file
BIN
public/invoices/invoice-INV-2025-000025.pdf
Normal file
Binary file not shown.
BIN
public/invoices/invoice-INV-2025-000026.pdf
Normal file
BIN
public/invoices/invoice-INV-2025-000026.pdf
Normal file
Binary file not shown.
BIN
public/invoices/invoice-INV-2025-000028.pdf
Normal file
BIN
public/invoices/invoice-INV-2025-000028.pdf
Normal file
Binary file not shown.
BIN
public/invoices/invoice-INV-2025-000029.pdf
Normal file
BIN
public/invoices/invoice-INV-2025-000029.pdf
Normal file
Binary file not shown.
BIN
public/invoices/invoice-INV-2025-000030.pdf
Normal file
BIN
public/invoices/invoice-INV-2025-000030.pdf
Normal file
Binary file not shown.
BIN
public/invoices/invoice-INV-2025-000032.pdf
Normal file
BIN
public/invoices/invoice-INV-2025-000032.pdf
Normal file
Binary file not shown.
BIN
public/invoices/invoice-INV-2025-000033.pdf
Normal file
BIN
public/invoices/invoice-INV-2025-000033.pdf
Normal file
Binary file not shown.
BIN
public/profile_upload/profile_1764645094.jpg
Normal file
BIN
public/profile_upload/profile_1764645094.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 48 KiB |
BIN
public/profile_upload/profile_1764743106.jpg
Normal file
BIN
public/profile_upload/profile_1764743106.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 209 KiB |
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
542
resources/views/admin/customers_add.blade.php
Normal file
542
resources/views/admin/customers_add.blade.php
Normal 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
|
||||
784
resources/views/admin/customers_view.blade.php
Normal file
784
resources/views/admin/customers_view.blade.php
Normal 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
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
830
resources/views/admin/invoice_edit.blade.php
Normal file
830
resources/views/admin/invoice_edit.blade.php
Normal file
@@ -0,0 +1,830 @@
|
||||
@extends('admin.layouts.app')
|
||||
|
||||
@section('page-title', 'Edit Invoice')
|
||||
|
||||
@section('content')
|
||||
<style>
|
||||
/* --------------------------------------------------
|
||||
GLOBAL VARIABLES & THEME
|
||||
-------------------------------------------------- */
|
||||
: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%);
|
||||
--danger-gradient: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
|
||||
--glass-bg: rgba(255, 255, 255, 0.95);
|
||||
--glass-border: rgba(255, 255, 255, 0.2);
|
||||
--shadow-soft: 0 8px 32px rgba(0, 0, 0, 0.08);
|
||||
--shadow-medium: 0 12px 48px rgba(0, 0, 0, 0.12);
|
||||
--shadow-strong: 0 20px 60px rgba(0, 0, 0, 0.15);
|
||||
--border-radius: 12px;
|
||||
}
|
||||
|
||||
/* --------------------------------------------------
|
||||
GLOBAL STYLES & ANIMATIONS
|
||||
-------------------------------------------------- */
|
||||
body {
|
||||
background: linear-gradient(135deg, #f8fafc 0%, #e2e8f0 100%);
|
||||
font-family: 'Inter', 'Segoe UI', system-ui, sans-serif;
|
||||
}
|
||||
|
||||
@keyframes fadeUp {
|
||||
0% { opacity: 0; transform: translateY(20px) scale(0.98); }
|
||||
100% { opacity: 1; transform: translateY(0) scale(1); }
|
||||
}
|
||||
|
||||
/* --------------------------------------------------
|
||||
COMPACT CARD DESIGN
|
||||
-------------------------------------------------- */
|
||||
.glass-card {
|
||||
background: var(--glass-bg);
|
||||
backdrop-filter: blur(20px);
|
||||
border-radius: var(--border-radius);
|
||||
border: 1px solid var(--glass-border);
|
||||
box-shadow: var(--shadow-strong);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
animation: fadeUp 0.6s ease;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.glass-card::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: linear-gradient(45deg,
|
||||
rgba(102, 126, 234, 0.03),
|
||||
rgba(118, 75, 162, 0.03) 50%,
|
||||
rgba(16, 185, 129, 0.03));
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* --------------------------------------------------
|
||||
COMPACT CARD HEADER
|
||||
-------------------------------------------------- */
|
||||
.card-header-compact {
|
||||
background: var(--primary-gradient);
|
||||
color: #fff;
|
||||
padding: 1rem 1.5rem;
|
||||
border: none;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.card-header-compact h4 {
|
||||
margin: 0;
|
||||
font-weight: 700;
|
||||
font-size: 1.3rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
/* --------------------------------------------------
|
||||
COMPACT CARD BODY
|
||||
-------------------------------------------------- */
|
||||
.card-body-compact {
|
||||
padding: 1.5rem;
|
||||
background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* --------------------------------------------------
|
||||
COMPACT FORM ELEMENTS
|
||||
-------------------------------------------------- */
|
||||
.form-grid-compact {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||
gap: 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.form-group-compact {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.form-label-compact {
|
||||
font-weight: 600;
|
||||
color: #374151;
|
||||
margin-bottom: 8px;
|
||||
font-size: 0.9rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.form-control-compact, .form-select-compact {
|
||||
border: 2px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
padding: 10px 12px;
|
||||
font-size: 0.9rem;
|
||||
transition: all 0.3s ease;
|
||||
background: white;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.form-control-compact:focus, .form-select-compact:focus {
|
||||
border-color: #667eea;
|
||||
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
/* --------------------------------------------------
|
||||
COMPACT BUTTONS
|
||||
-------------------------------------------------- */
|
||||
.btn-compact {
|
||||
padding: 10px 20px;
|
||||
border-radius: 8px;
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
transition: all 0.3s ease;
|
||||
border: none;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.btn-success-compact {
|
||||
background: var(--success-gradient);
|
||||
color: white;
|
||||
box-shadow: 0 4px 12px rgba(16, 185, 129, 0.3);
|
||||
}
|
||||
|
||||
.btn-success-compact:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 20px rgba(16, 185, 129, 0.4);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary-compact {
|
||||
background: var(--primary-gradient);
|
||||
color: white;
|
||||
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
|
||||
}
|
||||
|
||||
.btn-primary-compact:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.4);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-danger-compact {
|
||||
background: var(--danger-gradient);
|
||||
color: white;
|
||||
box-shadow: 0 4px 12px rgba(239, 68, 68, 0.3);
|
||||
}
|
||||
|
||||
.btn-danger-compact:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 20px rgba(239, 68, 68, 0.4);
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* --------------------------------------------------
|
||||
COMPACT SUMMARY CARDS
|
||||
-------------------------------------------------- */
|
||||
.summary-grid-compact {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.summary-card-compact {
|
||||
background: white;
|
||||
padding: 1rem;
|
||||
border-radius: 8px;
|
||||
box-shadow: var(--shadow-soft);
|
||||
border-left: 4px solid;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.summary-card-compact.total { border-left-color: #10b981; }
|
||||
.summary-card-compact.paid { border-left-color: #3b82f6; }
|
||||
.summary-card-compact.remaining { border-left-color: #f59e0b; }
|
||||
|
||||
.summary-value-compact {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.summary-label-compact {
|
||||
font-size: 0.8rem;
|
||||
color: #64748b;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
/* --------------------------------------------------
|
||||
COMPACT AMOUNT BREAKDOWN
|
||||
-------------------------------------------------- */
|
||||
.amount-breakdown-compact {
|
||||
background: white;
|
||||
padding: 1rem;
|
||||
border-radius: 8px;
|
||||
box-shadow: var(--shadow-soft);
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.breakdown-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0.5rem 0;
|
||||
border-bottom: 1px solid #f1f5f9;
|
||||
}
|
||||
|
||||
.breakdown-row:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.breakdown-label {
|
||||
font-weight: 600;
|
||||
color: #374151;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.breakdown-value {
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
/* --------------------------------------------------
|
||||
COMPACT TABLE
|
||||
-------------------------------------------------- */
|
||||
.table-compact {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-bottom: 0;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.table-compact thead th {
|
||||
background: var(--primary-gradient);
|
||||
color: white;
|
||||
padding: 0.75rem 1rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
font-size: 0.8rem;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.table-compact tbody tr {
|
||||
background: white;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.table-compact tbody tr:hover {
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.table-compact tbody td {
|
||||
padding: 0.75rem 1rem;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
/* --------------------------------------------------
|
||||
BADGE STYLES
|
||||
-------------------------------------------------- */
|
||||
.badge-compact {
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 20px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* --------------------------------------------------
|
||||
RESPONSIVE DESIGN
|
||||
-------------------------------------------------- */
|
||||
@media (max-width: 768px) {
|
||||
.glass-card {
|
||||
margin: 0.5rem;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.card-body-compact {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.form-grid-compact {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.summary-grid-compact {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.btn-compact {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.table-compact {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- Main Container -->
|
||||
<div class="container-fluid py-3">
|
||||
|
||||
<!-- Invoice Preview Section -->
|
||||
<div class="glass-card">
|
||||
<div class="card-header-compact">
|
||||
<h4>
|
||||
<i class="fas fa-file-invoice me-2"></i>Invoice Overview
|
||||
</h4>
|
||||
</div>
|
||||
<div class="card-body-compact">
|
||||
@include('admin.popup_invoice', [
|
||||
'invoice' => $invoice,
|
||||
'shipment' => $shipment,
|
||||
'embedded' => true
|
||||
])
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Edit Invoice Form -->
|
||||
<div class="glass-card">
|
||||
<div class="card-header-compact">
|
||||
<h4>
|
||||
<i class="fas fa-edit me-2"></i>Edit Invoice Details
|
||||
</h4>
|
||||
</div>
|
||||
<div class="card-body-compact">
|
||||
<form action="{{ route('admin.invoices.update', $invoice->id) }}" method="POST">
|
||||
@csrf
|
||||
|
||||
<div class="form-grid-compact">
|
||||
<!-- Invoice Date -->
|
||||
<div class="form-group-compact">
|
||||
<label class="form-label-compact">
|
||||
<i class="fas fa-calendar-day"></i>
|
||||
Invoice Date
|
||||
</label>
|
||||
<input type="date" class="form-control-compact" name="invoice_date"
|
||||
value="{{ $invoice->invoice_date }}" required>
|
||||
</div>
|
||||
|
||||
<!-- Due Date -->
|
||||
<div class="form-group-compact">
|
||||
<label class="form-label-compact">
|
||||
<i class="fas fa-clock"></i>
|
||||
Due Date
|
||||
</label>
|
||||
<input type="date" class="form-control-compact" name="due_date"
|
||||
value="{{ $invoice->due_date }}" required>
|
||||
</div>
|
||||
|
||||
<!-- Final Amount -->
|
||||
<div class="form-group-compact">
|
||||
<label class="form-label-compact">
|
||||
<i class="fas fa-money-bill-wave"></i>
|
||||
Final Amount (₹)
|
||||
</label>
|
||||
<input type="number" step="0.01" class="form-control-compact" name="final_amount"
|
||||
value="{{ $invoice->final_amount }}" required>
|
||||
</div>
|
||||
|
||||
<!-- Tax Type -->
|
||||
<div class="form-group-compact">
|
||||
<label class="form-label-compact">
|
||||
<i class="fas fa-receipt"></i>
|
||||
Tax Type
|
||||
</label>
|
||||
<div class="d-flex gap-3 mt-1">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="radio" name="tax_type"
|
||||
value="gst" @checked($invoice->tax_type === 'gst')>
|
||||
<label class="form-check-label fw-semibold">GST</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="radio" name="tax_type"
|
||||
value="igst" @checked($invoice->tax_type === 'igst')>
|
||||
<label class="form-check-label fw-semibold">IGST</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tax Percentage -->
|
||||
<div class="form-group-compact">
|
||||
<label class="form-label-compact">
|
||||
<i class="fas fa-percentage"></i>
|
||||
Tax Percentage (%)
|
||||
</label>
|
||||
<input type="number" step="0.01" min="0" max="28" class="form-control-compact"
|
||||
name="tax_percent"
|
||||
value="{{ old('tax_percent', $invoice->tax_type == 'gst' ? $invoice->cgst_percent + $invoice->sgst_percent : $invoice->igst_percent) }}"
|
||||
required>
|
||||
</div>
|
||||
|
||||
<!-- Status -->
|
||||
<div class="form-group-compact">
|
||||
<label class="form-label-compact">
|
||||
<i class="fas fa-tasks"></i>
|
||||
Status
|
||||
</label>
|
||||
<select class="form-select-compact" name="status" required>
|
||||
<option value="pending" @selected($invoice->status=='pending')>⏳ Pending</option>
|
||||
<option value="paid" @selected($invoice->status=='paid')>✅ Paid</option>
|
||||
<option value="overdue" @selected($invoice->status=='overdue')>⚠️ Overdue</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Notes -->
|
||||
<div class="form-group-compact" style="grid-column: 1 / -1;">
|
||||
<label class="form-label-compact">
|
||||
<i class="fas fa-sticky-note"></i>
|
||||
Notes
|
||||
</label>
|
||||
<textarea class="form-control-compact" rows="3" name="notes"
|
||||
placeholder="Add any additional notes...">{{ $invoice->notes }}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-end mt-3">
|
||||
@can('invoice.edit')
|
||||
<button type="submit" class="btn-success-compact btn-compact">
|
||||
<i class="fas fa-save me-2"></i>Update Invoice
|
||||
</button>
|
||||
@endcan
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@php
|
||||
$totalPaid = $invoice->installments->sum('amount');
|
||||
$remaining = $invoice->final_amount_with_gst - $totalPaid;
|
||||
@endphp
|
||||
|
||||
<!-- Amount Breakdown -->
|
||||
<div class="amount-breakdown-compact">
|
||||
<h6 class="fw-bold mb-3 text-dark">
|
||||
<i class="fas fa-calculator me-2"></i>Amount Breakdown
|
||||
</h6>
|
||||
|
||||
<div class="breakdown-row">
|
||||
<span class="breakdown-label">Total Amount (Before Tax):</span>
|
||||
<span class="breakdown-value">₹{{ number_format($invoice->final_amount, 2) }}</span>
|
||||
</div>
|
||||
|
||||
<div class="breakdown-row">
|
||||
<span class="breakdown-label">Tax Type:</span>
|
||||
<span class="breakdown-value text-primary">
|
||||
@if($invoice->tax_type === 'gst')
|
||||
GST (CGST + SGST)
|
||||
@else
|
||||
IGST
|
||||
@endif
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="breakdown-row">
|
||||
<span class="breakdown-label">Tax Percentage:</span>
|
||||
<span class="breakdown-value text-primary">
|
||||
@if($invoice->tax_type === 'gst')
|
||||
{{ $invoice->cgst_percent + $invoice->sgst_percent }}%
|
||||
@else
|
||||
{{ $invoice->igst_percent }}%
|
||||
@endif
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="breakdown-row">
|
||||
<span class="breakdown-label">GST Amount:</span>
|
||||
<span class="breakdown-value text-warning">₹{{ number_format($invoice->gst_amount, 2) }}</span>
|
||||
</div>
|
||||
|
||||
<div class="breakdown-row" style="border-top: 2px solid #e2e8f0; padding-top: 0.75rem;">
|
||||
<span class="breakdown-label fw-bold">Total Invoice Amount (Including GST):</span>
|
||||
<span class="breakdown-value fw-bold text-dark">₹{{ number_format($invoice->final_amount_with_gst, 2) }}</span>
|
||||
</div>
|
||||
|
||||
<div class="breakdown-row">
|
||||
<span class="breakdown-label text-success">Total Paid:</span>
|
||||
<span class="breakdown-value fw-bold text-success" id="paidAmount">₹{{ number_format($totalPaid, 2) }}</span>
|
||||
</div>
|
||||
|
||||
<div class="breakdown-row" style="border-bottom: none;">
|
||||
<span class="breakdown-label text-danger">Remaining:</span>
|
||||
<span class="breakdown-value fw-bold text-danger" id="remainingAmount">₹{{ number_format($remaining, 2) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Installment Summary -->
|
||||
<div class="summary-grid-compact">
|
||||
<div class="summary-card-compact total">
|
||||
<div class="summary-value-compact text-success">₹{{ number_format($invoice->final_amount_with_gst, 2) }}</div>
|
||||
<div class="summary-label-compact">Total Amount</div>
|
||||
</div>
|
||||
<div class="summary-card-compact paid">
|
||||
<div class="summary-value-compact text-primary">₹{{ number_format($totalPaid, 2) }}</div>
|
||||
<div class="summary-label-compact">Total Paid</div>
|
||||
</div>
|
||||
<div class="summary-card-compact remaining">
|
||||
<div class="summary-value-compact text-warning">₹{{ number_format($remaining, 2) }}</div>
|
||||
<div class="summary-label-compact">Remaining</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Installment Management -->
|
||||
<div class="glass-card">
|
||||
<div class="card-header-compact d-flex justify-content-between align-items-center">
|
||||
<h4>
|
||||
<i class="fas fa-credit-card me-2"></i>Installment Payments
|
||||
</h4>
|
||||
@can('invoice.add_installment')
|
||||
@if($remaining > 0)
|
||||
<button id="toggleInstallmentForm" class="btn-primary-compact btn-compact">
|
||||
<i class="fas fa-plus-circle me-2"></i>Add Installment
|
||||
</button>
|
||||
@endif
|
||||
@endcan
|
||||
</div>
|
||||
|
||||
<div class="card-body-compact">
|
||||
<!-- Installment Form -->
|
||||
<div id="installmentForm" class="d-none mb-3">
|
||||
<div class="glass-card" style="background: rgba(248, 250, 252, 0.8);">
|
||||
<div class="card-header-compact" style="background: var(--success-gradient);">
|
||||
<h4>
|
||||
<i class="fas fa-plus-circle me-2"></i>Add New Installment
|
||||
</h4>
|
||||
</div>
|
||||
<div class="card-body-compact">
|
||||
<form id="installmentSubmitForm">
|
||||
@csrf
|
||||
<div class="form-grid-compact">
|
||||
<div class="form-group-compact">
|
||||
<label class="form-label-compact">
|
||||
<i class="fas fa-calendar-day"></i>
|
||||
Installment Date
|
||||
</label>
|
||||
<input type="date" name="installment_date" class="form-control-compact" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group-compact">
|
||||
<label class="form-label-compact">
|
||||
<i class="fas fa-credit-card"></i>
|
||||
Payment Method
|
||||
</label>
|
||||
<select name="payment_method" class="form-select-compact" required>
|
||||
<option value="cash">💵 Cash</option>
|
||||
<option value="bank">🏦 Bank Transfer</option>
|
||||
<option value="upi">📱 UPI</option>
|
||||
<option value="cheque">🧾 Cheque</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group-compact">
|
||||
<label class="form-label-compact">
|
||||
<i class="fas fa-hashtag"></i>
|
||||
Reference No
|
||||
</label>
|
||||
<input type="text" name="reference_no" class="form-control-compact"
|
||||
placeholder="Enter reference number">
|
||||
</div>
|
||||
|
||||
<div class="form-group-compact">
|
||||
<label class="form-label-compact">
|
||||
<i class="fas fa-money-bill-wave"></i>
|
||||
Amount (₹)
|
||||
</label>
|
||||
<input type="number" name="amount" id="installmentAmount"
|
||||
class="form-control-compact" step="0.01" min="1"
|
||||
max="{{ $remaining }}" required
|
||||
placeholder="Enter installment amount">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-end mt-2">
|
||||
<button type="submit" class="btn-success-compact btn-compact" id="installmentSubmitBtn">
|
||||
<i class="fas fa-paper-plane me-2"></i>Submit Installment
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Installment History -->
|
||||
<h6 class="fw-bold mb-2 text-dark">
|
||||
<i class="fas fa-history me-2"></i>Installment History
|
||||
</h6>
|
||||
<div class="table-responsive">
|
||||
<table class="table-compact">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>Date</th>
|
||||
<th>Payment Method</th>
|
||||
<th>Reference No</th>
|
||||
<th>Amount</th>
|
||||
<th>Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="installmentTable">
|
||||
@foreach($invoice->installments as $i)
|
||||
<tr data-id="{{ $i->id }}">
|
||||
<td class="fw-bold text-muted">{{ $loop->iteration }}</td>
|
||||
<td>{{ $i->installment_date }}</td>
|
||||
<td>
|
||||
<span class="badge-compact bg-primary bg-opacity-10 text-primary">
|
||||
{{ ucfirst($i->payment_method) }}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
@if($i->reference_no)
|
||||
<span class="text-muted">{{ $i->reference_no }}</span>
|
||||
@else
|
||||
<span class="text-muted">-</span>
|
||||
@endif
|
||||
</td>
|
||||
<td class="fw-bold text-success">₹{{ number_format($i->amount, 2) }}</td>
|
||||
<td>
|
||||
<button class="btn-danger-compact btn-compact btn-sm deleteInstallment">
|
||||
<i class="fas fa-trash me-1"></i>Delete
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Add this just above the table -->
|
||||
<div id="noInstallmentsMsg" class="d-none text-center text-muted fw-bold py-4">
|
||||
No installments found. Click "Add Installment" to create one.
|
||||
</div>
|
||||
<table ...>
|
||||
<tbody id="installmentTable">
|
||||
@foreach($invoice->installments as $i)
|
||||
...
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
// Toggle Installment Form
|
||||
const toggleBtn = document.getElementById("toggleInstallmentForm");
|
||||
const formBox = document.getElementById("installmentForm");
|
||||
|
||||
if (toggleBtn) {
|
||||
toggleBtn.addEventListener("click", () => {
|
||||
formBox.classList.toggle("d-none");
|
||||
});
|
||||
}
|
||||
|
||||
// Add Installment
|
||||
const submitForm = document.getElementById("installmentSubmitForm");
|
||||
const submitBtn = document.getElementById("installmentSubmitBtn");
|
||||
|
||||
const formatINR = amt =>
|
||||
"₹" + Number(amt).toLocaleString("en-IN", {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2
|
||||
});
|
||||
|
||||
submitForm.addEventListener("submit", function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
submitBtn.innerHTML = '<i class="fas fa-spinner fa-spin me-2"></i>Processing...';
|
||||
submitBtn.disabled = true;
|
||||
|
||||
fetch("{{ route('admin.invoice.installment.store', $invoice->id) }}", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"X-CSRF-TOKEN": submitForm.querySelector("input[name=_token]").value,
|
||||
"Accept": "application/json"
|
||||
},
|
||||
body: new FormData(submitForm)
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
submitBtn.innerHTML = '<i class="fas fa-paper-plane me-2"></i>Submit Installment';
|
||||
submitBtn.disabled = false;
|
||||
|
||||
if (data.status === "error") {
|
||||
alert(data.message);
|
||||
return;
|
||||
}
|
||||
|
||||
const table = document.querySelector("#installmentTable");
|
||||
const index = table.rows.length + 1;
|
||||
|
||||
table.insertAdjacentHTML("beforeend", `
|
||||
<tr data-id="${data.installment.id}">
|
||||
<td class="fw-bold text-muted">${index}</td>
|
||||
<td>${data.installment.installment_date}</td>
|
||||
<td>
|
||||
<span class="badge-compact bg-primary bg-opacity-10 text-primary">
|
||||
${data.installment.payment_method.toUpperCase()}
|
||||
</span>
|
||||
</td>
|
||||
<td>${data.installment.reference_no || '-'}</td>
|
||||
<td class="fw-bold text-success">${formatINR(data.installment.amount)}</td>
|
||||
<td>
|
||||
<button class="btn-danger-compact btn-compact btn-sm deleteInstallment">
|
||||
<i class="fas fa-trash me-1"></i>Delete
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
`);
|
||||
|
||||
// Update all displayed values using GST fields!
|
||||
if (document.getElementById("paidAmount")) document.getElementById("paidAmount").textContent = formatINR(data.totalPaid);
|
||||
if (document.getElementById("remainingAmount")) document.getElementById("remainingAmount").textContent = formatINR(data.remaining);
|
||||
if (document.getElementById("baseAmount")) document.getElementById("baseAmount").textContent = formatINR(data.baseAmount);
|
||||
if (document.getElementById("gstAmount")) document.getElementById("gstAmount").textContent = formatINR(data.gstAmount);
|
||||
if (document.getElementById("totalInvoiceWithGst")) document.getElementById("totalInvoiceWithGst").textContent = formatINR(data.finalAmountWithGst);
|
||||
if (document.getElementById("invoiceStatus")) document.getElementById("invoiceStatus").textContent = data.isCompleted ? "Paid" : "Pending";
|
||||
|
||||
// Update summary cards if used
|
||||
const paidCard = document.querySelector(".summary-card-compact.paid .summary-value-compact");
|
||||
if (paidCard) paidCard.textContent = formatINR(data.totalPaid);
|
||||
const remainingCard = document.querySelector(".summary-card-compact.remaining .summary-value-compact");
|
||||
if (remainingCard) remainingCard.textContent = formatINR(data.remaining);
|
||||
|
||||
submitForm.reset();
|
||||
|
||||
// If fully paid, disable/add display logic
|
||||
if (data.isCompleted) {
|
||||
toggleBtn?.remove();
|
||||
formBox.classList.add("d-none");
|
||||
}
|
||||
|
||||
alert(data.message);
|
||||
})
|
||||
.catch(() => {
|
||||
submitBtn.innerHTML = '<i class="fas fa-paper-plane me-2"></i>Submit Installment';
|
||||
submitBtn.disabled = false;
|
||||
alert("Something went wrong. Please try again.");
|
||||
});
|
||||
});
|
||||
|
||||
// Delete Installment
|
||||
document.addEventListener("click", function (e) {
|
||||
if (!e.target.classList.contains("deleteInstallment")) return;
|
||||
|
||||
if (!confirm("Are you sure you want to delete this installment?")) return;
|
||||
|
||||
const row = e.target.closest("tr");
|
||||
const id = row.getAttribute("data-id");
|
||||
|
||||
fetch("{{ url('/admin/installment') }}/" + id, {
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
"X-CSRF-TOKEN": "{{ csrf_token() }}",
|
||||
"Accept": "application/json"
|
||||
}
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if (data.status === "success") {
|
||||
row.style.opacity = "0";
|
||||
setTimeout(() => row.remove(), 300);
|
||||
|
||||
// Update all displayed values using GST fields!
|
||||
if (document.getElementById("paidAmount")) document.getElementById("paidAmount").textContent = formatINR(data.totalPaid);
|
||||
if (document.getElementById("remainingAmount")) document.getElementById("remainingAmount").textContent = formatINR(data.remaining);
|
||||
if (document.getElementById("baseAmount")) document.getElementById("baseAmount").textContent = formatINR(data.baseAmount);
|
||||
if (document.getElementById("gstAmount")) document.getElementById("gstAmount").textContent = formatINR(data.gstAmount);
|
||||
if (document.getElementById("totalInvoiceWithGst")) document.getElementById("totalInvoiceWithGst").textContent = formatINR(data.finalAmountWithGst);
|
||||
if (document.getElementById("invoiceStatus")) document.getElementById("invoiceStatus").textContent = data.remaining === 0 ? "Paid" : "Pending";
|
||||
|
||||
// Update summary cards
|
||||
const paidCard = document.querySelector(".summary-card-compact.paid .summary-value-compact");
|
||||
if (paidCard) paidCard.textContent = formatINR(data.totalPaid);
|
||||
const remainingCard = document.querySelector(".summary-card-compact.remaining .summary-value-compact");
|
||||
if (remainingCard) remainingCard.textContent = formatINR(data.remaining);
|
||||
|
||||
|
||||
|
||||
alert(data.message);
|
||||
} else {
|
||||
alert(data.message || "Something went wrong. Please try again.");
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
alert("Something went wrong. Please try again.");
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@endsection
|
||||
@@ -5,6 +5,10 @@
|
||||
<title>Admin Panel</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet" />
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.css" rel="stylesheet" />
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.5/font/bootstrap-icons.css">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" integrity="sha512-DTOQO9RWCH3ppGqcWaEA1BIZOC6xxalwEsw9c2QQeAIftl+Vegovlnee1c9QX4TctnWMn13TZye+giMm8e2LwA==" crossorigin="anonymous" referrerpolicy="no-referrer" />
|
||||
|
||||
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
@@ -13,11 +17,12 @@
|
||||
font-family: 'Inter', Arial, sans-serif;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
transition: all 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
/* ✨ Sidebar Glass + Animated Highlight Effect */
|
||||
.sidebar {
|
||||
width: 190px;
|
||||
width: 200px;
|
||||
height: 100vh;
|
||||
background: linear-gradient(180deg, rgba(255, 255, 255, 0.9), rgba(232, 240, 255, 0.95)) !important;
|
||||
box-shadow: 2px 0 25px rgba(0, 0, 0, 0.08);
|
||||
@@ -29,6 +34,16 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
transition: all 0.3s ease-in-out;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
/* Sidebar collapsed state */
|
||||
.sidebar.collapsed {
|
||||
transform: translateX(-100%);
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.sidebar .logo {
|
||||
@@ -56,10 +71,10 @@
|
||||
align-items: center;
|
||||
color: #2b5cb6 !important;
|
||||
text-decoration: none;
|
||||
padding: 10px 12px;
|
||||
padding: 11px 13px;
|
||||
font-size: 0.97rem;
|
||||
border-radius: 12px;
|
||||
margin: 6px 6px 0 6px;
|
||||
margin: 10px 10px 0 10px;
|
||||
font-weight: 500;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
@@ -125,6 +140,7 @@
|
||||
margin-top: 12px;
|
||||
font-weight: 500;
|
||||
transition: 0.3s ease;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.sidebar form button:hover {
|
||||
@@ -140,6 +156,40 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: calc(100vw - 190px);
|
||||
margin-left: 190px;
|
||||
transition: all 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
/* Main content when sidebar is collapsed */
|
||||
.main-content.expanded {
|
||||
margin-left: 0;
|
||||
width: 100vw;
|
||||
}
|
||||
|
||||
/* Header hamburger button */
|
||||
.header-toggle {
|
||||
background: transparent;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
padding: 8px;
|
||||
margin-right: 15px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: transform 0.3s ease;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.header-toggle:hover {
|
||||
transform: scale(1.1);
|
||||
background: rgba(43, 92, 182, 0.1);
|
||||
}
|
||||
|
||||
.header-toggle i {
|
||||
font-size: 1.6rem;
|
||||
color: #2b5cb6;
|
||||
}
|
||||
|
||||
header {
|
||||
@@ -154,8 +204,13 @@
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.content-wrapper {
|
||||
padding: 18px 16px 0 16px;
|
||||
padding: 18px 16px 16px 16px;
|
||||
flex-grow: 1;
|
||||
overflow-y: auto;
|
||||
}
|
||||
@@ -173,31 +228,96 @@
|
||||
<div class="word"><strong>KENT</strong><br /><small>International Pvt. Ltd.</small></div>
|
||||
</div>
|
||||
|
||||
<a href="{{ route('admin.dashboard') }}" class="{{ request()->routeIs('admin.dashboard') ? 'active' : '' }}"><i class="bi bi-house"></i> Dashboard</a>
|
||||
<a href="{{ route('admin.shipments') }}" class="{{ request()->routeIs('admin.shipments') ? 'active' : '' }}"><i class="bi bi-truck"></i> Shipments</a>
|
||||
<a href="{{ route('admin.invoice') }}" class="{{ request()->routeIs('admin.invoice') ? 'active' : '' }}"><i class="bi bi-receipt"></i> Invoice</a>
|
||||
<a href="{{ route('admin.customers') }}" class="{{ request()->routeIs('admin.customers') ? 'active' : '' }}"><i class="bi bi-people"></i> Customers</a>
|
||||
<a href="{{ route('admin.reports') }}" class="{{ request()->routeIs('admin.reports') ? 'active' : '' }}"><i class="bi bi-graph-up"></i> Reports</a>
|
||||
<a href="{{ route('admin.chat_support') }}" class="{{ request()->routeIs('admin.chat_support') ? 'active' : '' }}"><i class="bi bi-chat-dots"></i> Chat Support</a>
|
||||
<!-- <a href="{{ route('admin.orders.index') }}"
|
||||
class="{{ request()->routeIs('admin.orders.*') ? 'active' : '' }}">
|
||||
<i class="bi bi-bag"></i> Orders
|
||||
</a> -->
|
||||
<a href="{{ route('admin.orders') }}" class="{{ request()->routeIs('admin.orders') ? 'active' : '' }}"><i class="bi bi-bag"></i> Orders</a>
|
||||
<a href="{{ route('admin.requests') }}" class="{{ request()->routeIs('admin.requests') ? 'active' : '' }}"><i class="bi bi-envelope"></i> Requests</a>
|
||||
<a href="{{ route('admin.staff') }}" class="{{ request()->routeIs('admin.staff') ? 'active' : '' }}"><i class="bi bi-person-badge"></i> Staff</a>
|
||||
<a href="{{ route('admin.account') }}" class="{{ request()->routeIs('admin.account') ? 'active' : '' }}"><i class="bi bi-gear"></i> Account</a>
|
||||
<a href="{{ route('admin.marklist.index') }}" class="{{ request()->routeIs('admin.marklist.index') ? 'active' : '' }}"><i class="bi bi-list-check"></i> Mark List</a>
|
||||
{{-- Dashboard (requires order.view) --}}
|
||||
@can('order.view')
|
||||
<a href="{{ route('admin.dashboard') }}" class="{{ request()->routeIs('admin.dashboard') ? 'active' : '' }}">
|
||||
<i class="bi bi-house"></i> Dashboard
|
||||
</a>
|
||||
@endcan
|
||||
|
||||
<form method="POST" action="{{ route('admin.logout') }}" class="mt-4 px-3">
|
||||
@csrf
|
||||
<button type="submit" class="btn btn-danger w-100"><i class="bi bi-box-arrow-right"></i> Logout</button>
|
||||
</form>
|
||||
{{-- Shipments --}}
|
||||
@can('shipment.view')
|
||||
<a href="{{ route('admin.shipments') }}" class="{{ request()->routeIs('admin.shipments') ? 'active' : '' }}">
|
||||
<i class="bi bi-truck"></i> Shipments
|
||||
</a>
|
||||
@endcan
|
||||
|
||||
{{-- Invoice --}}
|
||||
@can('invoice.view')
|
||||
<a href="{{ route('admin.invoices.index') }}" class="{{ request()->routeIs('admin.invoices.index') ? 'active' : '' }}">
|
||||
<i class="bi bi-receipt"></i> Invoice
|
||||
</a>
|
||||
@endcan
|
||||
|
||||
{{-- Customers --}}
|
||||
@can('customer.view')
|
||||
<a href="{{ route('admin.customers.index') }}" class="{{ request()->routeIs('admin.customers.index') ? 'active' : '' }}">
|
||||
<i class="bi bi-people"></i> Customers
|
||||
</a>
|
||||
@endcan
|
||||
|
||||
{{-- Reports --}}
|
||||
@can('report.view')
|
||||
<a href="{{ route('admin.reports') }}" class="{{ request()->routeIs('admin.reports') ? 'active' : '' }}">
|
||||
<i class="bi bi-graph-up"></i> Reports
|
||||
</a>
|
||||
@endcan
|
||||
|
||||
{{-- Chat Support (NO PERMISSION REQUIRED) --}}
|
||||
<a href="{{ route('admin.chat_support') }}" class="{{ request()->routeIs('admin.chat_support') ? 'active' : '' }}">
|
||||
<i class="bi bi-chat-dots"></i> Chat Support
|
||||
</a>
|
||||
|
||||
{{-- Orders --}}
|
||||
@can('orders.view')
|
||||
<a href="{{ route('admin.orders') }}" class="{{ request()->routeIs('admin.orders') ? 'active' : '' }}">
|
||||
<i class="bi bi-bag"></i> Orders
|
||||
</a>
|
||||
@endcan
|
||||
|
||||
{{-- Requests --}}
|
||||
@can('request.view')
|
||||
<a href="{{ route('admin.requests') }}" class="{{ request()->routeIs('admin.requests') ? 'active' : '' }}">
|
||||
<i class="bi bi-envelope"></i> Requests
|
||||
</a>
|
||||
@endcan
|
||||
|
||||
{{-- Profile Update Requests --}}
|
||||
@can('request.update_profile')
|
||||
<a href="{{ route('admin.profile.requests') }}">
|
||||
<i class="bi bi-person-lines-fill"></i> Profile Update Requests
|
||||
</a>
|
||||
@endcan
|
||||
|
||||
{{-- Staff (NO PERMISSION REQUIRED) --}}
|
||||
<a href="{{ route('admin.staff.index') }}" class="{{ request()->routeIs('admin.staff.*') ? 'active' : '' }}">
|
||||
<i class="bi bi-person-badge"></i> Staff
|
||||
</a>
|
||||
|
||||
{{-- Account Section --}}
|
||||
@can('account.view')
|
||||
<a href="{{ route('admin.account') }}" class="{{ request()->routeIs('admin.account') ? 'active' : '' }}">
|
||||
<i class="bi bi-gear"></i> Account
|
||||
</a>
|
||||
@endcan
|
||||
|
||||
{{-- Mark List --}}
|
||||
@can('mark_list.view')
|
||||
<a href="{{ route('admin.marklist.index') }}" class="{{ request()->routeIs('admin.marklist.index') ? 'active' : '' }}">
|
||||
<i class="bi bi-list-check"></i> Mark List
|
||||
</a>
|
||||
@endcan
|
||||
|
||||
</div>
|
||||
|
||||
<div class="main-content">
|
||||
<header>
|
||||
<h5>@yield('page-title')</h5>
|
||||
<div class="header-left">
|
||||
<button class="header-toggle" id="headerToggle">
|
||||
<i class="bi bi-list"></i>
|
||||
</button>
|
||||
<h5 class="mb-0">@yield('page-title')</h5>
|
||||
</div>
|
||||
<div class="d-flex align-items-center gap-3">
|
||||
<i class="bi bi-bell position-relative fs-4">
|
||||
<span class="position-absolute top-0 start-100 translate-middle badge rounded-pill bg-danger">2</span>
|
||||
@@ -224,7 +344,25 @@
|
||||
@yield('content')
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const headerToggle = document.getElementById('headerToggle');
|
||||
const sidebar = document.querySelector('.sidebar');
|
||||
const mainContent = document.querySelector('.main-content');
|
||||
|
||||
// Function to toggle sidebar
|
||||
function toggleSidebar() {
|
||||
sidebar.classList.toggle('collapsed');
|
||||
mainContent.classList.toggle('expanded');
|
||||
}
|
||||
|
||||
// Header toggle button click event
|
||||
if (headerToggle) {
|
||||
headerToggle.addEventListener('click', toggleSidebar);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
@@ -120,19 +120,33 @@
|
||||
@endif
|
||||
|
||||
<form method="POST" action="{{ route('admin.login.submit') }}">
|
||||
@csrf
|
||||
<div class="mb-3">
|
||||
<label>Email</label>
|
||||
<input type="email" name="email" class="form-control" value="{{ old('email') }}" required>
|
||||
</div>
|
||||
@csrf
|
||||
|
||||
<div class="mb-3">
|
||||
<label>Password</label>
|
||||
<input type="password" name="password" class="form-control" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label>Username / Email / Employee ID</label>
|
||||
<input
|
||||
type="text"
|
||||
name="login"
|
||||
class="form-control"
|
||||
value="{{ old('login') }}"
|
||||
placeholder="Enter Email or Username or EMP ID"
|
||||
required
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label>Password</label>
|
||||
<input
|
||||
type="password"
|
||||
name="password"
|
||||
class="form-control"
|
||||
required
|
||||
>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary w-100">Login</button>
|
||||
</form>
|
||||
|
||||
<button type="submit" class="btn btn-primary w-100">Login</button>
|
||||
</form>
|
||||
|
||||
<div class="secure-encrypted">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="green" class="bi bi-lock-fill" viewBox="0 0 16 16">
|
||||
|
||||
@@ -80,60 +80,158 @@
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
/* ✨ NEW BADGE STYLES - Same as Invoice System */
|
||||
.badge {
|
||||
padding: 7px 16px;
|
||||
border-radius: 20px;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
user-select: none;
|
||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.08);
|
||||
font-size: 11px !important;
|
||||
font-weight: 600 !important;
|
||||
padding: 6px 12px 6px 8px !important;
|
||||
border-radius: 20px !important;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.3px;
|
||||
display: inline-flex !important;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-size: cover !important;
|
||||
background-position: center !important;
|
||||
color: #fff !important;
|
||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
|
||||
border: 2px solid transparent !important;
|
||||
min-width: 40px;
|
||||
box-sizing: border-box;
|
||||
line-height: 1.2;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.bg-success {
|
||||
background-color: #28a745 !important;
|
||||
color: #fff !important;
|
||||
box-shadow: 0 0 10px #6ee86e77;
|
||||
/* Status icons */
|
||||
.status-icon {
|
||||
font-size: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* Custom status badge backgrounds with icons */
|
||||
.badge-active {
|
||||
background: url('/images/status-bg-paid.png') !important;
|
||||
}
|
||||
|
||||
.badge-inactive {
|
||||
background: url('/images/status-bg-overdue.png') !important;
|
||||
}
|
||||
|
||||
/* Fallback colors if images don't load - ALL WITH SAME SIZE */
|
||||
.badge.badge-active {
|
||||
background: linear-gradient(135deg, #d1fae5, #a7f3d0) !important;
|
||||
color: #065f46 !important;
|
||||
border-color: #10b981 !important;
|
||||
width: 98px;
|
||||
}
|
||||
|
||||
.badge.badge-inactive {
|
||||
background: linear-gradient(135deg, #e9d5ff, #c4b5fd) !important;
|
||||
color: #6b21a8 !important;
|
||||
border-color: #8b5cf6 !important;
|
||||
}
|
||||
|
||||
/* Animation effects for badges */
|
||||
.badge.badge-active {
|
||||
animation: pulseGreen 2s infinite;
|
||||
}
|
||||
|
||||
.badge.badge-inactive {
|
||||
animation: pulseRed 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulseGreen {
|
||||
0% { box-shadow: 0 0 8px #6ee86e77; }
|
||||
50% { box-shadow: 0 0 14px #5dd75d88; }
|
||||
100% { box-shadow: 0 0 8px #6ee86e77; }
|
||||
}
|
||||
|
||||
.bg-danger {
|
||||
background-color: #dc3545 !important;
|
||||
color: #fff !important;
|
||||
box-shadow: 0 0 10px #f97f7f77;
|
||||
animation: pulseRed 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulseRed {
|
||||
0% { box-shadow: 0 0 8px #f97f7f77; }
|
||||
50% { box-shadow: 0 0 14px #ff868677; }
|
||||
100% { box-shadow: 0 0 8px #f97f7f77; }
|
||||
}
|
||||
|
||||
/* ✨ ENHANCED ACTION BUTTONS - Professional & Attractive */
|
||||
.btn-action {
|
||||
background-color: #ffd283;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
border: none;
|
||||
color: #1b1c21;
|
||||
padding: 7px 20px;
|
||||
font-size: 0.95rem;
|
||||
border-radius: 8px;
|
||||
font-weight: 700;
|
||||
transition: all 0.25s ease;
|
||||
color: white;
|
||||
padding: 10px 22px;
|
||||
font-size: 0.85rem;
|
||||
border-radius: 12px;
|
||||
font-weight: 600;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
margin: 3px;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 2px 6px rgba(141, 106, 0, 0.1);
|
||||
display: inline-block;
|
||||
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
text-decoration: none;
|
||||
min-width: 120px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.btn-action::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.5s;
|
||||
}
|
||||
|
||||
.btn-action:hover::before {
|
||||
left: 100%;
|
||||
}
|
||||
|
||||
.btn-action:hover {
|
||||
background-color: #ffc85a;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 10px rgba(141, 106, 0, 0.25);
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 8px 25px rgba(102, 126, 234, 0.4);
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.btn-action:active {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
|
||||
}
|
||||
|
||||
/* Specific styles for Activate button */
|
||||
.btn-action-activate {
|
||||
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
|
||||
box-shadow: 0 4px 15px rgba(79, 172, 254, 0.3);
|
||||
}
|
||||
|
||||
.btn-action-activate:hover {
|
||||
box-shadow: 0 8px 25px rgba(79, 172, 254, 0.5);
|
||||
}
|
||||
|
||||
/* Specific styles for Deactivate button */
|
||||
.btn-action-deactivate {
|
||||
background: linear-gradient(135deg, #fa709a 0%, #fee140 100%);
|
||||
box-shadow: 0 4px 15px rgba(250, 112, 154, 0.3);
|
||||
}
|
||||
|
||||
.btn-action-deactivate:hover {
|
||||
box-shadow: 0 8px 25px rgba(250, 112, 154, 0.5);
|
||||
}
|
||||
|
||||
/* Button icons */
|
||||
.btn-icon {
|
||||
font-size: 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.text-muted {
|
||||
@@ -179,6 +277,139 @@
|
||||
from { box-shadow: 0 0 0px #b4a02455 inset; }
|
||||
to { box-shadow: 0 0 10px #b4a024aa inset; }
|
||||
}
|
||||
|
||||
/* ---------- Pagination Styles ---------- */
|
||||
.pagination-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-top: 15px;
|
||||
padding: 12px 0;
|
||||
border-top: 1px solid #eef3fb;
|
||||
}
|
||||
|
||||
.pagination-info {
|
||||
font-size: 13px;
|
||||
color: #9ba5bb;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.pagination-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.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%);
|
||||
}
|
||||
</style>
|
||||
|
||||
@if(session('success'))
|
||||
@@ -206,52 +437,208 @@
|
||||
<th>Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach($markList as $mark)
|
||||
<tr>
|
||||
<td>{{ $mark->id }}</td>
|
||||
<td>{{ $mark->mark_no }}</td>
|
||||
<td>{{ $mark->origin }}</td>
|
||||
<td>{{ $mark->destination }}</td>
|
||||
<td>{{ $mark->customer_name }}</td>
|
||||
<td>{{ $mark->customer_id }}</td>
|
||||
<td>{{ $mark->mobile_no }}</td>
|
||||
<td>{{ \Carbon\Carbon::parse($mark->date)->format('d-m-Y') }}</td>
|
||||
<td>
|
||||
@if($mark->status == 'active')
|
||||
<span class="badge bg-success">Active</span>
|
||||
@else
|
||||
<span class="badge bg-danger">In-Active</span>
|
||||
@endif
|
||||
</td>
|
||||
<td>
|
||||
@if($mark->status == 'active')
|
||||
<a href="{{ route('admin.marklist.toggle', $mark->id) }}" class="btn-action">Deactivate</a>
|
||||
@else
|
||||
<a href="{{ route('admin.marklist.toggle', $mark->id) }}" class="btn-action">Activate</a>
|
||||
@endif
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
<tbody id="markListTableBody">
|
||||
<!-- Data will be loaded dynamically -->
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@if($markList->isEmpty())
|
||||
<div class="py-4 text-center text-muted fst-italic">No mark numbers found.</div>
|
||||
@endif
|
||||
<!-- Pagination Controls -->
|
||||
<div class="pagination-container">
|
||||
<div class="pagination-info" id="pageInfo">Showing 0 entries</div>
|
||||
<div class="pagination-controls">
|
||||
<button class="pagination-img-btn" id="prevPageBtn" title="Previous page" 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">
|
||||
<!-- Page numbers will be inserted here -->
|
||||
</div>
|
||||
<button class="pagination-img-btn" id="nextPageBtn" title="Next page" 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 id="noResults" class="py-4 text-center text-muted fst-italic" style="display: none;">No mark numbers found.</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.getElementById('globalSearch').addEventListener('input', function () {
|
||||
const filter = this.value.toLowerCase();
|
||||
const rows = document.querySelectorAll('#markListTable tbody tr');
|
||||
// Pagination state
|
||||
let currentPage = 1;
|
||||
const itemsPerPage = 10;
|
||||
let allMarkList = @json($markList);
|
||||
let filteredMarkList = [...allMarkList];
|
||||
|
||||
rows.forEach(row => {
|
||||
const text = row.textContent.toLowerCase();
|
||||
row.style.display = text.includes(filter) ? '' : 'none';
|
||||
});
|
||||
// Initialize pagination
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
renderTable();
|
||||
updatePaginationControls();
|
||||
|
||||
// Bind pagination events
|
||||
document.getElementById('prevPageBtn').addEventListener('click', goToPreviousPage);
|
||||
document.getElementById('nextPageBtn').addEventListener('click', goToNextPage);
|
||||
|
||||
// Bind search event
|
||||
document.getElementById('globalSearch').addEventListener('input', handleSearch);
|
||||
});
|
||||
|
||||
// Pagination Functions
|
||||
function goToPreviousPage() {
|
||||
if (currentPage > 1) {
|
||||
currentPage--;
|
||||
renderTable();
|
||||
updatePaginationControls();
|
||||
}
|
||||
}
|
||||
|
||||
function goToNextPage() {
|
||||
const totalPages = Math.ceil(filteredMarkList.length / itemsPerPage);
|
||||
if (currentPage < totalPages) {
|
||||
currentPage++;
|
||||
renderTable();
|
||||
updatePaginationControls();
|
||||
}
|
||||
}
|
||||
|
||||
function updatePaginationControls() {
|
||||
const totalPages = Math.ceil(filteredMarkList.length / itemsPerPage);
|
||||
const prevBtn = document.getElementById('prevPageBtn');
|
||||
const nextBtn = document.getElementById('nextPageBtn');
|
||||
const pageInfo = document.getElementById('pageInfo');
|
||||
const paginationPages = document.getElementById('paginationPages');
|
||||
|
||||
prevBtn.disabled = currentPage === 1;
|
||||
nextBtn.disabled = currentPage === totalPages || totalPages === 0;
|
||||
|
||||
// Update page info text
|
||||
const startIndex = (currentPage - 1) * itemsPerPage + 1;
|
||||
const endIndex = Math.min(currentPage * itemsPerPage, filteredMarkList.length);
|
||||
pageInfo.textContent = `Showing ${startIndex} to ${endIndex} of ${filteredMarkList.length} entries`;
|
||||
|
||||
// Generate page numbers
|
||||
paginationPages.innerHTML = '';
|
||||
|
||||
if (totalPages <= 7) {
|
||||
// Show all pages
|
||||
for (let i = 1; i <= totalPages; i++) {
|
||||
addPageButton(i, paginationPages);
|
||||
}
|
||||
} else {
|
||||
// Show first page, current page range, and last page
|
||||
addPageButton(1, paginationPages);
|
||||
|
||||
if (currentPage > 3) {
|
||||
paginationPages.innerHTML += '<span class="pagination-ellipsis">...</span>';
|
||||
}
|
||||
|
||||
const start = Math.max(2, currentPage - 1);
|
||||
const end = Math.min(totalPages - 1, currentPage + 1);
|
||||
|
||||
for (let i = start; i <= end; i++) {
|
||||
addPageButton(i, paginationPages);
|
||||
}
|
||||
|
||||
if (currentPage < totalPages - 2) {
|
||||
paginationPages.innerHTML += '<span class="pagination-ellipsis">...</span>';
|
||||
}
|
||||
|
||||
addPageButton(totalPages, paginationPages);
|
||||
}
|
||||
}
|
||||
|
||||
function addPageButton(pageNumber, container) {
|
||||
const button = document.createElement('button');
|
||||
button.className = 'pagination-page-btn';
|
||||
if (pageNumber === currentPage) {
|
||||
button.classList.add('active');
|
||||
}
|
||||
button.textContent = pageNumber;
|
||||
button.addEventListener('click', () => {
|
||||
currentPage = pageNumber;
|
||||
renderTable();
|
||||
updatePaginationControls();
|
||||
});
|
||||
container.appendChild(button);
|
||||
}
|
||||
|
||||
// Search Function
|
||||
function handleSearch() {
|
||||
const filter = this.value.toLowerCase();
|
||||
filteredMarkList = allMarkList.filter(mark => {
|
||||
return Object.values(mark).some(value =>
|
||||
String(value).toLowerCase().includes(filter)
|
||||
);
|
||||
});
|
||||
|
||||
currentPage = 1;
|
||||
renderTable();
|
||||
updatePaginationControls();
|
||||
}
|
||||
|
||||
// Render Table Function - FIXED: Using direct URL construction
|
||||
function renderTable() {
|
||||
const tbody = document.getElementById('markListTableBody');
|
||||
const noResults = document.getElementById('noResults');
|
||||
tbody.innerHTML = '';
|
||||
|
||||
if (filteredMarkList.length === 0) {
|
||||
tbody.innerHTML = '';
|
||||
noResults.style.display = 'block';
|
||||
return;
|
||||
}
|
||||
|
||||
noResults.style.display = 'none';
|
||||
|
||||
// Calculate pagination
|
||||
const startIndex = (currentPage - 1) * itemsPerPage;
|
||||
const endIndex = startIndex + itemsPerPage;
|
||||
const paginatedItems = filteredMarkList.slice(startIndex, endIndex);
|
||||
|
||||
paginatedItems.forEach(mark => {
|
||||
const row = document.createElement('tr');
|
||||
row.innerHTML = `
|
||||
<td>${mark.id}</td>
|
||||
<td>${mark.mark_no}</td>
|
||||
<td>${mark.origin}</td>
|
||||
<td>${mark.destination}</td>
|
||||
<td>${mark.customer_name}</td>
|
||||
<td>${mark.customer_id}</td>
|
||||
<td>${mark.mobile_no}</td>
|
||||
<td>${new Date(mark.date).toLocaleDateString('en-GB')}</td>
|
||||
<td>
|
||||
${mark.status == 'active'
|
||||
? `<span class="badge badge-active">
|
||||
<i class="bi bi-check-circle-fill status-icon"></i>
|
||||
Active
|
||||
</span>`
|
||||
: `<span class="badge badge-inactive">
|
||||
<i class="bi bi-exclamation-triangle-fill status-icon"></i>
|
||||
In-Active
|
||||
</span>`
|
||||
}
|
||||
</td>
|
||||
<td>
|
||||
${mark.status == 'active'
|
||||
? `<a href="/admin/mark-list/status/${mark.id}" class="btn-action btn-action-deactivate">
|
||||
<i class="bi bi-power btn-icon"></i>
|
||||
Deactivate
|
||||
</a>`
|
||||
: `<a href="/admin/mark-list/status/${mark.id}" class="btn-action btn-action-activate">
|
||||
<i class="bi bi-play-circle btn-icon"></i>
|
||||
Activate
|
||||
</a>`
|
||||
}
|
||||
</td>
|
||||
`;
|
||||
tbody.appendChild(row);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
@endsection
|
||||
File diff suppressed because it is too large
Load Diff
70
resources/views/admin/orders/pdf.blade.php
Normal file
70
resources/views/admin/orders/pdf.blade.php
Normal file
@@ -0,0 +1,70 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Orders Report</title>
|
||||
<style>
|
||||
body { font-family: DejaVu Sans, sans-serif; font-size:12px; }
|
||||
table { width:100%; border-collapse: collapse; }
|
||||
th, td { border: 1px solid #ddd; padding: 6px; text-align: left; }
|
||||
th { background: #f2f2f2; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h3>Orders Report</h3>
|
||||
|
||||
@if(!empty($filters))
|
||||
<p>
|
||||
@if($filters['search']) Search: <strong>{{ $filters['search'] }}</strong> @endif
|
||||
@if($filters['status']) | Status: <strong>{{ ucfirst($filters['status']) }}</strong> @endif
|
||||
@if($filters['shipment']) | Shipment: <strong>{{ ucfirst($filters['shipment']) }}</strong> @endif
|
||||
</p>
|
||||
@endif
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Order ID</th>
|
||||
<th>Shipment ID</th>
|
||||
<th>Customer ID</th>
|
||||
<th>Company</th>
|
||||
<th>Origin</th>
|
||||
<th>Destination</th>
|
||||
<th>Order Date</th>
|
||||
<th>Invoice No</th>
|
||||
<th>Invoice Date</th>
|
||||
<th>Amount</th>
|
||||
<th>Amount + GST</th>
|
||||
<th>Invoice Status</th>
|
||||
<th>Shipment Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@forelse($orders as $order)
|
||||
@php
|
||||
$mark = $order->markList ?? null;
|
||||
$invoice = $order->invoice ?? null;
|
||||
$shipment = $order->shipments->first() ?? null;
|
||||
@endphp
|
||||
<tr>
|
||||
<td>{{ $order->order_id }}</td>
|
||||
<td>{{ $shipment->shipment_id ?? '-' }}</td>
|
||||
<td>{{ $mark->customer_id ?? '-' }}</td>
|
||||
<td>{{ $mark->company_name ?? '-' }}</td>
|
||||
<td>{{ $mark->origin ?? $order->origin ?? '-' }}</td>
|
||||
<td>{{ $mark->destination ?? $order->destination ?? '-' }}</td>
|
||||
<td>{{ $order->created_at ? $order->created_at->format('d-m-Y') : '-' }}</td>
|
||||
<td>{{ $invoice->invoice_number ?? '-' }}</td>
|
||||
<td>{{ $invoice?->invoice_date ? \Carbon\Carbon::parse($invoice->invoice_date)->format('d-m-Y') : '-' }}</td>
|
||||
<td>{{ $invoice?->final_amount ? number_format($invoice->final_amount, 2) : '-' }}</td>
|
||||
<td>{{ $invoice?->final_amount_with_gst ? number_format($invoice->final_amount_with_gst, 2) : '-' }}</td>
|
||||
<td>{{ $invoice->status ? ucfirst($invoice->status) : 'Pending' }}</td>
|
||||
<td>{{ $shipment?->status ? ucfirst(str_replace('_',' ',$shipment->status)) : 'Pending' }}</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr><td colspan="13" style="text-align:center">No orders found</td></tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user