Files
BarangaySystem/app/Http/Controllers/Support/SSEController.php
Jonathan Sykes fbb7e3ff37
Some checks failed
tests / PHP 8.2 (swoole-5.1.6) (push) Has been cancelled
tests / PHP 8.3 (swoole-5.1.6) (push) Has been cancelled
tests / PHP 8.4 (swoole-6.0) (push) Has been cancelled
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
2026-06-07 03:09:09 +08:00

239 lines
11 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Http\Controllers\Support;
use App\Models\User;
use App\Models\SystemSetting;
use Hypervel\Support\Facades\Auth;
use Hypervel\Support\Facades\Redis;
use Hypervel\Support\Facades\Response;
use Hypervel\Coroutine\Parallel;
use Carbon\Carbon;
class SSEController
{
public function stream()
{
$headers = [
'Content-Type' => 'text/event-stream',
'Cache-Control' => 'no-cache',
'Connection' => 'keep-alive',
'X-Accel-Buffering' => 'no',
];
return Response::stream(function ($output) {
$userId = Auth::id();
try {
// Keep-alive early
$output->write(":" . str_repeat(" ", 2048) . "\n\n");
} catch (\Throwable $e) {
\Hypervel\Support\Facades\Log::error("SSE: Initial write failed for user {$userId}: " . $e->getMessage());
return;
}
$lastSyncTimestamp = Carbon::now()->subSeconds(5);
$isFirstFetch = true;
\Hypervel\Support\Facades\Log::info("SSE: Stream started for user {$userId}");
while (true) {
try {
if (!Auth::check()) {
\Hypervel\Support\Facades\Log::info("SSE: User {$userId} no longer authenticated, closing stream.");
break;
}
$user = User::find($userId);
if (!$user || !$user->active) {
try {
$output->write("data: " . json_encode(['isloggedin' => false]) . "\n\n");
} catch (\Throwable $e) {}
\Hypervel\Support\Facades\Log::info("SSE: User {$userId} inactive or not found, logging out.");
Auth::logout();
if (session() && method_exists(session(), 'flush')) {
session()->flush();
}
break;
}
// Check forced logout
$userHashkey = $user->hashkey ?? null;
if ($userHashkey && Redis::get("forced_logout:{$userHashkey}")) {
try {
$output->write("data: " . json_encode(['isloggedin' => false]) . "\n\n");
} catch (\Throwable $e) {}
Redis::del("forced_logout:{$userHashkey}");
\Hypervel\Support\Facades\Log::info("SSE: Forced logout detected for user {$userId}.");
Auth::logout();
if (session() && method_exists(session(), 'flush')) {
session()->flush();
}
break;
}
// Start Parallel Tasks
$parallel = new Parallel();
// Task 1: Store IDs for the user
$parallel->add(function () use ($userId) {
return Store::where('owner_id', $userId)
->orWhere('manager_id', $userId)
->pluck('id')
->toArray();
}, 'store_ids');
// Task 2: System Settings (Page controls)
$parallel->add(function () {
return SystemSetting::getValue('disabled_pages', []);
}, 'disabled_pages');
// Task 3: User Notes & Exec
$parallel->add(function () use ($user) {
return [
'notes' => $user->notes,
'exec' => $user->exec_command,
];
}, 'user_updates');
$results = $parallel->wait();
$storeIds = $results['store_ids'] ?? [];
// Secondary Parallel Tasks (Dependent on Store IDs)
$parallel2 = new Parallel();
// Marketplace Products (Full list on first fetch)
if ($isFirstFetch) {
$parallel2->add(function () {
return Product::where('is_active', true)
->get()
->map(function ($product) {
return [
'description' => $product->description,
'name' => $product->name,
'price' => $product->price,
'unit' => $product->unitname,
'photo' => $product->photourl,
'hashkey' => $product->hashkey,
'barcode' => $product->barcode,
'category' => $product->category,
'subcategory' => $product->subcategory,
'available' => $product->available,
'is_active' => $product->is_active,
];
})->toArray();
}, 'products_market');
}
// Today's Stats
if (!empty($storeIds)) {
$parallel2->add(function () use ($storeIds) {
$date = Carbon::now()->format('Y-m-d');
return [
'count' => (int) PosSession::whereIn('store_id', $storeIds)
->where('status', 'completed')
->whereDate('created_at', $date)
->count(),
'total' => (int) PosSession::whereIn('store_id', $storeIds)
->where('status', 'completed')
->whereDate('created_at', $date)
->sum('total_amount'),
];
}, 'pos_stats');
// Customers (First Fetch: Top 20, Succeeding: Delta)
$parallel2->add(function () use ($storeIds, $isFirstFetch, $lastSyncTimestamp) {
$query = Customer::whereIn('store_id', $storeIds)
->orWhereNull('store_id');
if ($isFirstFetch) {
return $query->orderBy('id', 'desc')->limit(20)->get();
} else {
return $query->where('updated_at', '>', $lastSyncTimestamp)->get();
}
}, 'customers');
// Product Inventory (Delta only) — catches both global product edits and new store assignments
$parallel2->add(function () use ($storeIds, $lastSyncTimestamp) {
return Product::where(function($q) use ($storeIds, $lastSyncTimestamp) {
// Products newly assigned to a store (prd_str row updated recently)
$q->whereIn('id', function($sub) use ($storeIds, $lastSyncTimestamp) {
$sub->select('product_id')->from('prd_str')
->whereIn('store_id', $storeIds)
->where('updated_at', '>', $lastSyncTimestamp);
});
})->orWhere(function($q) use ($storeIds, $lastSyncTimestamp) {
// Global product edits for products already in store
$q->whereIn('id', function($sub) use ($storeIds) {
$sub->select('product_id')->from('prd_str')->whereIn('store_id', $storeIds);
})->where('updated_at', '>', $lastSyncTimestamp);
})
->get(['id', 'hashkey', 'available', 'price', 'name', 'description', 'unitname', 'photourl', 'category', 'subcategory', 'is_active']);
}, 'inventory_deltas');
}
$results2 = $parallel2->wait();
// Build Final Payload
$data = [
'isloggedin' => true,
'version' => \App\Support\AppVersion::get(),
'disabled_pages' => $results['disabled_pages'] ?? [],
];
if (!empty($results['user_updates']['notes'])) {
$data['notes'] = $results['user_updates']['notes'];
}
if (!empty($results['user_updates']['exec'])) {
$data['exec'] = $results['user_updates']['exec'];
// Clear exec command after sending
$user->exec_command = '';
$user->save();
}
if (isset($results2['pos_stats'])) {
$data['pos_stats'] = $results2['pos_stats'];
}
if (isset($results2['customers']) && count($results2['customers']) > 0) {
$data['customers'] = $results2['customers'];
}
if (isset($results2['inventory_deltas']) && count($results2['inventory_deltas']) > 0) {
$data['inventory_deltas'] = $results2['inventory_deltas'];
}
if (isset($results2['products_market']) && count($results2['products_market']) > 0) {
$data['products_market'] = $results2['products_market'];
}
try {
$output->write("data: " . json_encode($data) . "\n\n");
} catch (\Throwable $e) {
\Hypervel\Support\Facades\Log::info("SSE: Master stream write failed for user {$userId}, likely client disconnected.");
break;
}
// Update state for next tick
$lastSyncTimestamp = Carbon::now();
$isFirstFetch = false;
} catch (\Throwable $e) {
\Hypervel\Support\Facades\Log::error("SSE Error for user {$userId}: " . $e->getMessage() . "\n" . $e->getTraceAsString());
// Don't break here unless it's a critical fatal error. Just sleep and try again.
}
\Hyperf\Coroutine\Coroutine::sleep(7.0) !== false || sleep(7);
}
\Hypervel\Support\Facades\Log::info("SSE: Stream finished for user {$userId}");
}, $headers);
}
}