acct_type, [ // UserTypes::ULTIMATE, // UserTypes::OPERATOR, // UserTypes::SUPER_OPERATOR, // ], true); // if (!$user || !$IsAdmin) { // return response()->json(['success' => false, 'message' => 'Unauthorized '], 403); // } $canCreateGlobal = ProductPermissions::isActionAllowed(UserActions::CreateProductGlobal); $canCreateForOwnStore = ProductPermissions::isActionAllowed(UserActions::CreateProductForOwnStore); if (!$canCreateGlobal && !$canCreateForOwnStore) { return ResponseHelper::returnUnauthorized(); } // Validate request input $validated = $request->validate([ 'NewProductName' => 'required|string|max:255', 'NewProductAvailable' => 'required|numeric', 'NewProductBarcode' => 'nullable|string|max:255', 'NewProductCategory' => 'nullable|string|max:255', 'NewProductSubCategory' => 'nullable|string|max:255', 'NewProductDescription' => 'required|string', 'NewProductPrice' => 'required|numeric|min:0', 'NewProductUnitName' => 'required|string|max:100', 'files' => 'nullable|array', 'photourl' => 'nullable|array', 'TargetStore' => 'nullable|string', ]); $targetStoreHash = $validated['TargetStore'] ?? null; $targetStore = null; if (!$canCreateGlobal && !$targetStoreHash) { return ResponseHelper::returnUnauthorized('A target store is required to create a product.'); } if ($targetStoreHash) { $targetStore = Store::where('hashkey', $targetStoreHash)->first(); if (!$targetStore) { return response()->json(['success' => false, 'message' => 'Target store not found'], 404); } // Permission check for target store /** @var User $user */ $user = Auth::user(); $acctType = $user->acct_type instanceof UserTypes ? $user->acct_type : UserTypes::tryFrom($user->acct_type); $isBig3 = in_array($acctType, [UserTypes::ULTIMATE, UserTypes::SUPER_OPERATOR, UserTypes::OPERATOR]); $isAllowedAccess = false; if ($isBig3) { $isAllowedAccess = true; } else { // Owner, Primary Manager, or New Managers list if ($targetStore->owner_id === $user->id || $targetStore->manager_id === $user->id) { $isAllowedAccess = true; } elseif ($targetStore->managers()->where('user_id', $user->id)->exists()) { $isAllowedAccess = true; } else { // Ancestor check if (UserPermissions::isDescendantOfCurrentUser($targetStore->owner_id)) { $isAllowedAccess = true; } else { $managerIds = $targetStore->managerUsers()->pluck('users.id')->toArray(); foreach ($managerIds as $managerId) { if (UserPermissions::isDescendantOfCurrentUser($managerId)) { $isAllowedAccess = true; break; } } } } } if (!$isAllowedAccess) { return ResponseHelper::returnUnauthorized(); } } // Create new product $product = Product::create([ 'name' => $validated['NewProductName'], 'description' => $validated['NewProductDescription'], 'price' => $validated['NewProductPrice'], 'unitname' => $validated['NewProductUnitName'], 'available' => $validated['NewProductAvailable'], 'barcode' => $validated['NewProductBarcode'] ?? null, 'category' => $validated['NewProductCategory'] ?? null, 'subcategory' => $validated['NewProductSubCategory'] ?? null, 'photourl' => $validated['photourl'] ?? $validated['files'] ?? null, 'created_by' => $user->id, ]); if ($product) { if ($targetStore) { /** @var Store $targetStore */ /** @var Product $product */ StoreController::attachProducttoStore($targetStore, $product, (int) $product->available, (int) $product->price); } return response()->json([ 'success' => true, 'data' => [ 'hashkey' => $product->hashkey, ], ]); } return response()->json(['success' => false, 'message' => 'Failed to create product'], 500); } private static function searchProductTypes($array, $searchTerm) { $results = []; foreach ($array as $key => $value) { if (stripos($key, $searchTerm) !== false) { $results[$key] = $value; } if (is_array($value)) { $recursiveResults = self::searchProductTypes($value, $searchTerm); foreach ($recursiveResults as $rKey => $rValue) { if (array_key_exists($rKey, $results)) { if (is_array($results[$rKey]) && is_array($rValue)) { $results[$rKey] = array_merge_recursive($results[$rKey], $rValue); } else { $results[$rKey] = array($results[$rKey], $rValue); } } else { $results[$rKey] = $rValue; } } } } return $results; } public function getCategories(Request|false $request = false) { $nonRequest = $request == false; if (!$nonRequest) { $category = $request->input('category'); } else { $category = false; } $categories = array_keys(Config::get('market.productsCategorySubcategory')); if (!$nonRequest) { return response()->json([ 'success' => true, 'categories' => $categories, ]); } return $categories; } public function getSubcategories(Request|false $request = false, string|false $category = false) { $request = $request ?: request(); $category = $request ? $request->input('category') : $category; $categoryList = Config::get('market.productsCategorySubcategory', []); if (!$category || !array_key_exists($category, $categoryList)) { if ($request) { return response()->json([], 400); } return []; } $subcategories = $categoryList[$category] ?? []; if ($request) { return response()->json($subcategories); } return $subcategories; } public static function viewProductDetails(string|false $target = false, $withStores = false) { $hashfromRequest = request()->input('target'); $datafromRequest = request()->input('data'); $targetStoreHash = null; $hashkey = $hashfromRequest ?? $target; $storeProductAvailable = null; $storeProductPrice = null; $storeProductIsActive = null; $storeProductRemarks = null; $storeProductDescription = null; $storeProductSold = null; $storeProductReviews = null; $isFromStore = false; $storeProductQrCode = null; if ($datafromRequest && is_array($datafromRequest) && isset($datafromRequest['store_hash'])) { $targetStoreHash = $datafromRequest['store_hash']; try { $targetStore = Store::where('hashkey', $targetStoreHash)->firstOrFail(); } catch (\Throwable $th) { return ResponseHelper::returnIncorrectDetails(); } } else { $targetStore = null; } if (!$hashkey || is_numeric($hashkey)) { return ResponseHelper::returnFalseOrNull($hashfromRequest); } $product = Product::where('hashkey', $hashkey) ->first(); if (!$product) { return ResponseHelper::returnFalseOrNull($hashfromRequest); } if ($targetStore) { $storeProduct = $targetStore->products() ->where('prd_items.id', $product->id) ->first(); if (!$storeProduct) { return ResponseHelper::returnFalseOrNull($hashfromRequest); } $isFromStore = true; $storeProductAvailable = $storeProduct->pivot->available; $storeProductPrice = $storeProduct->pivot->price; $storeProductIsActive = $storeProduct->pivot->is_active; $storeProductRemarks = $storeProduct->pivot->remarks; $storeProductDescription = $storeProduct->pivot->description; $storeProductSold = $storeProduct->pivot->sold; $storeSoldToday = ProductTransaction::where('product_id', $product->id) ->where('store_id', $targetStore->id) ->whereDate('created_at', today()) ->sum('quantity'); $storeProductReviews = $storeProduct->pivot->reviews; // Store specific QR code: hashkey of product + store $storeProductQrCode = md5($product->hashkey . $targetStore->hashkey); } $soldToday = ProductTransaction::where('product_id', $product->id) ->whereDate('created_at', today()) ->sum('quantity'); // $productActive = $product->status === 'active'; // $productStockAvailable = $product->available > 0; // $storeActive = $store && $store->status === 'active'; // if (!$store || !$productActive || !$productStockAvailable || !$storeActive) { // if ($hashfromRequest) { // return response()->json(['success' => false]); // } else { // return null; // } // } $data = [ 'hashkey' => $product->hashkey, 'name' => $product->name, 'store_description' => $storeProductDescription, 'description' => $product->description, 'category' => $product->category, 'subcategory' => $product->subcategory, 'unitname' => $product->unitname, 'is_from_store' => $isFromStore, 'price' => $product->price, 'store_price' => $storeProductPrice, 'store_available' => $storeProductAvailable, 'available' => $product->available, 'store_sold' => $storeProductSold, 'sold' => $product->sold, 'sold_today' => (int) $soldToday, 'store_sold_today' => isset($storeSoldToday) ? (int) $storeSoldToday : null, 'store_reviews' => $storeProductReviews, 'reviews' => $product->reviews, 'store_remarks' => $storeProductRemarks, 'remarks' => $product->remarks, 'photourl' => $product->photourl, 'barcode' => $product->barcode, 'pos_qrcode' => $storeProductQrCode ?? $product->barcode ?? $product->hashkey, 'is_active' => $storeProductIsActive ?? $product->is_active, ]; if ($withStores) { $data['store_options'] = Store::select(['hashkey', 'name']) ->get() ->map(function ($store) { return [$store->hashkey, $store->name]; }); } if ($data['photourl']) { $data['photourlDropzone'] = PhotoURL::photoURLArraytoDropzoneData($data['photourl']); } if ($hashfromRequest) { return ResponseHelper::returnSuccessResponse($data, $data['hashkey']); } else { return $data; } } public static function viewProductDetailsByStoreEdit(string|false $target = false, $withStores = false) { // This method is intended to be called when we specifically want to view a product // in the context of a store (e.g., for store-specific editing). // It ensures store_hash is present or identifies it from context. $req = request(); $target = $target ?: $req->input('target'); $storeHash = $req->input('store_hash') ?? ($req->input('data')['store_hash'] ?? null); if (!$storeHash) { // Fallback: If no store hash is provided, maybe we shouldn't allow store-specific view? // But for now, let's let viewProductDetails handle the fallback. } return self::viewProductDetails($target, $withStores); } public static function editProductAdmin(Request|false $request = false, string|false $target = false) { /** @var User $user */ $user = Auth::user(); $acctType = $user->acct_type instanceof UserTypes ? $user->acct_type : UserTypes::tryFrom($user->acct_type); $isBig3 = in_array($acctType, [UserTypes::ULTIMATE, UserTypes::SUPER_OPERATOR, UserTypes::OPERATOR]); $req = $request ?: request(); $hash = $target ?: ($req?->input('target') ?? null); $data = $req?->input('data') ?? []; $storeHash = $data['store_hash'] ?? null; // Global edit requires ModifyAllProducts. Per-store edits are gated // below by per-store ownership instead. $canModifyGlobal = ProductPermissions::isActionAllowed(UserActions::ModifyAllProducts); $canModifyOwnStoreProduct = ProductPermissions::isActionAllowed(UserActions::ModifyOwnProduct) || ProductPermissions::isActionAllowed(UserActions::AddProducttoOwnStore); if (!$canModifyGlobal && !($storeHash && $canModifyOwnStoreProduct)) { return $target ? false : ResponseHelper::returnUnauthorized(); } if (!$hash) { return $target ? false : response()->json(['success' => false, 'message' => 'No target specified'], 400); } $product = Product::where('hashkey', $hash)->first(); if (!$product) { return $target ? false : response()->json(['success' => false, 'message' => 'Product not found'], 404); } $isStoreUpdate = (bool)$storeHash; $validated = $req->validate([ 'EditProductName' => $isStoreUpdate ? 'nullable|string|max:255' : 'required|string|max:255', 'EditProductAvailable' => 'nullable|numeric', 'EditProductBarcode' => 'nullable|string|max:255', 'EditProductCategory' => 'nullable|string|max:255', 'EditProductSubCategory' => 'nullable|string|max:255', 'EditProductDescription' => 'required|string', 'EditProductPrice' => 'required|numeric|min:0', 'EditProductUnitName' => $isStoreUpdate ? 'nullable|string|max:100' : 'required|string|max:100', 'files' => 'nullable|array', 'photourl' => 'nullable|array', 'status' => 'nullable|boolean', ]); if ($storeHash) { $store = Store::where('hashkey', $storeHash)->first(); if ($store) { $isAllowedAccess = false; if ($isBig3) { $isAllowedAccess = true; } else { if ($store->owner_id === $user->id || $store->manager_id === $user->id || $store->managers()->where('user_id', $user->id)->exists()) { $isAllowedAccess = true; } else { if (UserPermissions::isDescendantOfCurrentUser($store->owner_id)) { $isAllowedAccess = true; } else { $managerIds = $store->managerUsers()->pluck('users.id')->toArray(); foreach ($managerIds as $managerId) { if (UserPermissions::isDescendantOfCurrentUser($managerId)) { $isAllowedAccess = true; break; } } } } } if ($isAllowedAccess) { $product->stores()->syncWithoutDetaching([ $store->id => [ 'available' => $validated['EditProductAvailable'] ?? 0, 'price' => $validated['EditProductPrice'] ?? 0, 'is_active' => $validated['status'] ?? true, 'description' => $validated['EditProductDescription'], 'remarks' => $validated['EditProductDescription'], // Keep for compatibility ] ]); return $target ? true : response()->json([ 'success' => true, 'message' => 'Store-specific product data updated successfully', 'hashkey' => $product->hashkey, ]); } } } // PERMISSION CHECK: Restrict global product editing exclusively to the "Big 3" roles. if (!$isBig3) { return $target ? false : ResponseHelper::returnError('Global product editing is restricted to administrators.', 403); } $product->update([ 'name' => $validated['EditProductName'], 'description' => $validated['EditProductDescription'], 'price' => $validated['EditProductPrice'], 'unitname' => $validated['EditProductUnitName'], 'available' => $validated['EditProductAvailable'] ?? $product->available, 'barcode' => $validated['EditProductBarcode'] ?? null, 'category' => $validated['EditProductCategory'] ?? null, 'subcategory' => $validated['EditProductSubCategory'] ?? null, 'photourl' => $validated['photourl'] ?? $validated['files'] ?? $product->photourl, 'is_active' => $validated['status'] ?? $product->is_active, ]); return $target ? true : response()->json([ 'success' => true, 'message' => $product->wasChanged() ? 'Product updated successfully' : 'No changes were needed', 'hashkey' => $product->hashkey, ]); } public static function editProductAdminByStore(Request|false $request = false, string|false $target = false) { $req = $request ?: request(); $hash = $target ?: ($req?->input('target') ?? null); if (!$hash) { return $target ? false : response()->json(['success' => false, 'message' => 'No target specified'], 400); } $product = Product::where('hashkey', $hash)->first(); if (!$product) { return $target ? false : response()->json(['success' => false, 'message' => 'Product not found'], 404); } // return response()->json(['success' => false, 'message' => 'Product not found'], 404); $validated = $req->validate([ 'EditProductName' => 'required|string|max:255', // 'EditProductAvailable' => 'required|numeric', 'EditProductBarcode' => 'nullable|string|max:255', 'EditProductCategory' => 'nullable|string|max:255', 'EditProductSubCategory' => 'nullable|string|max:255', 'EditProductDescription' => 'required|string', 'EditProductPrice' => 'required|numeric|min:0', 'EditProductUnitName' => 'required|string|max:100', 'files' => 'nullable|array', // 'status' => 'nullable|boolean', ]); //TODO Check first if store_id is owned by current user or is descendant //TODO Then update the pivot table instead of the main product table $product->update([ // 'name' => $validated['EditProductName'], 'description' => $validated['EditProductDescription'], 'price' => $validated['EditProductPrice'], // 'available' => $validated['EditProductAvailable'], // 'barcode' => $validated['EditProductBarcode'] ?? null, // 'is_active' => $validated['status'] ?? $product->is_active, // uncomment if you include this field ]); if ($product->wasChanged()) { return $target ? true : response()->json([ 'success' => true, 'message' => 'Product updated successfully', 'hashkey' => $product->hashkey, ]); } return $target ? false : response()->json([ 'success' => false, 'message' => 'No changes were made', ], 500); } public function listProductsData(Request $request) { // $wholesaleOnly = $request->boolean('wholesale_only', false); // Fetch active and available products $user = Auth::user(); $UltOpsSupOps = $user && in_array($user->acct_type, [ UserTypes::ULTIMATE, UserTypes::OPERATOR, UserTypes::SUPER_OPERATOR, ], true); $storeHash = $request->input('store_hash'); $accessKey = $request->input('access_key'); $targetStore = null; $sessionHash = $request->input('session_hash'); if ($accessKey) { $keyObj = \App\Models\Market\PosAccessKey::where('access_key', $accessKey)->first(); if ($keyObj) { $targetStore = $keyObj->store; } } if (!$targetStore && $sessionHash) { $sessionObj = \App\Models\Market\PosSession::where('hashkey', $sessionHash)->first(); if ($sessionObj) { $targetStore = $sessionObj->store; } } if (!$targetStore && $storeHash) { $targetStore = Store::where('hashkey', $storeHash)->first(); } if ($targetStore) { $products = $targetStore->products() ->where('prd_str.is_active', true) ->get(); } elseif ($UltOpsSupOps) { $products = Product::select(['description', 'name', 'price', 'unitname', 'photourl', 'hashkey', 'barcode'])->get(); } else { $products = Product::where('is_active', true) ->get(); } $products = $products->map(function ($product) use ($targetStore) { $photo = $product->photourl; $price = $product->price; $qrcode = $product->barcode ?? $product->hashkey; $available = $product->available; if ($targetStore && isset($product->pivot)) { $price = $product->pivot->price ?? $product->price; $qrcode = md5($product->hashkey . $targetStore->hashkey); $available = $product->pivot->available ?? $product->available; } return [ 'description' => $product->description, 'name' => $product->name, 'price' => $price, 'unit' => $product->unitname, 'photo' => $photo, 'hashkey' => $product->hashkey, 'barcode' => $product->barcode, 'qrcode' => $qrcode, 'category' => $product->category, 'available' => $available, ]; }); return response()->json($products); } public function viewProductwithAddStoreData() { $hashfromRequest = request()->input('target'); if (!$hashfromRequest || is_numeric($hashfromRequest)) { return ResponseHelper::returnFalseOrNull($hashfromRequest); } return self::viewProductDetails($hashfromRequest, true); } public function AddProducttoStore() { /** @var User $user */ $user = Auth::user(); if (!$user) { return ResponseHelper::returnUnauthorized(); } $storeHash = request()->input('TargetStore'); $productHash = request()->input('target'); if (!$storeHash || !$productHash) { return ResponseHelper::returnIncorrectDetails(); } // Check both global and own-store permissions $allowedAny = ProductPermissions::isActionAllowed(userActions::AddProducttoAnyStore); if (!$allowedAny) { $allowedOwn = ProductPermissions::isActionAllowed(userActions::AddProducttoOwnStore, $productHash, $storeHash); if (!$allowedOwn) { return ResponseHelper::returnUnauthorized(); } } try { /** @var Store $store */ $storeObj = Store::where('hashkey', $storeHash)->firstOrFail(); /** @var Product $product */ $productObj = Product::where('hashkey', $productHash)->firstOrFail(); } catch (ModelNotFoundException $e) { return ResponseHelper::returnFalseOrNull('Store or Product not found'); } try { StoreController::attachProducttoStore($storeObj, $productObj); return ResponseHelper::returnSuccessResponse([], $productObj); } catch (\Throwable $th) { if (str_contains($th->getMessage(), 'Duplicate entry')) { return ResponseHelper::returnError('Already Added to Store'); } return ResponseHelper::returnError('Failed to add product to store: ' . $th->getMessage()); } } public function RemoveProductFromStore() { /** @var User $user */ $user = Auth::user(); if (!$user) { return ResponseHelper::returnUnauthorized(); } $storeHash = request()->input('TargetStore'); $productHash = request()->input('target'); if (!$storeHash || !$productHash) { return ResponseHelper::returnIncorrectDetails(); } try { $store = Store::where('hashkey', $storeHash)->firstOrFail(); $product = Product::where('hashkey', $productHash)->firstOrFail(); } catch (ModelNotFoundException $e) { return ResponseHelper::returnFalseOrNull('Store or Product not found'); } $acctType = $user->acct_type instanceof UserTypes ? $user->acct_type : UserTypes::tryFrom($user->acct_type); $isBig3 = in_array($acctType, [UserTypes::ULTIMATE, UserTypes::SUPER_OPERATOR, UserTypes::OPERATOR]); $isAllowedAccess = false; if ($isBig3) { $isAllowedAccess = true; } elseif ($store->owner_id === $user->id || $store->manager_id === $user->id || $store->managers()->where('user_id', $user->id)->exists()) { $isAllowedAccess = true; } elseif (UserPermissions::isDescendantOfCurrentUser($store->owner_id)) { $isAllowedAccess = true; } else { foreach ($store->managerUsers()->pluck('users.id')->toArray() as $managerId) { if (UserPermissions::isDescendantOfCurrentUser($managerId)) { $isAllowedAccess = true; break; } } } if (!$isAllowedAccess) { return ResponseHelper::returnUnauthorized(); } try { if ($store->products()->where('prd_items.id', $product->id)->exists()) { $store->products()->detach($product->id); return ResponseHelper::returnSuccessResponse([], $product); } else { return ResponseHelper::returnError('Product not attached to this store'); } } catch (\Throwable $th) { return ResponseHelper::returnFalseOrNull($th->getMessage()); } } /** * Return the set of store hashes (from the user's selectable stores) where this product is currently assigned. */ public function getAssignedStoresForProduct() { /** @var User $user */ $user = Auth::user(); if (!$user) { return ResponseHelper::returnUnauthorized(); } $productHash = request()->input('target'); if (!$productHash) { return ResponseHelper::returnIncorrectDetails(); } try { $product = Product::where('hashkey', $productHash)->firstOrFail(); } catch (ModelNotFoundException $e) { return ResponseHelper::returnFalseOrNull('Product not found'); } $storeHashes = $product->stores()->pluck('str.hashkey')->toArray(); return response()->json([ 'success' => true, 'data' => $storeHashes, ]); } /** * Assign a product to a store with ownership-aware permissions. * - Ultimate/SuperOperator/Operator: can assign to any store * - Store Owner: can assign to stores they own * - Store Manager: can assign to stores they manage */ public function AssignProductToOwnStore() { /** @var User $user */ $user = Auth::user(); if (!$user) { return ResponseHelper::returnUnauthorized(); } $acctType = $user->acct_type instanceof UserTypes ? $user->acct_type : UserTypes::tryFrom($user->acct_type); $isBig3 = in_array($acctType, [UserTypes::ULTIMATE, UserTypes::SUPER_OPERATOR, UserTypes::OPERATOR]); $storeHash = request()->input('TargetStore'); $productHash = request()->input('target'); $price = request()->input('price'); $available = request()->input('available'); $description = request()->input('description'); if (!$storeHash || !$productHash) { return ResponseHelper::returnIncorrectDetails(); } try { $store = Store::where('hashkey', $storeHash)->firstOrFail(); $product = Product::where('hashkey', $productHash)->firstOrFail(); } catch (ModelNotFoundException $e) { return ResponseHelper::returnFalseOrNull('Store or Product not found'); } // Big 3 always allowed. $isAllowedAccess = false; if ($isBig3) { $isAllowedAccess = true; } else { if ($store->owner_id === $user->id || $store->manager_id === $user->id || $store->managers()->where('user_id', $user->id)->exists()) { $isAllowedAccess = true; } else { if (UserPermissions::isDescendantOfCurrentUser($store->owner_id)) { $isAllowedAccess = true; } else { $managerIds = $store->managerUsers()->pluck('users.id')->toArray(); foreach ($managerIds as $managerId) { if (UserPermissions::isDescendantOfCurrentUser($managerId)) { $isAllowedAccess = true; break; } } } } } if (!$isAllowedAccess) { return ResponseHelper::returnUnauthorized(); } try { /** @var Store $store */ /** @var Product $product */ $pivotData = [ 'available' => (int) ($available ?? 0), 'price' => (float) ($price ?? 0), 'is_active' => true, ]; if ($description !== null && $description !== '') { $pivotData['description'] = $description; } $existing = $store->products()->where('prd_items.id', $product->id)->exists(); if ($existing) { $store->products()->updateExistingPivot($product->id, $pivotData); } else { $store->products()->attach($product->id, $pivotData); } return ResponseHelper::returnSuccessResponse( ['store_hash' => $storeHash, 'product_hash' => $productHash], $product->hashkey, 'Product assigned to store successfully' ); } catch (\Throwable $th) { return ResponseHelper::returnError('Failed to assign product to store: ' . $th->getMessage()); } } public function listProducts_Admin(Request $request) { /** @var User $user */ $user = Auth::user(); if (!$user) { return ResponseHelper::returnUnauthorized(); } $acctType = $user->acct_type instanceof UserTypes ? $user->acct_type : UserTypes::tryFrom($user->acct_type); $isBig3 = in_array($acctType, [UserTypes::ULTIMATE, UserTypes::SUPER_OPERATOR, UserTypes::OPERATOR]); if ($isBig3) { $products = Product::select(['id', 'hashkey', 'name', 'price', 'unitname', 'available', 'is_active', 'photourl', 'category', 'subcategory', 'created_by']) ->orderBy('id', 'desc') ->get(); } else { // Non-big 3 users see their own and their descendants' products, // plus any products assigned to stores they own or manage. /** @var User $user */ $allowedIds = array_merge([$user->id], $user->getAllDescendants()->pluck('id')->toArray()); $myStoreIds = Store::where(function ($q) use ($allowedIds) { $q->whereIn('owner_id', $allowedIds) ->orWhereIn('manager_id', $allowedIds) ->orWhereHas('managers', function ($mq) use ($allowedIds) { $mq->whereIn('user_id', $allowedIds); }); })->pluck('id')->toArray(); $assignedProductIds = DB::table('prd_str') ->whereIn('store_id', $myStoreIds) ->pluck('product_id') ->toArray(); $products = Product::where(function ($q) use ($allowedIds, $assignedProductIds) { $q->whereIn('created_by', $allowedIds) ->orWhereIn('id', $assignedProductIds); }) ->select(['id', 'hashkey', 'name', 'price', 'unitname', 'available', 'is_active', 'photourl', 'category', 'subcategory', 'created_by']) ->orderBy('id', 'desc') ->get(); } return response()->json([ 'success' => true, 'products' => $products ]); } public function listGlobalProductsForPicker(Request $request) { /** @var User $user */ $user = Auth::user(); if (!$user) { return ResponseHelper::returnUnauthorized(); } $products = Product::where('is_active', true) ->select(['id', 'hashkey', 'name', 'price', 'unitname', 'available', 'is_active', 'photourl', 'category', 'subcategory', 'description']) ->orderBy('name', 'asc') ->get(); return response()->json([ 'success' => true, 'products' => $products, ]); } public function deleteProduct_Admin(Request $request) { $user = Auth::user(); $isUltimate = $user && $user->acct_type === UserTypes::ULTIMATE; $canDeleteAny = ProductPermissions::isActionAllowed(UserActions::DeleteAllProducts); $canDeleteOwn = ProductPermissions::isActionAllowed(UserActions::DeleteOwnProduct); if (!$canDeleteAny && !$canDeleteOwn) { return ResponseHelper::returnUnauthorized(); } $hash = $request->input('target'); if (!$hash) { return ResponseHelper::returnIncorrectDetails(); } try { $product = Product::where('hashkey', $hash)->firstOrFail(); // PERMISSION CHECK: If not ultimate, must be the creator (or a // descendant ancestor of the creator). if (!$isUltimate && (int) $product->created_by !== (int) $user->id) { if (!UserPermissions::isDescendantOfCurrentUser((int) $product->created_by)) { return ResponseHelper::returnUnauthorized('You can only delete products you created.'); } } $product->delete(); return ResponseHelper::returnSuccessResponse([], $hash, 'Product deleted successfully'); } catch (Exception $e) { return ResponseHelper::returnError('Failed to delete product: ' . $e->getMessage()); } } public function toggleProductStatus_Admin(Request $request) { $user = Auth::user(); $isUltimate = $user && $user->acct_type === UserTypes::ULTIMATE; $hash = $request->input('target'); $data = $request->input('data') ?? []; $storeHash = $data['store_hash'] ?? null; if (!$hash) { return ResponseHelper::returnIncorrectDetails(); } // Toggling a per-store override is gated by store ownership below. // Toggling the global product flag requires ModifyAllProducts. $canModifyGlobal = ProductPermissions::isActionAllowed(UserActions::ModifyAllProducts); $canModifyOwnStoreProduct = ProductPermissions::isActionAllowed(UserActions::ModifyOwnProduct) || ProductPermissions::isActionAllowed(UserActions::AddProducttoOwnStore); if (!$canModifyGlobal && !($storeHash && $canModifyOwnStoreProduct)) { return ResponseHelper::returnUnauthorized(); } try { $product = Product::where('hashkey', $hash)->firstOrFail(); if ($storeHash) { $store = Store::where('hashkey', $storeHash)->first(); if ($store) { $isOwnStore = (int) $store->owner_id === (int) $user->id || (int) $store->manager_id === (int) $user->id; $isDescendant = false; if (!$isUltimate && !$isOwnStore) { $descendants = $user->getAllDescendants(); $descendantIds = $descendants->pluck('id')->toArray(); $isDescendant = in_array($store->owner_id, $descendantIds) || in_array($store->manager_id, $descendantIds); } if ($isUltimate || $isOwnStore || $isDescendant) { $pivot = $product->stores()->where('store_id', $store->id)->first(); $currentStatus = $pivot ? $pivot->pivot->is_active : true; $product->stores()->syncWithoutDetaching([ $store->id => [ 'is_active' => !$currentStatus, ] ]); return ResponseHelper::returnSuccessResponse( ['is_active' => !$currentStatus], $hash, 'Store-specific product status updated successfully' ); } } } // PERMISSION CHECK: If not ultimate, must be the creator if (!$isUltimate && (int) $product->created_by !== (int) $user->id) { return ResponseHelper::returnUnauthorized('You can only modify products you created.'); } $product->is_active = !$product->is_active; $product->save(); return ResponseHelper::returnSuccessResponse( ['is_active' => $product->is_active], $hash, 'Product status updated successfully' ); } catch (Exception $e) { return ResponseHelper::returnError('Failed to update product status: ' . $e->getMessage()); } } /** * Fuzzy-search global products by name so users can choose to import an existing * product into their store instead of creating a duplicate. */ public function fuzzySearchByName(Request $request) { $user = Auth::user(); if (!$user) { return ResponseHelper::returnUnauthorized(); } $canCreateGlobal = ProductPermissions::isActionAllowed(UserActions::CreateProductGlobal); $canCreateForOwnStore = ProductPermissions::isActionAllowed(UserActions::CreateProductForOwnStore); if (!$canCreateGlobal && !$canCreateForOwnStore) { return ResponseHelper::returnUnauthorized(); } $name = trim((string) $request->input('name', '')); $storeHash = $request->input('TargetStore'); $limit = 10; if ($name === '') { return response()->json(['success' => true, 'data' => []]); } $normalized = strtolower($name); $tokens = array_values(array_filter(preg_split('/\s+/', $normalized) ?: [], fn ($t) => strlen($t) >= 2)); $query = Product::query() ->select(['id', 'hashkey', 'name', 'price', 'unitname', 'category', 'subcategory', 'photourl', 'description']) ->where('is_active', 1); $query->where(function ($q) use ($normalized, $tokens) { $q->whereRaw('LOWER(name) LIKE ?', ['%' . $normalized . '%']) ->orWhereRaw('SOUNDEX(name) = SOUNDEX(?)', [$normalized]); foreach ($tokens as $tok) { $q->orWhereRaw('LOWER(name) LIKE ?', ['%' . $tok . '%']); } }); $candidates = $query->limit(50)->get(); // Score candidates by simple similarity; rank desc. $scored = $candidates->map(function ($p) use ($normalized) { $candidate = strtolower((string) $p->name); similar_text($normalized, $candidate, $percent); $contains = str_contains($candidate, $normalized) ? 15 : 0; $p->_score = $percent + $contains; return $p; })->filter(fn ($p) => $p->_score >= 45) ->sortByDesc('_score') ->values() ->take($limit); $alreadyInStoreIds = []; if ($storeHash) { $store = Store::where('hashkey', $storeHash)->first(); if ($store) { $alreadyInStoreIds = $store->products()->pluck('prd_items.id')->toArray(); } } $payload = $scored->map(function ($p) use ($alreadyInStoreIds) { return [ 'hashkey' => $p->hashkey, 'name' => $p->name, 'price' => $p->price, 'unitname' => $p->unitname, 'category' => $p->category, 'subcategory' => $p->subcategory, 'photourl' => $p->photourl, 'description' => $p->description, 'score' => round($p->_score, 1), 'already_in_store' => in_array($p->id, $alreadyInStoreIds, true), ]; }); return response()->json(['success' => true, 'data' => $payload]); } }