feat: implement barangay system phases 2-14
Some checks failed
tests / PHP 8.2 (swoole-5.1.6) (push) Has been cancelled
tests / PHP 8.3 (swoole-5.1.6) (push) Has been cancelled
tests / PHP 8.4 (swoole-6.0) (push) Has been cancelled

Complete adaptation from BukidBountyApp to Philippine barangay governance:

- Barangay models: Resident, Household, HouseholdMember, Blotter, BlotterHearing,
  DocumentRequest, RequestPayment, RequestType, BarangayProject, BarangayBudget
- Controllers: ResidentController, HouseholdController, BlotterController,
  BlotterHearingController, DocumentRequestController, RequestTypeController,
  ProjectController, BudgetController, QRPHController, AdminConsoleController,
  UserController, FileController, ChapterController, LoginController
- Vue pages: Home, ManageResidents, ResidentProfile, ManageHouseholds, ManageBlotters,
  BlotterDetail, RequestDocument, ManageDocumentRequests, DocumentRequestDetail,
  ManageRequestTypes, ManageProjects, BudgetLedger, AdminConsole
- Barangay roles: PunongBarangay, Kagawad, Secretary, Treasurer, SK, Tanod, BHW, Staff, Resident
- UserPermissions matrix rewritten with barangay-specific permission mappings
- VueRouteMap replaced with barangay SPA routes
- UserActions enum references corrected across all controllers
- Removed all market/cooperative/POS/subscription code and models
This commit is contained in:
Jonathan Sykes
2026-06-07 03:09:09 +08:00
parent 19fec0933b
commit fbb7e3ff37
234 changed files with 5582 additions and 39457 deletions

View File

@@ -1,78 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Console\Commands;
use App\Models\User;
use App\Models\Market\UserInfo;
use Hyperf\Command\Annotation\Command;
use Hyperf\Command\Command as SymfonyCommand;
use Psr\Container\ContainerInterface;
/**
* @Command
*/
class ImportReadmeMemberData extends SymfonyCommand
{
public function __construct(protected ContainerInterface $container)
{
parent::__construct('import:readme-member');
}
public function configure()
{
parent::configure();
$this->setDescription('Import member data from README.md for Rex Moran Loba');
}
public function handle()
{
$email = 'rexm.loba@gmail.com';
$user = User::where('email', $email)->first();
if (!$user) {
$this->output->info("User with email {$email} not found. Creating...");
$user = User::create([
'name' => 'Rex Moran Loba',
'fullname' => 'Rex Moran Loba',
'email' => $email,
'username' => 'rexmloba',
'password' => password_hash('password', PASSWORD_DEFAULT),
'hashkey' => bin2hex(random_bytes(16)),
'active' => true,
]);
}
$userInfo = $user->userInfo;
if (!$userInfo) {
$userInfo = new UserInfo(['user_id' => $user->id]);
$userInfo->hashkey = bin2hex(random_bytes(16));
}
$userInfo->firstname = 'Rex Moran';
$userInfo->lastname = 'Loba';
$userInfo->email = $email;
$userInfo->is_active = true;
// Example address data based on feedback
$userInfo->addresses = [
[
'house_no' => 'Unit 1234',
'street' => 'Street Name',
'barangay' => 'Barangay Name',
'city' => 'City Name',
'province' => 'Province Name',
'region' => 'Region Name',
'country' => 'Philippines',
'zipcode' => '1234'
]
];
if ($userInfo->save()) {
$this->output->success("Profile for Rex Moran Loba updated successfully.");
} else {
$this->output->error("Failed to update profile.");
}
}
}

View File

@@ -1,90 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Console\Commands;
use App\Models\Market\Product;
use App\Models\Market\Store;
use App\Models\User;
use Hypervel\Console\Command;
use Hypervel\Support\Facades\Auth;
class SeedDemoProducts extends Command
{
protected ?string $signature = 'demo:seed-products {store_hash : The hashkey of the target store}';
protected string $description = 'Seed demo agricultural products';
public function handle()
{
$storeHash = $this->argument('store_hash');
$store = Store::where('hashkey', $storeHash)->first();
if (! $store) {
$this->error('Store not found.');
return;
}
$ultimateUser = User::where('acct_type', 'ULTIMATE')->first();
if (! $ultimateUser) {
$this->error('No ULTIMATE user found.');
return;
}
Auth::loginUsingId($ultimateUser->id);
$products = [
['name' => 'Organic White Rice (25kg sack)', 'category' => 'Grains & Cereals', 'subcategory' => 'Rice', 'price' => 1200, 'unit' => 'sack', 'available' => 100],
['name' => 'Organic Brown Rice (5kg)', 'category' => 'Grains & Cereals', 'subcategory' => 'Rice', 'price' => 280, 'unit' => 'kg', 'available' => 200],
['name' => 'White Corn Grits', 'category' => 'Grains & Cereals', 'subcategory' => 'Corn', 'price' => 85, 'unit' => 'kg', 'available' => 300],
['name' => 'Yellow Corn (fresh ears)', 'category' => 'Grains & Cereals', 'subcategory' => 'Corn', 'price' => 35, 'unit' => 'piece', 'available' => 500],
['name' => 'Fresh Kangkong (Water Spinach)', 'category' => 'Fresh Produce', 'subcategory' => 'Leafy Vegetables', 'price' => 25, 'unit' => 'bundle', 'available' => 150],
['name' => 'Pechay (Bok Choy)', 'category' => 'Fresh Produce', 'subcategory' => 'Leafy Vegetables', 'price' => 30, 'unit' => 'bundle', 'available' => 150],
['name' => 'Ampalaya (Bitter Gourd)', 'category' => 'Fresh Produce', 'subcategory' => 'Vegetables', 'price' => 60, 'unit' => 'kg', 'available' => 100],
['name' => 'Sitaw (String Beans)', 'category' => 'Fresh Produce', 'subcategory' => 'Vegetables', 'price' => 55, 'unit' => 'bundle', 'available' => 120],
['name' => 'Kamote (Sweet Potato)', 'category' => 'Fresh Produce', 'subcategory' => 'Root Crops', 'price' => 40, 'unit' => 'kg', 'available' => 200],
['name' => 'Gabi (Taro Root)', 'category' => 'Fresh Produce', 'subcategory' => 'Root Crops', 'price' => 50, 'unit' => 'kg', 'available' => 150],
['name' => 'Sayote (Chayote)', 'category' => 'Fresh Produce', 'subcategory' => 'Vegetables', 'price' => 45, 'unit' => 'kg', 'available' => 180],
['name' => 'Labanos (Radish)', 'category' => 'Fresh Produce', 'subcategory' => 'Root Crops', 'price' => 30, 'unit' => 'bundle', 'available' => 100],
['name' => 'Fresh Tomatoes', 'category' => 'Fresh Produce', 'subcategory' => 'Vegetables', 'price' => 70, 'unit' => 'kg', 'available' => 200],
['name' => 'Carabao Mango (green)', 'category' => 'Fresh Produce', 'subcategory' => 'Fruits', 'price' => 90, 'unit' => 'kg', 'available' => 100],
['name' => 'Banana (Latundan)', 'category' => 'Fresh Produce', 'subcategory' => 'Fruits', 'price' => 65, 'unit' => 'hand', 'available' => 80],
['name' => 'Pineapple (Bukidnon)', 'category' => 'Fresh Produce', 'subcategory' => 'Fruits', 'price' => 80, 'unit' => 'piece', 'available' => 60],
['name' => 'Native Chicken (live)', 'category' => 'Livestock & Poultry', 'subcategory' => 'Poultry', 'price' => 350, 'unit' => 'piece', 'available' => 30],
['name' => 'Free-range Eggs', 'category' => 'Livestock & Poultry', 'subcategory' => 'Eggs', 'price' => 12, 'unit' => 'piece', 'available' => 500],
['name' => 'Fresh Tilapia', 'category' => 'Seafood', 'subcategory' => 'Freshwater Fish', 'price' => 130, 'unit' => 'kg', 'available' => 50],
['name' => 'Bangus (Milkfish)', 'category' => 'Seafood', 'subcategory' => 'Saltwater Fish', 'price' => 180, 'unit' => 'kg', 'available' => 50],
['name' => 'Carabao Milk (1L)', 'category' => 'Processed Goods', 'subcategory' => 'Dairy', 'price' => 120, 'unit' => 'liter', 'available' => 60],
['name' => 'Coconut Oil (Virgin, 1L)', 'category' => 'Processed Goods', 'subcategory' => 'Oils', 'price' => 250, 'unit' => 'bottle', 'available' => 40],
['name' => 'Organic Peanuts (roasted)', 'category' => 'Processed Goods', 'subcategory' => 'Nuts', 'price' => 95, 'unit' => '250g pack', 'available' => 80],
['name' => 'Muscovado Sugar', 'category' => 'Processed Goods', 'subcategory' => 'Sweeteners', 'price' => 110, 'unit' => '250g pack', 'available' => 60],
['name' => 'Fresh Turmeric (Luyang Dilaw)', 'category' => 'Fresh Produce', 'subcategory' => 'Herbs & Spices', 'price' => 60, 'unit' => '100g pack', 'available' => 100],
];
$created = [];
foreach ($products as $item) {
$product = new Product();
$product->name = $item['name'];
$product->category = $item['category'];
$product->subcategory = $item['subcategory'];
$product->price = $item['price'];
$product->unitname = $item['unit'];
$product->description = $item['name'];
$product->is_active = true;
$product->save();
$store->products()->attach($product->id, [
'price' => $item['price'],
'available' => $item['available'],
'is_active' => true,
]);
$created[] = $product;
}
$count = count($created);
$this->info("Created {$count} products and attached to store {$store->name}");
}
}

View File

@@ -1,751 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Accounting;
use App\Http\Controllers\AbstractController;
use App\Models\Accounting\Account;
use App\Models\Accounting\AccountTransaction;
use App\Models\Market\Store;
use App\Http\Controllers\Helpers\Permissions\UserPermissions;
use App\Enums\UserActions;
use App\Enums\UserTypes;
use App\Http\Controllers\Helpers\ResponseHelper;
use App\Support\ModuleHelper;
use Hypervel\Http\Request;
use Hypervel\Support\Facades\Auth;
use Hypervel\Support\Facades\DB;
use Hyperf\Stringable\Str;
class AccountingController extends AbstractController
{
// ─── helpers ────────────────────────────────────────────────────────────
private function resolveType(): UserTypes
{
$user = Auth::user();
return $user->acct_type instanceof UserTypes
? $user->acct_type
: (UserTypes::tryFrom($user->acct_type) ?? UserTypes::PUBLIC);
}
private function isBig3(UserTypes $t): bool
{
return in_array($t, [UserTypes::ULTIMATE, UserTypes::SUPER_OPERATOR, UserTypes::OPERATOR], true);
}
private function isStoreLevel(UserTypes $t): bool
{
return in_array($t, [UserTypes::STORE_OWNER, UserTypes::STORE_MANAGER], true);
}
/** IDs of stores the current user owns or manages (including descendants). */
private function userStoreIds(): array
{
$user = Auth::user();
$allowedUserIds = array_merge([$user->id], $user->getAllDescendants()->pluck('id')->toArray());
return 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);
});
})->pluck('id')->toArray();
}
/** Apply store_id scope to a query based on the caller's role. */
private function scopeAccounts($query, array $storeIds, bool $isBig3)
{
if ($isBig3) {
return $query->whereNull('store_id');
}
return $query->whereIn('store_id', $storeIds);
}
/** Check store-level access. Returns false if the accounting_store module is disabled. */
private function guardStore(): bool
{
return ModuleHelper::isEnabled('accounting_store');
}
// ─── public endpoints ────────────────────────────────────────────────────
/**
* Full Chart of Accounts tree.
*/
public function getAccountsTree(Request $request)
{
if (!UserPermissions::isActionPermitted(Auth::user()->acct_type, UserActions::ViewAccountingReports)) {
return ResponseHelper::returnUnauthorized();
}
$type = $this->resolveType();
$isBig3 = $this->isBig3($type);
if (!$isBig3 && !$this->guardStore()) {
return ResponseHelper::returnUnauthorized();
}
$storeIds = $isBig3 ? [] : $this->userStoreIds();
$base = Account::whereNull('parent_id')->where('is_active', true);
$base = $this->scopeAccounts($base, $storeIds, $isBig3);
$accounts = $base
->with(['children' => function ($q) use ($storeIds, $isBig3) {
$q = $q->where('is_active', true);
$q = $this->scopeAccounts($q, $storeIds, $isBig3);
$q->with(['children' => function ($q2) use ($storeIds, $isBig3) {
$q2 = $q2->where('is_active', true);
$q2 = $this->scopeAccounts($q2, $storeIds, $isBig3);
}]);
}])
->orderBy('id')
->get();
return response()->json(['success' => true, 'data' => $accounts]);
}
/**
* Leaf accounts for the Daily Entry form.
*/
public function getLeafAccounts(Request $request)
{
if (!UserPermissions::isActionPermitted(Auth::user()->acct_type, UserActions::ViewAccountingReports)) {
return ResponseHelper::returnUnauthorized();
}
$type = $this->resolveType();
$isBig3 = $this->isBig3($type);
if (!$isBig3 && !$this->guardStore()) {
return ResponseHelper::returnUnauthorized();
}
$storeIds = $isBig3 ? [] : $this->userStoreIds();
$base = Account::where('is_active', true)->whereDoesntHave('children');
$base = $this->scopeAccounts($base, $storeIds, $isBig3);
$leafAccounts = $base
->with('parent.parent')
->orderBy('id')
->get();
return response()->json(['success' => true, 'data' => $leafAccounts]);
}
/**
* Daily transactions for a given date.
*/
public function getDailyTransactions(Request $request)
{
if (!UserPermissions::isActionPermitted(Auth::user()->acct_type, UserActions::ViewAccountingReports)) {
return ResponseHelper::returnUnauthorized();
}
$type = $this->resolveType();
$isBig3 = $this->isBig3($type);
if (!$isBig3 && !$this->guardStore()) {
return ResponseHelper::returnUnauthorized();
}
$storeIds = $isBig3 ? [] : $this->userStoreIds();
$date = $request->input('date', date('Y-m-d'));
$accountIds = $this->scopeAccounts(Account::query(), $storeIds, $isBig3)->pluck('id');
$transactions = AccountTransaction::with(['account'])
->whereIn('account_id', $accountIds)
->whereDate('transaction_date', $date)
->get();
$mapped = [];
foreach ($transactions as $txn) {
$mapped[$txn->account_id] = [
'id' => $txn->id,
'amount' => $txn->amount,
'notes' => $txn->notes,
];
}
return response()->json(['success' => true, 'date' => $date, 'data' => $mapped]);
}
/**
* Bulk save daily transactions.
*/
public function saveDailyTransactions(Request $request)
{
if (!UserPermissions::isActionPermitted(Auth::user()->acct_type, UserActions::ManageAccounting)) {
return ResponseHelper::returnUnauthorized();
}
$type = $this->resolveType();
$isBig3 = $this->isBig3($type);
if (!$isBig3 && !$this->guardStore()) {
return ResponseHelper::returnUnauthorized();
}
$storeIds = $isBig3 ? [] : $this->userStoreIds();
$date = $request->input('date');
$entries = $request->input('entries', []);
$userId = Auth::id();
// Allowed account IDs for this user (prevent writing to other stores' accounts)
$allowedAccountIds = $this->scopeAccounts(Account::query(), $storeIds, $isBig3)->pluck('id')->flip()->all();
DB::beginTransaction();
try {
foreach ($entries as $accountId => $entryData) {
if (!array_key_exists($accountId, $allowedAccountIds)) {
continue; // skip accounts not owned by this user
}
$amount = (float)($entryData['amount'] ?? 0);
$account = Account::find($accountId);
if (!$account) {
continue;
}
$flow = $account->default_flow
?: ((strtoupper((string) $account->type) === 'REVENUE' || strtoupper((string) $account->type) === 'LIABILITY') ? 'INCOME' : 'EXPENSE');
$txn = AccountTransaction::where('account_id', $accountId)
->whereDate('transaction_date', $date)
->first();
if ($amount == 0) {
if ($txn) {
$txn->delete();
}
continue;
}
if ($txn) {
$txn->amount = $amount;
$txn->notes = $entryData['notes'] ?? null;
$txn->updated_by = $userId;
$txn->save();
} else {
AccountTransaction::create([
'hashkey' => Str::random(32),
'account_id' => $accountId,
'amount' => $amount,
'flow' => $flow,
'transaction_date' => $date,
'notes' => $entryData['notes'] ?? null,
'created_by' => $userId,
'updated_by' => $userId,
]);
}
}
DB::commit();
return response()->json(['success' => true, 'message' => 'Daily transactions saved.']);
} catch (\Exception $e) {
DB::rollBack();
return response()->json(['success' => false, 'message' => 'Error saving: ' . $e->getMessage()]);
}
}
/**
* Transaction list for CRUD tab.
*/
public function listTransactions(Request $request)
{
if (!UserPermissions::isActionPermitted(Auth::user()->acct_type, UserActions::ViewAccountingReports)) {
return ResponseHelper::returnUnauthorized();
}
$type = $this->resolveType();
$isBig3 = $this->isBig3($type);
if (!$isBig3 && !$this->guardStore()) {
return ResponseHelper::returnUnauthorized();
}
$storeIds = $isBig3 ? [] : $this->userStoreIds();
$accountIds = $this->scopeAccounts(Account::query(), $storeIds, $isBig3)->pluck('id');
$query = AccountTransaction::with(['account', 'creator', 'updater'])
->whereIn('account_id', $accountIds);
if ($request->filled('account_id')) {
$query->where('account_id', $request->account_id);
}
if ($request->filled('date_from')) {
$query->whereDate('transaction_date', '>=', $request->date_from);
}
if ($request->filled('date_to')) {
$query->whereDate('transaction_date', '<=', $request->date_to);
}
$transactions = $query->orderBy('transaction_date', 'desc')->paginate(50);
return response()->json(['success' => true, 'data' => $transactions]);
}
/**
* Delete single transaction.
*/
public function deleteTransaction(Request $request)
{
if (!UserPermissions::isActionPermitted(Auth::user()->acct_type, UserActions::ManageAccounting)) {
return ResponseHelper::returnUnauthorized();
}
$type = $this->resolveType();
$isBig3 = $this->isBig3($type);
$storeIds = $isBig3 ? [] : $this->userStoreIds();
$allowedAccountIds = $this->scopeAccounts(Account::query(), $storeIds, $isBig3)->pluck('id')->all();
$txn = AccountTransaction::find($request->id);
if ($txn && in_array($txn->account_id, $allowedAccountIds)) {
$txn->delete();
}
return response()->json(['success' => true]);
}
/**
* Monthly matrix report (Rows = Days, Columns = Accounts).
*/
public function getMonthlyReport(Request $request)
{
if (!UserPermissions::isActionPermitted(Auth::user()->acct_type, UserActions::ViewAccountingReports)) {
return ResponseHelper::returnUnauthorized();
}
$type = $this->resolveType();
$isBig3 = $this->isBig3($type);
if (!$isBig3 && !$this->guardStore()) {
return ResponseHelper::returnUnauthorized();
}
$storeIds = $isBig3 ? [] : $this->userStoreIds();
$year = $request->input('year', date('Y'));
$month = $request->input('month', date('m'));
$accountIds = $this->scopeAccounts(Account::query(), $storeIds, $isBig3)->pluck('id')->all();
$transactions = AccountTransaction::whereYear('transaction_date', $year)
->whereMonth('transaction_date', $month)
->whereIn('account_id', $accountIds)
->get();
$daysInMonth = cal_days_in_month(CAL_GREGORIAN, (int)$month, (int)$year);
$matrix = [];
for ($i = 1; $i <= $daysInMonth; $i++) {
$matrix[$i] = [];
}
foreach ($transactions as $txn) {
$day = (int)date('d', strtotime($txn->transaction_date));
$matrix[$day][$txn->account_id] = ($matrix[$day][$txn->account_id] ?? 0) + $txn->amount;
}
$columnTotals = [];
foreach ($transactions as $txn) {
$columnTotals[$txn->account_id] = ($columnTotals[$txn->account_id] ?? 0) + $txn->amount;
}
return response()->json([
'success' => true,
'year' => $year,
'month' => $month,
'days_in_month' => $daysInMonth,
'matrix' => $matrix,
'column_totals' => $columnTotals,
]);
}
/**
* Create a new account node. For store-level users the account is
* automatically scoped to their store (first owned store).
*/
public function createAccount(Request $request)
{
if (!UserPermissions::isActionPermitted(Auth::user()->acct_type, UserActions::ManageAccounting)) {
return ResponseHelper::returnUnauthorized();
}
$type = $this->resolveType();
$isBig3 = $this->isBig3($type);
if (!$isBig3 && !$this->guardStore()) {
return ResponseHelper::returnUnauthorized();
}
$name = trim((string) $request->input('name', ''));
$accountType = strtoupper(trim((string) $request->input('type', '')));
$defaultFlow = strtoupper(trim((string) $request->input('default_flow', '')));
$parentId = $request->input('parent_id') ?: null;
$description = $request->input('description') ?: null;
if ($name === '' || $accountType === '') {
return response()->json(['success' => false, 'message' => 'Name and type are required.'], 422);
}
if (!in_array($defaultFlow, ['INCOME', 'EXPENSE'], true)) {
$defaultFlow = in_array($accountType, ['REVENUE', 'LIABILITY']) ? 'INCOME' : 'EXPENSE';
}
if ($parentId !== null && !Account::where('id', $parentId)->exists()) {
return response()->json(['success' => false, 'message' => 'Parent account not found.'], 422);
}
// Determine store_id: null for Big3 (global), first store for store-level users
$storeId = null;
if (!$isBig3) {
$storeIds = $this->userStoreIds();
if (empty($storeIds)) {
return response()->json(['success' => false, 'message' => 'No store found for your account. Create a store first.'], 422);
}
// If a parent is supplied, inherit its store_id; otherwise use the request-supplied store_id or the user's first store
if ($parentId) {
$parent = Account::find($parentId);
$storeId = $parent?->store_id ?? $storeIds[0];
} else {
$requestedStoreId = $request->input('store_id');
$storeId = ($requestedStoreId && in_array($requestedStoreId, $storeIds))
? (int)$requestedStoreId
: $storeIds[0];
}
}
try {
$account = Account::create([
'hashkey' => Str::random(40),
'parent_id' => $parentId,
'store_id' => $storeId,
'type' => $accountType,
'default_flow' => $defaultFlow,
'name' => $name,
'description' => $description,
'is_active' => true,
'created_by' => Auth::id(),
'updated_by' => Auth::id(),
]);
} catch (\Throwable $e) {
\Hypervel\Support\Facades\Log::error('createAccount failed', [
'message' => $e->getMessage(),
'name' => $name,
'type' => $accountType,
'parent_id' => $parentId,
'store_id' => $storeId,
'user_id' => Auth::id(),
]);
return response()->json([
'success' => false,
'message' => 'Could not create account: ' . $e->getMessage(),
], 422);
}
return response()->json(['success' => true, 'data' => $account]);
}
/**
* Update editable fields (name, type, default_flow, description).
*/
public function updateAccount(Request $request)
{
if (!UserPermissions::isActionPermitted(Auth::user()->acct_type, UserActions::ManageAccounting)) {
return ResponseHelper::returnUnauthorized();
}
$account = Account::find($request->input('id'));
if (!$account) {
return response()->json(['success' => false, 'message' => 'Account not found.'], 404);
}
// Store-level users may only edit their own store's accounts
$type = $this->resolveType();
$isBig3 = $this->isBig3($type);
if (!$isBig3) {
if (!$this->guardStore()) {
return ResponseHelper::returnUnauthorized();
}
$storeIds = $this->userStoreIds();
if (!in_array($account->store_id, $storeIds)) {
return ResponseHelper::returnUnauthorized();
}
}
if ($request->filled('name')) {
$account->name = trim((string) $request->input('name'));
}
if ($request->filled('type')) {
$account->type = strtoupper(trim((string) $request->input('type')));
}
if ($request->filled('default_flow')) {
$flow = strtoupper(trim((string) $request->input('default_flow')));
if (!in_array($flow, ['INCOME', 'EXPENSE'], true)) {
return response()->json(['success' => false, 'message' => 'default_flow must be INCOME or EXPENSE.'], 422);
}
$account->default_flow = $flow;
}
if ($request->has('description')) {
$account->description = $request->input('description') ?: null;
}
$account->updated_by = Auth::id();
$account->save();
return response()->json(['success' => true, 'data' => $account]);
}
/**
* Archive (soft-disable) an account.
*/
public function archiveAccount(Request $request)
{
if (!UserPermissions::isActionPermitted(Auth::user()->acct_type, UserActions::ManageAccounting)) {
return ResponseHelper::returnUnauthorized();
}
$account = Account::find($request->input('id'));
if (!$account) {
return response()->json(['success' => false, 'message' => 'Account not found.'], 404);
}
$type = $this->resolveType();
$isBig3 = $this->isBig3($type);
if (!$isBig3) {
if (!$this->guardStore()) {
return ResponseHelper::returnUnauthorized();
}
$storeIds = $this->userStoreIds();
if (!in_array($account->store_id, $storeIds)) {
return ResponseHelper::returnUnauthorized();
}
}
if (Account::where('parent_id', $account->id)->where('is_active', true)->exists()) {
return response()->json(['success' => false, 'message' => 'Archive child accounts first.'], 422);
}
if (AccountTransaction::where('account_id', $account->id)->exists()) {
return response()->json(['success' => false, 'message' => 'Cannot archive: account has transactions. Move or delete them first.'], 422);
}
$account->is_active = false;
$account->updated_by = Auth::id();
$account->save();
return response()->json(['success' => true]);
}
/**
* Move an account under a new parent (or to root). Refuses cycles.
*/
public function moveAccount(Request $request)
{
if (!UserPermissions::isActionPermitted(Auth::user()->acct_type, UserActions::ManageAccounting)) {
return ResponseHelper::returnUnauthorized();
}
$account = Account::find($request->input('id'));
if (!$account) {
return response()->json(['success' => false, 'message' => 'Account not found.'], 404);
}
$type = $this->resolveType();
$isBig3 = $this->isBig3($type);
if (!$isBig3) {
if (!$this->guardStore()) {
return ResponseHelper::returnUnauthorized();
}
$storeIds = $this->userStoreIds();
if (!in_array($account->store_id, $storeIds)) {
return ResponseHelper::returnUnauthorized();
}
}
$newParentId = $request->input('parent_id');
$newParentId = ($newParentId === null || $newParentId === '' || $newParentId == 0) ? null : (int)$newParentId;
if ($newParentId === (int)$account->id) {
return response()->json(['success' => false, 'message' => 'An account cannot be its own parent.'], 422);
}
if ($newParentId !== null) {
$parent = Account::find($newParentId);
if (!$parent) {
return response()->json(['success' => false, 'message' => 'Target parent not found.'], 422);
}
$cursor = $parent;
$hops = 0;
while ($cursor && $hops < 1000) {
if ((int)$cursor->id === (int)$account->id) {
return response()->json(['success' => false, 'message' => 'Cannot move an account under one of its own descendants.'], 422);
}
$cursor = $cursor->parent_id ? Account::find($cursor->parent_id) : null;
$hops++;
}
}
$account->parent_id = $newParentId;
$account->updated_by = Auth::id();
$account->save();
return response()->json(['success' => true]);
}
/**
* Re-activate an archived account.
*/
public function restoreAccount(Request $request)
{
if (!UserPermissions::isActionPermitted(Auth::user()->acct_type, UserActions::ManageAccounting)) {
return ResponseHelper::returnUnauthorized();
}
$account = Account::find($request->input('id'));
if (!$account) {
return response()->json(['success' => false, 'message' => 'Account not found.'], 404);
}
$type = $this->resolveType();
$isBig3 = $this->isBig3($type);
if (!$isBig3) {
if (!$this->guardStore()) {
return ResponseHelper::returnUnauthorized();
}
$storeIds = $this->userStoreIds();
if (!in_array($account->store_id, $storeIds)) {
return ResponseHelper::returnUnauthorized();
}
}
$account->is_active = true;
$account->updated_by = Auth::id();
$account->save();
return response()->json(['success' => true]);
}
/**
* Theme metadata + drift summary for Manage Accounts.
*/
public function getThemeInfo(Request $request)
{
if (!UserPermissions::isActionPermitted(Auth::user()->acct_type, UserActions::ViewAccountingReports)) {
return ResponseHelper::returnUnauthorized();
}
$current = \App\Support\AccountingTheme::current();
$definition = \App\Support\AccountingTheme::definition($current);
return response()->json([
'success' => true,
'current' => $current,
'definition' => [
'label' => $definition['label'] ?? $current,
'description' => $definition['description'] ?? null,
'version' => $definition['version'] ?? 1,
],
'options' => \App\Support\AccountingTheme::options(),
'counts' => [
'theme_tagged' => Account::where('theme_key', $current)->count(),
'user_added' => Account::whereNull('theme_key')->count(),
'archived' => Account::where('is_active', false)->count(),
],
]);
}
/**
* Read-only drift report.
*/
public function getThemeDrift(Request $request)
{
if (!UserPermissions::isActionPermitted(Auth::user()->acct_type, UserActions::ViewAccountingReports)) {
return ResponseHelper::returnUnauthorized();
}
return response()->json([
'success' => true,
'data' => \App\Support\AccountingTheme::drift(),
]);
}
/**
* Switch the active accounting theme.
*/
public function setTheme(Request $request)
{
if (!UserPermissions::isActionPermitted(Auth::user()->acct_type, UserActions::ManageAccounting)) {
return ResponseHelper::returnUnauthorized();
}
$key = trim((string) $request->input('key', ''));
if ($key === '' || !\array_key_exists($key, \App\Support\AccountingTheme::all())) {
return response()->json(['success' => false, 'message' => 'Unknown theme.'], 422);
}
\App\Models\SystemSetting::setValue('accounting_theme', $key, 'accounting', 'string');
return response()->json(['success' => true, 'key' => $key]);
}
/**
* Idempotently re-apply the active theme (additive only).
*/
public function applyTheme(Request $request)
{
if (!UserPermissions::isActionPermitted(Auth::user()->acct_type, UserActions::ManageAccounting)) {
return ResponseHelper::returnUnauthorized();
}
try {
$stats = \App\Support\AccountingTheme::apply();
} catch (\Throwable $e) {
return response()->json([
'success' => false,
'message' => 'Failed to apply theme: ' . $e->getMessage(),
], 422);
}
return response()->json([
'success' => true,
'message' => sprintf(
'Theme "%s" applied: %d created, %d updated, %d unchanged.',
$stats['theme_key'],
$stats['created'],
$stats['stamped'],
$stats['skipped']
),
'data' => $stats,
]);
}
/**
* Recent transactions for the Reports page.
*/
public function listReports(Request $request)
{
if (!UserPermissions::isActionPermitted(Auth::user()->acct_type, UserActions::ViewAccountingReports)) {
return ResponseHelper::returnUnauthorized();
}
$type = $this->resolveType();
$isBig3 = $this->isBig3($type);
if (!$isBig3 && !$this->guardStore()) {
return ResponseHelper::returnUnauthorized();
}
$storeIds = $isBig3 ? [] : $this->userStoreIds();
$accountIds = $this->scopeAccounts(Account::query(), $storeIds, $isBig3)->pluck('id');
$transactions = AccountTransaction::with(['account', 'creator'])
->whereIn('account_id', $accountIds)
->orderBy('transaction_date', 'desc')
->limit(100)
->get();
return response()->json(['success' => true, 'transactions' => $transactions]);
}
}

View File

@@ -0,0 +1,162 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Admin;
use App\Enums\UserActions;
use App\Enums\UserTypes;
use App\Http\Controllers\Helpers\Permissions\UserPermissions;
use App\Http\Controllers\Helpers\ResponseHelper;
use App\Models\User;
use App\Models\DbBackup;
use Hyperf\Stringable\Str;
use Hypervel\Http\Request;
use Hypervel\Support\Facades\Auth;
use Hypervel\Support\Facades\DB;
use Hypervel\Support\Facades\Redis;
use Hypervel\Support\Facades\Response;
class AdminConsoleController
{
private function checkAccess(): bool
{
if (!Auth::check()) return false;
return UserPermissions::isActionPermitted(Auth::user()->acct_type, UserActions::UltimateConsole);
}
public function getSystemStats()
{
if (!$this->checkAccess()) return ResponseHelper::returnUnauthorized();
$globalMessage = Redis::get('system:global_message');
$redisStatus = ['connected' => false, 'ping_ms' => null, 'used_memory_human' => null, 'version' => null, 'error' => null];
try {
$start = microtime(true);
$pong = Redis::ping();
$redisStatus['ping_ms'] = round((microtime(true) - $start) * 1000, 2);
$redisStatus['connected'] = in_array($pong, [true, 'PONG', '+PONG'], true) || (is_string($pong) && stripos($pong, 'PONG') !== false);
$info = Redis::info();
if (is_array($info)) {
$flat = isset($info['Memory']) ? $info['Memory'] : $info;
$redisStatus['used_memory_human'] = $flat['used_memory_human'] ?? null;
$serverInfo = isset($info['Server']) ? $info['Server'] : $info;
$redisStatus['version'] = $serverInfo['redis_version'] ?? null;
}
} catch (\Throwable $e) {
$redisStatus['error'] = $e->getMessage();
}
$stats = [
'users' => User::count(),
'active_users' => User::where('active', true)->count(),
'residents' => DB::table('barangay_residents')->count(),
'households' => DB::table('barangay_households')->count(),
'blotters' => DB::table('barangay_blotters')->count(),
'document_requests' => DB::table('barangay_document_requests')->count(),
'projects' => DB::table('barangay_projects')->count(),
'announcements' => DB::table('announcements')->count(),
'php_version' => PHP_VERSION,
'server_time' => date('Y-m-d H:i:s'),
'maintenance_mode' => Redis::get('system:maintenance_mode') === 'true',
'global_message' => $globalMessage ? json_decode($globalMessage, true) : null,
'logs_count' => DB::table('logs')->count(),
'table_logs_count' => DB::table('table_logs')->count(),
'redis' => $redisStatus,
];
return Response::json(['success' => true, 'data' => $stats]);
}
public function runQuery(Request $request)
{
if (Auth::user()->acct_type !== UserTypes::SUPER_ADMIN || !UserPermissions::isActionPermitted(Auth::user()->acct_type, UserActions::UltimateQuery)) {
return ResponseHelper::returnUnauthorized();
}
$query = $request->input('query');
if (empty($query)) return ResponseHelper::returnError('Query cannot be empty');
$lower = strtolower(trim($query));
$allowed = str_starts_with($lower, 'select') || str_starts_with($lower, 'show') || str_starts_with($lower, 'describe') || str_starts_with($lower, 'explain');
if (!$allowed) {
return ResponseHelper::returnError('Only SELECT, SHOW, DESCRIBE, EXPLAIN queries are allowed');
}
try {
$results = DB::select($query);
return Response::json(['success' => true, 'data' => $results, 'count' => count($results)]);
} catch (\Throwable $e) {
return ResponseHelper::returnError('Query error: ' . $e->getMessage());
}
}
public function setMaintenanceMode(Request $request)
{
if (!$this->checkAccess()) return ResponseHelper::returnUnauthorized();
$enabled = (bool) $request->input('enabled', false);
Redis::set('system:maintenance_mode', $enabled ? 'true' : 'false');
return Response::json(['success' => true, 'maintenance_mode' => $enabled]);
}
public function setGlobalMessage(Request $request)
{
if (!$this->checkAccess()) return ResponseHelper::returnUnauthorized();
$message = $request->input('message');
if ($message) {
Redis::set('system:global_message', json_encode([
'text' => $message,
'type' => $request->input('type', 'info'),
'updated_at' => now()->toDateTimeString(),
]));
} else {
Redis::del('system:global_message');
}
return Response::json(['success' => true]);
}
public function clearCache(Request $request)
{
if (!$this->checkAccess()) return ResponseHelper::returnUnauthorized();
Redis::flushDB();
return Response::json(['success' => true, 'message' => 'Cache cleared']);
}
public function getLogs(Request $request)
{
if (!$this->checkAccess()) return ResponseHelper::returnUnauthorized();
$limit = min((int) $request->input('limit', 50), 200);
$logs = DB::table('logs')->orderByDesc('id')->limit($limit)->get();
return Response::json(['success' => true, 'data' => $logs]);
}
public function getTableLogs(Request $request)
{
if (!$this->checkAccess()) return ResponseHelper::returnUnauthorized();
$limit = min((int) $request->input('limit', 50), 200);
$logs = DB::table('table_logs')->orderByDesc('id')->limit($limit)->get();
return Response::json(['success' => true, 'data' => $logs]);
}
public function backupDatabase(Request $request)
{
if (!$this->checkAccess()) return ResponseHelper::returnUnauthorized();
$name = $request->input('name', 'backup_' . date('Y_m_d_His'));
$backup = DbBackup::create([
'name' => $name,
'status' => 'pending',
'created_by' => Auth::id(),
]);
return Response::json(['success' => true, 'data' => $backup, 'message' => 'Backup queued']);
}
public function listBackups()
{
if (!$this->checkAccess()) return ResponseHelper::returnUnauthorized();
$backups = DbBackup::orderByDesc('id')->limit(20)->get();
return Response::json(['success' => true, 'data' => $backups]);
}
}

View File

@@ -1,174 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Helpers\ResponseHelper;
use App\Models\Market\PosAccessKey;
use App\Models\Market\Store;
use App\Models\User;
use Hypervel\Http\Request;
use Hypervel\Support\Facades\Auth;
use Hyperf\Stringable\Str;
use App\Http\Controllers\Helpers\Permissions\UserPermissions;
use App\Enums\UserActions;
class PosAccessKeyController
{
/**
* List POS access keys. Auto-expires any past-due keys first.
* - Ultimate/super operator/operator: see all keys
* - Store owner/manager: see keys for stores they own or manage
*/
public function index(Request $request)
{
if (!UserPermissions::isActionPermitted(Auth::user()->acct_type, UserActions::ViewPosAccessKeys)) {
return ResponseHelper::returnUnauthorized();
}
// Auto-deactivate any expired keys
PosAccessKey::autoExpire();
$user = Auth::user();
$acctType = $user->acct_type->value ?? $user->acct_type ?? '';
// Ultimate, Super Operator, and Operator see everything
if (in_array($acctType, ['ult', 'super operator', 'operator'])) {
$query = PosAccessKey::with(['store:id,name,hashkey,owner_id', 'store.owner:id,name,nickname,hashkey'])
->orderBy('id', 'desc');
} else {
// Non-ultimate users see their own and their descendants' stores' keys
$descendants = $user->getAllDescendants();
$descendantIds = $descendants->pluck('id')->push($user->id)->toArray();
$storeIds = Store::whereIn('owner_id', $descendantIds)
->orWhereIn('manager_id', $descendantIds)
->pluck('id')
->toArray();
$query = PosAccessKey::with(['store:id,name,hashkey,owner_id', 'store.owner:id,name,nickname,hashkey'])
->whereIn('store_id', $storeIds)
->orderBy('id', 'desc');
}
$keys = $query->get();
return ResponseHelper::returnSuccessResponse($keys, 'pos_access_keys');
}
public function store(Request $request)
{
if (!UserPermissions::isActionPermitted(Auth::user()->acct_type, UserActions::CreatePosAccessKey)) {
return ResponseHelper::returnUnauthorized();
}
$validated = $request->validate([
'name' => 'required|string|max:255',
'store_hash' => 'required|string',
'expires_at' => 'nullable|string',
]);
$store = Store::where('hashkey', $validated['store_hash'])->first();
if (!$store) {
return ResponseHelper::returnError('Store not found', 404);
}
$user = Auth::user();
$acctType = $user->acct_type->value ?? $user->acct_type ?? '';
if (!in_array($acctType, ['ult', 'super operator', 'operator'])) {
$descendants = $user->getAllDescendants();
$allowedIds = $descendants->pluck('id')->push($user->id)->toArray();
if (!in_array($store->owner_id, $allowedIds) && !in_array($store->manager_id, $allowedIds)) {
return ResponseHelper::returnError('Unauthorized to create keys for this store', 403);
}
}
$data = [
'access_key' => 'PK-' . Str::upper(Str::random(16)),
'store_id' => $store->id,
'name' => $validated['name'],
'created_by' => Auth::id(),
'status' => 'active',
];
// Set expiry if provided
if (!empty($validated['expires_at'])) {
$data['expires_at'] = $validated['expires_at'];
}
$key = PosAccessKey::create($data);
return ResponseHelper::returnSuccessResponse($key, $key->hashkey, 'POS Access Key created');
}
public function destroy(Request $request)
{
if (!UserPermissions::isActionPermitted(Auth::user()->acct_type, UserActions::DeletePosAccessKey)) {
return ResponseHelper::returnUnauthorized();
}
$hashkey = $request->input('target');
$key = PosAccessKey::with('store')->where('hashkey', $hashkey)->first();
if (!$key) {
return ResponseHelper::returnError('Key not found', 404);
}
if (!$this->userOwnsKeyStore($key)) {
return ResponseHelper::returnError('Unauthorized to delete this key', 403);
}
$key->delete();
return ResponseHelper::returnSuccessResponse([], $hashkey, 'Key deleted');
}
public function toggleStatus(Request $request)
{
if (!UserPermissions::isActionPermitted(Auth::user()->acct_type, UserActions::TogglePosAccessKey)) {
return ResponseHelper::returnUnauthorized();
}
$hashkey = $request->input('target');
$key = PosAccessKey::with('store')->where('hashkey', $hashkey)->first();
if (!$key) {
return ResponseHelper::returnError('Key not found', 404);
}
if (!$this->userOwnsKeyStore($key)) {
return ResponseHelper::returnError('Unauthorized to modify this key', 403);
}
$key->status = $key->status === 'active' ? 'inactive' : 'active';
$key->save();
return ResponseHelper::returnSuccessResponse($key, $hashkey, 'Status updated');
}
/**
* Ownership gate shared by destroy/toggleStatus: Big 3 always pass;
* everyone else must own or manage (directly or via descendants) the
* store the key belongs to.
*/
private function userOwnsKeyStore(PosAccessKey $key): bool
{
$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;
}
$store = $key->store;
if (!$store) {
return false;
}
$allowedIds = $user->getAllDescendants()->pluck('id')->push($user->id)->toArray();
return in_array($store->owner_id, $allowedIds) || in_array($store->manager_id, $allowedIds);
}
}

View File

@@ -155,42 +155,9 @@ class SystemSettingsController
$settings['app_logo_url'] = cdn_asset('vendor/assets/icons/192x192.png'); // Fallback to PWA icon on the CDN
}
// Resolve main organization hashkey to a usable data object
$settings['main_organization_data'] = null;
if (!empty($settings['main_organization'])) {
$org = \App\Models\Market\Organization::where('hashkey', $settings['main_organization'])->first();
if ($org) {
$settings['main_organization_data'] = [
'hashkey' => $org->hashkey,
'name' => $org->name,
'type' => $org->type,
];
}
}
return $settings;
}
/**
* List organizations available for selection as the main cooperative/organization.
*/
public function listOrganizations()
{
if (!Auth::user()->isUltimate()) {
return ResponseHelper::returnUnauthorized();
}
$orgs = \App\Models\Market\Organization::where('is_active', true)
->orderBy('type')
->orderBy('name')
->get(['hashkey', 'name', 'type']);
return response()->json([
'success' => true,
'data' => $orgs,
]);
}
/**
* Get all modules with their effective state, env/config default, and
* any DB-stored override. Used by the Ultimate Console module manager.

View File

@@ -0,0 +1,207 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Admin;
use App\Enums\UserActions;
use App\Enums\UserTypes;
use App\Http\Controllers\Helpers\Permissions\UserPermissions;
use App\Http\Controllers\Helpers\ResponseHelper;
use App\Models\User;
use Hypervel\Http\Request;
use Hypervel\Support\Facades\Auth;
use Hypervel\Support\Facades\Hash;
use Hypervel\Support\Facades\Redis;
use Hypervel\Support\Facades\Validator;
class UserController
{
public function index(Request $request)
{
if (!UserPermissions::isActionPermitted(Auth::user()->acct_type, UserActions::ListAllUsersAsParentforUserCreation)) {
return ResponseHelper::returnUnauthorized();
}
$query = User::orderByDesc('id');
if ($search = $request->input('search')) {
$query->where(function ($q) use ($search) {
$q->where('name', 'like', "%{$search}%")
->orWhere('mobile_number', 'like', "%{$search}%")
->orWhere('username', 'like', "%{$search}%");
});
}
if ($acctType = $request->input('acct_type')) $query->where('acct_type', $acctType);
if ($request->input('active_only')) $query->where('active', true);
$users = $query->paginate((int) $request->input('per_page', 25));
return response()->json(['success' => true, 'data' => $users]);
}
public function show(Request $request)
{
$target = $request->input('target');
$user = is_numeric($target)
? User::find($target)
: User::where('hashkey', $target)->first();
if (!$user) return ResponseHelper::returnError('User not found', 404);
if (!UserPermissions::isActionPermitted(Auth::user()->acct_type, UserActions::ViewUserInfo)) {
return ResponseHelper::returnUnauthorized();
}
return response()->json(['success' => true, 'data' => $this->present($user)]);
}
public function store(Request $request)
{
if (!UserPermissions::isActionPermitted(Auth::user()->acct_type, UserActions::CreateUser)) {
return ResponseHelper::returnUnauthorized();
}
$validator = Validator::make($request->all(), [
'name' => 'required|string|max:255',
'mobile_number' => 'required|string|max:20|unique:users,mobile_number',
'username' => 'nullable|string|max:100|unique:users,username',
'password' => 'required|string|min:6',
'acct_type' => 'required|string',
'parentuid' => 'nullable|integer|exists:users,id',
]);
if ($validator->fails()) {
return response()->json(['success' => false, 'errors' => $validator->errors()], 422);
}
$acctType = UserTypes::tryFrom($request->input('acct_type'));
if (!$acctType) {
return ResponseHelper::returnError('Invalid account type', 422);
}
$user = User::create([
'name' => $request->input('name'),
'mobile_number' => $request->input('mobile_number'),
'username' => $request->input('username'),
'password' => Hash::make($request->input('password')),
'acct_type' => $acctType,
'parentuid' => $request->input('parentuid', Auth::id()),
'hashkey' => hash('sha256', uniqid((string) now(), true)),
'active' => true,
]);
return response()->json(['success' => true, 'data' => $this->present($user), 'message' => 'User created']);
}
public function update(Request $request)
{
$target = $request->input('target');
$user = User::where('hashkey', $target)->first();
if (!$user) return ResponseHelper::returnError('User not found', 404);
if (!UserPermissions::isActionPermitted($target, UserActions::ModifyUser)) {
return ResponseHelper::returnUnauthorized();
}
$allowedFields = ['name', 'username', 'acct_type', 'nickname', 'fullname'];
$data = $request->only($allowedFields);
if (isset($data['acct_type'])) {
$acctType = UserTypes::tryFrom($data['acct_type']);
if (!$acctType) return ResponseHelper::returnError('Invalid account type', 422);
$data['acct_type'] = $acctType;
}
$user->update($data);
return response()->json(['success' => true, 'data' => $this->present($user), 'message' => 'User updated']);
}
public function setActive(Request $request)
{
$target = $request->input('target');
$user = User::where('hashkey', $target)->first();
if (!$user) return ResponseHelper::returnError('User not found', 404);
$active = (bool) $request->input('active', true);
$action = $active ? UserActions::SetActiveUser : UserActions::SetInActiveUser;
if (!UserPermissions::isActionPermitted($target, $action)) {
return ResponseHelper::returnUnauthorized();
}
$user->update(['active' => $active]);
return response()->json(['success' => true, 'data' => $this->present($user)]);
}
public function changePassword(Request $request)
{
$target = $request->input('target');
$user = User::where('hashkey', $target)->first();
if (!$user) return ResponseHelper::returnError('User not found', 404);
if (!UserPermissions::isActionPermitted($target, UserActions::ChangeUserPassword)) {
return ResponseHelper::returnUnauthorized();
}
$validator = Validator::make($request->all(), [
'password' => 'required|string|min:6',
]);
if ($validator->fails()) {
return response()->json(['success' => false, 'errors' => $validator->errors()], 422);
}
$user->update(['password' => Hash::make($request->input('password'))]);
// Force logout all sessions for this user
Redis::del("user_session:{$user->id}");
return response()->json(['success' => true, 'message' => 'Password changed']);
}
public function forceLogout(Request $request)
{
$target = $request->input('target');
$user = User::where('hashkey', $target)->first();
if (!$user) return ResponseHelper::returnError('User not found', 404);
if (!UserPermissions::isActionPermitted($target, UserActions::ForceLogoutUser)) {
return ResponseHelper::returnUnauthorized();
}
Redis::del("user_session:{$user->id}");
return response()->json(['success' => true, 'message' => 'User session cleared']);
}
public function accountTypes()
{
$types = collect(UserTypes::cases())->map(fn ($t) => [
'value' => $t->value,
'label' => ucwords(str_replace('_', ' ', $t->value)),
]);
return response()->json(['success' => true, 'data' => $types]);
}
private function present(User $user): array
{
return [
'id' => $user->id,
'hashkey' => $user->hashkey,
'name' => $user->name,
'fullname' => $user->fullname,
'nickname' => $user->nickname,
'username' => $user->username,
'mobile_number' => $user->mobile_number,
'acct_type' => $user->acct_type,
'active' => (bool) $user->active,
'parentuid' => $user->parentuid,
'created_at' => $user->created_at,
'updated_at' => $user->updated_at,
];
}
}

View File

@@ -0,0 +1,184 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Auth;
use Hypervel\Http\Request;
use Hypervel\Support\Facades\Cache;
use Hypervel\Support\Facades\Response;
use Psr\Http\Message\ResponseInterface;
use Hypervel\Support\Facades\Auth;
use App\Models\User;
use Hypervel\Support\Facades\Hash;
use Hypervel\Support\Facades\Log;
use Hypervel\Support\Facades\Redis;
class LoginController
{
public function authenticate(Request $request): ResponseInterface
{
$credentials = $request->inputs(['mobile_number', 'password']);
$keepalive = $request->input('keepalive');
if (!$credentials['mobile_number'] || !$credentials['password']) {
return Response::json([
'success' => false,
'message' => 'Missing fields.',
], 422);
}
$candidates = self::phMobileVariants($credentials['mobile_number']);
$user = User::whereIn('mobile_number', $candidates)->first();
if (!$user) {
return Response::json([
'success' => false,
'message' => 'Account not found.',
], 401);
}
Log::info('Login attempt', [
'mobile_number' => $credentials['mobile_number'],
'candidates' => $candidates,
'user_found' => true,
'active' => $user->active,
'acct_type' => $user->acct_type->value ?? null,
]);
if (!$user->active) {
return Response::json([
'success' => false,
'message' => 'Account is inactive. Please contact support.',
], 401);
}
if ($user && Hash::check($credentials['password'], $user->password)) {
Auth::login($user); // or Auth::guard()->login($user)
$current_userHashkey = $user->hashkey;
$sessionId = session()->getId();
Redis::sadd("user_sessions:{$current_userHashkey}", $sessionId);
// $request->session()->regenerate();
Log::info('KeepAlive Value ' . $keepalive);
if ($keepalive === true || $keepalive === 'true') {
self::setSessiontoKeepAlive();
}
return Response::json([
'success' => true,
'message' => 'Login successful',
]);
}
return Response::json([
'success' => false,
'message' => 'Invalid credentials.',
], 401);
}
/**
* Set or extends current session auth<br>
* This Function is Not Working JWT Automatically sets based on config file ttl
* @param string|bool $sessionid if false then get current sessionID
* @param int|bool $aliveinseconds if true then consider it forever
* @return null|bool // Time To Live TTL
*/
public static function setSessiontoKeepAlive(string|false $sessionId = false, int|bool $aliveinseconds = true)
{
$sessionId = $sessionId ?: session()->getId();
if (!$sessionId) {
return false;
}
if ($aliveinseconds === true) {
$aliveinseconds = 7889472; // 3 months
}
if ($aliveinseconds === false) {
return false;
}
// The redis session driver stores the key under the Redis connection prefix.
// Cache::get/put uses a different prefix (cache store), so we use Redis::expire()
// directly to extend the TTL of the existing session key without reading its value.
$result = Redis::expire($sessionId, $aliveinseconds);
if (!$result) {
Log::warning('setSessiontoKeepAlive: session key not found in Redis, cannot extend TTL for: ' . $sessionId);
return false;
}
$ttl = Redis::ttl($sessionId);
Log::info('extended session: ' . $sessionId . ' for ' . $aliveinseconds . 's, TTL: ' . $ttl);
return $ttl;
}
/**
* Build all plausible stored forms of a Philippine mobile number so the
* caller can match whichever variant is in the DB. Variants returned:
* - 09XXXXXXXXX
* - 9XXXXXXXXX (no leading 0)
* - 639XXXXXXXXX
* - +639XXXXXXXXX
* plus the original raw input as a final fallback.
*/
public static function phMobileVariants(string $input): array
{
$digits = preg_replace('/\D+/', '', $input);
$core = null; // the 10-digit 9XXXXXXXXX portion
if (preg_match('/^639(\d{9})$/', $digits, $m)) {
$core = '9' . $m[1];
} elseif (preg_match('/^09(\d{9})$/', $digits, $m)) {
$core = '9' . $m[1];
} elseif (preg_match('/^9(\d{9})$/', $digits, $m)) {
$core = '9' . $m[1];
}
$variants = [$input];
if ($core) {
$variants[] = '0' . $core;
$variants[] = $core;
$variants[] = '63' . $core;
$variants[] = '+63' . $core;
}
return array_values(array_unique($variants));
}
/**
* Back-compat shim: still used by UserModifyAdminPageController to
* canonicalize on save (admin edit). Picks 09XXXXXXXXX when possible.
*/
public static function normalizePhMobile(string $input): string
{
$digits = preg_replace('/\D+/', '', $input);
if (preg_match('/^639(\d{9})$/', $digits, $m)) {
return '09' . $m[1];
}
if (preg_match('/^9(\d{9})$/', $digits, $m)) {
return '09' . $m[1];
}
if (preg_match('/^09\d{9}$/', $digits)) {
return $digits;
}
return $input;
}
public function extendcurrentSession()
{
$ttl = self::setSessiontoKeepAlive();
return Response::make('TTL is '.$ttl);
}
}

View File

@@ -0,0 +1,140 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Barangay;
use App\Enums\UserActions;
use App\Enums\Barangay\BlotterStatus;
use App\Http\Controllers\Helpers\Permissions\UserPermissions;
use App\Http\Controllers\Helpers\ResponseHelper;
use App\Models\Barangay\Blotter;
use Hypervel\Http\Request;
use Hypervel\Support\Facades\Auth;
use Hypervel\Support\Facades\Validator;
class BlotterController
{
private function checkRead(): bool
{
return UserPermissions::isActionPermitted(Auth::user()->acct_type, UserActions::ViewBlotters);
}
private function checkWrite(): bool
{
return UserPermissions::isActionPermitted(Auth::user()->acct_type, UserActions::ProcessBlotter);
}
public function index(Request $request)
{
if (!$this->checkRead()) return ResponseHelper::returnUnauthorized();
$query = Blotter::with(['assignedOfficer'])->orderByDesc('id');
if ($status = $request->input('status')) $query->where('status', $status);
if ($type = $request->input('incident_type')) $query->where('incident_type', $type);
if ($search = $request->input('search')) {
$query->where(function ($q) use ($search) {
$q->where('complainant_name', 'like', "%{$search}%")
->orWhere('respondent_name', 'like', "%{$search}%")
->orWhere('blotter_no', 'like', "%{$search}%");
});
}
$blotters = $query->paginate((int) $request->input('per_page', 20));
return response()->json(['success' => true, 'data' => $blotters]);
}
public function show(Request $request)
{
if (!$this->checkRead()) return ResponseHelper::returnUnauthorized();
$blotter = Blotter::with(['assignedOfficer', 'hearings.officer'])
->where('hashkey', $request->input('target'))
->first();
if (!$blotter) return ResponseHelper::returnError('Blotter not found', 404);
return response()->json(['success' => true, 'data' => $blotter]);
}
public function store(Request $request)
{
if (!$this->checkWrite()) return ResponseHelper::returnUnauthorized();
$validator = Validator::make($request->all(), [
'complainant_name' => 'required|string|max:255',
'complainant_contact' => 'nullable|string|max:30',
'complainant_address' => 'nullable|string|max:500',
'respondent_name' => 'required|string|max:255',
'respondent_contact' => 'nullable|string|max:30',
'respondent_address' => 'nullable|string|max:500',
'incident_type' => 'required|in:AMICABLE,UNLAWFUL,MINOR,OTHER',
'incident_date' => 'required|date',
'incident_location' => 'nullable|string|max:500',
'narrative' => 'required|string',
'complainant_user_id' => 'nullable|integer|exists:users,id',
'respondent_user_id' => 'nullable|integer|exists:users,id',
]);
if ($validator->fails()) {
return response()->json(['success' => false, 'errors' => $validator->errors()], 422);
}
$data = $validator->validated();
$data['hashkey'] = hash('sha256', uniqid((string) now(), true));
$data['blotter_no'] = Blotter::generateBlotterNo();
$data['status'] = BlotterStatus::FILED;
$data['complaint_date'] = now()->toDateString();
$data['filed_by'] = Auth::id();
$data['is_active'] = true;
$data['created_by'] = Auth::id();
$data['updated_by'] = Auth::id();
$blotter = Blotter::create($data);
return response()->json(['success' => true, 'data' => $blotter, 'message' => 'Blotter filed successfully']);
}
public function updateStatus(Request $request)
{
if (!$this->checkWrite()) return ResponseHelper::returnUnauthorized();
$blotter = Blotter::where('hashkey', $request->input('target'))->first();
if (!$blotter) return ResponseHelper::returnError('Blotter not found', 404);
$newStatus = BlotterStatus::tryFrom($request->input('status'));
if (!$newStatus) return ResponseHelper::returnError('Invalid status', 422);
$data = ['status' => $newStatus, 'updated_by' => Auth::id()];
if ($request->input('resolution')) $data['resolution'] = $request->input('resolution');
if ($request->input('endorsed_to')) $data['endorsed_to'] = $request->input('endorsed_to');
if ($request->input('settlement_type')) $data['settlement_type'] = $request->input('settlement_type');
$blotter->update($data);
return response()->json(['success' => true, 'data' => $blotter, 'message' => 'Status updated']);
}
public function assignOfficer(Request $request)
{
if (!$this->checkWrite()) return ResponseHelper::returnUnauthorized();
$blotter = Blotter::where('hashkey', $request->input('target'))->first();
if (!$blotter) return ResponseHelper::returnError('Blotter not found', 404);
$blotter->update([
'assigned_officer_id' => $request->input('officer_id'),
'updated_by' => Auth::id(),
]);
return response()->json(['success' => true, 'data' => $blotter, 'message' => 'Officer assigned']);
}
public function statusOptions()
{
$options = collect(BlotterStatus::cases())->map(fn ($s) => ['value' => $s->value, 'label' => $s->label()]);
return response()->json(['success' => true, 'data' => $options]);
}
}

View File

@@ -0,0 +1,77 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Barangay;
use App\Enums\UserActions;
use App\Http\Controllers\Helpers\Permissions\UserPermissions;
use App\Http\Controllers\Helpers\ResponseHelper;
use App\Models\Barangay\Blotter;
use App\Models\Barangay\BlotterHearing;
use Hypervel\Http\Request;
use Hypervel\Support\Facades\Auth;
use Hypervel\Support\Facades\Validator;
class BlotterHearingController
{
private function checkWrite(): bool
{
return UserPermissions::isActionPermitted(Auth::user()->acct_type, UserActions::ManageBlotterHearings);
}
public function index(Request $request)
{
$blotter = Blotter::where('hashkey', $request->input('blotter'))->first();
if (!$blotter) return ResponseHelper::returnError('Blotter not found', 404);
$hearings = BlotterHearing::with('officer')
->where('blotter_id', $blotter->id)
->orderBy('hearing_date')
->get();
return response()->json(['success' => true, 'data' => $hearings]);
}
public function schedule(Request $request)
{
if (!$this->checkWrite()) return ResponseHelper::returnUnauthorized();
$validator = Validator::make($request->all(), [
'blotter' => 'required|string',
'hearing_date' => 'required|date',
'officer_id' => 'nullable|integer|exists:users,id',
'notes' => 'nullable|string',
]);
if ($validator->fails()) {
return response()->json(['success' => false, 'errors' => $validator->errors()], 422);
}
$blotter = Blotter::where('hashkey', $request->input('blotter'))->first();
if (!$blotter) return ResponseHelper::returnError('Blotter not found', 404);
$hearing = BlotterHearing::create([
'blotter_id' => $blotter->id,
'hearing_date' => $request->input('hearing_date'),
'status' => 'SCHEDULED',
'officer_id' => $request->input('officer_id', $blotter->assigned_officer_id),
'notes' => $request->input('notes'),
]);
return response()->json(['success' => true, 'data' => $hearing, 'message' => 'Hearing scheduled']);
}
public function update(Request $request)
{
if (!$this->checkWrite()) return ResponseHelper::returnUnauthorized();
$hearing = BlotterHearing::find($request->input('hearing_id'));
if (!$hearing) return ResponseHelper::returnError('Hearing not found', 404);
$data = $request->only(['status', 'notes', 'resolution', 'next_hearing_date']);
$hearing->update($data);
return response()->json(['success' => true, 'data' => $hearing, 'message' => 'Hearing updated']);
}
}

View File

@@ -0,0 +1,126 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Barangay;
use App\Enums\UserActions;
use App\Http\Controllers\Helpers\Permissions\UserPermissions;
use App\Http\Controllers\Helpers\ResponseHelper;
use App\Models\Barangay\BarangayBudget;
use Hypervel\Http\Request;
use Hypervel\Support\Facades\Auth;
use Hypervel\Support\Facades\Validator;
class BudgetController
{
private function checkRead(): bool
{
return UserPermissions::isActionPermitted(Auth::user()->acct_type, UserActions::ViewBarangayBudget);
}
private function checkWrite(): bool
{
return UserPermissions::isActionPermitted(Auth::user()->acct_type, UserActions::ManageBarangayBudget);
}
public function index(Request $request)
{
if (!$this->checkRead()) return ResponseHelper::returnUnauthorized();
$query = BarangayBudget::with('encodedBy')->orderByDesc('date');
if ($year = $request->input('year')) $query->byYear((int) $year);
if ($category = $request->input('category')) $query->where('category', $category);
$entries = $query->paginate((int) $request->input('per_page', 30));
return response()->json(['success' => true, 'data' => $entries]);
}
public function summary(Request $request)
{
if (!$this->checkRead()) return ResponseHelper::returnUnauthorized();
$year = (int) $request->input('year', date('Y'));
$income = BarangayBudget::byYear($year)->income()->sum('amount');
$expense = BarangayBudget::byYear($year)->expense()->sum('amount');
$bySource = BarangayBudget::byYear($year)
->selectRaw('category, source, SUM(amount) as total')
->groupBy('category', 'source')
->orderBy('category')
->orderByDesc('total')
->get();
return response()->json([
'success' => true,
'data' => [
'year' => $year,
'income' => $income,
'expense' => $expense,
'balance' => $income - $expense,
'by_source' => $bySource,
],
]);
}
public function store(Request $request)
{
if (!$this->checkWrite()) return ResponseHelper::returnUnauthorized();
$validator = Validator::make($request->all(), [
'fiscal_year' => 'required|integer|min:2020|max:2100',
'category' => 'required|in:INCOME,EXPENSE',
'source' => 'required|string|max:255',
'amount' => 'required|numeric|min:0.01',
'description' => 'nullable|string|max:500',
'date' => 'required|date',
'reference' => 'nullable|string|max:100',
]);
if ($validator->fails()) {
return response()->json(['success' => false, 'errors' => $validator->errors()], 422);
}
$data = $validator->validated();
$data['hashkey'] = hash('sha256', uniqid((string) now(), true));
$data['encoded_by'] = Auth::id();
$entry = BarangayBudget::create($data);
return response()->json(['success' => true, 'data' => $entry, 'message' => 'Budget entry recorded']);
}
public function update(Request $request)
{
if (!$this->checkWrite()) return ResponseHelper::returnUnauthorized();
$entry = BarangayBudget::where('hashkey', $request->input('target'))->first();
if (!$entry) return ResponseHelper::returnError('Entry not found', 404);
$data = $request->only(['source', 'amount', 'description', 'date', 'reference', 'category']);
$entry->update($data);
return response()->json(['success' => true, 'data' => $entry, 'message' => 'Budget entry updated']);
}
public function destroy(Request $request)
{
if (!$this->checkWrite()) return ResponseHelper::returnUnauthorized();
$entry = BarangayBudget::where('hashkey', $request->input('target'))->first();
if (!$entry) return ResponseHelper::returnError('Entry not found', 404);
$entry->delete();
return response()->json(['success' => true, 'message' => 'Budget entry deleted']);
}
public function fiscalYears()
{
$years = BarangayBudget::distinct()->orderByDesc('fiscal_year')->pluck('fiscal_year');
return response()->json(['success' => true, 'data' => $years]);
}
}

View File

@@ -0,0 +1,210 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Barangay;
use App\Enums\UserActions;
use App\Enums\Barangay\DocumentStatus;
use App\Enums\Barangay\PaymentStatus;
use App\Http\Controllers\Helpers\Permissions\UserPermissions;
use App\Http\Controllers\Helpers\ResponseHelper;
use App\Models\Barangay\DocumentRequest;
use App\Models\Barangay\RequestPayment;
use App\Models\Barangay\RequestType;
use Hypervel\Http\Request;
use Hypervel\Support\Facades\Auth;
use Hypervel\Support\Facades\Validator;
class DocumentRequestController
{
private function checkRead(): bool
{
return UserPermissions::isActionPermitted(Auth::user()->acct_type, UserActions::ViewDocumentRequests);
}
private function checkWrite(): bool
{
return UserPermissions::isActionPermitted(Auth::user()->acct_type, UserActions::ProcessDocumentRequest);
}
public function index(Request $request)
{
if (!$this->checkRead()) return ResponseHelper::returnUnauthorized();
$query = DocumentRequest::with(['requestType', 'resident', 'processedBy'])->orderByDesc('id');
if ($status = $request->input('status')) $query->where('status', $status);
if ($payStatus = $request->input('payment_status')) $query->where('payment_status', $payStatus);
if ($typeId = $request->input('request_type_id')) $query->where('request_type_id', $typeId);
if ($search = $request->input('search')) {
$query->where('request_no', 'like', "%{$search}%");
}
$requests = $query->paginate((int) $request->input('per_page', 20));
return response()->json(['success' => true, 'data' => $requests]);
}
public function myRequests(Request $request)
{
$requests = DocumentRequest::with('requestType')
->where('resident_user_id', Auth::id())
->orderByDesc('id')
->get();
return response()->json(['success' => true, 'data' => $requests]);
}
public function show(Request $request)
{
if (!$this->checkRead()) return ResponseHelper::returnUnauthorized();
$doc = DocumentRequest::with(['requestType', 'resident', 'processedBy', 'payments'])
->where('hashkey', $request->input('target'))
->first();
if (!$doc) return ResponseHelper::returnError('Request not found', 404);
return response()->json(['success' => true, 'data' => $doc]);
}
public function store(Request $request)
{
$validator = Validator::make($request->all(), [
'request_type_id' => 'required|integer|exists:barangay_request_types,id',
'purpose' => 'required|string|max:500',
]);
if ($validator->fails()) {
return response()->json(['success' => false, 'errors' => $validator->errors()], 422);
}
$reqType = RequestType::findOrFail($request->input('request_type_id'));
$doc = DocumentRequest::create([
'hashkey' => hash('sha256', uniqid((string) now(), true)),
'request_no' => DocumentRequest::generateRequestNo(),
'resident_user_id' => Auth::id(),
'request_type_id' => $reqType->id,
'purpose' => $request->input('purpose'),
'fee_amount' => $reqType->base_fee,
'payment_status' => PaymentStatus::PENDING,
'status' => $reqType->base_fee > 0 ? DocumentStatus::PENDING_PAYMENT : DocumentStatus::PAID,
'requested_by' => Auth::id(),
]);
return response()->json(['success' => true, 'data' => $doc, 'message' => 'Document request submitted']);
}
public function updateStatus(Request $request)
{
if (!$this->checkWrite()) return ResponseHelper::returnUnauthorized();
$doc = DocumentRequest::where('hashkey', $request->input('target'))->first();
if (!$doc) return ResponseHelper::returnError('Request not found', 404);
$newStatus = DocumentStatus::tryFrom($request->input('status'));
if (!$newStatus) return ResponseHelper::returnError('Invalid status', 422);
$data = [
'status' => $newStatus,
'processed_by' => Auth::id(),
];
if ($newStatus === DocumentStatus::CLAIMED) {
$data['claimed_at'] = now();
}
if ($request->input('notes')) $data['notes'] = $request->input('notes');
$doc->update($data);
return response()->json(['success' => true, 'data' => $doc, 'message' => 'Status updated to ' . $newStatus->label()]);
}
public function confirmPayment(Request $request)
{
if (!$this->checkWrite()) return ResponseHelper::returnUnauthorized();
$doc = DocumentRequest::where('hashkey', $request->input('target'))->first();
if (!$doc) return ResponseHelper::returnError('Request not found', 404);
$validator = Validator::make($request->all(), [
'method' => 'required|in:CASH,GCASH,QRPH,BANK,WAIVED',
'reference' => 'nullable|string|max:100',
'amount' => 'nullable|numeric|min:0',
]);
if ($validator->fails()) {
return response()->json(['success' => false, 'errors' => $validator->errors()], 422);
}
$method = $request->input('method');
$amount = $request->input('amount', $doc->fee_amount);
RequestPayment::create([
'request_id' => $doc->id,
'amount' => $amount,
'method' => $method,
'reference' => $request->input('reference'),
'paid_at' => now(),
'verified_by' => Auth::id(),
]);
$doc->update([
'payment_status' => PaymentStatus::PAID,
'payment_method' => $method,
'payment_ref' => $request->input('reference'),
'status' => DocumentStatus::PROCESSING,
'processed_by' => Auth::id(),
]);
return response()->json(['success' => true, 'data' => $doc, 'message' => 'Payment confirmed']);
}
public function markReady(Request $request)
{
if (!$this->checkWrite()) return ResponseHelper::returnUnauthorized();
$doc = DocumentRequest::where('hashkey', $request->input('target'))->first();
if (!$doc) return ResponseHelper::returnError('Request not found', 404);
$doc->update(['status' => DocumentStatus::READY, 'processed_by' => Auth::id()]);
return response()->json(['success' => true, 'data' => $doc, 'message' => 'Marked as ready for pickup']);
}
public function markClaimed(Request $request)
{
if (!$this->checkWrite()) return ResponseHelper::returnUnauthorized();
$doc = DocumentRequest::where('hashkey', $request->input('target'))->first();
if (!$doc) return ResponseHelper::returnError('Request not found', 404);
$doc->update([
'status' => DocumentStatus::CLAIMED,
'claimed_at' => now(),
]);
return response()->json(['success' => true, 'data' => $doc, 'message' => 'Document marked as claimed']);
}
public function cancel(Request $request)
{
$doc = DocumentRequest::where('hashkey', $request->input('target'))->first();
if (!$doc) return ResponseHelper::returnError('Request not found', 404);
if ($doc->resident_user_id !== Auth::id() && !$this->checkWrite()) {
return ResponseHelper::returnUnauthorized();
}
if (in_array($doc->status, [DocumentStatus::CLAIMED, DocumentStatus::CANCELLED])) {
return ResponseHelper::returnError('Cannot cancel this request', 422);
}
$doc->update(['status' => DocumentStatus::CANCELLED]);
return response()->json(['success' => true, 'message' => 'Request cancelled']);
}
}

View File

@@ -0,0 +1,164 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Barangay;
use App\Enums\UserActions;
use App\Http\Controllers\Helpers\Permissions\UserPermissions;
use App\Http\Controllers\Helpers\ResponseHelper;
use App\Models\Barangay\Household;
use App\Models\Barangay\HouseholdMember;
use App\Models\Barangay\Resident;
use Hypervel\Http\Request;
use Hypervel\Support\Facades\Auth;
use Hypervel\Support\Facades\Validator;
class HouseholdController
{
private function checkRead(): bool
{
return UserPermissions::isActionPermitted(Auth::user()->acct_type, UserActions::ViewResidents);
}
private function checkWrite(): bool
{
return UserPermissions::isActionPermitted(Auth::user()->acct_type, UserActions::ManageResidents);
}
public function index(Request $request)
{
if (!$this->checkRead()) return ResponseHelper::returnUnauthorized();
$query = Household::with(['head', 'members.resident'])->orderByDesc('id');
if ($purok = $request->input('purok')) $query->where('purok', $purok);
if ($request->input('active_only')) $query->active();
$households = $query->paginate((int) $request->input('per_page', 20));
return response()->json(['success' => true, 'data' => $households]);
}
public function show(Request $request)
{
if (!$this->checkRead()) return ResponseHelper::returnUnauthorized();
$household = Household::with(['head', 'members.resident'])
->where('hashkey', $request->input('target'))
->first();
if (!$household) return ResponseHelper::returnError('Household not found', 404);
return response()->json(['success' => true, 'data' => $household]);
}
public function store(Request $request)
{
if (!$this->checkWrite()) return ResponseHelper::returnUnauthorized();
$validator = Validator::make($request->all(), [
'head_resident_id' => 'required|integer|exists:barangay_residents,id',
'address' => 'required|string|max:500',
'purok' => 'nullable|string|max:100',
'barangay' => 'nullable|string|max:100',
'city' => 'nullable|string|max:100',
'province' => 'nullable|string|max:100',
'ownership_type' => 'nullable|in:OWNED,RENTED,SHARED',
'monthly_rental' => 'nullable|numeric|min:0',
'has_electricity' => 'nullable|boolean',
'has_water' => 'nullable|boolean',
'housing_material' => 'nullable|string|max:100',
]);
if ($validator->fails()) {
return response()->json(['success' => false, 'errors' => $validator->errors()], 422);
}
$data = $validator->validated();
$year = date('Y');
$count = Household::whereYear('created_at', $year)->count() + 1;
$data['household_no'] = sprintf('HH-%s-%04d', $year, $count);
$data['hashkey'] = hash('sha256', uniqid((string) now(), true));
$data['member_count'] = 1;
$data['is_active'] = true;
$data['created_by'] = Auth::id();
$data['updated_by'] = Auth::id();
$household = Household::create($data);
// Add head as first member
HouseholdMember::create([
'household_id' => $household->id,
'resident_id' => $data['head_resident_id'],
'relationship_to_head' => 'HEAD',
'is_active' => true,
]);
return response()->json(['success' => true, 'data' => $household, 'message' => 'Household created']);
}
public function addMember(Request $request)
{
if (!$this->checkWrite()) return ResponseHelper::returnUnauthorized();
$validator = Validator::make($request->all(), [
'target' => 'required|string',
'resident_id' => 'required|integer|exists:barangay_residents,id',
'relationship_to_head' => 'required|string|max:100',
]);
if ($validator->fails()) {
return response()->json(['success' => false, 'errors' => $validator->errors()], 422);
}
$household = Household::where('hashkey', $request->input('target'))->first();
if (!$household) return ResponseHelper::returnError('Household not found', 404);
$existing = HouseholdMember::where('household_id', $household->id)
->where('resident_id', $request->input('resident_id'))
->first();
if ($existing) return ResponseHelper::returnError('Resident is already a member', 422);
$member = HouseholdMember::create([
'household_id' => $household->id,
'resident_id' => $request->input('resident_id'),
'relationship_to_head' => $request->input('relationship_to_head'),
'is_active' => true,
]);
$household->increment('member_count');
return response()->json(['success' => true, 'data' => $member, 'message' => 'Member added']);
}
public function removeMember(Request $request)
{
if (!$this->checkWrite()) return ResponseHelper::returnUnauthorized();
$household = Household::where('hashkey', $request->input('target'))->first();
if (!$household) return ResponseHelper::returnError('Household not found', 404);
$deleted = HouseholdMember::where('household_id', $household->id)
->where('resident_id', $request->input('resident_id'))
->delete();
if ($deleted) $household->decrement('member_count');
return response()->json(['success' => true, 'message' => 'Member removed']);
}
public function update(Request $request)
{
if (!$this->checkWrite()) return ResponseHelper::returnUnauthorized();
$household = Household::where('hashkey', $request->input('target'))->first();
if (!$household) return ResponseHelper::returnError('Household not found', 404);
$data = $request->except(['target', 'hashkey', 'household_no', 'created_by']);
$data['updated_by'] = Auth::id();
$household->update($data);
return response()->json(['success' => true, 'data' => $household, 'message' => 'Household updated']);
}
}

View File

@@ -0,0 +1,135 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Barangay;
use App\Enums\UserActions;
use App\Http\Controllers\Helpers\Permissions\UserPermissions;
use App\Http\Controllers\Helpers\ResponseHelper;
use App\Models\Barangay\BarangayProject;
use Hypervel\Http\Request;
use Hypervel\Support\Facades\Auth;
use Hypervel\Support\Facades\Validator;
class ProjectController
{
private function checkRead(): bool
{
return UserPermissions::isActionPermitted(Auth::user()->acct_type, UserActions::ViewBarangayProjects);
}
private function checkWrite(): bool
{
return UserPermissions::isActionPermitted(Auth::user()->acct_type, UserActions::ManageBarangayProjects);
}
public function index(Request $request)
{
if (!$this->checkRead()) return ResponseHelper::returnUnauthorized();
$query = BarangayProject::orderByDesc('id');
if ($status = $request->input('status')) $query->where('status', $status);
if ($type = $request->input('type')) $query->where('type', $type);
if ($year = $request->input('year')) {
$query->whereYear('start_date', $year);
}
$projects = $query->paginate((int) $request->input('per_page', 20));
return response()->json(['success' => true, 'data' => $projects]);
}
public function show(Request $request)
{
if (!$this->checkRead()) return ResponseHelper::returnUnauthorized();
$project = BarangayProject::where('hashkey', $request->input('target'))->first();
if (!$project) return ResponseHelper::returnError('Project not found', 404);
return response()->json(['success' => true, 'data' => $project]);
}
public function store(Request $request)
{
if (!$this->checkWrite()) return ResponseHelper::returnUnauthorized();
$validator = Validator::make($request->all(), [
'project_name' => 'required|string|max:500',
'description' => 'nullable|string',
'type' => 'required|in:INFRASTRUCTURE,LIVELIHOOD,HEALTH,EDUCATION,ENVIRONMENT,OTHERS',
'budget' => 'required|numeric|min:0',
'fund_source' => 'required|in:GENERAL_FUND,SK,PROVINCE,NATIONAL,OTHERS',
'start_date' => 'required|date',
'end_date' => 'nullable|date|after:start_date',
'implementing_office' => 'nullable|string|max:255',
'contractor' => 'nullable|string|max:255',
'location' => 'nullable|string|max:500',
'beneficiaries_count' => 'nullable|integer|min:0',
]);
if ($validator->fails()) {
return response()->json(['success' => false, 'errors' => $validator->errors()], 422);
}
$data = $validator->validated();
$data['hashkey'] = hash('sha256', uniqid((string) now(), true));
$data['status'] = 'PLANNED';
$data['created_by'] = Auth::id();
$data['updated_by'] = Auth::id();
$project = BarangayProject::create($data);
return response()->json(['success' => true, 'data' => $project, 'message' => 'Project created']);
}
public function update(Request $request)
{
if (!$this->checkWrite()) return ResponseHelper::returnUnauthorized();
$project = BarangayProject::where('hashkey', $request->input('target'))->first();
if (!$project) return ResponseHelper::returnError('Project not found', 404);
$data = $request->except(['target', 'hashkey', 'created_by']);
$data['updated_by'] = Auth::id();
$project->update($data);
return response()->json(['success' => true, 'data' => $project, 'message' => 'Project updated']);
}
public function updateStatus(Request $request)
{
if (!$this->checkWrite()) return ResponseHelper::returnUnauthorized();
$project = BarangayProject::where('hashkey', $request->input('target'))->first();
if (!$project) return ResponseHelper::returnError('Project not found', 404);
$validStatuses = ['PLANNED', 'ONGOING', 'COMPLETED', 'SUSPENDED', 'CANCELLED'];
$status = $request->input('status');
if (!in_array($status, $validStatuses)) {
return ResponseHelper::returnError('Invalid status', 422);
}
$project->update(['status' => $status, 'updated_by' => Auth::id()]);
return response()->json(['success' => true, 'data' => $project, 'message' => "Project status set to {$status}"]);
}
public function summary()
{
if (!$this->checkRead()) return ResponseHelper::returnUnauthorized();
$summary = [
'total' => BarangayProject::count(),
'planned' => BarangayProject::where('status', 'PLANNED')->count(),
'ongoing' => BarangayProject::where('status', 'ONGOING')->count(),
'completed' => BarangayProject::where('status', 'COMPLETED')->count(),
'total_budget' => BarangayProject::sum('budget'),
'by_type' => BarangayProject::selectRaw('type, count(*) as count, sum(budget) as budget')
->groupBy('type')->get(),
];
return response()->json(['success' => true, 'data' => $summary]);
}
}

View File

@@ -0,0 +1,95 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Barangay;
use App\Enums\UserActions;
use App\Http\Controllers\Helpers\Permissions\UserPermissions;
use App\Http\Controllers\Helpers\ResponseHelper;
use App\Models\Barangay\RequestType;
use Hypervel\Http\Request;
use Hypervel\Support\Facades\Auth;
use Hypervel\Support\Facades\Validator;
class RequestTypeController
{
private function checkWrite(): bool
{
return UserPermissions::isActionPermitted(Auth::user()->acct_type, UserActions::ManageRequestTypes);
}
public function index()
{
$types = RequestType::orderBy('name')->get();
return response()->json(['success' => true, 'data' => $types]);
}
public function active()
{
$types = RequestType::active()->orderBy('name')->get();
return response()->json(['success' => true, 'data' => $types]);
}
public function store(Request $request)
{
if (!$this->checkWrite()) return ResponseHelper::returnUnauthorized();
$validator = Validator::make($request->all(), [
'name' => 'required|string|max:255',
'code' => 'required|string|max:50|unique:barangay_request_types,code',
'description' => 'nullable|string',
'base_fee' => 'required|numeric|min:0',
'processing_days' => 'required|integer|min:1',
'requires_clearance' => 'nullable|boolean',
]);
if ($validator->fails()) {
return response()->json(['success' => false, 'errors' => $validator->errors()], 422);
}
$data = $validator->validated();
$data['is_active'] = true;
$data['code'] = strtoupper($data['code']);
$type = RequestType::create($data);
return response()->json(['success' => true, 'data' => $type, 'message' => 'Request type created']);
}
public function update(Request $request)
{
if (!$this->checkWrite()) return ResponseHelper::returnUnauthorized();
$type = RequestType::where('code', $request->input('target'))->first();
if (!$type) return ResponseHelper::returnError('Request type not found', 404);
$validator = Validator::make($request->all(), [
'name' => 'nullable|string|max:255',
'description' => 'nullable|string',
'base_fee' => 'nullable|numeric|min:0',
'processing_days' => 'nullable|integer|min:1',
'requires_clearance' => 'nullable|boolean',
]);
if ($validator->fails()) {
return response()->json(['success' => false, 'errors' => $validator->errors()], 422);
}
$type->update($validator->validated());
return response()->json(['success' => true, 'data' => $type, 'message' => 'Request type updated']);
}
public function toggleActive(Request $request)
{
if (!$this->checkWrite()) return ResponseHelper::returnUnauthorized();
$type = RequestType::where('code', $request->input('target'))->first();
if (!$type) return ResponseHelper::returnError('Request type not found', 404);
$type->update(['is_active' => !$type->is_active]);
return response()->json(['success' => true, 'data' => $type]);
}
}

View File

@@ -0,0 +1,159 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Barangay;
use App\Enums\UserActions;
use App\Http\Controllers\Helpers\Permissions\UserPermissions;
use App\Http\Controllers\Helpers\ResponseHelper;
use App\Models\Barangay\Resident;
use App\Models\User;
use Hypervel\Http\Request;
use Hypervel\Support\Facades\Auth;
use Hypervel\Support\Facades\Validator;
class ResidentController
{
private function checkRead(): bool
{
return UserPermissions::isActionPermitted(Auth::user()->acct_type, UserActions::ViewResidents);
}
private function checkWrite(): bool
{
return UserPermissions::isActionPermitted(Auth::user()->acct_type, UserActions::ManageResidents);
}
public function index(Request $request)
{
if (!$this->checkRead()) return ResponseHelper::returnUnauthorized();
$query = Resident::with('user')->orderByDesc('id');
if ($search = $request->input('search')) {
$query->where(function ($q) use ($search) {
$q->where('firstname', 'like', "%{$search}%")
->orWhere('lastname', 'like', "%{$search}%")
->orWhere('middlename', 'like', "%{$search}%");
});
}
if ($purok = $request->input('purok')) $query->where('purok', $purok);
if ($request->input('active_only')) $query->active();
$residents = $query->paginate((int) $request->input('per_page', 20));
return response()->json(['success' => true, 'data' => $residents]);
}
public function show(Request $request)
{
if (!$this->checkRead()) return ResponseHelper::returnUnauthorized();
$resident = Resident::with(['user', 'householdMemberships.household'])
->where('hashkey', $request->input('target'))
->first();
if (!$resident) return ResponseHelper::returnError('Resident not found', 404);
return response()->json(['success' => true, 'data' => $resident]);
}
public function store(Request $request)
{
if (!$this->checkWrite()) return ResponseHelper::returnUnauthorized();
$validator = Validator::make($request->all(), [
'firstname' => 'required|string|max:100',
'lastname' => 'required|string|max:100',
'middlename' => 'nullable|string|max:100',
'suffix' => 'nullable|string|max:20',
'dob' => 'required|date',
'birthplace' => 'nullable|string|max:255',
'gender' => 'required|in:MALE,FEMALE,OTHER',
'civil_status' => 'required|in:SINGLE,MARRIED,WIDOWED,SEPARATED,ANNULLED',
'citizenship' => 'nullable|string|max:100',
'religion' => 'nullable|string|max:100',
'occupation' => 'nullable|string|max:255',
'monthly_income' => 'nullable|numeric|min:0',
'blood_type' => 'nullable|string|max:5',
'voter_status' => 'nullable|boolean',
'head_of_household' => 'nullable|boolean',
'purok' => 'nullable|string|max:100',
'street' => 'nullable|string|max:255',
'barangay' => 'nullable|string|max:100',
'city' => 'nullable|string|max:100',
'province' => 'nullable|string|max:100',
'region' => 'nullable|string|max:100',
'philhealth_id' => 'nullable|string|max:50',
'sss_id' => 'nullable|string|max:50',
'gsis_id' => 'nullable|string|max:50',
'tin' => 'nullable|string|max:50',
'emergency_contact_name' => 'nullable|string|max:255',
'emergency_contact_phone' => 'nullable|string|max:30',
'emergency_contact_address' => 'nullable|string|max:255',
'user_id' => 'nullable|integer|exists:users,id',
]);
if ($validator->fails()) {
return response()->json(['success' => false, 'errors' => $validator->errors()], 422);
}
$data = $validator->validated();
$data['hashkey'] = hash('sha256', uniqid((string) now(), true));
$data['is_active'] = true;
$data['created_by'] = Auth::id();
$data['updated_by'] = Auth::id();
$resident = Resident::create($data);
return response()->json(['success' => true, 'data' => $resident, 'message' => 'Resident created']);
}
public function update(Request $request)
{
if (!$this->checkWrite()) return ResponseHelper::returnUnauthorized();
$resident = Resident::where('hashkey', $request->input('target'))->first();
if (!$resident) return ResponseHelper::returnError('Resident not found', 404);
$data = $request->except(['target', 'hashkey', 'created_by']);
$data['updated_by'] = Auth::id();
$resident->update($data);
return response()->json(['success' => true, 'data' => $resident, 'message' => 'Resident updated']);
}
public function setActive(Request $request)
{
if (!$this->checkWrite()) return ResponseHelper::returnUnauthorized();
$resident = Resident::where('hashkey', $request->input('target'))->first();
if (!$resident) return ResponseHelper::returnError('Resident not found', 404);
$resident->update(['is_active' => (bool) $request->input('active', true)]);
return response()->json(['success' => true, 'data' => $resident]);
}
public function search(Request $request)
{
if (!$this->checkRead()) return ResponseHelper::returnUnauthorized();
$term = $request->input('q', '');
$residents = Resident::where(function ($q) use ($term) {
$q->where('firstname', 'like', "%{$term}%")
->orWhere('lastname', 'like', "%{$term}%")
->orWhere('middlename', 'like', "%{$term}%");
})->active()->limit(20)->get(['id', 'hashkey', 'firstname', 'middlename', 'lastname', 'suffix', 'purok', 'dob']);
return response()->json(['success' => true, 'data' => $residents]);
}
public function puroks()
{
$puroks = Resident::distinct()->orderBy('purok')->pluck('purok')->filter()->values();
return response()->json(['success' => true, 'data' => $puroks]);
}
}

View File

@@ -0,0 +1,241 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers;
use App\Http\Controllers\Helpers\ResponseHelper;
use App\Http\Controllers\Helpers\Permissions\UserPermissions;
use App\Enums\UserActions;
use App\Models\Chapter;
use App\Models\ChapterMember;
use App\Models\User;
use App\Models\SystemSetting;
use App\Support\IslandGroupHelper;
use Hypervel\Http\Request;
use Hypervel\Support\Facades\Auth;
use Hypervel\Support\Facades\Validator;
use Hypervel\Support\Str;
class ChapterController
{
private const CHILD_LEVELS = [
'national' => ['region'],
'region' => ['province'],
'province' => ['city', 'municipal'],
'city' => ['barangay'],
'municipal' => ['barangay'],
'barangay' => ['purok'],
'purok' => [],
];
private function isAdmin(): bool
{
return UserPermissions::isActionPermitted(Auth::user()->acct_type, UserActions::ManageChapterMembers);
}
public function hierarchy(Request $request)
{
$chapterId = $request->input('chapter_id');
if (!$chapterId) {
$national = Chapter::where('level', 'national')->where('location_key', 'philippines')->first();
if (!$national) {
return response()->json(['chapters' => [], 'current' => null, 'breadcrumb' => []]);
}
$chapters = $national->children()
->where('is_active', true)
->withCount('activeMembers')
->with('leaders.user')
->get()
->map(fn ($c) => $this->formatChapter($c))
->values();
return response()->json(['chapters' => $chapters, 'current' => null, 'breadcrumb' => []]);
}
$current = Chapter::withCount('activeMembers')->with('leaders.user')->find($chapterId);
if (!$current) return ResponseHelper::returnError('Chapter not found', 404);
$chapters = $current->children()
->where('is_active', true)
->withCount('activeMembers')
->with('leaders.user')
->get()
->map(fn ($c) => $this->formatChapter($c))
->values();
return response()->json([
'chapters' => $chapters,
'current' => $this->formatChapter($current),
'breadcrumb' => $this->buildBreadcrumb($current),
]);
}
public function mapData(Request $request)
{
$level = $request->input('level', 'region');
$parentId = $request->input('parent_id');
$query = Chapter::where('level', $level)->where('is_active', true)->withCount('activeMembers');
if ($parentId) $query->where('parent_id', $parentId);
$chapters = $query->get()
->filter(fn ($c) => $c->lat && $c->lng)
->map(fn ($c) => [
'id' => $c->id,
'name' => $c->name,
'level' => $c->level,
'lat' => $c->lat,
'lng' => $c->lng,
'count' => $c->active_members_count,
])
->values();
return response()->json(['chapters' => $chapters]);
}
public function members(Request $request)
{
$chapterId = $request->input('chapter_id');
if (!$chapterId) return ResponseHelper::returnError('chapter_id required', 400);
$members = ChapterMember::with('user')
->where('chapter_id', $chapterId)
->where('is_active', true)
->get()
->map(fn ($cm) => [
'hashkey' => $cm->hashkey,
'name' => $cm->user?->fullname ?? $cm->user?->name,
'position' => $cm->position,
'photo' => $cm->user?->photourl ?? null,
'is_manual' => (bool) $cm->is_manual_override,
]);
return response()->json(['members' => $members]);
}
public function assignMember(Request $request)
{
if (!$this->isAdmin()) return ResponseHelper::returnUnauthorized();
$validator = Validator::make($request->all(), [
'user_hashkey' => 'required|string',
'chapter_id' => 'required|integer',
'position' => 'nullable|string|max:100',
]);
if ($validator->fails()) return ResponseHelper::returnError('Validation failed', 422, $validator->errors());
$user = User::where('hashkey', $request->input('user_hashkey'))->first();
if (!$user) return ResponseHelper::returnError('User not found', 404);
$chapter = Chapter::find($request->input('chapter_id'));
if (!$chapter) return ResponseHelper::returnError('Chapter not found', 404);
$existing = ChapterMember::where('user_id', $user->id)->where('chapter_id', $chapter->id)->first();
if ($existing) {
$existing->update([
'position' => $request->input('position'),
'is_manual_override' => true,
'assigned_by' => Auth::id(),
'assigned_at' => now(),
'updated_by' => Auth::id(),
]);
} else {
ChapterMember::create([
'hashkey' => (string) Str::uuid(),
'user_id' => $user->id,
'chapter_id' => $chapter->id,
'position' => $request->input('position'),
'is_manual_override' => true,
'is_active' => true,
'assigned_by' => Auth::id(),
'assigned_at' => now(),
'created_by' => Auth::id(),
]);
}
return response()->json(['success' => true]);
}
public function removeMember(Request $request)
{
if (!$this->isAdmin()) return ResponseHelper::returnUnauthorized();
$member = ChapterMember::where('hashkey', $request->input('hashkey'))->first();
if (!$member) return ResponseHelper::returnError('Member assignment not found', 404);
$member->update(['is_active' => false, 'updated_by' => Auth::id()]);
return response()->json(['success' => true]);
}
public function positions()
{
$raw = SystemSetting::getValue('chapter_positions');
$positions = is_string($raw) ? (json_decode($raw, true) ?? []) : [];
if (empty($positions)) {
$positions = ['Punong Barangay', 'Kagawad', 'Secretary', 'Treasurer', 'SK Chairperson', 'SK Councilor', 'Tanod', 'BHW', 'Daycare Worker', 'Staff', 'Member'];
}
return response()->json(['positions' => $positions]);
}
public function createChapter(Request $request)
{
if (!$this->isAdmin()) return ResponseHelper::returnUnauthorized();
$validator = Validator::make($request->all(), [
'name' => 'required|string|max:255',
'level' => 'required|in:national,region,province,city,municipal,barangay,purok',
'parent_id' => 'nullable|integer|exists:chapters,id',
'location_key' => 'nullable|string|max:255',
'lat' => 'nullable|numeric',
'lng' => 'nullable|numeric',
]);
if ($validator->fails()) {
return response()->json(['success' => false, 'errors' => $validator->errors()], 422);
}
$data = $validator->validated();
$data['hashkey'] = hash('sha256', uniqid((string) now(), true));
$data['is_active'] = true;
$data['created_by'] = Auth::id();
$data['updated_by'] = Auth::id();
$data['location_key'] = strtolower(trim($data['location_key'] ?? $data['name']));
$chapter = Chapter::create($data);
return response()->json(['success' => true, 'data' => $this->formatChapter($chapter), 'message' => 'Chapter created']);
}
private function formatChapter(Chapter $c): array
{
return [
'id' => $c->id,
'hashkey' => $c->hashkey,
'name' => $c->name,
'level' => $c->level,
'parent_id' => $c->parent_id,
'location_key' => $c->location_key,
'lat' => $c->lat,
'lng' => $c->lng,
'is_active' => $c->is_active,
'member_count' => $c->active_members_count ?? 0,
'child_levels' => self::CHILD_LEVELS[$c->level] ?? [],
];
}
private function buildBreadcrumb(Chapter $chapter): array
{
$crumbs = [];
$current = $chapter;
while ($current) {
array_unshift($crumbs, ['id' => $current->id, 'name' => $current->name, 'level' => $current->level]);
$current = $current->parent;
}
return $crumbs;
}
}

View File

@@ -0,0 +1,223 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers;
use Hypervel\Http\Request;
use App\Models\FileList;
use App\Models\FileContent;
use Hypervel\Http\UploadedFile;
use Hypervel\Support\Facades\File;
use Hypervel\Support\Facades\Storage;
use Hypervel\Support\Facades\Auth;
use Hypervel\Support\Facades\DB;
use \PDO;
use Hypervel\Support\Facades\Response;
use Hypervel\Support\Carbon;
use App\Http\Controllers\Helpers\Permissions\UserPermissions;
use App\Enums\UserActions;
class FileController
{
private static function isLikelyBinary($string, $threshold = 0.3): bool
{
if (strpos($string, "\x00") !== false) return true;
$len = strlen($string);
if ($len === 0) return false;
$nonPrintable = preg_match_all('~[^\x09\x0A\x0D\x20-\x7E]~', $string);
return ($nonPrintable / $len) > $threshold;
}
private static function insertFileContentPostgresPDO(array $data)
{
$pdo = DB::getPdo();
$stmt = $pdo->prepare('
INSERT INTO file_content
(hashkey, filehash, titlename, description, size_in_bytes, content, filelocation, created_by, updated_by, details, created_at, updated_at)
VALUES
(:hashkey, :filehash, :titlename, :description, :size_in_bytes, :content, :filelocation, :created_by, :updated_by, :details, :created_at, :updated_at)
');
$now = now()->toDateTimeString();
$stmt->bindParam(':hashkey', $data['hashkey']);
$stmt->bindParam(':filehash', $data['filehash']);
$stmt->bindParam(':titlename', $data['titlename']);
$stmt->bindParam(':description', $data['description']);
$stmt->bindParam(':size_in_bytes', $data['size_in_bytes']);
$stmt->bindParam(':content', $data['content'], PDO::PARAM_LOB);
$stmt->bindParam(':filelocation', $data['filelocation']);
$stmt->bindParam(':created_by', $data['created_by']);
$stmt->bindParam(':updated_by', $data['updated_by']);
$detailsJson = json_encode($data['details']);
$stmt->bindParam(':details', $detailsJson);
$stmt->bindParam(':created_at', $now);
$stmt->bindParam(':updated_at', $now);
$stmt->execute();
$id = DB::getPdo()->lastInsertId();
return FileContent::find($id);
}
private static function insertFileContentSql(array $data)
{
$now = Carbon::now();
$data['content'] = base64_encode($data['content']);
$data['details'] = json_encode($data['details']);
$data['created_at'] = $now;
$data['updated_at'] = $now;
return FileContent::create($data);
}
private static function insertFileContent(array $data)
{
$driver = DB::connection()->getDriverName();
if ($driver === 'pgsql') return self::insertFileContentPostgresPDO($data);
if ($driver === 'mysql') return self::insertFileContentSql($data);
throw new \RuntimeException("Unsupported database driver: {$driver}");
}
private static function uploadFileContent(UploadedFile|string $fileData, string $title, ?string $description = null, ?array $details = [])
{
if ($fileData instanceof UploadedFile) {
$fileHash = hash_file('sha256', $fileData->getRealPath());
$fileSize = $fileData->getSize();
$fileContent = File::get($fileData->getRealPath());
$path = $fileData->storeAs('files', $fileHash);
} elseif (is_string($fileData) && file_exists($fileData)) {
$fileHash = hash_file('sha256', $fileData);
$fileSize = filesize($fileData);
$fileContent = file_get_contents($fileData);
$path = Storage::put("files/{$fileHash}", $fileContent);
} elseif (self::isLikelyBinary($fileData)) {
$fileHash = hash('sha256', $fileData);
$fileSize = strlen($fileData);
$fileContent = $fileData;
$path = Storage::put("files/{$fileHash}", $fileContent);
} else {
throw new \InvalidArgumentException('Invalid file data provided.');
}
$existing = FileContent::where('filehash', $fileHash)->first();
if ($existing) return $existing;
$hashKey = hash('sha256', uniqid((string) now(), true));
$finfo = new \finfo(FILEINFO_MIME_TYPE);
$mimetype = $finfo->buffer($fileContent);
return self::insertFileContent([
'hashkey' => $hashKey,
'filehash' => $fileHash,
'titlename' => $title,
'description' => $description,
'size_in_bytes' => $fileSize,
'content' => $fileContent,
'filelocation' => $path,
'created_by' => Auth::id(),
'updated_by' => Auth::id(),
'details' => $details ?? [],
'mimetype' => $mimetype,
]);
}
private static function insertFileList(int $contentuid, string $title, string $filename, string $description, $categories, $details, $tags = [], $hidden = 0, ?string $file_type = null)
{
if (!FileContent::where('id', $contentuid)->exists()) {
throw new \Exception("File Content does not exist");
}
return FileList::create([
'contentuid' => $contentuid,
'hashkey' => hash('sha256', uniqid((string) now(), true)),
'title' => $title,
'filename' => $filename,
'description' => $description,
'categories' => $categories,
'details' => $details,
'tags' => $tags,
'hidden' => $hidden,
'file_type' => $file_type,
'is_public' => false,
'created_by' => Auth::id(),
'updated_by' => Auth::id(),
]);
}
public static function uploadFileList(string|UploadedFile $fileData, string $title, string $filename, ?string $description = null, ?array $details = [], $categories = null, $tags = [], $hidden = 0, ?string $file_type = null)
{
try {
$fileContent = self::uploadFileContent($fileData, $title, $description, $details);
return self::insertFileList($fileContent->id, $title, $filename, $description, $categories, $details, $tags, $hidden, $file_type);
} catch (\Throwable $th) {
return Response::json($th->getMessage(), 500);
}
}
public static function viewFilebyFileListHash(string $filelist_hash)
{
$filelist = FileList::where('hashkey', $filelist_hash)->first();
if (!$filelist) abort(404, 'File not found');
$cdnUrl = trim((string) ($filelist->cdn_url ?? ''));
if ($cdnUrl !== '') return redirect($cdnUrl);
$fileContent = $filelist->fileContent;
if (!$fileContent) abort(404, 'File content not found');
$content = $fileContent->content;
$driver = DB::connection()->getDriverName();
if (is_resource($content) && $driver !== 'mysql') $content = stream_get_contents($content);
if ($driver === 'mysql') $content = base64_decode($content);
$finfo = new \finfo(FILEINFO_MIME_TYPE);
$mimeType = $finfo->buffer($content);
$mimeMap = [
'image/jpeg' => 'jpg', 'image/png' => 'png', 'image/gif' => 'gif',
'image/webp' => 'webp', 'application/pdf' => 'pdf', 'text/plain' => 'txt',
'application/zip' => 'zip', 'application/json' => 'json',
];
$extension = $mimeMap[$mimeType] ?? pathinfo($filelist->filename, PATHINFO_EXTENSION);
$filename = pathinfo($filelist->filename, PATHINFO_FILENAME) . '.' . $extension;
return Response::make($content, 200, [
'Content-Type' => $mimeType,
'Content-Disposition' => 'inline; filename="' . $filename . '"',
'Content-Length' => strlen($content),
]);
}
public static function generateURLforFileListHash(string $filelist_hashkey): string
{
$cdnUrl = FileList::where('hashkey', $filelist_hashkey)->value('cdn_url');
if (is_string($cdnUrl) && trim($cdnUrl) !== '') return $cdnUrl;
return "/RequestData/File/$filelist_hashkey";
}
public static function UploadFilefromRequest(Request $request, string $category)
{
if (!Auth::check() || !UserPermissions::isActionPermitted(Auth::user()->acct_type, UserActions::UploadAllFiles)) {
return response()->json(['error' => 'Unauthorized'], 403);
}
if (!$request->hasFile('file')) {
return response()->json(false, 400);
}
$file = $request->file('file');
$filename = $file->getClientFilename();
$result = self::uploadFileList($file, '', $filename ?? '', '', [], $category, [], 0, null);
try {
if (is_string($result->hashkey) && !empty($result->hashkey)) {
return response()->json([
'success' => true,
'hashkey' => $result->hashkey,
'message' => 'File uploaded successfully',
'url' => $result->resolvedUrl(),
'name' => $filename,
], 200);
}
throw new \Exception("File upload failed");
} catch (\Throwable $th) {
return response()->json(['success' => false, 'error' => 'File upload failed'], 500);
}
}
}

View File

@@ -1,412 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers;
use Hypervel\Http\Request;
use App\Models\FileList;
use App\Models\FileContent;
use Hypervel\Http\UploadedFile;
use Hypervel\Support\Facades\File;
use Hypervel\Support\Facades\Storage;
use Hypervel\Support\Facades\Auth;
use Hypervel\Support\Facades\DB;
use \PDO;
use Hypervel\Support\Facades\Response;
use Hypervel\Support\Carbon;
use App\Http\Controllers\Helpers\Permissions\UserPermissions;
use App\Http\Controllers\Helpers\Permissions\ProductPermissions;
use App\Enums\UserActions;
class FilesMainController
{
private static function isLikelyBinary($string, $threshold = 0.3)
{
// Null byte check: very likely binary
if (strpos($string, "\x00") !== false) {
return true;
}
// If string is empty, consider it not binary
$len = strlen($string);
if ($len === 0)
return false;
// Count non-printable characters
$nonPrintable = preg_match_all('~[^\x09\x0A\x0D\x20-\x7E]~', $string);
// If more than $threshold of the content is non-printable, likely binary
return ($nonPrintable / $len) > $threshold;
}
private static function insertFileContentPostgresPDO(array $data)
{
$pdo = DB::getPdo();
$stmt = $pdo->prepare('
INSERT INTO file_content
(hashkey, filehash, titlename, description, size_in_bytes, content, filelocation, created_by, updated_by, details, created_at, updated_at)
VALUES
(:hashkey, :filehash, :titlename, :description, :size_in_bytes, :content, :filelocation, :created_by, :updated_by, :details, :created_at, :updated_at)
');
$now = now()->toDateTimeString();
$stmt->bindParam(':hashkey', $data['hashkey']);
$stmt->bindParam(':filehash', $data['filehash']);
$stmt->bindParam(':titlename', $data['titlename']);
$stmt->bindParam(':description', $data['description']);
$stmt->bindParam(':size_in_bytes', $data['size_in_bytes']);
$stmt->bindParam(':content', $data['content'], PDO::PARAM_LOB);
$stmt->bindParam(':filelocation', $data['filelocation']);
$stmt->bindParam(':created_by', $data['created_by']);
$stmt->bindParam(':updated_by', $data['updated_by']);
$detailsJson = json_encode($data['details']);
$stmt->bindParam(':details', $detailsJson);
$stmt->bindParam(':created_at', $now);
$stmt->bindParam(':updated_at', $now);
$stmt->execute();
// Optionally, get the inserted ID and fetch the model
$id = DB::getPdo()->lastInsertId();
return FileContent::find($id);
}
private static function insertFileContentSql(array $data)
{
$now = Carbon::now();
$data['content'] = base64_encode($data['content']);
$data['details'] = json_encode($data['details']);
$data['created_at'] = $now;
$data['updated_at'] = $now;
return FileContent::create($data);
}
private static function insertFileContent(array $data)
{
$driver = DB::connection()->getDriverName();
if ($driver === 'pgsql') {
return self::insertFileContentPostgresPDO($data);
}
if ($driver === 'mysql') {
return self::insertFileContentSql($data);
}
throw new \RuntimeException("Unsupported database driver: {$driver}");
}
/**
* Upload file content and store metadata in DB.
*
* @param \Hypervel\Http\UploadedFile|string $fileData // filepath, filebinaryData, UploadedFileRequest
* @param string $title
* @param string|null $description
* @param string|null $filelocation
* @param array|null $details
*/
private static function uploadFileContent(UploadedFile|string $fileData, string $title, ?string $description = null, ?array $details = [])
{
if ($fileData instanceof UploadedFile) {
$fileHash = hash_file('sha256', $fileData->getRealPath());
$fileSize = $fileData->getSize();
$filename = $fileHash;
$fileContent = File::get($fileData->getRealPath());
$path = $fileData->storeAs('files', $filename);
} elseif (is_string($fileData) && file_exists($fileData)) {
$fileHash = hash_file('sha256', $fileData);
$fileSize = filesize($fileData);
$fileContent = file_get_contents($fileData);
$path = Storage::put("files/{$fileHash}", $fileContent);
} elseif (self::isLikelyBinary($fileData)) {
$fileHash = hash('sha256', $fileData);
$fileSize = strlen($fileData);
// $path = Storage::putFile('files', $fileData);
$fileContent = $fileData;
$path = Storage::put("files/{$fileHash}", $fileContent);
} else {
throw new \InvalidArgumentException('Invalid file data provided.');
}
$hashKey = hash('sha256', uniqid((string) now(), true));
$finfo = new \finfo(FILEINFO_MIME_TYPE);
$mimetype = $finfo->buffer($fileContent);
// $fileContent = FileContent::create([
// 'hashkey' => $hashKey,
// 'filehash' => $fileHash,
// 'titlename' => $title,
// 'description' => $description,
// 'size_in_bytes' => $fileSize,
// 'content' => $fileContent,
// 'filelocation' => $filelocation ?? $path,
// 'created_by' => Auth::id(),
// 'updated_by' => Auth::id(),
// 'details' => $details ?? [],
// ]);
$fileContentDB = FileContent::where('filehash', $fileHash)->first();
if ($fileContentDB) {
return $fileContentDB;
}
$fileContentDB = self::insertFileContent([
'hashkey' => $hashKey,
'filehash' => $fileHash,
'titlename' => $title,
'description' => $description,
'size_in_bytes' => $fileSize,
'content' => $fileContent,
'filelocation' => $filelocation ?? $path,
'created_by' => Auth::id(),
'updated_by' => Auth::id(),
'details' => $details ?? [],
'mimetype' => $mimetype
]);
return $fileContentDB;
}
private static function insertFileList(int $contentuid, string $title, string $filename, string $description, $categories, $details, $tags = [], $hidden = 0, ?string $file_type = null)
{
$filecontent_exists = FileContent::where('id', $contentuid)->exists();
if (!$filecontent_exists) {
throw new \Exception("File Content Does not Exist", 1);
}
$data = [
'contentuid' => $contentuid,
'hashkey' => hash('sha256', uniqid((string) now(), true)),
'title' => $title,
'filename' => $filename,
'description' => $description,
'categories' => $categories,
'details' => $details,
'tags' => $tags,
'hidden' => $hidden,
'file_type' => $file_type,
'is_public' => false,
'created_by' => Auth::id(),
'updated_by' => Auth::id(),
];
$filelist = FileList::create($data);
return $filelist;
}
public static function uploadFileList(string|UploadedFile $fileData, string $title, string $filename, ?string $description = null, ?array $details = [], $categories = null, $tags = [], $hidden = 0, ?string $file_type = null)
{
try {
$fileContent = self::uploadFileContent($fileData, $title, $description, $details);
$filecontent_id = $fileContent->id;
// print_r($fileContent);
return self::insertFileList($filecontent_id, $title, $filename, $description, $categories, $details, $tags, $hidden, $file_type);
} catch (\Throwable $th) {
return Response::json($th->getMessage(), 500);
// return Response::make('Error uploading file content: ' . $th->getMessage(), 500);
return false;
}
}
public static function viewFilebyFileListHash(string $filelist_hash)
{
$filelist = FileList::where('hashkey', $filelist_hash)->first();
// return Response::json($filelist, 500);
if (!$filelist) {
abort(404, 'File not found');
}
$cdnUrl = trim((string) ($filelist->cdn_url ?? ''));
if ($cdnUrl !== '') {
return redirect($cdnUrl);
}
$fileContent = $filelist->fileContent;
if (!$fileContent) {
abort(404, 'File content not found');
}
$content = $fileContent->content;
$driver = DB::connection()->getDriverName();
if (is_resource($content) && $driver !== 'mysql') {
$content = stream_get_contents($content);
}
if ($driver === 'mysql') {
$content = base64_decode($content);
}
$finfo = new \finfo(FILEINFO_MIME_TYPE);
$mimeType = $finfo->buffer($content);
// Map common MIME types to extensions
$mimeMap = [
'image/jpeg' => 'jpg',
'image/png' => 'png',
'image/gif' => 'gif',
'image/webp' => 'webp',
'application/pdf' => 'pdf',
'text/plain' => 'txt',
'text/html' => 'html',
'application/zip' => 'zip',
'application/json' => 'json',
'audio/mpeg' => 'mp3',
'video/mp4' => 'mp4',
// add more as needed
];
// Pick extension from map, or fallback
$extension = $mimeMap[$mimeType] ?? pathinfo($filelist->filename, PATHINFO_EXTENSION);
// Build safe filename with extension
$filename = pathinfo($filelist->filename, PATHINFO_FILENAME) . '.' . $extension;
return Response::make($content, 200, [
'Content-Type' => $mimeType,
'Content-Disposition' => 'inline; filename="' . $filename . '"',
'Content-Length' => strlen($content),
]);
// return Response::make($content, 200, [
// 'Content-Type' => 'application/octet-stream',
// 'Content-Disposition' => 'inline; filename="' . $filelist->filename . '"',
// 'Content-Length' => strlen($content),
// ]);
}
private static function canUploadCategory(string $category): bool
{
if (!Auth::check()) {
return false;
}
if (UserPermissions::isActionPermitted(Auth::user()->acct_type, UserActions::UploadAllFiles)) {
return true;
}
if (strcasecmp($category, 'ProductMarket') === 0) {
return ProductPermissions::isActionAllowed(UserActions::CreateProductForOwnStore)
|| ProductPermissions::isActionAllowed(UserActions::AddProducttoOwnStore);
}
return false;
}
public static function UploadFilefromRequest(Request $request, string $category)
{
if (!self::canUploadCategory($category)) {
return response()->json(['error' => 'Unauthorized'], 403);
}
if (!$request->hasFile('file')) {
// $responseData =['success' => false, 'message' => 'No file uploaded'];
$responseData = false;
return response()->json(false, 400);
}
$file = $request->file('file');
$filename = $file->getClientFilename();
$result = self::uploadFileList(
$file,
'',
$filename ?? '',
'',
[],
$category,
[],
0,
null,
);
// return response()->json(['success' => false, 'error' => 'File upload failed'], 500);
// return $result;
$file_url = $result->resolvedUrl();
try {
if (is_string($result->hashkey) && !empty($result->hashkey)) {
return response()->json(['success' => true, 'hashkey' => $result->hashkey, 'message' => 'File uploaded successfully', 'url' => $file_url, 'name' => $filename], 200);
} else {
throw new \Exception("File upload failed", 1);
}
} catch (\Throwable $th) {
return response()->json(['success' => false, 'error' => 'File upload failed'], 500);
}
}
public static function generateURLforFileListHash(string $filelist_hashkey)
{
$cdnUrl = FileList::where('hashkey', $filelist_hashkey)->value('cdn_url');
if (is_string($cdnUrl) && trim($cdnUrl) !== '') {
return $cdnUrl;
}
return "/RequestData/File/$filelist_hashkey";
// return route('requestdata.file.view', ['hash' => $filelist_hashkey]);
}
public static function bladePreloadFileScript(string|FileList $FileHash): null|string
{
try {
if ($FileHash instanceof FileList) {
$filecontent = $FileHash->fileContent->content;
$mimeType = $FileHash->fileContent->mimetype ?? 'application/octet-stream';
$FileHash = $FileHash->hashkey;
} else {
$FileHash = FileList::where('hashkey', $FileHash)->firstOrFail();
$filecontent = $FileHash->fileContent->content;
$mimeType = $FileHash->fileContent->mimetype;
$FileHash = $FileHash->hashkey;
}
} catch (\Throwable $th) {
return '';
}
$base64 = base64_encode($filecontent);
$htmlString = "reqcacheupdateBase64toFile('$FileHash','$base64','$mimeType')";
return $htmlString;
}
}

View File

@@ -1,177 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers;
use App\Enums\Market\ProductTransactionType;
use App\Enums\Market\TransactionFlow;
use App\Enums\UserTypes;
use App\Http\Controllers\Helpers\ResponseHelper;
use App\Models\GlobalTransaction;
use App\Models\Market\Product;
use App\Models\Market\Store;
use Hypervel\Http\Request;
use Hypervel\Support\Facades\Auth;
use Hypervel\Support\Facades\Validator;
use App\Http\Controllers\Helpers\UserController;
use App\Http\Controllers\Helpers\Permissions\UserPermissions;
use App\Enums\UserActions;
class GlobalTransactionController
{
/**
* List transactions with optional filtering.
* Handles POST /admin/transactions/list
*/
public function list(Request $request)
{
if (!UserPermissions::isActionPermitted(Auth::user()->acct_type, UserActions::ViewGlobalTransactions)) {
return ResponseHelper::returnUnauthorized();
}
$user = Auth::user();
$productIdHash = $request->input('product_id');
$storeIdHash = $request->input('store_id');
$query = GlobalTransaction::with(['user:id,name,hashkey', 'product:id,name,hashkey', 'store:id,name,hashkey']);
if ($productIdHash) {
$product = Product::where('hashkey', $productIdHash)->first();
if ($product) {
$query->where('product_id', $product->id);
} else {
return response()->json([]);
}
}
if ($storeIdHash) {
$store = Store::where('hashkey', $storeIdHash)->first();
if ($store) {
$query->where('store_id', $store->id);
}
}
// Access Control: Ultimate can see everything.
// Others see transactions they created or relate to them.
if ($user->acct_type !== UserTypes::ULTIMATE) {
$query->where(function($q) use ($user) {
$q->where('user_id', $user->id)
->orWhere('created_by', $user->id);
});
}
$transactions = $query->orderBy('created_at', 'desc')->get();
// Transform for frontend
$transformed = $transactions->map(function ($tx) {
return [
'id' => $tx->id,
'hashkey' => $tx->hashkey,
'amount' => $tx->amount,
'type' => $tx->type ? $tx->type->label() : 'Unknown',
'status' => $tx->status,
'description' => $tx->description,
'created_at' => $tx->created_at,
'flow' => $tx->flow ? [
'value' => $tx->flow->value,
'label' => match($tx->flow) {
TransactionFlow::INCOME => 'Income',
TransactionFlow::EXPENSE => 'Expense',
TransactionFlow::NEUTRAL => 'Neutral',
default => 'Unknown'
}
] : null,
'user' => $tx->user ? $tx->user->name : null,
'product' => $tx->product ? $tx->product->name : null,
'store' => $tx->store ? $tx->store->name : null,
];
});
return response()->json($transformed);
}
/**
* Create a new global transaction.
* Handles POST /admin/transactions/create
*/
public function store(Request $request)
{
if (!UserPermissions::isActionPermitted(Auth::user()->acct_type, UserActions::CreateGlobalTransaction)) {
return ResponseHelper::returnUnauthorized();
}
$user = Auth::user();
$validator = Validator::make($request->all(), [
'user_hash' => 'nullable|string',
'amount' => 'required|numeric',
'type' => 'required|integer',
'description' => 'nullable|string',
'product_hash' => 'nullable|string',
'store_hash' => 'nullable|string',
'status' => 'nullable|string',
]);
if ($validator->fails()) {
return response()->json(['success' => false, 'errors' => $validator->errors()], 422);
}
$data = $validator->validated();
// Resolve IDs from hashes
$targetUserId = $user->id;
if (isset($data['user_hash'])) {
$targetUser = UserController::findUserIdByHash($data['user_hash']);
if ($targetUser) $targetUserId = $targetUser;
}
$productId = null;
if (isset($data['product_hash'])) {
$product = Product::where('hashkey', $data['product_hash'])->first();
if ($product) $productId = $product->id;
}
$storeId = null;
if (isset($data['store_hash'])) {
$store = Store::where('hashkey', $data['store_hash'])->first();
if ($store) $storeId = $store->id;
}
/** @var GlobalTransaction $transaction */
$transaction = GlobalTransaction::create([
'user_id' => $targetUserId,
'amount' => $data['amount'],
'type' => $data['type'],
'description' => $data['description'] ?? '',
'product_id' => $productId,
'store_id' => $storeId,
'status' => $data['status'] ?? 'completed',
'flow' => $request->has('flow') ? $request->input('flow') : ProductTransactionType::from($data['type'])->flow(),
'created_by' => $user->id,
]);
return response()->json([
'success' => true,
'message' => 'Transaction recorded successfully',
'hashkey' => $transaction->hashkey
]);
}
/**
* Get all product transaction types for dropdown selection.
*/
public function getTypes()
{
$types = collect(ProductTransactionType::cases())->map(function ($type) {
return [
'value' => $type->value,
'label' => $type->label(),
'flow' => $type->flow()->value,
'flow_label' => $type->flow()->name
];
});
return response()->json($types);
}
}

View File

@@ -11,8 +11,6 @@ use App\Models\User;
use Hypervel\Support\Facades\Auth;
use App\Enums\UserActions;
use App\Http\Controllers\Helpers\QueryHelper;
use App\Models\Market\Product;
use App\Models\Market\Store;
use Exception;
class LibLegacy

View File

@@ -1,191 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Helpers\Permissions;
use App\Enums\UserTypes;
use Hypervel\Http\Request;
use App\Models\User;
use Hypervel\Support\Facades\Auth;
use App\Enums\UserActions;
use App\Http\Controllers\Helpers\QueryHelper;
use App\Models\Market\Product;
use App\Models\Market\Store;
use Exception;
class ProductPermissions
{
public static function isModificationAllowed()
{
}
public static function isCreationAllowed()
{
}
public static function isActionAllowed(
UserActions $userAction,
Product|string|int|null $productHashorID = null,
Store|string|int|null $storeHashorID = null
): bool {
try {
$acct_type = Auth::user()->acct_type;
} catch (Exception $e) {
return false;
}
$defaultRoles = ProductPermissionsDefinition::getAllowedUserTypesAction($acct_type);
$additionalRoles = UserPermissions::isUserAllowedbyAdditionalRoles($userAction);
$deniedRoles = UserPermissions::isUserDeniedRoles($userAction);
if ($deniedRoles) {
return false;
}
if (!in_array($userAction, $defaultRoles, true) && !$additionalRoles) {
return false;
}
if (!ProductPermissionsDefinition::doesActionRequireDirectChildren($userAction)) {
return true;
}
if (!$storeHashorID && !$productHashorID) {
return false;
}
$store = null;
$product = null;
if ($storeHashorID) {
$store = QueryHelper::findOrNullByHashOrId($storeHashorID, Store::class);
}
if ($productHashorID) {
$product = QueryHelper::findOrNullByHashOrId($productHashorID, Product::class);
}
if (!$store && !$product) {
return false;
}
// Determine store from product if needed
if (!$store && $product) {
$store = $product->store ?? null;
}
if (!$store) {
return false;
}
$storeOwner = $store->owner;
if ($storeOwner && UserPermissions::isDescendantOfCurrentUser($storeOwner)) {
return true;
}
// Check all managers in the new store_managers table
$managerIds = $store->managerUsers()->pluck('users.id')->toArray();
foreach ($managerIds as $managerId) {
if (UserPermissions::isDescendantOfCurrentUser($managerId)) {
return true;
}
}
// Legacy manager check
if ($store->manager_id && UserPermissions::isDescendantOfCurrentUser($store->manager_id)) {
return true;
}
return false;
}
}
class ProductPermissionsDefinition
{
public static function getAllowedUserTypesAction(UserTypes $currentUserType)
{
return match ($currentUserType) {
UserTypes::ULTIMATE => UserActions::cases(),
UserTypes::SUPER_OPERATOR => [
UserActions::CreateStoreforSelf,
UserActions::CreateStoreGlobal,
UserActions::ModifyAllStores,
UserActions::ModifyOwnStore,
UserActions::CreateProductGlobal,
UserActions::CreateProductForOwnStore,
UserActions::CreateProductforSelf,
UserActions::ModifyAllProducts,
UserActions::ModifyOwnProduct,
UserActions::AddProducttoOwnStore,
UserActions::AddProducttoAnyStore,
UserActions::RemoveProductfromAnyStore,
],
UserTypes::OPERATOR => [
UserActions::CreateStoreforSelf,
UserActions::CreateStoreGlobal,
UserActions::ModifyAllStores,
UserActions::ModifyOwnStore,
UserActions::CreateProductGlobal,
UserActions::CreateProductForOwnStore,
UserActions::CreateProductforSelf,
UserActions::ModifyAllProducts,
UserActions::ModifyOwnProduct,
UserActions::AddProducttoOwnStore,
UserActions::AddProducttoAnyStore,
UserActions::RemoveProductfromAnyStore,
],
UserTypes::STORE_OWNER => [
UserActions::ModifyOwnStore,
UserActions::ModifyOwnProduct,
UserActions::AddProducttoOwnStore,
UserActions::CreateProductForOwnStore,
],
UserTypes::STORE_MANAGER => [
UserActions::ModifyOwnProduct,
UserActions::AddProducttoOwnStore,
UserActions::CreateProductForOwnStore,
],
default => [],
};
}
public static function doesActionRequireDirectChildren(UserActions $userAction)
{
return match ($userAction) {
UserActions::CreateStoreforSelf => true,
UserActions::CreateStoreGlobal => false,
UserActions::ModifyAllStores => false,
UserActions::ModifyOwnStore => true,
UserActions::CreateProductGlobal => false,
UserActions::CreateProductforSelf => true,
UserActions::ModifyAllProducts => false,
UserActions::ModifyOwnProduct => true,
UserActions::AddProducttoOwnStore=>true,
UserActions::AddProducttoAnyStore=>false,
UserActions::RemoveProductfromAnyStore=>false,
default => false,
};
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,55 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Market;
use App\Services\ActivityService;
use Hypervel\Http\Request;
use Hypervel\Support\Facades\Response;
use App\Http\Controllers\Helpers\ResponseHelper;
class ActivityController
{
/**
* Get recent activities.
*
* @param Request $request
* @return \Psr\Http\Message\ResponseInterface
*/
public function getRecent(Request $request)
{
$limit = (int) $request->input('limit', 10);
$service = new ActivityService();
$activities = $service->getRecentActivities($limit);
return Response::json([
'success' => true,
'data' => $activities
]);
}
/**
* Search activities.
*
* @param Request $request
* @return \Psr\Http\Message\ResponseInterface
*/
public function search(Request $request)
{
$query = $request->input('q', '');
$limit = (int) $request->input('limit', 20);
$service = new ActivityService();
if (empty($query)) {
$activities = $service->getRecentActivities($limit);
} else {
$activities = $service->searchActivities($query, $limit);
}
return Response::json([
'success' => true,
'data' => $activities
]);
}
}

View File

@@ -1,577 +0,0 @@
<?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());
}
}
}

View File

@@ -1,142 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Market;
use App\Models\Market\Cart;
use App\Models\Market\CartItem;
use App\Models\Market\Product;
use Hypervel\Http\Request;
use Hypervel\Support\Facades\Auth;
use Hypervel\Support\Facades\Response;
use Hypervel\Support\Str;
class CartController
{
public function getCart()
{
$user = Auth::user();
if (!$user) {
return Response::json(['error' => 'Unauthorized'], 401);
}
$cart = Cart::firstOrCreate(['user_id' => $user->id]);
$items = $cart->items()->with('product')->get();
return Response::json([
'success' => true,
'cart' => $cart,
'items' => $items,
'total' => $items->sum(fn($item) => $item->price * $item->quantity)
]);
}
public function addItem(Request $request)
{
$user = Auth::user();
if (!$user) {
return Response::json(['error' => 'Unauthorized'], 401);
}
$request->validate([
'product_hash' => 'required|string',
'quantity' => 'nullable|integer|min:1',
]);
$product = Product::where('hashkey', $request->input('product_hash'))->first();
if (!$product) {
return Response::json(['error' => 'Product not found'], 404);
}
$cart = Cart::firstOrCreate(['user_id' => $user->id]);
$item = $cart->items()->where('product_id', $product->id)->first();
if ($item) {
$item->quantity += $request->input('quantity', 1);
$item->save();
} else {
$cart->items()->create([
'product_id' => $product->id,
'quantity' => $request->input('quantity', 1),
'price' => $product->price,
'is_active' => true,
'hashkey' => Str::uuid()->toString(),
]);
}
return Response::json(['success' => true, 'message' => 'Item added to cart']);
}
public function updateItem(Request $request)
{
$user = Auth::user();
if (!$user) {
return Response::json(['error' => 'Unauthorized'], 401);
}
$request->validate([
'item_hash' => 'required|string',
'quantity' => 'required|integer|min:1',
]);
$item = CartItem::where('hashkey', $request->input('item_hash'))->first();
if (!$item) {
return Response::json(['error' => 'Item not found'], 404);
}
// Verify cart ownership
$cart = Cart::find($item->cart_id);
if ($cart->user_id !== $user->id) {
return Response::json(['error' => 'Forbidden'], 403);
}
$item->quantity = $request->input('quantity');
$item->save();
return Response::json(['success' => true, 'message' => 'Cart updated']);
}
public function removeItem(Request $request)
{
$user = Auth::user();
if (!$user) {
return Response::json(['error' => 'Unauthorized'], 401);
}
$request->validate([
'item_hash' => 'required|string',
]);
$item = CartItem::where('hashkey', $request->input('item_hash'))->first();
if (!$item) {
return Response::json(['error' => 'Item not found'], 404);
}
$cart = Cart::find($item->cart_id);
if ($cart->user_id !== $user->id) {
return Response::json(['error' => 'Forbidden'], 403);
}
$item->delete();
return Response::json(['success' => true, 'message' => 'Item removed from cart']);
}
public function clearCart()
{
$user = Auth::user();
if (!$user) {
return Response::json(['error' => 'Unauthorized'], 401);
}
$cart = Cart::where('user_id', $user->id)->first();
if ($cart) {
$cart->items()->delete();
}
return Response::json(['success' => true, 'message' => 'Cart cleared']);
}
}

View File

@@ -1,470 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Market;
use App\Http\Controllers\Helpers\ResponseHelper;
use App\Models\Market\CooperativeMember;
use App\Models\Market\Organization;
use App\Models\User;
use Hypervel\Http\Request;
use Hypervel\Support\Facades\Auth;
use Hypervel\Support\Facades\Hash;
use Hypervel\Support\Facades\Response;
use App\Http\Controllers\Helpers\Permissions\UserPermissions;
use App\Enums\UserActions;
use App\Enums\UserTypes;
use Hypervel\Support\Str;
class CooperativeController
{
public function listCooperatives(Request $request)
{
if (!UserPermissions::isActionPermitted(Auth::user()->acct_type, UserActions::ViewOrganizations)) {
return ResponseHelper::returnUnauthorized();
}
$query = Organization::where('is_active', true)->where('type', 'COOPERATIVE');
$cooperatives = $query->withCount('members')->get();
return response()->json([
'success' => true,
'data' => $cooperatives
]);
}
public function getCooperative(Request $request)
{
if (!UserPermissions::isActionPermitted(Auth::user()->acct_type, UserActions::ViewOrganizations)) {
return ResponseHelper::returnUnauthorized();
}
$hashkey = $request->input('hashkey');
if (!$hashkey) {
return ResponseHelper::returnIncorrectDetails();
}
$cooperative = Organization::where('hashkey', $hashkey)->with(['members.user.userInfo'])->first();
if (!$cooperative) {
return ResponseHelper::returnError('Cooperative not found', 404);
}
$currentUserMembership = null;
$user = Auth::user();
if ($user) {
$currentUserMembership = CooperativeMember::where('organization_id', $cooperative->id)
->where('user_id', $user->id)
->first();
}
return response()->json([
'success' => true,
'data' => $cooperative,
'is_member' => $currentUserMembership !== null,
'membership' => $currentUserMembership,
]);
}
public function createCooperative(Request $request)
{
if (!UserPermissions::isActionPermitted(Auth::user()->acct_type, UserActions::CreateOrganization)) {
return ResponseHelper::returnUnauthorized();
}
$name = $request->input('name');
$address = $request->input('address', '');
$registrationNumber = $request->input('registration_number', '');
$cin = $request->input('cin', '');
$tin = $request->input('tin', '');
$cooperativeType = $request->input('cooperative_type', '');
$cooperativeCategory = $request->input('cooperative_category', '');
$registrationDate = $request->input('registration_date', null);
$contactPerson = $request->input('contact_person', '');
$contactNumber = $request->input('contact_number', '');
$contactEmail = $request->input('contact_email', '');
if (empty(trim($name ?? ''))) {
return ResponseHelper::returnError('Cooperative name is required');
}
$cooperative = new Organization([
'hashkey' => Str::random(64),
'name' => trim($name),
'type' => 'COOPERATIVE',
'address' => trim($address ?? ''),
'registration_number' => trim($registrationNumber),
'cin' => trim($cin),
'tin' => trim($tin),
'cooperative_type' => trim($cooperativeType),
'cooperative_category' => trim($cooperativeCategory),
'registration_date' => $registrationDate,
'contact_person' => trim($contactPerson),
'contact_number' => trim($contactNumber),
'contact_email' => trim($contactEmail),
'is_active' => true,
'created_by' => Auth::id(),
]);
if ($cooperative->save()) {
return ResponseHelper::returnSuccessResponse($cooperative, $cooperative->hashkey, 'Cooperative created successfully');
}
return ResponseHelper::returnError('Failed to create cooperative');
}
public function joinCooperative(Request $request)
{
$user = Auth::user();
if (!$user) {
return ResponseHelper::returnUnauthorized();
}
$cooperativeHash = $request->input('cooperative_hash');
if (!$cooperativeHash) {
return ResponseHelper::returnIncorrectDetails();
}
$cooperative = Organization::where('hashkey', $cooperativeHash)->first();
if (!$cooperative) {
return ResponseHelper::returnError('Cooperative not found', 404);
}
// Check if already a member
$existing = CooperativeMember::where('organization_id', $cooperative->id)
->where('user_id', $user->id)
->first();
if ($existing) {
return ResponseHelper::returnError('Already a member of this cooperative');
}
$memberFields = $request->only([
'role', 'membership_type', 'membership_level', 'officer_position', 'officer_level',
'concurrent_position', 'concurrent_level', 'cooperative_name_alt', 'cooperative_position', 'year_beginning',
'priority_sector', 'common_bond', 'vulnerability_classifications',
'philsys_id', 'sss_number', 'pagibig_number',
'slp_track', 'slp_association_name', 'listahanan_id', 'fourtps_household_id',
'tupad_category', 'tupad_insurance_beneficiary_name', 'tupad_insurance_beneficiary_relation',
'preferred_occupation', 'nsrp_skills', 'employment_status', 'program_participation',
]);
$member = new CooperativeMember(array_merge($memberFields, [
'organization_id' => $cooperative->id,
'user_id' => $user->id,
'role' => $request->input('role', 'MEMBER'),
'joined_at' => now(),
'is_active' => true,
]));
if ($member->save()) {
// Sync with user settings
$settings = $user->settings ?? [];
$cooperatives = $settings['cooperatives'] ?? [];
if (!in_array($cooperativeHash, $cooperatives)) {
$cooperatives[] = $cooperativeHash;
$settings['cooperatives'] = $cooperatives;
$user->settings = $settings;
$user->save();
}
return ResponseHelper::returnSuccessResponse($member, $member->hashkey, 'Successfully joined cooperative');
}
return ResponseHelper::returnError('Failed to join cooperative');
}
public function addMember(Request $request)
{
if (!UserPermissions::isActionPermitted(Auth::user()->acct_type, UserActions::ManageOrganizations)) {
return ResponseHelper::returnUnauthorized();
}
$cooperativeHash = $request->input('cooperative_hash');
$userHash = $request->input('user_hash');
if (!$cooperativeHash || !$userHash) {
return ResponseHelper::returnIncorrectDetails();
}
$cooperative = Organization::where('hashkey', $cooperativeHash)->first();
$targetUser = User::where('hashkey', $userHash)->first();
if (!$cooperative || !$targetUser) {
return ResponseHelper::returnError('Cooperative or User not found', 404);
}
$existing = CooperativeMember::where('organization_id', $cooperative->id)
->where('user_id', $targetUser->id)
->first();
if ($existing) {
return ResponseHelper::returnError('User is already a member');
}
$memberFields = $request->only([
'role', 'membership_type', 'membership_level', 'officer_position', 'officer_level',
'concurrent_position', 'concurrent_level', 'cooperative_name_alt', 'cooperative_position', 'year_beginning',
'priority_sector', 'common_bond', 'vulnerability_classifications',
'philsys_id', 'sss_number', 'pagibig_number',
'slp_track', 'slp_association_name', 'listahanan_id', 'fourtps_household_id',
'tupad_category', 'tupad_insurance_beneficiary_name', 'tupad_insurance_beneficiary_relation',
'preferred_occupation', 'nsrp_skills', 'employment_status', 'program_participation',
]);
$member = new CooperativeMember(array_merge($memberFields, [
'organization_id' => $cooperative->id,
'user_id' => $targetUser->id,
'role' => $request->input('role', 'MEMBER'),
'joined_at' => now(),
'is_active' => true,
]));
if ($member->save()) {
return ResponseHelper::returnSuccessResponse($member, $member->hashkey, 'Member added to cooperative');
}
return ResponseHelper::returnError('Failed to add member');
}
public function updateMember(Request $request)
{
if (!UserPermissions::isActionPermitted(Auth::user()->acct_type, UserActions::ManageOrganizations)) {
return ResponseHelper::returnUnauthorized();
}
$memberHash = $request->input('member_hash');
if (!$memberHash) {
return ResponseHelper::returnIncorrectDetails();
}
$member = CooperativeMember::where('hashkey', $memberHash)->first();
if (!$member) {
return ResponseHelper::returnError('Member record not found', 404);
}
$memberFields = $request->only([
'role', 'membership_type', 'membership_level', 'officer_position', 'officer_level',
'concurrent_position', 'concurrent_level', 'cooperative_name_alt', 'cooperative_position', 'year_beginning',
'is_active'
]);
$member->fill($memberFields);
if ($member->save()) {
return ResponseHelper::returnSuccessResponse($member, $member->hashkey, 'Membership details updated');
}
return ResponseHelper::returnError('Failed to update membership details');
}
public function registerMember(Request $request)
{
$user = Auth::user();
if (!$user) {
return ResponseHelper::returnUnauthorized();
}
if (!UserPermissions::isActionPermitted($user->acct_type, UserActions::JoinCooperative)) {
return ResponseHelper::returnUnauthorized();
}
$cooperativeHash = $request->input('cooperative_hash');
if (!$cooperativeHash) {
return ResponseHelper::returnIncorrectDetails();
}
$cooperative = Organization::where('hashkey', $cooperativeHash)->first();
if (!$cooperative) {
return ResponseHelper::returnError('Cooperative not found', 404);
}
// Check if already a member
$existing = CooperativeMember::where('organization_id', $cooperative->id)
->where('user_id', $user->id)
->first();
if ($existing) {
return ResponseHelper::returnError('Already a member of this cooperative');
}
$memberFields = $request->only([
'role', 'membership_type', 'membership_level', 'officer_position', 'officer_level',
'concurrent_position', 'concurrent_level', 'cooperative_name_alt', 'cooperative_position', 'year_beginning',
'priority_sector', 'common_bond', 'vulnerability_classifications',
'philsys_id', 'sss_number', 'pagibig_number',
'slp_track', 'slp_association_name', 'listahanan_id', 'fourtps_household_id',
'tupad_category', 'tupad_insurance_beneficiary_name', 'tupad_insurance_beneficiary_relation',
'preferred_occupation', 'nsrp_skills', 'employment_status', 'program_participation',
]);
$member = new CooperativeMember(array_merge($memberFields, [
'hashkey' => Str::random(64),
'organization_id' => $cooperative->id,
'user_id' => $user->id,
'role' => $request->input('role', 'MEMBER'),
'joined_at' => now(),
'is_active' => true,
'created_by' => $user->id,
]));
if ($member->save()) {
// Sync with user settings
$settings = $user->settings ?? [];
$cooperatives = $settings['cooperatives'] ?? [];
if (!is_array($cooperatives)) $cooperatives = [];
if (!in_array($cooperativeHash, $cooperatives)) {
$cooperatives[] = $cooperativeHash;
$settings['cooperatives'] = $cooperatives;
$user->settings = $settings;
$user->save();
}
// Upgrade a plain USER to COOP_MEMBER on cooperative registration.
// Never downgrade a higher type (COORDINATOR, OPERATOR, etc.).
if ($user->acct_type === UserTypes::USER) {
$user->acct_type = UserTypes::COOP_MEMBER;
$user->save();
}
return ResponseHelper::returnSuccessResponse($member, $member->hashkey, 'Successfully registered as a cooperative member');
}
return ResponseHelper::returnError('Failed to register');
}
public function publicCompleteMembership(Request $request)
{
$userHashkey = $request->input('user_hashkey');
$coopHash = $request->input('cooperative_hash');
if (!$userHashkey || !$coopHash) {
return ResponseHelper::returnIncorrectDetails();
}
$user = User::where('hashkey', $userHashkey)->first();
if (!$user) {
return response()->json(['success' => false, 'message' => 'User not found'], 404);
}
$cooperative = Organization::where('hashkey', $coopHash)
->where('type', 'COOPERATIVE')
->where('is_active', true)
->first();
if (!$cooperative) {
return response()->json(['success' => false, 'message' => 'Cooperative not found'], 404);
}
$existing = CooperativeMember::where('organization_id', $cooperative->id)
->where('user_id', $user->id)
->first();
if ($existing) {
return response()->json(['success' => false, 'message' => 'Already a member'], 409);
}
$member = new CooperativeMember(array_merge(
$request->only([
'membership_type', 'membership_level', 'year_beginning',
'officer_position', 'officer_level', 'concurrent_position', 'concurrent_level',
'cooperative_position', 'cooperative_name_alt',
'priority_sector', 'common_bond', 'vulnerability_classifications',
'philsys_id', 'sss_number', 'pagibig_number',
'slp_track', 'slp_association_name', 'listahanan_id', 'fourtps_household_id',
'tupad_category', 'tupad_insurance_beneficiary_name', 'tupad_insurance_beneficiary_relation',
'preferred_occupation', 'nsrp_skills', 'employment_status', 'program_participation',
]),
[
'hashkey' => Str::random(64),
'organization_id' => $cooperative->id,
'user_id' => $user->id,
'role' => 'MEMBER',
'joined_at' => now(),
'is_active' => true,
'created_by' => $user->id,
]
));
$member->save();
$settings = $user->settings ?? [];
$settings['cooperatives'] = array_unique(array_merge($settings['cooperatives'] ?? [], [$coopHash]));
$user->settings = $settings;
$user->save();
return response()->json(['success' => true, 'message' => 'Membership application submitted successfully.']);
}
public function publicGetCooperative(Request $request, string $hkey)
{
try {
$cooperative = Organization::where('hashkey', $hkey)
->where('type', 'COOPERATIVE')
->where('is_active', true)
->select(['id', 'hashkey', 'name', 'type', 'cooperative_type', 'cooperative_category', 'contact_person', 'contact_number', 'address'])
->first();
} catch (\Throwable $e) {
return Response::json(['success' => false, 'message' => 'Service temporarily unavailable'], 500);
}
if (!$cooperative) {
return Response::json(['success' => false, 'message' => 'Cooperative not found'], 404);
}
return Response::json(['success' => true, 'data' => $cooperative]);
}
public function publicRegisterMember(Request $request)
{
$hkey = $request->input('cooperative_hash');
if (!$hkey) {
return ResponseHelper::returnIncorrectDetails();
}
$cooperative = Organization::where('hashkey', $hkey)
->where('type', 'COOPERATIVE')
->where('is_active', true)
->first();
if (!$cooperative) {
return Response::json(['success' => false, 'message' => 'Cooperative not found'], 404);
}
try {
$validated = $request->validate([
'name' => 'required|string|max:255',
'username' => 'required|string|max:255|unique:users,username',
'mobile_number' => ['required', 'string', 'max:20', 'unique:users,mobile_number', 'regex:/^(09|\+639)\d{9}$/'],
'password' => 'required|string|min:6',
]);
} catch (\Hypervel\Validation\ValidationException $e) {
return Response::json(['success' => false, 'errors' => $e->errors()], 422);
}
$parentUser = User::where('id', $cooperative->created_by)->first()
?? User::where('acct_type', 'COORDINATOR')->first()
?? User::orderBy('id')->first();
if (!$parentUser) {
return Response::json(['success' => false, 'message' => 'No valid parent user found'], 500);
}
$user = new User();
$user->username = $validated['username'];
$user->name = $validated['name'];
$user->mobile_number = $validated['mobile_number'];
$user->password = Hash::make($validated['password']);
$user->parentuid = $parentUser->id;
$user->acct_type = 'user';
$user->active = true;
$user->save();
return Response::json([
'success' => true,
'user_hashkey' => $user->hashkey,
'message' => 'Account created. Please complete your membership application.',
], 201);
}
}

View File

@@ -1,257 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Market;
use App\Http\Controllers\FilesMainController;
use App\Http\Controllers\Helpers\ResponseHelper;
use App\Models\Market\CooperativeDocument;
use App\Models\Market\Organization;
use App\Models\FileList;
use Hypervel\Http\Request;
use Hypervel\Support\Facades\Auth;
use App\Http\Controllers\Helpers\Permissions\UserPermissions;
use App\Enums\UserActions;
use Hypervel\Support\Str;
class CooperativeDocumentController
{
public function listDocuments(Request $request)
{
$orgHash = $request->input('orgHash');
if (!$orgHash) {
return ResponseHelper::returnIncorrectDetails();
}
$org = Organization::where('hashkey', $orgHash)->first();
if (!$org) {
return ResponseHelper::returnError('Organization not found', 404);
}
// Get latest versions (where parent_hashkey IS NULL or it is the latest in its group)
// For simplicity, we'll fetch all active docs and group them by parent_hashkey or hashkey
$allDocs = CooperativeDocument::where('organization_id', $org->id)
->where('is_active', true)
->orderBy('version_number', 'desc')
->get();
$fileHashes = $allDocs->pluck('file_hashkey')->filter()->unique()->toArray();
$fileLists = FileList::whereIn('hashkey', $fileHashes)->with('fileContent')->get()->keyBy('hashkey');
$grouped = [];
foreach ($allDocs as $doc) {
$rootKey = $doc->parent_hashkey ?? $doc->hashkey;
if (!isset($grouped[$rootKey])) {
$grouped[$rootKey] = [];
}
$grouped[$rootKey][] = $doc;
}
$data = [];
foreach ($grouped as $rootKey => $versions) {
$latest = $versions[0]; // ordered desc by version_number
$fileList = $fileLists[$latest->file_hashkey] ?? null;
if ($fileList) {
$history = [];
foreach ($versions as $v) {
$vFileList = $fileLists[$v->file_hashkey] ?? null;
if ($vFileList) {
$history[] = [
'hashkey' => $v->hashkey,
'version' => $v->version_number,
'name' => $vFileList->filename,
'date' => $v->created_at ? $v->created_at->format('Y-m-d H:i') : null,
'note' => $v->revision_note,
'url' => $vFileList->resolvedUrl()
];
}
}
$data[] = [
'hashkey' => $latest->hashkey,
'parent_hashkey' => $latest->parent_hashkey,
'name' => $fileList->filename,
'type' => $latest->document_type ?? 'Document',
'date' => $latest->created_at ? $latest->created_at->toDateString() : null,
'size' => $this->formatBytes($fileList->fileContent->size_in_bytes ?? 0),
'url' => $fileList->resolvedUrl(),
'version' => $latest->version_number,
'note' => $latest->revision_note,
'history' => $history
];
}
}
return response()->json([
'success' => true,
'data' => $data
]);
}
public function uploadDocument(Request $request)
{
if (!UserPermissions::isActionPermitted(Auth::user()->acct_type, UserActions::ManageOrganizations)) {
return ResponseHelper::returnUnauthorized();
}
$orgHash = $request->input('orgHash');
$docType = $request->input('type', 'OTHERS');
if (!$orgHash || !$request->hasFile('file')) {
return ResponseHelper::returnIncorrectDetails();
}
$org = Organization::where('hashkey', $orgHash)->first();
if (!$org) {
return ResponseHelper::returnError('Organization not found', 404);
}
$file = $request->file('file');
$filename = $file->getClientFilename();
$fileList = FilesMainController::uploadFileList(
$file,
$filename,
$filename,
'Cooperative Document for ' . $org->name,
['type' => 'coop_document', 'org_id' => $org->id],
'CooperativeDocuments',
[],
0,
'cooperative_document',
);
if (!$fileList || !isset($fileList->hashkey)) {
return ResponseHelper::returnError('File upload failed');
}
$doc = new CooperativeDocument([
'hashkey' => (string) Str::uuid(),
'organization_id' => $org->id,
'file_hashkey' => $fileList->hashkey,
'document_type' => $docType,
'created_by' => Auth::id(),
'is_active' => true,
]);
if ($doc->save()) {
return response()->json([
'success' => true,
'message' => 'Document uploaded successfully',
'data' => [
'hashkey' => $doc->hashkey,
'name' => $filename,
'url' => $fileList->resolvedUrl()
]
]);
}
return ResponseHelper::returnError('Failed to save document record');
}
public function reviseDocument(Request $request)
{
if (!UserPermissions::isActionPermitted(Auth::user()->acct_type, UserActions::ManageOrganizations)) {
return ResponseHelper::returnUnauthorized();
}
$parentHash = $request->input('parentHash');
$note = $request->input('note');
if (!$parentHash || !$request->hasFile('file')) {
return ResponseHelper::returnIncorrectDetails();
}
$parentDoc = CooperativeDocument::where('hashkey', $parentHash)->first();
if (!$parentDoc) {
return ResponseHelper::returnError('Original document not found', 404);
}
// The real parent is either its own parent or it is the parent
$rootHash = $parentDoc->parent_hashkey ?? $parentDoc->hashkey;
// Find highest version number
$lastVersion = CooperativeDocument::where('hashkey', $rootHash)
->orWhere('parent_hashkey', $rootHash)
->max('version_number');
$file = $request->file('file');
$filename = $file->getClientFilename();
$fileList = FilesMainController::uploadFileList(
$file,
$filename,
$filename,
'Revision of ' . $parentDoc->hashkey,
['type' => 'coop_document_revision', 'parent_id' => $parentDoc->id],
'CooperativeDocuments',
[],
0,
'cooperative_document_revision',
);
if (!$fileList) {
return ResponseHelper::returnError('File upload failed');
}
$doc = new CooperativeDocument([
'hashkey' => (string) Str::uuid(),
'parent_hashkey' => $rootHash,
'version_number' => $lastVersion + 1,
'organization_id' => $parentDoc->organization_id,
'file_hashkey' => $fileList->hashkey,
'document_type' => $parentDoc->document_type,
'revision_note' => $note,
'created_by' => Auth::id(),
'is_active' => true,
]);
if ($doc->save()) {
return response()->json([
'success' => true,
'message' => 'Revision uploaded successfully',
'data' => $doc
]);
}
return ResponseHelper::returnError('Failed to save revision record');
}
public function deleteDocument(Request $request)
{
if (!UserPermissions::isActionPermitted(Auth::user()->acct_type, UserActions::ManageOrganizations)) {
return ResponseHelper::returnUnauthorized();
}
$hashkey = $request->input('hashkey');
if (!$hashkey) {
return ResponseHelper::returnIncorrectDetails();
}
$doc = CooperativeDocument::where('hashkey', $hashkey)->first();
if (!$doc) {
return ResponseHelper::returnError('Document not found', 404);
}
// If it's a version, we might want to just deactivate that version.
// If it's the root, we might want to deactivate all versions.
// For now, let's just deactivate the specific one.
$doc->is_active = false;
if ($doc->save()) {
return response()->json(['success' => true, 'message' => 'Document/Revision deleted']);
}
return ResponseHelper::returnError('Failed to delete document');
}
private function formatBytes($bytes, $precision = 1)
{
$units = ['B', 'KB', 'MB', 'GB', 'TB'];
$bytes = max($bytes, 0);
$pow = floor(($bytes ? log($bytes) : 0) / log(1024));
$pow = min($pow, count($units) - 1);
$bytes /= (1 << (10 * $pow));
return round($bytes, $precision) . ' ' . $units[$pow];
}
}

View File

@@ -1,269 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Market;
use App\Http\Controllers\Helpers\ResponseHelper;
use App\Http\Controllers\Helpers\PaymentProcessor;
use App\Http\Controllers\Helpers\QrphDecoder;
use App\Models\SystemSetting;
use App\Models\Accounting\MemberLedger;
use App\Models\User;
use App\Models\GlobalTransaction;
use Hypervel\Http\Request;
use Hypervel\Support\Facades\Auth;
use Hypervel\Support\Facades\DB;
use Hypervel\Support\Str;
use App\Enums\Market\ProductTransactionType;
use App\Enums\Market\TransactionFlow;
class CreditController
{
public function getWalletData(Request $request)
{
$user = User::find(Auth::id());
if (!$user) return ResponseHelper::returnUnauthorized();
$history = MemberLedger::where('user_id', $user->id)
->where('is_active', true)
->orderBy('created_at', 'desc')
->limit(20)
->get();
return response()->json([
'success' => true,
'balance' => $user->total_balance,
'credit' => $user->total_credit,
'history' => $history
]);
}
public function topUp(Request $request)
{
// Check if Top Up is enabled globally
if (!(\App\Models\SystemSetting::getValue('top_up_enabled', true))) {
return ResponseHelper::returnError('Credit top-up is currently disabled by administrators.');
}
$amount = (float) $request->input('amount');
$method = $request->input('method', 'GCASH');
if ($amount <= 0) {
return ResponseHelper::returnError('Amount must be greater than zero');
}
$user = User::find(Auth::id());
if (!$user) return ResponseHelper::returnUnauthorized();
// Start Transaction
DB::beginTransaction();
try {
// 1. Simulate Payment Success (in real life this would be a webhook/callback)
$payment = PaymentProcessor::initiatePayment($amount, $method, $user->hashkey);
if (!$payment['success']) {
throw new \Exception('Payment initiation failed');
}
// 2. Update User Balance
$user->total_balance += $amount;
$user->save();
// 3. Record in MemberLedger
$ledger = new MemberLedger([
'hashkey' => Str::random(64),
'user_id' => $user->id,
'amount' => $amount,
'transaction_type' => 'TOP_UP',
'flow' => 'IN',
'balance_after' => $user->total_balance,
'description' => "Credit Top-up via {$method}",
'reference_id' => $payment['transaction_id'],
'created_by' => $user->id,
'is_active' => true,
]);
$ledger->save();
// 4. Record in GlobalTransaction (for compatibility with existing reports)
$globalTxn = new GlobalTransaction([
'hashkey' => Str::random(64),
'user_id' => $user->id,
'amount' => $amount,
'type' => ProductTransactionType::TOP_UP,
'status' => 'COMPLETED',
'description' => "Credit Top-up via {$method}",
'flow' => TransactionFlow::INCOME,
'created_by' => $user->id,
]);
$globalTxn->save();
DB::commit();
return response()->json([
'success' => true,
'message' => 'Top-up successful',
'balance' => $user->total_balance,
'transaction_id' => $payment['transaction_id']
]);
} catch (\Exception $e) {
DB::rollBack();
return ResponseHelper::returnError('Top-up failed: ' . $e->getMessage());
}
}
public function transferCredit(Request $request)
{
$recipientHash = $request->input('recipient_hash');
$amount = (float) $request->input('amount');
if ($amount <= 0) return ResponseHelper::returnError('Invalid amount');
$sender = User::find(Auth::id());
if (!$sender) return ResponseHelper::returnUnauthorized();
if ($sender->total_balance < $amount) {
return ResponseHelper::returnError('Insufficient balance');
}
$recipient = User::where('hashkey', $recipientHash)->first();
if (!$recipient) return ResponseHelper::returnError('Recipient not found');
if ($sender->id === $recipient->id) {
return ResponseHelper::returnError('Cannot transfer to yourself');
}
DB::beginTransaction();
try {
// Deduct from sender
$sender->total_balance -= $amount;
$sender->save();
// Add to recipient
$recipient->total_balance += $amount;
$recipient->save();
$txnRef = Str::random(12);
// Record Ledger for Sender
MemberLedger::create([
'hashkey' => Str::random(64),
'user_id' => $sender->id,
'amount' => $amount,
'transaction_type' => 'TRANSFER_OUT',
'flow' => 'OUT',
'balance_after' => $sender->total_balance,
'description' => "Credit Transfer to {$recipient->fullname}",
'reference_id' => $txnRef,
'created_by' => $sender->id,
]);
// Record Ledger for Recipient
MemberLedger::create([
'hashkey' => Str::random(64),
'user_id' => $recipient->id,
'amount' => $amount,
'transaction_type' => 'TRANSFER_IN',
'flow' => 'IN',
'balance_after' => $recipient->total_balance,
'description' => "Credit Transfer from {$sender->fullname}",
'reference_id' => $txnRef,
'created_by' => $sender->id,
]);
DB::commit();
return response()->json(['success' => true, 'message' => 'Transfer successful']);
} catch (\Exception $e) {
DB::rollBack();
return ResponseHelper::returnError('Transfer failed');
}
}
public function searchUsers(Request $request)
{
$query = $request->input('q');
if (empty($query)) return response()->json(['success' => true, 'data' => []]);
$users = User::where('fullname', 'like', "%{$query}%")
->orWhere('name', 'like', "%{$query}%")
->orWhere('mobile_number', 'like', "%{$query}%")
->where('id', '!=', Auth::id())
->limit(10)
->get(['hashkey', 'fullname', 'name', 'mobile_number']);
return response()->json(['success' => true, 'data' => $users]);
}
/**
* GET the current QRPH payment code (available to all authenticated users for top-up display).
*/
public function getQrphCode(Request $request)
{
$raw = SystemSetting::getValue('qrph_payment_code', null);
$imgHashkey = SystemSetting::getValue('qrph_payment_image_hashkey', null);
if (empty($raw)) {
return response()->json(['success' => true, 'qrph' => null, 'decoded' => null, 'image_url' => null]);
}
$decoded = QrphDecoder::decode($raw);
$imageUrl = $imgHashkey ? "/RequestData/File/{$imgHashkey}" : null;
return response()->json([
'success' => true,
'qrph' => $raw,
'decoded' => $decoded,
'image_url' => $imageUrl,
]);
}
/**
* SET the QRPH payment code — ULTIMATE only (enforced by route middleware).
* Accepts optional image_hashkey from a prior /File/Upload/QrphPayment call.
*/
public function setQrphCode(Request $request)
{
$raw = trim($request->input('qrph_code', ''));
$imgHashkey = trim($request->input('image_hashkey', ''));
if (empty($raw)) {
SystemSetting::setValue('qrph_payment_code', '');
SystemSetting::setValue('qrph_payment_image_hashkey', '');
return response()->json(['success' => true, 'message' => 'QRPH code cleared.']);
}
$decoded = QrphDecoder::decode($raw);
SystemSetting::setValue('qrph_payment_code', $raw);
if (!empty($imgHashkey)) {
SystemSetting::setValue('qrph_payment_image_hashkey', $imgHashkey);
}
$imageUrl = $imgHashkey ? "/RequestData/File/{$imgHashkey}" : null;
return response()->json([
'success' => true,
'message' => 'QRPH code saved.',
'decoded' => $decoded,
'image_url' => $imageUrl,
]);
}
/**
* Decode a QRPH string without saving — for preview before saving.
*/
public function decodeQrph(Request $request)
{
$raw = trim($request->input('qrph_code', ''));
if (empty($raw)) {
return ResponseHelper::returnError('No QR string provided.');
}
return response()->json([
'success' => true,
'decoded' => QrphDecoder::decode($raw),
]);
}
}

View File

@@ -1,143 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Market;
use App\Http\Controllers\Helpers\ResponseHelper;
use App\Models\Market\FarmerProfile;
use App\Models\Market\Organization;
use App\Models\User;
use Hypervel\Http\Request;
use Hypervel\Support\Facades\Auth;
use App\Http\Controllers\Helpers\Permissions\UserPermissions;
use App\Enums\UserActions;
class FarmerController
{
public function registerFarmer(Request $request)
{
$currentUser = Auth::user();
if (!$currentUser) {
return ResponseHelper::returnUnauthorized();
}
$validated = $request->validate([
'user_hash' => 'nullable|string',
'farm_name' => 'required|string|max:255',
'farm_location' => 'nullable|string',
'organization_hash' => 'nullable|string',
'main_crops' => 'nullable|array',
]);
// Determine the target user - if user_hash provided, use that user; otherwise use current user
if (!empty($validated['user_hash'])) {
$user = User::where('hashkey', $validated['user_hash'])->first();
if (!$user) {
return ResponseHelper::returnError('User not found', 404);
}
} else {
$user = $currentUser;
}
$organization = $validated['organization_hash'] ? Organization::where('hashkey', $validated['organization_hash'])->first() : null;
// Check if user already has a farmer profile
$existingProfile = FarmerProfile::where('user_id', $user->id)->first();
if ($existingProfile) {
return ResponseHelper::returnError('User already has a farmer profile');
}
$profile = new FarmerProfile([
'user_id' => $user->id,
'organization_id' => $organization?->id,
'farm_name' => $validated['farm_name'],
'farm_location' => $validated['farm_location'],
'main_crops' => $validated['main_crops'],
'verification_status' => 'VERIFIED',
'created_by' => $currentUser->id,
]);
if ($profile->save()) {
return ResponseHelper::returnSuccessResponse($profile, $profile->hashkey, 'Farmer profile created and pending verification');
}
return ResponseHelper::returnError('Failed to create farmer profile');
}
public function listFarmers(Request $request)
{
if (!UserPermissions::isActionPermitted(Auth::user()->acct_type, UserActions::ViewFarmers)) {
return ResponseHelper::returnUnauthorized();
}
$farmers = FarmerProfile::with(['user', 'organization'])->orderBy('created_at', 'desc')->get();
return response()->json([
'success' => true,
'data' => $farmers
]);
}
public function verifyFarmer(Request $request)
{
if (!UserPermissions::isActionPermitted(Auth::user()->acct_type, UserActions::VerifyFarmer)) {
return ResponseHelper::returnUnauthorized();
}
$hashkey = $request->input('target');
$status = $request->input('status'); // VERIFIED, REJECTED
if (!$hashkey || !$status) {
return ResponseHelper::returnIncorrectDetails();
}
$profile = FarmerProfile::where('hashkey', $hashkey)->first();
if (!$profile) {
return ResponseHelper::returnError('Profile not found', 404);
}
$profile->verification_status = $status;
$profile->save();
return ResponseHelper::returnSuccessResponse($profile, $profile->hashkey, 'Farmer verification status updated');
}
public function listOrganizations()
{
if (!UserPermissions::isActionPermitted(Auth::user()->acct_type, UserActions::ViewOrganizations)) {
return ResponseHelper::returnUnauthorized();
}
$orgs = Organization::where('is_active', true)->get();
return response()->json([
'success' => true,
'data' => $orgs
]);
}
public function createOrganization(Request $request)
{
if (!UserPermissions::isActionPermitted(Auth::user()->acct_type, UserActions::CreateOrganization)) {
return ResponseHelper::returnUnauthorized();
}
$validated = $request->validate([
'name' => 'required|string|max:255',
'type' => 'required|string|in:COOPERATIVE,ASSOCIATION,COMPANY',
'address' => 'nullable|string',
]);
$org = new Organization([
'name' => $validated['name'],
'type' => $validated['type'],
'address' => $validated['address'],
]);
if ($org->save()) {
return ResponseHelper::returnSuccessResponse($org, $org->hashkey, 'Organization created');
}
return ResponseHelper::returnError('Failed to create organization');
}
}

View File

@@ -1,178 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Market;
use App\Http\Controllers\Helpers\ResponseHelper;
use App\Models\Market\CooperativeResolution;
use App\Models\Market\CooperativeVote;
use App\Models\Market\Organization;
use App\Models\Market\CooperativeMember;
use Hypervel\Http\Request;
use Hypervel\Support\Facades\Auth;
use App\Http\Controllers\Helpers\Permissions\UserPermissions;
use App\Enums\UserActions;
use Hypervel\Support\Str;
class GovernanceController
{
public function listResolutions(Request $request)
{
if (!UserPermissions::isActionPermitted(Auth::user()->acct_type, UserActions::ViewResolutions)) {
return ResponseHelper::returnUnauthorized();
}
$orgHash = $request->input('org_hash');
if (!$orgHash) {
return ResponseHelper::returnIncorrectDetails();
}
$org = Organization::where('hashkey', $orgHash)->first();
if (!$org) {
return ResponseHelper::returnError('Organization not found', 404);
}
$resolutions = CooperativeResolution::where('organization_id', $org->id)
->where('is_active', true)
->withCount(['votes as yes_votes' => function($query) {
$query->where('vote_cast', 'YES');
}])
->withCount(['votes as no_votes' => function($query) {
$query->where('vote_cast', 'NO');
}])
->withCount(['votes as abstain_votes' => function($query) {
$query->where('vote_cast', 'ABSTAIN');
}])
->orderBy('created_at', 'desc')
->get();
return response()->json([
'success' => true,
'data' => $resolutions
]);
}
public function createResolution(Request $request)
{
if (!UserPermissions::isActionPermitted(Auth::user()->acct_type, UserActions::CreateResolution)) {
return ResponseHelper::returnUnauthorized();
}
$orgHash = $request->input('org_hash');
$title = $request->input('title');
$description = $request->input('description');
if (!$orgHash || !$title) {
return ResponseHelper::returnIncorrectDetails();
}
$org = Organization::where('hashkey', $orgHash)->first();
if (!$org) {
return ResponseHelper::returnError('Organization not found', 404);
}
$resolution = new CooperativeResolution([
'hashkey' => Str::random(64),
'organization_id' => $org->id,
'title' => trim($title),
'description' => trim($description ?? ''),
'status' => 'PROPOSED',
'created_by' => Auth::id(),
'is_active' => true,
]);
if ($resolution->save()) {
return ResponseHelper::returnSuccessResponse($resolution, $resolution->hashkey, 'Resolution created successfully');
}
return ResponseHelper::returnError('Failed to create resolution');
}
public function castVote(Request $request)
{
if (!UserPermissions::isActionPermitted(Auth::user()->acct_type, UserActions::VoteResolution)) {
return ResponseHelper::returnUnauthorized();
}
$resolutionHash = $request->input('resolution_hash');
$vote = $request->input('vote'); // YES, NO, ABSTAIN
if (!$resolutionHash || !in_array($vote, ['YES', 'NO', 'ABSTAIN'])) {
return ResponseHelper::returnIncorrectDetails();
}
$resolution = CooperativeResolution::where('hashkey', $resolutionHash)->first();
if (!$resolution) {
return ResponseHelper::returnError('Resolution not found', 404);
}
// Check if user is a member of the organization
$isMember = CooperativeMember::where('organization_id', $resolution->organization_id)
->where('user_id', Auth::id())
->where('is_active', true)
->exists();
if (!$isMember) {
return ResponseHelper::returnError('Only active members can vote on resolutions', 403);
}
// Check for existing vote
$existingVote = CooperativeVote::where('resolution_id', $resolution->id)
->where('user_id', Auth::id())
->first();
if ($existingVote) {
$existingVote->vote_cast = $vote;
$existingVote->updated_by = Auth::id();
if ($existingVote->save()) {
return ResponseHelper::returnSuccessResponse($existingVote, $existingVote->hashkey, 'Vote updated successfully');
}
} else {
$newVote = new CooperativeVote([
'hashkey' => Str::random(64),
'resolution_id' => $resolution->id,
'user_id' => Auth::id(),
'vote_cast' => $vote,
'created_by' => Auth::id(),
'is_active' => true,
]);
if ($newVote->save()) {
return ResponseHelper::returnSuccessResponse($newVote, $newVote->hashkey, 'Vote cast successfully');
}
}
return ResponseHelper::returnError('Failed to record vote');
}
public function updateResolutionStatus(Request $request)
{
if (!UserPermissions::isActionPermitted(Auth::user()->acct_type, UserActions::ManageOrganizations)) {
return ResponseHelper::returnUnauthorized();
}
$resolutionHash = $request->input('resolution_hash');
$status = $request->input('status'); // APPROVED, RESCINDED
if (!$resolutionHash || !in_array($status, ['APPROVED', 'RESCINDED', 'PROPOSED'])) {
return ResponseHelper::returnIncorrectDetails();
}
$resolution = CooperativeResolution::where('hashkey', $resolutionHash)->first();
if (!$resolution) {
return ResponseHelper::returnError('Resolution not found', 404);
}
$resolution->status = $status;
$resolution->updated_by = Auth::id();
if ($status === 'APPROVED') {
$resolution->date_approved = now();
}
if ($resolution->save()) {
return ResponseHelper::returnSuccessResponse($resolution, $resolution->hashkey, 'Resolution status updated');
}
return ResponseHelper::returnError('Failed to update resolution status');
}
}

View File

@@ -1,472 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Market;
use App\Enums\UserTypes;
use App\Http\Controllers\Helpers\ResponseHelper;
use App\Models\Market\PosSession;
use App\Models\Market\PosTransaction;
use App\Models\Market\Product;
use App\Models\Market\Store;
use App\Models\User;
use Hyperf\Stringable\Str;
use Hypervel\Http\Request;
use Hypervel\Support\Facades\DB;
use Hypervel\Support\Facades\Hash;
use Hypervel\Support\Facades\Response;
/**
* Performance / load-testing endpoints.
*
* These bypass session auth so a client machine can hit them with curl.
* Auth is via header `X-Perf-Token` matched against env `PERF_API_TOKEN`.
* If `PERF_API_TOKEN` is unset the endpoints are disabled (403 for all).
*
* All endpoints return timing metrics (ms) so the caller can chart how the
* box behaves under different batch sizes.
*/
class PerformanceController
{
private const DEFAULT_LIMIT = 1000;
private function authorize(Request $request)
{
$expected = (string) env('PERF_API_TOKEN', '');
if ($expected === '') {
return ResponseHelper::returnError('Performance API is disabled (PERF_API_TOKEN not set)', 403);
}
$provided = (string) ($request->header('X-Perf-Token') ?? $request->input('token', ''));
if (!hash_equals($expected, $provided)) {
return ResponseHelper::returnError('Invalid perf token', 401);
}
return null;
}
private function actingUser(): ?User
{
$hash = (string) env('PERF_ACTOR_HASH', '');
if ($hash !== '') {
$u = User::where('hashkey', $hash)->first();
if ($u) return $u;
}
return User::where('acct_type', UserTypes::ULTIMATE->value)
->orderBy('id', 'asc')
->first();
}
private function clampCount($raw): int
{
$n = (int) $raw;
if ($n < 1) $n = 1;
if ($n > self::DEFAULT_LIMIT) $n = self::DEFAULT_LIMIT;
return $n;
}
private function ms(float $start): float
{
return round((microtime(true) - $start) * 1000, 3);
}
public function ping(Request $request)
{
$deny = $this->authorize($request);
if ($deny) return $deny;
return Response::json([
'success' => true,
'ts' => now()->toIso8601String(),
'php' => PHP_VERSION,
]);
}
/**
* Core user seeder, returns ['hashes' => [...], 'ms' => float].
*/
private function _seedUsers(User $actor, int $count, string $type, ?int $parentId, string $prefix): array
{
$parentId = $parentId ?? $actor->id;
$hashed = Hash::make('Perf12345!');
$start = microtime(true);
$created = [];
DB::beginTransaction();
try {
foreach (range(1, $count) as $i) {
$suffix = Str::random(10);
$u = User::create([
'username' => "{$prefix}_{$suffix}",
'name' => "Perf User {$i}",
'mobile_number' => '09' . str_pad((string) random_int(0, 999999999), 9, '0', STR_PAD_LEFT),
'password' => $hashed,
'acct_type' => $type,
'parentuid' => $parentId,
'active' => true,
]);
$created[] = $u->hashkey;
}
DB::commit();
} catch (\Throwable $e) {
DB::rollBack();
throw $e;
}
return ['hashes' => $created, 'ms' => $this->ms($start)];
}
private function _seedStores(User $actor, int $count, string $category, ?int $ownerId, string $prefix): array
{
$start = microtime(true);
$created = [];
DB::beginTransaction();
try {
foreach (range(1, $count) as $i) {
$storeCode = StoreController::generateStoreCode($category);
$s = Store::create([
'storecode' => $storeCode,
'name' => "{$prefix} " . Str::random(8),
'description' => "Synthetic store for perf testing #{$i}",
'address' => 'Perf Lane ' . random_int(1, 9999),
'category' => $category,
'subcategory' => '',
'owner_id' => $ownerId,
'created_by' => $actor->id,
'is_active' => true,
'status' => 'active',
]);
$created[] = $s->hashkey;
}
DB::commit();
} catch (\Throwable $e) {
DB::rollBack();
throw $e;
}
return ['hashes' => $created, 'ms' => $this->ms($start)];
}
private function _seedProducts(User $actor, int $count, ?Store $store, bool $attach, string $prefix): array
{
$start = microtime(true);
$created = [];
DB::beginTransaction();
try {
foreach (range(1, $count) as $i) {
$price = random_int(10, 5000);
$available = random_int(10, 500);
$p = Product::create([
'name' => "{$prefix} " . Str::random(8),
'description' => "Synthetic product for perf testing #{$i}",
'price' => $price,
'unitname' => 'pc',
'available' => $available,
'category' => 'Perf',
'subcategory' => 'Synthetic',
'created_by' => $actor->id,
'is_active' => true,
]);
if ($store && $attach) {
$store->products()->attach($p->id, [
'available' => $available,
'price' => $price,
'description' => $p->description,
'is_active' => true,
]);
}
$created[] = $p->hashkey;
}
DB::commit();
} catch (\Throwable $e) {
DB::rollBack();
throw $e;
}
return ['hashes' => $created, 'ms' => $this->ms($start)];
}
/**
* POST /api/perf/seed/users
* body: { count, type?, parent_hash?, prefix? }
*/
public function seedUsers(Request $request)
{
$deny = $this->authorize($request);
if ($deny) return $deny;
$actor = $this->actingUser();
if (!$actor) return ResponseHelper::returnError('No actor user available', 500);
$count = $this->clampCount($request->input('count', 10));
$type = (string) $request->input('type', UserTypes::USER->value);
$prefix = (string) $request->input('prefix', 'perf');
if (!UserTypes::tryFrom($type)) {
return ResponseHelper::returnError("Invalid user type '{$type}'", 422);
}
$parentId = null;
if ($p = $request->input('parent_hash')) {
$parent = User::where('hashkey', (string) $p)->first();
if ($parent) $parentId = $parent->id;
}
try {
$r = $this->_seedUsers($actor, $count, $type, $parentId, $prefix);
} catch (\Throwable $e) {
return ResponseHelper::returnError($e->getMessage());
}
return Response::json([
'success' => true,
'count' => count($r['hashes']),
'total_ms' => $r['ms'],
'avg_ms' => round($r['ms'] / max(1, count($r['hashes'])), 3),
'sample' => array_slice($r['hashes'], 0, 5),
]);
}
/**
* POST /api/perf/seed/stores
* body: { count, owner_hash?, category?, prefix? }
*/
public function seedStores(Request $request)
{
$deny = $this->authorize($request);
if ($deny) return $deny;
$actor = $this->actingUser();
if (!$actor) return ResponseHelper::returnError('No actor user available', 500);
$count = $this->clampCount($request->input('count', 10));
$category = (string) $request->input('category', 'General');
$prefix = (string) $request->input('prefix', 'PerfStore');
$ownerId = null;
if ($h = $request->input('owner_hash')) {
$owner = User::where('hashkey', (string) $h)->first();
if ($owner) $ownerId = $owner->id;
}
try {
$r = $this->_seedStores($actor, $count, $category, $ownerId, $prefix);
} catch (\Throwable $e) {
return ResponseHelper::returnError($e->getMessage());
}
return Response::json([
'success' => true,
'count' => count($r['hashes']),
'total_ms' => $r['ms'],
'avg_ms' => round($r['ms'] / max(1, count($r['hashes'])), 3),
'sample' => array_slice($r['hashes'], 0, 5),
]);
}
/**
* POST /api/perf/seed/products
* body: { count, target_store_hash?, prefix?, attach_to_store? }
*/
public function seedProducts(Request $request)
{
$deny = $this->authorize($request);
if ($deny) return $deny;
$actor = $this->actingUser();
if (!$actor) return ResponseHelper::returnError('No actor user available', 500);
$count = $this->clampCount($request->input('count', 10));
$prefix = (string) $request->input('prefix', 'PerfProduct');
$attach = (bool) $request->input('attach_to_store', true);
$store = null;
if ($h = $request->input('target_store_hash')) {
$store = Store::where('hashkey', (string) $h)->first();
if (!$store) return ResponseHelper::returnError('Target store not found', 404);
}
try {
$r = $this->_seedProducts($actor, $count, $store, $attach, $prefix);
} catch (\Throwable $e) {
return ResponseHelper::returnError($e->getMessage());
}
return Response::json([
'success' => true,
'count' => count($r['hashes']),
'total_ms' => $r['ms'],
'avg_ms' => round($r['ms'] / max(1, count($r['hashes'])), 3),
'attached_to_store' => $store?->hashkey,
'sample' => array_slice($r['hashes'], 0, 5),
]);
}
/**
* POST /api/perf/seed/batch
* body: { users?, stores?, products?, prefix?, type?, category? }
* Runs all three seeders sequentially with per-phase timings.
*/
public function seedBatch(Request $request)
{
$deny = $this->authorize($request);
if ($deny) return $deny;
$actor = $this->actingUser();
if (!$actor) return ResponseHelper::returnError('No actor user available', 500);
$prefix = (string) $request->input('prefix', 'perf');
$type = (string) $request->input('type', UserTypes::USER->value);
if (!UserTypes::tryFrom($type)) {
return ResponseHelper::returnError("Invalid user type '{$type}'", 422);
}
$category = (string) $request->input('category', 'General');
$users = max(0, (int) $request->input('users', 0));
$stores = max(0, (int) $request->input('stores', 0));
$products = max(0, (int) $request->input('products', 0));
$phases = [];
$totalStart = microtime(true);
try {
if ($users > 0) {
$r = $this->_seedUsers($actor, $this->clampCount($users), $type, null, $prefix);
$phases['users'] = ['count' => count($r['hashes']), 'ms' => $r['ms']];
}
if ($stores > 0) {
$r = $this->_seedStores($actor, $this->clampCount($stores), $category, null, $prefix . 'Store');
$phases['stores'] = ['count' => count($r['hashes']), 'ms' => $r['ms']];
}
if ($products > 0) {
$r = $this->_seedProducts($actor, $this->clampCount($products), null, false, $prefix . 'Product');
$phases['products'] = ['count' => count($r['hashes']), 'ms' => $r['ms']];
}
} catch (\Throwable $e) {
return ResponseHelper::returnError($e->getMessage());
}
return Response::json([
'success' => true,
'total_ms' => $this->ms($totalStart),
'phases' => $phases,
]);
}
/**
* POST /api/perf/pos/simulate
* body: { store_hash, items?, cycles?, complete? }
*
* Runs end-to-end POS cycles entirely server-side: open session, add N
* line items, optionally complete + archive. Returns per-phase timings.
*/
public function simulatePos(Request $request)
{
$deny = $this->authorize($request);
if ($deny) return $deny;
$actor = $this->actingUser();
if (!$actor) return ResponseHelper::returnError('No actor user available', 500);
$storeHash = (string) $request->input('store_hash', '');
if ($storeHash === '') return ResponseHelper::returnError('store_hash is required', 422);
$store = Store::where('hashkey', $storeHash)->first();
if (!$store) return ResponseHelper::returnError('Store not found', 404);
$items = max(1, min(200, (int) $request->input('items', 5)));
$cycles = max(1, min(100, (int) $request->input('cycles', 1)));
$complete = (bool) $request->input('complete', true);
// Pull a pool of products attached to this store; fall back to global
// active products if the store has none yet.
$productIds = DB::table('prd_str')
->where('store_id', $store->id)
->where('is_active', true)
->pluck('product_id')
->toArray();
if (empty($productIds)) {
$productIds = Product::where('is_active', true)
->limit(max(50, $items * 2))
->pluck('id')
->toArray();
}
if (empty($productIds)) {
return ResponseHelper::returnError('No products available to simulate sales', 422);
}
$products = Product::whereIn('id', $productIds)->get(['id', 'hashkey', 'price'])->keyBy('id');
$cycleResults = [];
$totalStart = microtime(true);
for ($c = 1; $c <= $cycles; $c++) {
$cStart = microtime(true);
// 1. Open session
$t = microtime(true);
$session = PosSession::create([
'access_key' => Str::random(32),
'store_id' => $store->id,
'customer_name' => 'Perf Customer ' . $c,
'status' => 'active',
'created_by' => $actor->id,
]);
$openMs = $this->ms($t);
// 2. Add items (raw inserts for speed, mirrors PosController::addItem)
$t = microtime(true);
$now = now();
$rows = [];
$total = 0;
for ($i = 0; $i < $items; $i++) {
$pid = $productIds[array_rand($productIds)];
$product = $products[$pid] ?? null;
if (!$product) continue;
$qty = random_int(1, 5);
$price = (int) $product->price;
$line = $price * $qty;
$total += $line;
$rows[] = [
'pos_session_id' => $session->id,
'product_id' => $pid,
'quantity' => $qty,
'price_at_sale' => $price,
'total_price' => $line,
'hashkey' => Str::uuid()->toString() . Str::random(100),
'created_at' => $now,
'updated_at' => $now,
'created_by' => $actor->id,
];
}
if (!empty($rows)) {
DB::table('pos_transactions')->insert($rows);
}
$addMs = $this->ms($t);
// 3. Optionally complete the session
$completeMs = null;
if ($complete) {
$t = microtime(true);
DB::table('pos_sessions')->where('id', $session->id)->update([
'total_amount' => $total,
'received_amount' => $total,
'change_amount' => 0,
'status' => 'completed',
'payment_method' => 'cash',
'updated_at' => $now,
'updated_by' => $actor->id,
]);
$completeMs = $this->ms($t);
}
$cycleResults[] = [
'session_hash' => $session->hashkey,
'items' => count($rows),
'total' => $total,
'open_ms' => $openMs,
'add_items_ms' => $addMs,
'complete_ms' => $completeMs,
'cycle_ms' => $this->ms($cStart),
];
}
$totalMs = $this->ms($totalStart);
$cycleMsValues = array_column($cycleResults, 'cycle_ms');
$avg = $cycleMsValues ? array_sum($cycleMsValues) / count($cycleMsValues) : 0.0;
return Response::json([
'success' => true,
'store_hash' => $store->hashkey,
'cycles' => $cycles,
'items_per_cycle' => $items,
'total_ms' => $totalMs,
'avg_cycle_ms' => round($avg, 3),
'min_cycle_ms' => $cycleMsValues ? min($cycleMsValues) : 0,
'max_cycle_ms' => $cycleMsValues ? max($cycleMsValues) : 0,
'detail' => $cycleResults,
]);
}
}

View File

@@ -1,774 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Market;
use App\Http\Controllers\Helpers\ResponseHelper;
use App\Models\Market\PosSession;
use App\Models\Market\PosTransaction;
use App\Models\Market\PosSessionArchive;
use App\Models\Market\Product;
use App\Models\Market\Store;
use Hypervel\Http\Request;
use Hypervel\Support\Facades\Auth;
use Hypervel\Support\Facades\Response;
use Hyperf\Stringable\Str;
use App\Models\Market\PosAccessKey;
use App\Models\Market\Customer;
use Hypervel\Support\Facades\DB;
use App\Http\Controllers\Helpers\Permissions\UserPermissions;
use App\Enums\UserActions;
use Hyperf\Coroutine\Coroutine;
use App\Http\Controllers\Helpers\CacheHelper;
class PosController
{
public function startSession(Request $request)
{
$validated = $request->validate([
'store_hash' => 'nullable|string',
'customer_name' => 'nullable|string|max:255',
'access_key' => 'nullable|string',
]);
$store = null;
$accessKeyObj = null;
if ($request->input('access_key')) {
$accessKeyObj = PosAccessKey::where('access_key', $request->input('access_key'))
->where('status', 'active')
->first();
if ($accessKeyObj) {
if ($accessKeyObj->isExpired()) {
$accessKeyObj->status = 'inactive';
$accessKeyObj->save();
return ResponseHelper::returnError('Access key has expired', 401);
}
$store = $accessKeyObj->store;
} elseif (!Auth::check()) {
return ResponseHelper::returnError('Invalid or inactive access key', 401);
}
}
if (!$store && !empty($validated['store_hash'])) {
$store = Store::where('hashkey', $validated['store_hash'])->first();
}
if (!$store) {
return ResponseHelper::returnError('No store found. Please open the POS from a store page or use a valid access key.', 422);
}
// Authorization check
if (!UserPermissions::isUserAllowedAccessToStore(Auth::user(), $store)) {
return ResponseHelper::returnError('You are not authorized to start a POS session for this store.', 403);
}
/** @var PosSession $session */
$session = PosSession::create([
'access_key' => $accessKeyObj ? $accessKeyObj->access_key : Str::random(32),
'store_id' => $store->id,
'customer_name' => $validated['customer_name'] ?? null,
'status' => 'active',
'created_by' => Auth::id(),
]);
if ($accessKeyObj) {
$accessKeyObj->last_used_at = now();
$accessKeyObj->save();
}
$this->archiveSession($session, 'Session initialized');
$this->invalidateSessionCache($session);
return ResponseHelper::returnSuccessResponse([
'hashkey' => $session->hashkey,
'access_key' => $session->access_key,
], $session->hashkey, 'POS Session started');
}
public function getSession(Request $request)
{
$hashkey = ResponseHelper::getTargetHash();
$accessKey = $request->input('access_key');
if (!$hashkey && !$accessKey) {
return ResponseHelper::returnError('No key provided', 400);
}
$session = null;
if ($hashkey) {
$q = $this->getBaseSessionQuery()->where('hashkey', $hashkey);
$session = CacheHelper::get_cache($q);
if (!$session) {
$session = $q->first();
if ($session) {
CacheHelper::set_cache($q, $session);
}
}
// If not a session hash, check if it's a store hash
if (!$session) {
$sq = Store::where('hashkey', $hashkey);
$store = CacheHelper::get_cache($sq);
if (!$store) {
$store = $sq->first();
if ($store) {
CacheHelper::set_cache($sq, $store);
}
}
if ($store) {
$q = $this->getBaseSessionQuery()
->where('store_id', $store->id)
->where('status', 'active')
->orderBy('id', 'desc');
$session = CacheHelper::get_cache($q);
if (!$session) {
$session = $q->first();
if ($session) {
CacheHelper::set_cache($q, $session);
}
}
}
}
}
// If still no session and we have an accessKey, try that (as fallback or primary if no hashkey)
if (!$session && $accessKey) {
$q = $this->getBaseSessionQuery()
->where('access_key', $accessKey)
->orderBy('id', 'desc');
$session = CacheHelper::get_cache($q);
if (!$session) {
$session = $q->first();
if ($session) {
CacheHelper::set_cache($q, $session);
}
}
}
if (!$session) {
return ResponseHelper::returnError('Session not found', 404);
}
// Authorization check
if (!UserPermissions::isUserAllowedAccessToStore(Auth::user(), $session->store_id)) {
return ResponseHelper::returnError('You are not authorized to access this POS session.', 403);
}
// Return the full session with all eager loaded relations
return ResponseHelper::returnSuccessResponse($session, $session->hashkey);
}
private function getBaseSessionQuery()
{
return PosSession::with([
'transactions.product' => function ($q) {
// Only fetch minimal columns needed for the POS to reduce serialization time
$q->select(['id', 'hashkey', 'name', 'price', 'photourl', 'unitname', 'category']);
},
'store'
]);
}
public function addItem(Request $request)
{
$validated = $request->validate([
'session_hash' => 'required|string',
'product_hash' => 'required|string',
'quantity' => 'required|integer|min:1',
'price' => 'nullable|numeric|min:0',
]);
$sq = PosSession::where('hashkey', $validated['session_hash']);
$session = CacheHelper::get_cache($sq);
if (!$session) {
$session = $sq->first();
if ($session) {
CacheHelper::set_cache($sq, $session);
}
}
$pq = Product::select(['id', 'hashkey', 'price', 'is_active', 'name'])->where('hashkey', $validated['product_hash']);
$product = CacheHelper::get_cache($pq);
if (!$product) {
$product = $pq->first();
if ($product) {
CacheHelper::set_cache($pq, $product);
}
}
if (!$session || !$product) {
return ResponseHelper::returnError('Session or Product not found', 404);
}
$sessionNeedsSave = false;
if ($session->status !== 'active') {
$session->status = 'active';
$sessionNeedsSave = true;
}
$price = (int) $product->price;
$isActive = $product->is_active;
if ($session->store_id) {
$psq = DB::table('prd_str')
->where('store_id', $session->store_id)
->where('product_id', $product->id)
->select('price', 'is_active');
$storeProduct = CacheHelper::get_cache($psq);
if (!$storeProduct) {
$storeProduct = $psq->first();
if ($storeProduct) {
CacheHelper::set_cache($psq, $storeProduct, [], 3600); // 1 hour
}
}
if ($storeProduct) {
if (isset($storeProduct->price)) {
$price = (int) $storeProduct->price;
}
if (isset($storeProduct->is_active)) {
$isActive = (bool) $storeProduct->is_active;
}
} else {
return ResponseHelper::returnError('Product not available in this store', 403);
}
}
if (!$isActive) {
return ResponseHelper::returnError('Product is currently inactive in this store', 403);
}
// Use custom price if provided, otherwise use calculated store/product price
if ($request->has('price')) {
$price = (int) $validated['price'];
}
// Update or create the transaction using raw DB for max speed
$existingTx = DB::table('pos_transactions')
->where('pos_session_id', $session->id)
->where('product_id', $product->id)
->first();
if ($existingTx) {
DB::table('pos_transactions')->where('id', $existingTx->id)->update([
'quantity' => $validated['quantity'],
'price_at_sale' => $price,
'total_price' => $price * $validated['quantity'],
'updated_at' => now(),
]);
} else {
DB::table('pos_transactions')->insert([
'pos_session_id' => $session->id,
'product_id' => $product->id,
'quantity' => $validated['quantity'],
'price_at_sale' => $price,
'total_price' => $price * $validated['quantity'],
'hashkey' => \Hyperf\Stringable\Str::uuid()->toString() . \Hyperf\Stringable\Str::random(100),
'created_at' => now(),
'updated_at' => now(),
'created_by' => Auth::id() ?? null,
]);
}
// Load specific columns to be fast, just like in getSession to reduce payload and memory
$session->load([
'transactions.product' => function ($q) {
$q->select(['id', 'hashkey', 'name', 'price', 'photourl', 'unitname', 'category']);
},
'store'
]);
$t_load = microtime(true);
// Update session totals in memory
$total = $session->transactions->where('is_void', false)->sum('total_price');
$updateData = [];
if ($session->total_amount !== (int) $total) {
$session->total_amount = (int) $total;
$updateData['total_amount'] = (int) $total;
}
if ($sessionNeedsSave) {
$updateData['status'] = $session->status;
}
// Use raw DB update to skip ModelSavingListener overhead while making sure we still record who updated it
if (!empty($updateData)) {
$updateData['updated_at'] = now();
$updateData['updated_by'] = Auth::id();
DB::table('pos_sessions')->where('id', $session->id)->update($updateData);
}
$t_db = microtime(true);
// Invalidate all possible session cache keys
$this->invalidateSessionCache($session);
// Archive the session using already loaded transaction data (deferred to background coroutine)
$this->archiveSession($session, 'Item added/updated: ' . $product->name, $session->transactions);
return ResponseHelper::returnSuccessResponse($session, $session->hashkey, 'Item added to session');
}
public function removeItem(Request $request)
{
$validated = $request->validate([
'session_hash' => 'required|string',
'transaction_id' => 'required|integer',
]);
$session = PosSession::where('hashkey', $validated['session_hash'])->first();
if (!$session) {
return ResponseHelper::returnError('Session not found', 404);
}
$transaction = PosTransaction::where('id', $validated['transaction_id'])
->where('pos_session_id', $session->id)
->first();
if ($transaction) {
$transaction->delete();
// Re-calculate and archive efficiently
// Load relations ONCE with only necessary columns
$session->load([
'transactions.product' => function ($q) {
$q->select(['id', 'hashkey', 'name', 'price', 'photourl', 'unitname', 'category']);
},
'store'
]);
$total = $session->transactions->where('is_void', false)->sum('total_price');
$session->total_amount = (int) $total;
DB::table('pos_sessions')->where('id', $session->id)->update([
'total_amount' => (int) $total,
'updated_at' => now(),
'updated_by' => Auth::id(),
]);
// Invalidate all possible session cache keys
$this->invalidateSessionCache($session);
$this->archiveSession($session, 'Item removed', $session->transactions);
} else {
$session->load([
'transactions.product' => function ($q) {
$q->select(['id', 'hashkey', 'name', 'price', 'photourl', 'unitname', 'category']);
},
'store'
]);
}
return ResponseHelper::returnSuccessResponse($session, $session->hashkey, 'Item removed from session');
}
public function completeSession(Request $request)
{
$validated = $request->validate([
'session_hash' => 'required|string',
'received_amount' => 'required|integer|min:0',
'payment_method' => 'required|string',
'payment_field' => 'nullable|string',
'customer_name' => 'nullable|string|max:255',
]);
$session = PosSession::where('hashkey', $validated['session_hash'])->first();
if (!$session) {
return ResponseHelper::returnError('Session not found', 404);
}
$session->received_amount = $validated['received_amount'];
$session->change_amount = $validated['received_amount'] - $session->total_amount;
$session->payment_method = $validated['payment_method'];
$session->payment_details = ['payment_field' => $validated['payment_field']];
if (!empty($validated['customer_name'])) {
$session->customer_name = $validated['customer_name'];
}
$session->status = 'completed';
$session->save();
// Invalidate cache
$this->invalidateSessionCache($session);
if (!empty($validated['customer_name'])) {
$customerName = trim($validated['customer_name']);
$customer = Customer::where('name', $customerName)
->where(function ($q) use ($session) {
$q->where('store_id', $session->store_id)
->orWhereNull('store_id');
})
->first();
if (!$customer) {
Customer::create([
'name' => $customerName,
'store_id' => $session->store_id,
'created_by' => Auth::id(),
]);
} else {
$customer->updated_at = now();
$customer->save();
}
}
$this->archiveSession($session, 'Session completed');
return ResponseHelper::returnSuccessResponse($session, $session->hashkey, 'Transaction completed');
}
public function syncOffline(Request $request)
{
$validated = $request->validate([
'transactions' => 'required|array',
'transactions.*.store_hash' => 'required|string',
'transactions.*.customer_name' => 'nullable|string',
'transactions.*.items' => 'required|array',
'transactions.*.total' => 'required|numeric',
'transactions.*.received' => 'required|numeric',
'transactions.*.method' => 'required|string',
'transactions.*.timestamp' => 'required|string',
'transactions.*.local_id' => 'nullable|integer',
]);
$syncedCount = 0;
$syncedIds = [];
$errors = [];
$affectedStoreIds = [];
foreach ($validated['transactions'] as $txn) {
try {
DB::beginTransaction();
$store = Store::where('hashkey', $txn['store_hash'])->first();
if (!$store) {
throw new \Exception('Store not found for hash: ' . $txn['store_hash']);
}
// Convert ISO 8601 timestamp to MySQL datetime format
$offlineTimestamp = date('Y-m-d H:i:s', strtotime($txn['timestamp']));
// Create the session
$session = new PosSession([
'store_id' => $store->id,
'customer_name' => $txn['customer_name'] ?? null,
'total_amount' => (int) $txn['total'],
'received_amount' => (int) $txn['received'],
'change_amount' => (int) ($txn['received'] - $txn['total']),
'payment_method' => $txn['method'],
'status' => 'completed',
'created_by' => Auth::id(),
'access_key' => 'synced-' . Str::random(32),
'hashkey' => Str::random(32) . '-' . Str::random(100),
]);
// Manually set timestamps to preserve offline time
$session->created_at = $offlineTimestamp;
$session->updated_at = $offlineTimestamp;
$session->save();
// Add Items
foreach ($txn['items'] as $item) {
$product = Product::where('hashkey', $item['product_hashkey'])->first();
if (!$product) continue;
DB::table('pos_transactions')->insert([
'pos_session_id' => $session->id,
'product_id' => $product->id,
'quantity' => $item['quantity'],
'price_at_sale' => (int) $item['price_at_sale'],
'total_price' => (int) ($item['price_at_sale'] * $item['quantity']),
'hashkey' => Str::uuid()->toString() . Str::random(100),
'created_at' => $offlineTimestamp,
'updated_at' => now(),
'created_by' => Auth::id(),
]);
}
// Handle Customer
if (!empty($txn['customer_name'])) {
$customerName = trim($txn['customer_name']);
$customer = Customer::where('name', $customerName)
->where(function ($q) use ($store) {
$q->where('store_id', $store->id)
->orWhereNull('store_id');
})
->first();
if (!$customer) {
Customer::create([
'name' => $customerName,
'store_id' => $store->id,
'created_by' => Auth::id(),
]);
}
}
$this->archiveSession($session, 'Offline synced transaction');
$this->invalidateSessionCache($session);
DB::commit();
$syncedCount++;
if (isset($txn['local_id'])) {
$syncedIds[] = $txn['local_id'];
}
if (!in_array($store->id, $affectedStoreIds)) {
$affectedStoreIds[] = $store->id;
}
} catch (\Exception $e) {
DB::rollBack();
$errors[] = $e->getMessage();
}
}
return ResponseHelper::returnSuccessResponse([
'synced_count' => $syncedCount,
'synced_ids' => $syncedIds,
'errors' => $errors
], 'sync_offline', "Synced $syncedCount transactions");
}
public function voidSession(Request $request)
{
$validated = $request->validate([
'session_hash' => 'required|string',
'remarks' => 'nullable|string',
]);
$session = PosSession::where('hashkey', $validated['session_hash'])->first();
if (!$session) {
return ResponseHelper::returnError('Session not found', 404);
}
if (!UserPermissions::isUserAllowedAccessToStore(Auth::user(), $session->store_id)) {
return ResponseHelper::returnError('You are not authorized to void this POS session.', 403);
}
$session->status = 'voided';
$session->is_void = true;
$session->save();
// Invalidate cache
$this->invalidateSessionCache($session);
$this->archiveSession($session, 'Session voided: ' . ($validated['remarks'] ?? 'No remarks'));
return ResponseHelper::returnSuccessResponse($session, $session->hashkey, 'Transaction voided');
}
public function getPosSessions(Request $request)
{
$validated = $request->validate([
'store_hash' => 'required|string',
'page' => 'nullable|integer|min:1',
'per_page' => 'nullable|integer|min:1|max:100',
]);
$store = Store::where('hashkey', $validated['store_hash'])->first();
if (!$store) {
return ResponseHelper::returnError('Store not found', 404);
}
// Authorization check
if (!UserPermissions::isUserAllowedAccessToStore(Auth::user(), $store)) {
return ResponseHelper::returnError('You are not authorized to view sessions for this store.', 403);
}
$page = (int) ($validated['page'] ?? 1);
$perPage = (int) ($validated['per_page'] ?? 10);
$offset = ($page - 1) * $perPage;
$query = PosSession::with(['transactions.product'])
->where('store_id', $store->id)
->where('status', '!=', 'active')
->orderBy('id', 'desc');
$totalCount = $query->count();
$sessions = $query->limit($perPage)->offset($offset)->get();
// Map sessions to include item count and simplify if needed
$sessions = $sessions->map(function ($session) {
return [
'hashkey' => $session->hashkey,
'status' => $session->status,
'total_amount' => $session->total_amount,
'customer_name' => $session->customer_name,
'payment_method' => $session->payment_method,
'items_count' => $session->transactions->count(),
'created_at' => $session->created_at,
'transactions' => $session->transactions,
];
});
return ResponseHelper::returnSuccessResponse([
'sessions' => $sessions,
'total_count' => $totalCount,
'page' => $page,
'per_page' => $perPage,
], $store->hashkey, 'POS sessions fetched');
}
public function getTodayStats(Request $request)
{
if (!UserPermissions::isActionPermitted(Auth::user()->acct_type, UserActions::ViewPosReports)) {
return ResponseHelper::returnUnauthorized();
}
$date = now()->format('Y-m-d');
$query = PosSession::where('status', 'completed')
->whereDate('created_at', $date);
$storeName = null;
$storePhoto = null;
// If store_hash is provided, filter by store
if ($request->input('store_hash')) {
$store = Store::where('hashkey', $request->input('store_hash'))->first();
if ($store) {
// Authorization check
if (!UserPermissions::isUserAllowedAccessToStore(Auth::user(), $store)) {
return ResponseHelper::returnError('You are not authorized to view reports for this store.', 403);
}
$query->where('store_id', $store->id);
$storeName = $store->name;
$storePhoto = $store->photourl;
}
}
$stats = CacheHelper::get_cache($query);
if ($stats) {
return ResponseHelper::returnSuccessResponse($stats, 'today_stats');
}
$stats = [
'count' => (int) $query->count(),
'total' => (int) $query->sum('total_amount'),
'store_name' => $storeName,
'store_photo' => $storePhoto,
];
CacheHelper::set_cache($query, $stats, [], 300); // 5 minutes
return ResponseHelper::returnSuccessResponse($stats, 'today_stats');
}
public function getCustomers(Request $request)
{
if (!UserPermissions::isActionPermitted(Auth::user()->acct_type, UserActions::ViewCustomers)) {
return ResponseHelper::returnUnauthorized();
}
$queryText = $request->input('query');
$storeHash = $request->input('store_hash');
$customerQuery = Customer::where('is_active', true);
if ($storeHash) {
$store = Store::where('hashkey', $storeHash)->first();
if ($store) {
// Authorization check
if (!UserPermissions::isUserAllowedAccessToStore(Auth::user(), $store)) {
return ResponseHelper::returnError('You are not authorized to view customers for this store.', 403);
}
$customerQuery->where(function ($q) use ($store) {
$q->where('store_id', $store->id)
->orWhereNull('store_id');
});
}
}
if ($queryText) {
$customerQuery->where('name', 'like', '%' . $queryText . '%');
}
$finalQuery = $customerQuery->orderBy('name', 'asc')->limit(10);
$customers = CacheHelper::get_cache($finalQuery);
if (!$customers) {
$customers = $finalQuery->get();
CacheHelper::set_cache($finalQuery, $customers, [], 3600); // 1 hour
}
return ResponseHelper::returnSuccessResponse($customers, 'customer_suggestions');
}
private function updateSessionTotals(PosSession $session)
{
$total = $session->transactions()->where('is_void', false)->sum('total_price');
$session->total_amount = (int) $total;
$session->save();
}
private function archiveSession(PosSession $session, string $remarks = '', $transactions = null)
{
// Serialize all data NOW before spawning coroutine to avoid context issues
$sessionData = $session->toArray();
$sessionId = $session->id;
$sessionHashkey = $session->hashkey;
$userId = Auth::id();
if ($transactions !== null) {
$transactionsData = $transactions->toArray();
} else {
$transactionsData = $session->transactions()->with('product')->get()->toArray();
}
// Defer the archive INSERT to a background coroutine so it doesn't block the response
Coroutine::create(function () use ($sessionId, $sessionHashkey, $sessionData, $transactionsData, $userId, $remarks) {
try {
PosSessionArchive::create([
'pos_session_id' => $sessionId,
'hashkey' => $sessionHashkey,
'session_snapshot' => $sessionData,
'transactions_snapshot' => $transactionsData,
'created_by' => $userId,
'remarks' => $remarks,
]);
} catch (\Throwable $e) {
// Silently fail — archive is non-critical audit data
}
});
}
private function invalidateSessionCache(PosSession $session)
{
// 1. Invalidate by hashkey (with relations)
CacheHelper::erase_cache($this->getBaseSessionQuery()->where('hashkey', $session->hashkey));
// 1b. Invalidate simple hashkey lookup (used in addItem)
CacheHelper::erase_cache(PosSession::where('hashkey', $session->hashkey));
// 2. Invalidate by store_id (last active session)
if ($session->store_id) {
CacheHelper::erase_cache($this->getBaseSessionQuery()
->where('store_id', $session->store_id)
->where('status', 'active')
->orderBy('id', 'desc'));
}
// 3. Invalidate by access_key
if ($session->access_key) {
CacheHelper::erase_cache($this->getBaseSessionQuery()
->where('access_key', $session->access_key)
->orderBy('id', 'desc'));
}
// 4. Invalidate today stats cache for the store
if ($session->store_id) {
$date = now()->format('Y-m-d');
$statsQuery = PosSession::where('status', 'completed')
->whereDate('created_at', $date)
->where('store_id', $session->store_id);
CacheHelper::erase_cache($statsQuery);
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,190 +0,0 @@
<?php
namespace App\Http\Controllers\Market;
use App\Http\Controllers\Controller;
use App\Http\Controllers\FilesMainController;
use App\Http\Controllers\Helpers\Permissions\UserPermissions;
use App\Enums\UserActions;
use Hypervel\Http\Request;
use Hypervel\Support\Facades\Auth;
class ProductPhotoSearchController extends Controller
{
private const DDG_BROWSER_HEADERS = [
'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36',
'Accept-Language: en-US,en;q=0.9',
'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'Referer: https://duckduckgo.com/',
];
// Step 1: fetch the DDG search page to extract the vqd session token.
// DDG requires this token to serve the image JSON endpoint.
private static function getDdgVqd(string $query): ?string
{
$url = 'https://duckduckgo.com/?q=' . urlencode($query) . '&iax=images&ia=images';
$ctx = stream_context_create(['http' => [
'method' => 'GET',
'header' => implode("\r\n", self::DDG_BROWSER_HEADERS),
'timeout' => 10,
]]);
$html = @file_get_contents($url, false, $ctx);
if (!$html) return null;
// The vqd token appears in the page JS in several formats depending on
// DDG's current build. Try the quoted forms first (vqd="4-xxx" /
// vqd='4-xxx' / vqd:"4-xxx"), then fall back to the bare form.
$patterns = [
'/vqd=["\']([0-9a-zA-Z._\-]+)["\']/', // vqd="4-123..." or vqd='4-123...'
'/vqd["\']?\s*[:=]\s*["\']([0-9a-zA-Z._\-]+)["\']/', // vqd:"4-123..."
'/vqd=([0-9a-zA-Z._\-]+)/', // bare vqd=4-123...
];
foreach ($patterns as $pattern) {
if (preg_match($pattern, $html, $m) && !empty($m[1])) {
return $m[1];
}
}
return null;
}
// GET /api/products/photo-search?q=...&page=1
public function search(Request $request)
{
if (!UserPermissions::isActionPermitted(Auth::user()->acct_type, UserActions::SearchStockPhotos)) {
return response()->json(['error' => 'Unauthorized'], 403);
}
$query = trim($request->input('q', ''));
$page = max(1, (int) $request->input('page', 1));
if (!$query) {
return response()->json(['error' => 'Query required'], 422);
}
$vqd = self::getDdgVqd($query);
if (!$vqd) {
return response()->json(['error' => 'Could not reach image search service'], 502);
}
// s = offset; DDG returns ~15 results per call; page 1 = s=0, page 2 = s=15, etc.
$offset = ($page - 1) * 15;
$url = 'https://duckduckgo.com/i.js?' . http_build_query([
'q' => $query,
'vqd' => $vqd,
'o' => 'json',
'p' => '1',
'f' => ',,,',
'l' => 'us-en',
's' => $offset,
]);
$ctx = stream_context_create(['http' => [
'method' => 'GET',
'header' => implode("\r\n", self::DDG_BROWSER_HEADERS),
'timeout' => 10,
]]);
$raw = @file_get_contents($url, false, $ctx);
if ($raw === false) {
return response()->json(['error' => 'Failed to fetch image results'], 502);
}
$data = json_decode($raw, true);
$results = $data['results'] ?? [];
$photos = array_map(fn($r) => [
'id' => md5($r['image']), // stable ID from image URL
'thumb' => $r['thumbnail'], // DDG-proxied small thumb (safe to display)
'src' => $r['image'], // actual source image URL (used for download)
'title' => $r['title'] ?? '',
], array_slice($results, 0, 15));
return response()->json([
'success' => true,
'photos' => $photos,
'page' => $page,
'has_more' => count($results) >= 15,
]);
}
// POST /api/products/photo-download
// body: { src: "https://..." } — the actual source image URL from DDG results
public function download(Request $request)
{
if (!UserPermissions::isActionPermitted(Auth::user()->acct_type, UserActions::DownloadStockPhoto)) {
return response()->json(['error' => 'Unauthorized'], 403);
}
$src = $request->input('src', '');
// SSRF guard: must be http/https and must not target private/loopback IPs
$parsed = parse_url($src);
$scheme = $parsed['scheme'] ?? '';
$host = strtolower($parsed['host'] ?? '');
if (!in_array($scheme, ['http', 'https']) || !$host) {
return response()->json(['error' => 'Invalid URL'], 422);
}
// Block private/loopback ranges
if (preg_match('/^(localhost|127\.|10\.|192\.168\.|172\.(1[6-9]|2\d|3[01])\.|0\.0\.0\.0|::1)/i', $host)) {
return response()->json(['error' => 'Forbidden URL'], 403);
}
$ctx = stream_context_create(['http' => [
'method' => 'GET',
'header' => 'User-Agent: Mozilla/5.0' . "\r\n",
'timeout' => 15,
]]);
$raw = @file_get_contents($src, false, $ctx);
if ($raw === false || strlen($raw) < 500) {
return response()->json(['error' => 'Failed to fetch image'], 502);
}
// Resize to max 1280x720 using PHP GD (bundled — no Intervention Image needed)
$srcImg = @imagecreatefromstring($raw);
if (!$srcImg) {
return response()->json(['error' => 'Invalid image data'], 422);
}
$origW = imagesx($srcImg);
$origH = imagesy($srcImg);
$maxW = 1280;
$maxH = 720;
$ratio = min($maxW / $origW, $maxH / $origH, 1.0); // never upscale
$newW = (int) round($origW * $ratio);
$newH = (int) round($origH * $ratio);
$dstImg = imagescale($srcImg, $newW, $newH, IMG_BILINEAR_FIXED);
imagedestroy($srcImg);
ob_start();
imagejpeg($dstImg, null, 85);
$binary = ob_get_clean();
imagedestroy($dstImg);
// Save via existing pipeline — binary string branch in uploadFileContent handles this
$result = FilesMainController::uploadFileList(
$binary,
'stock-photo',
'stock_photo_' . time() . '.jpg',
'',
[],
'ProductMarket',
[],
0,
'image/jpeg'
);
if (!$result || empty($result->hashkey)) {
return response()->json(['error' => 'Save failed'], 500);
}
return response()->json([
'success' => true,
'hashkey' => $result->hashkey,
'url' => $result->resolvedUrl(),
]);
}
}

View File

@@ -1,163 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Market;
use App\Http\Controllers\Helpers\ResponseHelper;
use App\Models\Market\Courier;
use App\Models\Market\Shipment;
use App\Models\Market\Store;
use App\Models\Market\Customer;
use App\Models\GlobalTransaction;
use Hypervel\Http\Request;
use Hypervel\Support\Facades\Auth;
use App\Http\Controllers\Helpers\Permissions\UserPermissions;
use App\Enums\UserActions;
class ShipmentController
{
public function listShipments(Request $request)
{
if (!UserPermissions::isActionPermitted(Auth::user()->acct_type, UserActions::ViewShipments)) {
return ResponseHelper::returnUnauthorized();
}
$user = Auth::user();
$query = Shipment::with(['courier', 'transaction', 'store', 'customer']);
// filter by store if provided
if ($storeHash = $request->input('store_hash')) {
$store = Store::where('hashkey', $storeHash)->first();
if ($store) {
$query->where('store_id', $store->id);
}
}
// if not ultimate/admin, restrict to user's shipments
// (This logic might need adjustment based on how roles are defined)
// For now, let's just list all and allow filtering
$shipments = $query->orderBy('created_at', 'desc')->get();
return response()->json([
'success' => true,
'data' => $shipments
]);
}
public function createNewShipment(Request $request)
{
if (!UserPermissions::isActionPermitted(Auth::user()->acct_type, UserActions::CreateShipment)) {
return ResponseHelper::returnUnauthorized();
}
$user = Auth::user();
$validated = $request->validate([
'transaction_hash' => 'required|string',
'store_hash' => 'nullable|string',
'customer_hash' => 'nullable|string',
'courier_hash' => 'nullable|string',
'origin_address' => 'nullable|string',
'destination_address' => 'nullable|string',
'shipping_fee' => 'nullable|numeric',
'estimated_delivery_date' => 'nullable|date',
]);
$transaction = GlobalTransaction::where('hashkey', $validated['transaction_hash'])->first();
if (!$transaction) {
return ResponseHelper::returnError('Transaction not found', 404);
}
$store = $validated['store_hash'] ? Store::where('hashkey', $validated['store_hash'])->first() : null;
$customer = $validated['customer_hash'] ? Customer::where('hashkey', $validated['customer_hash'])->first() : null;
$courier = $validated['courier_hash'] ? Courier::where('hashkey', $validated['courier_hash'])->first() : null;
$shipment = new Shipment([
'transaction_id' => $transaction->id,
'store_id' => $store?->id,
'customer_id' => $customer?->id,
'courier_id' => $courier?->id,
'origin_address' => $validated['origin_address'] ?? $store?->address,
'destination_address' => $validated['destination_address'] ?? $customer?->address,
'shipping_fee' => $validated['shipping_fee'] ?? 0,
'estimated_delivery_date' => $validated['estimated_delivery_date'],
'status' => 'PENDING',
'created_by' => $user->id,
]);
if ($shipment->save()) {
return ResponseHelper::returnSuccessResponse($shipment, $shipment->hashkey, 'Shipment created successfully');
}
return ResponseHelper::returnError('Failed to create shipment');
}
public function updateShipmentStatus(Request $request)
{
if (!UserPermissions::isActionPermitted(Auth::user()->acct_type, UserActions::UpdateShipmentStatus)) {
return ResponseHelper::returnUnauthorized();
}
$hashkey = $request->input('target');
$status = $request->input('status');
if (!$hashkey || !$status) {
return ResponseHelper::returnIncorrectDetails();
}
$shipment = Shipment::where('hashkey', $hashkey)->first();
if (!$shipment) {
return ResponseHelper::returnError('Shipment not found', 404);
}
$shipment->status = $status;
if ($status === 'DELIVERED') {
$shipment->actual_delivery_date = now();
}
$shipment->save();
return ResponseHelper::returnSuccessResponse($shipment, $shipment->hashkey, 'Shipment status updated');
}
public function listCouriers()
{
if (!UserPermissions::isActionPermitted(Auth::user()->acct_type, UserActions::ViewCouriers)) {
return ResponseHelper::returnUnauthorized();
}
$couriers = Courier::where('is_active', true)->get();
return response()->json([
'success' => true,
'data' => $couriers
]);
}
public function createCourier(Request $request)
{
if (!UserPermissions::isActionPermitted(Auth::user()->acct_type, UserActions::CreateCourier)) {
return ResponseHelper::returnUnauthorized();
}
$validated = $request->validate([
'name' => 'required|string|max:255',
'contact_number' => 'nullable|string',
'type' => 'required|string|in:INTERNAL,EXTERNAL',
]);
$courier = new Courier([
'name' => $validated['name'],
'contact_number' => $validated['contact_number'],
'type' => $validated['type'],
]);
if ($courier->save()) {
return ResponseHelper::returnSuccessResponse($courier, $courier->hashkey, 'Courier created');
}
return ResponseHelper::returnError('Failed to create courier');
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,565 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Market;
use App\Enums\UserActions;
use App\Http\Controllers\Helpers\Permissions\UserPermissions;
use App\Http\Controllers\Helpers\ResponseHelper;
use App\Models\User;
use App\Models\Market\Store;
use App\Models\Market\Product;
use App\Models\GlobalTransaction;
use App\Models\FileContent;
use App\Models\DbBackup;
use Hyperf\Stringable\Str;
use Hypervel\Http\Request;
use Hypervel\Support\Facades\Auth;
use Hypervel\Support\Facades\DB;
use Hypervel\Support\Facades\Redis;
use Hypervel\Support\Facades\Response;
class UltimateController
{
/**
* Common check for Ultimate access.
*/
private function checkAccess()
{
if (!UserPermissions::isActionPermitted(0, UserActions::UltimateConsole)) {
return false;
}
return true;
}
/**
* Get system-wide statistics for the dashboard.
*/
public function getSystemStats()
{
if (!$this->checkAccess()) return ResponseHelper::returnUnauthorized();
$globalMessage = Redis::get('system:global_message');
$redisStatus = ['connected' => false, 'ping_ms' => null, 'used_memory_human' => null, 'version' => null, 'error' => null];
try {
$start = microtime(true);
$pong = Redis::ping();
$redisStatus['ping_ms'] = round((microtime(true) - $start) * 1000, 2);
$redisStatus['connected'] = $pong === true || $pong === 'PONG' || $pong === '+PONG' || (is_string($pong) && stripos($pong, 'PONG') !== false);
$info = Redis::info();
if (is_array($info)) {
$flat = isset($info['Memory']) && is_array($info['Memory']) ? $info['Memory'] : $info;
$redisStatus['used_memory_human'] = $flat['used_memory_human'] ?? null;
$serverInfo = isset($info['Server']) && is_array($info['Server']) ? $info['Server'] : $info;
$redisStatus['version'] = $serverInfo['redis_version'] ?? null;
}
} catch (\Throwable $e) {
$redisStatus['error'] = $e->getMessage();
}
$stats = [
'users' => User::count(),
'active_users' => User::where('active', true)->count(),
'stores' => Store::count(),
'active_stores' => Store::where('is_active', true)->count(),
'products' => Product::count(),
'transactions' => GlobalTransaction::count(),
'total_balance' => GlobalTransaction::sum('amount'),
'php_version' => PHP_VERSION,
'server_time' => date('Y-m-d H:i:s'),
'maintenance_mode' => Redis::get('system:maintenance_mode') === 'true',
'global_message' => $globalMessage ? json_decode($globalMessage, true) : null,
'logs_count' => DB::table('logs')->count(),
'table_logs_count' => DB::table('table_logs')->count(),
'pos_sessions_count' => DB::table('pos_sessions')->count(),
'cooperatives_count' => DB::table('organizations')->where('type', 'COOPERATIVE')->count(),
'carts_count' => DB::table('carts')->count(),
'farmer_profiles_count' => DB::table('farmer_profiles')->count(),
'redis' => $redisStatus,
];
return Response::json(['success' => true, 'data' => $stats]);
}
/**
* Execute a raw SQL query.
*/
public function runQuery(Request $request)
{
if (Auth::user()->acct_type !== \App\Enums\UserTypes::ULTIMATE || !UserPermissions::isActionPermitted(0, UserActions::UltimateQuery)) {
return ResponseHelper::returnUnauthorized();
}
$query = $request->input('query');
if (empty($query)) return ResponseHelper::returnError('Query cannot be empty');
try {
$queryLower = strtolower(trim($query));
if (str_starts_with($queryLower, 'select') || str_starts_with($queryLower, 'show') || str_starts_with($queryLower, 'describe')) {
$results = DB::select($query);
return Response::json(['success' => true, 'data' => $results]);
} else {
$affected = DB::statement($query);
return Response::json(['success' => true, 'affected' => $affected]);
}
} catch (\Throwable $th) {
return ResponseHelper::returnError($th->getMessage());
}
}
/**
* Toggle maintenance mode system-wide.
*/
public function toggleMaintenance(Request $request)
{
if (!$this->checkAccess() || !UserPermissions::isActionPermitted(0, UserActions::UltimateMaintenance)) {
return ResponseHelper::returnUnauthorized();
}
$enabled = (bool) $request->input('enabled');
Redis::set('system:maintenance_mode', $enabled ? 'true' : 'false');
return Response::json(['success' => true, 'maintenance_mode' => $enabled]);
}
/**
* Send a global message / broadcast.
*/
public function sendGlobalMessage(Request $request)
{
if (!$this->checkAccess() || !UserPermissions::isActionPermitted(0, UserActions::UltimateGlobalMessage)) {
return ResponseHelper::returnUnauthorized();
}
$message = $request->input('message');
$type = $request->input('type', 'info'); // info, success, warning, danger
if (empty($message)) {
Redis::del('system:global_message');
return Response::json(['success' => true, 'message' => 'Global message cleared']);
}
Redis::set('system:global_message', json_encode([
'text' => $message,
'type' => $type,
'timestamp' => time()
]));
return Response::json(['success' => true]);
}
/**
* Flush / Truncate specific tables.
*/
public function flushData(Request $request)
{
if (!$this->checkAccess() || !UserPermissions::isActionPermitted(0, UserActions::UltimateFlush)) {
return ResponseHelper::returnUnauthorized();
}
$target = $request->input('target');
try {
$affected = 0;
switch ($target) {
case 'transactions':
$affected = GlobalTransaction::count();
DB::statement('SET FOREIGN_KEY_CHECKS = 0');
GlobalTransaction::truncate();
DB::statement('SET FOREIGN_KEY_CHECKS = 1');
break;
case 'pos_sessions':
$affected = DB::table('pos_sessions')->count();
DB::table('pos_sessions')->truncate();
break;
case 'cache':
Redis::flushDB();
break;
case 'stores':
$affected = DB::table('str')->count();
DB::statement('SET FOREIGN_KEY_CHECKS = 0');
DB::table('prd_str')->truncate();
DB::table('store_managers')->truncate();
DB::table('str')->truncate();
DB::statement('SET FOREIGN_KEY_CHECKS = 1');
break;
case 'products':
$affected = DB::table('prd_items')->count();
DB::statement('SET FOREIGN_KEY_CHECKS = 0');
DB::table('prd_str')->truncate();
DB::table('prd_items')->truncate();
DB::statement('SET FOREIGN_KEY_CHECKS = 1');
break;
case 'cooperatives':
$affected = DB::table('organizations')->where('type', 'COOPERATIVE')->count();
DB::statement('SET FOREIGN_KEY_CHECKS = 0');
DB::table('cooperative_votes')->truncate();
DB::table('cooperative_resolutions')->truncate();
DB::table('cooperative_documents')->truncate();
DB::table('cooperative_members')->truncate();
DB::table('organizations')->where('type', 'COOPERATIVE')->delete();
DB::statement('SET FOREIGN_KEY_CHECKS = 1');
break;
case 'carts':
$affected = DB::table('carts')->count();
DB::statement('SET FOREIGN_KEY_CHECKS = 0');
DB::table('cart_items')->truncate();
DB::table('carts')->truncate();
DB::statement('SET FOREIGN_KEY_CHECKS = 1');
break;
case 'farmer_profiles':
$affected = DB::table('farmer_profiles')->count();
DB::statement('SET FOREIGN_KEY_CHECKS = 0');
DB::table('farmer_profiles')->truncate();
DB::statement('SET FOREIGN_KEY_CHECKS = 1');
break;
default:
return ResponseHelper::returnError('Invalid flush target');
}
return Response::json(['success' => true, 'message' => "Flushed $target successfully", 'affected' => $affected]);
} catch (\Throwable $th) {
return ResponseHelper::returnError($th->getMessage());
}
}
/**
* Trigger a test notification for a specific user.
*/
public function testNotification(Request $request)
{
if (!$this->checkAccess()) return ResponseHelper::returnUnauthorized();
$userHash = $request->input('user_hash');
$user = User::where('hashkey', $userHash)->first();
if (!$user) return ResponseHelper::returnError('User not found');
// Setting exec_command which SSEController picks up to notify client
$user->exec_command = 'toast:success:Test Notification from Ultimate Console: ' . date('H:i:s');
$user->save();
return Response::json(['success' => true]);
}
/**
* Batch management for various entities.
*/
public function batchManage(Request $request)
{
if (!$this->checkAccess() || !UserPermissions::isActionPermitted(0, UserActions::UltimateBatch)) {
return ResponseHelper::returnUnauthorized();
}
$action = $request->input('action');
$ids = $request->input('ids', []);
$data = $request->input('data', []);
if (empty($ids) && !in_array($action, ['cleanup_sessions'])) {
return ResponseHelper::returnError('No IDs provided');
}
try {
switch($action) {
case 'activate_users':
User::whereIn('id', $ids)->update(['active' => true]);
break;
case 'deactivate_users':
User::whereIn('id', $ids)->update(['active' => false]);
break;
case 'cleanup_sessions':
DB::table('pos_sessions')->where('status', 'VOIDED')->delete();
break;
case 'mass_transfer_points':
$amount = (float)($data['amount'] ?? 0);
if ($amount <= 0) return ResponseHelper::returnError('Invalid amount');
foreach ($ids as $id) {
GlobalTransaction::create([
'user_id' => $id,
'amount' => $amount,
'type' => 'REWARD',
'description' => 'Mass points adjustment via Ultimate Console',
'is_active' => true,
]);
}
break;
default:
return ResponseHelper::returnError('Invalid batch action');
}
return Response::json(['success' => true]);
} catch (\Throwable $th) {
return ResponseHelper::returnError($th->getMessage());
}
}
/**
* Run a system command (Artisan wrapper).
*/
public function runCommand(Request $request)
{
if (!$this->checkAccess()) return ResponseHelper::returnUnauthorized();
$command = $request->input('command');
if (empty($command)) return ResponseHelper::returnError('Command cannot be empty');
// Normalize command: strip 'php artisan ' if present
$command = preg_replace('/^php artisan\s+/', '', trim($command));
// Mapping for user-friendly commands
if ($command === 'reset-app all users') {
$command = 'app:reset-users';
}
if ($command === 'db seed') {
$command = 'db:seed';
}
// For security, only allow specific commands
$allowedCommands = [
'cache:clear', 'view:clear', 'config:clear', 'route:clear',
'migrate', 'migrate:rollback', 'migrate:fresh',
'db:seed', 'app:reset-users', 'optimize', 'optimize:clear'
];
$baseCommand = explode(' ', trim($command))[0];
if (!in_array($baseCommand, $allowedCommands)) {
return ResponseHelper::returnError("Command '{$baseCommand}' not allowed for security reasons.");
}
try {
// In Hyperf, running commands from HTTP request context is tricky.
// We'll use shell_exec in this local environment demo as a fallback.
$output = shell_exec("php artisan $command 2>&1");
return Response::json(['success' => true, 'output' => $output]);
} catch (\Throwable $th) {
return ResponseHelper::returnError($th->getMessage());
}
}
/**
* Run `php artisan migrate --force` (non-interactive).
*/
public function runMigrate(Request $request)
{
if (!$this->checkAccess()) return ResponseHelper::returnUnauthorized();
try {
$output = shell_exec('cd ' . escapeshellarg(BASE_PATH) . ' && php artisan migrate --force 2>&1');
return Response::json(['success' => true, 'output' => $output]);
} catch (\Throwable $th) {
return ResponseHelper::returnError($th->getMessage());
}
}
/**
* Download a full database backup.
* Puts system in maintenance mode during the process.
*/
public function downloadBackup()
{
if (!$this->checkAccess()) return ResponseHelper::returnUnauthorized();
// 1. Enable maintenance mode & Notify
Redis::set('system:maintenance_mode', 'true');
Redis::set('system:global_message', json_encode(['text' => 'System backup in progress. Transactions temporarily disabled.', 'type' => 'warning']));
try {
$filename = 'backup_' . date('Y-m-d_H-i-s') . '.sql';
$path = BASE_PATH . '/storage/app/backups/' . $filename;
if (!is_dir(dirname($path))) {
mkdir(dirname($path), 0755, true);
}
$dbHost = env('DB_HOST', '127.0.0.1');
$dbPort = env('DB_PORT', '3306');
$dbName = env('DB_DATABASE', 'bukid');
$dbUser = env('DB_USERNAME', 'root');
$dbPass = env('DB_PASSWORD', '');
$dump = new \Ifsnop\Mysqldump\Mysqldump(
"mysql:host={$dbHost};port={$dbPort};dbname={$dbName}",
$dbUser,
$dbPass,
[
'add-drop-table' => true,
'exclude-tables' => ['db_backups'] // Exclude the backups table
]
);
$dump->start($path);
if (!file_exists($path) || filesize($path) === 0) {
throw new \Exception('Backup file was not created or is empty.');
}
// Compress into 7z Ultra
$sevenZFilename = 'backup_' . date('Y-m-d_H-i-s') . '.7z';
$sevenZPath = BASE_PATH . '/storage/app/backups/' . $sevenZFilename;
// -mx=9 for Ultra compression
$path_escaped = escapeshellarg($path);
$sevenZPath_escaped = escapeshellarg($sevenZPath);
$command = "7z a -t7z -m0=lzma2 -mx=9 {$sevenZPath_escaped} {$path_escaped} 2>&1";
shell_exec($command);
if (!file_exists($sevenZPath)) {
throw new \Exception('Failed to create 7z archive.');
}
// Save to database
$fileContentRaw = file_get_contents($sevenZPath);
$fileHash = hash('sha256', $fileContentRaw);
$fileContent = new FileContent();
$fileContent->filehash = $fileHash;
$fileContent->titlename = $sevenZFilename;
$fileContent->description = 'System database backup';
$fileContent->size_in_bytes = filesize($sevenZPath);
$fileContent->content = base64_encode($fileContentRaw);
$fileContent->mimetype = 'application/x-7z-compressed';
$fileContent->created_by = Auth::id();
$fileContent->updated_by = Auth::id();
$fileContent->save();
$dbBackup = new DbBackup();
$dbBackup->file_content_hashkey = $fileContent->hashkey;
$dbBackup->filename = $sevenZFilename;
$dbBackup->size_in_bytes = filesize($sevenZPath);
$dbBackup->created_by = Auth::id();
$dbBackup->updated_by = Auth::id();
$dbBackup->save();
// Clean up the temporary files from filesystem
@unlink($path);
@unlink($sevenZPath);
// 2. Disable maintenance mode & Clear Notify
Redis::set('system:maintenance_mode', 'false');
Redis::del('system:global_message');
// 3. Return the binary content for download
return Response::make($fileContentRaw, 200, [
'Content-Type' => 'application/x-7z-compressed',
'Content-Disposition' => 'attachment; filename="' . $sevenZFilename . '"',
'Content-Length' => strlen($fileContentRaw),
]);
} catch (\Throwable $th) {
Redis::set('system:maintenance_mode', 'false');
return ResponseHelper::returnError($th->getMessage());
}
}
/**
* Get recently created backups.
*/
public function getBackups()
{
if (!$this->checkAccess()) return ResponseHelper::returnUnauthorized();
$backups = DbBackup::with(['creator'])
->orderBy('created_at', 'desc')
->limit(50)
->get();
return Response::json(['success' => true, 'data' => $backups]);
}
/**
* Rename a specific backup.
*/
public function renameBackup(Request $request)
{
if (!$this->checkAccess()) return ResponseHelper::returnUnauthorized();
$hash = $request->input('hash');
$newName = $request->input('name');
if (empty($newName)) return ResponseHelper::returnError('Name cannot be empty');
$backup = DbBackup::where('hashkey', $hash)->first();
if (!$backup) return ResponseHelper::returnError('Backup not found');
$backup->name = $newName;
$backup->save();
return Response::json(['success' => true]);
}
/**
* Delete a specific backup.
*/
public function deleteBackup(Request $request)
{
if (!$this->checkAccess()) return ResponseHelper::returnUnauthorized();
$hash = $request->input('hash');
$backup = DbBackup::where('hashkey', $hash)->first();
if (!$backup) return ResponseHelper::returnError('Backup not found');
// Delete associated file content
FileContent::where('hashkey', $backup->file_content_hashkey)->delete();
// Delete backup record
$backup->delete();
return Response::json(['success' => true]);
}
/**
* Download a specific backup from the database.
*/
public function downloadBackupByHash(Request $request)
{
if (!$this->checkAccess()) return ResponseHelper::returnUnauthorized();
$hash = $request->input('hash');
$backup = DbBackup::where('hashkey', $hash)->first();
if (!$backup) return ResponseHelper::returnError('Backup not found');
$fileContent = FileContent::where('hashkey', $backup->file_content_hashkey)->first();
if (!$fileContent) return ResponseHelper::returnError('File content not found');
$content = base64_decode($fileContent->content);
return Response::make($content, 200, [
'Content-Type' => $fileContent->mimetype,
'Content-Disposition' => 'attachment; filename="' . $backup->filename . '"',
'Content-Length' => strlen($content),
]);
}
/**
* Get system-wide logs from file and database.
*/
public function getSystemLogs(Request $request)
{
if (!$this->checkAccess()) return ResponseHelper::returnUnauthorized();
$type = $request->input('type', 'database');
if ($type === 'file') {
$logPath = BASE_PATH . '/storage/logs/hypervel.log';
if (!file_exists($logPath)) {
return Response::json(['success' => true, 'data' => 'No file logs found.']);
}
$logs = shell_exec("tail -n 1000 " . escapeshellarg($logPath));
return Response::json(['success' => true, 'data' => $logs]);
}
// Database logs (audit)
if ($type === 'audit') {
$logs = DB::table('table_logs')->orderBy('id', 'desc')->limit(500)->get();
return Response::json(['success' => true, 'data' => $logs]);
}
// Database logs (system)
$logs = DB::table('logs')->orderBy('uid', 'desc')->limit(500)->get();
return Response::json(['success' => true, 'data' => $logs]);
}
}

View File

@@ -1,169 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Market;
use App\Http\Controllers\Helpers\ResponseHelper;
use App\Models\Market\UserInfo;
use App\Models\User;
use Hypervel\Http\Request;
use Hypervel\Support\Facades\Auth;
use App\Http\Controllers\Helpers\Permissions\UserPermissions;
use App\Enums\UserActions;
class UserInfoController
{
public function getUserInfo(Request $request, string $hashkey)
{
$targetUser = User::where('hashkey', $hashkey)->first();
if (!$targetUser) {
return ResponseHelper::returnError('User not found', 404);
}
$currentUser = Auth::user();
if (!$currentUser) {
return ResponseHelper::returnUnauthorized();
}
// Check permission: can view self or has ViewUserInfo permission for others
if ($currentUser->id !== $targetUser->id && !UserPermissions::isActionPermitted($currentUser->acct_type, UserActions::ViewUserInfo)) {
return ResponseHelper::returnUnauthorized();
}
$userInfo = $targetUser->userInfo;
if (!$userInfo) {
// Lazy create if it doesn't exist (should have been backfilled but just in case)
$userInfo = UserInfo::create([
'user_id' => $targetUser->id,
'fullname' => $targetUser->fullname ?? $targetUser->name,
'email' => $targetUser->email,
'mobile' => $targetUser->mobile_number,
'is_active' => true,
]);
}
return response()->json([
'success' => true,
'data' => $userInfo
]);
}
public function updateUserInfo(Request $request, string $hashkey)
{
$targetUser = User::where('hashkey', $hashkey)->first();
if (!$targetUser) {
return ResponseHelper::returnError('User not found', 404);
}
$currentUser = Auth::user();
if (!$currentUser) {
return ResponseHelper::returnUnauthorized();
}
// Check permission: can manage self or has ManageUserInfo permission for others
if ($currentUser->id !== $targetUser->id && !UserPermissions::isActionPermitted($currentUser->acct_type, UserActions::ManageUserInfo)) {
return ResponseHelper::returnUnauthorized();
}
$userInfo = $targetUser->userInfo;
if (!$userInfo) {
$userInfo = new UserInfo(['user_id' => $targetUser->id]);
}
$validated = $request->validate([
'firstname' => 'nullable|string|max:255',
'middlename' => 'nullable|string|max:255',
'lastname' => 'nullable|string|max:255',
'suffix' => 'nullable|string|max:50',
'gender' => 'nullable|string|max:50',
'dob' => 'nullable|date',
'priority_sector' => 'nullable|string|max:255',
'messenger_id' => 'nullable|string|max:255',
'viber_number' => 'nullable|string|max:255',
'tiktok_username' => 'nullable|string|max:255',
'region' => 'nullable|string|max:255',
'province' => 'nullable|string|max:255',
'city' => 'nullable|string|max:255',
'barangay' => 'nullable|string|max:255',
'civil_status' => 'nullable|string|max:100',
'children_count' => 'nullable|integer',
'dependent_count' => 'nullable|integer',
'education_level' => 'nullable|string|max:255',
'course' => 'nullable|string|max:255',
'school' => 'nullable|string|max:255',
'year_last_attended' => 'nullable|string|max:50',
'livelihood_source' => 'nullable|string|max:255',
'last_company' => 'nullable|string|max:255',
'employer_name' => 'nullable|string|max:255',
'last_position' => 'nullable|string|max:255',
'occupation' => 'nullable|string|max:255',
'last_employment_year' => 'nullable|string|max:50',
'monthly_income' => 'nullable|numeric',
'tin' => 'nullable|string|max:100',
'philhealth_id' => 'nullable|string|max:100',
'gov_id' => 'nullable|string|max:100',
'id_type' => 'nullable|string|max:100',
'id_number' => 'nullable|string|max:100',
'beneficiary_type' => 'nullable|string|max:100',
'emergency_contact_name' => 'nullable|string|max:255',
'emergency_contact_address' => 'nullable|string|max:255',
'emergency_contact_phone' => 'nullable|string|max:50',
'emergency_contact_relation' => 'nullable|string|max:100',
'emergency_contact_user_id' => 'nullable|integer',
'fullname' => 'nullable|string|max:255',
'landline' => 'nullable|string|max:20',
'mobile' => 'nullable|string|max:20',
'email' => 'nullable|email|max:255',
'alt_email' => 'nullable|email|max:255',
'alt_landline' => 'nullable|string|max:20',
'alt_mobile' => 'nullable|string|max:20',
'facebook_url' => 'nullable|url|max:255',
'bank_details' => 'nullable|array',
'bank_account_no' => 'nullable|string|max:100',
'addresses' => 'nullable|array',
'other_details' => 'nullable|array',
]);
// Logic to automatically populate emergency_contact_user_id if phone matches a registered user
if (!empty($validated['emergency_contact_phone'])) {
$matchedUser = User::where('mobile_number', $validated['emergency_contact_phone'])->first();
if ($matchedUser) {
$validated['emergency_contact_user_id'] = $matchedUser->id;
}
}
$userInfo->fill($validated);
if ($userInfo->save()) {
// Also update core user fields if they match
if (isset($validated['fullname'])) $targetUser->fullname = $validated['fullname'];
if (isset($validated['email'])) $targetUser->email = $validated['email'];
if (isset($validated['mobile'])) $targetUser->mobile_number = $validated['mobile'];
$targetUser->save();
return ResponseHelper::returnSuccessResponse($userInfo, $userInfo->hashkey, 'User info updated');
}
return ResponseHelper::returnError('Failed to update user info');
}
public function searchEmergencyContact(Request $request)
{
$query = $request->input('q');
if (empty($query)) {
return response()->json(['success' => true, 'data' => []]);
}
$users = User::where('name', 'like', "%$query%")
->orWhere('fullname', 'like', "%$query%")
->orWhere('mobile_number', 'like', "%$query%")
->limit(10)
->get(['id', 'name', 'fullname', 'mobile_number', 'hashkey']);
return response()->json([
'success' => true,
'data' => $users
]);
}
}

View File

@@ -1,52 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Market;
use App\Http\Controllers\Helpers\ResponseHelper;
use App\Models\User;
use Hypervel\Http\Request;
use Hypervel\Support\Facades\Auth;
class UserSettingsController
{
/**
* Get the current user's settings.
*/
public function getSettings()
{
$user = Auth::user();
if (!$user) {
return ResponseHelper::returnUnauthorized();
}
return response()->json($user->settings ?? []);
}
/**
* Update the current user's settings.
*/
public function updateSettings(Request $request)
{
$user = Auth::user();
if (!$user) {
return ResponseHelper::returnUnauthorized();
}
$newSettings = $request->all();
$currentSettings = $user->settings ?? [];
// Merge new settings into current settings
$updatedSettings = array_merge($currentSettings, $newSettings);
// Save to database
$user->settings = $updatedSettings;
$user->save();
return response()->json([
'success' => true,
'settings' => $user->settings
]);
}
}

View File

@@ -1,101 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers;
use Hypervel\Http\Request;
use Hypervel\Support\Facades\Response;
use Hypervel\Support\Facades\File;
use Hypervel\Support\Facades\Cache;
class PageMemoryController
{
public static function getPageInMemory(string $viewName, array $data = [], int $ttlSeconds = 100)
{
$dataHash = sha1(json_encode($data));
$cacheKey = 'Cache:Pages:Views:' . $viewName . '-------' . $dataHash;
return Response::html(
cache()->remember($cacheKey, $ttlSeconds, function () use ($viewName, $data) {
return view($viewName, $data)->render();
})
);
}
public static function readAssetInMemory(string $assetFilename, int $ttlSeconds = 10000, $asset_folder = null): ?string
{
$assetFilename = ltrim($assetFilename, '/\\');
$cacheKey = 'Cache:Assets:Static:' . $assetFilename;
if (!$asset_folder) {
$filePath = storage_path('app/cache/static/' . $assetFilename);
} else {
$filePath = $asset_folder . $assetFilename;
}
$data = Cache::get($cacheKey);
if ($data) {
return $data['content'];
}
if (!File::exists($filePath)) {
return null;
}
$fileContent = file_get_contents($filePath);
$mimeType = File::mimeType($filePath) ?? 'application/octet-stream';
$data = [
'content' => $fileContent,
'mime' => $mimeType,
];
Cache::put($cacheKey, $data, $ttlSeconds);
return $fileContent;
}
public static function readPublicAssetInMemory($assetFilename, $ttlSeconds = 10000)
{
$asset_folder = public_path('static/');
return self::readAssetInMemory($assetFilename, $ttlSeconds, $asset_folder);
}
public static function getAssetInMemory(string $assetFilename, int $ttlSeconds = 10000)
{
$assetFilename = ltrim($assetFilename, '/\\');
$cacheKey = 'Cache:Assets:Static:' . $assetFilename;
$filePath = storage_path('app/cache/static/' . $assetFilename);
$data = Cache::get($cacheKey);
if ($data) {
return Response::raw($data['content'])
->withHeader('Content-Type', $data['mime'] ?? 'text/css')
->withHeader('Content-Length', (string) strlen($data['content']))
->withStatus(200);
}
try {
$fileContent = file_get_contents($filePath);
$mimeType = File::mimeType($filePath) ?? 'application/octet-stream';
// Cache content + mime type
$data = [
'content' => $fileContent,
'mime' => $mimeType,
];
Cache::put($cacheKey, $data, $ttlSeconds);
return Response::raw($data['content'])
->withHeader('Content-Type', $data['mime'] ?? 'text/css')
->withHeader('Content-Length', (string) strlen($data['content']))
->withStatus(200);
} catch (\Throwable $th) {
return Response::withStatus(404, 'Not Found ' . $th->getMessage());
}
}
}

View File

@@ -1,219 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Pages;
use Hypervel\Http\Request;
use App\Models\User;
use Hypervel\Support\Facades\Auth;
use Hypervel\Support\Facades\Log;
use Hypervel\Support\Facades\Redis;
use Hypervel\Support\Facades\Response;
use Hypervel\Support\Facades\Hash;
use Hypervel\Support\Facades\Session;
use App\Http\Controllers\Pages\PageController;
class AccountSettingsPageController
{
public $JSCommands = [
'SetDarkMode' => "UISetDarkMode();"
];
public function listDetails()
{
$currentuser = User::findOrFail(Auth::id());
$res = [];
$res['photourl'] = $currentuser->photourl[0] ?? '';
$res['mobile'] = $currentuser->mobile_number ?? '';
$res['name'] = $currentuser->name ?? $currentuser->nickname ?? $currentuser->fullname ?? $currentuser->username ?? '';
$res['fullname'] = $currentuser->fullname ?? $currentuser->name ?? '';
$res['nickname'] = $currentuser->nickname ?? $currentuser->username ?? '';
$res['joined'] = $currentuser->created_at ?? '';
$res['referralcode'] = $currentuser->referralcode ?? '';
$res['email'] = $currentuser->email ?? '';
$res['landline'] = $currentuser->landline ?? '';
$res['hashkey'] = $currentuser->hashkey ?? '';
$res['total_balance'] = $currentuser->total_balance ?? 0;
$res['settings'] = $currentuser->settings ?? [];
return Response::json($res ?: []);
}
public function listSettings()
{
return Response::json(Auth::user()->settings);
}
public function listRunScripts()
{
$scripts = '';
$settings = Auth::user()->settings;
$darkmode = $settings['dark_mode'] ?? $settings['darkmode'] ?? false;
if ($darkmode) {
$scripts .= $this->JSCommands['SetDarkMode'];
}
Response::raw($scripts);
}
public function changepassword(Request $request)
{
$validated = $request->validate([
'current_password' => 'required|string',
'new_password' => 'required|string|min:6',
'new_confirm_password' => 'required|string|same:new_password',
]);
if (!$validated['current_password'] or !$validated['new_password'] or !$validated['new_confirm_password']) {
return Response::json(['message' => 'Enter Old Password, New Password and Password Confirmation.'], 400);
}
try {
$user = User::findOrFail(Auth::id());
} catch (\Throwable $th) {
return Response::json(['message' => 'Internal server error during credit transfer'], 500);
}
$newhash = Hash::make($validated['current_password']);
if (!Hash::check($validated['current_password'], $user->password)) {
return Response::json(['message' => 'Your current password is incorrect.'], 400);
}
$user->password = Hash::make($validated['new_password']);
$user->save();
return Response::json(['message' => 'Password changed successfully'], 200);
}
public function getUserNotes()
{
try {
$user = User::findOrFail(Auth::id());
return Response::json($user->notes, 200);
} catch (\Throwable $th) {
return Response::json(['message' => 'User Not Found!'], 404);
}
}
public function clearUserNotes()
{
try {
$user = User::findOrFail(Auth::id());
$user->notes='';
$user->save();
return Response::json(['success' => true], 200);
} catch (\Throwable $th) {
return Response::json(['message' => 'User Not Found!'], 404);
}
}
public function logoutnow()
{
$sessionId = session()?->getId();
$user = Auth::user();
Log::info('[Logout] Attempting logout for session: ' . $sessionId);
if ($user && isset($user->hashkey)) {
// Signal SSE streams to terminate
Redis::setex("forced_logout:{$user->hashkey}", 60, "1");
Log::info('[Logout] Forced logout signal set for user: ' . $user->hashkey);
}
// Logout from all possible guards
Auth::logout();
try {
if (Auth::guard('jwt')->check()) {
Auth::guard('jwt')->logout();
}
} catch (\Throwable $th) {
// Ignore if JWT guard is not properly configured
}
if (session()) {
session()->flush();
session()->invalidate();
Log::info('[Logout] Session invalidated. New ID: ' . session()->getId());
}
// Forced Redis destruction for THIS session ID (covers multiple prefix formats)
if ($sessionId) {
$prefix = config('cache.prefix', 'bukidbountyapp_cache');
// Try idiomatic Cache forget first (handles prefixing automatically)
\Hypervel\Support\Facades\Cache::forget($sessionId);
// Try manual Redis deletion for both common prefix patterns (with and without colon)
Redis::del(($prefix ? $prefix . ':' : '') . $sessionId);
Redis::del(($prefix ? $prefix : '') . $sessionId);
Log::info('[Logout] Forced Redis/Cache deletion for session: ' . $sessionId);
}
return redirect('/login?logged_out=1');
}
public function updatePhoto(Request $request)
{
if (!$request->hasFile('photo')) {
return Response::json(['success' => false, 'message' => 'No photo uploaded'], 400);
}
try {
$user = User::findOrFail(Auth::id());
$file = $request->file('photo');
$filename = $file->getClientFilename();
// Upload the file using FilesMainController
$result = \App\Http\Controllers\FilesMainController::uploadFileList(
$file,
'User Profile Photo: ' . $user->username,
$filename ?? 'profile_photo.jpg',
'Uploaded by ' . $user->username,
['user_id' => $user->id, 'type' => 'profile_photo'],
'user_photos',
['profile_photo'],
0,
'profile_photo',
);
// If it's a response object, it might be an error response from uploadFileList
if (is_object($result) && method_exists($result, 'getStatusCode')) {
return $result;
}
if ($result && isset($result->hashkey)) {
$photoUrl = $result->resolvedUrl();
// Update user photoUrl array
$user->photourl = [$photoUrl];
$user->save();
return Response::json([
'success' => true,
'message' => 'Photo updated successfully',
'url' => $photoUrl
]);
}
return Response::json(['success' => false, 'message' => 'Failed to process file upload: No result hashkey.'], 500);
} catch (\Throwable $th) {
return Response::json(['success' => false, 'message' => $th->getMessage()], 500);
}
}
}

View File

@@ -1,34 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Pages;
use Hypervel\Http\Request;
use App\Models\User;
use Hypervel\Support\Facades\Auth;
use Hypervel\Support\Facades\Response;
use Hypervel\Support\Facades\Hash;
use Hypervel\Support\Facades\Session;
use App\Http\Controllers\Pages\Core;
class ApplicationController
{
public $JSCommands = [
'SetDarkMode' => "UISetDarkMode();"
];
public function logout()
{
}
}

View File

@@ -1,28 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Pages;
use Hypervel\Http\Request;
use App\Models\User;
use Hypervel\Support\Facades\Auth;
use Hypervel\Support\Facades\Response;
use Hypervel\Support\Facades\Hash;
use Hypervel\Support\Facades\Session;
use App\Http\Controllers\Pages\Core;
class HomeController
{
public function index()
{
}
}

View File

@@ -1,20 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Pages;
use Hypervel\Http\Request;
use Hypervel\Support\Facades\Response;
class PageController
{
public static function PageResponse($data)
{
if ($data) {
return Response::json($data, 200);
} else {
return Response::json(false, 404);
}
}
}

View File

@@ -1,84 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Pages;
use Hypervel\Http\Request;
use Hypervel\Support\Facades\Auth;
use App\Models\User;
use App\Enums\UserTypes;
use App\Http\Controllers\Helpers\Permissions\UserPermissions;
class TransferMyCreditPageController
{
use PageResponses_TransferMyCredit;
public static function TransferMyCredit(string $hashkey, float $amount)
{
$currentuser = Auth::id();
$currentuser = User::findOrFail($currentuser);
if ($amount <= 0) {
throw new \Exception('Invalid amount');
}
try {
$target_user = User::where('hashkey', $hashkey)->first();
$currentUserBalance = $currentuser->total_balance;
if ($currentuser->acct_type !== UserTypes::ULTIMATE && $currentUserBalance < $amount) {
throw new \Exception('Insufficient balance');
}
if (!$target_user) {
throw new \Exception('User not found');
}
if ($target_user->id === $currentuser->id) {
throw new \Exception('You cannot transfer points to yourself');
}
if (!UserPermissions::isDirectCreditTransfertoUserAllowed($hashkey)) {
throw new \Exception('Permission Denied');
}
//Add function to subtract from current user
if ($currentuser->acct_type !== UserTypes::ULTIMATE) {
$currentuser->total_balance -= $amount;
$currentuser->save();
}
$target_user->total_balance += $amount;
$target_user->save();
return true;
} catch (\Throwable $th) {
throw new \Exception( $th->getMessage());
}
}
}
trait PageResponses_TransferMyCredit
{
public function Response_TransferMyCredit(Request $request)
{
$target_user = $request->input('target_user');
$amount = $request->input('amount');
if (!$target_user || !is_string($target_user) || !$amount || !is_numeric($amount)) {
return Response::json(false, 404);
}
try {
$success = self::TransferMyCredit($target_user, (float) $amount);
} catch (\Throwable $th) {
return response()->json($th->getMessage(), 500);
}
if (!$success) {
return response()->json('User not found or transfer failed', 400);
}
return response()->json(true, 200);
}
}

View File

@@ -1,89 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Pages;
use Hypervel\Http\Request;
use App\Models\User;
use Hypervel\Support\Facades\Auth;
use Hypervel\Support\Facades\Response;
use App\Enums\UserTypes;
use App\Http\Controllers\Pages\PageController;
class UserListPageController
{
public static function ListChildren($id)
{
$users = User::findOrFail($id);
$children = $users->getAllDescendants()->map(function ($child) {
$store_hashkey = null;
if ($child->hasRole(['store owner', 'store manager'])) {
$store = \App\Models\Market\Store::where('owner_id', $child->id)
->orWhere('manager_id', $child->id)
->first();
$store_hashkey = $store?->hashkey;
}
return [
'id' => $child->id,
'hashkey' => $child->hashkey,
'mobile_number' => $child->mobile_number,
'total_balance' => $child->total_balance,
'acct_type' => $child->acct_type,
'is_active' => (bool)$child->active,
'name' => $child->name,
'fullname' => $child->fullname,
'nickname' => $child->nickname,
'username' => $child->username,
'store_hashkey' => $store_hashkey,
];
});
return $children;
}
public static function ListChildrenofCurrentUser()
{
if (Auth::user()->acct_type === UserTypes::ULTIMATE) {
return User::all()->map(function ($user) {
$store_hashkey = null;
if ($user->hasRole(['store owner', 'store manager'])) {
$store = \App\Models\Market\Store::where('owner_id', $user->id)
->orWhere('manager_id', $user->id)
->first();
$store_hashkey = $store?->hashkey;
}
return [
'id' => $user->id,
'hashkey' => $user->hashkey,
'mobile_number' => $user->mobile_number,
'total_balance' => $user->total_balance,
'acct_type' => $user->acct_type,
'is_active' => (bool)$user->active,
'name' => $user->name,
'fullname' => $user->fullname,
'nickname' => $user->nickname,
'username' => $user->username,
'store_hashkey' => $store_hashkey,
];
});
} else {
return self::ListChildren(Auth::id());
}
}
public static function Response_ListChildrenofCurrentUser()
{
$currentuser_children = self::ListChildrenofCurrentUser();
return Response::json([
'success' => true,
'users' => $currentuser_children
], 200);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,99 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Payment;
use App\Enums\UserActions;
use App\Http\Controllers\Helpers\Permissions\UserPermissions;
use App\Http\Controllers\Helpers\ResponseHelper;
use App\Http\Controllers\Helpers\QrphDecoder;
use App\Models\SystemSetting;
use Hypervel\Http\Request;
use Hypervel\Support\Facades\Auth;
class QRPHController
{
private function checkAdmin(): bool
{
return UserPermissions::isActionPermitted(Auth::user()->acct_type, UserActions::ManageQrphPaymentCode);
}
/**
* Get the static QR PH code stored in system settings.
*/
public function getQrCode()
{
$code = SystemSetting::getValue('qrph_payment_code');
$image = SystemSetting::getValue('qrph_payment_image_hashkey');
return response()->json([
'success' => true,
'data' => [
'qrph_code' => $code,
'qrph_image_hashkey' => $image,
'has_qr' => !empty($code),
],
]);
}
/**
* Update the static QR PH code (admin only).
*/
public function setQrCode(Request $request)
{
if (!$this->checkAdmin()) return ResponseHelper::returnUnauthorized();
$code = $request->input('qrph_code');
if (empty($code)) return ResponseHelper::returnError('QR PH code is required', 422);
try {
$decoded = QrphDecoder::decode($code);
} catch (\Throwable $e) {
return ResponseHelper::returnError('Invalid QR PH code: ' . $e->getMessage(), 422);
}
SystemSetting::setValue('qrph_payment_code', $code);
if ($hashkey = $request->input('image_hashkey')) {
SystemSetting::setValue('qrph_payment_image_hashkey', $hashkey);
}
return response()->json([
'success' => true,
'data' => $decoded,
'message' => 'QR PH code updated',
]);
}
/**
* Decode a QR PH string (admin utility).
*/
public function decode(Request $request)
{
if (!$this->checkAdmin()) return ResponseHelper::returnUnauthorized();
$code = $request->input('code');
if (empty($code)) return ResponseHelper::returnError('QR PH code is required', 422);
try {
$decoded = QrphDecoder::decode($code);
return response()->json(['success' => true, 'data' => $decoded]);
} catch (\Throwable $e) {
return ResponseHelper::returnError('Decode error: ' . $e->getMessage(), 422);
}
}
/**
* Remove QR PH code from system settings.
*/
public function removeQrCode()
{
if (!$this->checkAdmin()) return ResponseHelper::returnUnauthorized();
SystemSetting::setValue('qrph_payment_code', null);
SystemSetting::setValue('qrph_payment_image_hashkey', null);
return response()->json(['success' => true, 'message' => 'QR PH code removed']);
}
}

View File

@@ -1,60 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Photos;
use App\Models\Market\Product;
use App\Models\Market\Store;
use App\Models\User;
use Hypervel\Http\Request;
class PhotoGallery
{
public function handle(Request $request, string $type)
{
$hash = $request->input('target', false);
// Validate inputs
if (!$type) {
return response()->json(false);
}
if (!$hash || is_numeric($hash)) {
return response()->json(false);
}
$photoUrls = null;
switch ($type) {
// case 'ProductMarket':
// // Assuming you have a helper function RequestPhotos($hash, $type)
// $result = RequestPhotos($hash, $type);
// return response()->json($result);
case 'User':
$photoUrls = User::where('hashkey', $hash)
->value('photourl') ?? false;
break;
case 'StoreMarket':
$photoUrls = Store::where('hashkey', $hash)->value('photourl') ?? false;
break;
case 'ProductMarket':
$photoUrls = Product::where('hashkey', $hash)->value('photourl') ?? false;
break;
default:
return response()->json(false);
}
if (!$photoUrls) {
return response()->json(false);
}
// $decoded = tryjsondecode($photoUrls);
return response()->json($photoUrls);
}
}

View File

@@ -1,41 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Property;
use App\Http\Controllers\AbstractController;
use App\Models\Property\Property;
use App\Models\Property\Referral;
use App\Http\Controllers\Helpers\Permissions\UserPermissions;
use App\Enums\UserActions;
use App\Http\Controllers\Helpers\ResponseHelper;
use Hypervel\Http\Request;
use Hypervel\Support\Facades\Auth;
class PropertyManagementController extends AbstractController
{
public function listProperties(Request $request)
{
if (!UserPermissions::isActionPermitted(Auth::user()->acct_type, UserActions::ViewProperties)) {
return ResponseHelper::returnUnauthorized();
}
$properties = Property::with(['creator'])->where('is_active', true)->get();
return response()->json([
'properties' => $properties,
]);
}
public function listReferrals(Request $request)
{
if (!UserPermissions::isActionPermitted(Auth::user()->acct_type, UserActions::ViewReferrals)) {
return ResponseHelper::returnUnauthorized();
}
$referrals = Referral::with(['property', 'referrer', 'referred', 'creator'])->where('is_active', true)->get();
return response()->json([
'referrals' => $referrals,
]);
}
}

View File

@@ -17,7 +17,7 @@ class PwaManifestController
public function manifest(): ResponseInterface
{
$user = Auth::user();
$isUltimate = $user && ($user->acct_type === UserTypes::ULTIMATE || $user->acct_type === UserTypes::ULTIMATE->value);
$isUltimate = $user && ($user->acct_type === UserTypes::SUPER_ADMIN || $user->acct_type === UserTypes::SUPER_ADMIN->value);
$appName = SystemSettingsHelper::appName();
$appDescription = SystemSettingsHelper::appDescription();

View File

@@ -1,201 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Subscription;
use Hypervel\Http\Request;
use Hypervel\Support\Facades\Auth;
use Hypervel\Support\Facades\Response;
use App\Models\User;
use App\Models\Subscription\SubscriptionPlan;
use App\Models\Subscription\Subscription;
use App\Models\Subscription\SubscriptionInvoice;
use App\Enums\UserTypes;
use Carbon\Carbon;
class SubscriptionController
{
// ── User: list available plans (active only) ───────────────────────────
public function listAvailablePlans()
{
$plans = SubscriptionPlan::where('active', true)
->orderBy('price')
->get()
->map(fn($p) => [
'hashkey' => $p->hashkey,
'name' => $p->name,
'description' => $p->description,
'price' => $p->price,
'duration_days' => $p->duration_days,
'expiry_action' => $p->expiry_action,
]);
return Response::json($plans);
}
// ── User: get my current subscription ─────────────────────────────────
public function mySubscription()
{
$user = User::findOrFail(Auth::id());
$sub = self::getActiveSubscription($user->id);
if (!$sub) {
return Response::json([
'has_subscription' => false,
'balance' => $user->total_balance,
]);
}
return Response::json([
'has_subscription' => true,
'subscription' => self::formatUserSubscription($sub),
'balance' => $user->total_balance,
]);
}
// ── User: pay for a subscription via wallet ────────────────────────────
public function payWithWallet(Request $request)
{
$planHashkey = $request->input('plan_hashkey');
if (!$planHashkey) {
return Response::json('Plan is required.', 422);
}
$plan = SubscriptionPlan::where('hashkey', $planHashkey)
->where('active', true)
->first();
if (!$plan) {
return Response::json('Plan not found or no longer available.', 404);
}
$user = User::findOrFail(Auth::id());
if ($user->total_balance < $plan->price) {
return Response::json('Insufficient wallet balance.', 402);
}
$admin = User::where('acct_type', UserTypes::ULTIMATE->value)->first();
if (!$admin) {
return Response::json('Payment recipient not configured.', 500);
}
try {
// Deduct from user, credit admin
$user->total_balance -= $plan->price;
$user->save();
$admin->total_balance += $plan->price;
$admin->save();
// Create or extend subscription
$now = Carbon::now();
$expiry = $now->copy()->addDays($plan->duration_days);
$existing = self::getActiveSubscription($user->id);
if ($existing) {
// Extend from current expiry if still active, otherwise from now
$base = $existing->expires_at && $existing->expires_at->isFuture()
? $existing->expires_at
: $now;
$expiry = $base->copy()->addDays($plan->duration_days);
$existing->expires_at = $expiry;
$existing->status = 'active';
$existing->payment_method = 'wallet';
$existing->save();
$subscription = $existing;
} else {
$subscription = Subscription::create([
'user_id' => $user->id,
'plan_id' => $plan->id,
'status' => 'active',
'starts_at' => $now,
'expires_at' => $expiry,
'payment_method' => 'wallet',
]);
}
// Record invoice
SubscriptionInvoice::create([
'subscription_id' => $subscription->id,
'user_id' => $user->id,
'amount' => $plan->price,
'status' => 'paid',
'paid_at' => $now,
'payment_method' => 'wallet',
'payment_reference' => null,
'additional_details' => [
'plan_name' => $plan->name,
'plan_hashkey' => $plan->hashkey,
'admin_id' => $admin->id,
],
]);
return Response::json([
'success' => true,
'expires_at' => $expiry,
'balance' => $user->total_balance,
]);
} catch (\Throwable $th) {
return Response::json($th->getMessage(), 500);
}
}
// ── User: my invoice history ───────────────────────────────────────────
public function myInvoices()
{
$invoices = SubscriptionInvoice::where('user_id', Auth::id())
->orderByDesc('created_at')
->get()
->map(fn($inv) => [
'hashkey' => $inv->hashkey,
'amount' => $inv->amount,
'status' => $inv->status,
'payment_method' => $inv->payment_method,
'payment_reference' => $inv->payment_reference,
'paid_at' => $inv->paid_at,
'plan_name' => $inv->additional_details['plan_name'] ?? '',
'created_at' => $inv->created_at,
]);
return Response::json($invoices);
}
// ── Helper: get user's latest active subscription ─────────────────────
private static function getActiveSubscription(int $userId): ?Subscription
{
return Subscription::where('user_id', $userId)
->where('status', 'active')
->where('expires_at', '>', Carbon::now())
->with('plan')
->orderByDesc('expires_at')
->first();
}
private static function formatUserSubscription(Subscription $sub): array
{
$plan = $sub->plan;
$expiresAt = $sub->expires_at;
$daysRemaining = $expiresAt ? (int) now()->diffInDays($expiresAt, false) : 0;
return [
'hashkey' => $sub->hashkey,
'status' => $sub->status,
'starts_at' => $sub->starts_at,
'expires_at' => $expiresAt,
'days_remaining' => max(0, $daysRemaining),
'payment_method' => $sub->payment_method,
'plan' => $plan ? [
'hashkey' => $plan->hashkey,
'name' => $plan->name,
'price' => $plan->price,
'duration_days' => $plan->duration_days,
'expiry_action' => $plan->expiry_action,
] : null,
];
}
}

View File

@@ -1,126 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Subscription;
use Hypervel\Http\Request;
use Hypervel\Support\Facades\Auth;
use Hypervel\Support\Facades\Response;
use App\Models\Subscription\SubscriptionPlan;
use App\Models\Subscription\Subscription;
class SubscriptionPlanController
{
// ── Admin: list all plans ──────────────────────────────────────────────
public function listPlans()
{
$plans = SubscriptionPlan::orderBy('active', 'desc')
->orderBy('price')
->get()
->map(fn($p) => self::formatPlan($p));
return Response::json($plans);
}
// ── Admin: create plan ─────────────────────────────────────────────────
public function createPlan(Request $request)
{
$data = $request->validate([
'name' => 'required|string|max:255',
'description' => 'nullable|string',
'price' => 'required|numeric|min:0',
'duration_days' => 'required|integer|min:1',
'expiry_action' => 'required|in:restrict,warn,auto_deduct',
]);
$plan = SubscriptionPlan::create($data);
return Response::json(self::formatPlan($plan), 201);
}
// ── Admin: update plan ─────────────────────────────────────────────────
public function updatePlan(Request $request)
{
$hashkey = $request->input('hashkey');
$plan = SubscriptionPlan::where('hashkey', $hashkey)->firstOrFail();
$data = $request->validate([
'name' => 'sometimes|string|max:255',
'description' => 'nullable|string',
'price' => 'sometimes|numeric|min:0',
'duration_days' => 'sometimes|integer|min:1',
'expiry_action' => 'sometimes|in:restrict,warn,auto_deduct',
'active' => 'sometimes|boolean',
]);
$plan->fill($data);
$plan->save();
return Response::json(self::formatPlan($plan));
}
// ── Admin: toggle plan active/inactive ────────────────────────────────
public function togglePlan(Request $request)
{
$hashkey = $request->input('hashkey');
$plan = SubscriptionPlan::where('hashkey', $hashkey)->firstOrFail();
$plan->active = !$plan->active;
$plan->save();
return Response::json(['active' => $plan->active]);
}
// ── Admin: list all user subscriptions ────────────────────────────────
public function listAllSubscriptions(Request $request)
{
$status = $request->input('status'); // optional filter
$query = Subscription::with(['user', 'plan'])
->orderByDesc('created_at');
if ($status) {
$query->where('status', $status);
}
$results = $query->get()->map(fn($s) => self::formatSubscription($s));
return Response::json($results);
}
private static function formatPlan(SubscriptionPlan $plan): array
{
return [
'hashkey' => $plan->hashkey,
'name' => $plan->name,
'description' => $plan->description,
'price' => $plan->price,
'duration_days' => $plan->duration_days,
'expiry_action' => $plan->expiry_action,
'active' => $plan->active,
'created_at' => $plan->created_at,
];
}
private static function formatSubscription(Subscription $sub): array
{
return [
'hashkey' => $sub->hashkey,
'user' => [
'hashkey' => $sub->user?->hashkey,
'name' => $sub->user?->name ?? $sub->user?->fullname,
'mobile' => $sub->user?->mobile_number,
],
'plan' => [
'hashkey' => $sub->plan?->hashkey,
'name' => $sub->plan?->name,
'price' => $sub->plan?->price,
'expiry_action' => $sub->plan?->expiry_action,
],
'status' => $sub->status,
'starts_at' => $sub->starts_at,
'expires_at' => $sub->expires_at,
'payment_method' => $sub->payment_method,
];
}
}

View File

@@ -11,9 +11,6 @@ use App\Enums\UserActions;
use App\Models\Chapter;
use App\Models\ChapterMember;
use App\Models\User;
use App\Models\Market\UserInfo;
use App\Models\Market\CooperativeMember;
use App\Models\Market\Organization;
use App\Models\SystemSetting;
use App\Support\IslandGroupHelper;
use App\Support\SystemSettingsHelper;
@@ -41,10 +38,10 @@ class ChapterController
private function isAdminCaller($acctType): bool
{
return in_array($acctType, [
UserTypes::ULTIMATE,
UserTypes::SUPER_OPERATOR,
UserTypes::OPERATOR,
UserTypes::COORDINATOR,
UserTypes::SUPER_ADMIN,
UserTypes::PUNONG_BARANGAY,
UserTypes::KAGAWAD,
UserTypes::SECRETARY,
], true);
}
@@ -566,7 +563,7 @@ class ChapterController
];
// COOP_MEMBER: own chapter + officers only, no children/member lists.
if ($acctType === UserTypes::COOP_MEMBER) {
if ($acctType === UserTypes::RESIDENT) {
return response()->json(['own_chapter' => $ownChapter, 'children' => []]);
}
@@ -810,8 +807,8 @@ class ChapterController
}
// 3. Upgrade acct_type if currently a plain coop member.
if ($member->acct_type === UserTypes::COOP_MEMBER) {
$member->acct_type = UserTypes::COOP_OFFICER;
if ($member->acct_type === UserTypes::RESIDENT) {
$member->acct_type = UserTypes::KAGAWAD;
$member->save();
}
@@ -930,7 +927,7 @@ class ChapterController
$validated = $validator->validated();
$parentUser = User::where('id', $chapter->created_by)->first()
?? User::where('acct_type', UserTypes::COORDINATOR->value)->first()
?? User::where('acct_type', UserTypes::SECRETARY->value)->first()
?? User::orderBy('id')->first();
if (!$parentUser) {
return response()->json(['success' => false, 'message' => 'No valid parent user found'], 500);
@@ -942,7 +939,7 @@ class ChapterController
$user->mobile_number = $validated['mobile_number'];
$user->password = Hash::make($validated['password']);
$user->parentuid = $parentUser->id;
$user->acct_type = UserTypes::COOP_MEMBER;
$user->acct_type = UserTypes::RESIDENT;
$user->active = true;
if ($cooperative) {
$settings = $user->settings ?? [];

View File

@@ -5,10 +5,6 @@ declare(strict_types=1);
namespace App\Http\Controllers\Support;
use App\Models\User;
use App\Models\Market\Store;
use App\Models\Market\Customer;
use App\Models\Market\Product;
use App\Models\Market\PosSession;
use App\Models\SystemSetting;
use Hypervel\Support\Facades\Auth;
use Hypervel\Support\Facades\Redis;

View File

@@ -26,363 +26,61 @@ class VueRouteMap
* - 'allowedUserTypes' (array): List of allowed user types who can view this page
*/
protected static array $routes = [
/*
|--------------------------------------------------------------------------
| Example Usage
|--------------------------------------------------------------------------
|
| '/my-path' => [
| 'component' => 'MyVueComponent',
| 'middlewares' => ['auth'],
| 'name' => 'my.route.name',
| 'loginRequired' => true,
| 'allowedUserTypes' => ['ult', 'operator'],
| ],
*/
// ── Public / Auth
'/' => ['component' => 'Home', 'loginRequired' => false],
'/app' => ['component' => 'Home', 'loginRequired' => false],
'/barangaysystem' => ['component' => 'Home', 'loginRequired' => false],
// Public pages - no login required
'/' => [
'component' => 'Home',
'loginRequired' => false,
],
'/app' => [
'component' => 'Home',
'loginRequired' => false,
],
'/bukidbountyapp' => [
'component' => 'Home',
'loginRequired' => false,
],
// ── Dashboard / Home
'/home' => ['component' => 'Home', 'loginRequired' => true],
'/dashboard' => ['component' => 'Home', 'loginRequired' => true],
// Market pages - public access
'/list-products-market' => [
'component' => 'ListProductsMarket',
'loginRequired' => false,
],
'/list-stores' => [
'component' => 'ListStores',
'loginRequired' => false,
],
'/my-stores' => [
'component' => 'MyStores',
'loginRequired' => true,
'module' => 'stores',
],
'/buy-view-product-market' => [
'component' => 'BuyViewProductMarket',
'loginRequired' => false,
],
'/view-store-market' => [
'component' => 'ViewStoreMarket',
'loginRequired' => false,
],
'/view-all-photos' => [
'component' => 'ViewAllPhotos',
'loginRequired' => false,
],
'/photo-viewer' => [
'component' => 'PhotoViewer',
'loginRequired' => false,
],
'/create-store' => [
'component' => 'CreateStore',
'loginRequired' => true,
'allowedUserTypes' => ['ult', 'super operator', 'operator', 'store owner'],
'module' => 'stores',
],
'/pos' => [
'component' => 'PosMain',
'loginRequired' => false,
'module' => 'pos',
],
// ── Auth
'/accountsettings' => ['component' => 'AccountSettings', 'loginRequired' => true],
// Account settings - requires login
'/account-settings' => [
'component' => 'AccountSettings',
'loginRequired' => true,
],
// ── Announcements
'/manageannouncements' => ['component' => 'ManageAnnouncements', 'loginRequired' => true, 'module' => 'announcements'],
'/create-user' => [
'component' => 'CreateUser',
'loginRequired' => true,
'allowedUserTypes' => ['ult', 'super operator', 'operator', 'coordinator', 'store owner', 'store manager', 'supplier overseer', 'supplier'],
],
// ── System Settings / Admin
'/systemsettings' => ['component' => 'SystemSettings', 'loginRequired' => true],
'/landingpageeditor' => ['component' => 'LandingPageEditor', 'loginRequired' => true],
'/adminconsole' => ['component' => 'AdminConsole', 'loginRequired' => true],
// Administrative & Management pages
'/create-product' => [
'component' => 'CreateProductUltimate',
'loginRequired' => true,
'allowedUserTypes' => ['ult', 'super operator', 'operator', 'store owner'],
'module' => 'products',
],
'/add-products-to-store' => [
'component' => 'AddProductsToStore',
'loginRequired' => true,
'allowedUserTypes' => ['ult', 'super operator', 'operator', 'store owner', 'store manager'],
'module' => 'stores',
],
'/create-product-store-owner' => [
'component' => 'CreateProductStoreOwner',
'loginRequired' => true,
'allowedUserTypes' => ['ult', 'super operator', 'operator', 'store owner', 'store manager'],
'module' => 'products',
],
'/edit-product' => [
'component' => 'EditProductUltimate',
'loginRequired' => true,
'allowedUserTypes' => ['ult', 'super operator', 'operator', 'store owner'],
'module' => 'products',
],
'/edit-store' => [
'component' => 'EditStoreUltimate',
'loginRequired' => true,
'allowedUserTypes' => ['ult', 'super operator', 'operator', 'store owner', 'store manager'],
'module' => 'stores',
],
'/transfer-credit' => [
'component' => 'TransferMyCredit',
'loginRequired' => true,
'module' => 'credits',
],
'/user-list' => [
'component' => 'UserList',
'loginRequired' => true,
'allowedUserTypes' => ['ult', 'super operator', 'operator', 'audit'],
],
'/manage-transactions' => [
'component' => 'ManageGlobalTransactions',
'loginRequired' => true,
'allowedUserTypes' => ['ult', 'super operator', 'operator'],
'module' => 'transactions',
],
'/remove-product' => [
'component' => 'RemoveProductFromStoreAdmin',
'loginRequired' => true,
'allowedUserTypes' => ['ult', 'super operator', 'operator'],
'module' => 'stores',
],
'/assign-product-to-store' => [
'component' => 'AssignProductToStore',
'loginRequired' => true,
'allowedUserTypes' => ['ult', 'super operator', 'operator', 'store owner', 'store manager'],
],
'/manage-products' => [
'component' => 'ManageProductsAdmin',
'loginRequired' => true,
'allowedUserTypes' => ['ult', 'super operator', 'operator', 'store owner', 'store manager'],
'module' => 'products',
],
'/manage-stores' => [
'component' => 'ManageStoresAdmin',
'loginRequired' => true,
'allowedUserTypes' => ['ult', 'super operator', 'operator', 'store owner', 'store manager'],
'module' => 'stores',
],
'/pos-access-keys' => [
'component' => 'PosAccessKeys',
'loginRequired' => true,
'allowedUserTypes' => ['ult', 'super operator', 'operator', 'store owner', 'store manager'],
'module' => 'pos',
],
'/add-transaction' => [
'component' => 'AddTransaction',
'loginRequired' => true,
'allowedUserTypes' => ['ult', 'super operator', 'operator', 'store owner', 'store manager'],
'module' => 'transactions',
],
'/manage-product-admin' => [
'component' => 'ManageProductAdmin',
'loginRequired' => true,
'allowedUserTypes' => ['ult', 'super operator', 'operator', 'store owner', 'store manager'],
'module' => 'products',
],
'/batch-add-products' => [
'component' => 'BatchAddProducts',
'loginRequired' => true,
'allowedUserTypes' => ['ult', 'super operator', 'operator', 'store owner'],
'module' => 'batch',
],
'/batch-add-stores' => [
'component' => 'BatchAddStores',
'loginRequired' => true,
'allowedUserTypes' => ['ult', 'super operator', 'operator'],
'module' => 'batch',
],
'/batch-add-users' => [
'component' => 'BatchAddUsers',
'loginRequired' => true,
'allowedUserTypes' => ['ult', 'super operator', 'operator'],
'module' => 'batch',
],
'/batch-add-cooperatives' => [
'component' => 'BatchAddCooperatives',
'loginRequired' => true,
'allowedUserTypes' => ['ult', 'super operator'],
'module' => 'batch',
],
'/pos-history' => [
'component' => 'PosHistory',
'loginRequired' => true,
'allowedUserTypes' => ['ult', 'super operator', 'operator', 'store owner', 'store manager'],
'module' => 'pos',
],
// ── User Management
'/userlist' => ['component' => 'UserList', 'loginRequired' => true],
'/createuser' => ['component' => 'CreateUser', 'loginRequired' => true],
'/edituser' => ['component' => 'EditUser', 'loginRequired' => true],
'/manageuser' => ['component' => 'ManageUser', 'loginRequired' => true],
'/userregistration' => ['component' => 'UserRegistration', 'loginRequired' => true],
// Logistics & Shipments
// ── Chapter Hierarchy
'/createchapter' => ['component' => 'CreateChapter', 'loginRequired' => true, 'module' => 'chapters'],
'/registerchapter' => ['component' => 'RegisterChapter', 'loginRequired' => true, 'module' => 'chapters'],
'/chapterorgchart' => ['component' => 'ChapterOrgChart', 'loginRequired' => true, 'module' => 'chapters'],
'/assignchapterofficer' => ['component' => 'AssignChapterOfficer', 'loginRequired' => true, 'module' => 'chapters'],
// ── Barangay Residents
'/barangay/manageresidents' => ['component' => 'Barangay.ManageResidents', 'loginRequired' => true, 'module' => 'residents'],
'/barangay/residentprofile' => ['component' => 'Barangay.ResidentProfile', 'loginRequired' => true, 'module' => 'residents'],
// ── Barangay Households
'/barangay/managehouseholds' => ['component' => 'Barangay.ManageHouseholds', 'loginRequired' => true, 'module' => 'households'],
// Property Management
'/list-properties' => [
'component' => 'ListProperties',
'loginRequired' => true,
'allowedUserTypes' => ['ult', 'super operator', 'operator'],
'module' => 'properties',
],
'/list-referrals' => [
'component' => 'ListReferrals',
'loginRequired' => true,
'allowedUserTypes' => ['ult', 'super operator', 'operator'],
'module' => 'properties',
],
// ── Blotters
'/barangay/manageblotters' => ['component' => 'Barangay.ManageBlotters', 'loginRequired' => true, 'module' => 'blotters'],
'/barangay/blotterdetail' => ['component' => 'Barangay.BlotterDetail', 'loginRequired' => true, 'module' => 'blotters'],
// Reports
'/list-reports' => [
'component' => 'ListReports',
'loginRequired' => true,
'allowedUserTypes' => ['ult', 'super operator', 'operator', 'store owner', 'store manager'],
'module' => 'accounting',
],
'/shipment-list' => [
'component' => 'ShipmentList',
'loginRequired' => true,
'allowedUserTypes' => ['ult', 'super operator', 'operator', 'store owner', 'store manager', 'rider', 'audit'],
'module' => 'shipments',
],
'/shipment-detail' => [
'component' => 'ShipmentDetail',
'loginRequired' => true,
'allowedUserTypes' => ['ult', 'super operator', 'operator', 'store owner', 'store manager', 'rider', 'audit'],
'module' => 'shipments',
],
'/farmer-profile-edit' => [
'component' => 'FarmerProfileEdit',
'loginRequired' => true,
'module' => 'farmers',
],
'/verification-dashboard' => [
'component' => 'VerificationDashboard',
'loginRequired' => true,
'allowedUserTypes' => ['ult', 'super operator', 'operator'],
'module' => 'farmers',
],
'/cooperative-list' => [
'component' => 'CooperativeList',
'loginRequired' => true,
'allowedUserTypes' => ['ult', 'super operator', 'operator', 'coordinator', 'coop officer', 'coop member'],
'module' => 'cooperatives',
],
'/chapter-org-chart' => [
'component' => 'ChapterOrgChart',
'loginRequired' => true,
'allowedUserTypes' => ['ult', 'super operator', 'operator', 'coordinator', 'coop officer', 'coop member'],
'module' => 'cooperatives',
],
'/coop-member-search' => [
'component' => 'CoopMemberSearch',
'loginRequired' => true,
'allowedUserTypes' => ['ult', 'super operator', 'operator', 'coordinator', 'coop officer'],
'module' => 'cooperatives',
],
'/create-coop-user' => [
'component' => 'CreateCoopUser',
'loginRequired' => true,
'allowedUserTypes' => ['ult', 'super operator', 'operator', 'coordinator', 'coop officer'],
'module' => 'cooperatives',
],
'/assign-chapter-officer' => [
'component' => 'AssignChapterOfficer',
'loginRequired' => true,
'allowedUserTypes' => ['ult', 'super operator', 'operator', 'coordinator', 'coop officer'],
'module' => 'cooperatives',
],
'/create-chapter' => [
'component' => 'CreateChapter',
'loginRequired' => true,
'allowedUserTypes' => ['ult', 'super operator', 'operator', 'coordinator', 'coop officer'],
'module' => 'cooperatives',
],
'/register-chapter' => [
'component' => 'RegisterChapter',
'loginRequired' => false,
'module' => 'cooperatives',
],
'/create-cooperative' => [
'component' => 'CreateCooperative',
'loginRequired' => true,
'allowedUserTypes' => ['ult', 'super operator', 'coordinator'],
'module' => 'cooperatives',
],
// ── Document Requests
'/barangay/requestdocument' => ['component' => 'Barangay.RequestDocument', 'loginRequired' => true, 'module' => 'certificates'],
'/barangay/managedocumentrequests' => ['component' => 'Barangay.ManageDocumentRequests','loginRequired' => true, 'module' => 'documents'],
'/barangay/documentrequestdetail' => ['component' => 'Barangay.DocumentRequestDetail','loginRequired' => true, 'module' => 'documents'],
'/barangay/managerequesttypes' => ['component' => 'Barangay.ManageRequestTypes', 'loginRequired' => true, 'module' => 'documents'],
'/cooperative-detail' => [
'component' => 'CooperativeDetail',
'loginRequired' => true,
'allowedUserTypes' => ['ult', 'super operator', 'operator', 'coordinator', 'coop officer', 'coop member'],
'module' => 'cooperatives',
],
'/enroll-farmer' => [
'component' => 'EnrollFarmer',
'loginRequired' => true,
'allowedUserTypes' => ['ult', 'super operator', 'operator'],
'module' => 'farmers',
],
'/cooperative-member-register' => [
'component' => 'CooperativeMemberRegister',
'loginRequired' => true,
'module' => 'cooperatives',
],
'/register-coop' => [
'component' => 'RegisterCoop',
'loginRequired' => false,
'module' => 'cooperatives',
],
'/user-registration' => [
'component' => 'UserRegistration',
'loginRequired' => false,
],
'/user-info-edit' => [
'component' => 'UserInfoEdit',
'loginRequired' => true,
],
'/ultimate-console' => [
'component' => 'UltimateConsole',
'loginRequired' => true,
'allowedUserTypes' => ['ult'],
],
'/system-settings' => [
'component' => 'SystemSettings',
'loginRequired' => true,
'allowedUserTypes' => ['ult'],
],
'/landing-page-editor' => [
'component' => 'LandingPageEditor',
'loginRequired' => true,
'allowedUserTypes' => ['ult', 'super operator', 'operator', 'coordinator'],
'module' => 'landing_pages',
],
'/accounting-dashboard' => [
'component' => 'AccountingDashboard',
'loginRequired' => true,
'allowedUserTypes' => ['ult', 'super operator', 'operator', 'store owner', 'store manager'],
'module' => 'accounting',
'store_module' => 'accounting_store',
],
'/manage-accounts' => [
'component' => 'ManageAccounts',
'loginRequired' => true,
'allowedUserTypes' => ['ult', 'super operator', 'operator', 'store owner', 'store manager'],
'module' => 'accounting',
'store_module' => 'accounting_store',
],
// ── Projects
'/barangay/manageprojects' => ['component' => 'Barangay.ManageProjects', 'loginRequired' => true, 'module' => 'projects'],
// ── Budget
'/barangay/budgetledger' => ['component' => 'Barangay.BudgetLedger', 'loginRequired' => true, 'module' => 'budget'],
];
@@ -461,7 +159,7 @@ class VueRouteMap
$disabledPages = \App\Models\SystemSetting::getValue('disabled_pages', []);
if (is_array($disabledPages) && in_array(strtolower((string)$component), array_map('strtolower', $disabledPages))) {
// Ultimate accounts can still access to allow fixing settings
if (!$user || $user->acct_type !== UserTypes::ULTIMATE) {
if (!$user || $user->acct_type !== UserTypes::SUPER_ADMIN) {
return redirect('/');
}
}
@@ -601,7 +299,7 @@ class VueRouteMap
$disabledPages = \App\Models\SystemSetting::getValue('disabled_pages', []);
if (is_array($disabledPages) && in_array(strtolower((string)$vueComponent), array_map('strtolower', $disabledPages))) {
// Ultimate accounts can still access to allow fixing settings
if (!$user || $user->acct_type !== UserTypes::ULTIMATE) {
if (!$user || $user->acct_type !== UserTypes::SUPER_ADMIN) {
return redirect('/');
}
}

View File

@@ -1,78 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers;
use App\Enums\UserTypes;
use App\Http\Controllers\Helpers\Permissions\UserPermissions;
use Hypervel\Http\Request;
use App\Models\User;
use App\Enums\UserActions;
use App\Traits\Roles;
use Hypervel\Support\Facades\Hash;
use Hypervel\Support\Facades\Validator;
class UserCreateController
{
public function createUser(UserTypes $acct_type, Request $request)
{
// Step 1: Check if the current authenticated user has the permission to create a user
$userType = auth()->user()->acct_type; // Assuming you're using the `acct_type` field for the current user's type
if (!UserPermissions::isActionPermitted($acct_type, UserActions::CreateUser)) {
return response()->json(['error' => 'Permission denied'], 403);
}
// Step 2: Validate incoming request data
$validator = Validator::make($request->all(), [
'name' => 'required|string|max:255',
'email' => 'required|email|unique:users,email',
'mobile_number' => 'required|string|max:15',
'password' => 'required|string|min:8',
'username' => 'nullable|string|unique:users,username',
// Add any other validation rules needed
]);
if ($validator->fails()) {
return response()->json(['errors' => $validator->errors()], 422);
}
if ($acct_type instanceof UserTypes) {
$acct_type = $acct_type->value;
}
if (!is_string($acct_type) || !$acct_type) {
}
// Step 3: Create the new user
$user = User::create([
'name' => $request->input('name'),
'email' => $request->input('email'),
'mobile_number' => $request->input('mobile_number'),
'password' => Hash::make($request->input('password')),
'acct_type' => $acct_type,
'username' => $request->input('username'),
'created_by' => auth()->user()->id, // Currently authenticated user
// Add any other fields as needed
]);
// Step 4: Handle user-specific logic based on their `acct_type`
$this->handleUserTypeSpecificLogic($acct_type, $user);
return response()->json([
'message' => 'User created successfully',
'user' => $user
], 201);
}
}

View File

@@ -1,272 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\UserManagement;
use App\Enums\UserActions;
use App\Http\Controllers\Helpers\Permissions\UserPermissions;
use Hypervel\Http\Request;
use App\Enums\UserTypes;
use Hypervel\Support\Facades\Hash;
use Hypervel\Support\Facades\Response;
use App\Models\User;
class CreateUserControllerUltimate
{
public function listAllUserTypesforSelectHTML()
{
$currentUser = \Hypervel\Support\Facades\Auth::user();
if (!$currentUser) {
return Response::json([], 200);
}
if (!UserPermissions::isActionPermitted($currentUser->acct_type, UserActions::ViewAllUserTypes)) {
return Response::json(['error' => 'Unauthorized'], 401);
}
$currentUserType = $currentUser->acct_type;
$allowedTypes = \App\Http\Controllers\Helpers\Permissions\UserTypeService::getAllowedUserTypes($currentUserType);
$formatted = [];
foreach ($allowedTypes as $case) {
$label = str_replace('_', ' ', ucwords(strtolower($case->name)));
$formatted[] = [$case->value, $label];
}
return Response::json($formatted);
}
public static function listAllUsersforParentSelectHTML(Request $request, $dataResult = false)
{
$currentUser = \Hypervel\Support\Facades\Auth::user();
if (!$currentUser) {
return Response::json([], 200);
}
// Ultimate accounts can see all users
if ($currentUser->acct_type === UserTypes::ULTIMATE) {
$allowedIds = null;
} else {
// Only show current user and their descendants (direct or indirect children)
try {
$descendants = $currentUser->getAllDescendants();
$allowedIds = $descendants->pluck('id')->toArray();
$allowedIds[] = $currentUser->id;
} catch (\Throwable $th) {
return Response::json([], 200);
}
}
$excludeUser = $request->input('exclude_user', null);
$typeFilter = $request->input('type', null);
$usersQuery = User::select(['id', 'username', 'name', 'fullname', 'mobile_number', 'hashkey', 'acct_type']);
if ($allowedIds !== null) {
$usersQuery = $usersQuery->whereIn('id', $allowedIds);
}
// Exclude the specified user if provided
if ($excludeUser) {
$usersQuery = $usersQuery->where('hashkey', '!=', $excludeUser);
}
if ($typeFilter) {
$types = is_array($typeFilter) ? $typeFilter : [$typeFilter];
$usersQuery = $usersQuery->whereIn('acct_type', $types);
}
$users = $usersQuery->get();
if (!$dataResult) {
return Response::json($users);
} else {
return $users;
}
}
public function CreateUser(Request $request)
{
$usertypeString = $request->input('type');
if (!is_string($usertypeString) || empty($usertypeString)) {
return Response::json(['error' => 'User type is required'], 400);
}
$usertypeEnum = UserTypes::tryFrom($usertypeString);
if (!$usertypeEnum) {
return Response::json(['error' => 'Invalid User Type'], 400);
}
// Map UserTypes to specialized CreateUser UserActions
$action = match ($usertypeEnum) {
UserTypes::ULTIMATE => UserActions::CreateUserUltimate,
UserTypes::SUPER_OPERATOR => UserActions::CreateUserSuperOperator,
UserTypes::OPERATOR => UserActions::CreateUserOperator,
UserTypes::COORDINATOR => UserActions::CreateUserCoordinator,
UserTypes::SUPPLIER_OVERSEER => UserActions::CreateUserSupplierOverseer,
UserTypes::WHOLESALE_BUYER => UserActions::CreateUserWholesaleBuyer,
UserTypes::SUPPLIER => UserActions::CreateUserSupplier,
UserTypes::STORE_OWNER => UserActions::CreateUserStoreOwner,
UserTypes::STORE_MANAGER => UserActions::CreateUserStoreManager,
UserTypes::USER => UserActions::CreateUserUser,
UserTypes::RIDER => UserActions::CreateUserRider,
UserTypes::POS_TERMINAL => UserActions::CreateUserPOSTerminal,
UserTypes::AUDIT => UserActions::CreateUserAudit,
default => UserActions::CreateUser,
};
$currentUser = \Hypervel\Support\Facades\Auth::user();
$targetParentHash = $request->input('parent');
if (!$currentUser) {
return Response::json(['error' => 'Unauthorized'], 401);
}
$currentUserType = $currentUser->acct_type;
if (!($currentUserType instanceof UserTypes)) {
$currentUserType = UserTypes::tryFrom($currentUserType) ?? UserTypes::PUBLIC;
}
if ($currentUserType !== UserTypes::ULTIMATE) {
// Check the new user's type is in the allowed list for this creator
$allowedTypes = \App\Http\Controllers\Helpers\Permissions\UserTypeService::getAllowedUserTypes($currentUserType);
if (!in_array($usertypeEnum, $allowedTypes)) {
return Response::json(['error' => 'You are not allowed to create this user type.'], 401);
}
// Check that the chosen parent is the current user or a descendant
if ($targetParentHash) {
$isParentSelfOrDescendant = ($currentUser->hashkey === $targetParentHash)
|| UserPermissions::isDescendantOfCurrentUser($targetParentHash);
if (!$isParentSelfOrDescendant) {
return Response::json(['error' => 'Parent user is not in your hierarchy.'], 401);
}
}
}
$mobileRules = ['required', 'string', 'max:20', 'unique:users,mobile_number'];
if (!UserPermissions::isActionPermitted($currentUser->acct_type, UserActions::BypassMobileNumberFormat)) {
$mobileRules[] = 'regex:/^(09|\+639)\d{9}$/';
}
try {
$validated = $request->validate([
'username' => 'required|string|max:255|unique:users,username',
'name' => 'required|string|max:255',
'fullname' => 'nullable|string|max:255',
'mobile_number' => $mobileRules,
'password' => 'required|string|min:6',
'nickname' => 'nullable|string|max:255',
'parent' => 'required|string',
'type' => 'required|string',
]);
} catch (\Hypervel\Validation\ValidationException $e) {
return Response::json(['errors' => $e->errors()], 422);
}
$parentUser = User::where('hashkey', $validated['parent'])->first();
if (!$parentUser) {
return Response::json(['error' => 'Parent user not found'], 404);
}
$parent = $parentUser->id;
$user = new User();
$user->username = $validated['username'];
$user->name = $validated['name'];
$user->fullname = $validated['fullname'] ?? null;
$user->mobile_number = $validated['mobile_number'];
$user->password = Hash::make($validated['password']);
$user->nickname = $validated['nickname'] ?? null;
$user->parentuid = $parent;
$user->acct_type = $validated['type'];
$user->active = true;
$user->save();
return Response::json(['success' => true, 'hashkey' => $user->hashkey, 'message' => 'User created successfully'], 201);
}
public function checkIfUserMobileNumberExists(Request $request)
{
$request->validate([
'mobile_number' => 'required|string',
]);
$mobileNumber = $request->input('mobile_number');
$userExists = User::where('mobile_number', $mobileNumber)->exists();
return Response::json(['exists' => $userExists]);
}
public function checkIfUsernameExists(Request $request)
{
$request->validate([
'username' => 'required|string',
]);
$username = $request->input('username');
$userExists = User::where('username', $username)->exists();
return Response::json(['exists' => $userExists]);
}
public function publicRegisterUser(Request $request)
{
try {
$validated = $request->validate([
'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',
'nickname' => 'nullable|string|max:255',
]);
} catch (\Hypervel\Validation\ValidationException $e) {
return Response::json(['success' => false, 'errors' => $e->errors()], 422);
}
$parent = User::where('acct_type', UserTypes::ULTIMATE->value)->orderBy('id')->first();
if (!$parent) {
$parent = User::where('acct_type', UserTypes::COORDINATOR->value)->orderBy('id')->first();
}
if (!$parent) {
$parent = User::orderBy('id')->first();
}
if (!$parent) {
return Response::json(['success' => false, 'message' => 'No valid parent user found'], 500);
}
$user = new User();
$user->name = $validated['name'];
$user->mobile_number = $validated['mobile_number'];
$user->password = Hash::make($validated['password']);
$user->nickname = $validated['nickname'] ?? null;
$user->parentuid = $parent->id;
$user->acct_type = 'user';
$user->active = true;
$user->save();
return Response::json(['success' => true, 'hashkey' => $user->hashkey, 'message' => 'Account created successfully. Please log in.'], 201);
}
public function publicCheckMobileNumber(Request $request)
{
$request->validate([
'mobile_number' => 'required|string',
]);
return Response::json(['exists' => User::where('mobile_number', $request->input('mobile_number'))->exists()]);
}
}

View File

@@ -1,129 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\UserManagement;
use App\Http\Controllers\Helpers\ResponseHelper;
use App\Http\Controllers\Helpers\Permissions\UserPermissions;
use App\Enums\UserActions;
use App\Models\User;
use App\Models\Market\Organization;
use Hypervel\Http\Request;
use Hypervel\Support\Facades\Auth;
use Hypervel\Support\Str;
class UserAdditionalDetailsController
{
public function getDetails(Request $request)
{
$user = Auth::user();
if (!$user) {
return ResponseHelper::returnUnauthorized();
}
return response()->json([
'success' => true,
'data' => [
'settings' => $user->settings,
'details' => $user->details,
]
]);
}
public function updateCooperatives(Request $request)
{
$user = Auth::user();
if (!$user) {
return ResponseHelper::returnUnauthorized();
}
$cooperativeHash = $request->input('cooperative_hash');
$action = $request->input('action', 'add'); // 'add' or 'remove'
if (!$cooperativeHash) {
return ResponseHelper::returnIncorrectDetails();
}
$settings = $user->settings ?? [];
$cooperatives = $settings['cooperatives'] ?? [];
if ($action === 'add') {
if (!in_array($cooperativeHash, $cooperatives)) {
$cooperatives[] = $cooperativeHash;
}
} else {
$cooperatives = array_values(array_filter($cooperatives, fn($h) => $h !== $cooperativeHash));
}
$settings['cooperatives'] = $cooperatives;
$user->settings = $settings;
if ($user->save()) {
return response()->json([
'success' => true,
'message' => 'Cooperatives updated successfully',
'data' => $cooperatives
]);
}
return ResponseHelper::returnError('Failed to update cooperatives');
}
public function getUserCooperatives(Request $request)
{
$userHash = $request->input('user_hash');
if ($userHash) {
$targetUser = User::where('hashkey', $userHash)->first();
if (!$targetUser) {
return ResponseHelper::returnError('User not found', 404);
}
// Authorization check
if (!UserPermissions::isActionPermitted($targetUser->acct_type, UserActions::ViewUserInfo)) {
return ResponseHelper::returnUnauthorized();
}
$user = $targetUser;
} else {
$user = Auth::user();
}
if (!$user) {
return ResponseHelper::returnUnauthorized();
}
$cooperativeHashes = $user->settings['cooperatives'] ?? [];
if (empty($cooperativeHashes)) {
return response()->json(['success' => true, 'data' => []]);
}
$cooperatives = Organization::whereIn('hashkey', $cooperativeHashes)->get();
return response()->json([
'success' => true,
'data' => $cooperatives
]);
}
public function searchUsersByCooperative(Request $request)
{
if (!UserPermissions::isActionPermitted(Auth::user()->acct_type, UserActions::ViewUserInfo)) {
return ResponseHelper::returnUnauthorized();
}
$cooperativeHash = $request->input('cooperative_hash');
if (!$cooperativeHash) {
return ResponseHelper::returnIncorrectDetails();
}
// Search in the JSON field 'settings' for cooperatives array containing the hash
$users = User::where('settings->cooperatives', 'like', '%' . $cooperativeHash . '%')->get();
return response()->json([
'success' => true,
'data' => $users
]);
}
}

View File

@@ -1,14 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\UserPages;
use Hypervel\Http\Request;
class UltimateUserController
{
public function Home(){
}
}

View File

@@ -257,7 +257,7 @@ class viewHelperController
$viewPathDefault = $viewMap[$pagename]['default'] ?? $viewMap[$pagename]['public'] ?? null;
if (!$viewPath && !$viewPathDefault) {
if (Auth::check() && Auth::user()->acct_type->value === UserTypes::ULTIMATE->value) {
if (Auth::check() && Auth::user()->acct_type->value === UserTypes::SUPER_ADMIN->value) {
return response("View for page '{$pagename}' and user type '{$userType}' not found.", 404);
} else {
return abort(404, 'Page not found.');
@@ -431,7 +431,7 @@ class viewHelperController
}
$user = Auth::user();
return isset($user->acct_type) && $user->acct_type->value === UserTypes::ULTIMATE->value;
return isset($user->acct_type) && $user->acct_type->value === UserTypes::SUPER_ADMIN->value;
}

View File

@@ -25,7 +25,7 @@ class CheckMaintenanceMode
$user = Auth::user();
// Allow Ultimate users to bypass maintenance mode
if (!$user || $user->acct_type !== UserTypes::ULTIMATE) {
if (!$user || $user->acct_type !== UserTypes::SUPER_ADMIN) {
// Return 503 Service Unavailable
return ResponseHelper::returnError('System is currently under maintenance. Transactions are temporarily disabled. Please try again later.', 503);
}

View File

@@ -23,7 +23,7 @@ class EnsureUserIsUltimate extends Middleware
{
$user = Auth::user();
if (!$user || $user->acct_type !== UserTypes::ULTIMATE) {
if (!$user || $user->acct_type !== UserTypes::SUPER_ADMIN) {
throw new UnauthorizedHttpException('', 'Unauthorized: Only ultimate users allowed.');
}

View File

@@ -1,57 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Models\Accounting;
use App\Models\Model;
use App\Models\User;
class Account extends Model
{
protected ?string $table = 'accounts';
protected array $fillable = [
'hashkey',
'parent_id',
'store_id',
'type',
'default_flow',
'name',
'description',
'theme_key',
'theme_account_code',
'is_active',
'created_by',
'updated_by',
];
protected array $casts = [
'is_active' => 'boolean',
];
public function parent()
{
return $this->belongsTo(self::class, 'parent_id');
}
public function children()
{
return $this->hasMany(self::class, 'parent_id');
}
public function creator()
{
return $this->belongsTo(User::class, 'created_by');
}
public function updater()
{
return $this->belongsTo(User::class, 'updated_by');
}
public function transactions()
{
return $this->hasMany(AccountTransaction::class, 'account_id');
}
}

View File

@@ -1,49 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Models\Accounting;
use App\Models\Model;
use App\Models\User;
class AccountTransaction extends Model
{
protected ?string $table = 'account_transactions';
protected array $fillable = [
'hashkey',
'account_id',
'item',
'target_id',
'amount',
'flow',
'notes',
'transaction_date',
'reference',
'additional_details',
'created_by',
'updated_by',
];
protected array $casts = [
'additional_details' => 'json',
'amount' => 'decimal:2',
'transaction_date' => 'datetime',
];
public function account()
{
return $this->belongsTo(Account::class, 'account_id');
}
public function creator()
{
return $this->belongsTo(User::class, 'created_by');
}
public function updater()
{
return $this->belongsTo(User::class, 'updated_by');
}
}

View File

@@ -1,55 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Models\Accounting;
use App\Models\Model;
use App\Models\User;
use App\Models\Market\Organization;
class MemberLedger extends Model
{
protected ?string $table = 'member_ledgers';
protected array $fillable = [
'hashkey',
'user_id',
'organization_id',
'amount',
'transaction_type',
'flow',
'balance_after',
'description',
'reference_id',
'created_by',
'updated_by',
'is_active',
];
protected array $casts = [
'amount' => 'decimal:2',
'balance_after' => 'decimal:2',
'is_active' => 'boolean',
];
public function user()
{
return $this->belongsTo(User::class, 'user_id');
}
public function organization()
{
return $this->belongsTo(Organization::class, 'organization_id');
}
public function creator()
{
return $this->belongsTo(User::class, 'created_by');
}
public function updater()
{
return $this->belongsTo(User::class, 'updated_by');
}
}

View File

@@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
namespace App\Models\Barangay;
use App\Models\Model;
class BarangayBudget extends Model
{
protected ?string $table = 'barangay_budget';
protected array $fillable = [
'hashkey', 'fiscal_year', 'category', 'source',
'amount', 'description', 'date', 'reference', 'encoded_by',
];
protected array $casts = [
'amount' => 'decimal:2',
'date' => 'date',
];
public function encodedBy()
{
return $this->belongsTo(\App\Models\User::class, 'encoded_by');
}
public function scopeIncome($query)
{
return $query->where('category', 'INCOME');
}
public function scopeExpense($query)
{
return $query->where('category', 'EXPENSE');
}
public function scopeByYear($query, int $year)
{
return $query->where('fiscal_year', $year);
}
}

View File

@@ -0,0 +1,44 @@
<?php
declare(strict_types=1);
namespace App\Models\Barangay;
use App\Models\Model;
use Hypervel\Database\Eloquent\SoftDeletes;
class BarangayProject extends Model
{
use SoftDeletes;
protected ?string $table = 'barangay_projects';
protected array $fillable = [
'hashkey', 'project_name', 'description', 'type', 'budget',
'fund_source', 'start_date', 'end_date', 'status',
'implementing_office', 'contractor', 'location',
'beneficiaries_count', 'created_by', 'updated_by',
];
protected array $casts = [
'budget' => 'decimal:2',
'start_date' => 'date',
'end_date' => 'date',
'beneficiaries_count' => 'integer',
];
public function createdBy()
{
return $this->belongsTo(\App\Models\User::class, 'created_by');
}
public function scopeActive($query)
{
return $query->whereNotIn('status', ['CANCELLED', 'COMPLETED']);
}
public function scopeByType($query, string $type)
{
return $query->where('type', $type);
}
}

View File

@@ -0,0 +1,70 @@
<?php
declare(strict_types=1);
namespace App\Models\Barangay;
use App\Models\Model;
use App\Enums\Barangay\BlotterStatus;
use Hypervel\Database\Eloquent\SoftDeletes;
class Blotter extends Model
{
use SoftDeletes;
protected ?string $table = 'barangay_blotters';
protected array $fillable = [
'hashkey', 'blotter_no',
'complainant_user_id', 'complainant_name', 'complainant_contact', 'complainant_address',
'respondent_user_id', 'respondent_name', 'respondent_contact', 'respondent_address',
'incident_type', 'incident_date', 'incident_location', 'narrative',
'status', 'complaint_date', 'filed_by', 'assigned_officer_id',
'resolution', 'settlement_type', 'endorsed_to', 'is_active',
'created_by', 'updated_by',
];
protected array $casts = [
'incident_date' => 'date',
'complaint_date' => 'date',
'is_active' => 'boolean',
'status' => BlotterStatus::class,
];
public function complainant()
{
return $this->belongsTo(\App\Models\User::class, 'complainant_user_id');
}
public function respondent()
{
return $this->belongsTo(\App\Models\User::class, 'respondent_user_id');
}
public function assignedOfficer()
{
return $this->belongsTo(\App\Models\User::class, 'assigned_officer_id');
}
public function hearings()
{
return $this->hasMany(BlotterHearing::class, 'blotter_id');
}
public function nextHearing()
{
return $this->hearings()->where('status', 'SCHEDULED')->orderBy('hearing_date')->first();
}
public function scopeActive($query)
{
return $query->where('is_active', true);
}
public static function generateBlotterNo(): string
{
$year = date('Y');
$count = static::whereYear('created_at', $year)->count() + 1;
return sprintf('BLT-%s-%04d', $year, $count);
}
}

View File

@@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace App\Models\Barangay;
use App\Models\Model;
class BlotterHearing extends Model
{
protected ?string $table = 'barangay_blotter_hearings';
protected array $fillable = [
'blotter_id', 'hearing_date', 'status', 'officer_id',
'notes', 'resolution', 'next_hearing_date',
];
protected array $casts = [
'hearing_date' => 'datetime',
'next_hearing_date' => 'datetime',
];
public function blotter()
{
return $this->belongsTo(Blotter::class, 'blotter_id');
}
public function officer()
{
return $this->belongsTo(\App\Models\User::class, 'officer_id');
}
}

View File

@@ -0,0 +1,78 @@
<?php
declare(strict_types=1);
namespace App\Models\Barangay;
use App\Models\Model;
use App\Enums\Barangay\DocumentStatus;
use App\Enums\Barangay\PaymentStatus;
use Hypervel\Database\Eloquent\SoftDeletes;
class DocumentRequest extends Model
{
use SoftDeletes;
protected ?string $table = 'barangay_document_requests';
protected array $fillable = [
'hashkey', 'request_no', 'resident_user_id', 'request_type_id',
'purpose', 'fee_amount', 'payment_status', 'payment_method',
'payment_ref', 'qrph_code', 'status',
'requested_by', 'processed_by', 'claimed_at', 'notes',
];
protected array $casts = [
'fee_amount' => 'decimal:2',
'status' => DocumentStatus::class,
'payment_status' => PaymentStatus::class,
'claimed_at' => 'datetime',
];
public function requestType()
{
return $this->belongsTo(RequestType::class, 'request_type_id');
}
public function resident()
{
return $this->belongsTo(\App\Models\User::class, 'resident_user_id');
}
public function processedBy()
{
return $this->belongsTo(\App\Models\User::class, 'processed_by');
}
public function requestedBy()
{
return $this->belongsTo(\App\Models\User::class, 'requested_by');
}
public function payments()
{
return $this->hasMany(RequestPayment::class, 'request_id');
}
public function latestPayment()
{
return $this->payments()->latest()->first();
}
public static function generateRequestNo(): string
{
$year = date('Y');
$count = static::whereYear('created_at', $year)->count() + 1;
return sprintf('REQ-%s-%05d', $year, $count);
}
public function scopePending($query)
{
return $query->whereIn('status', [DocumentStatus::DRAFT, DocumentStatus::PENDING_PAYMENT]);
}
public function scopeForProcessing($query)
{
return $query->where('status', DocumentStatus::PAID);
}
}

View File

@@ -0,0 +1,51 @@
<?php
declare(strict_types=1);
namespace App\Models\Barangay;
use App\Models\Model;
use Hypervel\Database\Eloquent\SoftDeletes;
class Household extends Model
{
use SoftDeletes;
protected ?string $table = 'barangay_households';
protected array $fillable = [
'hashkey', 'household_no', 'head_resident_id',
'address', 'purok', 'barangay', 'city', 'province',
'member_count', 'ownership_type', 'monthly_rental',
'has_electricity', 'has_water', 'housing_material',
'is_active', 'created_by', 'updated_by',
];
protected array $casts = [
'monthly_rental' => 'decimal:2',
'has_electricity' => 'boolean',
'has_water' => 'boolean',
'is_active' => 'boolean',
'member_count' => 'integer',
];
public function head()
{
return $this->belongsTo(Resident::class, 'head_resident_id');
}
public function members()
{
return $this->hasMany(HouseholdMember::class, 'household_id');
}
public function activeMembers()
{
return $this->members()->where('is_active', true);
}
public function scopeActive($query)
{
return $query->where('is_active', true);
}
}

View File

@@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace App\Models\Barangay;
use App\Models\Model;
class HouseholdMember extends Model
{
protected ?string $table = 'barangay_household_members';
protected array $fillable = [
'household_id', 'resident_id', 'relationship_to_head', 'is_active',
];
protected array $casts = [
'is_active' => 'boolean',
];
public function household()
{
return $this->belongsTo(Household::class, 'household_id');
}
public function resident()
{
return $this->belongsTo(Resident::class, 'resident_id');
}
}

View File

@@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace App\Models\Barangay;
use App\Models\Model;
class RequestPayment extends Model
{
protected ?string $table = 'barangay_request_payments';
protected array $fillable = [
'request_id', 'amount', 'method', 'reference',
'qrph_raw', 'paid_at', 'verified_by',
];
protected array $casts = [
'amount' => 'decimal:2',
'paid_at' => 'datetime',
];
public function documentRequest()
{
return $this->belongsTo(DocumentRequest::class, 'request_id');
}
public function verifiedBy()
{
return $this->belongsTo(\App\Models\User::class, 'verified_by');
}
}

View File

@@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
namespace App\Models\Barangay;
use App\Models\Model;
class RequestType extends Model
{
protected ?string $table = 'barangay_request_types';
protected array $fillable = [
'name', 'code', 'description', 'base_fee',
'processing_days', 'is_active', 'requires_clearance',
];
protected array $casts = [
'base_fee' => 'decimal:2',
'processing_days' => 'integer',
'is_active' => 'boolean',
'requires_clearance' => 'boolean',
];
public function documentRequests()
{
return $this->hasMany(DocumentRequest::class, 'request_type_id');
}
public function scopeActive($query)
{
return $query->where('is_active', true);
}
}

View File

@@ -0,0 +1,67 @@
<?php
declare(strict_types=1);
namespace App\Models\Barangay;
use App\Models\Model;
use Hypervel\Database\Eloquent\SoftDeletes;
class Resident extends Model
{
use SoftDeletes;
protected ?string $table = 'barangay_residents';
protected array $fillable = [
'hashkey', 'user_id', 'firstname', 'middlename', 'lastname', 'suffix',
'dob', 'birthplace', 'gender', 'civil_status', 'citizenship', 'religion',
'occupation', 'monthly_income', 'blood_type',
'voter_status', 'registered_voter_id', 'voter_precinct', 'head_of_household',
'purok', 'street', 'barangay', 'city', 'province', 'region',
'philhealth_id', 'sss_id', 'gsis_id', 'tin',
'emergency_contact_name', 'emergency_contact_phone', 'emergency_contact_address',
'is_active', 'created_by', 'updated_by',
];
protected array $casts = [
'dob' => 'date',
'monthly_income' => 'decimal:2',
'voter_status' => 'boolean',
'head_of_household' => 'boolean',
'is_active' => 'boolean',
];
public function user()
{
return $this->belongsTo(\App\Models\User::class, 'user_id');
}
public function household()
{
return $this->hasOne(Household::class, 'head_resident_id');
}
public function householdMemberships()
{
return $this->hasMany(HouseholdMember::class, 'resident_id');
}
public function documentRequests()
{
return $this->hasMany(DocumentRequest::class, 'resident_user_id', 'user_id');
}
public function scopeActive($query)
{
return $query->where('is_active', true);
}
public function getFullnameAttribute(): string
{
$parts = array_filter([$this->firstname, $this->middlename, $this->lastname]);
$name = implode(' ', $parts);
if ($this->suffix) $name .= ', ' . $this->suffix;
return $name;
}
}

View File

@@ -4,14 +4,12 @@ declare(strict_types=1);
namespace App\Models;
use App\Models\Market\UserInfo;
class Chapter extends Model
{
protected ?string $table = 'chapters';
protected array $fillable = [
'hashkey', 'name', 'cooperative_id', 'level', 'parent_id', 'location_key',
'hashkey', 'name', 'level', 'parent_id', 'location_key',
'lat', 'lng', 'is_active', 'created_by', 'updated_by',
];
@@ -26,11 +24,6 @@ class Chapter extends Model
return $this->belongsTo(Chapter::class, 'parent_id');
}
public function cooperative()
{
return $this->belongsTo(\App\Models\Market\Organization::class, 'cooperative_id');
}
public function children()
{
return $this->hasMany(Chapter::class, 'parent_id');
@@ -51,16 +44,13 @@ class Chapter extends Model
return $this->activeMembers()->whereNotNull('position');
}
/**
* Find or create a chapter by level + location_key (normalized address field).
*/
public static function findOrCreateByLocation(string $level, string $locationKey, ?int $parentId = null): self
{
$key = strtolower(trim($locationKey));
return static::firstOrCreate(
['level' => $level, 'location_key' => $key],
[
'hashkey' => \Ramsey\Uuid\Uuid::uuid4()->toString(),
'hashkey' => hash('sha256', uniqid((string) now(), true)),
'name' => ucwords(strtolower($locationKey)),
'level' => $level,
'location_key' => $key,
@@ -69,43 +59,4 @@ class Chapter extends Model
]
);
}
/**
* Auto-assign a user to the appropriate chapters based on their UserInfo address.
* Creates chapter records on the fly if they don't exist.
*/
public static function autoAssignUser(int $userId): void
{
$info = UserInfo::where('user_id', $userId)->first();
if (!$info) {
return;
}
$national = static::firstOrCreate(
['level' => 'national', 'location_key' => 'philippines'],
['hashkey' => \Ramsey\Uuid\Uuid::uuid4()->toString(), 'name' => 'Philippines', 'level' => 'national', 'location_key' => 'philippines', 'is_active' => true]
);
ChapterMember::syncAutoAssignment($userId, $national->id);
if ($info->region) {
$region = static::findOrCreateByLocation('region', $info->region, $national->id);
ChapterMember::syncAutoAssignment($userId, $region->id);
if ($info->province) {
$province = static::findOrCreateByLocation('province', $info->province, $region->id);
ChapterMember::syncAutoAssignment($userId, $province->id);
if ($info->city) {
$city = static::findOrCreateByLocation('city', $info->city, $province->id);
ChapterMember::syncAutoAssignment($userId, $city->id);
if ($info->barangay) {
$barangay = static::findOrCreateByLocation('barangay', $info->barangay, $city->id);
ChapterMember::syncAutoAssignment($userId, $barangay->id);
}
}
}
}
}
}

View File

@@ -1,47 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Models\Generic;
use App\Models\Model;
use App\Models\User;
class TableLog extends Model
{
protected ?string $table = 'table_logs';
protected array $casts = [
'original_data' => 'array',
'new_data' => 'array',
];
protected array $fillable = [
'hashkey',
'table_name',
'target_id',
'original_data',
'new_data',
'created_by',
'updated_by',
];
// Auto-merge accessor
public function get_full_new_row(): array
{
return array_merge($this->original_data ?? [], $this->new_data ?? []);
}
public function data(){
return $this->get_full_new_row();
}
public function creator()
{
return $this->belongsTo(User::class, 'created_by');
}
public function updater()
{
return $this->belongsTo(User::class, 'updated_by');
}
}

View File

@@ -1,72 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Models;
use App\Models\Model;
use App\Models\User;
use App\Models\Market\Product;
use App\Models\Market\Store;
use App\Enums\Market\ProductTransactionType;
use App\Enums\Market\TransactionFlow;
class GlobalTransaction extends Model
{
protected ?string $table = 'global_transactions';
protected string $primaryKey = 'id';
public bool $incrementing = true;
protected string $keyType = 'int';
protected array $fillable = [
'hashkey',
'user_id',
'amount',
'type',
'status',
'description',
'product_id',
'store_id',
'flow',
'created_by',
'updated_by',
];
protected array $casts = [
'amount' => 'decimal:2',
'type' => ProductTransactionType::class,
'user_id' => 'integer',
'product_id' => 'integer',
'store_id' => 'integer',
'flow' => TransactionFlow::class,
'created_by' => 'integer',
'updated_by' => 'integer',
];
public function user()
{
return $this->belongsTo(User::class, 'user_id');
}
public function product()
{
return $this->belongsTo(Product::class, 'product_id');
}
public function store()
{
return $this->belongsTo(Store::class, 'store_id');
}
public function creator()
{
return $this->belongsTo(User::class, 'created_by');
}
public function updater()
{
return $this->belongsTo(User::class, 'updated_by');
}
}

View File

@@ -1,35 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Models\Market;
use App\Models\Model;
use App\Models\User;
class Cart extends Model
{
protected ?string $table = 'carts';
protected array $fillable = [
'hashkey',
'user_id',
'is_active',
'created_by',
'updated_by',
];
protected array $casts = [
'is_active' => 'boolean',
];
public function items()
{
return $this->hasMany(CartItem::class, 'cart_id');
}
public function user()
{
return $this->belongsTo(User::class, 'user_id');
}
}

View File

@@ -1,39 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Models\Market;
use App\Models\Model;
class CartItem extends Model
{
protected ?string $table = 'cart_items';
protected array $fillable = [
'hashkey',
'cart_id',
'product_id',
'quantity',
'price',
'is_active',
'created_by',
'updated_by',
];
protected array $casts = [
'is_active' => 'boolean',
'quantity' => 'integer',
'price' => 'float',
];
public function cart()
{
return $this->belongsTo(Cart::class, 'cart_id');
}
public function product()
{
return $this->belongsTo(Product::class, 'product_id');
}
}

View File

@@ -1,41 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Models\Market;
use App\Models\User;
use Hypervel\Database\Eloquent\Model;
class CooperativeDocument extends Model
{
protected ?string $table = 'cooperative_documents';
protected array $fillable = [
'hashkey',
'parent_hashkey',
'version_number',
'organization_id',
'file_hashkey',
'document_type',
'revision_note',
'created_by',
'updated_by',
'is_active',
];
public function organization()
{
return $this->belongsTo(Organization::class, 'organization_id');
}
public function creator()
{
return $this->belongsTo(User::class, 'created_by');
}
public function updater()
{
return $this->belongsTo(User::class, 'updated_by');
}
}

View File

@@ -1,63 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Models\Market;
use App\Models\Model;
use App\Models\User;
class CooperativeMember extends Model
{
protected ?string $table = 'cooperative_members';
protected array $fillable = [
'hashkey', 'organization_id', 'user_id', 'role',
'membership_type', 'membership_level',
'officer_position', 'officer_level',
'concurrent_position', 'concurrent_level',
'cooperative_name_alt', 'cooperative_position', 'year_beginning',
// Classification
'priority_sector', 'common_bond', 'vulnerability_classifications',
// Government IDs
'philsys_id', 'sss_number', 'pagibig_number',
// SLP
'slp_track', 'slp_association_name', 'listahanan_id', 'fourtps_household_id',
// TUPAD
'tupad_category', 'tupad_insurance_beneficiary_name', 'tupad_insurance_beneficiary_relation',
// OSEC/NSRP
'preferred_occupation', 'nsrp_skills', 'employment_status',
// Programs
'program_participation',
'joined_at', 'is_active', 'created_by', 'updated_by',
];
protected array $casts = [
'joined_at' => 'datetime',
'is_active' => 'boolean',
'priority_sector' => 'array',
'vulnerability_classifications' => 'array',
'nsrp_skills' => 'array',
'program_participation' => 'array',
];
public function organization()
{
return $this->belongsTo(Organization::class, 'organization_id');
}
public function user()
{
return $this->belongsTo(User::class, 'user_id');
}
public function creator()
{
return $this->belongsTo(User::class, 'created_by');
}
public function updater()
{
return $this->belongsTo(User::class, 'updated_by');
}
}

View File

@@ -1,51 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Models\Market;
use App\Models\Model;
use App\Models\User;
class CooperativeResolution extends Model
{
protected ?string $table = 'cooperative_resolutions';
protected array $fillable = [
'hashkey',
'organization_id',
'title',
'description',
'date_approved',
'document_url',
'status',
'created_by',
'updated_by',
'is_active',
];
protected array $casts = [
'date_approved' => 'date',
'is_active' => 'boolean',
];
public function organization()
{
return $this->belongsTo(Organization::class, 'organization_id');
}
public function votes()
{
return $this->hasMany(CooperativeVote::class, 'resolution_id');
}
public function creator()
{
return $this->belongsTo(User::class, 'created_by');
}
public function updater()
{
return $this->belongsTo(User::class, 'updated_by');
}
}

View File

@@ -1,47 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Models\Market;
use App\Models\Model;
use App\Models\User;
class CooperativeVote extends Model
{
protected ?string $table = 'cooperative_votes';
protected array $fillable = [
'hashkey',
'resolution_id',
'user_id',
'vote_cast',
'created_by',
'updated_by',
'is_active',
];
protected array $casts = [
'is_active' => 'boolean',
];
public function resolution()
{
return $this->belongsTo(CooperativeResolution::class, 'resolution_id');
}
public function voter()
{
return $this->belongsTo(User::class, 'user_id');
}
public function creator()
{
return $this->belongsTo(User::class, 'created_by');
}
public function updater()
{
return $this->belongsTo(User::class, 'updated_by');
}
}

View File

@@ -1,42 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Models\Market;
use App\Models\Model;
use App\Models\User;
class Courier extends Model
{
protected ?string $table = 'couriers';
protected array $fillable = [
'hashkey',
'name',
'contact_number',
'type',
'is_active',
'created_by',
'updated_by',
];
protected array $casts = [
'is_active' => 'boolean',
];
public function shipments()
{
return $this->hasMany(Shipment::class, 'courier_id');
}
public function creator()
{
return $this->belongsTo(User::class, 'created_by');
}
public function updater()
{
return $this->belongsTo(User::class, 'updated_by');
}
}

View File

@@ -1,52 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Models\Market;
use App\Models\Model;
use App\Models\User;
class Customer extends Model
{
protected ?string $table = 'cst';
protected array $fillable = [
'hashkey',
'name',
'phone',
'email',
'store_id',
'user_id',
'created_by',
'updated_by',
'is_active',
];
protected array $casts = [
'is_active' => 'boolean',
];
/**
* Relationships
*/
public function creator()
{
return $this->belongsTo(User::class, 'created_by');
}
public function updater()
{
return $this->belongsTo(User::class, 'updated_by');
}
public function user()
{
return $this->belongsTo(User::class, 'user_id');
}
public function store()
{
return $this->belongsTo(Store::class, 'store_id');
}
}

View File

@@ -1,53 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Models\Market;
use App\Models\Model;
use App\Models\User;
class FarmerProfile extends Model
{
protected ?string $table = 'farmer_profiles';
protected array $fillable = [
'hashkey',
'user_id',
'organization_id',
'farm_name',
'farm_location',
'main_crops',
'verification_status',
'certification_details',
'is_active',
'created_by',
'updated_by',
];
protected array $casts = [
'main_crops' => 'array',
'certification_details' => 'array',
'is_active' => 'boolean',
];
public function user()
{
return $this->belongsTo(User::class, 'user_id');
}
public function organization()
{
return $this->belongsTo(Organization::class, 'organization_id');
}
public function creator()
{
return $this->belongsTo(User::class, 'created_by');
}
public function updater()
{
return $this->belongsTo(User::class, 'updated_by');
}
}

View File

@@ -1,44 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Models\Market;
use App\Models\Model;
use App\Models\User;
class MainOrganization extends Model
{
protected ?string $table = 'main_organizations';
protected array $fillable = [
'organization_id',
'role',
'priority',
'is_active',
'metadata',
'created_by',
'updated_by',
];
protected array $casts = [
'is_active' => 'boolean',
'priority' => 'integer',
'metadata' => 'array',
];
public function organization()
{
return $this->belongsTo(Organization::class, 'organization_id');
}
public function creator()
{
return $this->belongsTo(User::class, 'created_by');
}
public function updater()
{
return $this->belongsTo(User::class, 'updated_by');
}
}

View File

@@ -1,78 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Models\Market;
use App\Models\Model;
use App\Models\User;
class Organization extends Model
{
protected ?string $table = 'organizations';
protected array $fillable = [
'hashkey',
'name',
'type',
'address',
'registration_number',
'cin',
'tin',
'cooperative_type',
'cooperative_category',
'registration_date',
'contact_person',
'contact_number',
'contact_email',
'compliance_status',
'is_active',
'created_by',
'updated_by',
];
protected array $casts = [
'is_active' => 'boolean',
'registration_date' => 'date',
];
public function members()
{
return $this->hasMany(CooperativeMember::class, 'organization_id');
}
public function farmerProfiles()
{
return $this->hasMany(FarmerProfile::class, 'organization_id');
}
public function stores()
{
return $this->belongsToMany(Store::class, 'org_str', 'organization_id', 'store_id')
->withTimestamps();
}
public function mainAssignments()
{
return $this->hasMany(MainOrganization::class, 'organization_id');
}
public function isMain(?string $role = null): bool
{
$query = $this->mainAssignments()->where('is_active', true);
if ($role !== null) {
$query->where('role', $role);
}
return $query->exists();
}
public function creator()
{
return $this->belongsTo(User::class, 'created_by');
}
public function updater()
{
return $this->belongsTo(User::class, 'updated_by');
}
}

View File

@@ -1,89 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Models\Market;
use App\Models\Model;
use App\Models\User;
/**
* @property int $id
* @property string $hashkey
* @property string $access_key
* @property int $store_id
* @property string $name
* @property string $status
* @property string $last_used_at
* @property int $created_by
* @property \Carbon\Carbon $created_at
* @property \Carbon\Carbon $updated_at
*/
class PosAccessKey extends Model
{
/**
* The table associated with the model.
*/
protected ?string $table = 'pos_access_keys';
/**
* The attributes that are mass assignable.
*/
protected array $fillable = [
'hashkey',
'access_key',
'store_id',
'name',
'status',
'is_active',
'expires_at',
'last_used_at',
'created_by',
'updated_by',
];
/**
* The attributes that should be cast to native types.
*/
protected array $casts = [
'id' => 'integer',
'store_id' => 'integer',
'created_by' => 'integer',
'updated_by' => 'integer',
'is_active' => 'boolean',
'expires_at' => 'datetime',
];
/**
* Check if this access key is expired.
*/
public function isExpired(): bool
{
return $this->expires_at !== null && $this->expires_at->isPast();
}
/**
* Auto-expire: set all expired active keys to inactive.
* Call this before listing or validating keys.
*/
public static function autoExpire(): void
{
self::where('status', 'active')
->whereNotNull('expires_at')
->where('expires_at', '<', now())
->update(['status' => 'inactive']);
}
/**
* Relationships
*/
public function store()
{
return $this->belongsTo(Store::class, 'store_id');
}
public function creator()
{
return $this->belongsTo(User::class, 'created_by');
}
}

View File

@@ -1,70 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Models\Market;
use App\Models\Model;
use App\Models\User;
class PosSession extends Model
{
protected ?string $table = 'pos_sessions';
protected array $fillable = [
'hashkey',
'access_key',
'store_id',
'created_by',
'updated_by',
'customer_name',
'total_amount',
'received_amount',
'change_amount',
'payment_method',
'payment_details',
'status',
'is_void',
'notes',
'additionaldata',
];
protected array $casts = [
'is_void' => 'boolean',
'payment_details' => 'array',
'additionaldata' => 'array',
'total_amount' => 'integer',
'received_amount' => 'integer',
'change_amount' => 'integer',
'created_by' => 'integer',
'updated_by' => 'integer',
];
public function updater()
{
return $this->belongsTo(User::class, 'updated_by');
}
/**
* Relationships
*/
public function transactions()
{
return $this->hasMany(PosTransaction::class, 'pos_session_id');
}
public function archives()
{
return $this->hasMany(PosSessionArchive::class, 'pos_session_id');
}
public function creator()
{
return $this->belongsTo(User::class, 'created_by');
}
public function store()
{
return $this->belongsTo(Store::class, 'store_id');
}
}

View File

@@ -1,49 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Models\Market;
use App\Models\Model;
use App\Models\User;
class PosSessionArchive extends Model
{
protected ?string $table = 'pos_sessions_archive';
protected array $fillable = [
'pos_session_id',
'hashkey',
'session_snapshot',
'transactions_snapshot',
'created_by',
'updated_by',
'remarks',
];
protected array $casts = [
'session_snapshot' => 'array',
'transactions_snapshot' => 'array',
'created_by' => 'integer',
'updated_by' => 'integer',
'pos_session_id' => 'integer',
];
/**
* Relationships
*/
public function session()
{
return $this->belongsTo(PosSession::class, 'pos_session_id');
}
public function creator()
{
return $this->belongsTo(User::class, 'created_by');
}
public function updater()
{
return $this->belongsTo(User::class, 'updated_by');
}
}

View File

@@ -1,49 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Models\Market;
use App\Models\Model;
class PosTransaction extends Model
{
protected ?string $table = 'pos_transactions';
protected array $fillable = [
'pos_session_id',
'product_id',
'quantity',
'price_at_sale',
'discount',
'total_price',
'is_void',
'remarks',
'hashkey',
'created_by',
'updated_by',
];
protected array $casts = [
'is_void' => 'boolean',
'quantity' => 'integer',
'price_at_sale' => 'integer',
'discount' => 'integer',
'total_price' => 'integer',
'created_by' => 'integer',
'updated_by' => 'integer',
];
/**
* Relationships
*/
public function session()
{
return $this->belongsTo(PosSession::class, 'pos_session_id');
}
public function product()
{
return $this->belongsTo(Product::class, 'product_id');
}
}

View File

@@ -1,91 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Models\Market;
use App\Models\Model;
use App\Models\User;
class Product extends Model
{
protected ?string $table = 'prd_items';
protected string $primaryKey = 'id';
public bool $incrementing = true;
protected string $keyType = 'int';
protected array $fillable = [
'hashkey',
'created_by',
'updated_by',
'created_for',
'category',
'subcategory',
'logs',
'specs',
'photourl',
'available',
'sold',
'price',
// 'store_id',
'owner_id',
'views',
'name',
'description',
'reviews',
'barcode',
'status',
'remarks',
'unitname',
'rating',
'sku',
'qrcode',
'shortcode',
'shortname',
'is_active',
'product_type'
];
protected array $casts = [
'available' => 'integer',
'sold' => 'integer',
'price' => 'integer',
'views' => 'integer',
'rating' => 'integer',
'is_active' => 'boolean',
'photourl' => 'array',
'reviews' => 'array',
'specs' => 'array',
];
public function creator()
{
return $this->belongsTo(User::class, 'created_by');
}
public function updater()
{
return $this->belongsTo(User::class, 'updated_by');
}
public function stores()
{
return $this->belongsToMany(Store::class, 'prd_str')
->withPivot(['available', 'price', 'is_active'])
->withTimestamps();
}
public function owner()
{
return $this->belongsTo(User::class, 'owner_id');
}
public function createdFor()
{
return $this->belongsTo(User::class, 'created_for');
}
}

View File

@@ -1,102 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Models\Market;
use App\Models\Model;
use App\Models\User;
class ProductTransaction extends Model
{
protected ?string $table = 'prd_trx';
/**
* The primary key for the model.
*/
protected string $primaryKey = 'id';
/**
* Indicates if the IDs are auto-incrementing.
*/
public bool $incrementing = true;
/**
* The "type" of the primary key.
*/
protected string $keyType = 'int';
/**
* The attributes that are mass assignable.
*/
protected array $fillable = [
'hashkey',
'created_by',
'updated_by',
'created_for',
'store_id',
'transactiontype',
'product_id',
'transactiondata',
'description',
'subtype',
'name',
'owner_id',
'transactionsessionhash',
'quantity',
'logs',
'remarks',
'price',
'is_void',
'last_total_price',
'last_total_discount',
'notes'
];
/**
* The attributes that should be cast.
*/
protected array $casts = [
'quantity' => 'integer',
'price' => 'integer',
'is_void' => 'boolean',
];
/**
* Relationships.
*/
public function product()
{
return $this->belongsTo(Product::class, 'product_id');
}
public function store()
{
return $this->belongsTo(Store::class, 'store_id');
}
public function owner()
{
return $this->belongsTo(User::class, 'owner_id');
}
public function creator()
{
return $this->belongsTo(User::class, 'created_by');
}
public function updater()
{
return $this->belongsTo(User::class, 'updated_by');
}
public function createdFor()
{
return $this->belongsTo(User::class, 'created_for');
}
public function session()
{
return $this->belongsTo(ProductTransactionSession::class, 'transactionsessionhash', 'hashkey');
}
}

View File

@@ -1,84 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Models\Market;
use App\Models\Model;
use App\Models\User;
class ProductTransactionSession extends Model
{
protected ?string $table = 'prd_trx_ses';
/**
* The primary key for the model.
*/
protected $primaryKey = 'id';
/**
* Indicates if the IDs are auto-incrementing.
*/
public $incrementing = true;
/**
* The "type" of the primary key.
*/
protected $keyType = 'int';
/**
* The attributes that are mass assignable.
*/
protected array $fillable = [
'hashkey',
'name',
'description',
'logs',
'remarks',
'created_by',
'updated_by',
'created_for',
'subtype',
'additionaldata',
'category',
'store_id',
'status',
'is_void',
'last_total_price',
'last_total_discount',
'notes',
];
protected array $casts = [
'is_void' => 'boolean',
];
/**
* Relationships.
*/
public function transactions()
{
return $this->hasMany(ProductTransaction::class, 'transactionsessionhash', 'hashkey');
}
public function store()
{
return $this->belongsTo(Store::class, 'store_id');
}
public function creator()
{
return $this->belongsTo(User::class, 'created_by');
}
public function updater()
{
return $this->belongsTo(User::class, 'updated_by');
}
public function createdFor()
{
return $this->belongsTo(User::class, 'created_for');
}
}

View File

@@ -1,72 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Models\Market;
use App\Models\Model;
use App\Models\User;
class ProductTransactionSessionArchive extends Model
{
protected ?string $table = 'prd_trx_ses_arc';
/**
* The primary key for the model.
*/
protected $primaryKey = 'id';
/**
* Indicates if the IDs are auto-incrementing.
*/
public $incrementing = true;
/**
* The "type" of the primary key.
*/
protected $keyType = 'int';
/**
* The attributes that are mass assignable.
*/
protected array $fillable = [
'name',
'description',
'details',
'hashkey',
'transactions_sessions_id',
'created_by',
'updated_by',
'created_for',
'transactions_snapshot'
];
protected array $casts = [
'details' => 'array',
'transactions_snapshot' => 'array',
];
/**
* Relationships.
*/
public function transactionSession()
{
return $this->belongsTo(ProductTransactionSession::class, 'transactions_sessions_id');
}
public function creator()
{
return $this->belongsTo(User::class, 'created_by');
}
public function updater()
{
return $this->belongsTo(User::class, 'updated_by');
}
public function createdFor()
{
return $this->belongsTo(User::class, 'created_for');
}
}

View File

@@ -1,69 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Models\Market;
use App\Models\Model;
use App\Models\User;
use App\Models\GlobalTransaction;
class Shipment extends Model
{
protected ?string $table = 'shipments';
protected array $fillable = [
'hashkey',
'transaction_id',
'store_id',
'customer_id',
'courier_id',
'tracking_number',
'status',
'origin_address',
'destination_address',
'estimated_delivery_date',
'actual_delivery_date',
'shipping_fee',
'is_active',
'created_by',
'updated_by',
];
protected array $casts = [
'estimated_delivery_date' => 'datetime',
'actual_delivery_date' => 'datetime',
'shipping_fee' => 'decimal:2',
'is_active' => 'boolean',
];
public function transaction()
{
return $this->belongsTo(GlobalTransaction::class, 'transaction_id');
}
public function store()
{
return $this->belongsTo(Store::class, 'store_id');
}
public function customer()
{
return $this->belongsTo(Customer::class, 'customer_id');
}
public function courier()
{
return $this->belongsTo(Courier::class, 'courier_id');
}
public function creator()
{
return $this->belongsTo(User::class, 'created_by');
}
public function updater()
{
return $this->belongsTo(User::class, 'updated_by');
}
}

Some files were not shown because too many files have changed in this diff Show More