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

578 lines
23 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\Permissions\UserTypeService;
use App\Http\Controllers\Helpers\ResponseHelper;
use App\Http\Controllers\Helpers\UserController;
use App\Models\Market\CooperativeMember;
use App\Models\Market\Organization;
use App\Models\Market\Product;
use App\Models\Market\Store;
use App\Models\User;
use Hypervel\Support\Str;
use Hypervel\Http\Request;
use Hypervel\Support\Facades\Auth;
use Hypervel\Support\Facades\DB;
use Hypervel\Support\Facades\Hash;
use Hypervel\Support\Facades\Response;
use Hypervel\Support\Facades\Validator;
class BatchController
{
/**
* Batch create products.
* Available for Ultimate, Super Operator, and Operator.
*/
public function batchCreateProducts(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]);
$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());
}
}
}