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,47 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Http\Requests\Auth\LoginRequest;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\View\View;
class AuthenticatedSessionController extends Controller
{
/**
* Display the login view.
*/
public function create(): View
{
return view('auth.login');
}
/**
* Handle an incoming authentication request.
*/
public function store(LoginRequest $request): RedirectResponse
{
$request->authenticate();
$request->session()->regenerate();
return redirect()->intended(route('dashboard', absolute: false));
}
/**
* Destroy an authenticated session.
*/
public function destroy(Request $request): RedirectResponse
{
Auth::guard('web')->logout();
$request->session()->invalidate();
$request->session()->regenerateToken();
return redirect('/');
}
}
@@ -0,0 +1,40 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Validation\ValidationException;
use Illuminate\View\View;
class ConfirmablePasswordController extends Controller
{
/**
* Show the confirm password view.
*/
public function show(): View
{
return view('auth.confirm-password');
}
/**
* Confirm the user's password.
*/
public function store(Request $request): RedirectResponse
{
if (! Auth::guard('web')->validate([
'email' => $request->user()->email,
'password' => $request->password,
])) {
throw ValidationException::withMessages([
'password' => __('auth.password'),
]);
}
$request->session()->put('auth.password_confirmed_at', time());
return redirect()->intended(route('dashboard', absolute: false));
}
}
@@ -0,0 +1,24 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
class EmailVerificationNotificationController extends Controller
{
/**
* Send a new email verification notification.
*/
public function store(Request $request): RedirectResponse
{
if ($request->user()->hasVerifiedEmail()) {
return redirect()->intended(route('dashboard', absolute: false));
}
$request->user()->sendEmailVerificationNotification();
return back()->with('status', 'verification-link-sent');
}
}
@@ -0,0 +1,21 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\View\View;
class EmailVerificationPromptController extends Controller
{
/**
* Display the email verification prompt.
*/
public function __invoke(Request $request): RedirectResponse|View
{
return $request->user()->hasVerifiedEmail()
? redirect()->intended(route('dashboard', absolute: false))
: view('auth.verify-email');
}
}
@@ -0,0 +1,63 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Models\User;
use Illuminate\Auth\Events\PasswordReset;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Password;
use Illuminate\Support\Str;
use Illuminate\Validation\Rules;
use Illuminate\Validation\ValidationException;
use Illuminate\View\View;
class NewPasswordController extends Controller
{
/**
* Display the password reset view.
*/
public function create(Request $request): View
{
return view('auth.reset-password', ['request' => $request]);
}
/**
* Handle an incoming new password request.
*
* @throws ValidationException
*/
public function store(Request $request): RedirectResponse
{
$request->validate([
'token' => ['required'],
'email' => ['required', 'email'],
'password' => ['required', 'confirmed', Rules\Password::defaults()],
]);
// Here we will attempt to reset the user's password. If it is successful we
// will update the password on an actual user model and persist it to the
// database. Otherwise we will parse the error and return the response.
$status = Password::reset(
$request->only('email', 'password', 'password_confirmation', 'token'),
function (User $user) use ($request) {
$user->forceFill([
'password' => Hash::make($request->password),
'remember_token' => Str::random(60),
])->save();
event(new PasswordReset($user));
}
);
// If the password was successfully reset, we will redirect the user back to
// the application's home authenticated view. If there is an error we can
// redirect them back to where they came from with their error message.
return $status == Password::PASSWORD_RESET
? redirect()->route('login')->with('status', __($status))
: back()->withInput($request->only('email'))
->withErrors(['email' => __($status)]);
}
}
@@ -0,0 +1,29 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\Rules\Password;
class PasswordController extends Controller
{
/**
* Update the user's password.
*/
public function update(Request $request): RedirectResponse
{
$validated = $request->validateWithBag('updatePassword', [
'current_password' => ['required', 'current_password'],
'password' => ['required', Password::defaults(), 'confirmed'],
]);
$request->user()->update([
'password' => Hash::make($validated['password']),
]);
return back()->with('status', 'password-updated');
}
}
@@ -0,0 +1,45 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Password;
use Illuminate\Validation\ValidationException;
use Illuminate\View\View;
class PasswordResetLinkController extends Controller
{
/**
* Display the password reset link request view.
*/
public function create(): View
{
return view('auth.forgot-password');
}
/**
* Handle an incoming password reset link request.
*
* @throws ValidationException
*/
public function store(Request $request): RedirectResponse
{
$request->validate([
'email' => ['required', 'email'],
]);
// We will send the password reset link to this user. Once we have attempted
// to send the link, we will examine the response then see the message we
// need to show to the user. Finally, we'll send out a proper response.
$status = Password::sendResetLink(
$request->only('email')
);
return $status == Password::RESET_LINK_SENT
? back()->with('status', __($status))
: back()->withInput($request->only('email'))
->withErrors(['email' => __($status)]);
}
}
@@ -0,0 +1,51 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Models\User;
use Illuminate\Auth\Events\Registered;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\Rules;
use Illuminate\Validation\ValidationException;
use Illuminate\View\View;
class RegisteredUserController extends Controller
{
/**
* Display the registration view.
*/
public function create(): View
{
return view('auth.register');
}
/**
* Handle an incoming registration request.
*
* @throws ValidationException
*/
public function store(Request $request): RedirectResponse
{
$request->validate([
'name' => ['required', 'string', 'max:255'],
'email' => ['required', 'string', 'lowercase', 'email', 'max:255', 'unique:'.User::class],
'password' => ['required', 'confirmed', Rules\Password::defaults()],
]);
$user = User::create([
'name' => $request->name,
'email' => $request->email,
'password' => Hash::make($request->password),
]);
event(new Registered($user));
Auth::login($user);
return redirect(route('dashboard', absolute: false));
}
}
@@ -0,0 +1,27 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Auth\Events\Verified;
use Illuminate\Foundation\Auth\EmailVerificationRequest;
use Illuminate\Http\RedirectResponse;
class VerifyEmailController extends Controller
{
/**
* Mark the authenticated user's email address as verified.
*/
public function __invoke(EmailVerificationRequest $request): RedirectResponse
{
if ($request->user()->hasVerifiedEmail()) {
return redirect()->intended(route('dashboard', absolute: false).'?verified=1');
}
if ($request->user()->markEmailAsVerified()) {
event(new Verified($request->user()));
}
return redirect()->intended(route('dashboard', absolute: false).'?verified=1');
}
}
+8
View File
@@ -0,0 +1,8 @@
<?php
namespace App\Http\Controllers;
abstract class Controller
{
//
}
@@ -0,0 +1,39 @@
<?php
namespace App\Http\Controllers;
use App\Models\Customer;
use App\Models\Item;
use App\Models\ProductionOrder;
use App\Models\PurchaseOrder;
use App\Models\PurchaseRequest;
use App\Models\SalesInvoice;
use App\Models\StockLevel;
use Illuminate\Support\Facades\DB;
class DashboardController extends Controller
{
public function index()
{
$totalSales = SalesInvoice::sum('total_amount');
$inventoryValue = StockLevel::join('items', 'items.id', '=', 'stock_levels.item_id')
->select(DB::raw('SUM(stock_levels.quantity * items.cost_price) as value'))
->value('value') ?? 0;
$productionInProgress = ProductionOrder::where('status', 'in_progress')->count();
$purchasePending = PurchaseRequest::where('stage', '!=', 'complete')->count();
$outstandingReceivables = SalesInvoice::whereIn('status', ['unpaid', 'partial'])
->sum(DB::raw('total_amount - paid_amount'));
return view('dashboard', compact(
'totalSales',
'inventoryValue',
'productionInProgress',
'purchasePending',
'outstandingReceivables'
));
}
}
@@ -0,0 +1,123 @@
<?php
namespace App\Http\Controllers\Inventory;
use App\Http\Controllers\Controller;
use App\Models\Item;
use App\Services\ItemImportService;
use Barryvdh\DomPDF\Facade\Pdf;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Artisan;
class ItemController extends Controller
{
public function index()
{
$items = Item::paginate(20);
return view('inventory.items.index', compact('items'));
}
public function create()
{
return view('inventory.items.create');
}
public function store(Request $request)
{
$request->validate([
'name' => 'required|string|max:255',
'type' => 'required|string|max:100',
'unit_of_measure' => 'required|string|max:50',
'cost_price' => 'nullable|numeric|min:0',
'selling_price' => 'nullable|numeric|min:0',
'minimum_stock_level' => 'nullable|numeric|min:0',
]);
$data = $request->all();
$data['item_code'] = 'ITEM-' . str_pad(Item::max('id') + 1, 5, '0', STR_PAD_LEFT);
Item::create($data);
return redirect()->route('inventory.items.index')->with('success', 'Item created successfully.');
}
public function show(Item $item)
{
return view('inventory.items.show', compact('item'));
}
public function edit(Item $item)
{
return view('inventory.items.edit', compact('item'));
}
public function update(Request $request, Item $item)
{
$request->validate([
'name' => 'required|string|max:255',
'type' => 'required|string|max:100',
'unit_of_measure' => 'required|string|max:50',
'cost_price' => 'nullable|numeric|min:0',
'selling_price' => 'nullable|numeric|min:0',
'minimum_stock_level' => 'nullable|numeric|min:0',
]);
$item->update($request->all());
return redirect()->route('inventory.items.index')->with('success', 'Item updated successfully.');
}
public function destroy(Item $item)
{
$item->delete();
return redirect()->route('inventory.items.index')->with('success', 'Item deleted successfully.');
}
public function import(Request $request)
{
$request->validate([
'file' => 'required|file|mimes:xlsx,xls|max:10240',
]);
try {
$result = app(ItemImportService::class)->import(
$request->file('file')->getPathname()
);
$msg = "Import complete: {$result['imported']} item(s) added";
if ($result['skipped'] > 0) {
$msg .= ", {$result['skipped']} skipped (already exist).";
} else {
$msg .= '.';
}
return redirect()->route('inventory.items.index')->with('success', $msg);
} catch (\Exception $e) {
return redirect()->route('inventory.items.index')
->with('error', 'Import failed: ' . $e->getMessage());
}
}
public function downloadTemplate()
{
$path = storage_path('app/items_template.xlsx');
if (!file_exists($path)) {
Artisan::call('items:template');
}
return response()->download($path, 'items_import_template.xlsx');
}
public function exportPdf()
{
$items = Item::orderBy('category')->orderBy('item_name')->get();
$pdf = Pdf::loadView('inventory.items.pdf', compact('items'))
->setPaper('a4', 'landscape');
return $pdf->download('items_' . now()->format('Y-m-d') . '.pdf');
}
}
@@ -0,0 +1,93 @@
<?php
namespace App\Http\Controllers\Inventory;
use App\Http\Controllers\Controller;
use App\Models\Item;
use App\Models\StockLevel;
use App\Models\StockMovement;
use App\Models\Warehouse;
use Illuminate\Http\Request;
class StockMovementController extends Controller
{
public function index()
{
$movements = StockMovement::with(['item', 'warehouse'])->latest()->paginate(20);
return view('inventory.movements.index', compact('movements'));
}
public function create()
{
$items = Item::all();
$warehouses = Warehouse::all();
return view('inventory.movements.create', compact('items', 'warehouses'));
}
public function store(Request $request)
{
$request->validate([
'item_id' => 'required|exists:items,id',
'warehouse_id' => 'required|exists:warehouses,id',
'type' => 'required|in:in,out,adjustment',
'quantity' => 'required|numeric|min:0.01',
'notes' => 'nullable|string',
]);
$stockLevel = StockLevel::firstOrCreate(
['item_id' => $request->item_id, 'warehouse_id' => $request->warehouse_id],
['quantity' => 0]
);
if ($request->type === 'in' || $request->type === 'adjustment') {
$stockLevel->increment('quantity', $request->quantity);
} else {
// Decrement but do not go below zero
$decrement = min($request->quantity, $stockLevel->quantity);
$stockLevel->decrement('quantity', $decrement);
}
StockMovement::create([
'item_id' => $request->item_id,
'warehouse_id' => $request->warehouse_id,
'type' => $request->type,
'quantity' => $request->quantity,
'notes' => $request->notes,
'created_by' => auth()->id(),
]);
return redirect()->route('inventory.movements.index')->with('success', 'Stock movement recorded successfully.');
}
public function show(StockMovement $stockMovement)
{
$stockMovement->load(['item', 'warehouse']);
return view('inventory.movements.show', compact('stockMovement'));
}
public function edit(StockMovement $stockMovement)
{
return view('inventory.movements.edit', compact('stockMovement'));
}
public function update(Request $request, StockMovement $stockMovement)
{
$request->validate([
'notes' => 'nullable|string',
]);
$stockMovement->update($request->only('notes'));
return redirect()->route('inventory.movements.index')->with('success', 'Stock movement updated successfully.');
}
public function destroy(StockMovement $stockMovement)
{
$stockMovement->delete();
return redirect()->route('inventory.movements.index')->with('success', 'Stock movement deleted successfully.');
}
}
@@ -0,0 +1,73 @@
<?php
namespace App\Http\Controllers\Inventory;
use App\Http\Controllers\Controller;
use App\Models\Item;
use App\Models\StockLevel;
use App\Models\StockMovement;
use App\Models\Warehouse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
class StockReportController extends Controller
{
public function summary()
{
$stockLevels = StockLevel::with(['item', 'warehouse'])->get();
return view('inventory.reports.summary', compact('stockLevels'));
}
public function movement(Request $request)
{
$query = StockMovement::with(['item', 'warehouse']);
if ($request->filled('from_date')) {
$query->whereDate('created_at', '>=', $request->from_date);
}
if ($request->filled('to_date')) {
$query->whereDate('created_at', '<=', $request->to_date);
}
if ($request->filled('item_id')) {
$query->where('item_id', $request->item_id);
}
$movements = $query->latest()->paginate(50)->withQueryString();
$items = Item::orderBy('item_name')->get();
return view('inventory.reports.movement', compact('movements', 'items'));
}
public function lowStock()
{
// Items whose total stock across all warehouses is below their minimum_stock_level
$items = Item::withSum('stockLevels', 'quantity')
->having(DB::raw('COALESCE(stock_levels_sum_quantity, 0)'), '<', DB::raw('minimum_stock_level'))
->get();
return view('inventory.reports.low-stock', compact('items'));
}
public function valuation()
{
$valuations = StockLevel::join('items', 'items.id', '=', 'stock_levels.item_id')
->join('warehouses', 'warehouses.id', '=', 'stock_levels.warehouse_id')
->select(
'items.id as item_id',
'items.item_name',
'items.item_code',
'warehouses.name as warehouse_name',
'stock_levels.quantity',
'items.cost_price',
DB::raw('stock_levels.quantity * items.cost_price as valuation')
)
->get();
$totalValuation = $valuations->sum('valuation');
return view('inventory.reports.valuation', compact('valuations', 'totalValuation'));
}
}
@@ -0,0 +1,63 @@
<?php
namespace App\Http\Controllers\Inventory;
use App\Http\Controllers\Controller;
use App\Models\Warehouse;
use Illuminate\Http\Request;
class WarehouseController extends Controller
{
public function index()
{
$warehouses = Warehouse::paginate(15);
return view('inventory.warehouses.index', compact('warehouses'));
}
public function create()
{
return view('inventory.warehouses.create');
}
public function store(Request $request)
{
$request->validate([
'name' => 'required|string|max:255',
'location' => 'nullable|string|max:255',
]);
Warehouse::create($request->all());
return redirect()->route('inventory.warehouses.index')->with('success', 'Warehouse created successfully.');
}
public function show(Warehouse $warehouse)
{
return view('inventory.warehouses.show', compact('warehouse'));
}
public function edit(Warehouse $warehouse)
{
return view('inventory.warehouses.edit', compact('warehouse'));
}
public function update(Request $request, Warehouse $warehouse)
{
$request->validate([
'name' => 'required|string|max:255',
'location' => 'nullable|string|max:255',
]);
$warehouse->update($request->all());
return redirect()->route('inventory.warehouses.index')->with('success', 'Warehouse updated successfully.');
}
public function destroy(Warehouse $warehouse)
{
$warehouse->delete();
return redirect()->route('inventory.warehouses.index')->with('success', 'Warehouse deleted successfully.');
}
}
@@ -0,0 +1,78 @@
<?php
namespace App\Http\Controllers\Production;
use App\Http\Controllers\Controller;
use App\Models\BillOfMaterial;
use App\Models\Item;
use Illuminate\Http\Request;
class BillOfMaterialController extends Controller
{
public function index()
{
$boms = BillOfMaterial::with(['product', 'rawMaterial'])
->get()
->groupBy('product_id');
return view('production.bom.index', compact('boms'));
}
public function create()
{
$finishedGoods = Item::where('category', 'finished_good')->get();
$rawMaterials = Item::where('category', 'raw_material')->get();
return view('production.bom.create', compact('finishedGoods', 'rawMaterials'));
}
public function store(Request $request)
{
$request->validate([
'product_id' => 'required|exists:items,id',
'raw_material_id' => 'required|exists:items,id',
'quantity_required' => 'required|numeric|min:0.01',
'unit_of_measure' => 'required|string|max:50',
]);
BillOfMaterial::create($request->all());
return redirect()->route('production.bom.index')->with('success', 'BOM entry created successfully.');
}
public function show(BillOfMaterial $billOfMaterial)
{
$billOfMaterial->load(['product', 'rawMaterial']);
return view('production.bom.show', compact('billOfMaterial'));
}
public function edit(BillOfMaterial $billOfMaterial)
{
$finishedGoods = Item::where('category', 'finished_good')->get();
$rawMaterials = Item::where('category', 'raw_material')->get();
return view('production.bom.edit', compact('billOfMaterial', 'finishedGoods', 'rawMaterials'));
}
public function update(Request $request, BillOfMaterial $billOfMaterial)
{
$request->validate([
'product_id' => 'required|exists:items,id',
'raw_material_id' => 'required|exists:items,id',
'quantity_required' => 'required|numeric|min:0.01',
'unit_of_measure' => 'required|string|max:50',
]);
$billOfMaterial->update($request->all());
return redirect()->route('production.bom.index')->with('success', 'BOM entry updated successfully.');
}
public function destroy(BillOfMaterial $billOfMaterial)
{
$billOfMaterial->delete();
return redirect()->route('production.bom.index')->with('success', 'BOM entry deleted successfully.');
}
}
@@ -0,0 +1,118 @@
<?php
namespace App\Http\Controllers\Production;
use App\Http\Controllers\Controller;
use App\Models\Item;
use App\Models\MaterialIssue;
use App\Models\ProductionOrder;
use App\Models\StockLevel;
use App\Models\StockMovement;
use App\Models\Warehouse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
class MaterialIssueController extends Controller
{
public function index()
{
$issues = MaterialIssue::with(['productionOrder', 'item', 'warehouse'])->paginate(15);
return view('production.material-issues.index', compact('issues'));
}
public function create()
{
$productionOrders = ProductionOrder::whereIn('status', ['planned', 'in_progress'])->get();
$items = Item::where('category', 'raw_material')->get();
$warehouses = Warehouse::all();
return view('production.material-issues.create', compact('productionOrders', 'items', 'warehouses'));
}
public function store(Request $request)
{
$request->validate([
'production_order_id' => 'required|exists:production_orders,id',
'item_id' => 'required|exists:items,id',
'warehouse_id' => 'required|exists:warehouses,id',
'quantity' => 'required|numeric|min:0.01',
'issue_date' => 'required|date',
]);
DB::transaction(function () use ($request) {
$issueNumber = 'MI-' . str_pad(MaterialIssue::max('id') + 1, 5, '0', STR_PAD_LEFT);
$stockLevel = StockLevel::firstOrCreate(
['item_id' => $request->item_id, 'warehouse_id' => $request->warehouse_id],
['quantity' => 0]
);
// Decrement stock, floor at zero
$decrement = min($request->quantity, $stockLevel->quantity);
$stockLevel->decrement('quantity', $decrement);
StockMovement::create([
'item_id' => $request->item_id,
'warehouse_id' => $request->warehouse_id,
'type' => 'out',
'quantity' => $request->quantity,
'reference_type' => 'MaterialIssue',
'reference_id' => null, // will be updated after create
'created_by' => auth()->id(),
]);
$issue = MaterialIssue::create([
'issue_number' => $issueNumber,
'production_order_id' => $request->production_order_id,
'item_id' => $request->item_id,
'warehouse_id' => $request->warehouse_id,
'quantity' => $request->quantity,
'issue_date' => $request->issue_date,
'issued_by' => auth()->id(),
]);
// Backfill reference_id on the movement just created
StockMovement::where('item_id', $request->item_id)
->where('warehouse_id', $request->warehouse_id)
->where('type', 'out')
->where('reference_type', 'MaterialIssue')
->whereNull('reference_id')
->latest('id')
->first()
?->update(['reference_id' => $issue->id]);
});
return redirect()->back()->with('success', 'Material issued successfully.');
}
public function show(MaterialIssue $materialIssue)
{
$materialIssue->load(['productionOrder', 'item', 'warehouse']);
return view('production.material-issues.show', compact('materialIssue'));
}
public function edit(MaterialIssue $materialIssue)
{
return view('production.material-issues.edit', compact('materialIssue'));
}
public function update(Request $request, MaterialIssue $materialIssue)
{
$request->validate([
'issue_date' => 'required|date',
]);
$materialIssue->update($request->only('issue_date', 'notes'));
return redirect()->route('production.material-issues.index')->with('success', 'Material issue updated successfully.');
}
public function destroy(MaterialIssue $materialIssue)
{
$materialIssue->delete();
return redirect()->route('production.material-issues.index')->with('success', 'Material issue deleted successfully.');
}
}
@@ -0,0 +1,94 @@
<?php
namespace App\Http\Controllers\Production;
use App\Http\Controllers\Controller;
use App\Models\Item;
use App\Models\ProductionCost;
use App\Models\ProductionOrder;
use Illuminate\Http\Request;
class ProductionOrderController extends Controller
{
public function index()
{
$orders = ProductionOrder::with('product')->paginate(15);
return view('production.orders.index', compact('orders'));
}
public function create()
{
// Only finished_good type items can be produced
$items = Item::where('category', 'finished_good')->get();
return view('production.orders.create', compact('items'));
}
public function store(Request $request)
{
$request->validate([
'product_id' => 'required|exists:items,id',
'quantity_to_produce' => 'required|numeric|min:1',
'production_date' => 'required|date',
]);
$data = $request->all();
$data['order_number'] = 'PRO-' . str_pad(ProductionOrder::max('id') + 1, 5, '0', STR_PAD_LEFT);
$data['created_by'] = auth()->id();
$data['status'] = 'planned';
$order = ProductionOrder::create($data);
return redirect()->route('production.orders.show', $order)->with('success', 'Production order created successfully.');
}
public function show(ProductionOrder $productionOrder)
{
$productionOrder->load(['product', 'materialIssues.item', 'outputs.item', 'cost']);
return view('production.orders.show', compact('productionOrder'));
}
public function edit(ProductionOrder $productionOrder)
{
$items = Item::where('category', 'finished_good')->get();
return view('production.orders.edit', compact('productionOrder', 'items'));
}
public function update(Request $request, ProductionOrder $productionOrder)
{
$request->validate([
'production_date' => 'required|date',
]);
$productionOrder->update($request->only('production_date', 'notes'));
return redirect()->route('production.orders.show', $productionOrder)->with('success', 'Production order updated successfully.');
}
public function destroy(ProductionOrder $productionOrder)
{
$productionOrder->delete();
return redirect()->route('production.orders.index')->with('success', 'Production order deleted successfully.');
}
public function start(ProductionOrder $productionOrder)
{
$productionOrder->update(['status' => 'in_progress']);
return redirect()->back()->with('success', 'Production order started.');
}
public function complete(ProductionOrder $productionOrder)
{
$productionOrder->update([
'status' => 'completed',
'completion_date' => now(),
]);
return redirect()->back()->with('success', 'Production order marked as completed.');
}
}
@@ -0,0 +1,105 @@
<?php
namespace App\Http\Controllers\Production;
use App\Http\Controllers\Controller;
use App\Models\Item;
use App\Models\ProductionOrder;
use App\Models\ProductionOutput;
use App\Models\StockLevel;
use App\Models\StockMovement;
use App\Models\Warehouse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
class ProductionOutputController extends Controller
{
public function index()
{
$outputs = ProductionOutput::with(['productionOrder', 'item', 'warehouse'])->paginate(15);
return view('production.outputs.index', compact('outputs'));
}
public function create()
{
$productionOrders = ProductionOrder::whereIn('status', ['in_progress'])->with('product')->get();
$warehouses = Warehouse::all();
return view('production.outputs.create', compact('productionOrders', 'warehouses'));
}
public function store(Request $request)
{
$request->validate([
'production_order_id' => 'required|exists:production_orders,id',
'item_id' => 'required|exists:items,id',
'warehouse_id' => 'required|exists:warehouses,id',
'quantity' => 'required|numeric|min:0.01',
'output_date' => 'required|date',
]);
DB::transaction(function () use ($request) {
$stockLevel = StockLevel::firstOrCreate(
['item_id' => $request->item_id, 'warehouse_id' => $request->warehouse_id],
['quantity' => 0]
);
$stockLevel->increment('quantity', $request->quantity);
$output = ProductionOutput::create([
'production_order_id' => $request->production_order_id,
'item_id' => $request->item_id,
'warehouse_id' => $request->warehouse_id,
'quantity' => $request->quantity,
'output_date' => $request->output_date,
'recorded_by' => auth()->id(),
]);
StockMovement::create([
'item_id' => $request->item_id,
'warehouse_id' => $request->warehouse_id,
'type' => 'in',
'quantity' => $request->quantity,
'reference_type' => 'ProductionOutput',
'reference_id' => $output->id,
'created_by' => auth()->id(),
]);
// Accumulate quantity_produced on the production order
ProductionOrder::where('id', $request->production_order_id)
->increment('quantity_produced', $request->quantity);
});
return redirect()->back()->with('success', 'Production output recorded and stock updated.');
}
public function show(ProductionOutput $productionOutput)
{
$productionOutput->load(['productionOrder', 'item', 'warehouse']);
return view('production.outputs.show', compact('productionOutput'));
}
public function edit(ProductionOutput $productionOutput)
{
return view('production.outputs.edit', compact('productionOutput'));
}
public function update(Request $request, ProductionOutput $productionOutput)
{
$request->validate([
'output_date' => 'required|date',
]);
$productionOutput->update($request->only('output_date', 'notes'));
return redirect()->route('production.outputs.index')->with('success', 'Production output updated successfully.');
}
public function destroy(ProductionOutput $productionOutput)
{
$productionOutput->delete();
return redirect()->route('production.outputs.index')->with('success', 'Production output deleted successfully.');
}
}
@@ -0,0 +1,60 @@
<?php
namespace App\Http\Controllers;
use App\Http\Requests\ProfileUpdateRequest;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Redirect;
use Illuminate\View\View;
class ProfileController extends Controller
{
/**
* Display the user's profile form.
*/
public function edit(Request $request): View
{
return view('profile.edit', [
'user' => $request->user(),
]);
}
/**
* Update the user's profile information.
*/
public function update(ProfileUpdateRequest $request): RedirectResponse
{
$request->user()->fill($request->validated());
if ($request->user()->isDirty('email')) {
$request->user()->email_verified_at = null;
}
$request->user()->save();
return Redirect::route('profile.edit')->with('status', 'profile-updated');
}
/**
* Delete the user's account.
*/
public function destroy(Request $request): RedirectResponse
{
$request->validateWithBag('userDeletion', [
'password' => ['required', 'current_password'],
]);
$user = $request->user();
Auth::logout();
$user->delete();
$request->session()->invalidate();
$request->session()->regenerateToken();
return Redirect::to('/');
}
}
@@ -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.');
}
}
@@ -0,0 +1,67 @@
<?php
namespace App\Http\Controllers\Sales;
use App\Http\Controllers\Controller;
use App\Models\Customer;
use Illuminate\Http\Request;
class CustomerController extends Controller
{
public function index()
{
$customers = Customer::paginate(15);
return view('sales.customers.index', compact('customers'));
}
public function create()
{
return view('sales.customers.create');
}
public function store(Request $request)
{
$request->validate([
'name' => 'required|string|max:255',
'email' => 'nullable|email|max:255',
'phone' => 'nullable|string|max:50',
'address' => 'nullable|string',
]);
Customer::create($request->all());
return redirect()->route('sales.customers.index')->with('success', 'Customer created successfully.');
}
public function show(Customer $customer)
{
return view('sales.customers.show', compact('customer'));
}
public function edit(Customer $customer)
{
return view('sales.customers.edit', compact('customer'));
}
public function update(Request $request, Customer $customer)
{
$request->validate([
'name' => 'required|string|max:255',
'email' => 'nullable|email|max:255',
'phone' => 'nullable|string|max:50',
'address' => 'nullable|string',
]);
$customer->update($request->all());
return redirect()->route('sales.customers.index')->with('success', 'Customer updated successfully.');
}
public function destroy(Customer $customer)
{
$customer->delete();
return redirect()->route('sales.customers.index')->with('success', 'Customer deleted successfully.');
}
}
@@ -0,0 +1,150 @@
<?php
namespace App\Http\Controllers\Sales;
use App\Http\Controllers\Controller;
use App\Models\Customer;
use App\Models\DeliveryNote;
use App\Models\DeliveryNoteItem;
use App\Models\SalesOrder;
use App\Models\SalesOrderItem;
use App\Models\StockLevel;
use App\Models\StockMovement;
use App\Models\Warehouse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
class DeliveryNoteController extends Controller
{
public function index()
{
$notes = DeliveryNote::with(['salesOrder.customer', 'warehouse'])->paginate(15);
return view('sales.delivery-notes.index', compact('notes'));
}
public function create()
{
$salesOrders = SalesOrder::where('status', 'confirmed')->with('customer')->get();
$warehouses = Warehouse::all();
return view('sales.delivery-notes.create', compact('salesOrders', 'warehouses'));
}
public function store(Request $request)
{
$request->validate([
'sales_order_id' => 'required|exists:sales_orders,id',
'warehouse_id' => 'required|exists:warehouses,id',
'delivery_date' => 'required|date',
'items' => 'required|array|min:1',
'items.*.item_id' => 'required|exists:items,id',
'items.*.quantity' => 'required|numeric|min:1',
]);
$deliveryNumber = 'DN-' . str_pad(DeliveryNote::max('id') + 1, 5, '0', STR_PAD_LEFT);
$note = DeliveryNote::create([
'delivery_number' => $deliveryNumber,
'sales_order_id' => $request->sales_order_id,
'warehouse_id' => $request->warehouse_id,
'delivery_date' => $request->delivery_date,
'status' => 'draft',
]);
$salesOrder = SalesOrder::with('items')->findOrFail($request->sales_order_id);
foreach ($request->items as $item) {
$soItem = $salesOrder->items->firstWhere('item_id', $item['item_id']);
DeliveryNoteItem::create([
'delivery_note_id' => $note->id,
'sales_order_item_id' => $soItem?->id ?? 0,
'item_id' => $item['item_id'],
'quantity_delivered' => $item['quantity'],
]);
}
return redirect()->route('sales.delivery-notes.show', $note)->with('success', 'Delivery note created successfully.');
}
public function show(DeliveryNote $deliveryNote)
{
$deliveryNote->load(['salesOrder.customer', 'warehouse', 'items.item']);
return view('sales.delivery-notes.show', compact('deliveryNote'));
}
public function edit(DeliveryNote $deliveryNote)
{
$warehouses = Warehouse::all();
return view('sales.delivery-notes.edit', compact('deliveryNote', 'warehouses'));
}
public function update(Request $request, DeliveryNote $deliveryNote)
{
$request->validate([
'delivery_date' => 'required|date',
'warehouse_id' => 'required|exists:warehouses,id',
]);
$deliveryNote->update($request->only('delivery_date', 'warehouse_id'));
return redirect()->route('sales.delivery-notes.show', $deliveryNote)->with('success', 'Delivery note updated successfully.');
}
public function destroy(DeliveryNote $deliveryNote)
{
$deliveryNote->delete();
return redirect()->route('sales.delivery-notes.index')->with('success', 'Delivery note deleted successfully.');
}
public function dispatch(DeliveryNote $deliveryNote)
{
DB::transaction(function () use ($deliveryNote) {
$deliveryNote->load(['items', 'salesOrder.items']);
foreach ($deliveryNote->items as $dnItem) {
$stockLevel = StockLevel::firstOrCreate(
['item_id' => $dnItem->item_id, 'warehouse_id' => $deliveryNote->warehouse_id],
['quantity' => 0]
);
$decrement = min($dnItem->quantity_delivered, $stockLevel->quantity);
$stockLevel->decrement('quantity', $decrement);
StockMovement::create([
'item_id' => $dnItem->item_id,
'warehouse_id' => $deliveryNote->warehouse_id,
'type' => 'out',
'quantity' => $dnItem->quantity_delivered,
'reference_type' => 'DeliveryNote',
'reference_id' => $deliveryNote->id,
'created_by' => auth()->id(),
]);
SalesOrderItem::where('sales_order_id', $deliveryNote->sales_order_id)
->where('item_id', $dnItem->item_id)
->increment('quantity_delivered', $dnItem->quantity_delivered);
}
$deliveryNote->update([
'status' => 'dispatched',
'dispatched_by' => auth()->id(),
]);
// Check if all sales order items are fully delivered
$salesOrder = $deliveryNote->salesOrder->fresh(['items']);
$allFullyDelivered = $salesOrder->items->every(
fn($soItem) => $soItem->quantity_delivered >= $soItem->quantity
);
if ($allFullyDelivered) {
$salesOrder->update(['status' => 'dispatched']);
}
});
return redirect()->back()->with('success', 'Delivery note dispatched and stock decremented.');
}
}
@@ -0,0 +1,86 @@
<?php
namespace App\Http\Controllers\Sales;
use App\Http\Controllers\Controller;
use App\Models\Customer;
use App\Models\PaymentReceipt;
use App\Models\SalesInvoice;
use Illuminate\Http\Request;
class PaymentReceiptController extends Controller
{
public function index()
{
$receipts = PaymentReceipt::with('salesInvoice.customer')->paginate(15);
return view('sales.payments.index', compact('receipts'));
}
public function create()
{
$invoices = SalesInvoice::whereIn('status', ['unpaid', 'partial'])->with('customer')->get();
$customers = Customer::all();
return view('sales.payments.create', compact('invoices', 'customers'));
}
public function store(Request $request)
{
$request->validate([
'sales_invoice_id' => 'required|exists:sales_invoices,id',
'receipt_date' => 'required|date',
'amount' => 'required|numeric|min:0.01',
'payment_method' => 'required|string|max:255',
]);
$receipt = PaymentReceipt::create(array_merge($request->all(), [
'created_by' => auth()->id(),
]));
$invoice = SalesInvoice::find($request->sales_invoice_id);
$newPaidAmount = $invoice->paid_amount + $request->amount;
$status = $newPaidAmount >= $invoice->total_amount ? 'paid' : 'partial';
$invoice->update([
'paid_amount' => $newPaidAmount,
'status' => $status,
]);
// Reduce the customer's outstanding balance by the amount received
$invoice->customer()->decrement('outstanding_balance', $request->amount);
return redirect()->route('sales.payments.index')->with('success', 'Payment receipt recorded successfully.');
}
public function show(PaymentReceipt $paymentReceipt)
{
$paymentReceipt->load('salesInvoice.customer');
return view('sales.payments.show', compact('paymentReceipt'));
}
public function edit(PaymentReceipt $paymentReceipt)
{
return view('sales.payments.edit', compact('paymentReceipt'));
}
public function update(Request $request, PaymentReceipt $paymentReceipt)
{
$request->validate([
'receipt_date' => 'required|date',
'payment_method' => 'required|string|max:255',
]);
$paymentReceipt->update($request->only('receipt_date', 'payment_method', 'notes'));
return redirect()->route('sales.payments.index')->with('success', 'Payment receipt updated successfully.');
}
public function destroy(PaymentReceipt $paymentReceipt)
{
$paymentReceipt->delete();
return redirect()->route('sales.payments.index')->with('success', 'Payment receipt deleted successfully.');
}
}
@@ -0,0 +1,89 @@
<?php
namespace App\Http\Controllers\Sales;
use App\Http\Controllers\Controller;
use App\Models\Customer;
use App\Models\SalesInvoice;
use App\Models\SalesOrder;
use Illuminate\Http\Request;
class SalesInvoiceController extends Controller
{
public function index()
{
$invoices = SalesInvoice::with('customer')->paginate(15);
return view('sales.invoices.index', compact('invoices'));
}
public function create()
{
$salesOrders = SalesOrder::where('status', 'dispatched')->with('customer')->get();
$customers = Customer::all();
return view('sales.invoices.create', compact('salesOrders', 'customers'));
}
public function store(Request $request)
{
$request->validate([
'sales_order_id' => 'required|exists:sales_orders,id',
'customer_id' => 'required|exists:customers,id',
'invoice_date' => 'required|date',
'subtotal' => 'required|numeric|min:0',
'vat_rate' => 'required|numeric|min:0',
'vat_amount' => 'required|numeric|min:0',
'total_amount' => 'required|numeric|min:0',
]);
$invoiceNumber = 'INV-' . str_pad(SalesInvoice::max('id') + 1, 5, '0', STR_PAD_LEFT);
SalesInvoice::create(array_merge($request->all(), [
'invoice_number' => $invoiceNumber,
'status' => 'unpaid',
'paid_amount' => 0,
'created_by' => auth()->id(),
]));
SalesOrder::where('id', $request->sales_order_id)->update(['status' => 'invoiced']);
return redirect()->route('sales.invoices.index')->with('success', 'Invoice created successfully.');
}
public function show(SalesInvoice $salesInvoice)
{
$salesInvoice->load(['customer', 'salesOrder', 'paymentReceipts']);
return view('sales.invoices.show', compact('salesInvoice'));
}
public function edit(SalesInvoice $salesInvoice)
{
$customers = Customer::all();
return view('sales.invoices.edit', compact('salesInvoice', 'customers'));
}
public function update(Request $request, SalesInvoice $salesInvoice)
{
$request->validate([
'invoice_date' => 'required|date',
'subtotal' => 'required|numeric|min:0',
'vat_rate' => 'required|numeric|min:0',
'vat_amount' => 'required|numeric|min:0',
'total_amount' => 'required|numeric|min:0',
]);
$salesInvoice->update($request->all());
return redirect()->route('sales.invoices.show', $salesInvoice)->with('success', 'Invoice updated successfully.');
}
public function destroy(SalesInvoice $salesInvoice)
{
$salesInvoice->delete();
return redirect()->route('sales.invoices.index')->with('success', 'Invoice deleted successfully.');
}
}
@@ -0,0 +1,106 @@
<?php
namespace App\Http\Controllers\Sales;
use App\Http\Controllers\Controller;
use App\Models\Customer;
use App\Models\Item;
use App\Models\SalesOrder;
use App\Models\SalesOrderItem;
use Illuminate\Http\Request;
class SalesOrderController extends Controller
{
public function index()
{
$orders = SalesOrder::with('customer')->paginate(15);
return view('sales.orders.index', compact('orders'));
}
public function create()
{
$customers = Customer::all();
$items = Item::where('category', 'finished_good')->get();
return view('sales.orders.create', compact('customers', 'items'));
}
public function store(Request $request)
{
$request->validate([
'customer_id' => 'required|exists:customers,id',
'order_date' => 'required|date',
'items' => 'required|array|min:1',
'items.*.item_id' => 'required|exists:items,id',
'items.*.quantity' => 'required|numeric|min:1',
'items.*.unit_price' => 'required|numeric|min:0',
]);
$orderNumber = 'SO-' . str_pad(SalesOrder::max('id') + 1, 5, '0', STR_PAD_LEFT);
$totalAmount = collect($request->items)->sum(fn($item) => $item['quantity'] * $item['unit_price']);
$order = SalesOrder::create([
'order_number' => $orderNumber,
'customer_id' => $request->customer_id,
'order_date' => $request->order_date,
'total_amount' => $totalAmount,
'status' => 'draft',
'created_by' => auth()->id(),
]);
foreach ($request->items as $item) {
SalesOrderItem::create([
'sales_order_id' => $order->id,
'item_id' => $item['item_id'],
'quantity' => $item['quantity'],
'unit_price' => $item['unit_price'],
'amount' => $item['quantity'] * $item['unit_price'],
'quantity_delivered' => 0,
]);
}
return redirect()->route('sales.orders.show', $order)->with('success', 'Sales order created successfully.');
}
public function show(SalesOrder $salesOrder)
{
$salesOrder->load(['customer', 'items.item', 'deliveryNotes', 'invoices']);
return view('sales.orders.show', compact('salesOrder'));
}
public function edit(SalesOrder $salesOrder)
{
$customers = Customer::all();
$items = Item::where('category', 'finished_good')->get();
return view('sales.orders.edit', compact('salesOrder', 'customers', 'items'));
}
public function update(Request $request, SalesOrder $salesOrder)
{
$request->validate([
'customer_id' => 'required|exists:customers,id',
'order_date' => 'required|date',
]);
$salesOrder->update($request->only('customer_id', 'order_date', 'notes'));
return redirect()->route('sales.orders.show', $salesOrder)->with('success', 'Sales order updated successfully.');
}
public function destroy(SalesOrder $salesOrder)
{
$salesOrder->delete();
return redirect()->route('sales.orders.index')->with('success', 'Sales order deleted successfully.');
}
public function confirm(SalesOrder $salesOrder)
{
$salesOrder->update(['status' => 'confirmed']);
return redirect()->back()->with('success', 'Sales order confirmed.');
}
}
+86
View File
@@ -0,0 +1,86 @@
<?php
namespace App\Http\Requests\Auth;
use Illuminate\Auth\Events\Lockout;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\Str;
use Illuminate\Validation\ValidationException;
class LoginRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'email' => ['required', 'string', 'email'],
'password' => ['required', 'string'],
];
}
/**
* Attempt to authenticate the request's credentials.
*
* @throws ValidationException
*/
public function authenticate(): void
{
$this->ensureIsNotRateLimited();
if (! Auth::attempt($this->only('email', 'password'), $this->boolean('remember'))) {
RateLimiter::hit($this->throttleKey());
throw ValidationException::withMessages([
'email' => trans('auth.failed'),
]);
}
RateLimiter::clear($this->throttleKey());
}
/**
* Ensure the login request is not rate limited.
*
* @throws ValidationException
*/
public function ensureIsNotRateLimited(): void
{
if (! RateLimiter::tooManyAttempts($this->throttleKey(), 5)) {
return;
}
event(new Lockout($this));
$seconds = RateLimiter::availableIn($this->throttleKey());
throw ValidationException::withMessages([
'email' => trans('auth.throttle', [
'seconds' => $seconds,
'minutes' => ceil($seconds / 60),
]),
]);
}
/**
* Get the rate limiting throttle key for the request.
*/
public function throttleKey(): string
{
return Str::transliterate(Str::lower($this->string('email')).'|'.$this->ip());
}
}
@@ -0,0 +1,31 @@
<?php
namespace App\Http\Requests;
use App\Models\User;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class ProfileUpdateRequest extends FormRequest
{
/**
* Get the validation rules that apply to the request.
*
* @return array<string, ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'name' => ['required', 'string', 'max:255'],
'email' => [
'required',
'string',
'lowercase',
'email',
'max:255',
Rule::unique(User::class)->ignore($this->user()->id),
],
];
}
}