feat: implement barangay system phases 2-14
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:
@@ -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]);
|
||||
}
|
||||
}
|
||||
162
app/Http/Controllers/Admin/AdminConsoleController.php
Normal file
162
app/Http/Controllers/Admin/AdminConsoleController.php
Normal 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]);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
207
app/Http/Controllers/Admin/UserController.php
Normal file
207
app/Http/Controllers/Admin/UserController.php
Normal 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,
|
||||
];
|
||||
}
|
||||
}
|
||||
184
app/Http/Controllers/Auth/LoginController.php
Normal file
184
app/Http/Controllers/Auth/LoginController.php
Normal 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);
|
||||
|
||||
}
|
||||
}
|
||||
140
app/Http/Controllers/Barangay/BlotterController.php
Normal file
140
app/Http/Controllers/Barangay/BlotterController.php
Normal 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]);
|
||||
}
|
||||
}
|
||||
77
app/Http/Controllers/Barangay/BlotterHearingController.php
Normal file
77
app/Http/Controllers/Barangay/BlotterHearingController.php
Normal 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']);
|
||||
}
|
||||
}
|
||||
126
app/Http/Controllers/Barangay/BudgetController.php
Normal file
126
app/Http/Controllers/Barangay/BudgetController.php
Normal 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]);
|
||||
}
|
||||
}
|
||||
210
app/Http/Controllers/Barangay/DocumentRequestController.php
Normal file
210
app/Http/Controllers/Barangay/DocumentRequestController.php
Normal 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']);
|
||||
}
|
||||
}
|
||||
164
app/Http/Controllers/Barangay/HouseholdController.php
Normal file
164
app/Http/Controllers/Barangay/HouseholdController.php
Normal 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']);
|
||||
}
|
||||
}
|
||||
135
app/Http/Controllers/Barangay/ProjectController.php
Normal file
135
app/Http/Controllers/Barangay/ProjectController.php
Normal 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]);
|
||||
}
|
||||
}
|
||||
95
app/Http/Controllers/Barangay/RequestTypeController.php
Normal file
95
app/Http/Controllers/Barangay/RequestTypeController.php
Normal 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]);
|
||||
}
|
||||
}
|
||||
159
app/Http/Controllers/Barangay/ResidentController.php
Normal file
159
app/Http/Controllers/Barangay/ResidentController.php
Normal 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]);
|
||||
}
|
||||
}
|
||||
241
app/Http/Controllers/ChapterController.php
Normal file
241
app/Http/Controllers/ChapterController.php
Normal 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;
|
||||
}
|
||||
}
|
||||
223
app/Http/Controllers/FileController.php
Normal file
223
app/Http/Controllers/FileController.php
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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']);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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]);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
99
app/Http/Controllers/Payment/QRPHController.php
Normal file
99
app/Http/Controllers/Payment/QRPHController.php
Normal 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']);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -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 ?? [];
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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('/');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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()]);
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Controllers\UserPages;
|
||||
|
||||
use Hypervel\Http\Request;
|
||||
|
||||
class UltimateUserController
|
||||
{
|
||||
public function Home(){
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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.');
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user