acct_type instanceof UserTypes ? $user->acct_type : UserTypes::tryFrom($user->acct_type); $isBig3 = in_array($acctType, [UserTypes::ULTIMATE, UserTypes::SUPER_OPERATOR, UserTypes::OPERATOR]); $isStoreOwner = $acctType === UserTypes::STORE_OWNER; if (!$isBig3 && !$isStoreOwner) { return ResponseHelper::returnUnauthorized(); } $products = $request->input('products', []); if (empty($products)) { return ResponseHelper::returnError('No products provided'); } $targetStoreHash = $request->input('target_store_hash'); $targetStore = null; if ($targetStoreHash) { $targetStore = Store::where('hashkey', $targetStoreHash)->first(); if (!$targetStore) { return ResponseHelper::returnError('Target store not found'); } } // STORE_OWNER must operate against a store they actually own/manage. // Without that, batch-import has no destination and must be refused // up-front so the UI doesn't silently 401 partway through. if (!$isBig3) { if (!$targetStore) { return ResponseHelper::returnError('You must select one of your stores before importing products.', 422); } $ownsTarget = (int) $targetStore->owner_id === (int) $user->id || (int) $targetStore->manager_id === (int) $user->id || $targetStore->managers()->where('user_id', $user->id)->exists(); if (!$ownsTarget) { return ResponseHelper::returnError('You can only import products into stores you own.', 403); } } $results = []; $errors = []; DB::beginTransaction(); try { foreach ($products as $index => $productData) { $source = $productData['source'] ?? 'new'; if ($source === 'existing') { if (!$targetStore) { $errors[] = 'Row ' . ($index + 1) . ': A target store is required to import an existing product.'; continue; } $validator = Validator::make($productData, [ 'product_hash' => 'required|string', 'price' => 'nullable|numeric|min:0', 'available' => 'nullable|numeric', 'description' => 'nullable|string', ]); if ($validator->fails()) { $errors[] = 'Row ' . ($index + 1) . ': ' . implode(', ', $validator->errors()->all()); continue; } $existing = Product::where('hashkey', $productData['product_hash'])->first(); if (!$existing) { $errors[] = 'Row ' . ($index + 1) . ': Selected global product not found.'; continue; } $alreadyAttached = $targetStore->products()->where('prd_items.id', $existing->id)->exists(); if ($alreadyAttached) { $errors[] = 'Row ' . ($index + 1) . ": '{$existing->name}' is already in the target store."; continue; } $price = $productData['price'] ?? null; $price = ($price === null || $price === '' || (float) $price <= 0) ? (float) $existing->price : (float) $price; $description = $productData['description'] ?? ''; if (trim((string) $description) === '') { $description = (string) ($existing->description ?? ''); } $available = (int) ($productData['available'] ?? 0); $targetStore->products()->attach($existing->id, [ 'available' => $available, 'price' => $price, 'description' => $description, 'is_active' => true, ]); $results[] = $existing->hashkey; continue; } $validator = Validator::make($productData, [ 'name' => 'required|string|max:255', 'price' => 'required|numeric|min:0', 'available' => 'required|numeric', 'unitname' => 'required|string|max:100', 'description' => 'nullable|string', 'category' => 'nullable|string|max:255', 'subcategory' => 'nullable|string|max:255', 'barcode' => 'nullable|string|max:255', 'photourl' => 'nullable|array', 'photourl.*' => 'nullable|string', ]); if ($validator->fails()) { $errors[] = "Row " . ($index + 1) . ": " . implode(', ', $validator->errors()->all()); continue; } // Reject if a global product with this name already exists. // Owners should pick the existing one via the fuzzy-search // modal instead of creating a duplicate global entry. $duplicate = Product::whereRaw('LOWER(TRIM(name)) = ?', [strtolower(trim((string) $productData['name']))]) ->first(); if ($duplicate) { $errors[] = "Row " . ($index + 1) . ": '{$productData['name']}' already exists globally. Use 'Pick existing' to import it instead."; continue; } $product = Product::create([ 'name' => $productData['name'], 'description' => $productData['description'] ?? '', 'price' => $productData['price'], 'unitname' => $productData['unitname'], 'available' => $productData['available'], 'barcode' => $productData['barcode'] ?? null, 'category' => $productData['category'] ?? null, 'subcategory' => $productData['subcategory'] ?? null, 'photourl' => $productData['photourl'] ?? [], 'created_by' => $user->id, 'is_active' => true, ]); if ($targetStore) { $targetStore->products()->attach($product->id, [ 'available' => (int) $productData['available'], 'price' => (float) $productData['price'], 'description' => $productData['description'] ?? '', 'is_active' => true, ]); } $results[] = $product->hashkey; } if (!empty($errors)) { DB::rollBack(); return Response::json(['success' => false, 'message' => 'Batch creation failed', 'errors' => $errors], 422); } DB::commit(); return Response::json(['success' => true, 'count' => count($results), 'data' => $results]); } catch (\Throwable $th) { DB::rollBack(); return ResponseHelper::returnError($th->getMessage()); } } /** * Serve the batch products Excel template as a file download. * Available for all authenticated users with batch module access. */ public function downloadProductTemplate() { $templatePath = BASE_PATH . '/resources/templates/batch-products-template.xlsx'; if (!file_exists($templatePath)) { return Response::json(['success' => false, 'message' => 'Template file not found.'], 404); } $fileContent = file_get_contents($templatePath); $filename = 'bukidbounty-batch-products-template.xlsx'; return Response::make($fileContent, 200, [ 'Content-Type' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 'Content-Disposition' => 'attachment; filename="' . $filename . '"', 'Content-Length' => strlen($fileContent), 'Cache-Control' => 'no-cache, no-store, must-revalidate', 'Pragma' => 'no-cache', 'Expires' => '0', ]); } /** * Batch create stores. * Available for Ultimate, Super Operator, and Operator. */ public function batchCreateStores(Request $request) { $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) { return ResponseHelper::returnUnauthorized(); } $stores = $request->input('stores', []); if (empty($stores)) { return ResponseHelper::returnError('No stores provided'); } $results = []; $errors = []; DB::beginTransaction(); try { foreach ($stores as $index => $storeData) { $validator = Validator::make($storeData, [ 'name' => 'required|string|max:255', 'description' => 'required|string', 'address' => 'required|string', 'category' => 'nullable|string|max:100', 'subcategory' => 'nullable|string|max:100', 'owner_hash' => 'nullable|string', ]); if ($validator->fails()) { $errors[] = "Row " . ($index + 1) . ": " . implode(', ', $validator->errors()->all()); continue; } $ownerId = null; if (!empty($storeData['owner_hash'])) { $ownerId = UserController::findUserIdByHash($storeData['owner_hash']); } $storeCode = StoreController::generateStoreCode($storeData['category'] ?? 'General'); $store = Store::create([ 'storecode' => $storeCode, 'name' => $storeData['name'], 'description' => $storeData['description'], 'address' => $storeData['address'], 'category' => $storeData['category'] ?? 'General', 'subcategory' => $storeData['subcategory'] ?? '', 'owner_id' => $ownerId, 'created_by' => $user->id, 'is_active' => true, 'status' => 'active', ]); $results[] = $store->hashkey; } if (!empty($errors)) { DB::rollBack(); return Response::json(['success' => false, 'message' => 'Batch creation failed', 'errors' => $errors], 422); } DB::commit(); return Response::json(['success' => true, 'count' => count($results), 'data' => $results]); } catch (\Throwable $th) { DB::rollBack(); return ResponseHelper::returnError($th->getMessage()); } } /** * Batch create users. * Available for Ultimate, Super Operator, and Operator. */ public function batchCreateUsers(Request $request) { $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) { return ResponseHelper::returnUnauthorized(); } $usersData = $request->input('users', []); if (empty($usersData)) { return ResponseHelper::returnError('No users provided'); } $results = []; $errors = []; DB::beginTransaction(); try { foreach ($usersData as $index => $data) { $validator = Validator::make($data, [ 'username' => 'required|string|max:255|unique:users,username', 'name' => 'required|string|max:255', 'mobile_number' => ['required', 'string', 'max:20', 'unique:users,mobile_number', 'regex:/^(09|\+639)\d{9}$/'], 'password' => 'required|string|min:6', 'type' => 'required|string', 'parent_hash' => 'nullable|string', ]); if ($validator->fails()) { $errors[] = "Row " . ($index + 1) . ": " . implode(', ', $validator->errors()->all()); continue; } $usertypeEnum = UserTypes::tryFrom($data['type']); if (!$usertypeEnum) { $errors[] = "Row " . ($index + 1) . ": Invalid User Type"; continue; } // Check if creator is allowed to create this user type if ($acctType !== UserTypes::ULTIMATE) { $allowedTypes = UserTypeService::getAllowedUserTypes($acctType); if (!in_array($usertypeEnum, $allowedTypes)) { $errors[] = "Row " . ($index + 1) . ": You are not allowed to create user type '{$data['type']}'"; continue; } } $parentId = $user->id; // Default to creator if (!empty($data['parent_hash'])) { $parent = User::where('hashkey', $data['parent_hash'])->first(); if ($parent) { $parentId = $parent->id; } } $newUser = User::create([ 'username' => $data['username'], 'name' => $data['name'], 'mobile_number' => $data['mobile_number'], 'password' => Hash::make($data['password']), 'acct_type' => $data['type'], 'parentuid' => $parentId, 'active' => true, ]); $results[] = $newUser->hashkey; } if (!empty($errors)) { DB::rollBack(); return Response::json(['success' => false, 'message' => 'Batch creation failed', 'errors' => $errors], 422); } DB::commit(); return Response::json(['success' => true, 'count' => count($results), 'data' => $results]); } catch (\Throwable $th) { DB::rollBack(); return ResponseHelper::returnError($th->getMessage()); } } /** * Batch create cooperatives. * Available for Ultimate and Super Operator. */ public function batchCreateCooperatives(Request $request) { $user = Auth::user(); if (!$user) return ResponseHelper::returnUnauthorized(); $acctType = $user->acct_type instanceof UserTypes ? $user->acct_type : UserTypes::tryFrom($user->acct_type); $isAllowed = in_array($acctType, [UserTypes::ULTIMATE, UserTypes::SUPER_OPERATOR]); if (!$isAllowed) { return ResponseHelper::returnUnauthorized(); } $cooperatives = $request->input('cooperatives', []); if (empty($cooperatives)) { return ResponseHelper::returnError('No cooperatives provided'); } $results = []; $errors = []; DB::beginTransaction(); try { foreach ($cooperatives as $index => $data) { $validator = Validator::make($data, [ 'name' => 'required|string|max:255', 'address' => 'nullable|string', 'registration_number' => 'nullable|string|max:255', 'cin' => 'nullable|string|max:255', 'tin' => 'nullable|string|max:255', 'cooperative_type' => 'nullable|string|max:100', 'cooperative_category' => 'nullable|string|max:100', 'registration_date' => 'nullable|date', 'contact_person' => 'nullable|string|max:255', 'contact_number' => 'nullable|string|max:50', 'contact_email' => 'nullable|email|max:255', ]); if ($validator->fails()) { $errors[] = "Row " . ($index + 1) . ": " . implode(', ', $validator->errors()->all()); continue; } $cooperative = new Organization([ 'hashkey' => Str::random(64), 'name' => trim($data['name']), 'type' => 'COOPERATIVE', 'address' => trim($data['address'] ?? ''), 'registration_number' => trim($data['registration_number'] ?? ''), 'cin' => trim($data['cin'] ?? ''), 'tin' => trim($data['tin'] ?? ''), 'cooperative_type' => trim($data['cooperative_type'] ?? ''), 'cooperative_category' => trim($data['cooperative_category'] ?? ''), 'registration_date' => $data['registration_date'] ?? null, 'contact_person' => trim($data['contact_person'] ?? ''), 'contact_number' => trim($data['contact_number'] ?? ''), 'contact_email' => trim($data['contact_email'] ?? ''), 'is_active' => true, 'created_by' => $user->id, ]); if (!$cooperative->save()) { $errors[] = "Row " . ($index + 1) . ": Failed to save cooperative"; continue; } $results[] = $cooperative->hashkey; } if (!empty($errors)) { DB::rollBack(); return Response::json(['success' => false, 'message' => 'Batch creation failed', 'errors' => $errors], 422); } DB::commit(); return Response::json(['success' => true, 'count' => count($results), 'data' => $results]); } catch (\Throwable $th) { DB::rollBack(); return ResponseHelper::returnError($th->getMessage()); } } /** * Batch add cooperative members with automatic user account creation. * Each row creates a new User and a corresponding CooperativeMember in one transaction. */ public function batchCreateCooperativeMembers(Request $request) { $user = Auth::user(); if (!$user) return ResponseHelper::returnUnauthorized(); if (!UserPermissions::isActionPermitted($user->acct_type, UserActions::ManageOrganizations)) { return ResponseHelper::returnUnauthorized(); } $cooperativeHash = $request->input('cooperative_hash'); $members = $request->input('members', []); if (!$cooperativeHash) { return ResponseHelper::returnError('Cooperative hash is required'); } if (empty($members)) { return ResponseHelper::returnError('No members provided'); } $cooperative = Organization::where('hashkey', $cooperativeHash) ->where('type', 'COOPERATIVE') ->first(); if (!$cooperative) { return ResponseHelper::returnError('Cooperative not found', 404); } $parentUser = User::where('id', $cooperative->created_by)->first() ?? $user; $results = []; $errors = []; DB::beginTransaction(); try { foreach ($members as $index => $data) { $validator = Validator::make($data, [ 'username' => 'required|string|max:255|unique:users,username', 'name' => 'required|string|max:255', 'mobile_number' => ['required', 'string', 'max:20', 'unique:users,mobile_number', 'regex:/^(09|\+639)\d{9}$/'], 'password' => 'required|string|min:6', 'role' => 'nullable|string|max:50', 'membership_type' => 'nullable|string|max:50', ]); if ($validator->fails()) { $errors[] = 'Row ' . ($index + 1) . ': ' . implode(', ', $validator->errors()->all()); continue; } $newUser = new User(); $newUser->username = $data['username']; $newUser->name = $data['name']; $newUser->mobile_number = $data['mobile_number']; $newUser->password = Hash::make($data['password']); $newUser->parentuid = $parentUser->id; $newUser->acct_type = 'user'; $newUser->active = true; $newUser->save(); $member = new CooperativeMember([ 'hashkey' => Str::random(64), 'organization_id' => $cooperative->id, 'user_id' => $newUser->id, 'role' => $data['role'] ?? 'MEMBER', 'membership_type' => $data['membership_type'] ?? null, 'joined_at' => now(), 'is_active' => true, 'created_by' => $user->id, ]); if (!$member->save()) { $errors[] = 'Row ' . ($index + 1) . ': Failed to save membership'; continue; } $results[] = [ 'user_hashkey' => $newUser->hashkey, 'member_hashkey' => $member->hashkey, ]; } if (!empty($errors)) { DB::rollBack(); return Response::json(['success' => false, 'message' => 'Batch creation failed', 'errors' => $errors], 422); } DB::commit(); return Response::json(['success' => true, 'count' => count($results), 'data' => $results]); } catch (\Throwable $th) { DB::rollBack(); return ResponseHelper::returnError($th->getMessage()); } } }