json($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.storesCategorySubcategory', []); 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 function store(Request $request) { $validator = Validator::make($request->all(), [ 'name' => 'required|string|max:255|unique:str,name', 'description' => 'required|string', 'address' => 'required|string', 'category' => 'nullable|string|max:100', 'subcategory' => 'nullable|string|max:100', 'remarks' => 'nullable|string', 'photourl' => 'nullable|array', 'files' => 'nullable|array', 'owner' => 'nullable|string', 'manager' => 'nullable|string', 'managers' => 'nullable|array', 'cooperatives' => 'nullable|array', 'cooperatives.*' => 'string', ], [ 'name.unique' => 'A store with this name already exists. Please choose a different name.', ]); if ($validator->fails()) { return response()->json([ 'success' => false, 'message' => $validator->errors()->first(), 'errors' => $validator->errors() ], 422); } $validated = $validator->validated(); $storeCode = $request->input('storecode') ?? self::generateStoreCode($request->input('category', 'General')); /** @var User $currentUser */ $currentUser = Auth::user(); if (!$currentUser) { return ResponseHelper::returnUnauthorized(); } $acctTypeEarly = $currentUser->acct_type instanceof UserTypes ? $currentUser->acct_type : UserTypes::tryFrom($currentUser->acct_type); $isStoreOwner = $acctTypeEarly === UserTypes::STORE_OWNER; // STORE_OWNER: force owner to self, ignore any client-supplied owner/manager if ($isStoreOwner) { $ownerId = $currentUser->id; $managerId = $currentUser->id; } else { $ownerId = ($validated['owner'] ?? null) ? UserController::findUserIdByHash($validated['owner']) : null; $managerId = ($validated['manager'] ?? null) ? UserController::findUserIdByHash($validated['manager']) : null; } $acctType = $acctTypeEarly; $isBig3 = in_array($acctType, [UserTypes::ULTIMATE, UserTypes::SUPER_OPERATOR, UserTypes::OPERATOR]); // RBAC: only Big3 or STORE_OWNER may create stores if (!$isBig3 && !$isStoreOwner) { return response()->json(['success' => false, 'message' => 'You are not allowed to create stores.'], 403); } if (!$isBig3 && !$isStoreOwner && $ownerId) { $isParent = UserPermissions::IsParentofTargetUser($validated['owner']); if (!$isParent && Auth::id() !== $ownerId) { return response()->json(['success' => false, 'message' => 'Unauthorized to create store for this owner'], 403); } } // STORE_OWNER cannot override status — always active $status = $isStoreOwner ? 'active' : $request->input('status', 'active'); $data = [ 'storecode' => $storeCode, 'name' => $validated['name'], 'description' => $validated['description'], 'status' => $status, 'remarks' => $isStoreOwner ? '' : ($validated['remarks'] ?? ''), 'category' => $validated['category'] ?? 'General', 'subcategory' => $validated['subcategory'] ?? '', 'photourl' => $validated['photourl'] ?? $validated['files'] ?? '', 'address' => $validated['address'], 'owner_id' => $ownerId, 'manager_id' => $managerId, 'created_by' => Auth::id(), 'is_active' => true, ]; $store = Store::create($data); // STORE_OWNER auto-self-assigns as manager if ($isStoreOwner) { \App\Models\Market\StoreManager::create([ 'store_id' => $store->id, 'user_id' => $currentUser->id, 'created_by' => $currentUser->id, ]); } // Handle multiple managers. For STORE_OWNER, restrict to STORE_MANAGER descendants. if ($request->filled('managers') && is_array($request->input('managers'))) { $descendantIds = null; if ($isStoreOwner) { $descendantIds = $currentUser->getAllDescendants()->pluck('id')->toArray(); } foreach ($request->input('managers') as $mgrHash) { $mgrId = UserController::findUserIdByHash($mgrHash); if (!$mgrId) { continue; } if ($isStoreOwner) { if (!in_array($mgrId, $descendantIds, true)) { continue; } $mgrUser = User::find($mgrId); $mgrType = $mgrUser?->acct_type instanceof UserTypes ? $mgrUser->acct_type : UserTypes::tryFrom($mgrUser?->acct_type ?? ''); if ($mgrType !== UserTypes::STORE_MANAGER) { continue; } if ($mgrId === $currentUser->id) { continue; // already added above } } \App\Models\Market\StoreManager::create([ 'store_id' => $store->id, 'user_id' => $mgrId, 'created_by' => Auth::id(), ]); } } // Sync cooperative links (optional, many-to-many) — admins only if (!$isStoreOwner) { $this->syncStoreCooperatives($store, $request->input('cooperatives', [])); } return Response::json(['success' => true, 'hashkey' => $store->hashkey], 201); } /** * One-click store creation: assigns the current user as both owner * and sole manager, with sensible defaults. Used by the Store Owner home. */ public function autoCreate(Request $request) { /** @var User $currentUser */ $currentUser = Auth::user(); if (!$currentUser) { return ResponseHelper::returnUnauthorized(); } $acctType = $currentUser->acct_type instanceof UserTypes ? $currentUser->acct_type : UserTypes::tryFrom($currentUser->acct_type); $isBig3 = in_array($acctType, [UserTypes::ULTIMATE, UserTypes::SUPER_OPERATOR, UserTypes::OPERATOR]); $isStoreOwner = $acctType === UserTypes::STORE_OWNER; if (!$isBig3 && !$isStoreOwner) { return response()->json(['success' => false, 'message' => 'You are not allowed to create stores.'], 403); } $displayName = $currentUser->nickname ?: ($currentUser->name ?: 'My'); $name = trim((string) $request->input('name')) ?: ($displayName . "'s Store"); $description = trim((string) $request->input('description')) ?: 'Auto-created store'; $address = trim((string) $request->input('address')) ?: 'Address not set'; $category = $request->input('category', 'General'); // `str.name` is globally unique; if the auto-generated name collides // (two owners sharing a nickname), append a short suffix until free. $baseName = $name; $attempt = 0; while (Store::where('name', $name)->exists()) { $attempt++; if ($attempt > 20) { return response()->json([ 'success' => false, 'message' => 'Could not generate a unique store name. Please use Custom Create.', ], 409); } $name = $baseName . ' #' . strtoupper(substr(bin2hex(random_bytes(2)), 0, 4)); } $store = Store::create([ 'storecode' => self::generateStoreCode($category), 'name' => $name, 'description' => $description, 'status' => 'active', 'remarks' => '', 'category' => $category, 'subcategory' => '', 'photourl' => '', 'address' => $address, 'owner_id' => $currentUser->id, 'manager_id' => $currentUser->id, 'created_by' => $currentUser->id, 'is_active' => true, ]); \App\Models\Market\StoreManager::create([ 'store_id' => $store->id, 'user_id' => $currentUser->id, 'created_by' => $currentUser->id, ]); return Response::json([ 'success' => true, 'hashkey' => $store->hashkey, 'name' => $store->name, ], 201); } public static function generateStoreCode(string $category, ?int $num = null, int $length = 8): string { $characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_'; $charactersLength = strlen($characters); $num = $num ?? rand(1, 99); $randomString = ''; for ($i = 0; $i < $length; $i++) { $randomString .= $characters[rand(0, $charactersLength - 1)]; } $categoryPrefix = substr($category, 0, 2); $categorySuffix = substr($category, -2, 2); $cat = strtoupper($categoryPrefix . $categorySuffix); return date('Y') . '-' . $cat . '-' . $num . '-' . $randomString; } public static function viewStoreDetailsLegacy(Request|false $request = false, string|false $target = false) { try { if ($target) { $hash = $target; } else { $hash = $request->input('target'); } if (!$hash) { return $target ? false : response()->json(['success' => false]); } $store = Store::where('hashkey', $hash)->first(); if (!$store) { return $target ? false : response()->json(['success' => false]); } $products = Product::where('store_id', $store->id) ->select('name', 'hashkey', 'price', 'unitname', 'available', 'is_active as status', 'photourl') ->get() ->filter(fn($product) => $product->available) ->map(function ($product) { $photourl = $product->photourl; if ($photourl && (is_array($photourl) || is_object($photourl))) { $product->photourl = (array)$photourl; } elseif (is_string($photourl)) { $product->photourl = json_decode($photourl, true) ?: []; } else { $product->photourl = []; } return $product; }) ->values(); $store->products = $products; if ($target) { return $store; } else { return response()->json(['success' => true, 'data' => $store]); } } catch (\Exception $e) { return response()->json(['success' => false, 'message' => $e->getMessage()], 500); } } public static function viewStoreDetails(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]); } // Fetch the store by hashkey $store = Store::where('hashkey', $hash) ->select( 'id', 'photourl', 'hashkey', 'name', 'category', 'subcategory', 'created_at as created', 'updated_at as modified', 'description', 'is_active as status', 'address', 'owner_id', 'manager_id' ) ->first(); if (!$store) { return $target ? false : response()->json(['success' => false]); } /** @var User $user */ $user = Auth::user(); $products = $store->products() ->select( 'prd_items.name', 'prd_items.hashkey', 'prd_str.price as store_price', 'prd_items.price', 'prd_items.unitname', 'prd_str.available', 'prd_str.is_active as status', 'prd_items.photourl' ) ->get() //TODO Decide later wheether to include unavailable store products // ->filter(fn($product) => $product->available) ->map(function ($product) { $photourl = $product->photourl; if ($photourl && is_array($photourl)) { $product->photourl = $photourl; } elseif (is_string($photourl)) { $product->photourl = json_decode($photourl, true) ?: []; } else { $product->photourl = []; } return $product; }) ->values(); $store->products = $products; // Resolve store photo URLs: prefer cdn_url, fall back to /RequestData/File/{hash} $rawPhotos = $store->photourl ?? []; if (!empty($rawPhotos) && is_array($rawPhotos)) { $store->resolved_photos = \App\Models\FileList::whereIn('hashkey', $rawPhotos) ->get(['hashkey', 'cdn_url']) ->keyBy('hashkey') ->pipe(function ($fileMap) use ($rawPhotos) { return collect($rawPhotos)->map(function ($hash) use ($fileMap) { $file = $fileMap->get($hash); if (!$file) return null; $cdn = trim((string)($file->cdn_url ?? '')); return $cdn !== '' ? $cdn : "/RequestData/File/{$hash}"; })->filter()->values()->toArray(); }); } else { $store->resolved_photos = []; } // Check if user can add products to this store $store->can_add_product = ProductPermissions::isActionAllowed(UserActions::AddProducttoOwnStore, null, $store) || ProductPermissions::isActionAllowed(UserActions::AddProducttoAnyStore, null, $store); // Check if user can assign existing products to this store $store->can_assign_product = self::canUserAssignProduct($store); // Check if user can access POS for this store $store->can_access_pos = self::canUserAccessPos($store); // Return the formatted response return $target ? $store : response()->json(['success' => true, 'data' => $store]); } /** * Determine if the current user can assign existing products to a given store. * Mirrors the permission check in ProductController::AssignProductToOwnStore. */ private static function canUserAssignProduct(Store $store): bool { /** @var User $user */ $user = Auth::user(); if (!$user) { return false; } $acctType = $user->acct_type->value ?? $user->acct_type ?? ''; if (in_array($acctType, ['ult', 'super operator', 'operator'])) { return true; } if ($store->owner_id === $user->id || $store->manager_id === $user->id) { return true; } if ($store->managers()->where('user_id', $user->id)->exists()) { return true; } if (UserPermissions::isDescendantOfCurrentUser($store->owner_id)) { return true; } $managerIds = $store->managerUsers()->pluck('users.id')->toArray(); foreach ($managerIds as $managerId) { if (UserPermissions::isDescendantOfCurrentUser($managerId)) { return true; } } return false; } /** * Determine if the current user can access POS for a given store. * Returns true if: * - User is the store owner or manager * - User is a direct/indirect parent of the store owner (via parentuid chain) * - User has an admin role (ult, super operator, operator) */ private static function canUserAccessPos(Store $store): bool { /** @var User $user */ $user = Auth::user(); if (!$user) { return false; } $acctType = $user->acct_type->value ?? $user->acct_type ?? ''; // Big 3 always have access if (in_array($acctType, ['ult', 'super operator', 'operator'])) { return true; } // Store owner if ($store->owner_id === $user->id) { return true; } // Check if user is a store manager (primary or in the list) if ($store->manager_id === $user->id) { return true; } if ($store->managers()->where('user_id', $user->id)->exists()) { return true; } // Check hierarchy: parent of owner or any manager if (UserPermissions::isDescendantOfCurrentUser($store->owner_id)) { return true; } $managerIds = $store->managerUsers()->pluck('users.id')->toArray(); foreach ($managerIds as $managerId) { if (UserPermissions::isDescendantOfCurrentUser($managerId)) { return true; } } return false; } public static function editStoreDetails(Request|false $request = false, string|false $target = false) { $req = $request ?: request(); $hash = $target ?: ($req?->input('target') ?? null); // file_put_contents('Storedetails.txt',$hash); if (!$hash) { return $target ? false : response()->json(['success' => false]); } $store = Store::where('hashkey', $hash) ->select( 'photourl', 'hashkey', 'name', 'category', 'subcategory', 'created_at as created', 'updated_at as modified', 'description', 'photourl', 'status', 'is_active', 'remarks', 'address', 'owner_id', 'manager_id', )->with('owner:hashkey,id') ->with('manager:hashkey,id') ->first(); if (!$store) { return $target ? false : response()->json(['success' => false]); } try { $store->owner_hashkey = $store->owner->hashkey; } catch (\Exception $th) { $store->owner_hashkey = null; } try { $store->manager_hashkey = $store->manager->hashkey; } catch (\Throwable $th) { $store->manager_hashkey = 0; } try { $store->photourlDropzone = PhotoURL::photoURLArraytoDropzoneData($store->photourl); } catch (\Throwable $th) { $store->photourlDropzone = []; } try { $store->ParentList = CreateUserControllerUltimate::listAllUsersforParentSelectHTML(request(), true); } catch (\Exception $e) { $store->ParentList = []; } // Include multi-manager hashkeys for frontend try { $store->managers_hashkeys = $store->managerUsers()->pluck('users.hashkey')->toArray(); } catch (\Throwable $th) { $store->managers_hashkeys = []; } // Include linked cooperative hashkeys try { $store->cooperative_hashkeys = $store->cooperatives()->pluck('organizations.hashkey')->toArray(); } catch (\Throwable $th) { $store->cooperative_hashkeys = []; } $products = $store->products() ->select( 'prd_items.name', 'prd_items.hashkey', 'prd_str.price', 'prd_items.unitname', 'prd_str.available', 'prd_str.is_active as status', 'prd_items.photourl' ) ->get() ->filter(fn($product) => $product->available) ->map(function ($product) { $photourl = $product->photourl; if ($photourl && is_array($photourl)) { $product->photourl = $photourl; } elseif (is_string($photourl)) { $product->photourl = json_decode($photourl, true) ?: []; } else { $product->photourl = []; } return $product; }) ->values(); return $target ? $store : response()->json($store); } public function update(Request $request) { $hashkey = request()->input('target'); $store = Store::where('hashkey', $hashkey)->first(); if (!$store) { return response()->json([ 'success' => false, 'message' => 'Store not found.' ], 404); } /** @var User $user */ $user = Auth::user(); if (!$user) { return response()->json(['error' => 'Unauthorized'], 401); } $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]); // Permission check: Big 3, parent of owner, direct owner, direct manager, or pivot manager $isParentOfOwner = UserPermissions::isDescendantOfCurrentUser($store->owner_id); $isDirectManager = $store->manager_id === $user->id; $isManagerViaPivot = $store->managers()->where('user_id', $user->id)->exists(); if (!$isBig3 && !$isParentOfOwner && $store->owner_id !== $user->id && !$isDirectManager && !$isManagerViaPivot) { return response()->json(['error' => 'Unauthorized to modify this store'], 403); } $storeId = $store->id; $validator = Validator::make($request->all(), [ 'name' => [ 'required', 'string', 'max:255', function ($attribute, $value, $fail) use ($storeId) { $conflict = \App\Models\Market\Store::where('name', $value) ->where('id', '!=', $storeId) ->exists(); if ($conflict) { $fail('A store with this name already exists. Please choose a different name.'); } }, ], 'description' => 'required|string', 'address' => 'required|string', 'category' => 'nullable|string|max:100', 'subcategory' => 'nullable|string|max:100', 'remarks' => 'nullable|string', 'photourl' => 'nullable|array', 'files' => 'nullable|array', 'owner' => 'nullable|string', 'manager' => 'nullable|string', 'managers' => 'nullable|array', 'cooperatives' => 'nullable|array', 'cooperatives.*' => 'string', ]); if ($validator->fails()) { return response()->json([ 'success' => false, 'errors' => $validator->errors() ], 422); } $validated = $validator->validated(); $ownerId = ($validated['owner'] ?? null) ? UserController::findUserIdByHash($validated['owner']) : $store->owner_id; $managerId = ($validated['manager'] ?? null) ? UserController::findUserIdByHash($validated['manager']) : $store->manager_id; $status = $request->input('status', $store->status ?? 'active'); $data = [ 'name' => $validated['name'], 'description' => $validated['description'], 'address' => $validated['address'], 'remarks' => $validated['remarks'] ?? $store->remarks, 'category' => $validated['category'] ?? $store->category, 'subcategory' => $validated['subcategory'] ?? $store->subcategory, 'status' => $status, 'photourl' => $validated['photourl'] ?? $validated['files'] ?? $store->photourl, 'owner_id' => $ownerId, 'manager_id' => $managerId, 'updated_by' => Auth::id(), ]; $store->update($data); // Handle multiple managers if ($request->filled('managers') && is_array($request->input('managers'))) { // Remove existing managers not in the new list $newMgrIds = []; foreach ($request->input('managers') as $mgrHash) { $mgrId = UserController::findUserIdByHash($mgrHash); if ($mgrId) $newMgrIds[] = $mgrId; } $store->managers()->whereNotIn('user_id', $newMgrIds)->delete(); // Add new ones foreach ($newMgrIds as $mgrId) { if (!$store->managers()->where('user_id', $mgrId)->exists()) { \App\Models\Market\StoreManager::create([ 'store_id' => $store->id, 'user_id' => $mgrId, 'created_by' => Auth::id(), ]); } } } // Sync cooperative links (only when key is present, allows clearing with empty array) if ($request->has('cooperatives')) { $this->syncStoreCooperatives($store, $request->input('cooperatives', []), true); } return response()->json([ 'success' => true, 'message' => 'Store updated successfully.', 'hashkey' => $store->hashkey, ], 200); } /** * Attach a store to a product with optional pivot data. * * This creates (or updates) a relationship between a given Product and Store * in the `product_store` pivot table. It allows specifying stock quantity, * price, and active status for that store-product link. * * If either the product or store cannot be found, the method safely returns false. * * @param \App\Models\Market\Product|int $product The Product model instance or its ID. * @param \App\Models\Market\Store|int $store The Store model instance or its ID. * @param int $available Quantity available for this product in the store (default: 0) * @param int $price Price for this product in the store (default: 0) * @param bool $is_active Whether the product is active in this store (default: true) * @return bool Returns true on success, false if product/store not found or an exception occurs. * * @example * ```php * // Attach store #5 to product #12 with stock and price * Product::attachStoreToProduct(12, 5, available: 40, price: 1499, is_active: true); * * // Using model instances * $product = Product::find(12); * $store = Store::find(5); * Product::attachStoreToProduct($product, $store, 25, 1299); * ``` */ public static function attachStoreToProduct(Product|int $product, Store|int $store, int $available = 0, int $price = 0, bool $is_active = true) { if (is_int($product)) { try { $product = Product::findOrFail($product); } catch (\Throwable $th) { return false; } } if (is_int($store)) { try { $store = Store::findOrFail($store); } catch (\Throwable $th) { return false; } } try { $product->stores()->syncWithoutDetaching([ $store->id => [ 'available' => $available ?? 0, 'price' => $price ?? 0, 'is_active' => $is_active, ] ]); return true; } catch (\Throwable $th) { return false; } } /** * Attach a product to a store with optional pivot data. * * This creates (or updates) a relationship between a given Store and Product * in the `product_store` pivot table. You can specify availability, price, * and activation status as pivot attributes. * * If either the store or product is not found, the method safely returns false. * * @param \App\Models\Market\Store|int $store The Store model instance or its ID. * @param \App\Models\Market\Product|int $product The Product model instance or its ID. * @param int $available Quantity available (default: 0) * @param int $price Product price (default: 0) * @param bool $is_active Whether the product is active in this store (default: true) * @return bool Returns true on success, false if store/product not found or an exception occurs. * * @example * ```php * // Attach product #10 to store #3 * Store::attachProductToStore(3, 10, 25, 1999, true); * * // Using model instances * $store = Store::find(3); * $product = Product::find(10); * Store::attachProductToStore($store, $product, available: 100, price: 2499); * ``` */ public static function attachProducttoStore(Store|int $store, Product|int $product, int $available = 0, int $price = 0, bool $is_active = true) { if (is_int($store)) { try { $store = Store::findOrFail($store); } catch (\Throwable $th) { return false; } } if (is_int($product)) { try { $product = Product::findOrFail($product); } catch (\Throwable $th) { return false; } } $store->products()->attach($product->id, ['available' => $available ?? 0, 'price' => $price ?? 0, 'is_active' => $is_active,]); return true; } /** * Detach (unlink) a product from a store. * * This removes the relationship between a given Store and Product * from the `product_store` pivot table, without deleting either record. * * If the link does not exist, this method silently succeeds (no error is thrown). * If either the store or product cannot be found, the method safely returns false. * * @param \App\Models\Market\Store|int $store The Store model instance or its ID. * @param \App\Models\Market\Product|int $product The Product model instance or its ID. * @return bool Returns true on success, false if store/product not found or an exception occurs. * * @example * ```php * // Detach product #10 from store #3 * Store::detachProductFromStore(3, 10); * * // Using model instances * $store = Store::find(3); * $product = Product::find(10); * Store::detachProductFromStore($store, $product); * ``` */ public static function detachProductFromStore(Store|int $store, Product|int $product) { // Resolve Store if (is_int($store)) { try { $store = Store::findOrFail($store); } catch (\Throwable $th) { return false; } } // Resolve Product if (is_int($product)) { try { $product = Product::findOrFail($product); } catch (\Throwable $th) { return false; } } // Attempt to detach try { $store->products()->detach($product->id); return true; } catch (\Throwable $th) { return false; } } /** * Detach (unlink) a store from a product. * * This removes the relationship between a given Product and Store * from the `product_store` pivot table, without deleting either record. * * If the relationship doesn’t exist, the operation succeeds silently. * If either the product or store is not found, the method safely returns false. * * @param \App\Models\Market\Product|int $product The Product model instance or its ID. * @param \App\Models\Market\Store|int $store The Store model instance or its ID. * @return bool Returns true on success, false if product/store not found or an exception occurs. * * @example * ```php * // Detach store #2 from product #7 * Product::detachStoreFromProduct(7, 2); * * // Using model instances * $product = Product::find(7); * $store = Store::find(2); * Product::detachStoreFromProduct($product, $store); * ``` */ public static function detachStoreFromProduct(Product|int|string $product, Store|int|string $store) { if (is_int($product)) { try { $product = Product::findOrFail($product); } catch (\Throwable $th) { $product = null; } } if (is_int($store)) { try { $store = Store::findOrFail($store); } catch (\Throwable $th) { $store = null; } } if (is_string($product)) { try { $product = Product::where('hashkey', $product)->first(); } catch (\Throwable $th) { $store = null; } } if (is_string($store)) { try { $store = Store::where('hashkey', $store)->first(); } catch (\Throwable $th) { $store = null; } } if (!$store || !$product) { return false; } // Attempt to detach try { $product->stores()->detach($store->id); return true; } catch (\Throwable $th) { return false; } } /** * Synchronize (replace) all products attached to a given store. * * This method accepts either a Store model instance or a store ID. * It replaces the store's entire product list in the `product_store` pivot table * with the provided data array. * * Existing product links not included in the `$products` array will be detached. * New links will be created, and existing ones will be updated. * * @param \App\Models\Market\Store|int $store The Store model instance or its ID. * @param array $products Associative array of product IDs and pivot attributes. * Format: * [ * product_id => [ * 'available' => (int), * 'price' => (int), * 'is_active' => (bool), * ], * ... * ] * @return bool Returns true on success, false if the store is not found or an exception occurs. * * @example * ```php * // Replace all products for store #1 * Store::syncProductsToStore(1, [ * 10 => ['available' => 50, 'price' => 1499, 'is_active' => true], * 11 => ['available' => 20, 'price' => 1999, 'is_active' => false], * ]); * * // Using a Store instance * $store = Store::find(1); * Store::syncProductsToStore($store, [ * 5 => ['available' => 100, 'price' => 2999, 'is_active' => true], * ]); * ``` */ public static function syncProductsToStore(Store|int $store, array $products) { // $products format: [product_id => ['available' => ..., 'price' => ..., 'is_active' => ...]] // Resolve store if (is_int($store)) { try { $store = Store::findOrFail($store); } catch (\Throwable $th) { return false; } } // Validate array structure (optional) if (!is_array($products) || empty($products)) { return false; } try { // Replace all existing product links $store->products()->sync($products); return true; } catch (\Throwable $th) { return false; } } /** * Synchronize (replace) all stores attached to a given product. * * This method accepts either a Product model instance or a product ID. * It replaces the product's entire store list in the `product_store` pivot table * with the provided data array. * * Existing store links not listed in the `$stores` array will be detached. * New ones will be created, and existing ones will be updated. * * @param \App\Models\Market\Product|int $product The Product model instance or its ID. * @param array $stores Associative array of store IDs and pivot attributes. * Format: * [ * store_id => [ * 'available' => (int), * 'price' => (int), * 'is_active' => (bool), * ], * ... * ] * @return bool Returns true on success, false if the product is not found or an exception occurs. * * @example * ```php * // Replace all stores linked to product #5 * Product::syncStoresToProduct(5, [ * 1 => ['available' => 30, 'price' => 1799, 'is_active' => true], * 2 => ['available' => 15, 'price' => 1999, 'is_active' => false], * ]); * * // Using a Product instance * $product = Product::find(5); * Product::syncStoresToProduct($product, [ * 3 => ['available' => 40, 'price' => 1499, 'is_active' => true], * ]); * ``` */ public static function syncStoresToProduct(Product|int $product, array $stores) { // $stores format: [store_id => ['available' => ..., 'price' => ..., 'is_active' => ...]] // Resolve product if (is_int($product)) { try { $product = Product::findOrFail($product); } catch (\Throwable $th) { return false; } } if (!is_array($stores) || empty($stores)) { return false; } try { // Replace all existing store links $product->stores()->sync($stores); return true; } catch (\Throwable $th) { return false; } } public function listStoresActiveDataAll() { $stores = Store::where('status', 'active') ->select('hashkey', 'photourl', 'name', 'category', 'subcategory') ->get() ->map(function ($store) { if (is_array($store->photourl)) { $photoUrl = $store->photourl; } elseif (is_string($store->photourl)) { $photoUrl = json_decode($store->photourl, true) ?: []; } else { $photoUrl = []; } if (is_array($photoUrl) && !empty($photoUrl)) { $photoUrl = $photoUrl[0]; } else { $photoUrl = null; } $store->photourl = $photoUrl; return $store; }); return $stores; } /** * List stores the current user is authorized to manage. * - Ultimate users: all stores * - Store owners: stores they own * - Store managers: stores they manage * Returns a JSON array of stores with hashkey, name, and role. */ public function listStoresForCurrentUser() { $user = Auth::user(); if (!$user) { return Response::json([]); } $isUltimate = $user->acct_type === UserTypes::ULTIMATE; $isSuperOperator = $user->acct_type === UserTypes::SUPER_OPERATOR; $isOperator = $user->acct_type === UserTypes::OPERATOR; if ($isUltimate) { // Ultimate can see all active stores $stores = Store::where('is_active', true) ->select(['hashkey', 'name', 'category', 'owner_id', 'manager_id']) ->get(); } elseif ($isSuperOperator || $isOperator) { // Super Operator and Operator see their own and descendants' stores $descendants = $user->getAllDescendants(); $allowedIds = $descendants->pluck('id')->push($user->id)->toArray(); $stores = Store::where('is_active', true) ->where(function($query) use ($allowedIds) { $query->whereIn('owner_id', $allowedIds) ->orWhereIn('manager_id', $allowedIds); }) ->select(['hashkey', 'name', 'category', 'owner_id', 'manager_id']) ->get(); } else { // Non-admin users: only stores they own or manage (including store_managers pivot) $stores = Store::where('is_active', true) ->where(function($query) use ($user) { $query->where('owner_id', $user->id) ->orWhere('manager_id', $user->id) ->orWhereHas('managers', function ($mq) use ($user) { $mq->where('user_id', $user->id); }); }) ->select(['hashkey', 'name', 'category', 'owner_id', 'manager_id']) ->get(); } $formattedStores = $stores->map(function ($store) use ($user) { $role = 'viewer'; if ($store->owner_id === $user->id) { $role = 'owner'; } elseif ($store->manager_id === $user->id) { $role = 'manager'; } elseif ($store->managers()->where('user_id', $user->id)->exists()) { $role = 'manager'; } elseif ($user->isUltimate() || $user->isSuperOperator() || $user->isOperator()) { $role = 'admin'; } return [ 'hashkey' => $store->hashkey, 'name' => $store->name, 'category' => $store->category, 'role' => $role, ]; }); return Response::json($formattedStores); } public function removeProductfromStore() { //TODO fix permissions here $product = request()->input('product_hash'); $store = request()->input('store_hash'); $isUlt = Auth::user()->acct_type === UserTypes::ULTIMATE; if ($isUlt) { $allowed = ProductPermissions::isActionAllowed(UserActions::AddProducttoAnyStore, $product, $store); } else { $allowed = ProductPermissions::isActionAllowed(UserActions::AddProducttoOwnStore, $product, $store); } if (!$allowed) { return ResponseHelper::returnUnauthorized(); } $go = self::detachStoreFromProduct($product, $store); if ($go === true) { $datareturn = [ 'store_hash' => $store, 'product_hash' => $product, ]; return ResponseHelper::returnSuccessResponse($datareturn, $product, ''); } else { return ResponseHelper::returnError($go); } } public function listSelectableStoresForAddingProduct() { /** @var \App\Models\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) { // Big 3 can see all stores $stores = Store::select(['hashkey', 'name', 'category', 'owner_id', 'manager_id']) ->get(); } else { // Include self + all descendants so a store owner sees their own stores. $allowedUserIds = array_merge([$user->id], $user->getAllDescendants()->pluck('id')->toArray()); $stores = Store::where(function ($q) use ($allowedUserIds) { $q->whereIn('owner_id', $allowedUserIds) ->orWhereIn('manager_id', $allowedUserIds) ->orWhereHas('managers', function ($mq) use ($allowedUserIds) { $mq->whereIn('user_id', $allowedUserIds); }); }) ->select(['hashkey', 'name', 'category', 'owner_id', 'manager_id']) ->get(); } $formattedStores = $stores->map(function ($store) use ($user) { $role = 'admin'; if ($store->owner_id === $user->id) { $role = 'owner'; } elseif ($store->manager_id === $user->id) { $role = 'manager'; } return [ 'hashkey' => $store->hashkey, 'name' => $store->name, 'category' => $store->category, 'role' => $role, ]; }); return response()->json([ 'success' => true, 'data' => $formattedStores, ]); } public function listStores_Admin(Request $request) { try { $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]); $columns = ['id', 'hashkey', 'name', 'category', 'subcategory', 'is_active', 'status', 'photourl', 'address', 'owner_id', 'manager_id']; if ($isBig3) { $stores = Store::with(['cooperatives:organizations.id,organizations.hashkey,organizations.name']) ->select($columns) ->orderBy('id', 'desc') ->get() ->each(fn($s) => $s->setAttribute('user_can_manage', true)); } else { $allowedUserIds = array_merge([$user->id], $user->getAllDescendants()->pluck('id')->toArray()); $stores = Store::with(['cooperatives:organizations.id,organizations.hashkey,organizations.name', 'managers:id,store_id,user_id']) ->select($columns) ->where(function ($q) use ($allowedUserIds) { $q->whereIn('owner_id', $allowedUserIds) ->orWhereIn('manager_id', $allowedUserIds) ->orWhereHas('managers', function ($mq) use ($allowedUserIds) { $mq->whereIn('user_id', $allowedUserIds); }); }) ->orderBy('id', 'desc') ->get() ->each(function ($s) use ($allowedUserIds) { $s->setAttribute('user_can_manage', in_array($s->owner_id, $allowedUserIds) || in_array($s->manager_id, $allowedUserIds) || $s->managers->pluck('user_id')->intersect($allowedUserIds)->isNotEmpty() ); unset($s->managers); }); } return response()->json([ 'success' => true, 'stores' => $stores ]); } catch (\Throwable $e) { \Hypervel\Support\Facades\Log::error('listStores_Admin failed', [ 'user_id' => Auth::id(), 'message' => $e->getMessage(), 'file' => $e->getFile() . ':' . $e->getLine(), ]); return response()->json([ 'success' => false, 'message' => 'Failed to load stores: ' . $e->getMessage(), ], 500); } } /** * Resolve cooperative hashkeys to ids (filtering to type=COOPERATIVE) * and sync onto the store. When $forceReplace is false and the input * array is empty, this is a no-op (used on create). */ private function syncStoreCooperatives(Store $store, $coopHashes, bool $forceReplace = false): void { if (!is_array($coopHashes)) { $coopHashes = []; } if (!$forceReplace && empty($coopHashes)) { return; } $coopIds = empty($coopHashes) ? [] : Organization::whereIn('hashkey', $coopHashes) ->where('type', 'COOPERATIVE') ->pluck('id') ->toArray(); $store->cooperatives()->sync($coopIds); } /** * Lightweight cooperative list for the store create/edit pickers. * Returns only active organizations of type COOPERATIVE. */ public function listCooperativesForStore(Request $request) { $user = Auth::user(); if (!$user) { return ResponseHelper::returnUnauthorized(); } $coops = Organization::where('is_active', true) ->where('type', 'COOPERATIVE') ->select(['hashkey', 'name', 'cooperative_type', 'address']) ->orderBy('name') ->get(); return response()->json([ 'success' => true, 'data' => $coops, ]); } public function deleteStore_Admin(Request $request) { $user = Auth::user(); if (!$user) { return ResponseHelper::returnUnauthorized(); } $hash = $request->input('target'); if (!$hash) { return ResponseHelper::returnIncorrectDetails(); } try { $store = Store::where('hashkey', $hash)->firstOrFail(); // Check permissions: Ultimate or Owner/Manager (or descendant thereof, or pivot manager) $isUltimate = $user->acct_type === UserTypes::ULTIMATE; if (!$isUltimate) { $descendants = $user->getAllDescendants(); $descendantIds = $descendants->pluck('id')->toArray(); $allowedIds = array_merge([$user->id], $descendantIds); $isManagerViaPivot = $store->managers()->whereIn('user_id', $allowedIds)->exists(); if (!in_array($store->owner_id, $allowedIds) && !in_array($store->manager_id, $allowedIds) && !$isManagerViaPivot) { return ResponseHelper::returnUnauthorized(); } } $store->delete(); return ResponseHelper::returnSuccessResponse([], $hash, 'Store deleted successfully'); } catch (\Exception $e) { return ResponseHelper::returnError('Failed to delete store: ' . $e->getMessage()); } } public function toggleStoreStatus_Admin(Request $request) { $user = Auth::user(); if (!$user) { return ResponseHelper::returnUnauthorized(); } $hash = $request->input('target'); if (!$hash) { return ResponseHelper::returnIncorrectDetails(); } try { $store = Store::where('hashkey', $hash)->firstOrFail(); // Check permissions: Ultimate or Owner/Manager (or descendant thereof, or pivot manager) $isUltimate = $user->acct_type === UserTypes::ULTIMATE; if (!$isUltimate) { $descendants = $user->getAllDescendants(); $descendantIds = $descendants->pluck('id')->toArray(); $allowedIds = array_merge([$user->id], $descendantIds); $isManagerViaPivot = $store->managers()->whereIn('user_id', $allowedIds)->exists(); if (!in_array($store->owner_id, $allowedIds) && !in_array($store->manager_id, $allowedIds) && !$isManagerViaPivot) { return ResponseHelper::returnUnauthorized(); } } $store->is_active = !$store->is_active; $store->save(); return ResponseHelper::returnSuccessResponse( ['is_active' => $store->is_active], $hash, 'Store status updated successfully' ); } catch (\Exception $e) { return ResponseHelper::returnError('Failed to update store status: ' . $e->getMessage()); } } }