Files
hub/app/Http/Controllers/Seller/Purchasing/RequisitionsController.php
kelly c7d6ee5e21 feat: multiple UI/UX improvements and case-insensitive search
- Refactor New Quote page to enterprise data-entry layout (2-column, dense)
- Add Payment Terms dropdown (COD, NET 15, NET 30, NET 60)
- Fix sidebar menu active states and route names
- Fix brand filter badge visibility on brands page
- Remove company_name references (use business instead)
- Polish Promotions page layout
- Fix double-click issue on sidebar menu collapse
- Make all searches case-insensitive (like -> ilike for PostgreSQL)
2025-12-14 15:36:00 -07:00

436 lines
16 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Http\Controllers\Seller\Purchasing;
use App\Http\Controllers\Controller;
use App\Models\Accounting\ApVendor;
use App\Models\Accounting\GlAccount;
use App\Models\Business;
use App\Models\Department;
use App\Models\Purchasing\PurchaseRequisition;
use App\Models\Purchasing\PurchaseRequisitionItem;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\View\View;
class RequisitionsController extends Controller
{
/**
* Display list of requisitions for the business.
*/
public function index(Request $request, Business $business): View
{
$user = auth()->user();
$query = PurchaseRequisition::forBusiness($business->id)
->with(['requestedBy', 'vendor', 'department', 'approvedBy'])
->withCount('items');
// Non-owners see only their own requisitions
if ($business->owner_user_id !== $user->id && ! $this->userCanViewAllRequisitions($user, $business)) {
$query->where('requested_by_user_id', $user->id);
}
// Filters
if ($status = $request->get('status')) {
$query->where('status', $status);
}
if ($priority = $request->get('priority')) {
$query->where('priority', $priority);
}
if ($search = $request->get('search')) {
$query->where(function ($q) use ($search) {
$q->where('requisition_number', 'ilike', "%{$search}%")
->orWhere('notes', 'ilike', "%{$search}%");
});
}
$requisitions = $query->orderByDesc('created_at')->paginate(20);
// Get counts for tabs
$statusCounts = PurchaseRequisition::forBusiness($business->id)
->selectRaw('status, COUNT(*) as count')
->groupBy('status')
->pluck('count', 'status');
return view('seller.purchasing.requisitions.index', [
'business' => $business,
'requisitions' => $requisitions,
'statusCounts' => $statusCounts,
'filters' => $request->only(['status', 'priority', 'search']),
]);
}
/**
* Show form to create a new requisition.
*/
public function create(Request $request, Business $business): View
{
$this->authorizeCreate($business);
$vendors = ApVendor::where('business_id', $business->id)
->where('is_active', true)
->orderBy('name')
->get();
$departments = Department::where('business_id', $business->id)
->where('is_active', true)
->orderBy('name')
->get();
$glAccounts = GlAccount::where('business_id', $business->id)
->where('is_active', true)
->whereIn('account_type', ['expense', 'asset'])
->orderBy('account_number')
->get();
return view('seller.purchasing.requisitions.create', [
'business' => $business,
'vendors' => $vendors,
'departments' => $departments,
'glAccounts' => $glAccounts,
'priorities' => PurchaseRequisition::getPriorityOptions(),
]);
}
/**
* Store a new requisition.
*/
public function store(Request $request, Business $business): RedirectResponse
{
$this->authorizeCreate($business);
$validated = $request->validate([
'department_id' => 'nullable|exists:departments,id',
'vendor_id' => 'nullable|exists:ap_vendors,id',
'priority' => 'required|in:low,normal,high,urgent',
'needed_by_date' => 'nullable|date|after:today',
'notes' => 'nullable|string|max:2000',
'items' => 'required|array|min:1',
'items.*.description' => 'required|string|max:500',
'items.*.quantity' => 'required|numeric|min:0.0001',
'items.*.unit' => 'nullable|string|max:50',
'items.*.est_unit_cost' => 'nullable|numeric|min:0',
'items.*.suggested_vendor_id' => 'nullable|exists:ap_vendors,id',
'items.*.gl_account_id' => 'nullable|exists:gl_accounts,id',
'items.*.notes' => 'nullable|string|max:500',
'submit_action' => 'required|in:save_draft,submit',
]);
$requisition = PurchaseRequisition::create([
'business_id' => $business->id,
'department_id' => $validated['department_id'] ?? null,
'requested_by_user_id' => auth()->id(),
'vendor_id' => $validated['vendor_id'] ?? null,
'priority' => $validated['priority'],
'needed_by_date' => $validated['needed_by_date'] ?? null,
'notes' => $validated['notes'] ?? null,
'status' => PurchaseRequisition::STATUS_DRAFT,
]);
foreach ($validated['items'] as $itemData) {
$requisition->items()->create([
'description' => $itemData['description'],
'quantity' => $itemData['quantity'],
'unit' => $itemData['unit'] ?? null,
'est_unit_cost' => $itemData['est_unit_cost'] ?? null,
'suggested_vendor_id' => $itemData['suggested_vendor_id'] ?? null,
'gl_account_id' => $itemData['gl_account_id'] ?? null,
'notes' => $itemData['notes'] ?? null,
]);
}
// Submit if requested
if ($validated['submit_action'] === 'submit') {
$requisition->submit();
return redirect()
->route('seller.business.purchasing.requisitions.show', [$business, $requisition])
->with('success', 'Requisition submitted for approval.');
}
return redirect()
->route('seller.business.purchasing.requisitions.show', [$business, $requisition])
->with('success', 'Requisition draft saved.');
}
/**
* Show a single requisition.
*/
public function show(Request $request, Business $business, PurchaseRequisition $requisition): View
{
$this->authorizeView($business, $requisition);
$requisition->load(['items.suggestedVendor', 'items.glAccount', 'requestedBy', 'approvedBy', 'vendor', 'department', 'purchaseOrder']);
return view('seller.purchasing.requisitions.show', [
'business' => $business,
'requisition' => $requisition,
]);
}
/**
* Show form to edit a requisition.
*/
public function edit(Request $request, Business $business, PurchaseRequisition $requisition): View
{
$this->authorizeEdit($business, $requisition);
$requisition->load('items');
$vendors = ApVendor::where('business_id', $business->id)
->where('is_active', true)
->orderBy('name')
->get();
$departments = Department::where('business_id', $business->id)
->where('is_active', true)
->orderBy('name')
->get();
$glAccounts = GlAccount::where('business_id', $business->id)
->where('is_active', true)
->whereIn('account_type', ['expense', 'asset'])
->orderBy('account_number')
->get();
return view('seller.purchasing.requisitions.edit', [
'business' => $business,
'requisition' => $requisition,
'vendors' => $vendors,
'departments' => $departments,
'glAccounts' => $glAccounts,
'priorities' => PurchaseRequisition::getPriorityOptions(),
]);
}
/**
* Update a requisition.
*/
public function update(Request $request, Business $business, PurchaseRequisition $requisition): RedirectResponse
{
$this->authorizeEdit($business, $requisition);
$validated = $request->validate([
'department_id' => 'nullable|exists:departments,id',
'vendor_id' => 'nullable|exists:ap_vendors,id',
'priority' => 'required|in:low,normal,high,urgent',
'needed_by_date' => 'nullable|date|after:today',
'notes' => 'nullable|string|max:2000',
'items' => 'required|array|min:1',
'items.*.id' => 'nullable|exists:purchase_requisition_items,id',
'items.*.description' => 'required|string|max:500',
'items.*.quantity' => 'required|numeric|min:0.0001',
'items.*.unit' => 'nullable|string|max:50',
'items.*.est_unit_cost' => 'nullable|numeric|min:0',
'items.*.suggested_vendor_id' => 'nullable|exists:ap_vendors,id',
'items.*.gl_account_id' => 'nullable|exists:gl_accounts,id',
'items.*.notes' => 'nullable|string|max:500',
'submit_action' => 'required|in:save_draft,submit',
]);
$requisition->update([
'department_id' => $validated['department_id'] ?? null,
'vendor_id' => $validated['vendor_id'] ?? null,
'priority' => $validated['priority'],
'needed_by_date' => $validated['needed_by_date'] ?? null,
'notes' => $validated['notes'] ?? null,
]);
// Sync items - delete removed, update existing, create new
$existingItemIds = collect($validated['items'])
->pluck('id')
->filter()
->toArray();
// Delete items not in the update
$requisition->items()
->whereNotIn('id', $existingItemIds)
->delete();
foreach ($validated['items'] as $itemData) {
if (! empty($itemData['id'])) {
// Update existing item
PurchaseRequisitionItem::where('id', $itemData['id'])
->update([
'description' => $itemData['description'],
'quantity' => $itemData['quantity'],
'unit' => $itemData['unit'] ?? null,
'est_unit_cost' => $itemData['est_unit_cost'] ?? null,
'suggested_vendor_id' => $itemData['suggested_vendor_id'] ?? null,
'gl_account_id' => $itemData['gl_account_id'] ?? null,
'notes' => $itemData['notes'] ?? null,
]);
} else {
// Create new item
$requisition->items()->create([
'description' => $itemData['description'],
'quantity' => $itemData['quantity'],
'unit' => $itemData['unit'] ?? null,
'est_unit_cost' => $itemData['est_unit_cost'] ?? null,
'suggested_vendor_id' => $itemData['suggested_vendor_id'] ?? null,
'gl_account_id' => $itemData['gl_account_id'] ?? null,
'notes' => $itemData['notes'] ?? null,
]);
}
}
// Submit if requested and still in draft
if ($validated['submit_action'] === 'submit' && $requisition->isDraft()) {
$requisition->submit();
return redirect()
->route('seller.business.purchasing.requisitions.show', [$business, $requisition])
->with('success', 'Requisition submitted for approval.');
}
return redirect()
->route('seller.business.purchasing.requisitions.show', [$business, $requisition])
->with('success', 'Requisition updated.');
}
/**
* Submit a draft requisition for approval.
*/
public function submit(Request $request, Business $business, PurchaseRequisition $requisition): RedirectResponse
{
$this->authorizeEdit($business, $requisition);
if (! $requisition->isDraft()) {
return back()->with('error', 'Only draft requisitions can be submitted.');
}
if ($requisition->items->isEmpty()) {
return back()->with('error', 'Requisition must have at least one item.');
}
$requisition->submit();
return redirect()
->route('seller.business.purchasing.requisitions.show', [$business, $requisition])
->with('success', 'Requisition submitted for approval.');
}
/**
* Delete a draft requisition.
*/
public function destroy(Request $request, Business $business, PurchaseRequisition $requisition): RedirectResponse
{
$this->authorizeDelete($business, $requisition);
if (! $requisition->isDraft()) {
return back()->with('error', 'Only draft requisitions can be deleted.');
}
$requisition->delete();
return redirect()
->route('seller.business.purchasing.requisitions.index', $business)
->with('success', 'Requisition deleted.');
}
// =========================================================================
// AUTHORIZATION HELPERS
// =========================================================================
protected function authorizeCreate(Business $business): void
{
$user = auth()->user();
// Check if user has permission to submit requisitions
if (! $this->userCanSubmitRequisitions($user, $business)) {
abort(403, 'You do not have permission to create requisitions.');
}
}
protected function authorizeView(Business $business, PurchaseRequisition $requisition): void
{
if ($requisition->business_id !== $business->id) {
abort(404);
}
$user = auth()->user();
// Owner can view all
if ($business->owner_user_id === $user->id) {
return;
}
// User can view their own
if ($requisition->requested_by_user_id === $user->id) {
return;
}
// Users with view all permission
if ($this->userCanViewAllRequisitions($user, $business)) {
return;
}
abort(403, 'You do not have permission to view this requisition.');
}
protected function authorizeEdit(Business $business, PurchaseRequisition $requisition): void
{
$this->authorizeView($business, $requisition);
if (! $requisition->canBeEdited()) {
abort(403, 'This requisition cannot be edited.');
}
$user = auth()->user();
// Only the requester can edit their own requisition (unless owner)
if ($requisition->requested_by_user_id !== $user->id && $business->owner_user_id !== $user->id) {
abort(403, 'You can only edit your own requisitions.');
}
}
protected function authorizeDelete(Business $business, PurchaseRequisition $requisition): void
{
$this->authorizeEdit($business, $requisition);
}
protected function userCanSubmitRequisitions($user, Business $business): bool
{
// Owner always can
if ($business->owner_user_id === $user->id) {
return true;
}
// Check pivot permission
$pivot = $user->businesses()->where('businesses.id', $business->id)->first()?->pivot;
if (! $pivot) {
return false;
}
$permissions = $pivot->permissions ?? [];
return in_array('can_submit_requisition', $permissions) || in_array('*', $permissions);
}
protected function userCanViewAllRequisitions($user, Business $business): bool
{
// Owner always can
if ($business->owner_user_id === $user->id) {
return true;
}
$pivot = $user->businesses()->where('businesses.id', $business->id)->first()?->pivot;
if (! $pivot) {
return false;
}
$permissions = $pivot->permissions ?? [];
return in_array('can_view_all_requisitions', $permissions)
|| in_array('can_approve_requisition', $permissions)
|| in_array('*', $permissions);
}
}