1510 lines
54 KiB
PHP
1510 lines
54 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\Http\Controllers\Helpers\UserController;
|
||
use App\Http\Controllers\UserManagement\CreateUserControllerUltimate;
|
||
use App\Http\Controllers\viewHelperController;
|
||
use App\Models\FileList;
|
||
use App\Models\Market\Organization;
|
||
use App\Models\Market\Product;
|
||
use App\Models\User;
|
||
use Hypervel\Http\Request;
|
||
use Hypervel\Support\Facades\Auth;
|
||
use Hypervel\Support\Facades\Response;
|
||
|
||
use Hypervel\Support\Facades\Hash;
|
||
use Hypervel\Support\Facades\Session;
|
||
|
||
use App\Models\Market\Store;
|
||
use Hypervel\Support\Facades\Validator;
|
||
use Hypervel\Support\Facades\Config;
|
||
use Symfony\Component\HttpFoundation\Test\Constraint\ResponseHeaderLocationSame;
|
||
|
||
class StoreController
|
||
{
|
||
public function getCategories(Request|false $request = false)
|
||
{
|
||
$nonRequest = $request == false;
|
||
$categories = array_keys(Config::get('market.storesCategorySubcategory', []));
|
||
|
||
if (!$nonRequest) {
|
||
return response()->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());
|
||
}
|
||
}
|
||
} |