- 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)
329 lines
11 KiB
PHP
329 lines
11 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Http\Controllers\Seller\Management;
|
|
|
|
use App\Http\Controllers\Controller;
|
|
use App\Models\Accounting\ApVendor;
|
|
use App\Models\Accounting\GlAccount;
|
|
use App\Models\Business;
|
|
use App\Support\ManagementDivisionFilter;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Validation\Rule;
|
|
|
|
class ApVendorsController extends Controller
|
|
{
|
|
use ManagementDivisionFilter;
|
|
|
|
/**
|
|
* Vendors list page.
|
|
*
|
|
* GET /s/{business}/management/ap/vendors
|
|
*/
|
|
public function index(Request $request, Business $business)
|
|
{
|
|
$filterData = $this->getDivisionFilterData($business, $request);
|
|
$isParent = $business->parent_id === null && Business::where('parent_id', $business->id)->exists();
|
|
|
|
$query = ApVendor::whereIn('business_id', $filterData['business_ids'])
|
|
->with('business')
|
|
->withCount('bills');
|
|
|
|
// Search filter
|
|
if ($request->filled('search')) {
|
|
$search = $request->search;
|
|
$query->where(function ($q) use ($search) {
|
|
$q->where('name', 'ilike', "%{$search}%")
|
|
->orWhere('code', 'ilike', "%{$search}%")
|
|
->orWhere('contact_email', 'ilike', "%{$search}%");
|
|
});
|
|
}
|
|
|
|
// Active filter
|
|
if ($request->has('active')) {
|
|
$query->where('is_active', $request->boolean('active'));
|
|
}
|
|
|
|
$vendors = $query->orderBy('name')->paginate(20)->withQueryString();
|
|
|
|
// For parent business, compute which child divisions use each vendor
|
|
if ($isParent) {
|
|
$childBusinessIds = Business::where('parent_id', $business->id)->pluck('id')->toArray();
|
|
$childBusinesses = Business::whereIn('id', $childBusinessIds)->get()->keyBy('id');
|
|
|
|
$vendors->getCollection()->transform(function ($vendor) use ($childBusinessIds, $childBusinesses) {
|
|
// Get divisions that have bills or POs with this vendor
|
|
$divisionsUsingVendor = collect();
|
|
|
|
// Check if vendor belongs to a child directly
|
|
if (in_array($vendor->business_id, $childBusinessIds)) {
|
|
$divisionsUsingVendor->push($childBusinesses[$vendor->business_id] ?? null);
|
|
}
|
|
|
|
// Check for bills from other children using this vendor
|
|
$billBusinessIds = $vendor->bills()
|
|
->whereIn('business_id', $childBusinessIds)
|
|
->distinct()
|
|
->pluck('business_id')
|
|
->toArray();
|
|
|
|
foreach ($billBusinessIds as $bizId) {
|
|
if (! $divisionsUsingVendor->contains('id', $bizId) && isset($childBusinesses[$bizId])) {
|
|
$divisionsUsingVendor->push($childBusinesses[$bizId]);
|
|
}
|
|
}
|
|
|
|
$vendor->divisions_using = $divisionsUsingVendor->filter()->unique('id')->values();
|
|
|
|
return $vendor;
|
|
});
|
|
}
|
|
|
|
// Get GL accounts for default expense account dropdown
|
|
$glAccounts = GlAccount::where('business_id', $business->id)
|
|
->where('is_active', true)
|
|
->where('is_header', false)
|
|
->whereIn('account_type', ['expense', 'asset'])
|
|
->orderBy('account_number')
|
|
->get();
|
|
|
|
return view('seller.management.ap.vendors.index', $this->withDivisionFilter([
|
|
'business' => $business,
|
|
'vendors' => $vendors,
|
|
'glAccounts' => $glAccounts,
|
|
'isParent' => $isParent,
|
|
], $filterData));
|
|
}
|
|
|
|
/**
|
|
* Store a new vendor.
|
|
*
|
|
* POST /s/{business}/management/ap/vendors
|
|
*/
|
|
public function store(Request $request, Business $business)
|
|
{
|
|
$validated = $request->validate([
|
|
'code' => 'nullable|string|max:50',
|
|
'name' => 'required|string|max:255',
|
|
'legal_name' => 'nullable|string|max:255',
|
|
'tax_id' => 'nullable|string|max:50',
|
|
'default_payment_terms' => 'nullable|integer|min:0',
|
|
'default_gl_account_id' => ['nullable', 'integer', Rule::exists('gl_accounts', 'id')->where('business_id', $business->id)],
|
|
'contact_name' => 'nullable|string|max:255',
|
|
'contact_email' => 'nullable|email|max:255',
|
|
'contact_phone' => 'nullable|string|max:50',
|
|
'address_line1' => 'nullable|string|max:255',
|
|
'address_line2' => 'nullable|string|max:255',
|
|
'city' => 'nullable|string|max:100',
|
|
'state' => 'nullable|string|max:100',
|
|
'postal_code' => 'nullable|string|max:20',
|
|
'country' => 'nullable|string|max:100',
|
|
'is_1099' => 'boolean',
|
|
'notes' => 'nullable|string|max:1000',
|
|
]);
|
|
|
|
// Generate code if not provided
|
|
if (empty($validated['code'])) {
|
|
$validated['code'] = $this->generateVendorCode($business->id, $validated['name']);
|
|
}
|
|
|
|
$vendor = ApVendor::create([
|
|
'business_id' => $business->id,
|
|
...$validated,
|
|
'is_active' => true,
|
|
]);
|
|
|
|
if ($request->wantsJson()) {
|
|
return response()->json([
|
|
'success' => true,
|
|
'vendor' => $vendor,
|
|
]);
|
|
}
|
|
|
|
return back()->with('success', "Vendor {$vendor->name} created successfully.");
|
|
}
|
|
|
|
/**
|
|
* Show create vendor form.
|
|
*
|
|
* GET /s/{business}/management/ap/vendors/create
|
|
*/
|
|
public function create(Request $request, Business $business)
|
|
{
|
|
$glAccounts = GlAccount::where('business_id', $business->id)
|
|
->where('is_active', true)
|
|
->where('is_header', false)
|
|
->whereIn('account_type', ['expense', 'asset'])
|
|
->orderBy('account_number')
|
|
->get();
|
|
|
|
return view('seller.management.ap.vendors.create', compact(
|
|
'business',
|
|
'glAccounts'
|
|
));
|
|
}
|
|
|
|
/**
|
|
* Show vendor details.
|
|
*
|
|
* GET /s/{business}/management/ap/vendors/{vendor}
|
|
*/
|
|
public function show(Request $request, Business $business, ApVendor $vendor)
|
|
{
|
|
$allowedBusinessIds = $this->getAllowedBusinessIds($business);
|
|
if (! in_array($vendor->business_id, $allowedBusinessIds)) {
|
|
abort(403, 'Access denied.');
|
|
}
|
|
|
|
$vendor->load(['defaultGlAccount']);
|
|
|
|
// Get recent bills
|
|
$recentBills = $vendor->bills()
|
|
->with(['glAccount'])
|
|
->orderByDesc('bill_date')
|
|
->limit(10)
|
|
->get();
|
|
|
|
// Get recent payments
|
|
$recentPayments = $vendor->payments()
|
|
->with(['bills'])
|
|
->orderByDesc('payment_date')
|
|
->limit(10)
|
|
->get();
|
|
|
|
// Calculate metrics
|
|
$metrics = [
|
|
'total_bills' => $vendor->bills()->count(),
|
|
'unpaid_balance' => $vendor->bills()->unpaid()->sum('balance_due'),
|
|
'overdue_balance' => $vendor->bills()->overdue()->sum('balance_due'),
|
|
'ytd_payments' => $vendor->payments()
|
|
->whereYear('payment_date', now()->year)
|
|
->completed()
|
|
->sum('amount'),
|
|
];
|
|
|
|
return view('seller.management.ap.vendors.show', compact(
|
|
'business',
|
|
'vendor',
|
|
'recentBills',
|
|
'recentPayments',
|
|
'metrics'
|
|
));
|
|
}
|
|
|
|
/**
|
|
* Show edit vendor form.
|
|
*
|
|
* GET /s/{business}/management/ap/vendors/{vendor}/edit
|
|
*/
|
|
public function edit(Request $request, Business $business, ApVendor $vendor)
|
|
{
|
|
$allowedBusinessIds = $this->getAllowedBusinessIds($business);
|
|
if (! in_array($vendor->business_id, $allowedBusinessIds)) {
|
|
abort(403, 'Access denied.');
|
|
}
|
|
|
|
$glAccounts = GlAccount::where('business_id', $business->id)
|
|
->where('is_active', true)
|
|
->where('is_header', false)
|
|
->whereIn('account_type', ['expense', 'asset'])
|
|
->orderBy('account_number')
|
|
->get();
|
|
|
|
return view('seller.management.ap.vendors.edit', compact(
|
|
'business',
|
|
'vendor',
|
|
'glAccounts'
|
|
));
|
|
}
|
|
|
|
/**
|
|
* Update a vendor.
|
|
*
|
|
* PUT /s/{business}/management/ap/vendors/{vendor}
|
|
*/
|
|
public function update(Request $request, Business $business, ApVendor $vendor)
|
|
{
|
|
$allowedBusinessIds = $this->getAllowedBusinessIds($business);
|
|
if (! in_array($vendor->business_id, $allowedBusinessIds)) {
|
|
abort(403, 'Access denied.');
|
|
}
|
|
|
|
$validated = $request->validate([
|
|
'code' => 'nullable|string|max:50',
|
|
'name' => 'required|string|max:255',
|
|
'legal_name' => 'nullable|string|max:255',
|
|
'tax_id' => 'nullable|string|max:50',
|
|
'default_payment_terms' => 'nullable|integer|min:0',
|
|
'default_gl_account_id' => ['nullable', 'integer', Rule::exists('gl_accounts', 'id')->where('business_id', $business->id)],
|
|
'contact_name' => 'nullable|string|max:255',
|
|
'contact_email' => 'nullable|email|max:255',
|
|
'contact_phone' => 'nullable|string|max:50',
|
|
'address_line1' => 'nullable|string|max:255',
|
|
'address_line2' => 'nullable|string|max:255',
|
|
'city' => 'nullable|string|max:100',
|
|
'state' => 'nullable|string|max:100',
|
|
'postal_code' => 'nullable|string|max:20',
|
|
'country' => 'nullable|string|max:100',
|
|
'is_1099' => 'boolean',
|
|
'is_active' => 'boolean',
|
|
'notes' => 'nullable|string|max:1000',
|
|
]);
|
|
|
|
$vendor->update($validated);
|
|
|
|
if ($request->wantsJson()) {
|
|
return response()->json([
|
|
'success' => true,
|
|
'vendor' => $vendor->fresh(),
|
|
]);
|
|
}
|
|
|
|
return back()->with('success', "Vendor {$vendor->name} updated successfully.");
|
|
}
|
|
|
|
/**
|
|
* Toggle vendor active status.
|
|
*
|
|
* POST /s/{business}/management/ap/vendors/{vendor}/toggle-active
|
|
*/
|
|
public function toggleActive(Business $business, ApVendor $vendor)
|
|
{
|
|
$allowedBusinessIds = $this->getAllowedBusinessIds($business);
|
|
if (! in_array($vendor->business_id, $allowedBusinessIds)) {
|
|
abort(403, 'Access denied.');
|
|
}
|
|
|
|
$vendor->update(['is_active' => ! $vendor->is_active]);
|
|
|
|
$status = $vendor->is_active ? 'activated' : 'deactivated';
|
|
|
|
return back()->with('success', "Vendor {$vendor->name} {$status}.");
|
|
}
|
|
|
|
/**
|
|
* Generate vendor code from name.
|
|
*/
|
|
protected function generateVendorCode(int $businessId, string $name): string
|
|
{
|
|
// Take first 3 chars of each word, uppercase
|
|
$words = preg_split('/\s+/', strtoupper($name));
|
|
$prefix = '';
|
|
foreach ($words as $word) {
|
|
$prefix .= substr(preg_replace('/[^A-Z0-9]/', '', $word), 0, 3);
|
|
if (strlen($prefix) >= 6) {
|
|
break;
|
|
}
|
|
}
|
|
$prefix = substr($prefix, 0, 6);
|
|
|
|
// Check for uniqueness
|
|
$count = ApVendor::where('business_id', $businessId)
|
|
->where('code', 'ilike', "{$prefix}%")
|
|
->count();
|
|
|
|
return $count > 0 ? "{$prefix}-{$count}" : $prefix;
|
|
}
|
|
}
|