diff --git a/app/Exports/CitiesExport.php b/app/Exports/CitiesExport.php index 628ec10..dd18807 100644 --- a/app/Exports/CitiesExport.php +++ b/app/Exports/CitiesExport.php @@ -2,15 +2,14 @@ namespace App\Exports; -use App\Models\City; use Maatwebsite\Excel\Concerns\FromCollection; +use Maatwebsite\Excel\Concerns\ShouldAutoSize; use Maatwebsite\Excel\Concerns\WithHeadings; use Maatwebsite\Excel\Concerns\WithMapping; -use Maatwebsite\Excel\Concerns\ShouldAutoSize; use Maatwebsite\Excel\Concerns\WithStyles; use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; -class CitiesExport implements FromCollection, WithHeadings, WithMapping, ShouldAutoSize, WithStyles +class CitiesExport implements FromCollection, ShouldAutoSize, WithHeadings, WithMapping, WithStyles { protected $cities; @@ -20,8 +19,8 @@ public function __construct($cities) } /** - * @return \Illuminate\Support\Collection - */ + * @return \Illuminate\Support\Collection + */ public function collection() { return $this->cities; @@ -55,7 +54,7 @@ public function styles(Worksheet $sheet) { return [ // Style the first row as bold text. - 1 => ['font' => ['bold' => true]], + 1 => ['font' => ['bold' => true]], ]; } } diff --git a/app/Exports/CustomersExport.php b/app/Exports/CustomersExport.php index 7e251c6..eedfe97 100644 --- a/app/Exports/CustomersExport.php +++ b/app/Exports/CustomersExport.php @@ -2,15 +2,14 @@ namespace App\Exports; -use App\Models\Customer; use Maatwebsite\Excel\Concerns\FromCollection; +use Maatwebsite\Excel\Concerns\ShouldAutoSize; use Maatwebsite\Excel\Concerns\WithHeadings; use Maatwebsite\Excel\Concerns\WithMapping; -use Maatwebsite\Excel\Concerns\ShouldAutoSize; use Maatwebsite\Excel\Concerns\WithStyles; use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; -class CustomersExport implements FromCollection, WithHeadings, WithMapping, ShouldAutoSize, WithStyles +class CustomersExport implements FromCollection, ShouldAutoSize, WithHeadings, WithMapping, WithStyles { protected $customers; @@ -20,8 +19,8 @@ public function __construct($customers) } /** - * @return \Illuminate\Support\Collection - */ + * @return \Illuminate\Support\Collection + */ public function collection() { return $this->customers; @@ -67,7 +66,7 @@ public function styles(Worksheet $sheet) { return [ // Style the first row as bold text. - 1 => ['font' => ['bold' => true]], + 1 => ['font' => ['bold' => true]], ]; } } diff --git a/app/Exports/ProductTemplateExport.php b/app/Exports/ProductTemplateExport.php index ffd2292..76dfd45 100644 --- a/app/Exports/ProductTemplateExport.php +++ b/app/Exports/ProductTemplateExport.php @@ -2,18 +2,18 @@ namespace App\Exports; +use App\Models\Category; +use App\Models\Unit; use Maatwebsite\Excel\Concerns\FromCollection; +use Maatwebsite\Excel\Concerns\WithEvents; use Maatwebsite\Excel\Concerns\WithHeadings; -use Maatwebsite\Excel\Concerns\WithTitle; use Maatwebsite\Excel\Concerns\WithStyles; -use Maatwebsite\Excel\Concerns\WithEvents; +use Maatwebsite\Excel\Concerns\WithTitle; use Maatwebsite\Excel\Events\AfterSheet; -use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; use PhpOffice\PhpSpreadsheet\Cell\DataValidation; -use App\Models\Category; -use App\Models\Unit; +use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; -class ProductTemplateExport implements FromCollection, WithHeadings, WithTitle, WithStyles, WithEvents +class ProductTemplateExport implements FromCollection, WithEvents, WithHeadings, WithStyles, WithTitle { public function collection() { @@ -31,8 +31,8 @@ public function collection() '3000.00', // Limit Price '50', // Quantity '5', // Alert Quantity - 'https://example.com/image.jpg' // Image URL - ] + 'https://example.com/image.jpg', // Image URL + ], ]); } @@ -50,7 +50,7 @@ public function headings(): array 'Limit Price', 'Quantity*', 'Alert Quantity', - 'Image URL' + 'Image URL', ]; } @@ -65,16 +65,16 @@ public function styles(Worksheet $sheet) 1 => ['font' => ['bold' => true]], ]; } - + public function registerEvents(): array { return [ - AfterSheet::class => function(AfterSheet $event) { + AfterSheet::class => function (AfterSheet $event) { $sheet = $event->sheet; - + // 1. Category Dropdown (Column B) $categories = Category::pluck('name')->toArray(); - if(!empty($categories)) { + if (! empty($categories)) { $catValidation = $sheet->getCell('B2')->getDataValidation(); $catValidation->setType(DataValidation::TYPE_LIST); $catValidation->setErrorStyle(DataValidation::STYLE_INFORMATION); @@ -82,8 +82,8 @@ public function registerEvents(): array $catValidation->setShowInputMessage(true); $catValidation->setShowErrorMessage(true); $catValidation->setShowDropDown(true); - $catValidation->setFormula1('"' . implode(',', $categories) . '"'); - + $catValidation->setFormula1('"'.implode(',', $categories).'"'); + // Apply to 100 rows for ($i = 2; $i <= 100; $i++) { $sheet->getCell("B$i")->setDataValidation(clone $catValidation); @@ -92,30 +92,30 @@ public function registerEvents(): array // 2. Sub Category Dropdown (Column C) $subCategories = \App\Models\SubCategory::pluck('name')->toArray(); - if(!empty($subCategories)) { + if (! empty($subCategories)) { $subCatValidation = $sheet->getCell('C2')->getDataValidation(); $subCatValidation->setType(DataValidation::TYPE_LIST); $subCatValidation->setErrorStyle(DataValidation::STYLE_INFORMATION); $subCatValidation->setAllowBlank(true); $subCatValidation->setShowDropDown(true); - $subCatValidation->setFormula1('"' . implode(',', $subCategories) . '"'); - - for ($i = 2; $i <= 100; $i++) { + $subCatValidation->setFormula1('"'.implode(',', $subCategories).'"'); + + for ($i = 2; $i <= 100; $i++) { $sheet->getCell("C$i")->setDataValidation(clone $subCatValidation); } } // 3. Unit Dropdown (Column E) $units = Unit::pluck('name')->toArray(); - if(!empty($units)) { + if (! empty($units)) { $unitValidation = $sheet->getCell('E2')->getDataValidation(); $unitValidation->setType(DataValidation::TYPE_LIST); $unitValidation->setErrorStyle(DataValidation::STYLE_INFORMATION); $unitValidation->setAllowBlank(false); $unitValidation->setShowDropDown(true); - $unitValidation->setFormula1('"' . implode(',', $units) . '"'); - - for ($i = 2; $i <= 100; $i++) { + $unitValidation->setFormula1('"'.implode(',', $units).'"'); + + for ($i = 2; $i <= 100; $i++) { $sheet->getCell("E$i")->setDataValidation(clone $unitValidation); } } diff --git a/app/Exports/ProductsExport.php b/app/Exports/ProductsExport.php index eb333fd..fe61f0e 100644 --- a/app/Exports/ProductsExport.php +++ b/app/Exports/ProductsExport.php @@ -4,11 +4,11 @@ use App\Models\Product; use Maatwebsite\Excel\Concerns\FromCollection; +use Maatwebsite\Excel\Concerns\ShouldAutoSize; use Maatwebsite\Excel\Concerns\WithHeadings; use Maatwebsite\Excel\Concerns\WithMapping; -use Maatwebsite\Excel\Concerns\ShouldAutoSize; -class ProductsExport implements FromCollection, WithHeadings, WithMapping, ShouldAutoSize +class ProductsExport implements FromCollection, ShouldAutoSize, WithHeadings, WithMapping { protected $request; @@ -18,8 +18,8 @@ public function __construct($request) } /** - * @return \Illuminate\Support\Collection - */ + * @return \Illuminate\Support\Collection + */ public function collection() { $query = \App\Models\ProductVariant::query()->with(['product.category', 'product.subCategory', 'unit']); @@ -32,17 +32,17 @@ public function collection() if (isset($this->request['search']) && $this->request['search']) { $search = $this->request['search']; - $query->where(function($q) use ($search) { - $q->whereHas('product', function($pq) use ($search) { + $query->where(function ($q) use ($search) { + $q->whereHas('product', function ($pq) use ($search) { $pq->where('name', 'like', "%{$search}%") - ->orWhere('barcode_data', 'like', "%{$search}%"); + ->orWhere('barcode_data', 'like', "%{$search}%"); }) - ->orWhere('sku', 'like', "%{$search}%"); + ->orWhere('sku', 'like', "%{$search}%"); }); } if (isset($this->request['category_id']) && $this->request['category_id']) { - $query->whereHas('product', function($q) { + $query->whereHas('product', function ($q) { $q->where('category_id', $this->request['category_id']); }); } @@ -64,7 +64,7 @@ public function headings(): array 'Limit Price', 'Quantity', 'Alert Quantity', - 'Product Created At' + 'Product Created At', ]; } diff --git a/app/Exports/ResellerPaymentTemplateExport.php b/app/Exports/ResellerPaymentTemplateExport.php index b571122..2261285 100644 --- a/app/Exports/ResellerPaymentTemplateExport.php +++ b/app/Exports/ResellerPaymentTemplateExport.php @@ -2,17 +2,17 @@ namespace App\Exports; +use App\Models\Reseller; use Maatwebsite\Excel\Concerns\FromCollection; +use Maatwebsite\Excel\Concerns\ShouldAutoSize; use Maatwebsite\Excel\Concerns\WithHeadings; use Maatwebsite\Excel\Concerns\WithMapping; -use Maatwebsite\Excel\Concerns\ShouldAutoSize; -use App\Models\Reseller; -class ResellerPaymentTemplateExport implements FromCollection, WithHeadings, WithMapping, ShouldAutoSize +class ResellerPaymentTemplateExport implements FromCollection, ShouldAutoSize, WithHeadings, WithMapping { /** - * @return \Illuminate\Support\Collection - */ + * @return \Illuminate\Support\Collection + */ public function collection() { return Reseller::select('id', 'name', 'due_amount')->orderBy('name')->get(); @@ -27,7 +27,7 @@ public function headings(): array 'Payment Amount', 'Payment Method (cash/bank/other)', 'Reference', - 'Date (YYYY-MM-DD)' + 'Date (YYYY-MM-DD)', ]; } diff --git a/app/Exports/ResellersExport.php b/app/Exports/ResellersExport.php index a0c3e17..c3bf4af 100644 --- a/app/Exports/ResellersExport.php +++ b/app/Exports/ResellersExport.php @@ -2,15 +2,14 @@ namespace App\Exports; -use App\Models\Reseller; use Maatwebsite\Excel\Concerns\FromCollection; +use Maatwebsite\Excel\Concerns\ShouldAutoSize; use Maatwebsite\Excel\Concerns\WithHeadings; use Maatwebsite\Excel\Concerns\WithMapping; -use Maatwebsite\Excel\Concerns\ShouldAutoSize; use Maatwebsite\Excel\Concerns\WithStyles; use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; -class ResellersExport implements FromCollection, WithHeadings, WithMapping, ShouldAutoSize, WithStyles +class ResellersExport implements FromCollection, ShouldAutoSize, WithHeadings, WithMapping, WithStyles { protected $resellers; @@ -20,8 +19,8 @@ public function __construct($resellers) } /** - * @return \Illuminate\Support\Collection - */ + * @return \Illuminate\Support\Collection + */ public function collection() { return $this->resellers; @@ -69,7 +68,7 @@ public function styles(Worksheet $sheet) { return [ // Style the first row as bold text. - 1 => ['font' => ['bold' => true]], + 1 => ['font' => ['bold' => true]], ]; } } diff --git a/app/Exports/SuppliersExport.php b/app/Exports/SuppliersExport.php index 46a1281..d8642bc 100644 --- a/app/Exports/SuppliersExport.php +++ b/app/Exports/SuppliersExport.php @@ -2,15 +2,14 @@ namespace App\Exports; -use App\Models\Supplier; use Maatwebsite\Excel\Concerns\FromCollection; +use Maatwebsite\Excel\Concerns\ShouldAutoSize; use Maatwebsite\Excel\Concerns\WithHeadings; use Maatwebsite\Excel\Concerns\WithMapping; -use Maatwebsite\Excel\Concerns\ShouldAutoSize; use Maatwebsite\Excel\Concerns\WithStyles; use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; -class SuppliersExport implements FromCollection, WithHeadings, WithMapping, ShouldAutoSize, WithStyles +class SuppliersExport implements FromCollection, ShouldAutoSize, WithHeadings, WithMapping, WithStyles { protected $suppliers; @@ -20,8 +19,8 @@ public function __construct($suppliers) } /** - * @return \Illuminate\Support\Collection - */ + * @return \Illuminate\Support\Collection + */ public function collection() { return $this->suppliers; @@ -67,7 +66,7 @@ public function styles(Worksheet $sheet) { return [ // Style the first row as bold text. - 1 => ['font' => ['bold' => true]], + 1 => ['font' => ['bold' => true]], ]; } } diff --git a/app/Http/Controllers/Admin/PermissionManagementController.php b/app/Http/Controllers/Admin/PermissionManagementController.php index 763d2d5..a7e53ab 100644 --- a/app/Http/Controllers/Admin/PermissionManagementController.php +++ b/app/Http/Controllers/Admin/PermissionManagementController.php @@ -11,6 +11,7 @@ class PermissionManagementController extends Controller public function index() { $permissions = Permission::paginate(10); + return view('admin.permissions.index', compact('permissions')); } @@ -39,7 +40,7 @@ public function edit(Permission $permission) public function update(Request $request, Permission $permission) { $request->validate([ - 'name' => ['required', 'string', 'max:255', 'unique:permissions,name,' . $permission->id], + 'name' => ['required', 'string', 'max:255', 'unique:permissions,name,'.$permission->id], ]); $permission->update(['name' => $request->name]); @@ -54,7 +55,7 @@ public function destroy(Permission $permission) $criticalPermissions = [ 'view users', 'create users', 'edit users', 'delete users', 'view roles', 'create roles', 'edit roles', 'delete roles', - 'view permissions', 'assign permissions' + 'view permissions', 'assign permissions', ]; if (in_array($permission->name, $criticalPermissions)) { diff --git a/app/Http/Controllers/Admin/RoleManagementController.php b/app/Http/Controllers/Admin/RoleManagementController.php index 7254c5a..56c3e25 100644 --- a/app/Http/Controllers/Admin/RoleManagementController.php +++ b/app/Http/Controllers/Admin/RoleManagementController.php @@ -4,20 +4,22 @@ use App\Http\Controllers\Controller; use Illuminate\Http\Request; -use Spatie\Permission\Models\Role; use Spatie\Permission\Models\Permission; +use Spatie\Permission\Models\Role; class RoleManagementController extends Controller { public function index() { $roles = Role::with('permissions')->paginate(10); + return view('admin.roles.index', compact('roles')); } public function create() { $permissions = Permission::all(); + return view('admin.roles.create', compact('permissions')); } @@ -41,14 +43,14 @@ public function edit(Role $role) { $permissions = Permission::all(); $rolePermissions = $role->permissions->pluck('id')->toArray(); - + return view('admin.roles.edit', compact('role', 'permissions', 'rolePermissions')); } public function update(Request $request, Role $role) { $request->validate([ - 'name' => ['required', 'string', 'max:255', 'unique:roles,name,' . $role->id], + 'name' => ['required', 'string', 'max:255', 'unique:roles,name,'.$role->id], ]); $role->update(['name' => $request->name]); diff --git a/app/Http/Controllers/Admin/UserManagementController.php b/app/Http/Controllers/Admin/UserManagementController.php index 5870e37..a5587ec 100644 --- a/app/Http/Controllers/Admin/UserManagementController.php +++ b/app/Http/Controllers/Admin/UserManagementController.php @@ -3,19 +3,20 @@ namespace App\Http\Controllers\Admin; use App\Http\Controllers\Controller; -use Illuminate\Http\Request; use App\Models\User; -use Spatie\Permission\Models\Role; -use Spatie\Permission\Models\Permission; -use Illuminate\Support\Facades\Hash; +use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; +use Illuminate\Support\Facades\Hash; use Illuminate\Validation\Rules; +use Spatie\Permission\Models\Permission; +use Spatie\Permission\Models\Role; class UserManagementController extends Controller { public function index() { $users = User::with('roles')->paginate(10); + return view('admin.users.index', compact('users')); } @@ -23,6 +24,7 @@ public function create() { $roles = Role::all(); $permissions = Permission::all(); + return view('admin.users.create', compact('roles', 'permissions')); } @@ -58,7 +60,7 @@ public function edit(User $user) $permissions = Permission::all(); $userRoles = $user->roles->pluck('id')->toArray(); $userPermissions = $user->permissions->pluck('id')->toArray(); - + return view('admin.users.edit', compact('user', 'roles', 'permissions', 'userRoles', 'userPermissions')); } @@ -66,7 +68,7 @@ public function update(Request $request, User $user) { $request->validate([ 'name' => ['required', 'string', 'max:255'], - 'email' => ['required', 'string', 'email', 'max:255', 'unique:users,email,' . $user->id], + 'email' => ['required', 'string', 'email', 'max:255', 'unique:users,email,'.$user->id], ]); $user->update([ diff --git a/app/Http/Controllers/AttributeController.php b/app/Http/Controllers/AttributeController.php index b2b9ff8..f5c2a44 100644 --- a/app/Http/Controllers/AttributeController.php +++ b/app/Http/Controllers/AttributeController.php @@ -11,6 +11,7 @@ class AttributeController extends Controller public function index() { $attributes = Attribute::with('values')->get(); + return view('product_management.attributes.index', compact('attributes')); } @@ -33,7 +34,7 @@ public function store(Request $request) foreach ($values as $value) { AttributeValue::create([ 'attribute_id' => $attribute->id, - 'value' => trim($value) + 'value' => trim($value), ]); } } @@ -43,7 +44,7 @@ public function store(Request $request) public function edit(Attribute $attribute) { - return view('product_management.attributes.edit', compact('attribute')); + return view('product_management.attributes.edit', compact('attribute')); } public function update(Request $request, Attribute $attribute) @@ -61,6 +62,7 @@ public function update(Request $request, Attribute $attribute) public function destroy(Attribute $attribute) { $attribute->delete(); + return redirect()->route('attributes.index')->with('success', 'Attribute deleted successfully.'); } } diff --git a/app/Http/Controllers/CategoryController.php b/app/Http/Controllers/CategoryController.php index 9691dda..0cb2a1f 100644 --- a/app/Http/Controllers/CategoryController.php +++ b/app/Http/Controllers/CategoryController.php @@ -17,10 +17,11 @@ public function index(Request $request) if ($request->has('search')) { $search = $request->input('search'); $query->where('name', 'like', "%{$search}%") - ->orWhere('code', 'like', "%{$search}%"); + ->orWhere('code', 'like', "%{$search}%"); } $categories = $query->paginate(10); + return view('product_management.categories.index', compact('categories')); } @@ -44,7 +45,7 @@ public function store(Request $request) // Simple placeholder logic for image handling (to be improved if needed) $input = $request->all(); - + Category::create($input); return redirect()->route('categories.index')->with('success', 'Category created successfully.'); @@ -55,7 +56,7 @@ public function store(Request $request) */ public function edit(Category $category) { - return view('product_management.categories.edit', compact('category')); + return view('product_management.categories.edit', compact('category')); } /** @@ -65,7 +66,7 @@ public function update(Request $request, Category $category) { $request->validate([ 'name' => 'required', - 'code' => 'required|unique:categories,code,' . $category->id, + 'code' => 'required|unique:categories,code,'.$category->id, ]); $category->update($request->all()); @@ -79,6 +80,7 @@ public function update(Request $request, Category $category) public function destroy(Category $category) { $category->delete(); + return redirect()->route('categories.index')->with('success', 'Category deleted successfully.'); } } diff --git a/app/Http/Controllers/CityController.php b/app/Http/Controllers/CityController.php index de21673..c411561 100644 --- a/app/Http/Controllers/CityController.php +++ b/app/Http/Controllers/CityController.php @@ -14,13 +14,13 @@ public function index(Request $request) { $search = $request->input('search'); $query = City::query(); - + if ($search) { - $query->where(function ($q) use ($search) { + $query->where(function ($q) use ($search) { $q->where('city_name', 'like', "%{$search}%") - ->orWhere('postal_code', 'like', "%{$search}%") - ->orWhere('district', 'like', "%{$search}%"); - }); + ->orWhere('postal_code', 'like', "%{$search}%") + ->orWhere('district', 'like', "%{$search}%"); + }); } $sort = $request->input('sort', 'city_name'); @@ -34,13 +34,14 @@ public function index(Request $request) if ($request->has('export')) { $cities = $query->get(); - + if ($request->input('export') === 'excel') { return \Maatwebsite\Excel\Facades\Excel::download(new \App\Exports\CitiesExport($cities), 'cities.xlsx'); } - + if ($request->input('export') === 'pdf') { $pdf = \Barryvdh\DomPDF\Facade\Pdf::loadView('exports.cities_pdf', compact('cities')); + return $pdf->stream('cities.pdf'); } } @@ -56,6 +57,7 @@ public function index(Request $request) public function create() { $slData = config('locations.sri_lanka'); + return view('contacts.cities.create', compact('slData')); } @@ -90,6 +92,7 @@ public function show(City $city) public function edit(City $city) { $slData = config('locations.sri_lanka'); + return view('contacts.cities.edit', compact('city', 'slData')); } @@ -126,14 +129,14 @@ public function destroy(City $city) public function getCitiesByDistrict(Request $request) { $district = $request->input('district'); - - if (!$district) { + + if (! $district) { return response()->json([]); } $cities = City::where('district', $district) - ->orderBy('city_name') - ->get(['city_name', 'postal_code']); + ->orderBy('city_name') + ->get(['city_name', 'postal_code']); return response()->json($cities); } diff --git a/app/Http/Controllers/CourierController.php b/app/Http/Controllers/CourierController.php index f4ebc27..c124858 100644 --- a/app/Http/Controllers/CourierController.php +++ b/app/Http/Controllers/CourierController.php @@ -18,12 +18,13 @@ public function index(Request $request) $search = $request->input('search'); $query->where(function ($q) use ($search) { $q->where('name', 'like', "%{$search}%") - ->orWhere('phone', 'like', "%{$search}%") - ->orWhere('email', 'like', "%{$search}%"); + ->orWhere('phone', 'like', "%{$search}%") + ->orWhere('email', 'like', "%{$search}%"); }); } $couriers = $query->latest()->paginate(10); + return view('couriers.index', compact('couriers')); } @@ -90,9 +91,9 @@ public function destroy(Courier $courier) { // Check for orders if ($courier->orders()->exists()) { - return back()->with('error', 'Cannot delete courier with associated orders. Deactivate instead.'); + return back()->with('error', 'Cannot delete courier with associated orders. Deactivate instead.'); } - + $courier->delete(); return redirect()->route('couriers.index')->with('success', 'Courier deleted successfully.'); diff --git a/app/Http/Controllers/CourierPaymentController.php b/app/Http/Controllers/CourierPaymentController.php index de18ba0..58df087 100644 --- a/app/Http/Controllers/CourierPaymentController.php +++ b/app/Http/Controllers/CourierPaymentController.php @@ -19,11 +19,11 @@ public function index(Request $request) // Search if ($request->filled('search')) { $search = $request->search; - $query->where(function($q) use ($search) { - $q->whereHas('courier', function($cq) use ($search) { + $query->where(function ($q) use ($search) { + $q->whereHas('courier', function ($cq) use ($search) { $cq->where('name', 'like', "%{$search}%"); }) - ->orWhere('reference_number', 'like', "%{$search}%"); + ->orWhere('reference_number', 'like', "%{$search}%"); }); } @@ -41,7 +41,7 @@ public function index(Request $request) } $payments = $query->latest('payment_date')->paginate(20); - + return view('courier-payments.index', compact('payments')); } @@ -51,6 +51,7 @@ public function index(Request $request) public function create() { $couriers = Courier::where('is_active', true)->orderBy('name')->get(); + return view('courier-payments.create', compact('couriers')); } @@ -82,6 +83,7 @@ public function store(Request $request) public function edit(CourierPayment $courierPayment) { $couriers = Courier::where('is_active', true)->orderBy('name')->get(); + return view('courier-payments.edit', compact('courierPayment', 'couriers')); } diff --git a/app/Http/Controllers/CustomerController.php b/app/Http/Controllers/CustomerController.php index 847e093..fe19ac0 100644 --- a/app/Http/Controllers/CustomerController.php +++ b/app/Http/Controllers/CustomerController.php @@ -3,9 +3,9 @@ namespace App\Http\Controllers; use App\Models\Customer; -use Illuminate\Http\Request; -use App\Rules\SriLankaMobile; use App\Rules\SriLankaLandline; +use App\Rules\SriLankaMobile; +use Illuminate\Http\Request; class CustomerController extends Controller { @@ -23,10 +23,10 @@ public function index(Request $request) if ($search) { $query->where(function ($q) use ($search) { $q->where('name', 'like', "%{$search}%") - ->orWhere('business_name', 'like', "%{$search}%") - ->orWhere('mobile', 'like', "%{$search}%") - ->orWhere('email', 'like', "%{$search}%") - ->orWhere('address', 'like', "%{$search}%"); + ->orWhere('business_name', 'like', "%{$search}%") + ->orWhere('mobile', 'like', "%{$search}%") + ->orWhere('email', 'like', "%{$search}%") + ->orWhere('address', 'like', "%{$search}%"); }); } @@ -40,14 +40,15 @@ public function index(Request $request) if ($request->has('export')) { $customers = $query->get(); - + if ($request->input('export') === 'excel') { return \Maatwebsite\Excel\Facades\Excel::download(new \App\Exports\CustomersExport($customers), 'customers.xlsx'); } - + if ($request->input('export') === 'pdf') { $pdf = \Barryvdh\DomPDF\Facade\Pdf::loadView('exports.customers_pdf', compact('customers')); $pdf->setPaper('a4', 'landscape'); + return $pdf->stream('customers.pdf'); } } @@ -67,6 +68,7 @@ public function create() { $countries = config('locations.countries'); $slData = config('locations.sri_lanka'); + return view('contacts.customers.create', compact('countries', 'slData')); } @@ -108,6 +110,7 @@ public function edit(Customer $customer) { $countries = config('locations.countries'); $slData = config('locations.sri_lanka'); + return view('contacts.customers.edit', compact('customer', 'countries', 'slData')); } diff --git a/app/Http/Controllers/DashboardController.php b/app/Http/Controllers/DashboardController.php index 6e42f5b..30375e3 100644 --- a/app/Http/Controllers/DashboardController.php +++ b/app/Http/Controllers/DashboardController.php @@ -2,7 +2,6 @@ namespace App\Http\Controllers; -use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; class DashboardController extends Controller diff --git a/app/Http/Controllers/GuestProductController.php b/app/Http/Controllers/GuestProductController.php index c24e7c1..9e35542 100644 --- a/app/Http/Controllers/GuestProductController.php +++ b/app/Http/Controllers/GuestProductController.php @@ -10,8 +10,9 @@ class GuestProductController extends Controller public function index(Request $request) { $products = Product::where('quantity', '>', 0) // Only available products - ->latest() - ->paginate(12); + ->latest() + ->paginate(12); + return view('guest.products.index', compact('products')); } } diff --git a/app/Http/Controllers/OrderController.php b/app/Http/Controllers/OrderController.php index fd54ceb..ded75ea 100644 --- a/app/Http/Controllers/OrderController.php +++ b/app/Http/Controllers/OrderController.php @@ -2,19 +2,18 @@ namespace App\Http\Controllers; +use App\Models\Courier; +use App\Models\Customer; use App\Models\Order; use App\Models\OrderItem; +use App\Models\OrderLog; use App\Models\Product; use App\Models\ProductVariant; use App\Models\Reseller; -use App\Models\Customer; -use App\Models\OrderLog; -use App\Models\Courier; +use Barryvdh\DomPDF\Facade\Pdf; use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; -use Illuminate\Support\Str; use Illuminate\Support\Facades\DB; -use Barryvdh\DomPDF\Facade\Pdf; class OrderController extends Controller { @@ -23,19 +22,19 @@ class OrderController extends Controller */ public function index(Request $request) { - $query = Order::with(['user', 'reseller', 'customer', 'items', 'courier']); + $query = Order::with(['user', 'reseller', 'customer', 'items', 'courier']); // 1. Search (Order Number, Customer Name, Mobile) if ($request->filled('search')) { $search = $request->search; $query->where(function ($q) use ($search) { $q->where('order_number', 'like', "%{$search}%") - ->orWhereHas('customer', function ($subQ) use ($search) { - $subQ->where('name', 'like', "%{$search}%") - ->orWhere('mobile', 'like', "%{$search}%"); - }) - ->orWhere('customer_name', 'like', "%{$search}%") - ->orWhere('customer_phone', 'like', "%{$search}%"); + ->orWhereHas('customer', function ($subQ) use ($search) { + $subQ->where('name', 'like', "%{$search}%") + ->orWhere('mobile', 'like', "%{$search}%"); + }) + ->orWhere('customer_name', 'like', "%{$search}%") + ->orWhere('customer_phone', 'like', "%{$search}%"); }); } @@ -88,8 +87,8 @@ public function create() $sequence = intval($parts[2]) + 1; } } - $nextOrderNumber = 'ORD-' . $dateStr . '-' . str_pad($sequence, 4, '0', STR_PAD_LEFT); - + $nextOrderNumber = 'ORD-'.$dateStr.'-'.str_pad($sequence, 4, '0', STR_PAD_LEFT); + $couriers = Courier::all(); $slData = config('locations.sri_lanka'); @@ -102,13 +101,13 @@ public function create() public function searchProducts(Request $request) { $query = $request->get('q'); - + $products = Product::with(['variants.unit']) ->where('name', 'like', "%{$query}%") ->orWhere('description', 'like', "%{$query}%") // Optional: Search description too ->limit(20) ->get(); - + // Flatten variants for easier frontend consumption $results = []; foreach ($products as $product) { @@ -116,7 +115,7 @@ public function searchProducts(Request $request) // Determine display name (e.g., "Product Name - XL / Red") $variantName = $product->name; if ($variant->unit && $variant->unit_value) { - $variantName .= " (" . $variant->unit_value . " " . $variant->unit->short_name . ")"; + $variantName .= ' ('.$variant->unit_value.' '.$variant->unit->short_name.')'; } $results[] = [ @@ -131,7 +130,7 @@ public function searchProducts(Request $request) ]; } } - + return response()->json($results); } @@ -141,13 +140,13 @@ public function searchProducts(Request $request) public function searchResellers(Request $request) { $query = $request->get('q'); - + $resellers = Reseller::where('name', 'like', "%{$query}%") ->orWhere('business_name', 'like', "%{$query}%") ->orWhere('mobile', 'like', "%{$query}%") ->limit(20) ->get(['id', 'name', 'business_name', 'mobile']); - + return response()->json($resellers); } @@ -160,14 +159,14 @@ public function store(Request $request) 'order_type' => 'required|in:reseller,direct', 'order_date' => 'required|date', 'reseller_id' => 'required_if:order_type,reseller|nullable|exists:resellers,id', - + // Customer Details 'customer.name' => 'required|string|max:255', 'customer.mobile' => 'required|string|max:20', 'customer.landline' => 'nullable|string|max:20', 'customer.address' => 'required|string', 'customer.city' => 'nullable|string', // Optional if we just store address string - + // Products 'items' => 'required|array|min:1', 'items.*.id' => 'required|exists:product_variants,id', @@ -205,34 +204,34 @@ public function store(Request $request) // 2. Create Order $orderNumber = $this->generateOrderNumber(); - - $order = new Order(); + + $order = new Order; $order->order_number = $orderNumber; $order->order_date = $validated['order_date']; $order->order_type = $validated['order_type']; $order->user_id = Auth::id(); // Admin creating the order $order->reseller_id = $validated['order_type'] === 'reseller' ? $validated['reseller_id'] : null; $order->customer_id = $customer->id; - + // Fallback legacy fields (optional, but good for redundancy if migrated) $order->customer_name = $customer->name; $order->customer_phone = $customer->mobile; $order->customer_address = $customer->address; $order->status = $validated['order_status'] ?? 'pending'; - + // New Fields $order->courier_id = $validated['courier_id'] ?? null; $order->courier_charge = $validated['courier_charge'] ?? 0; $order->payment_method = $validated['payment_method'] ?? 'COD'; $order->call_status = $validated['call_status'] ?? 'pending'; $order->sales_note = $validated['sales_note'] ?? null; - + // Capture Address Snapshot $order->customer_city = $validated['customer']['city'] ?? null; $order->customer_district = $validated['customer']['district'] ?? null; $order->customer_province = $validated['customer']['province'] ?? null; - + $order->save(); $totalAmount = 0; @@ -242,22 +241,22 @@ public function store(Request $request) // 3. Process Items foreach ($validated['items'] as $itemData) { $variant = ProductVariant::with('product')->find($itemData['id']); - + // Stock Validation (Optional: Validation rule could handle this, but explicit check is safer) if ($variant->quantity < $itemData['quantity']) { throw new \Exception("Insufficient stock for {$variant->product->name} (SKU: {$variant->sku})"); } - + // Limit Price Validation if ($itemData['selling_price'] < $variant->limit_price) { - throw new \Exception("Selling price for {$variant->product->name} (SKU: {$variant->sku}) cannot be lower than limit price ({$variant->limit_price})"); + throw new \Exception("Selling price for {$variant->product->name} (SKU: {$variant->sku}) cannot be lower than limit price ({$variant->limit_price})"); } $qty = $itemData['quantity']; $unitPrice = $itemData['selling_price']; $basePrice = $variant->limit_price; // Assuming limit_price IS the base/cost price for commission calc $subtotal = $unitPrice * $qty; - + $itemCost = $basePrice * $qty; $itemCommission = ($unitPrice - $basePrice) * $qty; @@ -280,7 +279,7 @@ public function store(Request $request) // Accumulate Totals $totalAmount += $subtotal; $totalCost += $itemCost; - + // Commission only applies for Reseller orders if ($order->order_type === 'reseller') { $totalCommission += $itemCommission; @@ -297,22 +296,24 @@ public function store(Request $request) $this->logAction($order->id, 'created', 'Order created successfully.'); DB::commit(); + return response()->json([ 'success' => true, 'message' => 'Order created successfully!', 'redirect' => route('orders.index'), - 'order_number' => $orderNumber + 'order_number' => $orderNumber, ]); - + } catch (\Exception $e) { DB::rollBack(); + return response()->json([ 'success' => false, - 'message' => $e->getMessage() + 'message' => $e->getMessage(), ], 422); } } - + /** * Generate unique order number. */ @@ -320,26 +321,26 @@ private function generateOrderNumber() { $dateStr = date('Ymd'); $latestOrder = Order::whereDate('created_at', today())->latest()->first(); - + $sequence = 1; if ($latestOrder) { - $parts = explode('-', $latestOrder->order_number); - if (count($parts) === 3 && $parts[1] === $dateStr) { - $sequence = intval($parts[2]) + 1; - } + $parts = explode('-', $latestOrder->order_number); + if (count($parts) === 3 && $parts[1] === $dateStr) { + $sequence = intval($parts[2]) + 1; + } } do { - $number = 'ORD-' . $dateStr . '-' . str_pad($sequence, 4, '0', STR_PAD_LEFT); + $number = 'ORD-'.$dateStr.'-'.str_pad($sequence, 4, '0', STR_PAD_LEFT); $exists = Order::where('order_number', $number)->exists(); if ($exists) { $sequence++; } } while ($exists); - + return $number; } - + /** * Log action helper. */ @@ -352,12 +353,14 @@ private function logAction($orderId, $action, $description = null) 'description' => $description, ]); } + /** * Display the specified order (Invoice View). */ public function show(Order $order) { $order->load(['items.variant', 'customer', 'reseller', 'user']); + return view('orders.show', compact('order')); } @@ -368,7 +371,8 @@ public function downloadPdf(Order $order) { $order->load(['items.variant', 'customer', 'reseller', 'user']); $pdf = Pdf::loadView('orders.pdf', compact('order')); - return $pdf->download('invoice-' . $order->order_number . '.pdf'); + + return $pdf->download('invoice-'.$order->order_number.'.pdf'); } /** @@ -379,12 +383,12 @@ public function edit(Order $order) $order->load(['items.variant', 'customer', 'reseller']); $couriers = Courier::all(); $slData = config('locations.sri_lanka'); - + return view('orders.edit', [ 'order' => $order, 'orderFull' => $order, 'couriers' => $couriers, - 'slData' => $slData + 'slData' => $slData, ]); } @@ -397,14 +401,14 @@ public function update(Request $request, Order $order) 'order_type' => 'required|in:reseller,direct', 'order_date' => 'required|date', 'reseller_id' => 'required_if:order_type,reseller|nullable|exists:resellers,id', - + // Customer Details 'customer.name' => 'required|string|max:255', 'customer.mobile' => 'required|string|max:20', 'customer.landline' => 'nullable|string|max:20', 'customer.address' => 'required|string', 'customer.city' => 'nullable|string', - + // Products 'items' => 'required|array|min:1', 'items.*.id' => 'required|exists:product_variants,id', @@ -434,7 +438,7 @@ public function update(Request $request, Order $order) } } } - + // 2. Clear OLD items $order->items()->delete(); @@ -458,15 +462,15 @@ public function update(Request $request, Order $order) $order->customer_name = $customer->name; $order->customer_phone = $customer->mobile; $order->customer_address = $customer->address; - - // Create/Update Logic for New Fields + + // Create/Update Logic for New Fields $order->status = $validated['order_status'] ?? $order->status; $order->courier_id = $validated['courier_id'] ?? null; $order->courier_charge = $validated['courier_charge'] ?? 0; $order->payment_method = $validated['payment_method'] ?? 'COD'; $order->call_status = $validated['call_status'] ?? 'pending'; $order->sales_note = $validated['sales_note'] ?? null; - // Capture Address Snapshot + // Capture Address Snapshot $order->customer_city = $validated['customer']['city'] ?? null; $order->customer_district = $validated['customer']['district'] ?? null; $order->customer_province = $validated['customer']['province'] ?? null; @@ -480,22 +484,22 @@ public function update(Request $request, Order $order) // 4. Process NEW Items foreach ($validated['items'] as $itemData) { $variant = ProductVariant::with('product')->find($itemData['id']); - + // Stock Check if ($variant->quantity < $itemData['quantity']) { throw new \Exception("Insufficient stock for {$variant->product->name} (SKU: {$variant->sku})"); } - + // Limit Price Check if ($itemData['selling_price'] < $variant->limit_price) { - throw new \Exception("Selling price for {$variant->product->name} (SKU: {$variant->sku}) cannot be lower than limit price ({$variant->limit_price})"); + throw new \Exception("Selling price for {$variant->product->name} (SKU: {$variant->sku}) cannot be lower than limit price ({$variant->limit_price})"); } $qty = $itemData['quantity']; $unitPrice = $itemData['selling_price']; $basePrice = $variant->limit_price; $subtotal = $unitPrice * $qty; - + $itemCost = $basePrice * $qty; $itemCommission = ($unitPrice - $basePrice) * $qty; @@ -507,7 +511,7 @@ public function update(Request $request, Order $order) 'sku' => $variant->sku, 'quantity' => $qty, 'unit_price' => $unitPrice, - 'base_price' => $basePrice, + 'base_price' => $basePrice, 'total_price' => $subtotal, 'subtotal' => $subtotal, ]); @@ -518,7 +522,7 @@ public function update(Request $request, Order $order) // Accumulate totals $totalAmount += $subtotal; $totalCost += $itemCost; - + if ($order->order_type === 'reseller') { $totalCommission += $itemCommission; } @@ -531,6 +535,7 @@ public function update(Request $request, Order $order) $order->save(); DB::commit(); + return response()->json([ 'success' => true, 'message' => 'Order updated successfully!', @@ -539,9 +544,10 @@ public function update(Request $request, Order $order) } catch (\Exception $e) { DB::rollBack(); + return response()->json([ 'success' => false, - 'message' => $e->getMessage() + 'message' => $e->getMessage(), ], 422); } } @@ -567,38 +573,41 @@ public function destroy(Order $order) $order->delete(); DB::commit(); + return redirect()->route('orders.index')->with('success', 'Order deleted successfully and stock restored.'); } catch (\Exception $e) { DB::rollBack(); - return redirect()->back()->with('error', 'Failed to delete order: ' . $e->getMessage()); + + return redirect()->back()->with('error', 'Failed to delete order: '.$e->getMessage()); } } + /** * Display the Call List for orders. */ public function callList(Request $request) { $query = Order::with(['customer', 'reseller', 'items']); - - // Default to 'pending' call status if not specified, + + // Default to 'pending' call status if not specified, // OR user might want to see all. Let's start with all but maybe sort by pending. // Actually, user said "Focused and filtering on call status". - + if ($request->filled('search')) { $search = $request->search; $query->where(function ($q) use ($search) { $q->where('order_number', 'like', "%{$search}%") - ->orWhereHas('customer', function ($subQ) use ($search) { - $subQ->where('name', 'like', "%{$search}%") - ->orWhere('mobile', 'like', "%{$search}%"); - }); + ->orWhereHas('customer', function ($subQ) use ($search) { + $subQ->where('name', 'like', "%{$search}%") + ->orWhere('mobile', 'like', "%{$search}%"); + }); }); } if ($request->filled('call_status')) { $query->where('call_status', $request->call_status); } - + if ($request->filled('date_from')) { $query->whereDate('order_date', '>=', $request->date_from); } @@ -617,7 +626,7 @@ public function callList(Request $request) public function updateStatus(Request $request, $id) { $order = Order::findOrFail($id); - + $validated = $request->validate([ 'status' => 'nullable|in:pending,hold,confirm,shipped,delivered,cancelled', 'call_status' => 'nullable|in:pending,confirm,cancel', @@ -627,13 +636,13 @@ public function updateStatus(Request $request, $id) if (array_key_exists('status', $validated)) { $order->status = $validated['status']; } - + if (array_key_exists('call_status', $validated)) { $order->call_status = $validated['call_status']; } - + if (array_key_exists('sales_note', $validated)) { - $order->sales_note = $validated['sales_note']; + $order->sales_note = $validated['sales_note']; } $order->save(); @@ -642,7 +651,7 @@ public function updateStatus(Request $request, $id) 'success' => true, 'message' => 'Status updated successfully!', 'call_status' => $order->call_status, - 'status' => $order->status + 'status' => $order->status, ]); } } diff --git a/app/Http/Controllers/PackingController.php b/app/Http/Controllers/PackingController.php index aa03c9a..a8afde5 100644 --- a/app/Http/Controllers/PackingController.php +++ b/app/Http/Controllers/PackingController.php @@ -15,20 +15,22 @@ class PackingController extends Controller public function index() { $orders = Order::whereIn('status', ['confirmed', 'packing']) - ->orderBy('created_at', 'asc') - ->get(); + ->orderBy('created_at', 'asc') + ->get(); + return view('orders.packing.index', compact('orders')); } - + /** * Packing Interface for a specific order (Scanner UI). */ public function process($id) { $order = Order::with('items')->findOrFail($id); + return view('orders.packing.process', compact('order')); } - + /** * Mark as Packed / Create Waybill if not exists. */ @@ -39,17 +41,17 @@ public function markPacked(Request $request, $id) $order->packed_by = Auth::id(); $order->dispatched_at = now(); $order->save(); - + OrderLog::create([ 'order_id' => $order->id, 'user_id' => Auth::id(), 'action' => 'packed_dispatched', 'description' => 'Order packed and marked dispatched.', ]); - + return redirect()->route('orders.packing.index')->with('success', 'Order packed successfully.'); } - + /** * Create Packing Batch (Placeholder). */ diff --git a/app/Http/Controllers/ProductController.php b/app/Http/Controllers/ProductController.php index 5a6f304..dbc69ec 100644 --- a/app/Http/Controllers/ProductController.php +++ b/app/Http/Controllers/ProductController.php @@ -2,17 +2,16 @@ namespace App\Http\Controllers; -use App\Models\Product; +use App\Exports\ProductsExport; use App\Models\Category; -use App\Models\SubCategory; // Make sure SubCategory model is imported +use App\Models\Product; // Make sure SubCategory model is imported +use App\Models\ProductVariant; +use App\Models\SubCategory; use App\Models\Unit; -use App\Models\Attribute; +use CloudinaryLabs\CloudinaryLaravel\Facades\Cloudinary; use Illuminate\Http\Request; use Maatwebsite\Excel\Facades\Excel; -use App\Exports\ProductsExport; -use CloudinaryLabs\CloudinaryLaravel\Facades\Cloudinary; use Picqer\Barcode\BarcodeGeneratorHTML; -use App\Models\ProductVariant; class ProductController extends Controller { @@ -22,12 +21,12 @@ public function index(Request $request) if ($request->filled('search')) { $search = $request->input('search'); - $query->where(function($q) use ($search) { + $query->where(function ($q) use ($search) { $q->where('name', 'like', "%{$search}%") - ->orWhere('barcode_data', 'like', "%{$search}%") - ->orWhereHas('variants', function($subQ) use ($search) { - $subQ->where('sku', 'like', "%{$search}%"); - }); + ->orWhere('barcode_data', 'like', "%{$search}%") + ->orWhereHas('variants', function ($subQ) use ($search) { + $subQ->where('sku', 'like', "%{$search}%"); + }); }); } @@ -39,20 +38,22 @@ public function index(Request $request) $products->appends($request->all()); $categories = Category::all(); + return view('product_management.products.index', compact('products', 'categories')); } - public function export(Request $request) + public function export(Request $request) { - return Excel::download(new ProductsExport($request->all()), 'products_' . date('Y-m-d_H-i') . '.xlsx'); + return Excel::download(new ProductsExport($request->all()), 'products_'.date('Y-m-d_H-i').'.xlsx'); } public function create() { $categories = Category::all(); - $subCategories = SubCategory::all(); + $subCategories = SubCategory::all(); $units = Unit::all(); - return view('product_management.products.create', compact('categories', 'subCategories', 'units')); + + return view('product_management.products.create', compact('categories', 'subCategories', 'units')); } public function store(Request $request) @@ -65,7 +66,7 @@ public function store(Request $request) 'warranty_period' => 'nullable|integer|min:0', 'warranty_period_type' => 'nullable|in:years,months,days', 'image' => 'nullable|image|mimes:jpeg,png,jpg,gif|max:2048', - + // Variants Validation 'variants' => 'required|array|min:1', 'variants.*.unit_id' => 'required|exists:units,id', @@ -100,7 +101,7 @@ public function store(Request $request) $variantImage = null; // Handle variant image upload check if ($request->hasFile("variants.{$index}.image")) { - $variantImage = Cloudinary::uploadApi()->upload($request->file("variants.{$index}.image")->getRealPath(), ['verify' => false])['secure_url']; + $variantImage = Cloudinary::uploadApi()->upload($request->file("variants.{$index}.image")->getRealPath(), ['verify' => false])['secure_url']; } $product->variants()->create([ @@ -124,6 +125,7 @@ public function edit(Product $product) $categories = Category::all(); $subCategories = SubCategory::all(); $units = Unit::all(); + return view('product_management.products.edit', compact('product', 'categories', 'subCategories', 'units')); } @@ -142,11 +144,11 @@ public function update(Request $request, Product $product) 'variants.*.id' => 'nullable|exists:product_variants,id', 'variants.*.unit_id' => 'required|exists:units,id', 'variants.*.unit_value' => 'nullable|string|max:50', - 'variants.*.sku' => 'required|string|distinct', + 'variants.*.sku' => 'required|string|distinct', // We can't easily use unique rule with ignore inside array validation in Laravel validation simple syntax // We'll rely on DB constraints or manual check if needed, but 'distinct' helps within the request. // For proper DB unique check ignoring self: we might need custom closure or loop check. - + 'variants.*.selling_price' => 'required|numeric|min:0', 'variants.*.limit_price' => 'nullable|numeric|min:0', 'variants.*.quantity' => 'required|integer|min:0', // Allowing manual update here for now @@ -158,7 +160,7 @@ public function update(Request $request, Product $product) if ($request->hasFile('image')) { $validated['image'] = Cloudinary::uploadApi()->upload($request->file('image')->getRealPath(), ['verify' => false])['secure_url']; } - + $product->update([ 'name' => $validated['name'], 'category_id' => $validated['category_id'], @@ -174,8 +176,8 @@ public function update(Request $request, Product $product) foreach ($request->variants as $index => $variantData) { $variantImage = null; - if ($request->hasFile("variants.{$index}.image")) { - $variantImage = Cloudinary::uploadApi()->upload($request->file("variants.{$index}.image")->getRealPath(), ['verify' => false])['secure_url']; + if ($request->hasFile("variants.{$index}.image")) { + $variantImage = Cloudinary::uploadApi()->upload($request->file("variants.{$index}.image")->getRealPath(), ['verify' => false])['secure_url']; } if (isset($variantData['id']) && $variantData['id']) { @@ -218,26 +220,29 @@ public function destroy(Product $product) { try { $product->delete(); // Cascades deletes variants + return redirect()->route('products.index')->with('success', 'Product deleted successfully.'); } catch (\Exception $e) { - return back()->with('error', 'Error deleting product: ' . $e->getMessage()); + return back()->with('error', 'Error deleting product: '.$e->getMessage()); } } + public function success(Product $product) { $product->load('variants.unit'); + return view('product_management.products.success', compact('product')); } public function printBarcode(ProductVariant $variant) { - $generator = new BarcodeGeneratorHTML(); + $generator = new BarcodeGeneratorHTML; $barcode = $generator->getBarcode($variant->sku, $generator::TYPE_CODE_128); - + return view('product_management.products.barcode_preview', compact('variant', 'barcode')); } - public function bulkPrintBarcode(Request $request) + public function bulkPrintBarcode(Request $request) { $request->validate([ 'products' => 'required|string', // Comma separated IDs @@ -245,9 +250,9 @@ public function bulkPrintBarcode(Request $request) $productIds = explode(',', $request->products); $variants = ProductVariant::whereIn('product_id', $productIds) - ->with(['product']) - ->orderBy('product_id') - ->get(); + ->with(['product']) + ->orderBy('product_id') + ->get(); if ($variants->isEmpty()) { return back()->with('error', 'No variants found for selected products.'); @@ -255,7 +260,7 @@ public function bulkPrintBarcode(Request $request) $pdf = app('dompdf.wrapper'); $pdf->loadView('product_management.products.barcode-pdf', compact('variants')); - + return $pdf->stream('barcodes.pdf'); } @@ -264,19 +269,20 @@ public function bulkDestroy(Request $request) $request->validate([ 'products' => 'required|string', ]); - + $ids = explode(',', $request->products); - + // Use logic that ensures observers are fired (if needed) or just bulk delete // For deleting images etc, we might need loop. But for now, simple delete. Product::whereIn('id', $ids)->delete(); - - return back()->with('success', count($ids) . ' products deleted successfully.'); + + return back()->with('success', count($ids).' products deleted successfully.'); } public function show(Product $product) { $product->load(['category', 'subCategory', 'variants.unit']); + return response()->json($product); } } diff --git a/app/Http/Controllers/ProductImportController.php b/app/Http/Controllers/ProductImportController.php index fead2fd..5a31e15 100644 --- a/app/Http/Controllers/ProductImportController.php +++ b/app/Http/Controllers/ProductImportController.php @@ -2,16 +2,15 @@ namespace App\Http\Controllers; -use Illuminate\Http\Request; -use App\Models\Product; +use App\Exports\ProductTemplateExport; use App\Models\Category; +use App\Models\Product; +use App\Models\ProductVariant; use App\Models\SubCategory; use App\Models\Unit; -use App\Models\ProductVariant; -use Maatwebsite\Excel\Facades\Excel; -use App\Exports\ProductTemplateExport; +use Illuminate\Http\Request; use Illuminate\Support\Facades\DB; -use Carbon\Carbon; +use Maatwebsite\Excel\Facades\Excel; class ProductImportController extends Controller { @@ -43,122 +42,134 @@ public function preview(Request $request) $hasErrors = false; // Pre-fetch related data for quick validation/lookup - $categories = Category::all()->keyBy(function($item) { return strtolower($item->name); }); - $subCategories = SubCategory::all()->keyBy(function($item) { return strtolower($item->name); }); - $units = Unit::all()->keyBy(function($item) { return strtolower($item->name); }); - - $existingSkus = ProductVariant::pluck('sku')->map(fn($sku) => strtolower($sku))->toArray(); + $categories = Category::all()->keyBy(function ($item) { + return strtolower($item->name); + }); + $subCategories = SubCategory::all()->keyBy(function ($item) { + return strtolower($item->name); + }); + $units = Unit::all()->keyBy(function ($item) { + return strtolower($item->name); + }); + + $existingSkus = ProductVariant::pluck('sku')->map(fn ($sku) => strtolower($sku))->toArray(); $fileSkus = []; - + // Grouping Logic: We process rows but need to identify products // In this preview, we just list flattened variants but attach product-level flags foreach ($rows as $index => $row) { - // Columns: - // 0=Name, 1=Cat, 2=SubCat, 3=Desc, 4=Unit, 5=Value, 6=SKU, 7=Price, 8=Limit, 9=Qty, 10=Alert, 11=ImageURL - if (empty($row[0])) continue; - - $name = trim($row[0]); - $catName = isset($row[1]) ? trim($row[1]) : null; - $subCatName = isset($row[2]) ? trim($row[2]) : null; - $desc = isset($row[3]) ? trim($row[3]) : null; - $unitName = isset($row[4]) ? trim($row[4]) : null; - $unitValue = isset($row[5]) ? trim($row[5]) : null; - $sku = isset($row[6]) ? trim($row[6]) : null; - $price = isset($row[7]) ? (float) $row[7] : 0; - $limit = isset($row[8]) ? (float) $row[8] : null; - $qty = isset($row[9]) ? (int) $row[9] : 0; - $alert = isset($row[10]) ? (int) $row[10] : 0; - $imageUrl = isset($row[11]) ? trim($row[11]) : null; - - $errors = []; - $rowStatus = 'OK'; // OK, ERROR, MISSING_DATA - - // 1. Validation: Category - $catId = null; - if (!$catName) { - $errors['category'] = "Required"; - } else { - $key = strtolower($catName); - if (isset($categories[$key])) { - $catId = $categories[$key]->id; - } else { - $errors['category'] = "MISSING_CATEGORY"; // Special code for UI - } - } - - // 2. Validation: Sub Category - $subCatId = null; - if ($subCatName) { - $key = strtolower($subCatName); - if (isset($subCategories[$key])) { - $subCatObj = $subCategories[$key]; - if ($catId && $subCatObj->category_id != $catId) { - $errors['sub_category'] = "Mismatch"; - } else { - $subCatId = $subCatObj->id; - } - } else { - $errors['sub_category'] = "MISSING_SUB_CATEGORY"; - } - } - - // 3. Validation: Unit - $unitId = null; - if (!$unitName) { - $errors['unit'] = "Required"; - } else { - $key = strtolower($unitName); - if (isset($units[$key])) { - $unitId = $units[$key]->id; - } else { - $errors['unit'] = "MISSING_UNIT"; - } - } - - // 4. Validation: SKU - if (!$sku) { - $errors['sku'] = "Required"; - } else { - $skuLower = strtolower($sku); - if (in_array($skuLower, $existingSkus)) { - $errors['sku'] = "Exists in DB"; - } elseif (in_array($skuLower, $fileSkus)) { - $errors['sku'] = "Duplicate in File"; - } - $fileSkus[] = $skuLower; - } - - // 5. Validation: Price & Qty - if ($price <= 0) $errors['price'] = "Invalid"; - if ($qty < 0) $errors['qty'] = "Invalid"; - - // Check if only "Missing" errors exist or actual logic errors - if (!empty($errors)) { - $hasErrors = true; - } else { - $validRowsCount++; - } - - $previewData[] = [ - 'row_id' => $index, // for tracking - 'name' => $name, - 'category_id' => $catId, - 'category_name' => $catName, - 'sub_category_id' => $subCatId, - 'sub_category_name' => $subCatName, - 'description' => $desc, - 'unit_id' => $unitId, - 'unit_name' => $unitName, - 'unit_value' => $unitValue, - 'sku' => $sku, - 'selling_price' => $price, - 'limit_price' => $limit, - 'quantity' => $qty, - 'alert_quantity' => $alert, - 'image_url' => $imageUrl, - 'errors' => $errors - ]; + // Columns: + // 0=Name, 1=Cat, 2=SubCat, 3=Desc, 4=Unit, 5=Value, 6=SKU, 7=Price, 8=Limit, 9=Qty, 10=Alert, 11=ImageURL + if (empty($row[0])) { + continue; + } + + $name = trim($row[0]); + $catName = isset($row[1]) ? trim($row[1]) : null; + $subCatName = isset($row[2]) ? trim($row[2]) : null; + $desc = isset($row[3]) ? trim($row[3]) : null; + $unitName = isset($row[4]) ? trim($row[4]) : null; + $unitValue = isset($row[5]) ? trim($row[5]) : null; + $sku = isset($row[6]) ? trim($row[6]) : null; + $price = isset($row[7]) ? (float) $row[7] : 0; + $limit = isset($row[8]) ? (float) $row[8] : null; + $qty = isset($row[9]) ? (int) $row[9] : 0; + $alert = isset($row[10]) ? (int) $row[10] : 0; + $imageUrl = isset($row[11]) ? trim($row[11]) : null; + + $errors = []; + $rowStatus = 'OK'; // OK, ERROR, MISSING_DATA + + // 1. Validation: Category + $catId = null; + if (! $catName) { + $errors['category'] = 'Required'; + } else { + $key = strtolower($catName); + if (isset($categories[$key])) { + $catId = $categories[$key]->id; + } else { + $errors['category'] = 'MISSING_CATEGORY'; // Special code for UI + } + } + + // 2. Validation: Sub Category + $subCatId = null; + if ($subCatName) { + $key = strtolower($subCatName); + if (isset($subCategories[$key])) { + $subCatObj = $subCategories[$key]; + if ($catId && $subCatObj->category_id != $catId) { + $errors['sub_category'] = 'Mismatch'; + } else { + $subCatId = $subCatObj->id; + } + } else { + $errors['sub_category'] = 'MISSING_SUB_CATEGORY'; + } + } + + // 3. Validation: Unit + $unitId = null; + if (! $unitName) { + $errors['unit'] = 'Required'; + } else { + $key = strtolower($unitName); + if (isset($units[$key])) { + $unitId = $units[$key]->id; + } else { + $errors['unit'] = 'MISSING_UNIT'; + } + } + + // 4. Validation: SKU + if (! $sku) { + $errors['sku'] = 'Required'; + } else { + $skuLower = strtolower($sku); + if (in_array($skuLower, $existingSkus)) { + $errors['sku'] = 'Exists in DB'; + } elseif (in_array($skuLower, $fileSkus)) { + $errors['sku'] = 'Duplicate in File'; + } + $fileSkus[] = $skuLower; + } + + // 5. Validation: Price & Qty + if ($price <= 0) { + $errors['price'] = 'Invalid'; + } + if ($qty < 0) { + $errors['qty'] = 'Invalid'; + } + + // Check if only "Missing" errors exist or actual logic errors + if (! empty($errors)) { + $hasErrors = true; + } else { + $validRowsCount++; + } + + $previewData[] = [ + 'row_id' => $index, // for tracking + 'name' => $name, + 'category_id' => $catId, + 'category_name' => $catName, + 'sub_category_id' => $subCatId, + 'sub_category_name' => $subCatName, + 'description' => $desc, + 'unit_id' => $unitId, + 'unit_name' => $unitName, + 'unit_value' => $unitValue, + 'sku' => $sku, + 'selling_price' => $price, + 'limit_price' => $limit, + 'quantity' => $qty, + 'alert_quantity' => $alert, + 'image_url' => $imageUrl, + 'errors' => $errors, + ]; } session(['product_import_preview_data' => $previewData]); @@ -170,12 +181,12 @@ public function store(Request $request) { $previewData = session('product_import_preview_data'); - if (!$previewData) { + if (! $previewData) { return redirect()->route('products.import.show')->with('error', 'Session expired. Please upload again.'); } $count = 0; - + // Regroup by Product Name to avoid creating duplicate products if rows are scrambled // (Though usually file is sorted, better safe) $groupedData = collect($previewData)->groupBy('name'); @@ -185,20 +196,20 @@ public function store(Request $request) // Use the FIRST valid row's creation data for the product // Find a row that has valid product data? Or just take the first one? $firstRow = $variants->first(); - + // Skip if critical product errors exist? - // Actually we rely on UI to block import if errors exist. + // Actually we rely on UI to block import if errors exist. // But if user skips invalid rows, we proceed with valid ones. - + // 1. Find or Create Product $product = Product::where('name', $productName)->first(); - if (!$product) { + if (! $product) { $image = null; - if (!empty($firstRow['image_url'])) { + if (! empty($firstRow['image_url'])) { // Attempt upload try { - $image = \CloudinaryLabs\CloudinaryLaravel\Facades\Cloudinary::uploadApi()->upload($firstRow['image_url'], ['verify' => false])['secure_url']; + $image = \CloudinaryLabs\CloudinaryLaravel\Facades\Cloudinary::uploadApi()->upload($firstRow['image_url'], ['verify' => false])['secure_url']; } catch (\Exception $e) { // Ignore image error, continue creation? } @@ -215,13 +226,17 @@ public function store(Request $request) // 2. Add Variants foreach ($variants as $row) { - if (!empty($row['errors'])) continue; + if (! empty($row['errors'])) { + continue; + } // Check duplicate SKU again to be safe - if (ProductVariant::where('sku', $row['sku'])->exists()) continue; + if (ProductVariant::where('sku', $row['sku'])->exists()) { + continue; + } - $variantImage = null; // Currently template supports 1 image per row, usually mapping to Product Image. - // If user provides specific variant image logic, we'd need another column. + $variantImage = null; // Currently template supports 1 image per row, usually mapping to Product Image. + // If user provides specific variant image logic, we'd need another column. // For now, let's assume image_url on row applies to Product if new, or ignored if variant? // "Expert" decision: If product exists, maybe update image? No, safer to leave. // Let's assume URL is for the PRODUCT. diff --git a/app/Http/Controllers/PurchaseController.php b/app/Http/Controllers/PurchaseController.php index 4d0b6e1..34e14bd 100644 --- a/app/Http/Controllers/PurchaseController.php +++ b/app/Http/Controllers/PurchaseController.php @@ -5,10 +5,8 @@ use App\Models\Purchase; use App\Models\PurchaseItem; use App\Models\Supplier; -use App\Models\Product; use Illuminate\Http\Request; use Illuminate\Support\Facades\DB; -use Illuminate\Support\Str; class PurchaseController extends Controller { @@ -23,10 +21,10 @@ public function index(Request $request) $search = $request->search; $query->where(function ($q) use ($search) { $q->where('purchase_number', 'like', "%{$search}%") - ->orWhereHas('supplier', function ($sq) use ($search) { - $sq->where('name', 'like', "%{$search}%") - ->orWhere('business_name', 'like', "%{$search}%"); - }); + ->orWhereHas('supplier', function ($sq) use ($search) { + $sq->where('name', 'like', "%{$search}%") + ->orWhere('business_name', 'like', "%{$search}%"); + }); }); } @@ -42,11 +40,12 @@ public function index(Request $request) $totalPurchases = Purchase::count(); $totalSpent = Purchase::sum('net_total'); // Calculate total due (net_total - paid_amount) - // Since we don't have a direct column, strict SQL or collection sum. + // Since we don't have a direct column, strict SQL or collection sum. // SQL is better for performance. $totalDue = Purchase::query()->selectRaw('SUM(net_total - paid_amount) as due')->value('due') ?? 0; $purchases = $query->paginate(15); + return view('purchases.index', compact('purchases', 'totalPurchases', 'totalSpent', 'totalDue')); } @@ -57,7 +56,8 @@ public function create() { $suppliers = Supplier::all(); // Generate a suggested ID - $suggestedNumber = 'PUR-' . date('Ymd') . '-' . mt_rand(1000, 9999); + $suggestedNumber = 'PUR-'.date('Ymd').'-'.mt_rand(1000, 9999); + return view('purchases.create', compact('suppliers', 'suggestedNumber')); } @@ -79,33 +79,33 @@ public function store(Request $request) DB::beginTransaction(); try { // Calculate total paid amount from payments array - $paidAmount = 0; - $paymentsData = []; - - if ($request->has('payments') && is_array($request->payments)) { - foreach ($request->payments as $payment) { - $paidAmount += floatval($payment['amount'] ?? 0); + $paidAmount = 0; + $paymentsData = []; + + if ($request->has('payments') && is_array($request->payments)) { + foreach ($request->payments as $payment) { + $paidAmount += floatval($payment['amount'] ?? 0); + } + $paymentsData = $request->payments; } - $paymentsData = $request->payments; - } - $purchase = Purchase::create([ - 'purchase_number' => $request->purchase_number, - 'supplier_id' => $request->supplier_id, - 'purchase_date' => $request->purchase_date, - 'currency' => $request->currency ?? 'LKR', - 'sub_total' => $request->sub_total, - 'discount_type' => $request->discount_type, - 'discount_value' => $request->discount_value ?? 0, - 'discount_amount' => $request->discount_amount ?? 0, - 'net_total' => $request->net_total, - 'paid_amount' => $paidAmount, - 'payments_data' => json_encode($paymentsData), - 'payment_method' => null, // Deprecated, keeping for backward compatibility - 'payment_reference' => $request->payment_reference, - 'payment_account' => null, // Deprecated - 'payment_note' => null, // Deprecated - ]); + $purchase = Purchase::create([ + 'purchase_number' => $request->purchase_number, + 'supplier_id' => $request->supplier_id, + 'purchase_date' => $request->purchase_date, + 'currency' => $request->currency ?? 'LKR', + 'sub_total' => $request->sub_total, + 'discount_type' => $request->discount_type, + 'discount_value' => $request->discount_value ?? 0, + 'discount_amount' => $request->discount_amount ?? 0, + 'net_total' => $request->net_total, + 'paid_amount' => $paidAmount, + 'payments_data' => json_encode($paymentsData), + 'payment_method' => null, // Deprecated, keeping for backward compatibility + 'payment_reference' => $request->payment_reference, + 'payment_account' => null, // Deprecated + 'payment_note' => null, // Deprecated + ]); foreach ($request->items as $item) { PurchaseItem::create([ @@ -119,11 +119,13 @@ public function store(Request $request) } DB::commit(); + return redirect()->route('purchases.index')->with('success', 'Purchase recorded successfully.'); } catch (\Exception $e) { DB::rollBack(); - return back()->with('error', 'Error creating purchase: ' . $e->getMessage())->withInput(); + + return back()->with('error', 'Error creating purchase: '.$e->getMessage())->withInput(); } } @@ -133,6 +135,7 @@ public function store(Request $request) public function show(Purchase $purchase) { $purchase->load(['items', 'supplier']); + return view('purchases.show', compact('purchase')); } @@ -142,6 +145,7 @@ public function show(Purchase $purchase) public function pdf(Purchase $purchase) { $purchase->load(['items', 'supplier']); + return view('purchases.pdf', compact('purchase')); } @@ -152,6 +156,7 @@ public function edit(Purchase $purchase) { $suppliers = Supplier::all(); $purchase->load('items'); + return view('purchases.edit', compact('purchase', 'suppliers')); } @@ -164,7 +169,7 @@ public function update(Request $request, Purchase $purchase) $request->validate([ 'supplier_id' => 'required|exists:suppliers,id', 'purchase_date' => 'required|date', - 'purchase_number' => 'required|unique:purchases,purchase_number,' . $purchase->id, + 'purchase_number' => 'required|unique:purchases,purchase_number,'.$purchase->id, 'items' => 'required|array|min:1', 'items.*.product_name' => 'required|string', 'items.*.quantity' => 'required|numeric|min:1', @@ -176,7 +181,7 @@ public function update(Request $request, Purchase $purchase) // Calculate total paid amount from payments array $paidAmount = 0; $paymentsData = []; - + if ($request->has('payments') && is_array($request->payments)) { foreach ($request->payments as $payment) { $paidAmount += floatval($payment['amount'] ?? 0); @@ -217,11 +222,13 @@ public function update(Request $request, Purchase $purchase) } DB::commit(); + return redirect()->route('purchases.index')->with('success', 'Purchase updated successfully.'); } catch (\Exception $e) { DB::rollBack(); - return back()->with('error', 'Error updating purchase: ' . $e->getMessage())->withInput(); + + return back()->with('error', 'Error updating purchase: '.$e->getMessage())->withInput(); } } @@ -231,6 +238,7 @@ public function update(Request $request, Purchase $purchase) public function destroy(Purchase $purchase) { $purchase->delete(); + return redirect()->route('purchases.index')->with('success', 'Purchase deleted successfully.'); } } diff --git a/app/Http/Controllers/QuickCreateController.php b/app/Http/Controllers/QuickCreateController.php index 3729c9e..604b838 100644 --- a/app/Http/Controllers/QuickCreateController.php +++ b/app/Http/Controllers/QuickCreateController.php @@ -5,7 +5,6 @@ use App\Models\Category; use App\Models\SubCategory; use App\Models\Unit; -use App\Models\Attribute; use Illuminate\Http\Request; use Illuminate\Support\Facades\Validator; @@ -14,33 +13,32 @@ class QuickCreateController extends Controller public function storeCategory(Request $request) { - // Auto-generate code if not provided (though validator required it, we should probably make it optional in validation or generate before) // Wait, validator says REQUIRED. So I must change validation rule or send it from JS. // Better: Make it optional, generate if missing. - + $data = $request->all(); - // Since we changed logic, let's remove validation for code IF we generate it. + // Since we changed logic, let's remove validation for code IF we generate it. // ACTUALLY, let's just create a new Validator instance with modified rules or better yet, merge input. - + // Revised Logic: $data['code'] = $request->code ?? \Illuminate\Support\Str::slug($request->name); - + $validator = Validator::make($data, [ 'name' => 'required|string|max:255', 'code' => 'required|string|max:255|unique:categories,code', ]); if ($validator->fails()) { - return response()->json(['errors' => $validator->errors()], 422); + return response()->json(['errors' => $validator->errors()], 422); } - + $category = Category::create($data); return response()->json([ 'success' => true, 'category' => $category, - 'message' => 'Category created successfully.' + 'message' => 'Category created successfully.', ]); } @@ -62,7 +60,7 @@ public function storeSubCategory(Request $request) return response()->json([ 'success' => true, 'subCategory' => $subCategory, - 'message' => 'Sub Category created successfully.' + 'message' => 'Sub Category created successfully.', ]); } @@ -82,7 +80,7 @@ public function storeUnit(Request $request) return response()->json([ 'success' => true, 'unit' => $unit, - 'message' => 'Unit created successfully.' + 'message' => 'Unit created successfully.', ]); } } diff --git a/app/Http/Controllers/ReportController.php b/app/Http/Controllers/ReportController.php index 825b079..66a4977 100644 --- a/app/Http/Controllers/ReportController.php +++ b/app/Http/Controllers/ReportController.php @@ -2,13 +2,13 @@ namespace App\Http\Controllers; -use Illuminate\Http\Request; use App\Models\Order; use App\Models\Product; -use App\Models\City; +use App\Models\PurchaseItem; use App\Models\User; -use Illuminate\Support\Facades\DB; use Carbon\Carbon; +use Illuminate\Http\Request; +use Illuminate\Support\Facades\DB; class ReportController extends Controller { @@ -22,16 +22,16 @@ public function index() $totalOrders = Order::count(); $pendingOrders = Order::where('status', 'pending')->count(); $todaySales = Order::where('status', 'delivered')->whereDate('created_at', Carbon::today())->sum('total_amount'); - + // Month-wise Sales for Chart $monthlySales = Order::select( - DB::raw('sum(total_amount) as sums'), + DB::raw('sum(total_amount) as sums'), DB::raw("strftime('%Y-%m', created_at) as month") ) - ->groupBy('month') - ->orderBy('month', 'desc') - ->limit(6) - ->get(); + ->groupBy('month') + ->orderBy('month', 'desc') + ->limit(6) + ->get(); return view('reports.index', compact('totalSales', 'totalOrders', 'pendingOrders', 'todaySales', 'monthlySales')); } @@ -48,7 +48,7 @@ public function provinceSale() ->groupBy('cities.province') ->orderBy('total_sales', 'desc') ->get(); - + return view('reports.province', compact('provinceSales')); } @@ -59,48 +59,48 @@ public function profitLoss(Request $request) { $query = Order::with(['items', 'courierPayment']) ->where('status', 'delivered'); - + if ($request->has('start_date') && $request->has('end_date')) { $query->whereBetween('created_at', [$request->input('start_date'), $request->input('end_date')]); } - + $orders = $query->get(); - + $data = [ 'total_sales' => 0, 'cogs' => 0, // Cost of Goods Sold from FIFO 'courier_cost' => 0, // Real courier cost 'delivery_income' => 0, // Delivery fees charged 'gross_profit' => 0, - 'net_profit' => 0 + 'net_profit' => 0, ]; - + foreach ($orders as $order) { $data['total_sales'] += $order->total_amount; // Includes product price + delivery fee usually, assuming total_amount is final bill. - + // COGS - $orderCogs = $order->items->sum('total_price') - $order->items->sum(function($item){ - return ($item->unit_price - $item->cost_price) * $item->quantity; - // Wait, cost_price is unit cost. - // Margin = (Unit Price - Cost Price) * Qty - // COGS = Cost Price * Qty + $orderCogs = $order->items->sum('total_price') - $order->items->sum(function ($item) { + return ($item->unit_price - $item->cost_price) * $item->quantity; + // Wait, cost_price is unit cost. + // Margin = (Unit Price - Cost Price) * Qty + // COGS = Cost Price * Qty }); // Simpler: - $orderCogs = $order->items->sum(function($item) { + $orderCogs = $order->items->sum(function ($item) { return $item->cost_price * $item->quantity; }); $data['cogs'] += $orderCogs; - + // Logistics $data['courier_cost'] += $order->courier_cost; // What we pay courier $data['delivery_income'] += $order->delivery_fee; // What we charge customer (if separated in total, or part of it) // Assuming total_amount includes delivery_fee. } - + $data['gross_profit'] = $data['total_sales'] - $data['cogs']; $data['net_profit'] = $data['gross_profit'] - $data['courier_cost']; // Note: Operational expenses (OpEx) aren't tracked here yet. - + return view('reports.profit_loss', compact('data')); } @@ -109,19 +109,31 @@ public function profitLoss(Request $request) */ public function stockReport() { - $products = Product::with(['purchaseItems' => function($q) { - $q->where('remaining_quantity', '>', 0); - }])->get(); - - // Calculate Valuation per product based on FIFO batches - $products->map(function($product) { - $product->stock_value = $product->purchaseItems->sum(function($item) { - return $item->remaining_quantity * $item->purchasing_price; - }); - return $product; - }); - - return view('reports.stock', compact('products')); + // Use pagination and subqueries for better performance + + // Calculate Global Totals directly from DB + $totalStockValue = PurchaseItem::where('remaining_quantity', '>', 0) + ->sum(DB::raw('remaining_quantity * purchase_price')); + + $totalItemsInStock = PurchaseItem::where('remaining_quantity', '>', 0) + ->sum('remaining_quantity'); + + // Fetch paginated products with eager loaded purchase items and their purchase details + $products = Product::with(['purchaseItems' => function ($q) { + $q->where('remaining_quantity', '>', 0)->with('purchase'); + }]) + ->addSelect(['stock_value' => PurchaseItem::selectRaw('sum(remaining_quantity * purchase_price)') + ->whereColumn('product_id', 'products.id') + ->where('remaining_quantity', '>', 0), + ]) + ->paginate(20); + + // No need to map and calculate stock_value in PHP as we use addSelect, + // OR we can calculate it on the fly in the view if needed, but addSelect is cleaner for sorting if required later. + // However, standard attribute access would work if we append it, but we are using paginator. + // The addSelect puts it into the model attributes. + + return view('reports.stock', compact('products', 'totalStockValue', 'totalItemsInStock')); } /** @@ -129,11 +141,11 @@ public function stockReport() */ public function packetCount() { - $packers = User::withCount(['packedOrders' => function($q){ - // Count dispatched orders packed by user - $q->where('status', 'dispatched')->orWhere('status', 'delivered'); + $packers = User::withCount(['packedOrders' => function ($q) { + // Count dispatched orders packed by user + $q->where('status', 'dispatched')->orWhere('status', 'delivered'); }])->get(); // Filter by role if needed - + return view('reports.packet_count', compact('packers')); } @@ -145,7 +157,7 @@ public function productSales() $productSales = DB::table('order_items') ->join('orders', 'order_items.order_id', '=', 'orders.id') ->select( - 'order_items.product_name', + 'order_items.product_name', 'order_items.sku', DB::raw('sum(order_items.quantity) as total_qty'), DB::raw('sum(order_items.total_price) as total_revenue'), @@ -155,24 +167,24 @@ public function productSales() ->groupBy('order_items.product_id', 'order_items.product_name', 'order_items.sku', 'orders.status') ->orderBy('total_qty', 'desc') ->get(); - + return view('reports.product_sales', compact('productSales')); } - + /** * User Wise Sales Report (Sales Rep/Reseller Performance). */ public function userSales() { // Assuming 'user_id' on order is the creator/sales rep - $userSales = User::withSum(['orders' => function($q) { + $userSales = User::withSum(['orders' => function ($q) { $q->where('status', 'delivered'); }], 'total_amount') - ->withCount(['orders' => function($q) { - $q->where('status', 'delivered'); - }]) - ->get(); - + ->withCount(['orders' => function ($q) { + $q->where('status', 'delivered'); + }]) + ->get(); + return view('reports.user_sales', compact('userSales')); } } diff --git a/app/Http/Controllers/ResellerController.php b/app/Http/Controllers/ResellerController.php index f2e51a2..fe0a6bb 100644 --- a/app/Http/Controllers/ResellerController.php +++ b/app/Http/Controllers/ResellerController.php @@ -3,9 +3,9 @@ namespace App\Http\Controllers; use App\Models\Reseller; -use Illuminate\Http\Request; -use App\Rules\SriLankaMobile; use App\Rules\SriLankaLandline; +use App\Rules\SriLankaMobile; +use Illuminate\Http\Request; class ResellerController extends Controller { @@ -20,9 +20,9 @@ public function index(Request $request) if ($search) { $query->where(function ($q) use ($search) { $q->where('name', 'like', "%{$search}%") - ->orWhere('business_name', 'like', "%{$search}%") - ->orWhere('mobile', 'like', "%{$search}%") - ->orWhere('email', 'like', "%{$search}%"); + ->orWhere('business_name', 'like', "%{$search}%") + ->orWhere('mobile', 'like', "%{$search}%") + ->orWhere('email', 'like', "%{$search}%"); }); } @@ -36,14 +36,15 @@ public function index(Request $request) } if ($request->has('export')) { $resellers = $query->get(); - + if ($request->input('export') === 'excel') { return \Maatwebsite\Excel\Facades\Excel::download(new \App\Exports\ResellersExport($resellers), 'resellers.xlsx'); } - + if ($request->input('export') === 'pdf') { $pdf = \Barryvdh\DomPDF\Facade\Pdf::loadView('exports.resellers_pdf', compact('resellers')); $pdf->setPaper('a4', 'landscape'); + return $pdf->stream('resellers.pdf'); } } @@ -65,6 +66,7 @@ public function create() { $countries = config('locations.countries'); $slData = config('locations.sri_lanka'); + return view('contacts.resellers.create', compact('countries', 'slData')); } @@ -107,6 +109,7 @@ public function edit(Reseller $reseller) { $countries = config('locations.countries'); $slData = config('locations.sri_lanka'); + return view('contacts.resellers.edit', compact('reseller', 'countries', 'slData')); } diff --git a/app/Http/Controllers/ResellerDuesController.php b/app/Http/Controllers/ResellerDuesController.php index 38b94bc..c8c3686 100644 --- a/app/Http/Controllers/ResellerDuesController.php +++ b/app/Http/Controllers/ResellerDuesController.php @@ -2,9 +2,8 @@ namespace App\Http\Controllers; -use App\Models\Reseller; use App\Models\Order; -use App\Models\ResellerPayment; +use App\Models\Reseller; use Illuminate\Http\Request; class ResellerDuesController extends Controller @@ -19,18 +18,18 @@ public function index(Request $request) if ($search) { $query->where('name', 'like', "%{$search}%") - ->orWhere('business_name', 'like', "%{$search}%") - ->orWhere('mobile', 'like', "%{$search}%"); + ->orWhere('business_name', 'like', "%{$search}%") + ->orWhere('mobile', 'like', "%{$search}%"); } - + // Default sort by name, but allow sorting by due_amount $sort = $request->input('sort', 'name'); $direction = $request->input('direction', 'asc'); - + if ($sort === 'due_amount') { $query->orderBy('due_amount', $direction); } else { - $query->orderBy('name', 'asc'); + $query->orderBy('name', 'asc'); } $resellers = $query->paginate(20); @@ -44,14 +43,14 @@ public function index(Request $request) public function show(Request $request, $id) { $reseller = Reseller::findOrFail($id); - + // --- 1. Filter Parameters --- $startDate = $request->input('start_date'); $endDate = $request->input('end_date'); // --- 2. Build Query (UNION) --- // We use DB queries for performance and UNION compatibility - + // Orders Query $ordersQuery = \DB::table('orders') ->select( @@ -81,41 +80,41 @@ public function show(Request $request, $id) // Apply Date Filters to sub-queries if possible, or usually better to apply to the UNION // But for "Opening Balance" calculation we need strict separation. - + // --- 3. Calculate Global Math --- - + // A. Global Opening Adjustment (The "Manual" part) // This is what makes the final balance match the DB due_amount // Formula: Adj = API_Due - (All_Orders - All_Payments) // If the system is perfect, Adj is 0. If imported data exists, Adj is the initial balance. - + $allOrdersSum = $reseller->orders()->where('status', '!=', 'cancelled')->sum('total_amount'); $allPaymentsSum = $reseller->payments()->where('status', '!=', 'cancelled')->sum('amount'); $globalAdjustment = $reseller->due_amount - ($allOrdersSum - $allPaymentsSum); - + // B. Calculate "Balance Forward" (Balance BEFORE the start_date) $balanceForward = $globalAdjustment; - + if ($startDate) { $ordersBefore = $reseller->orders() ->where('status', '!=', 'cancelled') ->where('created_at', '<', $startDate) ->sum('total_amount'); - + $paymentsBefore = $reseller->payments() ->where('status', '!=', 'cancelled') // Exclude cancelled ->where('payment_date', '<', $startDate) ->sum('amount'); - + $balanceForward += ($ordersBefore - $paymentsBefore); } // --- 4. Fetch Paginated Records for View --- - + // Bind queries for Union // We need to filter the UNION result by date $combinedQuery = $ordersQuery->union($paymentsQuery); - + // Improve: Apply date filters to the combined query wrapper // Laravel's union builder is tricky with where clauses on the union itself without a subquery wrapper. // Easiest is to apply where to both parts if simple. @@ -125,10 +124,10 @@ public function show(Request $request, $id) } if ($endDate) { // Include the whole end day - $ordersQuery->where('created_at', '<=', $endDate . ' 23:59:59'); - $paymentsQuery->where('payment_date', '<=', $endDate . ' 23:59:59'); + $ordersQuery->where('created_at', '<=', $endDate.' 23:59:59'); + $paymentsQuery->where('payment_date', '<=', $endDate.' 23:59:59'); } - + // Now paginate the UNION // Note: Union and orderBy/paginate requires careful syntax $transactions = $ordersQuery->union($paymentsQuery) @@ -139,54 +138,54 @@ public function show(Request $request, $id) // --- 5. Calculate Running Balance for Display --- // We are displaying Descending (Newest First). // Row 1 Balance = Balance at End of Period (or End of Page) - + // We need the "Closing Balance" of this specific PAGE to start subtracting down. // Closing Balance of Page = BalanceForward + NetChange(All Items in range up to this page's start??) - // Actually: + // Actually: // Total Balance at End of Filtered Selection = BalanceForward + Sum(All Visible Items in Range) - + // Let's get the sum of all items in the filtered range (Orders - Payments) $ordersInRange = \DB::table('orders')->where('reseller_id', $id)->where('status', '!=', 'cancelled'); $paymentsInRange = \DB::table('reseller_payments')->where('reseller_id', $id); - + if ($startDate) { $ordersInRange->where('created_at', '>=', $startDate); $paymentsInRange->where('payment_date', '>=', $startDate); } if ($endDate) { - $ordersInRange->where('created_at', '<=', $endDate . ' 23:59:59'); - $paymentsInRange->where('payment_date', '<=', $endDate . ' 23:59:59'); + $ordersInRange->where('created_at', '<=', $endDate.' 23:59:59'); + $paymentsInRange->where('payment_date', '<=', $endDate.' 23:59:59'); } - + $netChangeInRange = $ordersInRange->sum('total_amount') - $paymentsInRange->sum('amount'); $closingBalance = $balanceForward + $netChangeInRange; // But we are PAGINATING. If we are on Page 2, the top item is NOT the closing balance. // The top item of Page 2 is (ClosingBalance - NetChangeOfPage1). - + // Calculate Net Change of items *newer* than current page's items? // Offset approach: // Skip = ($page - 1) * $perPage // We need the sum of the first (Skip) items from the sorted QUERY to subtract from ClosingBalance. - + $currentPage = $transactions->currentPage(); $perPage = $transactions->perPage(); $offset = ($currentPage - 1) * $perPage; - + // Calculate Sum of items skipped (Newer items not on this page) - // We reuse the union query logic but limit/offset? + // We reuse the union query logic but limit/offset? // Actually, just fetching the top N items and summing them is safest. $newerItemsAdjustment = 0; if ($offset > 0) { // Re-run the union query with limit=$offset to sum their effect $newOrders = clone $ordersQuery; // These already have filters applied $newPayments = clone $paymentsQuery; - + $newerItems = $newOrders->union($newPayments) ->orderBy('date', 'desc') ->limit($offset) ->get(); - + foreach ($newerItems as $item) { if ($item->type === 'Order') { $newerItemsAdjustment += $item->amount; @@ -195,27 +194,27 @@ public function show(Request $request, $id) } } } - + $startingBalanceForPage = $closingBalance - $newerItemsAdjustment; - + // --- 6. Transform for View --- // Iterate current page items and assign running balance - // Since we go DESC, + // Since we go DESC, // Item 1 Balance = $startingBalanceForPage // Item 1 PrevBalance (for Item 2) = $startingBalanceForPage - (Item1 Effect) - + $running = $startingBalanceForPage; - + $statement = $transactions->map(function ($t) use (&$running) { $t = (object) $t; $t->balance = $running; - + // Prepare for next item (which is older) // If current is DEBIT (Order), it INCREASED the balance to get here. So older balance was LOWER. // OldBalance = CurrentBalance - OrderAmount // If current is CREDIT (Payment), it DECREASED the balance. So older balance was HIGHER. // OldBalance = CurrentBalance + PaymentAmount - + if ($t->type === 'Order') { $t->debit = $t->amount; $t->credit = 0; @@ -223,22 +222,22 @@ public function show(Request $request, $id) $running -= $t->amount; } else { // Payment $t->debit = 0; - + // If cancelled, credit is effectively 0 for balance calculation, but we might want to show the amount visually with strikethrough? // For Balance correctness: if (isset($t->status) && $t->status === 'cancelled') { $t->credit = 0; $effectiveAmount = 0; - $t->description = $t->description . ' (Cancelled)'; + $t->description = $t->description.' (Cancelled)'; } else { $t->credit = $t->amount; $effectiveAmount = $t->amount; } - + $t->url = route('reseller-payments.edit', $t->original_id); $running += $effectiveAmount; } - + return $t; }); diff --git a/app/Http/Controllers/ResellerOrderController.php b/app/Http/Controllers/ResellerOrderController.php index 762a8c1..12dc66b 100644 --- a/app/Http/Controllers/ResellerOrderController.php +++ b/app/Http/Controllers/ResellerOrderController.php @@ -5,8 +5,6 @@ use App\Models\Order; use App\Models\Product; use App\Models\Reseller; -use Illuminate\Http\Request; -use Illuminate\Support\Facades\Auth; class ResellerOrderController extends OrderController { @@ -19,9 +17,9 @@ public function create() // Get only resellers $resellers = Reseller::all(); $cities = \App\Models\City::all(); - + return view('orders.reseller_create', compact('products', 'resellers', 'cities')); } - + // Store uses OrderController@store but with reseller_id validation being critical } diff --git a/app/Http/Controllers/ResellerPaymentController.php b/app/Http/Controllers/ResellerPaymentController.php index 0c9d1ec..ef4da50 100644 --- a/app/Http/Controllers/ResellerPaymentController.php +++ b/app/Http/Controllers/ResellerPaymentController.php @@ -20,10 +20,10 @@ public function index(Request $request) if ($search) { $query->whereHas('reseller', function ($q) use ($search) { $q->where('name', 'like', "%{$search}%") - ->orWhere('business_name', 'like', "%{$search}%"); + ->orWhere('business_name', 'like', "%{$search}%"); })->orWhere('reference_id', 'like', "%{$search}%"); } - + if ($request->has('method') && $request->input('method') != '') { $query->where('payment_method', $request->input('method')); } @@ -48,6 +48,7 @@ public function index(Request $request) public function create() { $resellers = Reseller::all(); + return view('resellers.payments.create', compact('resellers')); } @@ -83,6 +84,7 @@ public function store(Request $request) public function edit(ResellerPayment $resellerPayment) { $resellers = Reseller::all(); + return view('resellers.payments.edit', compact('resellerPayment', 'resellers')); } @@ -92,7 +94,8 @@ public function edit(ResellerPayment $resellerPayment) public function downloadInvoice(ResellerPayment $resellerPayment) { $pdf = \Barryvdh\DomPDF\Facade\Pdf::loadView('resellers.payments.invoice', ['payment' => $resellerPayment]); - return $pdf->download('receipt-' . str_pad($resellerPayment->id, 6, '0', STR_PAD_LEFT) . '.pdf'); + + return $pdf->download('receipt-'.str_pad($resellerPayment->id, 6, '0', STR_PAD_LEFT).'.pdf'); } /** @@ -101,32 +104,32 @@ public function downloadInvoice(ResellerPayment $resellerPayment) public function downloadBulkInvoices(Request $request) { $zip = new \ZipArchive; - $fileName = 'payment_vouchers_' . date('Y-m-d_His') . '.zip'; - + $fileName = 'payment_vouchers_'.date('Y-m-d_His').'.zip'; + // Ensure the directory exists - if (!file_exists(storage_path('app/public/temp'))) { + if (! file_exists(storage_path('app/public/temp'))) { mkdir(storage_path('app/public/temp'), 0755, true); } - $zipPath = storage_path('app/public/temp/' . $fileName); + $zipPath = storage_path('app/public/temp/'.$fileName); - if ($zip->open($zipPath, \ZipArchive::CREATE | \ZipArchive::OVERWRITE) !== TRUE) { - return back()->with('error', 'Could not create Zip file.'); + if ($zip->open($zipPath, \ZipArchive::CREATE | \ZipArchive::OVERWRITE) !== true) { + return back()->with('error', 'Could not create Zip file.'); } - // Logic: + // Logic: // 1. If 'payment_ids' array is present (from checkboxes), use that. - // 2. Else if 'select_all_matching' is present/implied by absence of IDs but presence of filters? + // 2. Else if 'select_all_matching' is present/implied by absence of IDs but presence of filters? // Actually, simpler: If IDs -> use IDs. Else -> use filters. - + $paymentIds = $request->input('payment_ids'); - + if ($paymentIds) { - // Explode if it's a comma-separated string (sometimes happens with hidden inputs), + // Explode if it's a comma-separated string (sometimes happens with hidden inputs), // but usually array if from checkboxes. if (is_string($paymentIds)) { $paymentIds = explode(',', $paymentIds); } - + $payments = ResellerPayment::with('reseller')->whereIn('id', $paymentIds)->get(); } else { // Fallback to Filters @@ -136,10 +139,10 @@ public function downloadBulkInvoices(Request $request) if ($search) { $query->whereHas('reseller', function ($q) use ($search) { $q->where('name', 'like', "%{$search}%") - ->orWhere('business_name', 'like', "%{$search}%"); + ->orWhere('business_name', 'like', "%{$search}%"); })->orWhere('reference_id', 'like', "%{$search}%"); } - + if ($request->has('method') && $request->input('method') != '') { $query->where('payment_method', $request->input('method')); } @@ -147,11 +150,11 @@ public function downloadBulkInvoices(Request $request) if ($request->filled('start_date')) { $query->whereDate('payment_date', '>=', $request->input('start_date')); } - + if ($request->filled('end_date')) { $query->whereDate('payment_date', '<=', $request->input('end_date')); } - + $payments = $query->get(); } @@ -162,7 +165,7 @@ public function downloadBulkInvoices(Request $request) foreach ($payments as $payment) { $pdf = \Barryvdh\DomPDF\Facade\Pdf::loadView('resellers.payments.invoice', ['payment' => $payment]); $content = $pdf->output(); - $zip->addFromString('voucher-' . str_pad($payment->id, 6, '0', STR_PAD_LEFT) . '.pdf', $content); + $zip->addFromString('voucher-'.str_pad($payment->id, 6, '0', STR_PAD_LEFT).'.pdf', $content); } $zip->close(); @@ -193,7 +196,7 @@ public function update(Request $request, ResellerPayment $resellerPayment) // Adjust Reseller Due (Subtracting difference: if new amount is higher, due decreases more) $reseller = Reseller::find($request->reseller_id); // Example: Due 1000. Paid 500 (Old). Due becomes 500. - // Update Payment to 800 (diff +300). + // Update Payment to 800 (diff +300). // Due should be 200. (500 - 300). $reseller->due_amount -= $difference; $reseller->save(); @@ -208,14 +211,14 @@ public function update(Request $request, ResellerPayment $resellerPayment) public function cancel(ResellerPayment $resellerPayment) { if ($resellerPayment->status === 'cancelled') { - return redirect()->route('reseller-payments.index')->with('error', 'Payment is already cancelled.'); + return redirect()->route('reseller-payments.index')->with('error', 'Payment is already cancelled.'); } DB::transaction(function () use ($resellerPayment) { // Reverse the financial impact (Add back the amount to Due) $reseller = $resellerPayment->reseller; $reseller->increment('due_amount', $resellerPayment->amount); - + // Mark as cancelled $resellerPayment->update(['status' => 'cancelled']); }); diff --git a/app/Http/Controllers/ResellerPaymentImportController.php b/app/Http/Controllers/ResellerPaymentImportController.php index 3864469..772cb99 100644 --- a/app/Http/Controllers/ResellerPaymentImportController.php +++ b/app/Http/Controllers/ResellerPaymentImportController.php @@ -2,13 +2,13 @@ namespace App\Http\Controllers; -use Illuminate\Http\Request; +use App\Exports\ResellerPaymentTemplateExport; use App\Models\Reseller; use App\Models\ResellerPayment; -use Maatwebsite\Excel\Facades\Excel; -use App\Exports\ResellerPaymentTemplateExport; -use Illuminate\Support\Facades\DB; use Carbon\Carbon; +use Illuminate\Http\Request; +use Illuminate\Support\Facades\DB; +use Maatwebsite\Excel\Facades\Excel; class ResellerPaymentImportController extends Controller { @@ -19,7 +19,7 @@ public function show() public function downloadTemplate() { - return Excel::download(new ResellerPaymentTemplateExport, 'reseller_payment_template_' . date('Y-m-d') . '.xlsx'); + return Excel::download(new ResellerPaymentTemplateExport, 'reseller_payment_template_'.date('Y-m-d').'.xlsx'); } public function preview(Request $request) @@ -43,40 +43,45 @@ public function preview(Request $request) foreach ($rows as $index => $row) { // Expected columns: 0=ID, 1=Name, 2=Due, 3=Amount, 4=Method, 5=Ref, 6=Date $amount = isset($row[3]) ? (float) $row[3] : 0; - + // Skip rows with no payment amount - if ($amount <= 0) continue; + if ($amount <= 0) { + continue; + } $resellerId = isset($row[0]) ? $row[0] : null; $method = isset($row[4]) ? strtolower($row[4]) : 'cash'; $reference = isset($row[5]) ? $row[5] : null; $dateStr = isset($row[6]) ? $row[6] : date('Y-m-d'); - + // Validate Reseller $reseller = Reseller::find($resellerId); $errors = []; - if (!$reseller) { + if (! $reseller) { $errors[] = "Invalid Reseller ID: $resellerId"; } - if (!in_array($method, ['cash', 'bank', 'other'])) { + if (! in_array($method, ['cash', 'bank', 'other'])) { $errors[] = "Invalid Method: $method (Use cash, bank, or other)"; } - + try { // Excel dates are sometimes integers (days since 1900-01-01) if (is_numeric($dateStr)) { $date = \PhpOffice\PhpSpreadsheet\Shared\Date::excelToDateTimeObject($dateStr)->format('Y-m-d'); } else { - $date = Carbon::parse($dateStr)->format('Y-m-d'); + $date = Carbon::parse($dateStr)->format('Y-m-d'); } } catch (\Exception $e) { $errors[] = "Invalid Date: $dateStr"; $date = null; } - if (!empty($errors)) $hasErrors = true; - else $validRowsCount++; + if (! empty($errors)) { + $hasErrors = true; + } else { + $validRowsCount++; + } $previewData[] = [ 'reseller_id' => $resellerId, @@ -86,10 +91,10 @@ public function preview(Request $request) 'method' => $method, 'reference' => $reference, 'date' => $date, - 'errors' => $errors + 'errors' => $errors, ]; } - + // Cache the valid data for processing? Or just send to view and re-submit file? // Re-submitting file is stateless but user might lose selection. // Better: Encode data in hidden field or use session. @@ -103,15 +108,17 @@ public function store(Request $request) { $previewData = session('import_preview_data'); - if (!$previewData) { + if (! $previewData) { return redirect()->route('reseller-payments.import.show')->with('error', 'Session expired. Please upload the file again.'); } $count = 0; - + DB::transaction(function () use ($previewData, &$count) { foreach ($previewData as $row) { - if (!empty($row['errors'])) continue; + if (! empty($row['errors'])) { + continue; + } // Create Payment ResellerPayment::create([ @@ -129,7 +136,7 @@ public function store(Request $request) $reseller->due_amount -= $row['amount']; $reseller->save(); } - + $count++; } }); diff --git a/app/Http/Controllers/ResellerTargetController.php b/app/Http/Controllers/ResellerTargetController.php index 08bec8d..5eed3eb 100644 --- a/app/Http/Controllers/ResellerTargetController.php +++ b/app/Http/Controllers/ResellerTargetController.php @@ -19,10 +19,10 @@ public function index(Request $request) $search = $request->input('search'); $query->where(function ($q) use ($search) { $q->where('ref_id', 'like', "%{$search}%") - ->orWhereHas('reseller', function ($q) use ($search) { - $q->where('name', 'like', "%{$search}%") - ->orWhere('business_name', 'like', "%{$search}%"); - }); + ->orWhereHas('reseller', function ($q) use ($search) { + $q->where('name', 'like', "%{$search}%") + ->orWhere('business_name', 'like', "%{$search}%"); + }); }); } @@ -41,6 +41,7 @@ public function index(Request $request) public function create() { $resellers = Reseller::orderBy('name')->get(); + return view('resellers.targets.create', compact('resellers')); } @@ -73,6 +74,7 @@ public function store(Request $request) public function edit(ResellerTarget $resellerTarget) { $resellers = Reseller::orderBy('name')->get(); + return view('resellers.targets.edit', compact('resellerTarget', 'resellers')); } diff --git a/app/Http/Controllers/SubCategoryController.php b/app/Http/Controllers/SubCategoryController.php index b53fa09..04e3c23 100644 --- a/app/Http/Controllers/SubCategoryController.php +++ b/app/Http/Controllers/SubCategoryController.php @@ -2,8 +2,8 @@ namespace App\Http\Controllers; -use App\Models\SubCategory; use App\Models\Category; +use App\Models\SubCategory; use Illuminate\Http\Request; class SubCategoryController extends Controller @@ -15,23 +15,25 @@ public function index(Request $request) if ($request->has('search')) { $search = $request->input('search'); $query->where('name', 'like', "%{$search}%") - ->orWhereHas('category', function($q) use ($search) { - $q->where('name', 'like', "%{$search}%"); - }); + ->orWhereHas('category', function ($q) use ($search) { + $q->where('name', 'like', "%{$search}%"); + }); } if ($request->has('category_id') && $request->category_id != '') { - $query->where('category_id', $request->category_id); + $query->where('category_id', $request->category_id); } $subCategories = $query->paginate(10); $categories = Category::all(); // For filter dropdown + return view('product_management.sub_categories.index', compact('subCategories', 'categories')); } public function create() { $categories = Category::all(); + return view('product_management.sub_categories.create', compact('categories')); } @@ -50,6 +52,7 @@ public function store(Request $request) public function edit(SubCategory $subCategory) { $categories = Category::all(); + return view('product_management.sub_categories.edit', compact('subCategory', 'categories')); } @@ -68,6 +71,7 @@ public function update(Request $request, SubCategory $subCategory) public function destroy(SubCategory $subCategory) { $subCategory->delete(); + return redirect()->route('sub-categories.index')->with('success', 'Sub Category deleted successfully.'); } } diff --git a/app/Http/Controllers/SupplierController.php b/app/Http/Controllers/SupplierController.php index 6e47e30..8a7843f 100644 --- a/app/Http/Controllers/SupplierController.php +++ b/app/Http/Controllers/SupplierController.php @@ -3,9 +3,9 @@ namespace App\Http\Controllers; use App\Models\Supplier; -use Illuminate\Http\Request; -use App\Rules\SriLankaMobile; use App\Rules\SriLankaLandline; +use App\Rules\SriLankaMobile; +use Illuminate\Http\Request; class SupplierController extends Controller { @@ -20,9 +20,9 @@ public function index(Request $request) if ($search) { $query->where(function ($q) use ($search) { $q->where('name', 'like', "%{$search}%") - ->orWhere('business_name', 'like', "%{$search}%") - ->orWhere('mobile', 'like', "%{$search}%") - ->orWhere('email', 'like', "%{$search}%"); + ->orWhere('business_name', 'like', "%{$search}%") + ->orWhere('mobile', 'like', "%{$search}%") + ->orWhere('email', 'like', "%{$search}%"); }); } @@ -36,14 +36,15 @@ public function index(Request $request) } if ($request->has('export')) { $suppliers = $query->get(); - + if ($request->input('export') === 'excel') { return \Maatwebsite\Excel\Facades\Excel::download(new \App\Exports\SuppliersExport($suppliers), 'suppliers.xlsx'); } - + if ($request->input('export') === 'pdf') { $pdf = \Barryvdh\DomPDF\Facade\Pdf::loadView('exports.suppliers_pdf', compact('suppliers')); $pdf->setPaper('a4', 'landscape'); + return $pdf->stream('suppliers.pdf'); } } @@ -60,6 +61,7 @@ public function create() { $countries = config('locations.countries'); $slData = config('locations.sri_lanka'); + return view('contacts.suppliers.create', compact('countries', 'slData')); } @@ -102,6 +104,7 @@ public function edit(Supplier $supplier) { $countries = config('locations.countries'); $slData = config('locations.sri_lanka'); + return view('contacts.suppliers.edit', compact('supplier', 'countries', 'slData')); } diff --git a/app/Http/Controllers/UnitController.php b/app/Http/Controllers/UnitController.php index 9f89be4..f249118 100644 --- a/app/Http/Controllers/UnitController.php +++ b/app/Http/Controllers/UnitController.php @@ -17,10 +17,11 @@ public function index(Request $request) if ($request->has('search')) { $search = $request->input('search'); $query->where('name', 'like', "%{$search}%") - ->orWhere('short_name', 'like', "%{$search}%"); + ->orWhere('short_name', 'like', "%{$search}%"); } $units = $query->paginate(10); + return view('product_management.units.index', compact('units')); } @@ -76,6 +77,7 @@ public function update(Request $request, Unit $unit) public function destroy(Unit $unit) { $unit->delete(); + return redirect()->route('units.index')->with('success', 'Unit deleted successfully.'); } } diff --git a/app/Http/Controllers/WaybillController.php b/app/Http/Controllers/WaybillController.php index d86f4a6..52485dc 100644 --- a/app/Http/Controllers/WaybillController.php +++ b/app/Http/Controllers/WaybillController.php @@ -15,30 +15,31 @@ public function index() { // Orders ready for waybill (e.g., confirmed but not yet printed, or any confirmed) $orders = Order::where('status', 'confirmed')->latest()->paginate(20); + return view('orders.waybill.index', compact('orders')); } - + /** * Print selected waybills (A4 4-up). */ public function print(Request $request) { $orderIds = $request->input('order_ids', []); - + if (empty($orderIds)) { return back()->with('error', 'No orders selected.'); } - + $orders = Order::whereIn('id', $orderIds)->with('items', 'city')->get(); - + // Generate unique waybill numbers if missing foreach ($orders as $order) { - if (!$order->waybill_number) { - $order->waybill_number = 'WB-' . $order->order_number; - $order->save(); + if (! $order->waybill_number) { + $order->waybill_number = 'WB-'.$order->order_number; + $order->save(); } } - + return view('orders.waybill.print', compact('orders')); } } diff --git a/app/Models/Category.php b/app/Models/Category.php index 60c7536..362b994 100644 --- a/app/Models/Category.php +++ b/app/Models/Category.php @@ -15,7 +15,7 @@ public function subCategories() { return $this->hasMany(SubCategory::class); } - + public function products() { return $this->hasMany(Product::class); diff --git a/app/Models/Courier.php b/app/Models/Courier.php index c9ee1e8..d54b963 100644 --- a/app/Models/Courier.php +++ b/app/Models/Courier.php @@ -2,9 +2,8 @@ namespace App\Models; -use Illuminate\Database\Eloquent\Model; - use Illuminate\Database\Eloquent\Factories\HasFactory; +use Illuminate\Database\Eloquent\Model; class Courier extends Model { diff --git a/app/Models/CourierPayment.php b/app/Models/CourierPayment.php index d03d424..469a644 100644 --- a/app/Models/CourierPayment.php +++ b/app/Models/CourierPayment.php @@ -2,9 +2,8 @@ namespace App\Models; -use Illuminate\Database\Eloquent\Model; - use Illuminate\Database\Eloquent\Factories\HasFactory; +use Illuminate\Database\Eloquent\Model; class CourierPayment extends Model { diff --git a/app/Models/Order.php b/app/Models/Order.php index 9962178..4a36296 100644 --- a/app/Models/Order.php +++ b/app/Models/Order.php @@ -2,9 +2,8 @@ namespace App\Models; -use Illuminate\Database\Eloquent\Model; - use Illuminate\Database\Eloquent\Factories\HasFactory; +use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\SoftDeletes; class Order extends Model @@ -89,12 +88,12 @@ public function city() { return $this->belongsTo(City::class); } - + public function courier() { return $this->belongsTo(Courier::class); } - + public function courierPayment() { return $this->belongsTo(CourierPayment::class); diff --git a/app/Models/OrderItem.php b/app/Models/OrderItem.php index b15ab2d..3d0c7f9 100644 --- a/app/Models/OrderItem.php +++ b/app/Models/OrderItem.php @@ -2,9 +2,8 @@ namespace App\Models; -use Illuminate\Database\Eloquent\Model; - use Illuminate\Database\Eloquent\Factories\HasFactory; +use Illuminate\Database\Eloquent\Model; class OrderItem extends Model { @@ -23,8 +22,6 @@ class OrderItem extends Model 'total_price', 'subtotal', // Use total_price or subtotal depending on schema, migration didn't add subtotal but existing had total_price ]; - - protected $casts = [ 'unit_price' => 'decimal:2', diff --git a/app/Models/OrderLog.php b/app/Models/OrderLog.php index df6b734..4ce4449 100644 --- a/app/Models/OrderLog.php +++ b/app/Models/OrderLog.php @@ -2,9 +2,8 @@ namespace App\Models; -use Illuminate\Database\Eloquent\Model; - use Illuminate\Database\Eloquent\Factories\HasFactory; +use Illuminate\Database\Eloquent\Model; class OrderLog extends Model { diff --git a/app/Models/Product.php b/app/Models/Product.php index 911804a..fa1ed76 100644 --- a/app/Models/Product.php +++ b/app/Models/Product.php @@ -38,6 +38,11 @@ public function variants() return $this->hasMany(ProductVariant::class); } + public function purchaseItems() + { + return $this->hasMany(PurchaseItem::class); + } + public function getTotalQuantityAttribute() { return $this->variants->sum('quantity'); @@ -56,23 +61,29 @@ public function getPriceDisplayAttribute() return number_format($min, 2); } - return number_format($min, 2) . ' - ' . number_format($max, 2); + return number_format($min, 2).' - '.number_format($max, 2); } public function getStockStatusAttribute() { - if ($this->variants->isEmpty()) return 'Out of Stock'; + if ($this->variants->isEmpty()) { + return 'Out of Stock'; + } $totalQty = $this->total_quantity; - if ($totalQty == 0) return 'Out of Stock'; + if ($totalQty == 0) { + return 'Out of Stock'; + } // Check if any variant is low on stock $hasLowStock = $this->variants->contains(function ($variant) { return $variant->quantity <= $variant->alert_quantity; }); - if ($hasLowStock) return 'Low Stock'; + if ($hasLowStock) { + return 'Low Stock'; + } - return 'In Stock'; + return 'In Stock'; } } diff --git a/app/Models/PurchaseItem.php b/app/Models/PurchaseItem.php index dfa3580..88039f2 100644 --- a/app/Models/PurchaseItem.php +++ b/app/Models/PurchaseItem.php @@ -11,6 +11,7 @@ class PurchaseItem extends Model 'product_id', 'product_name', 'quantity', + 'remaining_quantity', 'purchase_price', 'total', ]; diff --git a/app/Models/User.php b/app/Models/User.php index 6f1aeaf..38bece4 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -11,7 +11,7 @@ class User extends Authenticatable { /** @use HasFactory<\Database\Factories\UserFactory> */ - use HasFactory, Notifiable, HasRoles; + use HasFactory, HasRoles, Notifiable; /** * The attributes that are mass assignable. @@ -47,6 +47,4 @@ protected function casts(): array 'password' => 'hashed', ]; } - - } diff --git a/app/Rules/SriLankaLandline.php b/app/Rules/SriLankaLandline.php index db76a3d..c46ba00 100644 --- a/app/Rules/SriLankaLandline.php +++ b/app/Rules/SriLankaLandline.php @@ -15,7 +15,7 @@ class SriLankaLandline implements ValidationRule public function validate(string $attribute, mixed $value, Closure $fail): void { // Sri Lankan landline strictly: 0XXXXXXXXX (10 digits, usually starts with 011, 038 etc but we check 0 followed by 9 digits) - if (!preg_match('/^0\d{9}$/', $value)) { + if (! preg_match('/^0\d{9}$/', $value)) { $fail('The :attribute must be a valid Sri Lankan landline number (e.g., 0112345678).'); } } diff --git a/app/Rules/SriLankaMobile.php b/app/Rules/SriLankaMobile.php index 43dfed9..3a7d82d 100644 --- a/app/Rules/SriLankaMobile.php +++ b/app/Rules/SriLankaMobile.php @@ -15,7 +15,7 @@ class SriLankaMobile implements ValidationRule public function validate(string $attribute, mixed $value, Closure $fail): void { // Sri Lankan mobile number strictly: 07XXXXXXXX (10 digits) - if (!preg_match('/^07\d{8}$/', $value)) { + if (! preg_match('/^07\d{8}$/', $value)) { $fail('The :attribute must be a valid Sri Lankan mobile number (e.g., 0712345678).'); } } diff --git a/app/Services/StockService.php b/app/Services/StockService.php index 9992e6b..660f526 100644 --- a/app/Services/StockService.php +++ b/app/Services/StockService.php @@ -4,22 +4,19 @@ use App\Models\Product; use App\Models\PurchaseItem; -use Illuminate\Support\Facades\DB; class StockService { /** * Deduct stock using FIFO method and calculate weighted cost price. - * - * @param Product $product - * @param int $quantity + * * @return float Weighted Cost Price per Item */ public function deductStock(Product $product, int $quantity): float { $remainingToDeduct = $quantity; $totalCost = 0; - + // Product master stock deduction (simple count) $product->quantity -= $quantity; $product->save(); @@ -28,14 +25,16 @@ public function deductStock(Product $product, int $quantity): float // Get verified batches with stock, ordered by oldest first $batches = PurchaseItem::where('product_id', $product->id) ->where('remaining_quantity', '>', 0) - ->whereHas('purchase', function($q) { + ->whereHas('purchase', function ($q) { $q->where('status', 'verified'); }) ->orderBy('created_at', 'asc') // FIFO ->get(); foreach ($batches as $batch) { - if ($remainingToDeduct <= 0) break; + if ($remainingToDeduct <= 0) { + break; + } $available = $batch->remaining_quantity; $batchCost = $batch->purchasing_price; @@ -44,14 +43,14 @@ public function deductStock(Product $product, int $quantity): float // Take all needed from this batch $batch->remaining_quantity -= $remainingToDeduct; $batch->save(); - + $totalCost += ($remainingToDeduct * $batchCost); $remainingToDeduct = 0; } else { // Take whatever is available $batch->remaining_quantity = 0; $batch->save(); - + $totalCost += ($available * $batchCost); $remainingToDeduct -= $available; } diff --git a/config/locations.php b/config/locations.php index 6239765..85c674b 100644 --- a/config/locations.php +++ b/config/locations.php @@ -2,29 +2,29 @@ return [ 'countries' => [ - "Sri Lanka", // Prioritized - "Afghanistan", "Albania", "Algeria", "Andorra", "Angola", "Antigua and Barbuda", "Argentina", "Armenia", "Australia", - "Austria", "Azerbaijan", "Bahamas", "Bahrain", "Bangladesh", "Barbados", "Belarus", "Belgium", "Belize", "Benin", - "Bhutan", "Bolivia", "Bosnia and Herzegovina", "Botswana", "Brazil", "Brunei", "Bulgaria", "Burkina Faso", "Burundi", - "Cabo Verde", "Cambodia", "Cameroon", "Canada", "Central African Republic", "Chad", "Chile", "China", "Colombia", - "Comoros", "Congo (Congo-Brazzaville)", "Costa Rica", "Croatia", "Cuba", "Cyprus", "Czechia (Czech Republic)", - "Democratic Republic of the Congo", "Denmark", "Djibouti", "Dominica", "Dominican Republic", "Ecuador", "Egypt", - "El Salvador", "Equatorial Guinea", "Eritrea", "Estonia", "Eswatini (fmr. 'Swaziland')", "Ethiopia", "Fiji", "Finland", - "France", "Gabon", "Gambia", "Georgia", "Germany", "Ghana", "Greece", "Grenada", "Guatemala", "Guinea", "Guinea-Bissau", - "Guyana", "Haiti", "Holy See", "Honduras", "Hungary", "Iceland", "India", "Indonesia", "Iran", "Iraq", "Ireland", "Israel", - "Italy", "Jamaica", "Japan", "Jordan", "Kazakhstan", "Kenya", "Kiribati", "Kuwait", "Kyrgyzstan", "Laos", "Latvia", - "Lebanon", "Lesotho", "Liberia", "Libya", "Liechtenstein", "Lithuania", "Luxembourg", "Madagascar", "Malawi", "Malaysia", - "Maldives", "Mali", "Malta", "Marshall Islands", "Mauritania", "Mauritius", "Mexico", "Micronesia", "Moldova", "Monaco", - "Mongolia", "Montenegro", "Morocco", "Mozambique", "Myanmar (formerly Burma)", "Namibia", "Nauru", "Nepal", "Netherlands", - "New Zealand", "Nicaragua", "Niger", "Nigeria", "North Korea", "North Macedonia", "Norway", "Oman", "Pakistan", "Palau", - "Palestine State", "Panama", "Papua New Guinea", "Paraguay", "Peru", "Philippines", "Poland", "Portugal", "Qatar", - "Romania", "Russia", "Rwanda", "Saint Kitts and Nevis", "Saint Lucia", "Saint Vincent and the Grenadines", "Samoa", - "San Marino", "Sao Tome and Principe", "Saudi Arabia", "Senegal", "Serbia", "Seychelles", "Sierra Leone", "Singapore", - "Slovakia", "Slovenia", "Solomon Islands", "Somalia", "South Africa", "South Korea", "South Sudan", "Spain", "Sudan", - "Suriname", "Sweden", "Switzerland", "Syria", "Tajikistan", "Tanzania", "Thailand", "Timor-Leste", "Togo", "Tonga", - "Trinidad and Tobago", "Tunisia", "Turkey", "Turkmenistan", "Tuvalu", "Uganda", "Ukraine", "United Arab Emirates", - "United Kingdom", "United States of America", "Uruguay", "Uzbekistan", "Vanuatu", "Venezuela", "Vietnam", "Yemen", - "Zambia", "Zimbabwe" + 'Sri Lanka', // Prioritized + 'Afghanistan', 'Albania', 'Algeria', 'Andorra', 'Angola', 'Antigua and Barbuda', 'Argentina', 'Armenia', 'Australia', + 'Austria', 'Azerbaijan', 'Bahamas', 'Bahrain', 'Bangladesh', 'Barbados', 'Belarus', 'Belgium', 'Belize', 'Benin', + 'Bhutan', 'Bolivia', 'Bosnia and Herzegovina', 'Botswana', 'Brazil', 'Brunei', 'Bulgaria', 'Burkina Faso', 'Burundi', + 'Cabo Verde', 'Cambodia', 'Cameroon', 'Canada', 'Central African Republic', 'Chad', 'Chile', 'China', 'Colombia', + 'Comoros', 'Congo (Congo-Brazzaville)', 'Costa Rica', 'Croatia', 'Cuba', 'Cyprus', 'Czechia (Czech Republic)', + 'Democratic Republic of the Congo', 'Denmark', 'Djibouti', 'Dominica', 'Dominican Republic', 'Ecuador', 'Egypt', + 'El Salvador', 'Equatorial Guinea', 'Eritrea', 'Estonia', "Eswatini (fmr. 'Swaziland')", 'Ethiopia', 'Fiji', 'Finland', + 'France', 'Gabon', 'Gambia', 'Georgia', 'Germany', 'Ghana', 'Greece', 'Grenada', 'Guatemala', 'Guinea', 'Guinea-Bissau', + 'Guyana', 'Haiti', 'Holy See', 'Honduras', 'Hungary', 'Iceland', 'India', 'Indonesia', 'Iran', 'Iraq', 'Ireland', 'Israel', + 'Italy', 'Jamaica', 'Japan', 'Jordan', 'Kazakhstan', 'Kenya', 'Kiribati', 'Kuwait', 'Kyrgyzstan', 'Laos', 'Latvia', + 'Lebanon', 'Lesotho', 'Liberia', 'Libya', 'Liechtenstein', 'Lithuania', 'Luxembourg', 'Madagascar', 'Malawi', 'Malaysia', + 'Maldives', 'Mali', 'Malta', 'Marshall Islands', 'Mauritania', 'Mauritius', 'Mexico', 'Micronesia', 'Moldova', 'Monaco', + 'Mongolia', 'Montenegro', 'Morocco', 'Mozambique', 'Myanmar (formerly Burma)', 'Namibia', 'Nauru', 'Nepal', 'Netherlands', + 'New Zealand', 'Nicaragua', 'Niger', 'Nigeria', 'North Korea', 'North Macedonia', 'Norway', 'Oman', 'Pakistan', 'Palau', + 'Palestine State', 'Panama', 'Papua New Guinea', 'Paraguay', 'Peru', 'Philippines', 'Poland', 'Portugal', 'Qatar', + 'Romania', 'Russia', 'Rwanda', 'Saint Kitts and Nevis', 'Saint Lucia', 'Saint Vincent and the Grenadines', 'Samoa', + 'San Marino', 'Sao Tome and Principe', 'Saudi Arabia', 'Senegal', 'Serbia', 'Seychelles', 'Sierra Leone', 'Singapore', + 'Slovakia', 'Slovenia', 'Solomon Islands', 'Somalia', 'South Africa', 'South Korea', 'South Sudan', 'Spain', 'Sudan', + 'Suriname', 'Sweden', 'Switzerland', 'Syria', 'Tajikistan', 'Tanzania', 'Thailand', 'Timor-Leste', 'Togo', 'Tonga', + 'Trinidad and Tobago', 'Tunisia', 'Turkey', 'Turkmenistan', 'Tuvalu', 'Uganda', 'Ukraine', 'United Arab Emirates', + 'United Kingdom', 'United States of America', 'Uruguay', 'Uzbekistan', 'Vanuatu', 'Venezuela', 'Vietnam', 'Yemen', + 'Zambia', 'Zimbabwe', ], 'sri_lanka' => [ @@ -37,5 +37,5 @@ 'North Central' => ['Anuradhapura', 'Polonnaruwa'], 'Uva' => ['Badulla', 'Monaragala'], 'Sabaragamuwa' => ['Ratnapura', 'Kegalle'], - ] + ], ]; diff --git a/database/migrations/2025_12_23_025302_create_orders_table.php b/database/migrations/2025_12_23_025302_create_orders_table.php index c19aeef..0d9a372 100644 --- a/database/migrations/2025_12_23_025302_create_orders_table.php +++ b/database/migrations/2025_12_23_025302_create_orders_table.php @@ -16,7 +16,7 @@ public function up(): void $table->string('order_number')->unique(); $table->foreignId('user_id')->nullable()->constrained()->onDelete('set null'); // Creator (Admin or Reseller) $table->foreignId('reseller_id')->nullable()->constrained('users')->onDelete('set null'); // If linked to a reseller - + // Customer Details (Denormalized for snapshot or linked if Customer model exists, sticking to requirements for now) $table->string('customer_name')->nullable(); $table->string('customer_phone')->nullable(); @@ -25,21 +25,21 @@ public function up(): void // Order Status $table->string('status')->default('pending'); // pending, on_hold, confirmed, packing, dispatched, delivered, returned, cancelled - + // Payment $table->string('payment_method')->default('cod'); // cod, online $table->string('payment_status')->default('pending'); // pending, paid, failed - + // Financials $table->decimal('total_amount', 10, 2)->default(0); - + // Meta $table->text('sales_note')->nullable(); - + // Logistics $table->string('waybill_number')->nullable(); $table->string('courier_id')->nullable(); // Using string for now or foreign key if courier table exists - + // Workflow Tracking $table->foreignId('packed_by')->nullable()->constrained('users')->onDelete('set null'); $table->timestamp('dispatched_at')->nullable(); diff --git a/database/migrations/2025_12_23_025303_create_order_items_table.php b/database/migrations/2025_12_23_025303_create_order_items_table.php index 316f9ec..4059f2a 100644 --- a/database/migrations/2025_12_23_025303_create_order_items_table.php +++ b/database/migrations/2025_12_23_025303_create_order_items_table.php @@ -15,15 +15,15 @@ public function up(): void $table->id(); $table->foreignId('order_id')->constrained()->onDelete('cascade'); $table->foreignId('product_id')->nullable()->constrained()->onDelete('set null'); - + // Snapshot of product details at time of order $table->string('product_name'); $table->string('sku')->nullable(); - + $table->integer('quantity'); $table->decimal('unit_price', 10, 2); $table->decimal('total_price', 10, 2); // quantity * unit_price - + $table->timestamps(); }); } diff --git a/database/migrations/2025_12_23_030637_create_courier_payments_table.php b/database/migrations/2025_12_23_030637_create_courier_payments_table.php index 730cd7d..f282539 100644 --- a/database/migrations/2025_12_23_030637_create_courier_payments_table.php +++ b/database/migrations/2025_12_23_030637_create_courier_payments_table.php @@ -15,11 +15,11 @@ public function up(): void $table->id(); $table->foreignId('courier_id')->constrained()->onDelete('cascade'); $table->foreignId('user_id')->nullable()->constrained()->onDelete('set null'); - + $table->decimal('amount', 10, 2); $table->date('payment_date'); $table->string('reference_number')->nullable(); - + $table->timestamps(); }); } diff --git a/database/migrations/2025_12_23_030638_add_courier_fields_to_orders_table.php b/database/migrations/2025_12_23_030638_add_courier_fields_to_orders_table.php index 23fbf41..33c1a33 100644 --- a/database/migrations/2025_12_23_030638_add_courier_fields_to_orders_table.php +++ b/database/migrations/2025_12_23_030638_add_courier_fields_to_orders_table.php @@ -12,26 +12,26 @@ public function up(): void { Schema::table('orders', function (Blueprint $table) { - // We previously defined 'courier_id' as nullable string. + // We previously defined 'courier_id' as nullable string. // Better to change it to unsignedBigInteger if we want relation, or just leave it and add foreign key constraint if data allows. // For safety and simplicity in this additive migration: - + // $table->foreignId('courier_id')->change(); // Only if courier_id was already bigInteger, but it was string. - // So we'll trust the user to manage the migration refresh or we can drop/add. + // So we'll trust the user to manage the migration refresh or we can drop/add. // Given we just merged OMS, we can probably drop the string column and add the foreign key if there's no data. // BUT, to be safe: - + if (Schema::hasColumn('orders', 'courier_id')) { - $table->dropColumn('courier_id'); + $table->dropColumn('courier_id'); } }); - + Schema::table('orders', function (Blueprint $table) { - $table->foreignId('courier_id')->nullable()->after('waybill_number')->constrained('couriers')->onDelete('set null'); - $table->decimal('courier_cost', 10, 2)->default(0)->after('total_amount'); // Real cost - $table->decimal('delivery_fee', 10, 2)->default(0)->after('courier_cost'); // Charged to customer - - $table->foreignId('courier_payment_id')->nullable()->constrained('courier_payments')->onDelete('set null'); + $table->foreignId('courier_id')->nullable()->after('waybill_number')->constrained('couriers')->onDelete('set null'); + $table->decimal('courier_cost', 10, 2)->default(0)->after('total_amount'); // Real cost + $table->decimal('delivery_fee', 10, 2)->default(0)->after('courier_cost'); // Charged to customer + + $table->foreignId('courier_payment_id')->nullable()->constrained('courier_payments')->onDelete('set null'); }); } @@ -44,7 +44,7 @@ public function down(): void $table->dropForeign(['courier_id']); $table->dropColumn('courier_id'); $table->string('courier_id')->nullable(); // Revert to string - + $table->dropColumn(['courier_cost', 'delivery_fee']); $table->dropForeign(['courier_payment_id']); $table->dropColumn('courier_payment_id'); diff --git a/database/migrations/2025_12_23_031257_add_analytics_fields_to_tables.php b/database/migrations/2025_12_23_031257_add_analytics_fields_to_tables.php index 46db42e..6ccd01c 100644 --- a/database/migrations/2025_12_23_031257_add_analytics_fields_to_tables.php +++ b/database/migrations/2025_12_23_031257_add_analytics_fields_to_tables.php @@ -11,15 +11,13 @@ */ public function up(): void { - if (!Schema::hasColumn('cities', 'province')) { + if (! Schema::hasColumn('cities', 'province')) { Schema::table('cities', function (Blueprint $table) { $table->string('province')->nullable()->after('district'); }); } - - - if (!Schema::hasColumn('order_items', 'cost_price')) { + if (! Schema::hasColumn('order_items', 'cost_price')) { Schema::table('order_items', function (Blueprint $table) { $table->decimal('cost_price', 10, 2)->default(0)->after('unit_price'); // FIFO Cost Snapshot }); @@ -34,9 +32,7 @@ public function down(): void Schema::table('cities', function (Blueprint $table) { $table->dropColumn('province'); }); - - Schema::table('order_items', function (Blueprint $table) { $table->dropColumn('cost_price'); }); diff --git a/database/migrations/2026_01_03_122955_update_orders_reseller_id_fk.php b/database/migrations/2026_01_03_122955_update_orders_reseller_id_fk.php index 92eae9b..468ab2c 100644 --- a/database/migrations/2026_01_03_122955_update_orders_reseller_id_fk.php +++ b/database/migrations/2026_01_03_122955_update_orders_reseller_id_fk.php @@ -14,12 +14,12 @@ public function up(): void Schema::table('orders', function (Blueprint $table) { // Drop existing foreign key (assuming standard naming convention) $table->dropForeign(['reseller_id']); - + // Add new foreign key referencing resellers table $table->foreign('reseller_id') - ->references('id') - ->on('resellers') - ->onDelete('set null'); + ->references('id') + ->on('resellers') + ->onDelete('set null'); }); } @@ -30,12 +30,12 @@ public function down(): void { Schema::table('orders', function (Blueprint $table) { $table->dropForeign(['reseller_id']); - + // Revert to referencing users table $table->foreign('reseller_id') - ->references('id') - ->on('users') - ->onDelete('set null'); + ->references('id') + ->on('users') + ->onDelete('set null'); }); } }; diff --git a/database/migrations/2026_01_03_133335_create_product_variants_table.php b/database/migrations/2026_01_03_133335_create_product_variants_table.php index 8ea9947..d07d00b 100644 --- a/database/migrations/2026_01_03_133335_create_product_variants_table.php +++ b/database/migrations/2026_01_03_133335_create_product_variants_table.php @@ -2,8 +2,8 @@ use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; -use Illuminate\Support\Facades\Schema; use Illuminate\Support\Facades\DB; +use Illuminate\Support\Facades\Schema; return new class extends Migration { @@ -41,7 +41,7 @@ public function up(): void 'limit_price' => $product->limit_price, 'alert_quantity' => $product->alert_quantity, 'quantity' => $product->quantity, - 'image' => null, + 'image' => null, 'created_at' => $product->created_at, 'updated_at' => $product->updated_at, ]); @@ -50,9 +50,10 @@ public function up(): void // Remove columns from products table safely Schema::table('products', function (Blueprint $table) { if (Schema::hasColumn('products', 'unit_id')) { - try { + try { $table->dropForeign(['unit_id']); - } catch (\Exception $e) {} + } catch (\Exception $e) { + } } }); @@ -71,7 +72,7 @@ public function up(): void $columnsToDrop[] = $col; } } - if (!empty($columnsToDrop)) { + if (! empty($columnsToDrop)) { $table->dropColumn($columnsToDrop); } }); @@ -104,10 +105,10 @@ public function down(): void 'quantity' => $firstVariant->quantity, ]); } - + // Restore unique constraint on SKU after populating Schema::table('products', function (Blueprint $table) { - $table->unique('sku'); + $table->unique('sku'); }); Schema::dropIfExists('product_variants'); diff --git a/database/migrations/2026_02_04_194259_upgrade_orders_structure.php b/database/migrations/2026_02_04_194259_upgrade_orders_structure.php index 2a60c1c..e18742e 100644 --- a/database/migrations/2026_02_04_194259_upgrade_orders_structure.php +++ b/database/migrations/2026_02_04_194259_upgrade_orders_structure.php @@ -12,32 +12,32 @@ public function up(): void { Schema::table('orders', function (Blueprint $table) { - if (!Schema::hasColumn('orders', 'order_date')) { + if (! Schema::hasColumn('orders', 'order_date')) { $table->date('order_date')->after('order_number')->useCurrent(); } - if (!Schema::hasColumn('orders', 'order_type')) { + if (! Schema::hasColumn('orders', 'order_type')) { $table->string('order_type')->default('direct')->after('order_date'); // 'direct', 'reseller' } - if (!Schema::hasColumn('orders', 'customer_id')) { + if (! Schema::hasColumn('orders', 'customer_id')) { $table->foreignId('customer_id')->nullable()->after('reseller_id')->constrained()->nullOnDelete(); } - if (!Schema::hasColumn('orders', 'total_cost')) { + if (! Schema::hasColumn('orders', 'total_cost')) { $table->decimal('total_cost', 15, 2)->default(0)->after('total_amount'); } - if (!Schema::hasColumn('orders', 'total_commission')) { + if (! Schema::hasColumn('orders', 'total_commission')) { $table->decimal('total_commission', 15, 2)->default(0)->after('total_cost'); } }); Schema::table('order_items', function (Blueprint $table) { - if (!Schema::hasColumn('order_items', 'product_variant_id')) { - $table->foreignId('product_variant_id')->nullable()->after('product_id')->constrained()->nullOnDelete(); + if (! Schema::hasColumn('order_items', 'product_variant_id')) { + $table->foreignId('product_variant_id')->nullable()->after('product_id')->constrained()->nullOnDelete(); } - if (!Schema::hasColumn('order_items', 'base_price')) { - $table->decimal('base_price', 15, 2)->default(0)->after('unit_price'); // Snapshot of limit_price/cost + if (! Schema::hasColumn('order_items', 'base_price')) { + $table->decimal('base_price', 15, 2)->default(0)->after('unit_price'); // Snapshot of limit_price/cost } - if (!Schema::hasColumn('order_items', 'subtotal')) { - $table->decimal('subtotal', 15, 2)->default(0)->after('total_price'); + if (! Schema::hasColumn('order_items', 'subtotal')) { + $table->decimal('subtotal', 15, 2)->default(0)->after('total_price'); } }); } @@ -61,10 +61,10 @@ public function down(): void Schema::table('order_items', function (Blueprint $table) { $columns = ['product_variant_id', 'base_price', 'subtotal']; - foreach ($columns as $column) { + foreach ($columns as $column) { if (Schema::hasColumn('order_items', $column)) { if ($column === 'product_variant_id') { - $table->dropForeign(['product_variant_id']); + $table->dropForeign(['product_variant_id']); } $table->dropColumn($column); } diff --git a/database/migrations/2026_02_04_210320_add_fulfillment_details_to_orders_table.php b/database/migrations/2026_02_04_210320_add_fulfillment_details_to_orders_table.php index 71610c4..482b714 100644 --- a/database/migrations/2026_02_04_210320_add_fulfillment_details_to_orders_table.php +++ b/database/migrations/2026_02_04_210320_add_fulfillment_details_to_orders_table.php @@ -12,28 +12,28 @@ public function up(): void { Schema::table('orders', function (Blueprint $table) { - if (!Schema::hasColumn('orders', 'courier_id')) { + if (! Schema::hasColumn('orders', 'courier_id')) { $table->foreignId('courier_id')->nullable()->constrained()->onDelete('set null'); } - if (!Schema::hasColumn('orders', 'courier_charge')) { + if (! Schema::hasColumn('orders', 'courier_charge')) { $table->decimal('courier_charge', 10, 2)->default(0); } - if (!Schema::hasColumn('orders', 'payment_method')) { + if (! Schema::hasColumn('orders', 'payment_method')) { $table->string('payment_method')->nullable(); } - if (!Schema::hasColumn('orders', 'call_status')) { + if (! Schema::hasColumn('orders', 'call_status')) { $table->string('call_status')->nullable(); } - if (!Schema::hasColumn('orders', 'sales_note')) { + if (! Schema::hasColumn('orders', 'sales_note')) { $table->text('sales_note')->nullable(); } - if (!Schema::hasColumn('orders', 'customer_city')) { + if (! Schema::hasColumn('orders', 'customer_city')) { $table->string('customer_city')->nullable(); } - if (!Schema::hasColumn('orders', 'customer_district')) { + if (! Schema::hasColumn('orders', 'customer_district')) { $table->string('customer_district')->nullable(); } - if (!Schema::hasColumn('orders', 'customer_province')) { + if (! Schema::hasColumn('orders', 'customer_province')) { $table->string('customer_province')->nullable(); } }); diff --git a/database/migrations/2026_02_04_213316_create_purchases_table.php b/database/migrations/2026_02_04_213316_create_purchases_table.php index dda82a6..772c189 100644 --- a/database/migrations/2026_02_04_213316_create_purchases_table.php +++ b/database/migrations/2026_02_04_213316_create_purchases_table.php @@ -17,14 +17,14 @@ public function up(): void $table->foreignId('supplier_id')->constrained()->onDelete('cascade'); $table->date('purchase_date'); $table->string('currency')->default('LKR'); - + // Financials $table->decimal('sub_total', 12, 2)->default(0); $table->string('discount_type')->default('fixed'); // fixed or percentage $table->decimal('discount_value', 12, 2)->default(0); // The entered value (e.g., 10 for 10%) $table->decimal('discount_amount', 12, 2)->default(0); // The calculated amount to subtract $table->decimal('net_total', 12, 2)->default(0); - + // Payments $table->decimal('paid_amount', 12, 2)->default(0); $table->string('payment_method')->nullable(); // Cash, Card, Cheque, Other diff --git a/database/migrations/2026_02_09_031541_add_remaining_quantity_to_purchase_items_table.php b/database/migrations/2026_02_09_031541_add_remaining_quantity_to_purchase_items_table.php new file mode 100644 index 0000000..b6b7610 --- /dev/null +++ b/database/migrations/2026_02_09_031541_add_remaining_quantity_to_purchase_items_table.php @@ -0,0 +1,30 @@ +integer('remaining_quantity')->default(0)->after('quantity'); + $table->index('remaining_quantity'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('purchase_items', function (Blueprint $table) { + $table->dropIndex(['remaining_quantity']); + $table->dropColumn('remaining_quantity'); + }); + } +}; diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php index cd6e42d..75cd187 100644 --- a/database/seeders/DatabaseSeeder.php +++ b/database/seeders/DatabaseSeeder.php @@ -2,7 +2,6 @@ namespace Database\Seeders; -use App\Models\User; use Illuminate\Database\Console\Seeds\WithoutModelEvents; use Illuminate\Database\Seeder; diff --git a/database/seeders/RolesAndPermissionsSeeder.php b/database/seeders/RolesAndPermissionsSeeder.php index c77221b..c4e8fb0 100644 --- a/database/seeders/RolesAndPermissionsSeeder.php +++ b/database/seeders/RolesAndPermissionsSeeder.php @@ -2,12 +2,11 @@ namespace Database\Seeders; -use Illuminate\Database\Console\Seeds\WithoutModelEvents; -use Illuminate\Database\Seeder; -use Spatie\Permission\Models\Role; -use Spatie\Permission\Models\Permission; use App\Models\User; +use Illuminate\Database\Seeder; use Illuminate\Support\Facades\Hash; +use Spatie\Permission\Models\Permission; +use Spatie\Permission\Models\Role; class RolesAndPermissionsSeeder extends Seeder { @@ -67,13 +66,13 @@ public function run(): void ); // Ensure password is correct if user already existed - if (!$superAdmin->wasRecentlyCreated) { - $superAdmin->password = 'password'; - $superAdmin->save(); + if (! $superAdmin->wasRecentlyCreated) { + $superAdmin->password = 'password'; + $superAdmin->save(); } $superAdmin->assignRole('super admin'); - + // Display warning in console $this->command->warn('⚠️ WARNING: Default super admin created with password "password"'); $this->command->warn(' Please change this password immediately in production!'); diff --git a/database/seeders/StockReportSeeder.php b/database/seeders/StockReportSeeder.php new file mode 100644 index 0000000..d35467d --- /dev/null +++ b/database/seeders/StockReportSeeder.php @@ -0,0 +1,91 @@ + 'Product '.$i, + 'created_at' => now(), + 'updated_at' => now(), + ]; + } + foreach (array_chunk($products, 100) as $chunk) { + Product::insert($chunk); + } + + $productIds = Product::pluck('id')->toArray(); + + // Create a Supplier + $supplier = Supplier::create([ + 'business_name' => 'Test Business', + 'name' => 'Test Supplier', + 'email' => 'supplier@test.com', + 'mobile' => '1234567890', + 'address' => '123 Test St', + ]); + + // Create 500 purchases + $purchases = []; + for ($i = 0; $i < 500; $i++) { + $purchases[] = [ + 'purchase_number' => 'PUR-'.$i, + 'supplier_id' => $supplier->id, + 'purchase_date' => now()->toDateString(), + 'created_at' => now(), + 'updated_at' => now(), + 'sub_total' => 0, + 'net_total' => 0, + 'paid_amount' => 0, + ]; + } + foreach (array_chunk($purchases, 100) as $chunk) { + Purchase::insert($chunk); + } + $purchaseIds = Purchase::pluck('id')->toArray(); + + // Create 5000 purchase items + $purchaseItems = []; + foreach ($purchaseIds as $purchaseId) { + // Each purchase has 10 items + for ($j = 0; $j < 10; $j++) { + $qty = rand(1, 100); + $purchaseItems[] = [ + 'purchase_id' => $purchaseId, + 'product_id' => $productIds[array_rand($productIds)], + 'product_name' => 'Product Name', + 'quantity' => $qty, + 'remaining_quantity' => rand(0, $qty), // Some sold + 'purchase_price' => rand(10, 1000), + 'total' => 0, + 'created_at' => now(), + 'updated_at' => now(), + ]; + } + } + + // Insert in chunks to avoid limits + foreach (array_chunk($purchaseItems, 1000) as $chunk) { + PurchaseItem::insert($chunk); + } + } +} diff --git a/resources/views/reports/stock.blade.php b/resources/views/reports/stock.blade.php index 76a5e01..99165c7 100644 --- a/resources/views/reports/stock.blade.php +++ b/resources/views/reports/stock.blade.php @@ -41,11 +41,11 @@
Total Inventory Value -
{{ number_format($products->sum('stock_value'), 2) }}
+
{{ number_format($totalStockValue, 2) }}
Total Items In Stock -
{{ $products->sum('quantity') }}
+
{{ number_format($totalItemsInStock) }}
@@ -67,7 +67,7 @@
{{ $product->sku }}
- {{ $product->quantity }} + {{ number_format($product->quantity) }} {{ number_format($product->stock_value, 2) }} @@ -78,13 +78,13 @@ @foreach($product->purchaseItems as $batch)
- Batch #{{ $batch->purchase->purchasing_number }} + Batch #{{ $batch->purchase ? $batch->purchase->purchase_number : 'N/A' }} {{ $batch->remaining_quantity }} left - @ {{ number_format($batch->purchasing_price, 2) }} + @ {{ number_format($batch->purchase_price, 2) }}
@endforeach @@ -98,9 +98,9 @@ - Total: - {{ number_format($products->sum('stock_value'), 2) }} - + + {{ $products->links() }} + diff --git a/routes/api.php b/routes/api.php index e63df3f..e0bda93 100644 --- a/routes/api.php +++ b/routes/api.php @@ -1,8 +1,8 @@ user(); diff --git a/routes/web.php b/routes/web.php index cbc1947..5c180e2 100644 --- a/routes/web.php +++ b/routes/web.php @@ -1,12 +1,11 @@ route('login'); @@ -38,9 +37,6 @@ Route::resource('cities', \App\Http\Controllers\CityController::class); }); - - - // Product Management Routes Route::middleware(['auth', 'verified'])->prefix('admin')->group(function () { Route::get('products/export', [\App\Http\Controllers\ProductController::class, 'export'])->name('products.export'); @@ -48,13 +44,13 @@ Route::post('products/bulk-destroy', [\App\Http\Controllers\ProductController::class, 'bulkDestroy'])->name('products.destroy.bulk'); Route::get('products/print-barcodes-bulk', [\App\Http\Controllers\ProductController::class, 'bulkPrintBarcode'])->name('products.barcode.bulk'); Route::get('variants/{variant}/print-barcode', [\App\Http\Controllers\ProductController::class, 'printBarcode'])->name('products.barcode.print'); - + // Product Import Route::get('products/import', [\App\Http\Controllers\ProductImportController::class, 'show'])->name('products.import.show'); Route::get('products/import/template', [\App\Http\Controllers\ProductImportController::class, 'downloadTemplate'])->name('products.import.template'); Route::post('products/import/preview', [\App\Http\Controllers\ProductImportController::class, 'preview'])->name('products.import.preview'); Route::post('products/import/store', [\App\Http\Controllers\ProductImportController::class, 'store'])->name('products.import.store'); - + Route::resource('products', \App\Http\Controllers\ProductController::class); Route::resource('categories', \App\Http\Controllers\CategoryController::class); Route::resource('sub-categories', \App\Http\Controllers\SubCategoryController::class); @@ -74,18 +70,18 @@ Route::get('/create', [\App\Http\Controllers\OrderController::class, 'create'])->name('create'); Route::post('/', [\App\Http\Controllers\OrderController::class, 'store'])->name('store'); Route::get('/call-list', [\App\Http\Controllers\OrderController::class, 'callList'])->name('call-list'); - + // Search APIs Route::get('/search-products', [\App\Http\Controllers\OrderController::class, 'searchProducts'])->name('search-products'); Route::get('/search-resellers', [\App\Http\Controllers\OrderController::class, 'searchResellers'])->name('search-resellers'); - + // CRUD & PDF Route::get('/{order}', [\App\Http\Controllers\OrderController::class, 'show'])->name('show'); Route::get('/{order}/edit', [\App\Http\Controllers\OrderController::class, 'edit'])->name('edit'); Route::put('/{order}', [\App\Http\Controllers\OrderController::class, 'update'])->name('update'); Route::delete('/{order}', [\App\Http\Controllers\OrderController::class, 'destroy'])->name('destroy'); Route::get('/{order}/pdf', [\App\Http\Controllers\OrderController::class, 'downloadPdf'])->name('pdf'); - + // Status update Route::post('/{id}/status', [\App\Http\Controllers\OrderController::class, 'updateStatus'])->name('status.update'); @@ -137,5 +133,3 @@ }); require __DIR__.'/auth.php'; - - diff --git a/server_utils/debug_login.php b/server_utils/debug_login.php index e572ebb..01acac5 100644 --- a/server_utils/debug_login.php +++ b/server_utils/debug_login.php @@ -3,7 +3,6 @@ * Simple Login Debug - No Laravel Bootstrap * Direct database connection only */ - error_reporting(E_ALL); ini_set('display_errors', 1); @@ -48,42 +47,42 @@ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, ]); - + echo "
✅ Database connected successfully
"; - + $email = 'admin@shoppy-max.com'; $testPassword = 'password'; - + // Find user - echo "

1️⃣ User Database Check

"; - - $stmt = $pdo->prepare("SELECT * FROM users WHERE email = ?"); + echo '

1️⃣ User Database Check

'; + + $stmt = $pdo->prepare('SELECT * FROM users WHERE email = ?'); $stmt->execute([$email]); $user = $stmt->fetch(); - - if (!$user) { + + if (! $user) { echo "
❌ User NOT FOUND with email: {$email}
"; - echo "

Checking all users in database...

"; - $allUsers = $pdo->query("SELECT id, name, email, user_type FROM users")->fetchAll(); - echo "
" . print_r($allUsers, true) . "
"; + echo '

Checking all users in database...

'; + $allUsers = $pdo->query('SELECT id, name, email, user_type FROM users')->fetchAll(); + echo '
'.print_r($allUsers, true).'
'; exit; } - + echo "
✅ User found in database
"; - echo ""; + echo '
'; echo ""; echo ""; echo ""; echo ""; - echo ""; + echo ''; echo ""; - echo ""; - echo ""; - echo ""; - echo "
ID{$user['id']}
Name{$user['name']}
Email{$user['email']}
User Type{$user['user_type']}
Email Verified" . ($user['email_verified_at'] ? '✅ Yes - ' . $user['email_verified_at'] : '❌ No') . "
Email Verified'.($user['email_verified_at'] ? '✅ Yes - '.$user['email_verified_at'] : '❌ No').'
Created At{$user['created_at']}
Password Hash (first 60 chars)" . substr($user['password'], 0, 60) . "
Hash Length" . strlen($user['password']) . " characters
Hash Type" . (strpos($user['password'], '$2y$') === 0 ? '✅ Bcrypt' : '⚠️ Unknown') . "
"; - + echo 'Password Hash (first 60 chars)'.substr($user['password'], 0, 60).''; + echo 'Hash Length'.strlen($user['password']).' characters'; + echo 'Hash Type'.(strpos($user['password'], '$2y$') === 0 ? '✅ Bcrypt' : '⚠️ Unknown').''; + echo ''; + // Check roles - echo "

2️⃣ Roles Check

"; + echo '

2️⃣ Roles Check

'; $stmt = $pdo->prepare(" SELECT r.name FROM roles r @@ -92,128 +91,128 @@ "); $stmt->execute([$user['id']]); $roles = $stmt->fetchAll(PDO::FETCH_COLUMN); - + if (count($roles) > 0) { - echo "
✅ User has roles: " . implode(', ', $roles) . "
"; + echo "
✅ User has roles: ".implode(', ', $roles).'
'; } else { echo "
⚠️ User has NO roles assigned!
"; } - + // Test password - echo "

3️⃣ Password Hash Test

"; + echo '

3️⃣ Password Hash Test

'; echo "
Testing password: {$testPassword} against stored hash
"; - + $passwordVerified = password_verify($testPassword, $user['password']); - + if ($passwordVerified) { echo "
"; - echo "

✅ PASSWORD VERIFICATION SUCCESSFUL!

"; + echo '

✅ PASSWORD VERIFICATION SUCCESSFUL!

'; echo "

The password '{$testPassword}' matches the hash in the database.

"; - echo "

This means the issue is NOT with the password hash.

"; - echo "
"; + echo '

This means the issue is NOT with the password hash.

'; + echo ''; } else { echo "
"; - echo "

❌ PASSWORD VERIFICATION FAILED!

"; + echo '

❌ PASSWORD VERIFICATION FAILED!

'; echo "

The password '{$testPassword}' does NOT match the hash in database.

"; - echo "

This is the problem! The password hash needs to be regenerated.

"; - echo "
"; - + echo '

This is the problem! The password hash needs to be regenerated.

'; + echo ''; + // Show fix option echo "
"; - echo "

🔧 Fix Available

"; - + echo '

🔧 Fix Available

'; + if (isset($_GET['fix'])) { // Generate new hash $newHash = password_hash($testPassword, PASSWORD_BCRYPT, ['cost' => 12]); - + // Update database - $stmt = $pdo->prepare("UPDATE users SET password = ? WHERE id = ?"); + $stmt = $pdo->prepare('UPDATE users SET password = ? WHERE id = ?'); $stmt->execute([$newHash, $user['id']]); - + echo "
"; - echo "

✅ PASSWORD FIXED!

"; - echo "

New password hash has been generated and saved.

"; - echo "

New hash: " . substr($newHash, 0, 60) . "...

"; - echo "

Now try logging in!

"; + echo '

✅ PASSWORD FIXED!

'; + echo '

New password hash has been generated and saved.

'; + echo '

New hash: '.substr($newHash, 0, 60).'...

'; + echo '

Now try logging in!

'; echo "

Go to Login Page

"; - echo "
"; - + echo '
'; + // Verify the fix $verifyNew = password_verify($testPassword, $newHash); - echo "

Verification test: " . ($verifyNew ? '✅ New hash works!' : '❌ Something went wrong') . "

"; - + echo '

Verification test: '.($verifyNew ? '✅ New hash works!' : '❌ Something went wrong').'

'; + } else { - echo "

Click the button below to automatically fix the password hash:

"; + echo '

Click the button below to automatically fix the password hash:

'; echo "🔧 Fix Password Hash Now"; } - echo ""; + echo ''; } - + // Additional checks - echo "

4️⃣ Additional Information

"; - - echo ""; - echo ""; - echo ""; - echo ""; - + echo '

4️⃣ Additional Information

'; + + echo '
PHP Version" . phpversion() . "
Password Hashing Available" . (function_exists('password_hash') ? '✅ Yes' : '❌ No') . "
Bcrypt Available" . (defined('PASSWORD_BCRYPT') ? '✅ Yes' : '❌ No') . "
'; + echo ''; + echo ''; + echo ''; + // Test hash generation $testHash = password_hash('test123', PASSWORD_BCRYPT); $testVerify = password_verify('test123', $testHash); - echo ""; - echo "
PHP Version'.phpversion().'
Password Hashing Available'.(function_exists('password_hash') ? '✅ Yes' : '❌ No').'
Bcrypt Available'.(defined('PASSWORD_BCRYPT') ? '✅ Yes' : '❌ No').'
Test Hash/Verify" . ($testVerify ? '✅ Working' : '❌ Not working') . "
"; - + echo 'Test Hash/Verify'.($testVerify ? '✅ Working' : '❌ Not working').''; + echo ''; + // Summary - echo "

5️⃣ Summary & Next Steps

"; - + echo '

5️⃣ Summary & Next Steps

'; + if ($passwordVerified) { echo "
"; - echo "

✅ DIAGNOSIS: Password is Correct!

"; - echo "

The password hash is working correctly. The login issue might be caused by:

"; - echo ""; - echo "

Try these solutions:

"; - echo "
    "; - echo "
  1. Clear browser cookies and cache
  2. "; - echo "
  3. Try in incognito/private window
  4. "; - echo "
  5. Check Laravel logs in storage/logs/laravel.log
  6. "; - echo "
  7. Run: php artisan cache:clear and php artisan config:clear
  8. "; - echo "
"; - echo "
"; + echo '

✅ DIAGNOSIS: Password is Correct!

'; + echo '

The password hash is working correctly. The login issue might be caused by:

'; + echo ''; + echo '

Try these solutions:

'; + echo '
    '; + echo '
  1. Clear browser cookies and cache
  2. '; + echo '
  3. Try in incognito/private window
  4. '; + echo '
  5. Check Laravel logs in storage/logs/laravel.log
  6. '; + echo '
  7. Run: php artisan cache:clear and php artisan config:clear
  8. '; + echo '
'; + echo ''; } else { echo "
"; - echo "

❌ DIAGNOSIS: Password Hash is Incorrect!

"; + echo '

❌ DIAGNOSIS: Password Hash is Incorrect!

'; echo "

Action Required: Click the 'Fix Password Hash Now' button above.

"; - echo "
"; + echo ''; } - + echo "
"; - echo "

⚠️ SECURITY REMINDER

"; - echo "

DELETE this file (debug_login_simple.php) from your server immediately after fixing!

"; - echo "
"; - + echo '

⚠️ SECURITY REMINDER

'; + echo '

DELETE this file (debug_login_simple.php) from your server immediately after fixing!

'; + echo ''; + echo "
"; - echo "

📋 Login Details

"; + echo '

📋 Login Details

'; echo "

Login URL: https://shoppymax.codezela.com/login

"; - echo "

Email: admin@shoppy-max.com

"; - echo "

Password: password

"; - echo "
"; - + echo '

Email: admin@shoppy-max.com

'; + echo '

Password: password

'; + echo ''; + } catch (PDOException $e) { echo "
"; - echo "

❌ Database Connection Error

"; - echo "

Error: " . htmlspecialchars($e->getMessage()) . "

"; - echo "

Check the database credentials at the top of this file.

"; - echo "
"; + echo '

❌ Database Connection Error

'; + echo '

Error: '.htmlspecialchars($e->getMessage()).'

'; + echo '

Check the database credentials at the top of this file.

'; + echo ''; } catch (Exception $e) { echo "
"; - echo "

❌ Error

"; - echo "

" . htmlspecialchars($e->getMessage()) . "

"; - echo "
"; + echo '

❌ Error

'; + echo '

'.htmlspecialchars($e->getMessage()).'

'; + echo ''; } ?>