chore: initial commit of existing codebase

This commit is contained in:
ghassan committed 2026-05-19 12:40:08 +03:00
commit 11e94889b2
292 files changed
+38586

No files matched your search

@@ -0,0 +1,150 @@
<?php
namespace App\Http\Controllers\Purchase;
use App\Http\Controllers\Controller;
use App\Models\GoodsReceiptNote;
use App\Models\GrnItem;
use App\Models\PurchaseOrder;
use App\Models\StockLevel;
use App\Models\StockMovement;
use App\Models\Warehouse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
class GoodsReceiptNoteController extends Controller
{
public function index()
{
$grns = GoodsReceiptNote::with(['purchaseOrder.supplier', 'warehouse'])->paginate(15);
return view('purchase.grns.index', compact('grns'));
}
public function create()
{
$purchaseOrders = PurchaseOrder::whereIn('status', ['sent', 'partial'])->with('supplier')->get();
$warehouses = Warehouse::all();
return view('purchase.grns.create', compact('purchaseOrders', 'warehouses'));
}
public function store(Request $request)
{
$request->validate([
'purchase_order_id' => 'required|exists:purchase_orders,id',
'warehouse_id' => 'required|exists:warehouses,id',
'received_date' => 'required|date',
'items' => 'required|array|min:1',
'items.*.item_id' => 'required|exists:items,id',
'items.*.quantity' => 'required|numeric|min:1',
]);
$grnNumber = 'GRN-' . str_pad(GoodsReceiptNote::max('id') + 1, 5, '0', STR_PAD_LEFT);
$po = PurchaseOrder::findOrFail($request->purchase_order_id);
$grn = GoodsReceiptNote::create([
'grn_number' => $grnNumber,
'purchase_order_id' => $request->purchase_order_id,
'supplier_id' => $po->supplier_id,
'warehouse_id' => $request->warehouse_id,
'received_date' => $request->received_date,
'status' => 'draft',
'received_by' => auth()->id(),
]);
foreach ($request->items as $item) {
GrnItem::create([
'goods_receipt_note_id' => $grn->id,
'purchase_order_item_id' => $item['po_item_id'] ?? $po->items()->where('item_id', $item['item_id'])->first()?->id ?? 0,
'item_id' => $item['item_id'],
'quantity_received' => $item['quantity'],
'unit_cost' => $item['unit_cost'] ?? 0,
'type' => in_array($item['type'] ?? '', ['inventory', 'consumable']) ? $item['type'] : 'inventory',
]);
}
return redirect()->route('purchase.grns.show', $grn)->with('success', 'GRN created successfully.');
}
public function show(GoodsReceiptNote $grn)
{
$grn->load(['purchaseOrder.supplier', 'warehouse', 'items.item']);
return view('purchase.grns.show', compact('grn'));
}
public function edit(GoodsReceiptNote $grn)
{
$warehouses = Warehouse::all();
return view('purchase.grns.edit', compact('grn', 'warehouses'));
}
public function update(Request $request, GoodsReceiptNote $grn)
{
$request->validate([
'received_date' => 'required|date',
'warehouse_id' => 'required|exists:warehouses,id',
]);
$grn->update($request->only('received_date', 'warehouse_id'));
return redirect()->route('purchase.grns.show', $grn)->with('success', 'GRN updated successfully.');
}
public function destroy(GoodsReceiptNote $grn)
{
$grn->delete();
return redirect()->route('purchase.grns.index')->with('success', 'GRN deleted successfully.');
}
public function confirm(GoodsReceiptNote $grn)
{
if ($grn->status === 'confirmed') {
return redirect()->back()->with('error', 'GRN is already confirmed.');
}
DB::transaction(function () use ($grn) {
$grn->load('items');
foreach ($grn->items as $grnItem) {
$stockLevel = StockLevel::firstOrCreate(
['item_id' => $grnItem->item_id, 'warehouse_id' => $grn->warehouse_id],
['quantity' => 0]
);
$stockLevel->increment('quantity', $grnItem->quantity_received);
StockMovement::create([
'item_id' => $grnItem->item_id,
'warehouse_id' => $grn->warehouse_id,
'type' => 'in',
'quantity' => $grnItem->quantity_received,
'reference_type' => 'GoodsReceiptNote',
'reference_id' => $grn->id,
'created_by' => auth()->id(),
]);
$grn->purchaseOrder->items()
->where('item_id', $grnItem->item_id)
->increment('quantity_received', $grnItem->quantity_received);
}
$grn->update(['status' => 'confirmed']);
// Check if all PO items have been fully received
$po = $grn->purchaseOrder->fresh(['items']);
$allFullyReceived = $po->items->every(
fn($poItem) => $poItem->quantity_received >= $poItem->quantity
);
if ($allFullyReceived) {
$po->update(['status' => 'received']);
}
});
return redirect()->back()->with('success', 'GRN confirmed and stock updated.');
}
}
@@ -0,0 +1,101 @@
<?php
namespace App\Http\Controllers\Purchase;
use App\Http\Controllers\Controller;
use App\Models\Item;
use App\Models\PurchaseOrder;
use App\Models\PurchaseOrderItem;
use App\Models\PurchaseRequest;
use App\Models\Supplier;
use Illuminate\Http\Request;
class PurchaseOrderController extends Controller
{
public function index()
{
$orders = PurchaseOrder::with('supplier')->paginate(15);
return view('purchase.orders.index', compact('orders'));
}
public function create()
{
$suppliers = Supplier::all();
$items = Item::all();
$purchaseRequests = PurchaseRequest::where('status', 'approved')->get();
return view('purchase.orders.create', compact('suppliers', 'items', 'purchaseRequests'));
}
public function store(Request $request)
{
$request->validate([
'supplier_id' => 'required|exists:suppliers,id',
'po_date' => 'required|date',
'items' => 'required|array|min:1',
'items.*.item_id' => 'required|exists:items,id',
'items.*.quantity' => 'required|numeric|min:1',
'items.*.rate' => 'required|numeric|min:0',
]);
$poNumber = 'PO-' . str_pad(PurchaseOrder::max('id') + 1, 5, '0', STR_PAD_LEFT);
$totalAmount = collect($request->items)->sum(fn($item) => $item['quantity'] * $item['rate']);
$order = PurchaseOrder::create([
'po_number' => $poNumber,
'supplier_id' => $request->supplier_id,
'po_date' => $request->po_date,
'total_amount' => $totalAmount,
'status' => 'draft',
'created_by' => auth()->id(),
]);
foreach ($request->items as $item) {
PurchaseOrderItem::create([
'purchase_order_id' => $order->id,
'item_id' => $item['item_id'],
'quantity' => $item['quantity'],
'rate' => $item['rate'],
'amount' => $item['quantity'] * $item['rate'],
]);
}
return redirect()->route('purchase.orders.show', $order)->with('success', 'Purchase order created successfully.');
}
public function show(PurchaseOrder $purchaseOrder)
{
$purchaseOrder->load(['supplier', 'items.item']);
return view('purchase.orders.show', compact('purchaseOrder'));
}
public function edit(PurchaseOrder $purchaseOrder)
{
$suppliers = Supplier::all();
$items = Item::all();
return view('purchase.orders.edit', compact('purchaseOrder', 'suppliers', 'items'));
}
public function update(Request $request, PurchaseOrder $purchaseOrder)
{
$request->validate([
'supplier_id' => 'required|exists:suppliers,id',
'po_date' => 'required|date',
]);
$purchaseOrder->update($request->only('supplier_id', 'po_date', 'status'));
return redirect()->route('purchase.orders.show', $purchaseOrder)->with('success', 'Purchase order updated successfully.');
}
public function destroy(PurchaseOrder $purchaseOrder)
{
$purchaseOrder->delete();
return redirect()->route('purchase.orders.index')->with('success', 'Purchase order deleted successfully.');
}
}
@@ -0,0 +1,50 @@
<?php
namespace App\Http\Controllers\Purchase;
use App\Http\Controllers\Controller;
use App\Models\PurchaseRequest;
use App\Models\Supplier;
use App\Services\PurchaseStageService;
class PurchasePipelineController extends Controller
{
private function withRelations()
{
return PurchaseRequest::with([
'requestedBy',
'signature.signedBy',
'rfqInvitations.supplier',
'supplierQuotes',
'awardedQuote.supplier',
]);
}
public function index(PurchaseStageService $stages)
{
$active = $this->withRelations()->where('stage', '!=', 'complete')->latest()->get();
$completed = $this->withRelations()->where('stage', 'complete')->latest()->get();
return view('purchase.pipeline.index', compact('active', 'completed', 'stages'));
}
public function show(PurchaseRequest $purchaseRequest, PurchaseStageService $stages)
{
$purchaseRequest->load([
'requestedBy',
'items',
'signature.signedBy',
'rfqInvitations.supplier',
'supplierQuotes',
'awardedQuote.supplier',
]);
$suppliers = Supplier::where('is_active', true)->orderBy('name')->get();
return view('purchase.pipeline.show', [
'pr' => $purchaseRequest,
'stages' => $stages,
'suppliers' => $suppliers,
]);
}
}
@@ -0,0 +1,173 @@
<?php
namespace App\Http\Controllers\Purchase;
use App\Http\Controllers\Controller;
use App\Models\PurchaseRequest;
use App\Models\PurchaseRequestItem;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
class PurchaseRequestController extends Controller
{
public function index()
{
$requests = PurchaseRequest::with(['requestedBy', 'items'])->latest()->paginate(15);
return view('purchase.requests.index', compact('requests'));
}
public function create()
{
return view('purchase.requests.create');
}
public function store(Request $request)
{
$request->validate([
'date' => 'required|date',
'project_name' => 'required|string|max:255',
'department' => 'nullable|string|max:255',
'requested_by_name' => 'required|string|max:255',
'required_date_text' => 'nullable|string|max:100',
'location' => 'nullable|string|max:255',
'remarks' => 'nullable|string',
'items' => 'required|array|min:1',
'items.*.description' => 'required|string',
'items.*.unit' => 'nullable|string|max:50',
'items.*.quantity_required' => 'required|numeric|min:0.01',
'items.*.purpose_use' => 'nullable|string|max:255',
'items.*.required_date' => 'nullable|date',
]);
DB::transaction(function () use ($request) {
$year = now()->format('y');
$next = PurchaseRequest::max('id') + 1;
$mprNumber = 'MPR' . $year . '-' . str_pad($next, 4, '0', STR_PAD_LEFT);
$pr = PurchaseRequest::create([
'request_number' => $mprNumber,
'date' => $request->date,
'project_name' => $request->project_name,
'department' => $request->department,
'requested_by_name' => $request->requested_by_name,
'required_date_text' => $request->required_date_text,
'location' => $request->location,
'remarks' => $request->remarks,
'status' => 'pending',
'requested_by' => auth()->id(),
]);
foreach ($request->items as $item) {
if (empty(trim($item['description']))) {
continue;
}
PurchaseRequestItem::create([
'purchase_request_id' => $pr->id,
'description' => $item['description'],
'unit' => $item['unit'] ?? null,
'quantity_required' => $item['quantity_required'],
'purpose_use' => $item['purpose_use'] ?? null,
'required_date' => $item['required_date'] ?? null,
]);
}
});
return redirect()->route('purchase.requests.index')->with('success', 'Purchase request submitted successfully.');
}
public function show(PurchaseRequest $purchaseRequest)
{
$purchaseRequest->load(['items', 'requestedBy', 'approvedBy']);
return view('purchase.requests.show', compact('purchaseRequest'));
}
public function edit(PurchaseRequest $purchaseRequest)
{
$purchaseRequest->load('items');
return view('purchase.requests.edit', compact('purchaseRequest'));
}
public function update(Request $request, PurchaseRequest $purchaseRequest)
{
$request->validate([
'date' => 'required|date',
'project_name' => 'required|string|max:255',
'department' => 'nullable|string|max:255',
'requested_by_name' => 'required|string|max:255',
'required_date_text' => 'nullable|string|max:100',
'location' => 'nullable|string|max:255',
'remarks' => 'nullable|string',
'items' => 'required|array|min:1',
'items.*.description' => 'required|string',
'items.*.unit' => 'nullable|string|max:50',
'items.*.quantity_required' => 'required|numeric|min:0.01',
'items.*.purpose_use' => 'nullable|string|max:255',
'items.*.required_date' => 'nullable|date',
]);
DB::transaction(function () use ($request, $purchaseRequest) {
$purchaseRequest->update([
'date' => $request->date,
'project_name' => $request->project_name,
'department' => $request->department,
'requested_by_name' => $request->requested_by_name,
'required_date_text' => $request->required_date_text,
'location' => $request->location,
'remarks' => $request->remarks,
]);
$purchaseRequest->items()->delete();
foreach ($request->items as $item) {
if (empty(trim($item['description']))) {
continue;
}
PurchaseRequestItem::create([
'purchase_request_id' => $purchaseRequest->id,
'description' => $item['description'],
'unit' => $item['unit'] ?? null,
'quantity_required' => $item['quantity_required'],
'purpose_use' => $item['purpose_use'] ?? null,
'required_date' => $item['required_date'] ?? null,
]);
}
});
return redirect()->route('purchase.requests.show', $purchaseRequest)->with('success', 'Purchase request updated successfully.');
}
public function destroy(PurchaseRequest $purchaseRequest)
{
$purchaseRequest->delete();
return redirect()->route('purchase.requests.index')->with('success', 'Purchase request deleted.');
}
public function approve(PurchaseRequest $purchaseRequest)
{
$purchaseRequest->update([
'status' => 'approved',
'approved_by' => auth()->id(),
'approved_at' => now(),
]);
return redirect()->back()->with('success', 'Purchase request approved.');
}
public function reject(PurchaseRequest $purchaseRequest)
{
$purchaseRequest->update(['status' => 'rejected']);
return redirect()->back()->with('success', 'Purchase request rejected.');
}
public function print(PurchaseRequest $purchaseRequest)
{
$purchaseRequest->load(['items', 'requestedBy', 'approvedBy']);
return view('purchase.requests.print', compact('purchaseRequest'));
}
}
@@ -0,0 +1,42 @@
<?php
namespace App\Http\Controllers\Purchase;
use App\Http\Controllers\Controller;
use App\Models\PurchaseRequest;
use App\Models\PurchaseSignature;
use App\Services\PurchaseStageService;
use Illuminate\Http\Request;
class PurchaseSignatureController extends Controller
{
public function show(PurchaseRequest $purchaseRequest)
{
$purchaseRequest->load('signature.signedBy');
return view('purchase.signature.show', ['request' => $purchaseRequest]);
}
public function store(Request $request, PurchaseRequest $purchaseRequest, PurchaseStageService $stages)
{
$validated = $request->validate([
'signature_image' => ['required', 'string'],
]);
if ($purchaseRequest->signature) {
return back()->with('error', 'This request has already been signed.');
}
PurchaseSignature::create([
'purchase_request_id' => $purchaseRequest->id,
'signed_by' => auth()->id(),
'signature_image' => $validated['signature_image'],
'signed_at' => now(),
'ip_address' => $request->ip(),
]);
$stages->advance($purchaseRequest);
return redirect()->route('purchase.pipeline.index')
->with('success', 'Signature saved. Request moved to RFQ stage.');
}
}
@@ -0,0 +1,117 @@
<?php
namespace App\Http\Controllers\Purchase;
use App\Http\Controllers\Controller;
use App\Models\PurchaseRequest;
use App\Models\Supplier;
use App\Services\PurchaseStageService;
use App\Services\RfqInvitationService;
use Illuminate\Http\Request;
class RfqController extends Controller
{
public function show(PurchaseRequest $purchaseRequest)
{
$suppliers = Supplier::where('is_active', true)->orderBy('name')->get();
$invitations = $purchaseRequest->rfqInvitations()->with('supplier', 'quote')->get();
return view('purchase.rfq.show', ['request' => $purchaseRequest, 'suppliers' => $suppliers, 'invitations' => $invitations]);
}
public function selectSuppliers(Request $request, PurchaseRequest $purchaseRequest, RfqInvitationService $service, PurchaseStageService $stages)
{
$mode = $request->input('mode', 'global');
$alreadySelected = $purchaseRequest->rfqInvitations()->pluck('supplier_id')->toArray();
$added = 0;
if ($mode === 'by_item') {
// item_suppliers[item_id][] = supplier_id → invert to supplier_id → [item_ids]
$itemSuppliers = $request->input('item_suppliers', []);
$supplierItems = [];
foreach ($itemSuppliers as $itemId => $supplierIds) {
foreach ((array) $supplierIds as $supplierId) {
$supplierItems[$supplierId][] = (int) $itemId;
}
}
if (empty($supplierItems)) {
return redirect()->back()->with('error', 'Please assign at least one supplier to an item.');
}
foreach ($supplierItems as $supplierId => $itemIds) {
if (in_array($supplierId, $alreadySelected)) {
continue;
}
$channel = $request->input('channel_' . $supplierId, 'email');
$supplier = Supplier::findOrFail($supplierId);
$service->select($purchaseRequest, $supplier, $channel, $itemIds);
$added++;
}
} else {
$validated = $request->validate([
'supplier_ids' => ['required', 'array', 'min:1'],
'supplier_ids.*' => ['required', 'exists:suppliers,id'],
]);
foreach ($validated['supplier_ids'] as $supplierId) {
if (in_array($supplierId, $alreadySelected)) {
continue;
}
$channel = $request->input('channel_' . $supplierId, 'email');
$supplier = Supplier::findOrFail($supplierId);
$service->select($purchaseRequest, $supplier, $channel);
$added++;
}
}
$stages->setStage($purchaseRequest, 'rfq');
return redirect()->route('purchase.pipeline.show', $purchaseRequest)
->with('success', $added . ' supplier(s) added. Now send them the quote request links.');
}
public function sendAll(PurchaseRequest $purchaseRequest, RfqInvitationService $service, PurchaseStageService $stages)
{
$pending = $purchaseRequest->rfqInvitations()->where('status', 'pending')->with('supplier')->get();
if ($pending->isEmpty()) {
return redirect()->back()->with('error', 'No unsent invitations. Select suppliers first.');
}
foreach ($pending as $invitation) {
$service->sendInvitation($invitation);
}
$stages->setStage($purchaseRequest, 'quoting');
return redirect()->route('purchase.pipeline.show', $purchaseRequest)
->with('success', $pending->count() . ' supplier(s) notified. Waiting for quotes.');
}
public function store(Request $request, PurchaseRequest $purchaseRequest, RfqInvitationService $service, PurchaseStageService $stages)
{
$validated = $request->validate([
'supplier_ids' => ['required', 'array', 'min:1'],
'supplier_ids.*' => ['required', 'exists:suppliers,id'],
]);
$alreadyInvited = $purchaseRequest->rfqInvitations()->pluck('supplier_id')->toArray();
$sent = 0;
foreach ($validated['supplier_ids'] as $supplierId) {
if (in_array($supplierId, $alreadyInvited)) {
continue;
}
$channel = $request->input('channel_' . $supplierId, 'both');
$supplier = Supplier::findOrFail($supplierId);
$service->invite($purchaseRequest, $supplier, $channel);
$sent++;
}
$stages->setStage($purchaseRequest, 'quoting');
return redirect()->route('purchase.requests.rfq', $purchaseRequest)
->with('success', $sent . ' invitation(s) sent. Waiting for supplier quotes.');
}
}
@@ -0,0 +1,100 @@
<?php
namespace App\Http\Controllers\Purchase;
use App\Http\Controllers\Controller;
use App\Models\RfqInvitation;
use App\Models\SupplierQuote;
use App\Models\SupplierQuoteItem;
use App\Services\PurchaseStageService;
use Illuminate\Http\Request;
class RfqPortalController extends Controller
{
private function resolve(string $token): RfqInvitation
{
return RfqInvitation::where('token', $token)
->with(['purchaseRequest.items', 'supplier'])
->firstOrFail();
}
public function show(string $token)
{
$invitation = $this->resolve($token);
if ($invitation->isSubmitted()) {
return view('rfq.submitted', compact('invitation'));
}
if ($invitation->isExpired()) {
return view('rfq.expired', compact('invitation'));
}
if ($invitation->status === 'sent') {
$invitation->update(['status' => 'opened', 'opened_at' => now()]);
}
$purchaseRequest = $invitation->purchaseRequest;
$items = $purchaseRequest->items;
return view('rfq.show', compact('invitation', 'purchaseRequest', 'items'));
}
public function submit(Request $request, string $token)
{
$invitation = $this->resolve($token);
if ($invitation->isSubmitted() || $invitation->isExpired()) {
abort(403, 'This link is no longer valid.');
}
$validated = $request->validate([
'lead_time_days' => ['nullable', 'integer', 'min:0'],
'payment_terms' => ['nullable', 'string', 'max:200'],
'notes' => ['nullable', 'string', 'max:1000'],
'items' => ['required', 'array'],
'items.*.unit_price' => ['required', 'numeric', 'min:0'],
]);
$purchaseItems = $invitation->purchaseRequest->items;
$quote = SupplierQuote::create([
'rfq_invitation_id' => $invitation->id,
'purchase_request_id' => $invitation->purchase_request_id,
'supplier_id' => $invitation->supplier_id,
'submitted_at' => now(),
'lead_time_days' => $validated['lead_time_days'],
'payment_terms' => $validated['payment_terms'],
'notes' => $validated['notes'],
'total_amount' => 0,
]);
$total = 0;
foreach ($purchaseItems as $i => $item) {
$unitPrice = (float)($validated['items'][$i]['unit_price'] ?? 0);
$qty = (float)$item->quantity;
$totalPrice = round($unitPrice * $qty, 3);
$total += $totalPrice;
SupplierQuoteItem::create([
'supplier_quote_id' => $quote->id,
'description' => $item->description,
'unit' => $item->unit ?? '',
'quantity' => $qty,
'unit_price' => $unitPrice,
'total_price' => $totalPrice,
]);
}
$quote->update(['total_amount' => round($total, 3)]);
$invitation->update(['status' => 'submitted']);
// If at least 1 quote is in, move to comparison stage
$pr = $invitation->purchaseRequest;
if ($pr->stage === 'quoting') {
app(PurchaseStageService::class)->setStage($pr, 'comparison');
}
return view('rfq.submitted', compact('invitation'));
}
}
@@ -0,0 +1,137 @@
<?php
namespace App\Http\Controllers\Purchase;
use App\Http\Controllers\Controller;
use App\Models\Supplier;
use App\Services\SupplierImportService;
use Barryvdh\DomPDF\Facade\Pdf;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Artisan;
class SupplierController extends Controller
{
public function index()
{
$suppliers = Supplier::orderBy('name')->get();
$stats = [
'total' => $suppliers->count(),
'active' => $suppliers->where('is_active', true)->count(),
'inactive' => $suppliers->where('is_active', false)->count(),
'with_email' => $suppliers->filter(fn($s) => !empty($s->email))->count(),
'categories' => $suppliers->whereNotNull('category')->groupBy('category')->count(),
];
$categories = Supplier::whereNotNull('category')
->where('category', '!=', '')
->distinct()
->orderBy('category')
->pluck('category');
return view('purchase.suppliers.index', compact('suppliers', 'stats', 'categories'));
}
public function create()
{
return view('purchase.suppliers.create');
}
public function store(Request $request)
{
$request->validate([
'name' => 'required|string|max:255',
'credit_days' => 'nullable|integer|min:0',
]);
Supplier::create(array_merge(
$request->only([
'supplier_code', 'name', 'category', 'contact_person',
'email', 'secondary_email', 'phone', 'phone2', 'whatsapp',
'address', 'website', 'tax_number', 'credit_terms', 'credit_days', 'remarks',
]),
['is_active' => (bool) $request->input('is_active', 1)]
));
return redirect()->route('purchase.suppliers.index')->with('success', 'Supplier created successfully.');
}
public function show(Supplier $supplier)
{
return view('purchase.suppliers.show', compact('supplier'));
}
public function edit(Supplier $supplier)
{
return view('purchase.suppliers.edit', compact('supplier'));
}
public function update(Request $request, Supplier $supplier)
{
$request->validate([
'name' => 'required|string|max:255',
'credit_days' => 'nullable|integer|min:0',
'is_active' => 'nullable',
]);
$supplier->update(array_merge(
$request->only([
'supplier_code', 'name', 'category', 'contact_person',
'email', 'secondary_email', 'phone', 'phone2', 'whatsapp',
'address', 'website', 'tax_number', 'credit_terms', 'credit_days', 'remarks',
]),
['is_active' => (bool) $request->input('is_active', 0)]
));
return redirect()->route('purchase.suppliers.index')->with('success', 'Supplier updated successfully.');
}
public function destroy(Supplier $supplier)
{
$supplier->delete();
return redirect()->route('purchase.suppliers.index')->with('success', 'Supplier deleted successfully.');
}
public function import(Request $request)
{
$request->validate([
'file' => 'required|file|mimes:xlsx,xls|max:10240',
]);
try {
$result = app(SupplierImportService::class)->import(
$request->file('file')->getPathname()
);
$msg = "Import complete: {$result['imported']} added, {$result['updated']} updated"
. ($result['skipped'] > 0 ? ", {$result['skipped']} skipped." : '.');
return redirect()->route('purchase.suppliers.index')->with('success', $msg);
} catch (\Exception $e) {
return redirect()->route('purchase.suppliers.index')
->with('error', 'Import failed: ' . $e->getMessage());
}
}
public function downloadTemplate()
{
$path = storage_path('app/suppliers_template.xlsx');
if (!file_exists($path)) {
Artisan::call('suppliers:template');
}
return response()->download($path, 'suppliers_import_template.xlsx');
}
public function exportPdf()
{
$suppliers = Supplier::orderBy('name')->get();
$pdf = Pdf::loadView('purchase.suppliers.pdf', compact('suppliers'))
->setPaper('a4', 'landscape');
return $pdf->download('suppliers_' . now()->format('Y-m-d') . '.pdf');
}
}
@@ -0,0 +1,85 @@
<?php
namespace App\Http\Controllers\Purchase;
use App\Http\Controllers\Controller;
use App\Models\GoodsReceiptNote;
use App\Models\PurchaseOrder;
use App\Models\Supplier;
use App\Models\SupplierInvoice;
use Illuminate\Http\Request;
class SupplierInvoiceController extends Controller
{
public function index()
{
$invoices = SupplierInvoice::with('supplier')->paginate(15);
return view('purchase.invoices.index', compact('invoices'));
}
public function create()
{
$suppliers = Supplier::all();
$purchaseOrders = PurchaseOrder::where('status', 'received')->with('supplier')->get();
$grns = GoodsReceiptNote::where('status', 'confirmed')->with('purchaseOrder.supplier')->get();
return view('purchase.invoices.create', compact('suppliers', 'purchaseOrders', 'grns'));
}
public function store(Request $request)
{
$request->validate([
'supplier_id' => 'required|exists:suppliers,id',
'invoice_number' => 'required|string|max:255',
'invoice_date' => 'required|date',
'subtotal' => 'required|numeric|min:0',
'vat_amount' => 'required|numeric|min:0',
'total_amount' => 'required|numeric|min:0',
]);
SupplierInvoice::create(array_merge($request->all(), [
'status' => 'unpaid',
'paid_amount' => 0,
]));
return redirect()->route('purchase.invoices.index')->with('success', 'Supplier invoice created successfully.');
}
public function show(SupplierInvoice $supplierInvoice)
{
$supplierInvoice->load(['supplier', 'purchaseOrder']);
return view('purchase.invoices.show', compact('supplierInvoice'));
}
public function edit(SupplierInvoice $supplierInvoice)
{
$suppliers = Supplier::all();
return view('purchase.invoices.edit', compact('supplierInvoice', 'suppliers'));
}
public function update(Request $request, SupplierInvoice $supplierInvoice)
{
$request->validate([
'supplier_id' => 'required|exists:suppliers,id',
'invoice_number' => 'required|string|max:255',
'invoice_date' => 'required|date',
'subtotal' => 'required|numeric|min:0',
'vat_amount' => 'required|numeric|min:0',
'total_amount' => 'required|numeric|min:0',
]);
$supplierInvoice->update($request->all());
return redirect()->route('purchase.invoices.index')->with('success', 'Supplier invoice updated successfully.');
}
public function destroy(SupplierInvoice $supplierInvoice)
{
$supplierInvoice->delete();
return redirect()->route('purchase.invoices.index')->with('success', 'Supplier invoice deleted successfully.');
}
}
@@ -0,0 +1,85 @@
<?php
namespace App\Http\Controllers\Purchase;
use App\Http\Controllers\Controller;
use App\Models\SupplierInvoice;
use App\Models\SupplierPayment;
use Illuminate\Http\Request;
class SupplierPaymentController extends Controller
{
public function index()
{
$payments = SupplierPayment::with('supplierInvoice.supplier')->paginate(15);
return view('purchase.payments.index', compact('payments'));
}
public function create()
{
$invoices = SupplierInvoice::whereIn('status', ['unpaid', 'partial'])->with('supplier')->get();
return view('purchase.payments.create', compact('invoices'));
}
public function store(Request $request)
{
$request->validate([
'supplier_invoice_id' => 'required|exists:supplier_invoices,id',
'payment_date' => 'required|date',
'amount' => 'required|numeric|min:0.01',
'payment_method' => 'required|string|max:255',
]);
$payment = SupplierPayment::create(array_merge($request->all(), [
'created_by' => auth()->id(),
]));
// Refresh invoice and recompute paid status
$invoice = SupplierInvoice::find($request->supplier_invoice_id);
$newPaidAmount = $invoice->paid_amount + $request->amount;
$status = $newPaidAmount >= $invoice->total_amount ? 'paid' : 'partial';
$invoice->update([
'paid_amount' => $newPaidAmount,
'status' => $status,
]);
return redirect()->route('purchase.payments.index')->with('success', 'Payment recorded successfully.');
}
public function show(SupplierPayment $supplierPayment)
{
$supplierPayment->load('supplierInvoice.supplier');
return view('purchase.payments.show', compact('supplierPayment'));
}
public function edit(SupplierPayment $supplierPayment)
{
$invoices = SupplierInvoice::whereIn('status', ['unpaid', 'partial'])->with('supplier')->get();
return view('purchase.payments.edit', compact('supplierPayment', 'invoices'));
}
public function update(Request $request, SupplierPayment $supplierPayment)
{
$request->validate([
'payment_date' => 'required|date',
'amount' => 'required|numeric|min:0.01',
'payment_method' => 'required|string|max:255',
]);
$supplierPayment->update($request->all());
return redirect()->route('purchase.payments.index')->with('success', 'Payment updated successfully.');
}
public function destroy(SupplierPayment $supplierPayment)
{
$supplierPayment->delete();
return redirect()->route('purchase.payments.index')->with('success', 'Payment deleted successfully.');
}
}
@@ -0,0 +1,48 @@
<?php
namespace App\Http\Controllers\Purchase;
use App\Http\Controllers\Controller;
use App\Models\PurchaseRequest;
use App\Models\SupplierQuote;
use App\Services\PurchaseStageService;
use Illuminate\Http\Request;
class SupplierQuoteController extends Controller
{
public function index(PurchaseRequest $purchaseRequest)
{
$quotes = $purchaseRequest->supplierQuotes()->with('supplier', 'items')->get();
return view('purchase.quotes.index', ['request' => $purchaseRequest, 'quotes' => $quotes]);
}
public function compare(PurchaseRequest $purchaseRequest)
{
$quotes = $purchaseRequest->supplierQuotes()->with('supplier', 'items')->get();
$items = $purchaseRequest->items;
return view('purchase.quotes.compare', ['request' => $purchaseRequest, 'quotes' => $quotes, 'items' => $items]);
}
public function award(Request $request, PurchaseRequest $purchaseRequest, SupplierQuote $quote, PurchaseStageService $stages)
{
$validated = $request->validate([
'award_reason' => ['required', 'string', 'min:5'],
]);
if ($purchaseRequest->awardedQuote) {
return back()->with('error', 'A quote has already been awarded for this request.');
}
$quote->update([
'is_awarded' => true,
'award_reason' => $validated['award_reason'],
'awarded_at' => now(),
'awarded_by' => auth()->id(),
]);
$stages->setStage($purchaseRequest, 'lpo');
return redirect()->route('purchase.pipeline.index')
->with('success', 'Quote awarded to ' . $quote->supplier->name . '. Ready to issue LPO.');
}
}