- 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)
436 lines
16 KiB
PHP
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);
|
|
}
|
|
}
|