Files
hub/app/Http/Controllers/Api/Accounting/ApVendorController.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

208 lines
6.9 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Http\Controllers\Api\Accounting;
use App\Http\Controllers\Controller;
use App\Models\Accounting\ApVendor;
use App\Models\Business;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
class ApVendorController extends Controller
{
/**
* List vendors for a business.
*
* GET /api/{business}/ap/vendors
*/
public function index(Request $request, Business $business): JsonResponse
{
$query = ApVendor::where('business_id', $business->id);
// Search
if ($request->filled('search')) {
$search = $request->search;
$query->where(function ($q) use ($search) {
$q->where('name', 'ilike', "%{$search}%")
->orWhere('code', 'ilike', "%{$search}%");
});
}
// Active filter
if ($request->has('active')) {
$query->where('is_active', $request->boolean('active'));
}
$vendors = $query->orderBy('name')->paginate($request->get('per_page', 50));
return response()->json([
'success' => true,
'data' => $vendors->items(),
'meta' => [
'current_page' => $vendors->currentPage(),
'last_page' => $vendors->lastPage(),
'per_page' => $vendors->perPage(),
'total' => $vendors->total(),
],
]);
}
/**
* Get a single vendor.
*
* GET /api/{business}/ap/vendors/{vendor}
*/
public function show(Business $business, ApVendor $vendor): JsonResponse
{
if ($vendor->business_id !== $business->id) {
return response()->json([
'success' => false,
'message' => 'Vendor does not belong to this business.',
], 403);
}
return response()->json([
'success' => true,
'data' => $vendor,
]);
}
/**
* Create a new vendor.
*
* POST /api/{business}/ap/vendors
*/
public function store(Request $request, Business $business): JsonResponse
{
try {
$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|exists:gl_accounts,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,
]);
return response()->json([
'success' => true,
'message' => "Vendor {$vendor->name} created.",
'data' => $vendor,
], 201);
} catch (\Exception $e) {
Log::error('Vendor creation failed', [
'business_id' => $business->id,
'error' => $e->getMessage(),
]);
return response()->json([
'success' => false,
'message' => 'Failed to create vendor: '.$e->getMessage(),
], 500);
}
}
/**
* Update a vendor.
*
* PUT /api/{business}/ap/vendors/{vendor}
*/
public function update(Request $request, Business $business, ApVendor $vendor): JsonResponse
{
if ($vendor->business_id !== $business->id) {
return response()->json([
'success' => false,
'message' => 'Vendor does not belong to this business.',
], 403);
}
try {
$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|exists:gl_accounts,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);
return response()->json([
'success' => true,
'message' => "Vendor {$vendor->name} updated.",
'data' => $vendor->fresh(),
]);
} catch (\Exception $e) {
Log::error('Vendor update failed', [
'vendor_id' => $vendor->id,
'error' => $e->getMessage(),
]);
return response()->json([
'success' => false,
'message' => 'Failed to update vendor: '.$e->getMessage(),
], 500);
}
}
/**
* Generate vendor code from name.
*/
protected function generateVendorCode(int $businessId, string $name): string
{
$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);
$count = ApVendor::where('business_id', $businessId)
->where('code', 'ilike', "{$prefix}%")
->count();
return $count > 0 ? "{$prefix}-{$count}" : $prefix;
}
}