Files
BarangaySystem/app/Http/Controllers/Market/ProductController.php
2026-06-06 18:43:00 +08:00

1141 lines
43 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Http\Controllers\Market;
use App\Enums\UserActions;
use App\Enums\UserTypes;
use App\Http\Controllers\Helpers\Permissions\ProductPermissions;
use App\Http\Controllers\Helpers\Permissions\UserPermissions;
use App\Http\Controllers\Helpers\PhotoURL;
use App\Http\Controllers\Helpers\ResponseHelper;
use App\Models\Market\Product;
use App\Models\Market\ProductTransaction;
use App\Models\Market\Store;
use App\Models\User;
use Exception;
use Hyperf\Database\Model\ModelNotFoundException;
use Hypervel\Http\Request;
use Hypervel\Support\Facades\Auth;
use Hypervel\Support\Facades\Config;
use Hypervel\Support\Facades\DB;
use Hypervel\Support\Facades\Response;
class ProductController
{
public function createNew_Admin(Request $request)
{
/** @var User $user */
$user = Auth::user();
//TODO Fix this permissions check to be unified with PRoductpermissions
// $IsAdmin = in_array($user?->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]);
}
}