mirror of
https://github.com/mivodev/mivo.git
synced 2026-01-26 05:25:42 +07:00
326 lines
10 KiB
PHP
326 lines
10 KiB
PHP
<?php
|
|
|
|
namespace App\Controllers;
|
|
|
|
use App\Core\Controller;
|
|
use App\Models\Config;
|
|
use App\Libraries\RouterOSAPI;
|
|
use App\Helpers\HotspotHelper;
|
|
|
|
class ReportController extends Controller
|
|
{
|
|
public function index($session)
|
|
{
|
|
$data = $this->getSellingReportData($session);
|
|
if (!$data) {
|
|
header('Location: /');
|
|
exit;
|
|
}
|
|
|
|
return $this->view('reports/selling', $data);
|
|
}
|
|
|
|
public function sellingExport($session, $type)
|
|
{
|
|
$data = $this->getSellingReportData($session);
|
|
if (!$data) {
|
|
header('Content-Type: application/json');
|
|
echo json_encode(['error' => 'No data found']);
|
|
exit;
|
|
}
|
|
|
|
$report = $data['report'];
|
|
$exportData = [];
|
|
|
|
foreach ($report as $row) {
|
|
$exportData[] = [
|
|
'Date/Batch' => $row['date'],
|
|
'Status' => $row['status'] ?? '-',
|
|
'Qty (Stock)' => $row['count'],
|
|
'Used' => $row['realized_count'],
|
|
'Realized Income' => $row['realized_total'],
|
|
'Total Stock' => $row['total']
|
|
];
|
|
}
|
|
|
|
header('Content-Type: application/json');
|
|
echo json_encode($exportData);
|
|
exit;
|
|
}
|
|
|
|
private function getSellingReportData($session)
|
|
{
|
|
$configModel = new Config();
|
|
$config = $configModel->getSession($session);
|
|
|
|
if (!$config) return null;
|
|
|
|
$API = new RouterOSAPI();
|
|
$users = [];
|
|
|
|
$profilePriceMap = [];
|
|
if ($API->connect($config['ip_address'], $config['username'], $config['password'])) {
|
|
$users = $API->comm("/ip/hotspot/user/print");
|
|
$profiles = $API->comm("/ip/hotspot/user/profile/print");
|
|
$API->disconnect();
|
|
|
|
// Build Price Map from Profile Scripts
|
|
foreach ($profiles as $p) {
|
|
$meta = HotspotHelper::parseProfileMetadata($p['on-login'] ?? '');
|
|
if (!empty($meta['price'])) {
|
|
$profilePriceMap[$p['name']] = intval($meta['price']);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Aggregate Data
|
|
$report = [];
|
|
$totalIncome = 0;
|
|
$totalVouchers = 0;
|
|
|
|
// Realized (Used) Metrics
|
|
$totalRealizedIncome = 0;
|
|
$totalUsedVouchers = 0;
|
|
|
|
foreach ($users as $user) {
|
|
// Smart Price Detection
|
|
$price = $this->detectPrice($user, $profilePriceMap);
|
|
if ($price <= 0) continue;
|
|
|
|
// Inject price back to user array for downstream logic
|
|
$user['price'] = $price;
|
|
|
|
// Determine Date from Comment
|
|
$date = 'Unknown Date';
|
|
$comment = $user['comment'] ?? '';
|
|
|
|
if (!empty($comment)) {
|
|
$date = $comment;
|
|
}
|
|
|
|
if (!isset($report[$date])) {
|
|
$report[$date] = [
|
|
'date' => $date,
|
|
'count' => 0,
|
|
'total' => 0,
|
|
'realized_total' => 0,
|
|
'realized_count' => 0
|
|
];
|
|
}
|
|
|
|
$price = intval($user['price']);
|
|
|
|
// Check if Used
|
|
// Criteria: uptime != 0s OR bytes-out > 0 OR bytes-in > 0
|
|
$isUsed = false;
|
|
if ((isset($user['uptime']) && $user['uptime'] != '0s') ||
|
|
(isset($user['bytes-out']) && $user['bytes-out'] > 0)) {
|
|
$isUsed = true;
|
|
}
|
|
|
|
$report[$date]['count']++;
|
|
$report[$date]['total'] += $price;
|
|
$totalIncome += $price;
|
|
$totalVouchers++;
|
|
|
|
if ($isUsed) {
|
|
$report[$date]['realized_count']++;
|
|
$report[$date]['realized_total'] += $price;
|
|
$totalRealizedIncome += $price;
|
|
$totalUsedVouchers++;
|
|
}
|
|
}
|
|
|
|
// Calculate Status for each batch
|
|
foreach ($report as &$row) {
|
|
if ($row['realized_count'] === 0) {
|
|
$row['status'] = 'New';
|
|
} elseif ($row['realized_count'] >= $row['count']) {
|
|
$row['status'] = 'Sold Out';
|
|
} else {
|
|
$row['status'] = 'Selling';
|
|
}
|
|
}
|
|
unset($row);
|
|
|
|
// Sort by key (Date/Comment) desc
|
|
krsort($report);
|
|
|
|
return [
|
|
'session' => $session,
|
|
'report' => $report,
|
|
'totalIncome' => $totalIncome,
|
|
'totalVouchers' => $totalVouchers,
|
|
'totalRealizedIncome' => $totalRealizedIncome,
|
|
'totalUsedVouchers' => $totalUsedVouchers,
|
|
'currency' => $config['currency'] ?? 'Rp'
|
|
];
|
|
}
|
|
public function resume($session)
|
|
{
|
|
$configModel = new Config();
|
|
$config = $configModel->getSession($session);
|
|
|
|
if (!$config) {
|
|
header('Location: /');
|
|
exit;
|
|
}
|
|
|
|
$API = new RouterOSAPI();
|
|
$users = [];
|
|
|
|
$profilePriceMap = [];
|
|
if ($API->connect($config['ip_address'], $config['username'], $config['password'])) {
|
|
$users = $API->comm("/ip/hotspot/user/print");
|
|
$profiles = $API->comm("/ip/hotspot/user/profile/print");
|
|
$API->disconnect();
|
|
|
|
foreach ($profiles as $p) {
|
|
$meta = HotspotHelper::parseProfileMetadata($p['on-login'] ?? '');
|
|
if (!empty($meta['price'])) {
|
|
$profilePriceMap[$p['name']] = intval($meta['price']);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Initialize Aggregates
|
|
$daily = [];
|
|
$monthly = [];
|
|
$yearly = [];
|
|
$totalIncome = 0;
|
|
|
|
// Realized Metrics for Resume?
|
|
// Usually Resume is just general financial overview.
|
|
// We'll stick to Stock for now unless requested, as Resume mimics Mikhmon's logic closer.
|
|
// Or we can just calculate standard revenue based on Stock if that's what user expects for "Resume",
|
|
// OR we can add Realized. Let's keep Resume simple first, focus on Selling Report.
|
|
|
|
foreach ($users as $user) {
|
|
$price = $this->detectPrice($user, $profilePriceMap);
|
|
if ($price <= 0) continue;
|
|
|
|
$user['price'] = $price;
|
|
|
|
// Try to parse Date from Comment
|
|
// Supported formats:
|
|
// - MM/DD/YYYY or MM.DD.YYYY (US)
|
|
// - DD-MM-YYYY (EU/ID)
|
|
// - YYYY-MM-DD (ISO)
|
|
// Regex explanations:
|
|
// 1. \b starts word boundary to avoid matching parts of batch IDs (e.g. 711-...)
|
|
// 2. We look for 3 groups of digits separated by / . or -
|
|
$comment = $user['comment'] ?? '';
|
|
$dateObj = null;
|
|
|
|
if (preg_match('/\b(\d{1,2})[\/.-](\d{1,2})[\/.-](\d{2,4})\b/', $comment, $matches)) {
|
|
// Heuristic: If 3rd part is year (4 digits or > 31), use it.
|
|
// If 1st part > 12, it's likely Day (DD-MM-YYYY).
|
|
// Mivo Generator format often: MM.DD.YY or DD.MM.YY
|
|
|
|
$p1 = intval($matches[1]);
|
|
$p2 = intval($matches[2]);
|
|
$p3 = intval($matches[3]);
|
|
|
|
$year = $p3;
|
|
$month = $p1;
|
|
$day = $p2;
|
|
|
|
// Adjust 2-digit year
|
|
if ($year < 100) $year += 2000;
|
|
|
|
// Guess format
|
|
// If p1 > 12, it must be Day. (DD-MM-YYYY)
|
|
if ($p1 > 12) {
|
|
$day = $p1;
|
|
$month = $p2;
|
|
}
|
|
|
|
// Validate date
|
|
if (checkdate($month, $day, $year)) {
|
|
$dateObj = (new \DateTime())->setDate($year, $month, $day);
|
|
}
|
|
}
|
|
// Check for ISO YYYY-MM-DD
|
|
elseif (preg_match('/\b(\d{4})[\/.-](\d{1,2})[\/.-](\d{1,2})\b/', $comment, $matches)) {
|
|
if (checkdate($matches[2], $matches[3], $matches[1])) {
|
|
$dateObj = (new \DateTime())->setDate($matches[1], $matches[2], $matches[3]);
|
|
}
|
|
}
|
|
|
|
// Fallback: If no date found -> "Unknown Date" in resume?
|
|
// Resume requires Month/Year keys. If we can't parse date, we can't add to daily/monthly.
|
|
// We'll skip or add to "Unknown"?
|
|
// Current logic skips if !$dateObj
|
|
if (!$dateObj) continue;
|
|
|
|
$price = intval($user['price']);
|
|
$totalIncome += $price;
|
|
|
|
// Formats
|
|
$dayKey = $dateObj->format('Y-m-d');
|
|
$monthKey = $dateObj->format('Y-m');
|
|
$yearKey = $dateObj->format('Y');
|
|
|
|
// Daily
|
|
if (!isset($daily[$dayKey])) $daily[$dayKey] = 0;
|
|
$daily[$dayKey] += $price;
|
|
|
|
// Monthly
|
|
if (!isset($monthly[$monthKey])) $monthly[$monthKey] = 0;
|
|
$monthly[$monthKey] += $price;
|
|
|
|
// Yearly
|
|
if (!isset($yearly[$yearKey])) $yearly[$yearKey] = 0;
|
|
$yearly[$yearKey] += $price;
|
|
}
|
|
|
|
// Sort Keys
|
|
ksort($daily);
|
|
ksort($monthly);
|
|
ksort($yearly);
|
|
|
|
return $this->view('reports/resume', [
|
|
'session' => $session,
|
|
'daily' => $daily,
|
|
'monthly' => $monthly,
|
|
'yearly' => $yearly,
|
|
'totalIncome' => $totalIncome,
|
|
'currency' => $config['currency'] ?? 'Rp'
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Smart Price Detection Logic
|
|
* Hierarchy:
|
|
* 1. Comment Override (p:5000)
|
|
* 2. Profile Script (Standard Profile)
|
|
* 3. Profile Name Fallback (50K) -- REMOVED loose number matching to avoid garbage data
|
|
*/
|
|
private function detectPrice($user, $profileMap)
|
|
{
|
|
$comment = $user['comment'] ?? '';
|
|
|
|
// 1. Comment Override (p:5000 or price:5000)
|
|
// Updated: Added \b to prevent matching "up-123" as "p-123"
|
|
if (preg_match('/\b(?:p|price)[:-]\s*(\d+)/i', $comment, $matches)) {
|
|
return intval($matches[1]);
|
|
}
|
|
|
|
// 2. Profile Script
|
|
$profile = $user['profile'] ?? 'default';
|
|
if (isset($profileMap[$profile])) {
|
|
return $profileMap[$profile];
|
|
}
|
|
|
|
// 3. Fallback: Parse Profile Name (Strict "K" notation only)
|
|
// Matches "5K", "5k" -> 5000
|
|
if (preg_match('/(\d+)k\b/i', $profile, $m)) {
|
|
return intval($m[1]) * 1000;
|
|
}
|
|
|
|
// DEPRECATED: Loose number matching caused garbage data (e.g. "up-311" -> 311)
|
|
|
|
return 0;
|
|
}
|
|
}
|