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

@@ -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,
];
}
}