1141 lines
43 KiB
PHP
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]);
|
|
}
|
|
}
|
|
|
|
|
|
|