feat: Implement comprehensive Forex Trading Journal platform with Myfxbook analytics

This commit is contained in:
Antigravity Bot
2026-08-31 01:38:36 +03:30
parent ded79a9a9e
commit a6bf56fafb
12 changed files with 3549 additions and 1933 deletions
+542
View File
@@ -0,0 +1,542 @@
<?php
/**
* Forex Trading Analytics Engine (Myfxbook Grade Calculations)
* Precision metrics, risk ratios, drawdown curves, and performance breakdowns.
*/
class AnalyticsEngine {
private PDO $db;
public function __construct(PDO $db) {
$this->db = $db;
}
public function getFullAnalytics(int $accountId, ?string $from = null, ?string $to = null, ?string $symbol = null, ?string $strategy = null): array {
// 1. Fetch Account Details
$stmtAcc = $this->db->prepare("SELECT * FROM accounts WHERE id = ?");
$stmtAcc->execute([$accountId]);
$account = $stmtAcc->fetch();
if (!$account) {
return ['error' => 'Account not found'];
}
$initialBalance = (float)$account['initial_balance'];
// 2. Fetch Trades Query Filtered
$sql = "SELECT * FROM trades WHERE account_id = ?";
$params = [$accountId];
if ($from) {
$sql .= " AND open_time >= ?";
$params[] = $from . ' 00:00:00';
}
if ($to) {
$sql .= " AND open_time <= ?";
$params[] = $to . ' 23:59:59';
}
if ($symbol && $symbol !== 'ALL') {
$sql .= " AND symbol = ?";
$params[] = $symbol;
}
if ($strategy && $strategy !== 'ALL') {
$sql .= " AND strategy = ?";
$params[] = $strategy;
}
$sql .= " ORDER BY open_time ASC, id ASC";
$stmtTrades = $this->db->prepare($sql);
$stmtTrades->execute($params);
$trades = $stmtTrades->fetchAll();
// 3. Process Metrics & Aggregations
$totalTrades = count($trades);
$closedTrades = [];
$openTrades = [];
$grossProfit = 0.0;
$grossLoss = 0.0;
$totalCommission = 0.0;
$totalSwap = 0.0;
$totalPips = 0.0;
$wonTradesCount = 0;
$lostTradesCount = 0;
$beTradesCount = 0;
$longTradesCount = 0;
$longWonCount = 0;
$longProfit = 0.0;
$shortTradesCount = 0;
$shortWonCount = 0;
$shortProfit = 0.0;
$bestTradeProfit = 0.0;
$worstTradeProfit = 0.0;
$bestTradePips = 0.0;
$worstTradePips = 0.0;
$winPipsSum = 0.0;
$lossPipsSum = 0.0;
$currentBalance = $initialBalance;
$peakBalance = $initialBalance;
$maxDrawdownAmount = 0.0;
$maxDrawdownPercent = 0.0;
$growthCurve = [];
$drawdownCurve = [];
$consecutiveWins = 0;
$consecutiveLosses = 0;
$maxConsecutiveWins = 0;
$maxConsecutiveLosses = 0;
$totalDurationSec = 0;
$winDurationSec = 0;
$lossDurationSec = 0;
$dailyReturns = []; // date => pnl
$symbolStats = [];
$strategyStats = [];
$emotionStats = [];
$sessionStats = ['London' => ['count' => 0, 'profit' => 0, 'wins' => 0], 'New York' => ['count' => 0, 'profit' => 0, 'wins' => 0], 'Asian' => ['count' => 0, 'profit' => 0, 'wins' => 0], 'Overlap' => ['count' => 0, 'profit' => 0, 'wins' => 0]];
$dayOfWeekStats = [
1 => ['name' => 'Monday', 'fa' => 'دوشنبه', 'count' => 0, 'profit' => 0, 'wins' => 0],
2 => ['name' => 'Tuesday', 'fa' => 'سه‌شنبه', 'count' => 0, 'profit' => 0, 'wins' => 0],
3 => ['name' => 'Wednesday', 'fa' => 'چهارشنبه', 'count' => 0, 'profit' => 0, 'wins' => 0],
4 => ['name' => 'Thursday', 'fa' => 'پنج‌شنبه', 'count' => 0, 'profit' => 0, 'wins' => 0],
5 => ['name' => 'Friday', 'fa' => 'جمعه', 'count' => 0, 'profit' => 0, 'wins' => 0],
6 => ['name' => 'Saturday', 'fa' => 'شنبه', 'count' => 0, 'profit' => 0, 'wins' => 0],
7 => ['name' => 'Sunday', 'fa' => 'یک‌شنبه', 'count' => 0, 'profit' => 0, 'wins' => 0],
];
$hourlyStats = [];
for ($h = 0; $h < 24; $h++) {
$hourlyStats[$h] = ['hour' => $h, 'count' => 0, 'profit' => 0, 'wins' => 0];
}
// Add starting point to growth curve
$growthCurve[] = [
'date' => date('Y-m-d H:i', strtotime($account['created_at'] ?? 'now')),
'trade_index' => 0,
'balance' => round($initialBalance, 2),
'equity' => round($initialBalance, 2),
'profit' => 0.0,
'gain_percent' => 0.0,
'symbol' => 'START',
'ticket' => '-'
];
$tradeIndex = 0;
foreach ($trades as $t) {
$profit = (float)$t['profit'];
$pips = (float)$t['pips'];
$comm = (float)$t['commission'];
$swap = (float)$t['swap'];
$sym = $t['symbol'];
$strat = $t['strategy'] ?: 'Price Action';
$emo = $t['emotion'] ?: 'Disciplined';
$sess = $t['session'] ?: 'London';
if ($t['status'] === 'open') {
$openTrades[] = $t;
continue;
}
$closedTrades[] = $t;
$tradeIndex++;
$totalCommission += $comm;
$totalSwap += $swap;
$totalPips += $pips;
// Balance & Equity Tracking
$netTradeProfit = $profit + $comm + $swap;
$currentBalance += $netTradeProfit;
if ($currentBalance > $peakBalance) {
$peakBalance = $currentBalance;
}
$currentDDAmount = $peakBalance - $currentBalance;
$currentDDPercent = $peakBalance > 0 ? ($currentDDAmount / $peakBalance) * 100 : 0;
if ($currentDDAmount > $maxDrawdownAmount) {
$maxDrawdownAmount = $currentDDAmount;
}
if ($currentDDPercent > $maxDrawdownPercent) {
$maxDrawdownPercent = $currentDDPercent;
}
$gainPercent = $initialBalance > 0 ? (($currentBalance - $initialBalance) / $initialBalance) * 100 : 0;
$closeDate = $t['close_time'] ?: $t['open_time'];
$growthCurve[] = [
'date' => date('Y-m-d H:i', strtotime($closeDate)),
'trade_index' => $tradeIndex,
'balance' => round($currentBalance, 2),
'equity' => round($currentBalance, 2),
'profit' => round($netTradeProfit, 2),
'gain_percent' => round($gainPercent, 2),
'symbol' => $sym,
'ticket' => $t['ticket'] ?: "#$tradeIndex"
];
$drawdownCurve[] = [
'date' => date('Y-m-d H:i', strtotime($closeDate)),
'trade_index' => $tradeIndex,
'drawdown_percent' => round(-$currentDDPercent, 2),
'drawdown_amount' => round($currentDDAmount, 2)
];
// Win / Loss classification
if ($profit > 0) {
$wonTradesCount++;
$grossProfit += $profit;
$winPipsSum += $pips;
$consecutiveWins++;
$consecutiveLosses = 0;
if ($consecutiveWins > $maxConsecutiveWins) {
$maxConsecutiveWins = $consecutiveWins;
}
if ($profit > $bestTradeProfit) {
$bestTradeProfit = $profit;
}
if ($pips > $bestTradePips) {
$bestTradePips = $pips;
}
} elseif ($profit < 0) {
$lostTradesCount++;
$grossLoss += abs($profit);
$lossPipsSum += abs($pips);
$consecutiveLosses++;
$consecutiveWins = 0;
if ($consecutiveLosses > $maxConsecutiveLosses) {
$maxConsecutiveLosses = $consecutiveLosses;
}
if ($profit < $worstTradeProfit) {
$worstTradeProfit = $profit;
}
if ($pips < $worstTradePips) {
$worstTradePips = $pips;
}
} else {
$beTradesCount++;
}
// Long / Short stats
if (strtolower($t['trade_type']) === 'buy') {
$longTradesCount++;
$longProfit += $netTradeProfit;
if ($profit > 0) $longWonCount++;
} else {
$shortTradesCount++;
$shortProfit += $netTradeProfit;
if ($profit > 0) $shortWonCount++;
}
// Duration calculation
if ($t['open_time'] && $t['close_time']) {
$dur = max(0, strtotime($t['close_time']) - strtotime($t['open_time']));
$totalDurationSec += $dur;
if ($profit > 0) $winDurationSec += $dur;
elseif ($profit < 0) $lossDurationSec += $dur;
}
// Daily return aggregation
$dayKey = date('Y-m-d', strtotime($t['open_time']));
if (!isset($dailyReturns[$dayKey])) {
$dailyReturns[$dayKey] = ['profit' => 0.0, 'pips' => 0.0, 'trades' => 0, 'wins' => 0, 'losses' => 0];
}
$dailyReturns[$dayKey]['profit'] += $netTradeProfit;
$dailyReturns[$dayKey]['pips'] += $pips;
$dailyReturns[$dayKey]['trades']++;
if ($profit > 0) $dailyReturns[$dayKey]['wins']++;
elseif ($profit < 0) $dailyReturns[$dayKey]['losses']++;
// By Symbol Breakdown
if (!isset($symbolStats[$sym])) {
$symbolStats[$sym] = ['symbol' => $sym, 'count' => 0, 'wins' => 0, 'losses' => 0, 'profit' => 0.0, 'pips' => 0.0, 'longs' => 0, 'shorts' => 0];
}
$symbolStats[$sym]['count']++;
$symbolStats[$sym]['profit'] += $netTradeProfit;
$symbolStats[$sym]['pips'] += $pips;
if ($profit > 0) $symbolStats[$sym]['wins']++;
elseif ($profit < 0) $symbolStats[$sym]['losses']++;
if (strtolower($t['trade_type']) === 'buy') $symbolStats[$sym]['longs']++;
else $symbolStats[$sym]['shorts']++;
// By Strategy Breakdown
if (!isset($strategyStats[$strat])) {
$strategyStats[$strat] = ['strategy' => $strat, 'count' => 0, 'wins' => 0, 'losses' => 0, 'profit' => 0.0, 'gross_profit' => 0.0, 'gross_loss' => 0.0];
}
$strategyStats[$strat]['count']++;
$strategyStats[$strat]['profit'] += $netTradeProfit;
if ($profit > 0) {
$strategyStats[$strat]['wins']++;
$strategyStats[$strat]['gross_profit'] += $profit;
} elseif ($profit < 0) {
$strategyStats[$strat]['losses']++;
$strategyStats[$strat]['gross_loss'] += abs($profit);
}
// By Emotion Breakdown
if (!isset($emotionStats[$emo])) {
$emotionStats[$emo] = ['emotion' => $emo, 'count' => 0, 'wins' => 0, 'profit' => 0.0, 'pips' => 0.0];
}
$emotionStats[$emo]['count']++;
$emotionStats[$emo]['profit'] += $netTradeProfit;
$emotionStats[$emo]['pips'] += $pips;
if ($profit > 0) $emotionStats[$emo]['wins']++;
// By Session Breakdown
if (isset($sessionStats[$sess])) {
$sessionStats[$sess]['count']++;
$sessionStats[$sess]['profit'] += $netTradeProfit;
if ($profit > 0) $sessionStats[$sess]['wins']++;
}
// By Day of Week & Hour
$openTs = strtotime($t['open_time']);
$dow = (int)date('N', $openTs); // 1 = Monday .. 7 = Sunday
if (isset($dayOfWeekStats[$dow])) {
$dayOfWeekStats[$dow]['count']++;
$dayOfWeekStats[$dow]['profit'] += $netTradeProfit;
if ($profit > 0) $dayOfWeekStats[$dow]['wins']++;
}
$hr = (int)date('G', $openTs); // 0 .. 23
if (isset($hourlyStats[$hr])) {
$hourlyStats[$hr]['count']++;
$hourlyStats[$hr]['profit'] += $netTradeProfit;
if ($profit > 0) $hourlyStats[$hr]['wins']++;
}
}
// Open floating calculation
$openFloatingProfit = 0.0;
foreach ($openTrades as $ot) {
$openFloatingProfit += (float)$ot['profit'] + (float)$ot['commission'] + (float)$ot['swap'];
}
$currentEquity = $currentBalance + $openFloatingProfit;
// Total Net Closed Profit
$totalClosedProfit = $currentBalance - $initialBalance;
$totalGainPercent = $initialBalance > 0 ? (($currentEquity - $initialBalance) / $initialBalance) * 100 : 0;
$closedGainPercent = $initialBalance > 0 ? ($totalClosedProfit / $initialBalance) * 100 : 0;
$closedCount = count($closedTrades);
$winRate = $closedCount > 0 ? ($wonTradesCount / $closedCount) * 100 : 0;
$lossRate = $closedCount > 0 ? ($lostTradesCount / $closedCount) * 100 : 0;
$profitFactor = $grossLoss > 0 ? round($grossProfit / $grossLoss, 2) : ($grossProfit > 0 ? 99.9 : 0.0);
$avgWin = $wonTradesCount > 0 ? $grossProfit / $wonTradesCount : 0.0;
$avgLoss = $lostTradesCount > 0 ? $grossLoss / $lostTradesCount : 0.0;
$winLossRatio = $avgLoss > 0 ? round($avgWin / $avgLoss, 2) : 0.0;
$expectancy = ($winRate / 100 * $avgWin) - ($lossRate / 100 * $avgLoss);
$avgPipsWin = $wonTradesCount > 0 ? $winPipsSum / $wonTradesCount : 0.0;
$avgPipsLoss = $lostTradesCount > 0 ? $lossPipsSum / $lostTradesCount : 0.0;
$longWinRate = $longTradesCount > 0 ? ($longWonCount / $longTradesCount) * 100 : 0;
$shortWinRate = $shortTradesCount > 0 ? ($shortWonCount / $shortTradesCount) * 100 : 0;
$avgTradeDuration = $closedCount > 0 ? round($totalDurationSec / $closedCount) : 0;
$avgWinDuration = $wonTradesCount > 0 ? round($winDurationSec / $wonTradesCount) : 0;
$avgLossDuration = $lostTradesCount > 0 ? round($lossDurationSec / $lostTradesCount) : 0;
// Sharpe & Sortino Ratio estimation
$dailyPnLValues = array_column($dailyReturns, 'profit');
$sharpeRatio = 0.0;
$sortinoRatio = 0.0;
if (count($dailyPnLValues) > 2) {
$meanDaily = array_sum($dailyPnLValues) / count($dailyPnLValues);
$variance = 0.0;
$downsideVariance = 0.0;
foreach ($dailyPnLValues as $dp) {
$diff = $dp - $meanDaily;
$variance += ($diff * $diff);
if ($dp < 0) {
$downsideVariance += ($dp * $dp);
}
}
$stdDev = sqrt($variance / count($dailyPnLValues));
$downsideStdDev = sqrt($downsideVariance / count($dailyPnLValues));
if ($stdDev > 0) {
$sharpeRatio = round(($meanDaily / $stdDev) * sqrt(252), 2); // Annualized (252 trading days)
}
if ($downsideStdDev > 0) {
$sortinoRatio = round(($meanDaily / $downsideStdDev) * sqrt(252), 2);
}
}
// Today, This Week, This Month profit
$todayStr = date('Y-m-d');
$thisWeekStart = date('Y-m-d', strtotime('monday this week'));
$thisMonthStart = date('Y-m-01');
$todayProfit = $dailyReturns[$todayStr]['profit'] ?? 0.0;
$thisWeekProfit = 0.0;
$thisMonthProfit = 0.0;
foreach ($dailyReturns as $dDate => $dData) {
if ($dDate >= $thisWeekStart) {
$thisWeekProfit += $dData['profit'];
}
if ($dDate >= $thisMonthStart) {
$thisMonthProfit += $dData['profit'];
}
}
// Monthly Returns Table (Year x Months Heatmap)
$monthlyReturns = [];
foreach ($dailyReturns as $dDate => $dData) {
$yr = (int)date('Y', strtotime($dDate));
$mo = (int)date('n', strtotime($dDate));
if (!isset($monthlyReturns[$yr])) {
$monthlyReturns[$yr] = [
'year' => $yr,
'months' => array_fill(1, 12, ['profit' => 0.0, 'gain_percent' => 0.0, 'trades' => 0]),
'total_profit' => 0.0,
'total_gain_percent' => 0.0,
'total_trades' => 0
];
}
$monthlyReturns[$yr]['months'][$mo]['profit'] += $dData['profit'];
$monthlyReturns[$yr]['months'][$mo]['trades'] += $dData['trades'];
$monthlyReturns[$yr]['total_profit'] += $dData['profit'];
$monthlyReturns[$yr]['total_trades'] += $dData['trades'];
}
foreach ($monthlyReturns as $yr => &$yData) {
foreach ($yData['months'] as $mo => &$mVal) {
if ($initialBalance > 0) {
$mVal['gain_percent'] = round(($mVal['profit'] / $initialBalance) * 100, 2);
}
}
if ($initialBalance > 0) {
$yData['total_gain_percent'] = round(($yData['total_profit'] / $initialBalance) * 100, 2);
}
}
unset($yData, $mVal);
// Format Breakdown Lists
$symbolsList = array_values($symbolStats);
usort($symbolsList, fn($a, $b) => $b['profit'] <=> $a['profit']);
foreach ($symbolsList as &$sItem) {
$sItem['win_rate'] = $sItem['count'] > 0 ? round(($sItem['wins'] / $sItem['count']) * 100, 1) : 0;
$sItem['avg_profit'] = $sItem['count'] > 0 ? round($sItem['profit'] / $sItem['count'], 2) : 0;
}
$strategiesList = array_values($strategyStats);
usort($strategiesList, fn($a, $b) => $b['profit'] <=> $a['profit']);
foreach ($strategiesList as &$stItem) {
$stItem['win_rate'] = $stItem['count'] > 0 ? round(($stItem['wins'] / $stItem['count']) * 100, 1) : 0;
$stItem['profit_factor'] = $stItem['gross_loss'] > 0 ? round($stItem['gross_profit'] / $stItem['gross_loss'], 2) : ($stItem['gross_profit'] > 0 ? 99.9 : 0.0);
}
$emotionsList = array_values($emotionStats);
usort($emotionsList, fn($a, $b) => $b['profit'] <=> $a['profit']);
foreach ($emotionsList as &$emItem) {
$emItem['win_rate'] = $emItem['count'] > 0 ? round(($emItem['wins'] / $emItem['count']) * 100, 1) : 0;
}
$sessionsList = [];
foreach ($sessionStats as $sessKey => $sessData) {
$sessionsList[] = [
'session' => $sessKey,
'count' => $sessData['count'],
'profit' => round($sessData['profit'], 2),
'win_rate' => $sessData['count'] > 0 ? round(($sessData['wins'] / $sessData['count']) * 100, 1) : 0
];
}
$daysOfWeekList = [];
foreach ($dayOfWeekStats as $dowKey => $dowData) {
$daysOfWeekList[] = [
'day_num' => $dowKey,
'name' => $dowData['name'],
'name_fa' => $dowData['fa'],
'count' => $dowData['count'],
'profit' => round($dowData['profit'], 2),
'win_rate' => $dowData['count'] > 0 ? round(($dowData['wins'] / $dowData['count']) * 100, 1) : 0
];
}
$hourlyList = array_values($hourlyStats);
foreach ($hourlyList as &$hrItem) {
$hrItem['profit'] = round($hrItem['profit'], 2);
$hrItem['win_rate'] = $hrItem['count'] > 0 ? round(($hrItem['wins'] / $hrItem['count']) * 100, 1) : 0;
}
return [
'account' => $account,
'summary' => [
'initial_balance' => round($initialBalance, 2),
'current_balance' => round($currentBalance, 2),
'current_equity' => round($currentEquity, 2),
'open_floating_profit' => round($openFloatingProfit, 2),
'total_closed_profit' => round($totalClosedProfit, 2),
'total_gain_percent' => round($totalGainPercent, 2),
'closed_gain_percent' => round($closedGainPercent, 2),
'today_profit' => round($todayProfit, 2),
'this_week_profit' => round($thisWeekProfit, 2),
'this_month_profit' => round($thisMonthProfit, 2),
'total_trades' => $totalTrades,
'closed_trades' => $closedCount,
'open_trades' => count($openTrades),
'won_trades' => $wonTradesCount,
'lost_trades' => $lostTradesCount,
'breakeven_trades' => $beTradesCount,
'win_rate' => round($winRate, 1),
'loss_rate' => round($lossRate, 1),
'profit_factor' => $profitFactor,
'gross_profit' => round($grossProfit, 2),
'gross_loss' => round($grossLoss, 2),
'avg_win' => round($avgWin, 2),
'avg_loss' => round($avgLoss, 2),
'win_loss_ratio' => $winLossRatio,
'expectancy' => round($expectancy, 2),
'total_pips' => round($totalPips, 1),
'avg_pips_win' => round($avgPipsWin, 1),
'avg_pips_loss' => round($avgPipsLoss, 1),
'best_trade_profit' => round($bestTradeProfit, 2),
'worst_trade_profit' => round($worstTradeProfit, 2),
'best_trade_pips' => round($bestTradePips, 1),
'worst_trade_pips' => round($worstTradePips, 1),
'max_consecutive_wins' => $maxConsecutiveWins,
'max_consecutive_losses' => $maxConsecutiveLosses,
'max_drawdown_amount' => round($maxDrawdownAmount, 2),
'max_drawdown_percent' => round($maxDrawdownPercent, 2),
'sharpe_ratio' => $sharpeRatio,
'sortino_ratio' => $sortinoRatio,
'total_commission' => round($totalCommission, 2),
'total_swap' => round($totalSwap, 2),
'long_trades' => $longTradesCount,
'long_won' => $longWonCount,
'long_win_rate' => round($longWinRate, 1),
'long_profit' => round($longProfit, 2),
'short_trades' => $shortTradesCount,
'short_won' => $shortWonCount,
'short_win_rate' => round($shortWinRate, 1),
'short_profit' => round($shortProfit, 2),
'avg_trade_duration_seconds' => $avgTradeDuration,
'avg_win_duration_seconds' => $avgWinDuration,
'avg_loss_duration_seconds' => $avgLossDuration,
],
'growth_curve' => $growthCurve,
'drawdown_curve' => $drawdownCurve,
'monthly_returns' => array_values($monthlyReturns),
'daily_pnl' => $dailyReturns,
'by_symbol' => $symbolsList,
'by_strategy' => $strategiesList,
'by_emotion' => $emotionsList,
'by_session' => $sessionsList,
'by_day_of_week' => $daysOfWeekList,
'by_hour' => $hourlyList,
'open_trades' => $openTrades
];
}
}
+596 -306
View File
@@ -1,12 +1,12 @@
<?php
// api.php - RESTful API handlers for ShahrNegar
/**
* Forex Trading Journal REST API
* Clean JSON API for accounts, trades, analytics, chart uploads, MT4/MT5 statement import, and exports.
*/
require_once __DIR__ . '/db.php';
// Set response headers
header('Content-Type: application/json; charset=utf-8');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST, PATCH, PUT, DELETE, OPTIONS');
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization, X-Requested-With');
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
@@ -14,321 +14,611 @@ if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
exit;
}
initDB();
$pdo = getDB();
require_once __DIR__ . '/db.php';
require_once __DIR__ . '/analytics.php';
$requestUri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
$pdo = Database::getConnection();
$analyticsEngine = new AnalyticsEngine($pdo);
// Parse request path
$uri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
$method = $_SERVER['REQUEST_METHOD'];
// Helper JSON response
function jsonResponse($data, int $statusCode = 200) {
function sendJson(array $data, int $statusCode = 200): void {
http_response_code($statusCode);
echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
exit;
}
// Helper to get raw JSON payload
function getJsonInput(): array {
$input = file_get_contents('php://input');
return json_decode($input, true) ?? [];
$raw = file_get_contents('php://input');
$decoded = json_decode($raw, true);
return is_array($decoded) ? $decoded : [];
}
// 1. GET /api/stats
if ($requestUri === '/api/stats' && $method === 'GET') {
$total = (int)$pdo->query("SELECT COUNT(*) FROM issues")->fetchColumn();
$pending = (int)$pdo->query("SELECT COUNT(*) FROM issues WHERE status = 'pending'")->fetchColumn();
$reviewing = (int)$pdo->query("SELECT COUNT(*) FROM issues WHERE status = 'reviewing'")->fetchColumn();
$inProgress = (int)$pdo->query("SELECT COUNT(*) FROM issues WHERE status = 'in_progress'")->fetchColumn();
$resolved = (int)$pdo->query("SELECT COUNT(*) FROM issues WHERE status = 'resolved'")->fetchColumn();
$upvotes = (int)$pdo->query("SELECT COALESCE(SUM(upvotes), 0) FROM issues")->fetchColumn();
// Helper Pip Calculator
function calculatePips(string $symbol, string $type, float $openPrice, float $closePrice): float {
$sym = strtoupper(trim($symbol));
$type = strtolower(trim($type));
$diff = ($type === 'buy') ? ($closePrice - $openPrice) : ($openPrice - $closePrice);
$catStmt = $pdo->query("SELECT category, COUNT(*) as count FROM issues GROUP BY category ORDER BY count DESC");
$categories = $catStmt->fetchAll();
$distStmt = $pdo->query("SELECT district, COUNT(*) as count FROM issues GROUP BY district ORDER BY district ASC");
$districts = $distStmt->fetchAll();
$prioStmt = $pdo->query("SELECT priority, COUNT(*) as count FROM issues GROUP BY priority");
$priorities = [];
foreach ($prioStmt->fetchAll() as $row) {
$priorities[$row['priority']] = (int)$row['count'];
}
$resolutionRate = $total > 0 ? round(($resolved / $total) * 100, 1) : 0;
jsonResponse([
'total' => $total,
'pending' => $pending,
'reviewing' => $reviewing,
'in_progress' => $inProgress,
'resolved' => $resolved,
'total_upvotes' => $upvotes,
'categories' => $categories,
'districts' => $districts,
'priorities' => $priorities,
'resolution_rate' => $resolutionRate
]);
}
// 2. GET /api/issues
if ($requestUri === '/api/issues' && $method === 'GET') {
$category = $_GET['category'] ?? null;
$status = $_GET['status'] ?? null;
$district = isset($_GET['district']) ? (int)$_GET['district'] : 0;
$priority = $_GET['priority'] ?? null;
$search = $_GET['search'] ?? null;
$sortBy = $_GET['sort_by'] ?? 'newest';
$sql = "SELECT * FROM issues WHERE 1=1";
$params = [];
if ($category && $category !== 'all') {
$sql .= " AND category = ?";
$params[] = $category;
}
if ($status && $status !== 'all') {
$sql .= " AND status = ?";
$params[] = $status;
}
if ($district > 0) {
$sql .= " AND district = ?";
$params[] = $district;
}
if ($priority && $priority !== 'all') {
$sql .= " AND priority = ?";
$params[] = $priority;
}
if ($search) {
$sql .= " AND (title LIKE ? OR description LIKE ? OR address LIKE ? OR tracking_code LIKE ?)";
$searchParam = "%{$search}%";
$params[] = $searchParam;
$params[] = $searchParam;
$params[] = $searchParam;
$params[] = $searchParam;
}
if ($sortBy === 'upvotes') {
$sql .= " ORDER BY upvotes DESC, id DESC";
} elseif ($sortBy === 'oldest') {
$sql .= " ORDER BY id ASC";
} elseif ($sortBy === 'emergency') {
$sql .= " ORDER BY CASE priority WHEN 'emergency' THEN 1 WHEN 'high' THEN 2 WHEN 'medium' THEN 3 ELSE 4 END, id DESC";
if (strpos($sym, 'JPY') !== false) {
return round($diff * 100, 1);
} elseif (strpos($sym, 'XAU') !== false || strpos($sym, 'GOLD') !== false) {
return round($diff * 10, 1); // 10 pips per $1.00 move
} elseif (strpos($sym, 'BTC') !== false || strpos($sym, 'ETH') !== false || strpos($sym, 'US30') !== false || strpos($sym, 'NAS') !== false) {
return round($diff, 1); // 1 point
} else {
$sql .= " ORDER BY id DESC";
}
$stmt = $pdo->prepare($sql);
$stmt->execute($params);
$rows = $stmt->fetchAll();
$commentCountStmt = $pdo->prepare("SELECT COUNT(*) FROM comments WHERE issue_id = ?");
$issues = [];
foreach ($rows as $row) {
$commentCountStmt->execute([$row['id']]);
$row['comment_count'] = (int)$commentCountStmt->fetchColumn();
$issues[] = $row;
}
jsonResponse($issues);
}
// 3. POST /api/issues (Create new issue)
if ($requestUri === '/api/issues' && $method === 'POST') {
$data = getJsonInput();
if (empty($data['title']) || empty($data['category']) || empty($data['address'])) {
jsonResponse(['error' => 'اطلاعات ضروری تکمیل نشده است.'], 400);
}
$trackingCode = 'SHR-' . rand(10000, 99999);
$now = date('Y-m-d H:i');
$stmt = $pdo->prepare("INSERT INTO issues (
tracking_code, title, category, description, address, district, priority, status,
lat, lng, image_url, reporter_name, reporter_phone, upvotes, created_at
) VALUES (?, ?, ?, ?, ?, ?, ?, 'pending', ?, ?, ?, ?, ?, 1, ?)");
$stmt->execute([
$trackingCode,
$data['title'],
$data['category'],
$data['description'] ?? '',
$data['address'],
(int)($data['district'] ?? 1),
$data['priority'] ?? 'medium',
(float)($data['lat'] ?? 35.7538),
(float)($data['lng'] ?? 51.4172),
$data['image_url'] ?? 'https://images.unsplash.com/photo-1515162816999-a0c47dc192f7?auto=format&fit=crop&w=800&q=80',
$data['reporter_name'] ?? 'شهروند مسئول',
$data['reporter_phone'] ?? '',
$now
]);
$issueId = $pdo->lastInsertId();
// Timeline initial record
$tlStmt = $pdo->prepare("INSERT INTO timeline (issue_id, status, title, description, created_at) VALUES (?, 'pending', 'ثبت اولیه گزارش در سامانه', 'گزارش شما با موفقیت در سامانه شهرنگار ثبت گردید و در نوبت بررسی کارشناسی قرار گرفت.', ?)");
$tlStmt->execute([$issueId, $now]);
$fetchStmt = $pdo->prepare("SELECT * FROM issues WHERE id = ?");
$fetchStmt->execute([$issueId]);
jsonResponse($fetchStmt->fetch(), 201);
}
// 4. GET /api/track/{code}
if (preg_match('#^/api/track/([^/]+)$#', $requestUri, $matches) && $method === 'GET') {
$code = strtoupper(trim(urldecode($matches[1])));
$stmt = $pdo->prepare("SELECT * FROM issues WHERE UPPER(tracking_code) = ?");
$stmt->execute([$code]);
$issue = $stmt->fetch();
if (!$issue) {
jsonResponse(['error' => "هیچ گزارشی با کد رهگیری «{$code}» یافت نشد."], 404);
}
$tlStmt = $pdo->prepare("SELECT * FROM timeline WHERE issue_id = ? ORDER BY id ASC");
$tlStmt->execute([$issue['id']]);
$issue['timeline'] = $tlStmt->fetchAll();
$cmStmt = $pdo->prepare("SELECT * FROM comments WHERE issue_id = ? ORDER BY id DESC");
$cmStmt->execute([$issue['id']]);
$issue['comments'] = $cmStmt->fetchAll();
jsonResponse($issue);
}
// 5. GET /api/issues/{id}
if (preg_match('#^/api/issues/(\d+)$#', $requestUri, $matches) && $method === 'GET') {
$issueId = (int)$matches[1];
$stmt = $pdo->prepare("SELECT * FROM issues WHERE id = ?");
$stmt->execute([$issueId]);
$issue = $stmt->fetch();
if (!$issue) {
jsonResponse(['error' => 'گزارش مورد نظر یافت نشد.'], 404);
}
$tlStmt = $pdo->prepare("SELECT * FROM timeline WHERE issue_id = ? ORDER BY id ASC");
$tlStmt->execute([$issueId]);
$issue['timeline'] = $tlStmt->fetchAll();
$cmStmt = $pdo->prepare("SELECT * FROM comments WHERE issue_id = ? ORDER BY id DESC");
$cmStmt->execute([$issueId]);
$issue['comments'] = $cmStmt->fetchAll();
jsonResponse($issue);
}
// 6. POST /api/issues/{id}/upvote
if (preg_match('#^/api/issues/(\d+)/upvote$#', $requestUri, $matches) && $method === 'POST') {
$issueId = (int)$matches[1];
$stmt = $pdo->prepare("UPDATE issues SET upvotes = upvotes + 1 WHERE id = ?");
$stmt->execute([$issueId]);
if ($stmt->rowCount() === 0) {
jsonResponse(['error' => 'گزارش یافت نشد.'], 404);
}
$fetch = $pdo->prepare("SELECT upvotes FROM issues WHERE id = ?");
$fetch->execute([$issueId]);
$newUpvotes = (int)$fetch->fetchColumn();
jsonResponse(['id' => $issueId, 'upvotes' => $newUpvotes]);
}
// 7. POST /api/issues/{id}/comment
if (preg_match('#^/api/issues/(\d+)/comment$#', $requestUri, $matches) && $method === 'POST') {
$issueId = (int)$matches[1];
$data = getJsonInput();
if (empty($data['content'])) {
jsonResponse(['error' => 'متن نظر نمی‌تواند خالی باشد.'], 400);
}
$check = $pdo->prepare("SELECT id FROM issues WHERE id = ?");
$check->execute([$issueId]);
if (!$check->fetch()) {
jsonResponse(['error' => 'گزارش یافت نشد.'], 404);
}
$now = date('Y-m-d H:i');
$author = !empty($data['author_name']) ? trim($data['author_name']) : 'شهروند محترم';
$stmt = $pdo->prepare("INSERT INTO comments (issue_id, author_name, content, created_at) VALUES (?, ?, ?, ?)");
$stmt->execute([$issueId, $author, trim($data['content']), $now]);
$commentId = $pdo->lastInsertId();
$cm = $pdo->prepare("SELECT * FROM comments WHERE id = ?");
$cm->execute([$commentId]);
jsonResponse($cm->fetch(), 201);
}
// 8. PATCH / POST /api/issues/{id}/status (Admin Status Update)
if (preg_match('#^/api/issues/(\d+)/status$#', $requestUri, $matches) && in_array($method, ['PATCH', 'POST'])) {
$issueId = (int)$matches[1];
$data = getJsonInput();
$check = $pdo->prepare("SELECT * FROM issues WHERE id = ?");
$check->execute([$issueId]);
$current = $check->fetch();
if (!$current) {
jsonResponse(['error' => 'گزارش یافت نشد.'], 404);
}
$newStatus = $data['status'] ?? $current['status'];
$officialResponse = $data['official_response'] ?? $current['official_response'];
$resolvedImageUrl = $data['resolved_image_url'] ?? $current['resolved_image_url'];
$now = date('Y-m-d H:i');
$resolvedAt = ($newStatus === 'resolved') ? $now : $current['resolved_at'];
$upd = $pdo->prepare("UPDATE issues SET status = ?, official_response = ?, resolved_image_url = ?, resolved_at = ? WHERE id = ?");
$upd->execute([$newStatus, $officialResponse, $resolvedImageUrl, $resolvedAt, $issueId]);
$statusTitles = [
'pending' => 'در صف بررسی مجدد',
'reviewing' => 'بررسی کارشناسی و ارجاع به معاونت مربوطه',
'in_progress' => 'اعزام اکیپ اجرایی و آغاز عملیات میدانی',
'resolved' => 'اتمام عملیات و رفع کامل مسئله',
'rejected' => 'عدم احراز یا خارج از حیطه اختیارات شهرداری'
];
$tlTitle = !empty($data['timeline_title']) ? $data['timeline_title'] : ($statusTitles[$newStatus] ?? "تغییر وضعیت به {$newStatus}");
$tlDesc = !empty($data['timeline_desc']) ? $data['timeline_desc'] : ($officialResponse ?: 'وضعیت پرونده توسط مدیریت سامانه به‌روزرسانی شد.');
$tlStmt = $pdo->prepare("INSERT INTO timeline (issue_id, status, title, description, created_at) VALUES (?, ?, ?, ?, ?)");
$tlStmt->execute([$issueId, $newStatus, $tlTitle, $tlDesc, $now]);
$updated = $pdo->prepare("SELECT * FROM issues WHERE id = ?");
$updated->execute([$issueId]);
jsonResponse($updated->fetch());
}
// 9. POST /api/upload
if ($requestUri === '/api/upload' && $method === 'POST') {
if (!isset($_FILES['file'])) {
jsonResponse(['error' => 'فایلی ارسال نشده است.'], 400);
}
$uploadDir = __DIR__ . '/static/uploads/';
if (!is_dir($uploadDir)) {
mkdir($uploadDir, 0777, true);
}
$ext = pathinfo($_FILES['file']['name'], PATHINFO_EXTENSION);
$filename = uniqid('img_', true) . '.' . $ext;
$target = $uploadDir . $filename;
if (move_uploaded_file($_FILES['file']['tmp_name'], $target)) {
jsonResponse(['url' => '/static/uploads/' . $filename]);
} else {
jsonResponse(['error' => 'خطا در ذخیره‌سازی فایل.'], 500);
return round($diff * 10000, 1); // Standard forex 4/5 digits
}
}
// Default 404 for API
jsonResponse(['error' => 'مسیر مورد نظر در API یافت نشد.'], 404);
try {
// -------------------------------------------------------------
// 1. ACCOUNTS ENDPOINTS
// -------------------------------------------------------------
if (preg_match('#^/api/accounts/?$#', $uri)) {
if ($method === 'GET') {
$stmt = $pdo->query("
SELECT a.*,
(SELECT COUNT(*) FROM trades t WHERE t.account_id = a.id) as total_trades,
(SELECT COALESCE(SUM(profit + commission + swap), 0) FROM trades t WHERE t.account_id = a.id AND t.status = 'closed') as net_closed_profit,
(SELECT COALESCE(SUM(profit + commission + swap), 0) FROM trades t WHERE t.account_id = a.id AND t.status = 'open') as net_open_profit
FROM accounts a ORDER BY a.is_default DESC, a.id ASC
");
$accounts = $stmt->fetchAll();
sendJson(['success' => true, 'accounts' => $accounts]);
} elseif ($method === 'POST') {
$input = getJsonInput();
$name = trim($input['name'] ?? 'حساب معاملاتی جدید');
$broker = trim($input['broker'] ?? 'IC Markets');
$accNum = trim($input['account_number'] ?? '');
$currency = trim($input['currency'] ?? 'USD');
$initBal = (float)($input['initial_balance'] ?? 10000.0);
$leverage = (float)($input['leverage'] ?? 100.0);
$accType = trim($input['account_type'] ?? 'Real');
$isDefault = !empty($input['is_default']) ? 1 : 0;
if ($isDefault) {
$pdo->exec("UPDATE accounts SET is_default = 0");
}
$stmt = $pdo->prepare("
INSERT INTO accounts (name, broker, account_number, currency, initial_balance, leverage, account_type, is_default)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
");
$stmt->execute([$name, $broker, $accNum, $currency, $initBal, $leverage, $accType, $isDefault]);
$newId = (int)$pdo->lastInsertId();
// Optionally generate sample data
if (!empty($input['with_sample_data'])) {
Database::generateSampleTrades($newId, $initBal);
}
sendJson(['success' => true, 'id' => $newId, 'message' => 'حساب معاملاتی با موفقیت ایجاد شد']);
}
}
if (preg_match('#^/api/accounts/(\d+)$#', $uri, $matches)) {
$accId = (int)$matches[1];
if ($method === 'GET') {
$stmt = $pdo->prepare("SELECT * FROM accounts WHERE id = ?");
$stmt->execute([$accId]);
$acc = $stmt->fetch();
if (!$acc) sendJson(['success' => false, 'error' => 'حساب یافت نشد'], 404);
sendJson(['success' => true, 'account' => $acc]);
} elseif ($method === 'PUT') {
$input = getJsonInput();
$stmt = $pdo->prepare("
UPDATE accounts SET
name = COALESCE(?, name),
broker = COALESCE(?, broker),
account_number = COALESCE(?, account_number),
currency = COALESCE(?, currency),
initial_balance = COALESCE(?, initial_balance),
leverage = COALESCE(?, leverage),
account_type = COALESCE(?, account_type),
is_default = COALESCE(?, is_default),
updated_at = CURRENT_TIMESTAMP
WHERE id = ?
");
$stmt->execute([
$input['name'] ?? null,
$input['broker'] ?? null,
$input['account_number'] ?? null,
$input['currency'] ?? null,
isset($input['initial_balance']) ? (float)$input['initial_balance'] : null,
isset($input['leverage']) ? (float)$input['leverage'] : null,
$input['account_type'] ?? null,
isset($input['is_default']) ? (int)$input['is_default'] : null,
$accId
]);
sendJson(['success' => true, 'message' => 'حساب به‌روزرسانی شد']);
} elseif ($method === 'DELETE') {
$stmt = $pdo->prepare("DELETE FROM accounts WHERE id = ?");
$stmt->execute([$accId]);
sendJson(['success' => true, 'message' => 'حساب و معاملات آن حذف شدند']);
}
}
// -------------------------------------------------------------
// 2. ANALYTICS ENDPOINT (Myfxbook Style)
// -------------------------------------------------------------
if (preg_match('#^/api/analytics/?$#', $uri)) {
if ($method === 'GET') {
$accountId = (int)($_GET['account_id'] ?? 0);
if ($accountId <= 0) {
// Get default account
$stmtDef = $pdo->query("SELECT id FROM accounts ORDER BY is_default DESC, id ASC LIMIT 1");
$accountId = (int)($stmtDef->fetchColumn() ?: 1);
}
$from = $_GET['from'] ?? null;
$to = $_GET['to'] ?? null;
$symbol = $_GET['symbol'] ?? null;
$strategy = $_GET['strategy'] ?? null;
$data = $analyticsEngine->getFullAnalytics($accountId, $from, $to, $symbol, $strategy);
sendJson(['success' => true, 'data' => $data]);
}
}
// -------------------------------------------------------------
// 3. TRADES CRUD ENDPOINTS
// -------------------------------------------------------------
if (preg_match('#^/api/trades/?$#', $uri)) {
if ($method === 'GET') {
$accountId = (int)($_GET['account_id'] ?? 0);
if ($accountId <= 0) {
$stmtDef = $pdo->query("SELECT id FROM accounts ORDER BY is_default DESC, id ASC LIMIT 1");
$accountId = (int)($stmtDef->fetchColumn() ?: 1);
}
$page = max(1, (int)($_GET['page'] ?? 1));
$limit = max(5, min(100, (int)($_GET['limit'] ?? 50)));
$offset = ($page - 1) * $limit;
$status = $_GET['status'] ?? 'all';
$symbol = $_GET['symbol'] ?? 'all';
$strategy = $_GET['strategy'] ?? 'all';
$search = trim($_GET['search'] ?? '');
$sortBy = $_GET['sort_by'] ?? 'open_time';
$sortDir = strtolower($_GET['sort_dir'] ?? 'desc') === 'asc' ? 'ASC' : 'DESC';
$allowedSorts = ['open_time', 'close_time', 'profit', 'pips', 'lot_size', 'symbol', 'id'];
if (!in_array($sortBy, $allowedSorts)) $sortBy = 'open_time';
$where = ["account_id = ?"];
$params = [$accountId];
if ($status !== 'all') {
$where[] = "status = ?";
$params[] = $status;
}
if ($symbol !== 'all' && !empty($symbol)) {
$where[] = "symbol = ?";
$params[] = $symbol;
}
if ($strategy !== 'all' && !empty($strategy)) {
$where[] = "strategy = ?";
$params[] = $strategy;
}
if (!empty($search)) {
$where[] = "(symbol LIKE ? OR ticket LIKE ? OR entry_notes LIKE ? OR exit_notes LIKE ? OR tags LIKE ?)";
$sParam = "%$search%";
$params[] = $sParam;
$params[] = $sParam;
$params[] = $sParam;
$params[] = $sParam;
$params[] = $sParam;
}
$whereClause = implode(" AND ", $where);
// Total count
$stmtCount = $pdo->prepare("SELECT COUNT(*) FROM trades WHERE $whereClause");
$stmtCount->execute($params);
$totalCount = (int)$stmtCount->fetchColumn();
// Fetch records
$sql = "SELECT * FROM trades WHERE $whereClause ORDER BY $sortBy $sortDir LIMIT $limit OFFSET $offset";
$stmtList = $pdo->prepare($sql);
$stmtList->execute($params);
$trades = $stmtList->fetchAll();
sendJson([
'success' => true,
'trades' => $trades,
'pagination' => [
'total' => $totalCount,
'page' => $page,
'limit' => $limit,
'total_pages' => ceil($totalCount / $limit)
]
]);
} elseif ($method === 'POST') {
$input = getJsonInput();
$accountId = (int)($input['account_id'] ?? 1);
$symbol = strtoupper(trim($input['symbol'] ?? 'EURUSD'));
$tradeType = strtolower(trim($input['trade_type'] ?? 'buy'));
$lotSize = (float)($input['lot_size'] ?? 0.1);
$openPrice = (float)($input['open_price'] ?? 0.0);
$closePrice = isset($input['close_price']) && $input['close_price'] !== '' ? (float)$input['close_price'] : null;
$stopLoss = isset($input['stop_loss']) && $input['stop_loss'] !== '' ? (float)$input['stop_loss'] : null;
$takeProfit = isset($input['take_profit']) && $input['take_profit'] !== '' ? (float)$input['take_profit'] : null;
$openTime = !empty($input['open_time']) ? date('Y-m-d H:i:s', strtotime($input['open_time'])) : date('Y-m-d H:i:s');
$closeTime = !empty($input['close_time']) ? date('Y-m-d H:i:s', strtotime($input['close_time'])) : null;
$status = trim($input['status'] ?? ($closePrice !== null ? 'closed' : 'open'));
$strategy = trim($input['strategy'] ?? 'Price Action');
$timeframe = trim($input['timeframe'] ?? 'M15');
$session = trim($input['session'] ?? 'London');
$emotion = trim($input['emotion'] ?? 'Disciplined');
$comm = (float)($input['commission'] ?? 0.0);
$swap = (float)($input['swap'] ?? 0.0);
$profit = isset($input['profit']) ? (float)$input['profit'] : 0.0;
$pips = isset($input['pips']) ? (float)$input['pips'] : 0.0;
$rr = isset($input['risk_reward']) ? (float)$input['risk_reward'] : null;
// Auto calculate pips if not provided and closed
if ($closePrice !== null && empty($input['pips'])) {
$pips = calculatePips($symbol, $tradeType, $openPrice, $closePrice);
}
// Auto calculate profit if not provided and closed
if ($closePrice !== null && !isset($input['profit'])) {
// Rough standard lot pip value estimation ($10 per lot per pip for EURUSD)
$pipVal = 10.0 * $lotSize;
$profit = round($pips * $pipVal, 2);
}
$stmt = $pdo->prepare("
INSERT INTO trades (
account_id, ticket, symbol, trade_type, lot_size,
open_price, close_price, stop_loss, take_profit,
open_time, close_time, pips, profit, commission, swap,
status, strategy, timeframe, session, risk_reward, emotion,
entry_notes, exit_notes, lessons, screenshot_entry, screenshot_exit, tags
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
");
$stmt->execute([
$accountId,
trim($input['ticket'] ?? ''),
$symbol,
$tradeType,
$lotSize,
$openPrice,
$closePrice,
$stopLoss,
$takeProfit,
$openTime,
$closeTime,
$pips,
$profit,
$comm,
$swap,
$status,
$strategy,
$timeframe,
$session,
$rr,
$emotion,
trim($input['entry_notes'] ?? ''),
trim($input['exit_notes'] ?? ''),
trim($input['lessons'] ?? ''),
trim($input['screenshot_entry'] ?? ''),
trim($input['screenshot_exit'] ?? ''),
trim($input['tags'] ?? '')
]);
$tradeId = (int)$pdo->lastInsertId();
sendJson(['success' => true, 'id' => $tradeId, 'message' => 'معامله با موفقیت ثبت شد']);
}
}
if (preg_match('#^/api/trades/(\d+)$#', $uri, $matches)) {
$tradeId = (int)$matches[1];
if ($method === 'GET') {
$stmt = $pdo->prepare("SELECT * FROM trades WHERE id = ?");
$stmt->execute([$tradeId]);
$trade = $stmt->fetch();
if (!$trade) sendJson(['success' => false, 'error' => 'معامله یافت نشد'], 404);
sendJson(['success' => true, 'trade' => $trade]);
} elseif ($method === 'PUT') {
$input = getJsonInput();
$stmtGet = $pdo->prepare("SELECT * FROM trades WHERE id = ?");
$stmtGet->execute([$tradeId]);
$curr = $stmtGet->fetch();
if (!$curr) sendJson(['success' => false, 'error' => 'معامله یافت نشد'], 404);
$symbol = strtoupper(trim($input['symbol'] ?? $curr['symbol']));
$tradeType = strtolower(trim($input['trade_type'] ?? $curr['trade_type']));
$openPrice = isset($input['open_price']) ? (float)$input['open_price'] : (float)$curr['open_price'];
$closePrice = array_key_exists('close_price', $input) ? ($input['close_price'] !== null && $input['close_price'] !== '' ? (float)$input['close_price'] : null) : $curr['close_price'];
$pips = isset($input['pips']) ? (float)$input['pips'] : $curr['pips'];
$profit = isset($input['profit']) ? (float)$input['profit'] : $curr['profit'];
if ($closePrice !== null && (!isset($input['pips']) || $input['pips'] === '')) {
$pips = calculatePips($symbol, $tradeType, $openPrice, (float)$closePrice);
}
$status = $input['status'] ?? ($closePrice !== null ? 'closed' : $curr['status']);
$closeTime = array_key_exists('close_time', $input) ? (!empty($input['close_time']) ? date('Y-m-d H:i:s', strtotime($input['close_time'])) : null) : $curr['close_time'];
$stmt = $pdo->prepare("
UPDATE trades SET
ticket = COALESCE(?, ticket),
symbol = ?,
trade_type = ?,
lot_size = COALESCE(?, lot_size),
open_price = ?,
close_price = ?,
stop_loss = ?,
take_profit = ?,
open_time = COALESCE(?, open_time),
close_time = ?,
pips = ?,
profit = ?,
commission = COALESCE(?, commission),
swap = COALESCE(?, swap),
status = ?,
strategy = COALESCE(?, strategy),
timeframe = COALESCE(?, timeframe),
session = COALESCE(?, session),
risk_reward = ?,
emotion = COALESCE(?, emotion),
entry_notes = COALESCE(?, entry_notes),
exit_notes = COALESCE(?, exit_notes),
lessons = COALESCE(?, lessons),
screenshot_entry = COALESCE(?, screenshot_entry),
screenshot_exit = COALESCE(?, screenshot_exit),
tags = COALESCE(?, tags),
updated_at = CURRENT_TIMESTAMP
WHERE id = ?
");
$stmt->execute([
$input['ticket'] ?? null,
$symbol,
$tradeType,
isset($input['lot_size']) ? (float)$input['lot_size'] : null,
$openPrice,
$closePrice,
isset($input['stop_loss']) && $input['stop_loss'] !== '' ? (float)$input['stop_loss'] : null,
isset($input['take_profit']) && $input['take_profit'] !== '' ? (float)$input['take_profit'] : null,
!empty($input['open_time']) ? date('Y-m-d H:i:s', strtotime($input['open_time'])) : null,
$closeTime,
$pips,
$profit,
isset($input['commission']) ? (float)$input['commission'] : null,
isset($input['swap']) ? (float)$input['swap'] : null,
$status,
$input['strategy'] ?? null,
$input['timeframe'] ?? null,
$input['session'] ?? null,
isset($input['risk_reward']) ? (float)$input['risk_reward'] : null,
$input['emotion'] ?? null,
$input['entry_notes'] ?? null,
$input['exit_notes'] ?? null,
$input['lessons'] ?? null,
$input['screenshot_entry'] ?? null,
$input['screenshot_exit'] ?? null,
$input['tags'] ?? null,
$tradeId
]);
sendJson(['success' => true, 'message' => 'معامله با موفقیت ویرایش شد']);
} elseif ($method === 'DELETE') {
$stmt = $pdo->prepare("DELETE FROM trades WHERE id = ?");
$stmt->execute([$tradeId]);
sendJson(['success' => true, 'message' => 'معامله حذف شد']);
}
}
// -------------------------------------------------------------
// 4. MT4 / MT5 STATEMENT IMPORTER (CSV & HTML)
// -------------------------------------------------------------
if (preg_match('#^/api/trades/import/?$#', $uri) && $method === 'POST') {
$accountId = (int)($_POST['account_id'] ?? 1);
if (!isset($_FILES['statement']) || $_FILES['statement']['error'] !== UPLOAD_ERR_OK) {
sendJson(['success' => false, 'error' => 'فایل گزارش انتخاب نشده یا آپلود ناموفق بود'], 400);
}
$fileTmp = $_FILES['statement']['tmp_name'];
$fileName = $_FILES['statement']['name'];
$content = file_get_contents($fileTmp);
$importedCount = 0;
// Check if file is CSV
if (str_ends_with(strtolower($fileName), '.csv') || strpos($content, ',') !== false && strpos($content, '<html') === false) {
$lines = explode("\n", $content);
$header = [];
foreach ($lines as $lineNum => $line) {
$line = trim($line);
if (empty($line)) continue;
$row = str_getcsv($line);
if ($lineNum === 0 || empty($header)) {
$header = array_map('strtolower', array_map('trim', $row));
continue;
}
if (count($row) < 5) continue;
$rowAssoc = [];
foreach ($header as $idx => $colName) {
$rowAssoc[$colName] = $row[$idx] ?? '';
}
$ticket = $rowAssoc['ticket'] ?? $rowAssoc['order'] ?? (string)rand(1000000, 9999999);
$symbol = strtoupper($rowAssoc['symbol'] ?? $rowAssoc['item'] ?? 'EURUSD');
$type = strtolower($rowAssoc['type'] ?? $rowAssoc['cmd'] ?? 'buy');
if (!in_array($type, ['buy', 'sell'])) continue;
$size = (float)($rowAssoc['size'] ?? $rowAssoc['volume'] ?? $rowAssoc['lots'] ?? 0.1);
$openPrice = (float)($rowAssoc['open price'] ?? $rowAssoc['price'] ?? $rowAssoc['open_price'] ?? 0.0);
$closePrice = (float)($rowAssoc['close price'] ?? $rowAssoc['close_price'] ?? 0.0);
$profit = (float)($rowAssoc['profit'] ?? $rowAssoc['p/l'] ?? 0.0);
$comm = (float)($rowAssoc['commission'] ?? $rowAssoc['taxes'] ?? 0.0);
$swap = (float)($rowAssoc['swap'] ?? 0.0);
$openTime = $rowAssoc['open time'] ?? $rowAssoc['time'] ?? date('Y-m-d H:i:s');
$closeTime = $rowAssoc['close time'] ?? $rowAssoc['time'] ?? date('Y-m-d H:i:s');
$pips = calculatePips($symbol, $type, $openPrice, $closePrice);
$stmtIns = $pdo->prepare("
INSERT INTO trades (
account_id, ticket, symbol, trade_type, lot_size,
open_price, close_price, open_time, close_time, pips, profit, commission, swap, status, strategy, emotion
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'closed', 'Imported MT4/MT5', 'Disciplined')
");
$stmtIns->execute([$accountId, $ticket, $symbol, $type, $size, $openPrice, $closePrice, $openTime, $closeTime, $pips, $profit, $comm, $swap]);
$importedCount++;
}
} else {
// HTML Statement Parser (MT4 / MT5 Standard HTML Reports)
preg_match_all('#<tr[^>]*>(.*?)</tr>#is', $content, $trMatches);
foreach ($trMatches[1] as $trHtml) {
preg_match_all('#<td[^>]*>(.*?)</td>#is', $trHtml, $tdMatches);
$tds = array_map(fn($t) => trim(strip_tags($t)), $tdMatches[1] ?? []);
if (count($tds) >= 10 && (strtolower($tds[2] ?? '') === 'buy' || strtolower($tds[2] ?? '') === 'sell')) {
$ticket = $tds[0];
$openTime = date('Y-m-d H:i:s', strtotime($tds[1]));
$type = strtolower($tds[2]);
$size = (float)$tds[3];
$symbol = strtoupper($tds[4]);
$openPrice = (float)$tds[5];
$sl = (float)($tds[6] ?? 0);
$tp = (float)($tds[7] ?? 0);
$closeTime = isset($tds[8]) ? date('Y-m-d H:i:s', strtotime($tds[8])) : $openTime;
$closePrice = (float)($tds[9] ?? $openPrice);
$comm = (float)($tds[10] ?? 0);
$swap = (float)($tds[11] ?? 0);
$profit = (float)($tds[12] ?? 0);
$pips = calculatePips($symbol, $type, $openPrice, $closePrice);
$stmtIns = $pdo->prepare("
INSERT INTO trades (
account_id, ticket, symbol, trade_type, lot_size,
open_price, close_price, stop_loss, take_profit,
open_time, close_time, pips, profit, commission, swap, status, strategy, emotion
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'closed', 'MT4/MT5 Statement', 'Disciplined')
");
$stmtIns->execute([$accountId, $ticket, $symbol, $type, $size, $openPrice, $closePrice, $sl, $tp, $openTime, $closeTime, $pips, $profit, $comm, $swap]);
$importedCount++;
}
}
}
sendJson(['success' => true, 'imported_count' => $importedCount, 'message' => "تعداد $importedCount معامله با موفقیت ایمپورت شد"]);
}
// -------------------------------------------------------------
// 5. EXPORT TRADES (CSV)
// -------------------------------------------------------------
if (preg_match('#^/api/trades/export/?$#', $uri) && $method === 'GET') {
$accountId = (int)($_GET['account_id'] ?? 1);
$stmt = $pdo->prepare("SELECT * FROM trades WHERE account_id = ? ORDER BY open_time ASC");
$stmt->execute([$accountId]);
$trades = $stmt->fetchAll();
header('Content-Type: text/csv; charset=utf-8');
header('Content-Disposition: attachment; filename="forex_trades_' . date('Ymd_His') . '.csv"');
$out = fopen('php://output', 'w');
// UTF-8 BOM for Excel
fputs($out, "\xEF\xBB\xBF");
fputcsv($out, ['ID', 'Ticket', 'Symbol', 'Type', 'Volume', 'Open Price', 'Close Price', 'S/L', 'T/P', 'Open Time', 'Close Time', 'Pips', 'Profit ($)', 'Commission', 'Swap', 'Status', 'Strategy', 'Timeframe', 'Session', 'Emotion', 'Notes']);
foreach ($trades as $t) {
fputcsv($out, [
$t['id'],
$t['ticket'],
$t['symbol'],
$t['trade_type'],
$t['lot_size'],
$t['open_price'],
$t['close_price'],
$t['stop_loss'],
$t['take_profit'],
$t['open_time'],
$t['close_time'],
$t['pips'],
$t['profit'],
$t['commission'],
$t['swap'],
$t['status'],
$t['strategy'],
$t['timeframe'],
$t['session'],
$t['emotion'],
$t['entry_notes']
]);
}
fclose($out);
exit;
}
// -------------------------------------------------------------
// 6. SCREENSHOT IMAGE UPLOAD
// -------------------------------------------------------------
if (preg_match('#^/api/upload/?$#', $uri) && $method === 'POST') {
if (!isset($_FILES['image']) || $_FILES['image']['error'] !== UPLOAD_ERR_OK) {
sendJson(['success' => false, 'error' => 'تصویری ارسال نشد یا خطایی رخ داد'], 400);
}
$file = $_FILES['image'];
$allowed = ['image/jpeg', 'image/png', 'image/webp', 'image/gif'];
if (!in_array($file['type'], $allowed)) {
sendJson(['success' => false, 'error' => 'فرمت تصویر نامعتبر است (تنها JPG, PNG, WEBP مجاز است)'], 400);
}
$uploadDir = __DIR__ . '/uploads';
if (!is_dir($uploadDir)) mkdir($uploadDir, 0777, true);
$ext = pathinfo($file['name'], PATHINFO_EXTENSION);
$newFilename = 'chart_' . uniqid() . '.' . $ext;
$dest = $uploadDir . '/' . $newFilename;
if (move_uploaded_file($file['tmp_name'], $dest)) {
sendJson(['success' => true, 'url' => '/uploads/' . $newFilename]);
} else {
sendJson(['success' => false, 'error' => 'ذخیره تصویر با خطا مواجه شد'], 500);
}
}
// -------------------------------------------------------------
// 7. STRATEGIES & DAILY LOGS
// -------------------------------------------------------------
if (preg_match('#^/api/strategies/?$#', $uri)) {
if ($method === 'GET') {
$stmt = $pdo->query("SELECT * FROM strategies ORDER BY name ASC");
sendJson(['success' => true, 'strategies' => $stmt->fetchAll()]);
} elseif ($method === 'POST') {
$input = getJsonInput();
$name = trim($input['name'] ?? '');
if (empty($name)) sendJson(['success' => false, 'error' => 'نام استراتژی الزامی است'], 400);
$stmt = $pdo->prepare("INSERT OR IGNORE INTO strategies (name, description, color) VALUES (?, ?, ?)");
$stmt->execute([$name, $input['description'] ?? '', $input['color'] ?? '#3b82f6']);
sendJson(['success' => true, 'message' => 'استراتژی ثبت شد']);
}
}
// -------------------------------------------------------------
// 8. RESET & SEED SAMPLE DATA
// -------------------------------------------------------------
if (preg_match('#^/api/reset_sample/?$#', $uri) && $method === 'POST') {
$accountId = (int)($_GET['account_id'] ?? 1);
$pdo->exec("DELETE FROM trades WHERE account_id = $accountId");
Database::generateSampleTrades($accountId, 10000.0);
sendJson(['success' => true, 'message' => 'داده‌های نمونه مای‌اف‌ایکس‌بوک با موفقیت بازیابی شدند']);
}
sendJson(['success' => false, 'error' => 'مسیر مورد نظر یافت نشد (API Not Found)'], 404);
} catch (Throwable $e) {
sendJson(['success' => false, 'error' => 'Server Error: ' . $e->getMessage()], 500);
}
-280
View File
@@ -1,280 +0,0 @@
import sqlite3
import os
import random
from datetime import datetime, timedelta
DB_PATH = os.path.join(os.path.dirname(__file__), "urban_issues.db")
def get_db_connection():
conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row
return conn
def init_db():
conn = get_db_connection()
cursor = conn.cursor()
# Issues Table
cursor.execute("""
CREATE TABLE IF NOT EXISTS issues (
id INTEGER PRIMARY KEY AUTOINCREMENT,
tracking_code TEXT UNIQUE NOT NULL,
title TEXT NOT NULL,
category TEXT NOT NULL,
description TEXT NOT NULL,
address TEXT NOT NULL,
district INTEGER NOT NULL,
priority TEXT NOT NULL, -- 'low', 'medium', 'high', 'emergency'
status TEXT NOT NULL, -- 'pending', 'reviewing', 'in_progress', 'resolved', 'rejected'
lat REAL NOT NULL,
lng REAL NOT NULL,
image_url TEXT,
resolved_image_url TEXT,
reporter_name TEXT,
reporter_phone TEXT,
official_response TEXT,
upvotes INTEGER DEFAULT 0,
created_at TEXT NOT NULL,
resolved_at TEXT
)
""")
# Comments Table
cursor.execute("""
CREATE TABLE IF NOT EXISTS comments (
id INTEGER PRIMARY KEY AUTOINCREMENT,
issue_id INTEGER NOT NULL,
author_name TEXT NOT NULL,
content TEXT NOT NULL,
created_at TEXT NOT NULL,
FOREIGN KEY (issue_id) REFERENCES issues(id) ON DELETE CASCADE
)
""")
# Timeline Table
cursor.execute("""
CREATE TABLE IF NOT EXISTS timeline (
id INTEGER PRIMARY KEY AUTOINCREMENT,
issue_id INTEGER NOT NULL,
status TEXT NOT NULL,
title TEXT NOT NULL,
description TEXT,
created_at TEXT NOT NULL,
FOREIGN KEY (issue_id) REFERENCES issues(id) ON DELETE CASCADE
)
""")
# Check if empty, then seed initial realistic data
cursor.execute("SELECT COUNT(*) FROM issues")
count = cursor.fetchone()[0]
if count == 0:
seed_data(cursor)
conn.commit()
conn.close()
def seed_data(cursor):
now = datetime.now()
sample_issues = [
{
"tracking_code": "SHR-78214",
"title": "فرونشست و چاله عمیق در لاین کندرو",
"category": "آسفالت و معابر",
"description": "وجود چاله عمیق با قطر حدود ۱ متر پس از بارندگی اخیر که باعث خسارت به لاستیک خودروها و ترمزهای ناگهانی خطرناک می‌شود.",
"address": "بزرگراه شهید همت، نرسیده به خروجی گاندی، لاین کندرو",
"district": 3,
"priority": "emergency",
"status": "in_progress",
"lat": 35.7538,
"lng": 51.4172,
"image_url": "https://images.unsplash.com/photo-1515162816999-a0c47dc192f7?auto=format&fit=crop&w=800&q=80",
"resolved_image_url": None,
"reporter_name": "علی رضایی",
"reporter_phone": "09121112233",
"official_response": "اکیپ آسفالت‌ریزی ناحیه ۲ شهرداری منطقه ۳ اعزام گردیده و عملیات زیرسازی و لکه‌گیری در حال انجام است.",
"upvotes": 42,
"days_ago": 2,
"resolved_at": None,
"timeline": [
("pending", "ثبت گزارش توسط شهروند", "گزارش در سامانه ۱۳۷ با موفقیت ثبت شد."),
("reviewing", "تایید کارشناس و ارجاع به معاونت فنی و عمران", "موضوع به ناحیه ۲ ارجاع داده شد."),
("in_progress", "اعزام اکیپ عملیاتی لکه‌گیری", "تیم فنی با تجهیزات در محل مستقر شد.")
],
"comments": [
("سارا محمدی", "دیروز لاستیک ماشین من هم اینجا آسیب دید، ممنون که پیگیری می‌کنید."),
("حسین کاظمی", "امیدوارم امشب تا قبل از ترافیک صبحگاهی تموم بشه.")
]
},
{
"tracking_code": "SHR-65109",
"title": "خاموشی کامل پایه چراغ‌های روشنایی بوستان",
"category": "روشنایی و برق",
"description": "کل مسیر پیاده‌روی شرقی بوستان ملت در تاریکی مطلق است که باعث کاهش امنیت خانواده‌ها و ورزشکاران شبانه شده است.",
"address": "خیابان ولیعصر، بوستان ملت، ضلع شرقی جنب دریاچه",
"district": 3,
"priority": "high",
"status": "resolved",
"lat": 35.7801,
"lng": 51.4116,
"image_url": "https://images.unsplash.com/photo-1509114397022-ed747cca3f65?auto=format&fit=crop&w=800&q=80",
"resolved_image_url": "https://images.unsplash.com/photo-1517457373958-b7bdd4587205?auto=format&fit=crop&w=800&q=80",
"reporter_name": "مریم احمدی",
"reporter_phone": "09123334455",
"official_response": "کابل‌کشی زیرزمینی و تعویض پروژکتورهای معیوب توسط اداره زیباسازی و تاسیسات منطقه انجام و روشنایی کامل برقرار گردید.",
"upvotes": 68,
"days_ago": 5,
"resolved_at": (now - timedelta(days=1)).strftime("%Y-%m-%d %H:%M"),
"timeline": [
("pending", "ثبت گزارش توسط شهروند", "گزارش دریافت شد."),
("reviewing", "بررسی میدانی اداره تاسیسات", "نقص کابل اصلی تایید شد."),
("in_progress", "تعمیرات و تعویض کابل", "تیم تاسیسات در حال اجرای خط جدید."),
("resolved", "تکمیل و رفع نقص روشنایی", "چراغ‌ها روشن و مدار به طور کامل تست شد.")
],
"comments": [
("امیر رستمی", "خیلی سریع درستش کردن، واقعا تشکر از عوامل شهرداری."),
("نگار توکلی", "دیشب رفتم پارک، همه چراغ‌ها درست شده بود.")
]
},
{
"tracking_code": "SHR-91203",
"title": "انباشت پسماند و سرریز مخزن زباله شهری",
"category": "نظافت و پسماند",
"description": "مخزن مکانیزه زباله شکسته شده و زباله‌ها در پیاده‌رو پخش شده که باعث بوی نامطبوع و تجمع حشرات شده است.",
"address": "خیابان آزادی، تقاطع خیابان استاد معین، پلاک ۴۲",
"district": 9,
"priority": "medium",
"status": "pending",
"lat": 35.6997,
"lng": 51.3486,
"image_url": "https://images.unsplash.com/photo-1532996122724-e3c354a0b15b?auto=format&fit=crop&w=800&q=80",
"resolved_image_url": None,
"reporter_name": "سعید کریمی",
"reporter_phone": "09355556677",
"official_response": "گزارش در نوبت بررسی ناظر پسماند منطقه قرار گرفته است.",
"upvotes": 19,
"days_ago": 1,
"resolved_at": None,
"timeline": [
("pending", "ثبت گزارش در سامانه", "منتظر بازرسی و تخصیص سطل جدید مکانیزه.")
],
"comments": [
("مهدی بهرامی", "لطفا سطل‌های پدال‌دار بگذارید که درش بسته بمونه.")
]
},
{
"tracking_code": "SHR-44390",
"title": "آسیب دیدگی و شکستن شاخه‌های درخت کهنسال بر اثر طوفان",
"category": "فضای سبز و بوستان‌ها",
"description": "شاخه‌های سنگین درخت چنار شکسته و روی سیم‌های برق و پیاده‌رو معلق مانده است، احتمال سقوط روی عابرین وجود دارد.",
"address": "خیابان شریعتی، بالاتر از پل رومی، نبش کوچه یاس",
"district": 1,
"priority": "emergency",
"status": "in_progress",
"lat": 35.7985,
"lng": 51.4332,
"image_url": "https://images.unsplash.com/photo-1542601906990-b4d3fb778b09?auto=format&fit=crop&w=800&q=80",
"resolved_image_url": None,
"reporter_name": "پویا سرمدی",
"reporter_phone": "09124445566",
"official_response": "اکیپ سازمان بوستان‌ها و آتش‌نشانی با جرثقیل در حال هرس ایمن و رفع خطر هستند.",
"upvotes": 54,
"days_ago": 1,
"resolved_at": None,
"timeline": [
("pending", "ثبت فوری گزارش خطر", "هشدار سقوط شاخه دریافت شد."),
("reviewing", "هماهنگی با سازمان آتش‌نشانی و فضای سبز", "اعلام وضعیت اضطراری."),
("in_progress", "عملیات رفع خطر و هرس", "حصارکشی پیاده‌رو و استقرار بالابر.")
],
"comments": [
("رویا صبوری", "خدا رو شکر سریع اومدن نوار خطر کشیدن.")
]
},
{
"tracking_code": "SHR-33219",
"title": "سد معبر طولانی مصالح ساختمانی و تخریب پیاده‌رو",
"category": "ساختمان‌سازی و سد معبر",
"description": "یک پروژه ساختمانی بیش از سه هفته است که تمام پیاده‌رو و بخشی از خیابان را با نخاله و داربست بدون راه عبور ایمن مسدود کرده است.",
"address": "سعادت‌آباد، میدان کاج، خیابان مروارید، پلاک ۱۸",
"district": 2,
"priority": "high",
"status": "reviewing",
"lat": 35.7794,
"lng": 51.3758,
"image_url": "https://images.unsplash.com/photo-1541888946425-d0fbb18086f6?auto=format&fit=crop&w=800&q=80",
"resolved_image_url": None,
"reporter_name": "فرشید نادری",
"reporter_phone": "09127778899",
"official_response": "اخطاریه ماده ۱۰۰ و رفع سد معبر برای مالک صادر شد. مهلت رفع ۴۸ ساعت می‌باشد.",
"upvotes": 37,
"days_ago": 3,
"resolved_at": None,
"timeline": [
("pending", "ثبت گزارش تخلف ساختمانی", "ثبت شد."),
("reviewing", "بازدید مامور اجرای احکام شهرداری", "صدور اخطاریه رسمی رفع انسداد معبر.")
],
"comments": [
("کامران شمس", "مادر من با ویلچر مجبور شد بره توی خیابان که خیلی خطرناک بود!")
]
},
{
"tracking_code": "SHR-55821",
"title": "خرابی و چشمک‌زن ماندن چراغ راهنمایی تقاطع پرتردد",
"category": "ترافیک و حمل‌ونقل",
"description": "چراغ راهنمایی تقاطع به مدت دو روز چشمک‌زن زرد مانده که باعث گره کور ترافیکی و تصادفات مکرر شده است.",
"address": "میدان فاطمی (جهاد)، تقاطع خیابان جویبار و شهید گمنام",
"district": 6,
"priority": "high",
"status": "resolved",
"lat": 35.7208,
"lng": 51.4082,
"image_url": "https://images.unsplash.com/photo-1517649763962-0c623266ddc0?auto=format&fit=crop&w=800&q=80",
"resolved_image_url": "https://images.unsplash.com/photo-1508873696983-2df5703bc275?auto=format&fit=crop&w=800&q=80",
"reporter_name": "ندا زمانی",
"reporter_phone": "09191113355",
"official_response": "برد الکترونیکی کنترلر چراغ توسط شرکت کنترل ترافیک تهران تعویض و زمان‌بندی هوشمند مجدداً فعال گردید.",
"upvotes": 85,
"days_ago": 4,
"resolved_at": (now - timedelta(days=2)).strftime("%Y-%m-%d %H:%M"),
"timeline": [
("pending", "ثبت گزارش خرابی علائم ترافیکی", "گزارش دریافت گردید."),
("reviewing", "ارجاع به شرکت کنترل ترافیک", "تایید خرابی سخت‌افزاری کنترلر."),
("in_progress", "تعویض برد فرمان و سنسورها", "عملیات فنی در محل انجام شد."),
("resolved", "راه‌اندازی و اتصال به مرکز کنترل", "چراغ طبق برنامه زمان‌بندی هوشمند شروع به کار کرد.")
],
"comments": [
("سامان یوسفی", "دست مریزاد، امروز صبح ترافیک خیلی روان‌تر بود.")
]
}
]
for item in sample_issues:
created_time = (now - timedelta(days=item["days_ago"], hours=random.randint(1, 10))).strftime("%Y-%m-%d %H:%M")
cursor.execute("""
INSERT INTO issues (
tracking_code, title, category, description, address, district, priority, status,
lat, lng, image_url, resolved_image_url, reporter_name, reporter_phone,
official_response, upvotes, created_at, resolved_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""", (
item["tracking_code"], item["title"], item["category"], item["description"],
item["address"], item["district"], item["priority"], item["status"],
item["lat"], item["lng"], item["image_url"], item["resolved_image_url"],
item["reporter_name"], item["reporter_phone"], item["official_response"],
item["upvotes"], created_time, item["resolved_at"]
))
issue_id = cursor.lastrowid
for tl_status, tl_title, tl_desc in item["timeline"]:
cursor.execute("""
INSERT INTO timeline (issue_id, status, title, description, created_at)
VALUES (?, ?, ?, ?, ?)
""", (issue_id, tl_status, tl_title, tl_desc, created_time))
for author, comment_text in item["comments"]:
cursor.execute("""
INSERT INTO comments (issue_id, author_name, content, created_at)
VALUES (?, ?, ?, ?)
""", (issue_id, author, comment_text, created_time))
+284 -258
View File
@@ -1,273 +1,299 @@
<?php
// db.php - Database initialization and PDO helper for ShahrNegar
/**
* Forex Trading Journal Database Layer (SQLite)
* High-performance, robust SQLite storage for Forex accounts, trades, and analytics.
*/
$dbPath = __DIR__ . '/urban_issues.db';
class Database {
private static ?PDO $pdo = null;
private static string $dbFile = __DIR__ . '/forex_journal.db';
function getDB(): PDO {
global $dbPath;
$pdo = new PDO('sqlite:' . $dbPath);
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
return $pdo;
}
public static function getConnection(): PDO {
if (self::$pdo === null) {
$isNew = !file_exists(self::$dbFile);
self::$pdo = new PDO('sqlite:' . self::$dbFile);
self::$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
self::$pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
// Enable WAL mode for high concurrency and performance
self::$pdo->exec('PRAGMA journal_mode = WAL;');
self::$pdo->exec('PRAGMA foreign_keys = ON;');
function initDB() {
$pdo = getDB();
self::initializeSchema();
// Create Issues Table
$pdo->exec("CREATE TABLE IF NOT EXISTS issues (
id INTEGER PRIMARY KEY AUTOINCREMENT,
tracking_code TEXT UNIQUE NOT NULL,
title TEXT NOT NULL,
category TEXT NOT NULL,
description TEXT NOT NULL,
address TEXT NOT NULL,
district INTEGER NOT NULL,
priority TEXT NOT NULL,
status TEXT NOT NULL,
lat REAL NOT NULL,
lng REAL NOT NULL,
image_url TEXT,
resolved_image_url TEXT,
reporter_name TEXT,
reporter_phone TEXT,
official_response TEXT,
upvotes INTEGER DEFAULT 0,
created_at TEXT NOT NULL,
resolved_at TEXT
)");
// Create Comments Table
$pdo->exec("CREATE TABLE IF NOT EXISTS comments (
id INTEGER PRIMARY KEY AUTOINCREMENT,
issue_id INTEGER NOT NULL,
author_name TEXT NOT NULL,
content TEXT NOT NULL,
created_at TEXT NOT NULL,
FOREIGN KEY (issue_id) REFERENCES issues(id) ON DELETE CASCADE
)");
// Create Timeline Table
$pdo->exec("CREATE TABLE IF NOT EXISTS timeline (
id INTEGER PRIMARY KEY AUTOINCREMENT,
issue_id INTEGER NOT NULL,
status TEXT NOT NULL,
title TEXT NOT NULL,
description TEXT,
created_at TEXT NOT NULL,
FOREIGN KEY (issue_id) REFERENCES issues(id) ON DELETE CASCADE
)");
// Check count and seed if empty
$stmt = $pdo->query("SELECT COUNT(*) FROM issues");
$count = (int)$stmt->fetchColumn();
if ($count === 0) {
seedInitialData($pdo);
if ($isNew || self::isAccountTableEmpty()) {
self::seedInitialData();
}
}
return self::$pdo;
}
}
function seedInitialData(PDO $pdo) {
$now = new DateTime();
private static function initializeSchema(): void {
$db = self::$pdo;
$sampleIssues = [
[
'tracking_code' => 'SHR-78214',
'title' => 'فرونشست و چاله عمیق در لاین کندرو',
'category' => 'آسفالت و معابر',
'description' => 'وجود چاله عمیق با قطر حدود ۱ متر پس از بارندگی اخیر که باعث خسارت به لاستیک خودروها و ترمزهای ناگهانی خطرناک می‌شود.',
'address' => 'بزرگراه شهید همت، نرسیده به خروجی گاندی، لاین کندرو',
'district' => 3,
'priority' => 'emergency',
'status' => 'in_progress',
'lat' => 35.7538,
'lng' => 51.4172,
'image_url' => 'https://images.unsplash.com/photo-1515162816999-a0c47dc192f7?auto=format&fit=crop&w=800&q=80',
'resolved_image_url' => null,
'reporter_name' => 'علی رضایی',
'reporter_phone' => '09121112233',
'official_response' => 'اکیپ آسفالت‌ریزی ناحیه ۲ شهرداری منطقه ۳ اعزام گردیده و عملیات زیرسازی و لکه‌گیری در حال انجام است.',
'upvotes' => 42,
'days_ago' => 2,
'resolved_at' => null,
'timeline' => [
['pending', 'ثبت گزارش توسط شهروند', 'گزارش در سامانه ۱۳۷ با موفقیت ثبت شد.'],
['reviewing', 'تایید کارشناس و ارجاع به معاونت فنی و عمران', 'موضوع به ناحیه ۲ ارجاع داده شد.'],
['in_progress', 'اعزام اکیپ عملیاتی لکه‌گیری', 'تیم فنی با تجهیزات در محل مستقر شد.']
],
'comments' => [
['سارا محمدی', 'دیروز لاستیک ماشین من هم اینجا آسیب دید، ممنون که پیگیری می‌کنید.'],
['حسین کاظمی', 'امیدوارم امشب تا قبل از ترافیک صبحگاهی تموم بشه.']
]
],
[
'tracking_code' => 'SHR-65109',
'title' => 'خاموشی کامل پایه چراغ‌های روشنایی بوستان',
'category' => 'روشنایی و برق',
'description' => 'کل مسیر پیاده‌روی شرقی بوستان ملت در تاریکی مطلق است که باعث کاهش امنیت خانواده‌ها و ورزشکاران شبانه شده است.',
'address' => 'خیابان ولیعصر، بوستان ملت، ضلع شرقی جنب دریاچه',
'district' => 3,
'priority' => 'high',
'status' => 'resolved',
'lat' => 35.7801,
'lng' => 51.4116,
'image_url' => 'https://images.unsplash.com/photo-1509114397022-ed747cca3f65?auto=format&fit=crop&w=800&q=80',
'resolved_image_url' => 'https://images.unsplash.com/photo-1517457373958-b7bdd4587205?auto=format&fit=crop&w=800&q=80',
'reporter_name' => 'مریم احمدی',
'reporter_phone' => '09123334455',
'official_response' => 'کابل‌کشی زیرزمینی و تعویض پروژکتورهای معیوب توسط اداره زیباسازی و تاسیسات منطقه انجام و روشنایی کامل برقرار گردید.',
'upvotes' => 68,
'days_ago' => 5,
'resolved_at' => (clone $now)->modify('-1 day')->format('Y-m-d H:i'),
'timeline' => [
['pending', 'ثبت گزارش توسط شهروند', 'گزارش دریافت شد.'],
['reviewing', 'بررسی میدانی اداره تاسیسات', 'نقص کابل اصلی تایید شد.'],
['in_progress', 'تعمیرات و تعویض کابل', 'تیم تاسیسات در حال اجرای خط جدید.'],
['resolved', 'تکمیل و رفع نقص روشنایی', 'چراغ‌ها روشن و مدار به طور کامل تست شد.']
],
'comments' => [
['امیر رستمی', 'خیلی سریع درستش کردن، واقعا تشکر از عوامل شهرداری.'],
['نگار توکلی', 'دیشب رفتم پارک، همه چراغ‌ها درست شده بود.']
]
],
[
'tracking_code' => 'SHR-91203',
'title' => 'انباشت پسماند و سرریز مخزن زباله شهری',
'category' => 'نظافت و پسماند',
'description' => 'مخزن مکانیزه زباله شکسته شده و زباله‌ها در پیاده‌رو پخش شده که باعث بوی نامطبوع و تجمع حشرات شده است.',
'address' => 'خیابان آزادی، تقاطع خیابان استاد معین، پلاک ۴۲',
'district' => 9,
'priority' => 'medium',
'status' => 'pending',
'lat' => 35.6997,
'lng' => 51.3486,
'image_url' => 'https://images.unsplash.com/photo-1532996122724-e3c354a0b15b?auto=format&fit=crop&w=800&q=80',
'resolved_image_url' => null,
'reporter_name' => 'سعید کریمی',
'reporter_phone' => '09355556677',
'official_response' => 'گزارش در نوبت بررسی ناظر پسماند منطقه قرار گرفته است.',
'upvotes' => 19,
'days_ago' => 1,
'resolved_at' => null,
'timeline' => [
['pending', 'ثبت گزارش در سامانه', 'منتظر بازرسی و تخصیص سطل جدید مکانیزه.']
],
'comments' => [
['مهدی بهرامی', 'لطفا سطل‌های پدال‌دار بگذارید که درش بسته بمونه.']
]
],
[
'tracking_code' => 'SHR-44390',
'title' => 'آسیب دیدگی و شکستن شاخه‌های درخت کهنسال بر اثر طوفان',
'category' => 'فضای سبز و بوستان‌ها',
'description' => 'شاخه‌های سنگین درخت چنار شکسته و روی سیم‌های برق و پیاده‌رو معلق مانده است، احتمال سقوط روی عابرین وجود دارد.',
'address' => 'خیابان شریعتی، بالاتر از پل رومی، نبش کوچه یاس',
'district' => 1,
'priority' => 'emergency',
'status' => 'in_progress',
'lat' => 35.7985,
'lng' => 51.4332,
'image_url' => 'https://images.unsplash.com/photo-1542601906990-b4d3fb778b09?auto=format&fit=crop&w=800&q=80',
'resolved_image_url' => null,
'reporter_name' => 'پویا سرمدی',
'reporter_phone' => '09124445566',
'official_response' => 'اکیپ سازمان بوستان‌ها و آتش‌نشانی با جرثقیل در حال هرس ایمن و رفع خطر هستند.',
'upvotes' => 54,
'days_ago' => 1,
'resolved_at' => null,
'timeline' => [
['pending', 'ثبت فوری گزارش خطر', 'هشدار سقوط شاخه دریافت شد.'],
['reviewing', 'هماهنگی با سازمان آتش‌نشانی و فضای سبز', 'اعلام وضعیت اضطراری.'],
['in_progress', 'عملیات رفع خطر و هرس', 'حصارکشی پیاده‌رو و استقرار بالابر.']
],
'comments' => [
['رویا صبوری', 'خدا رو شکر سریع اومدن نوار خطر کشیدن.']
]
],
[
'tracking_code' => 'SHR-33219',
'title' => 'سد معبر طولانی مصالح ساختمانی و تخریب پیاده‌رو',
'category' => 'ساختمان‌سازی و سد معبر',
'description' => 'یک پروژه ساختمانی بیش از سه هفته است که تمام پیاده‌رو و بخشی از خیابان را با نخاله و داربست بدون راه عبور ایمن مسدود کرده است.',
'address' => 'سعادت‌آباد، میدان کاج، خیابان مروارید، پلاک ۱۸',
'district' => 2,
'priority' => 'high',
'status' => 'reviewing',
'lat' => 35.7794,
'lng' => 51.3758,
'image_url' => 'https://images.unsplash.com/photo-1541888946425-d0fbb18086f6?auto=format&fit=crop&w=800&q=80',
'resolved_image_url' => null,
'reporter_name' => 'فرشید نادری',
'reporter_phone' => '09127778899',
'official_response' => 'اخطاریه ماده ۱۰۰ و رفع سد معبر برای مالک صادر شد. مهلت رفع ۴۸ ساعت می‌باشد.',
'upvotes' => 37,
'days_ago' => 3,
'resolved_at' => null,
'timeline' => [
['pending', 'ثبت گزارش تخلف ساختمانی', 'ثبت شد.'],
['reviewing', 'بازدید مامور اجرای احکام شهرداری', 'صدور اخطاریه رسمی رفع انسداد معبر.']
],
'comments' => [
['کامران شمس', 'مادر من با ویلچر مجبور شد بره توی خیابان که خیلی خطرناک بود!']
]
],
[
'tracking_code' => 'SHR-55821',
'title' => 'خرابی و چشمک‌زن ماندن چراغ راهنمایی تقاطع پرتردد',
'category' => 'ترافیک و حمل‌ونقل',
'description' => 'چراغ راهنمایی تقاطع به مدت دو روز چشمک‌زن زرد مانده که باعث گره کور ترافیکی و تصادفات مکرر شده است.',
'address' => 'میدان فاطمی (جهاد)، تقاطع خیابان جویبار و شهید گمنام',
'district' => 6,
'priority' => 'high',
'status' => 'resolved',
'lat' => 35.7208,
'lng' => 51.4082,
'image_url' => 'https://images.unsplash.com/photo-1517649763962-0c623266ddc0?auto=format&fit=crop&w=800&q=80',
'resolved_image_url' => 'https://images.unsplash.com/photo-1508873696983-2df5703bc275?auto=format&fit=crop&w=800&q=80',
'reporter_name' => 'ندا زمانی',
'reporter_phone' => '09191113355',
'official_response' => 'برد الکترونیکی کنترلر چراغ توسط شرکت کنترل ترافیک تهران تعویض و زمان‌بندی هوشمند مجدداً فعال گردید.',
'upvotes' => 85,
'days_ago' => 4,
'resolved_at' => (clone $now)->modify('-2 days')->format('Y-m-d H:i'),
'timeline' => [
['pending', 'ثبت گزارش خرابی علائم ترافیکی', 'گزارش دریافت گردید.'],
['reviewing', 'ارجاع به شرکت کنترل ترافیک', 'تایید خرابی سخت‌افزاری کنترلر.'],
['in_progress', 'تعویض برد فرمان و سنسورها', 'عملیات فنی در محل انجام شد.'],
['resolved', 'راه‌اندازی و اتصال به مرکز کنترل', 'چراغ طبق برنامه زمان‌بندی هوشمند شروع به کار کرد.']
],
'comments' => [
['سامان یوسفی', 'دست مریزاد، امروز صبح ترافیک خیلی روان‌تر بود.']
]
]
];
// 1. Trading Accounts Table
$db->exec("
CREATE TABLE IF NOT EXISTS accounts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
broker TEXT DEFAULT 'IC Markets',
account_number TEXT DEFAULT '',
currency TEXT DEFAULT 'USD',
initial_balance REAL NOT NULL DEFAULT 10000.0,
leverage REAL DEFAULT 100.0,
account_type TEXT DEFAULT 'Real', -- Real, Demo, PropFirm
is_default INTEGER DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
");
$insertIssue = $pdo->prepare("INSERT INTO issues (
tracking_code, title, category, description, address, district, priority, status,
lat, lng, image_url, resolved_image_url, reporter_name, reporter_phone,
official_response, upvotes, created_at, resolved_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
// 2. Trades Table
$db->exec("
CREATE TABLE IF NOT EXISTS trades (
id INTEGER PRIMARY KEY AUTOINCREMENT,
account_id INTEGER NOT NULL,
ticket TEXT DEFAULT '',
symbol TEXT NOT NULL, -- EURUSD, XAUUSD, GBPUSD, etc.
trade_type TEXT NOT NULL, -- buy, sell
lot_size REAL NOT NULL DEFAULT 0.1,
open_price REAL NOT NULL,
close_price REAL DEFAULT NULL,
stop_loss REAL DEFAULT NULL,
take_profit REAL DEFAULT NULL,
open_time DATETIME NOT NULL,
close_time DATETIME DEFAULT NULL,
pips REAL DEFAULT 0.0,
profit REAL DEFAULT 0.0, -- Net P&L in currency ($)
commission REAL DEFAULT 0.0,
swap REAL DEFAULT 0.0,
status TEXT DEFAULT 'closed', -- open, closed
strategy TEXT DEFAULT 'Price Action',
timeframe TEXT DEFAULT 'M15',
session TEXT DEFAULT 'London', -- London, New York, Asian, Overlap
risk_reward REAL DEFAULT NULL,
risk_percent REAL DEFAULT 1.0,
emotion TEXT DEFAULT 'Disciplined', -- Disciplined, Greedy, Fearful, Revenge, Confident, Impatient, Neutral
entry_notes TEXT DEFAULT '',
exit_notes TEXT DEFAULT '',
lessons TEXT DEFAULT '',
screenshot_entry TEXT DEFAULT '',
screenshot_exit TEXT DEFAULT '',
tags TEXT DEFAULT '',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (account_id) REFERENCES accounts(id) ON DELETE CASCADE
);
");
$insertTimeline = $pdo->prepare("INSERT INTO timeline (issue_id, status, title, description, created_at) VALUES (?, ?, ?, ?, ?)");
$insertComment = $pdo->prepare("INSERT INTO comments (issue_id, author_name, content, created_at) VALUES (?, ?, ?, ?)");
// 3. Daily Notes / Journal Mindset Log
$db->exec("
CREATE TABLE IF NOT EXISTS daily_logs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
account_id INTEGER NOT NULL,
log_date DATE NOT NULL,
notes TEXT DEFAULT '',
mindset_score INTEGER DEFAULT 5, -- 1 to 5
market_condition TEXT DEFAULT 'Normal',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (account_id) REFERENCES accounts(id) ON DELETE CASCADE,
UNIQUE(account_id, log_date)
);
");
foreach ($sampleIssues as $item) {
$createdTime = (clone $now)->modify("-{$item['days_ago']} days")->format('Y-m-d H:i');
$insertIssue->execute([
$item['tracking_code'], $item['title'], $item['category'], $item['description'],
$item['address'], $item['district'], $item['priority'], $item['status'],
$item['lat'], $item['lng'], $item['image_url'], $item['resolved_image_url'],
$item['reporter_name'], $item['reporter_phone'], $item['official_response'],
$item['upvotes'], $createdTime, $item['resolved_at']
// 4. Strategies / Setups Table
$db->exec("
CREATE TABLE IF NOT EXISTS strategies (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE,
description TEXT DEFAULT '',
color TEXT DEFAULT '#3b82f6',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
");
// Create indexes for fast analytical aggregations
$db->exec("CREATE INDEX IF NOT EXISTS idx_trades_account ON trades(account_id);");
$db->exec("CREATE INDEX IF NOT EXISTS idx_trades_open_time ON trades(open_time);");
$db->exec("CREATE INDEX IF NOT EXISTS idx_trades_close_time ON trades(close_time);");
$db->exec("CREATE INDEX IF NOT EXISTS idx_trades_symbol ON trades(symbol);");
$db->exec("CREATE INDEX IF NOT EXISTS idx_trades_status ON trades(status);");
}
private static function isAccountTableEmpty(): bool {
$stmt = self::$pdo->query("SELECT COUNT(*) FROM accounts");
return ((int)$stmt->fetchColumn()) === 0;
}
public static function seedInitialData(): void {
$db = self::$pdo;
// Default Strategies
$strategies = [
['Price Action', 'Break of structure, support & resistance', '#3b82f6'],
['ICT / SMC', 'Order blocks, Fair Value Gaps (FVG) and liquidity sweeps', '#8b5cf6'],
['Trend Following', 'Moving average pullbacks and trend continuation', '#10b981'],
['Scalping', 'Fast momentum moves on M1/M5 charts', '#f59e0b'],
['Reversal / Divergence', 'RSI/MACD divergence at key supply/demand levels', '#ec4899'],
['News Trading', 'High-impact economic news releases', '#ef4444'],
];
$stmtStrat = $db->prepare("INSERT OR IGNORE INTO strategies (name, description, color) VALUES (?, ?, ?)");
foreach ($strategies as $strat) {
$stmtStrat->execute($strat);
}
// Default Accounts
$stmtAcc = $db->prepare("
INSERT INTO accounts (name, broker, account_number, currency, initial_balance, leverage, account_type, is_default)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
");
$stmtAcc->execute(['حساب معاملاتی اصلی (Main Live)', 'IC Markets', '8841920', 'USD', 10000.0, 100.0, 'Real', 1]);
$mainAccountId = (int)$db->lastInsertId();
$stmtAcc->execute(['چالش پراپ فرم ۱۰۰ هزار دلاری (FTMO Prop)', 'FTMO', '2948173', 'USD', 100000.0, 100.0, 'PropFirm', 0]);
// Seed rich realistic trading history for main account
self::generateSampleTrades($mainAccountId, 10000.0);
}
public static function generateSampleTrades(int $accountId, float $startBalance = 10000.0): void {
$db = self::$pdo;
// Trade definition array:
// [0:symbol, 1:type, 2:lot, 3:openPrice, 4:closePrice, 5:sl, 6:tp, 7:pips, 8:profit, 9:comm, 10:swap, 11:strat, 12:tf, 13:session, 14:rr, 15:emotion, 16:entryNotes, 17:exitNotes, 18:lessons]
$tradeDataList = [
['EURUSD', 'buy', 1.0, 1.08200, 1.08650, 1.07950, 1.08700, 45.0, 450.0, -7.0, 0.0, 'ICT / SMC', 'H1', 'London', 1.8, 'Disciplined', 'جاروب نقدینگی لندن و واکنش به FVG', 'رسیدن به هدف نقدینگی سقف روز قبل', 'پایبندی به استراتژی'],
['XAUUSD', 'buy', 0.5, 2350.20, 2368.50, 2342.00, 2370.00, 183.0, 915.0, -5.0, -2.1, 'Price Action', 'M15', 'New York', 2.2, 'Confident', 'تست مجدد سطح حمایتی ۲۳۵۰ با کندل تاییدیه پین بار', 'خروج در نزدیکی تارگت با سود عالی', 'صبر برای پولبک کلید موفقیت بود'],
['GBPUSD', 'sell', 1.2, 1.27600, 1.27150, 1.27850, 1.27000, 45.0, 540.0, -8.4, 1.5, 'Trend Following', 'M15', 'London', 1.8, 'Disciplined', 'شکست خط روند نزولی و پولبک به EMA50', 'ریسک فری شد و در تارگت ۲ بسته شد', 'مدیریت ترید عالی'],
['USDJPY', 'buy', 1.0, 154.800, 154.450, 154.400, 155.600, -35.0, -225.0, -7.0, 0.0, 'Scalping', 'M5', 'Asian', 0.0, 'Fearful', 'ورود با مومنتوم صعودی ضعیف در نشست آسیا', 'اصابت به استاپ لاس به دلیل اسپرد بالا', 'در سشن آسیا نباید روی جفت‌های کم‌نوسان بدون کاتالیزور معامله کرد'],
['EURUSD', 'sell', 1.5, 1.08900, 1.08350, 1.09150, 1.08200, 55.0, 825.0, -10.5, 0.0, 'ICT / SMC', 'M15', 'New York', 2.2, 'Disciplined', 'شکار نقدینگی سشن نیویورک و تایید با اردر بلاک خرسی', 'کست کامل سود در کف سشن لندن', 'ورود دقیق روی اوردربلاک'],
['XAUUSD', 'sell', 0.8, 2385.00, 2392.50, 2392.00, 2370.00, -75.0, -600.0, -8.0, 0.0, 'Reversal / Divergence', 'M15', 'New York', 0.0, 'Greedy', 'ورود برخلاف روند قدرتمند بدون واگرایی قطعی', 'استاپ خورد، طلا شتاب شدید صعودی داشت', 'هرگز نباید جلوی قطار سریع‌السیر ایستاد!'],
['BTCUSD', 'buy', 0.2, 63200.0, 65100.0, 62400.0, 65500.0, 1900.0, 380.0, -3.0, -0.5, 'Trend Following', 'H4', 'London', 2.4, 'Confident', 'حفظ حمایت رند ۶۳۰۰۰ و تشکیل الگوی دابل باتم', 'خروج پله‌ای در ۶۵۱۰۰', 'معامله سوینگی تمیز'],
['US30', 'buy', 0.5, 39200.0, 39650.0, 39000.0, 39700.0, 450.0, 225.0, -5.0, 0.0, 'Scalping', 'M5', 'New York', 2.2, 'Disciplined', 'اوپن سشن نیویورک و جهش شاخص داوجونز', 'خروج سریع با تارگت تعیین شده', 'مدیریت ریسک عالی'],
['GBPUSD', 'buy', 1.0, 1.26800, 1.27400, 1.26500, 1.27500, 60.0, 600.0, -7.0, 0.0, 'ICT / SMC', 'M15', 'London', 2.0, 'Disciplined', 'برگشت قیمت از ناحیه تقاضا و جاروب کف آسیا', 'تارگت لمس شد', 'برنامه معاملاتی مو به مو اجرا شد'],
['EURUSD', 'sell', 1.0, 1.08500, 1.08800, 1.08800, 1.08000, -30.0, -300.0, -7.0, 0.0, 'Price Action', 'M15', 'London', 0.0, 'Impatient', 'ورود عجولانه قبل از بسته شدن کندل ۱۵ دقیقه', 'برگشت قیمت به بالا و استاپ', 'صبر برای کلوز کندل ضروری است'],
['XAUUSD', 'buy', 0.7, 2360.00, 2378.00, 2353.00, 2380.00, 180.0, 1260.0, -7.0, -1.8, 'ICT / SMC', 'H1', 'Overlap', 2.5, 'Disciplined', 'تلاقی FVG با لول ۵۰٪ فیبوناچی در اورلپ لندن/نیویورک', 'تارگت کامل محقق شد', 'یکی از بهترین معاملات ماه'],
['AUDUSD', 'sell', 1.0, 0.66800, 0.66350, 0.67050, 0.66200, 45.0, 450.0, -7.0, 1.2, 'Trend Following', 'H1', 'Asian', 1.8, 'Neutral', 'شکست رنج قیمتی و تاییدیه با داده‌های اشتغال استرالیا', 'خروج با سود خوب', 'انطباق خوب با تحلیل فاندامنتال'],
['USDJPY', 'sell', 1.0, 156.200, 155.300, 156.600, 155.000, 90.0, 580.0, -7.0, 0.0, 'Price Action', 'H1', 'New York', 2.2, 'Disciplined', 'سقف دو قلو در تایم فریم یک ساعته با کندل اینگالف نزولی', 'خروج در حمایت کلیدی ۱۵۵.۳۰', 'معامله بسیار روان و بدون استرس'],
['EURUSD', 'buy', 1.2, 1.08300, 1.08100, 1.08050, 1.08900, -20.0, -240.0, -8.4, 0.0, 'Scalping', 'M5', 'London', 0.0, 'Neutral', 'تلاش برای گرفتن بریک اوت سریع که فیک اوت شد', 'حد ضرر نجات داد', 'ضرر کوچک و کنترل‌شده'],
['XAUUSD', 'buy', 0.6, 2375.00, 2394.00, 2368.00, 2395.00, 190.0, 1140.0, -6.0, 0.0, 'ICT / SMC', 'M15', 'New York', 2.7, 'Confident', 'سوییپ نقدینگی کف روزانه در اخبار CPI و واکنش انفجاری', 'خروج در تارگت ۲۳۹۴', 'مدیریت پوزیشن عالی حین خبر'],
['GBPUSD', 'sell', 1.0, 1.27500, 1.27100, 1.27800, 1.27000, 40.0, 400.0, -7.0, 0.0, 'Price Action', 'M15', 'London', 1.3, 'Disciplined', 'برخورد به سقف کانال نزولی', 'برداشت سود مطمئن', 'معامله استاندارد طبق پلن'],
['BTCUSD', 'sell', 0.25, 66500.0, 64200.0, 67200.0, 64000.0, 2300.0, 575.0, -3.5, 0.0, 'Reversal / Divergence', 'H4', 'New York', 3.2, 'Disciplined', 'واگرایی منفی مشهود در RSI تایم ۴ ساعته', 'ریزش عالی بیت‌کوین و سیو سود', 'نسبت ریسک به ریوارد بالای ۳'],
['US30', 'sell', 0.4, 39800.0, 39950.0, 39950.0, 39400.0, -150.0, -60.0, -4.0, 0.0, 'Scalping', 'M5', 'New York', 0.0, 'Neutral', 'ترید خلاف ترند در اوپن بازار', 'استاپ کوچک خورد', 'مدیریت ریسک عالی'],
['EURUSD', 'buy', 1.0, 1.08400, 1.08920, 1.08150, 1.09000, 52.0, 520.0, -7.0, 0.0, 'ICT / SMC', 'H1', 'London', 2.1, 'Disciplined', 'پر شدن FVG سشن لندن و چرخش روند', 'تارگت لمس شد', 'آرامش و صبر کامل'],
['XAUUSD', 'buy', 0.5, 2390.00, 2415.00, 2382.00, 2420.00, 250.0, 1250.0, -5.0, -2.5, 'Trend Following', 'H4', 'New York', 3.1, 'Confident', 'شکست سقف تاریخی قبلی و پولبک استاندارد', 'سود فوق‌العاده با تارگت بالای ۲۴۱۵', 'رعایت تمام اصول استراتژی'],
['USDJPY', 'buy', 1.0, 155.100, 155.650, 154.750, 155.800, 55.0, 355.0, -7.0, 0.0, 'Price Action', 'M15', 'Asian', 1.5, 'Disciplined', 'حمایت معتبر در سشن توکیو', 'خروج با سود خوب در شروع سشن لندن', 'ترید تمیز'],
['GBPUSD', 'buy', 1.0, 1.26900, 1.26600, 1.26600, 1.27600, -30.0, -300.0, -7.0, 0.0, 'Price Action', 'M15', 'London', 0.0, 'Fearful', 'انتشار داده‌های ضعیف بریتانیا و استاپ خوردن', 'استاپ رعایت شد', 'پایبندی به استاپ لاس نجات‌بخش بود'],
['EURUSD', 'sell', 1.0, 1.08800, 1.08300, 1.09050, 1.08200, 50.0, 500.0, -7.0, 0.0, 'ICT / SMC', 'M15', 'New York', 2.0, 'Disciplined', 'شکار نقدینگی خریداران بالای ۱.۰۸۸۰ و ریزش سریع', 'خروج در کف روز', 'ستاپ کاملاً مکانیکی و تست شده']
];
$stmtTrade = $db->prepare("
INSERT INTO trades (
account_id, ticket, symbol, trade_type, lot_size,
open_price, close_price, stop_loss, take_profit,
open_time, close_time, pips, profit, commission, swap,
status, strategy, timeframe, session, risk_reward, emotion,
entry_notes, exit_notes, lessons, tags
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
");
$baseTime = time() - (count($tradeDataList) * 86400 * 0.9);
$ticketBase = 7418290;
foreach ($tradeDataList as $i => $t) {
$ticket = (string)($ticketBase + $i * 17);
$openTs = $baseTime + ($i * 75000) + rand(1000, 15000);
$durationSec = rand(1800, 28800);
$closeTs = $openTs + $durationSec;
$openTimeStr = date('Y-m-d H:i:s', $openTs);
$closeTimeStr = date('Y-m-d H:i:s', $closeTs);
$stmtTrade->execute([
$accountId,
$ticket,
$t[0],
$t[1],
$t[2],
$t[3],
$t[4],
$t[5],
$t[6],
$openTimeStr,
$closeTimeStr,
$t[7],
$t[8],
$t[9],
$t[10],
'closed',
$t[11],
$t[12],
$t[13],
$t[14],
$t[15],
$t[16],
$t[17],
$t[18],
'Live,DayTrade,Forex'
]);
}
// Add 2 open trades
$stmtTrade->execute([
$accountId,
(string)($ticketBase + 991),
'XAUUSD',
'buy',
0.5,
2405.50,
null,
2396.00,
2425.00,
date('Y-m-d H:i:s', time() - 3600),
null,
45.0,
225.0,
-5.0,
0.0,
'open',
'ICT / SMC',
'H1',
'London',
2.0,
'Disciplined',
'ورود پس از شکست مقاومت ۲۴۰۰ و تثبیت بالای آن',
'',
'',
'OpenTrade,Swing'
]);
$issueId = $pdo->lastInsertId();
foreach ($item['timeline'] as $tl) {
$insertTimeline->execute([$issueId, $tl[0], $tl[1], $tl[2], $createdTime]);
}
foreach ($item['comments'] as $cm) {
$insertComment->execute([$issueId, $cm[0], $cm[1], $createdTime]);
}
$stmtTrade->execute([
$accountId,
(string)($ticketBase + 992),
'EURUSD',
'sell',
1.0,
1.08620,
null,
1.08850,
1.08100,
date('Y-m-d H:i:s', time() - 7200),
null,
12.0,
120.0,
-7.0,
0.0,
'open',
'Price Action',
'M15',
'London',
2.2,
'Confident',
'تشکیل الگوی پوشای نزولی در لول مقاومت روزانه',
'',
'',
'OpenTrade,Intraday'
]);
}
}
BIN
View File
Binary file not shown.
+1 -1
View File
@@ -1,3 +1,3 @@
<?php
// index.php - Main entry point
// Main entry fallback
require __DIR__ . '/router.php';
-338
View File
@@ -1,338 +0,0 @@
import os
import uuid
import random
from datetime import datetime
from typing import Optional, List
from fastapi import FastAPI, HTTPException, UploadFile, File, Form, Query
from fastapi.staticfiles import StaticFiles
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel
import shutil
from database import get_db_connection, init_db
app = FastAPI(title="سامانه هوشمند مدیریت مسائل شهری - شهرنگار")
# CORS setup
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
UPLOAD_DIR = os.path.join(os.path.dirname(__file__), "static", "uploads")
os.makedirs(UPLOAD_DIR, exist_ok=True)
# Initialize database on startup
@app.on_event("startup")
def on_startup():
init_db()
# Pydantic models
class IssueCreate(BaseModel):
title: str
category: str
description: str
address: str
district: int
priority: str = "medium"
lat: float = 35.6892
lng: float = 51.3890
image_url: Optional[str] = None
reporter_name: Optional[str] = "شهروند ناشناس"
reporter_phone: Optional[str] = None
class CommentCreate(BaseModel):
author_name: str
content: str
class StatusUpdate(BaseModel):
status: str
official_response: Optional[str] = None
timeline_title: Optional[str] = None
timeline_desc: Optional[str] = None
resolved_image_url: Optional[str] = None
# Routes
@app.get("/api/stats")
def get_stats():
conn = get_db_connection()
cursor = conn.cursor()
total = cursor.execute("SELECT COUNT(*) FROM issues").fetchone()[0]
pending = cursor.execute("SELECT COUNT(*) FROM issues WHERE status = 'pending'").fetchone()[0]
reviewing = cursor.execute("SELECT COUNT(*) FROM issues WHERE status = 'reviewing'").fetchone()[0]
in_progress = cursor.execute("SELECT COUNT(*) FROM issues WHERE status = 'in_progress'").fetchone()[0]
resolved = cursor.execute("SELECT COUNT(*) FROM issues WHERE status = 'resolved'").fetchone()[0]
upvotes = cursor.execute("SELECT COALESCE(SUM(upvotes), 0) FROM issues").fetchone()[0]
# Category counts
cat_rows = cursor.execute("SELECT category, COUNT(*) as cnt FROM issues GROUP BY category ORDER BY cnt DESC").fetchall()
categories = [{"category": row["category"], "count": row["cnt"]} for row in cat_rows]
# District counts
dist_rows = cursor.execute("SELECT district, COUNT(*) as cnt FROM issues GROUP BY district ORDER BY district ASC").fetchall()
districts = [{"district": row["district"], "count": row["cnt"]} for row in dist_rows]
# Priority counts
prio_rows = cursor.execute("SELECT priority, COUNT(*) as cnt FROM issues GROUP BY priority").fetchall()
priorities = {row["priority"]: row["cnt"] for row in prio_rows}
conn.close()
return {
"total": total,
"pending": pending,
"reviewing": reviewing,
"in_progress": in_progress,
"resolved": resolved,
"total_upvotes": upvotes,
"categories": categories,
"districts": districts,
"priorities": priorities,
"resolution_rate": round((resolved / total * 100) if total > 0 else 0, 1)
}
@app.get("/api/issues")
def list_issues(
category: Optional[str] = None,
status: Optional[str] = None,
district: Optional[int] = None,
priority: Optional[str] = None,
search: Optional[str] = None,
sort_by: Optional[str] = "newest"
):
conn = get_db_connection()
cursor = conn.cursor()
query = "SELECT * FROM issues WHERE 1=1"
params = []
if category and category != "all":
query += " AND category = ?"
params.append(category)
if status and status != "all":
query += " AND status = ?"
params.append(status)
if district and district > 0:
query += " AND district = ?"
params.append(district)
if priority and priority != "all":
query += " AND priority = ?"
params.append(priority)
if search:
query += " AND (title LIKE ? OR description LIKE ? OR address LIKE ? OR tracking_code LIKE ?)"
s_pattern = f"%{search}%"
params.extend([s_pattern, s_pattern, s_pattern, s_pattern])
if sort_by == "upvotes":
query += " ORDER BY upvotes DESC, id DESC"
elif sort_by == "oldest":
query += " ORDER BY id ASC"
elif sort_by == "emergency":
query += " ORDER BY CASE priority WHEN 'emergency' THEN 1 WHEN 'high' THEN 2 WHEN 'medium' THEN 3 ELSE 4 END, id DESC"
else: # newest
query += " ORDER BY id DESC"
rows = cursor.execute(query, params).fetchall()
issues = []
for r in rows:
item = dict(r)
# Fetch comment count
c_count = cursor.execute("SELECT COUNT(*) FROM comments WHERE issue_id = ?", (r["id"],)).fetchone()[0]
item["comment_count"] = c_count
issues.append(item)
conn.close()
return issues
@app.get("/api/issues/{issue_id}")
def get_issue(issue_id: int):
conn = get_db_connection()
cursor = conn.cursor()
issue_row = cursor.execute("SELECT * FROM issues WHERE id = ?", (issue_id,)).fetchone()
if not issue_row:
conn.close()
raise HTTPException(status_code=404, detail="گزارش مورد نظر یافت نشد.")
issue = dict(issue_row)
# Timeline
timeline_rows = cursor.execute("SELECT * FROM timeline WHERE issue_id = ? ORDER BY id ASC", (issue_id,)).fetchall()
issue["timeline"] = [dict(t) for t in timeline_rows]
# Comments
comment_rows = cursor.execute("SELECT * FROM comments WHERE issue_id = ? ORDER BY id DESC", (issue_id,)).fetchall()
issue["comments"] = [dict(c) for c in comment_rows]
conn.close()
return issue
@app.get("/api/track/{tracking_code}")
def track_issue(tracking_code: str):
conn = get_db_connection()
cursor = conn.cursor()
code = tracking_code.strip().upper()
issue_row = cursor.execute("SELECT * FROM issues WHERE UPPER(tracking_code) = ?", (code,)).fetchone()
if not issue_row:
conn.close()
raise HTTPException(status_code=404, detail=f"هیچ گزارشی با کد رهگیری «{tracking_code}» یافت نشد.")
issue_id = issue_row["id"]
issue = dict(issue_row)
timeline_rows = cursor.execute("SELECT * FROM timeline WHERE issue_id = ? ORDER BY id ASC", (issue_id,)).fetchall()
issue["timeline"] = [dict(t) for t in timeline_rows]
comment_rows = cursor.execute("SELECT * FROM comments WHERE issue_id = ? ORDER BY id DESC", (issue_id,)).fetchall()
issue["comments"] = [dict(c) for c in comment_rows]
conn.close()
return issue
@app.post("/api/issues")
def create_issue(payload: IssueCreate):
conn = get_db_connection()
cursor = conn.cursor()
# Generate unique tracking code
rand_num = random.randint(10000, 99999)
tracking_code = f"SHR-{rand_num}"
now_str = datetime.now().strftime("%Y-%m-%d %H:%M")
cursor.execute("""
INSERT INTO issues (
tracking_code, title, category, description, address, district, priority, status,
lat, lng, image_url, reporter_name, reporter_phone, upvotes, created_at
) VALUES (?, ?, ?, ?, ?, ?, ?, 'pending', ?, ?, ?, ?, ?, 1, ?)
""", (
tracking_code, payload.title, payload.category, payload.description, payload.address,
payload.district, payload.priority, payload.lat, payload.lng, payload.image_url,
payload.reporter_name or "شهروند", payload.reporter_phone or "", now_str
))
issue_id = cursor.lastrowid
# Add initial timeline event
cursor.execute("""
INSERT INTO timeline (issue_id, status, title, description, created_at)
VALUES (?, 'pending', 'ثبت اولیه گزارش در سامانه', 'گزارش شما با موفقیت در سامانه شهرنگار ثبت گردید و در نوبت بررسی کارشناسی قرار گرفت.', ?)
""", (issue_id, now_str))
conn.commit()
created = cursor.execute("SELECT * FROM issues WHERE id = ?", (issue_id,)).fetchone()
res = dict(created)
conn.close()
return res
@app.post("/api/issues/{issue_id}/upvote")
def upvote_issue(issue_id: int):
conn = get_db_connection()
cursor = conn.cursor()
cursor.execute("UPDATE issues SET upvotes = upvotes + 1 WHERE id = ?", (issue_id,))
if cursor.rowcount == 0:
conn.close()
raise HTTPException(status_code=404, detail="گزارش یافت نشد.")
conn.commit()
new_upvotes = cursor.execute("SELECT upvotes FROM issues WHERE id = ?", (issue_id,)).fetchone()[0]
conn.close()
return {"id": issue_id, "upvotes": new_upvotes}
@app.post("/api/issues/{issue_id}/comment")
def add_comment(issue_id: int, payload: CommentCreate):
conn = get_db_connection()
cursor = conn.cursor()
check = cursor.execute("SELECT id FROM issues WHERE id = ?", (issue_id,)).fetchone()
if not check:
conn.close()
raise HTTPException(status_code=404, detail="گزارش یافت نشد.")
now_str = datetime.now().strftime("%Y-%m-%d %H:%M")
cursor.execute("""
INSERT INTO comments (issue_id, author_name, content, created_at)
VALUES (?, ?, ?, ?)
""", (issue_id, payload.author_name or "شهروند محترم", payload.content, now_str))
conn.commit()
comment_id = cursor.lastrowid
comment = cursor.execute("SELECT * FROM comments WHERE id = ?", (comment_id,)).fetchone()
conn.close()
return dict(comment)
@app.patch("/api/issues/{issue_id}/status")
def update_issue_status(issue_id: int, payload: StatusUpdate):
conn = get_db_connection()
cursor = conn.cursor()
check = cursor.execute("SELECT * FROM issues WHERE id = ?", (issue_id,)).fetchone()
if not check:
conn.close()
raise HTTPException(status_code=404, detail="گزارش یافت نشد.")
now_str = datetime.now().strftime("%Y-%m-%d %H:%M")
resolved_at = now_str if payload.status == "resolved" else check["resolved_at"]
# Update issue
cursor.execute("""
UPDATE issues
SET status = ?,
official_response = COALESCE(?, official_response),
resolved_image_url = COALESCE(?, resolved_image_url),
resolved_at = ?
WHERE id = ?
""", (payload.status, payload.official_response, payload.resolved_image_url, resolved_at, issue_id))
# Add timeline entry if requested or automatically
status_titles = {
"pending": "در صف بررسی مجدد",
"reviewing": "بررسی کارشناسی و ارجاع به معاونت مربوطه",
"in_progress": "اعزام اکیپ اجرایی و آغاز عملیات میدانی",
"resolved": "اتمام عملیات و رفع کامل مسئله",
"rejected": "عدم احراز یا خارج از حیطه اختیارات شهرداری"
}
tl_title = payload.timeline_title or status_titles.get(payload.status, f"تغییر وضعیت به {payload.status}")
tl_desc = payload.timeline_desc or payload.official_response or "وضعیت پرونده توسط مدیریت سامانه به‌روزرسانی شد."
cursor.execute("""
INSERT INTO timeline (issue_id, status, title, description, created_at)
VALUES (?, ?, ?, ?, ?)
""", (issue_id, payload.status, tl_title, tl_desc, now_str))
conn.commit()
updated = cursor.execute("SELECT * FROM issues WHERE id = ?", (issue_id,)).fetchone()
res = dict(updated)
conn.close()
return res
@app.post("/api/upload")
async def upload_file(file: UploadFile = File(...)):
filename = f"{uuid.uuid4().hex[:10]}_{file.filename}"
filepath = os.path.join(UPLOAD_DIR, filename)
with open(filepath, "wb") as buffer:
shutil.copyfileobj(file.file, buffer)
return {"url": f"/static/uploads/{filename}"}
# Serve frontend static files
app.mount("/static", StaticFiles(directory=os.path.join(os.path.dirname(__file__), "static")), name="static")
@app.get("/")
def serve_index():
from fastapi.responses import FileResponse
return FileResponse(os.path.join(os.path.dirname(__file__), "static", "index.html"))
+42 -25
View File
@@ -1,45 +1,62 @@
<?php
// router.php - Front controller & router for PHP built-in server
/**
* Built-in PHP Router for Forex Trading Journal
*/
$uri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
$uri = urldecode(parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH));
// 1. Route API requests
if (strpos($uri, '/api/') === 0) {
// API Router
if (strpos($uri, '/api/') === 0 || $uri === '/api') {
require __DIR__ . '/api.php';
exit;
}
// 2. Serve static files if they exist
$filePath = __DIR__ . $uri;
if ($uri !== '/' && file_exists($filePath) && !is_dir($filePath)) {
$ext = pathinfo($filePath, PATHINFO_EXTENSION);
// Uploaded Images
if (strpos($uri, '/uploads/') === 0) {
$filePath = __DIR__ . $uri;
if (file_exists($filePath) && is_file($filePath)) {
$ext = strtolower(pathinfo($filePath, PATHINFO_EXTENSION));
$mimes = [
'jpg' => 'image/jpeg',
'jpeg' => 'image/jpeg',
'png' => 'image/png',
'webp' => 'image/webp',
'gif' => 'image/gif'
];
$mime = $mimes[$ext] ?? 'application/octet-stream';
header('Content-Type: ' . $mime);
readfile($filePath);
exit;
}
http_response_code(404);
echo "File not found";
exit;
}
// Static Files
$staticFile = __DIR__ . '/static' . $uri;
if (file_exists($staticFile) && is_file($staticFile)) {
$ext = strtolower(pathinfo($staticFile, PATHINFO_EXTENSION));
$mimes = [
'css' => 'text/css',
'js' => 'application/javascript',
'json' => 'application/json',
'css' => 'text/css; charset=utf-8',
'js' => 'application/javascript; charset=utf-8',
'json' => 'application/json; charset=utf-8',
'png' => 'image/png',
'jpg' => 'image/jpeg',
'jpeg' => 'image/jpeg',
'gif' => 'image/gif',
'svg' => 'image/svg+xml',
'ico' => 'image/x-icon',
'woff' => 'font/woff',
'woff2'=> 'font/woff2',
'ttf' => 'font/ttf',
'html' => 'text/html; charset=utf-8'
];
if (isset($mimes[$ext])) {
header('Content-Type: ' . $mimes[$ext]);
}
readfile($filePath);
$mime = $mimes[$ext] ?? 'text/plain; charset=utf-8';
header('Content-Type: ' . $mime);
readfile($staticFile);
exit;
}
// 3. Serve Frontend Application
if (file_exists(__DIR__ . '/static/index.html')) {
header('Content-Type: text/html; charset=utf-8');
readfile(__DIR__ . '/static/index.html');
exit;
}
echo "سامانه مدیریت مسائل شهری شهرنگار فعال است.";
// Default Single Page Application Entry
header('Content-Type: text/html; charset=utf-8');
readfile(__DIR__ . '/static/index.html');
+765 -725
View File
File diff suppressed because it is too large Load Diff
+1101
View File
File diff suppressed because it is too large Load Diff
+218
View File
@@ -0,0 +1,218 @@
/*
* Forex Trading Journal Pro • Myfxbook Style Theme
*/
@import url('https://fonts.googleapis.com/css2?family=Vazirmatn:wght@300;400;500;600;700;800;900&family=JetBrains+Mono:wght@400;500;700&display=swap');
:root {
--font-main: 'Vazirmatn', -apple-system, BlinkMacSystemFont, sans-serif;
--font-mono: 'JetBrains Mono', monospace;
/* Dark Theme Default */
--bg-main: #0B0F19;
--bg-card: #111827;
--bg-card-hover: #1F2937;
--bg-input: #1A2234;
--border-color: #1F293D;
--text-primary: #F3F4F6;
--text-secondary: #9CA3AF;
--text-muted: #6B7280;
--color-green: #10B981;
--color-green-soft: rgba(16, 185, 129, 0.15);
--color-red: #EF4444;
--color-red-soft: rgba(239, 68, 68, 0.15);
--color-blue: #3B82F6;
--color-blue-soft: rgba(59, 130, 246, 0.15);
--color-purple: #8B5CF6;
--color-amber: #F59E0B;
}
body.light-mode {
--bg-main: #F4F6F9;
--bg-card: #FFFFFF;
--bg-card-hover: #F8FAFC;
--bg-input: #EDF2F7;
--border-color: #E2E8F0;
--text-primary: #0F172A;
--text-secondary: #475569;
--text-muted: #94A3B8;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: var(--font-main);
background-color: var(--bg-main);
color: var(--text-primary);
direction: rtl;
min-height: 100vh;
overflow-x: hidden;
transition: background-color 0.25s ease, color 0.25s ease;
}
.num-font {
font-family: var(--font-mono);
direction: ltr;
display: inline-block;
}
/* Custom Scrollbar */
::-webkit-scrollbar {
width: 6px;
height: 6px;
}
::-webkit-scrollbar-track {
background: var(--bg-main);
}
::-webkit-scrollbar-thumb {
background: #374151;
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: #4B5563;
}
/* Cards & Glassmorphism */
.glass-card {
background-color: var(--bg-card);
border: 1px solid var(--border-color);
border-radius: 14px;
box-shadow: 0 4px 20px -2px rgba(0, 0, 0, 0.25);
transition: transform 0.15s ease, box-shadow 0.15s ease, border-color 0.15s ease;
}
.glass-card:hover {
border-color: rgba(59, 130, 246, 0.3);
}
.stat-card {
position: relative;
overflow: hidden;
}
.stat-card::after {
content: '';
position: absolute;
top: 0;
right: 0;
left: 0;
height: 2px;
background: linear-gradient(90deg, var(--color-blue), var(--color-purple));
opacity: 0;
transition: opacity 0.2s ease;
}
.stat-card:hover::after {
opacity: 1;
}
/* Tabs */
.nav-tab {
padding: 10px 18px;
font-weight: 600;
font-size: 0.92rem;
color: var(--text-secondary);
border-radius: 10px;
cursor: pointer;
transition: all 0.2s ease;
display: inline-flex;
align-items: center;
gap: 8px;
}
.nav-tab:hover {
color: var(--text-primary);
background-color: var(--bg-input);
}
.nav-tab.active {
color: #FFFFFF;
background: linear-gradient(135deg, #2563EB, #1D4ED8);
box-shadow: 0 4px 14px rgba(37, 99, 235, 0.35);
}
/* Badges */
.badge-buy {
background-color: var(--color-green-soft);
color: var(--color-green);
border: 1px solid rgba(16, 185, 129, 0.3);
font-weight: 700;
}
.badge-sell {
background-color: var(--color-red-soft);
color: var(--color-red);
border: 1px solid rgba(239, 68, 68, 0.3);
font-weight: 700;
}
.profit-pos {
color: var(--color-green);
font-weight: 700;
}
.profit-neg {
color: var(--color-red);
font-weight: 700;
}
/* Heatmap Grid Cell */
.heatmap-cell {
transition: transform 0.15s ease;
}
.heatmap-cell:hover {
transform: scale(1.05);
z-index: 10;
}
/* Modal Animations */
.modal-backdrop {
background-color: rgba(0, 0, 0, 0.75);
backdrop-filter: blur(4px);
transition: opacity 0.2s ease;
}
.modal-content {
background-color: var(--bg-card);
border: 1px solid var(--border-color);
border-radius: 16px;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.5);
animation: modalIn 0.25s cubic-bezier(0.16, 1, 0.3, 1);
}
@keyframes modalIn {
from {
opacity: 0;
transform: scale(0.95) translateY(10px);
}
to {
opacity: 1;
transform: scale(1) translateY(0);
}
}
/* Table styles */
.custom-table th {
background-color: rgba(17, 24, 39, 0.6);
color: var(--text-secondary);
font-weight: 600;
font-size: 0.82rem;
padding: 12px 14px;
border-bottom: 1px solid var(--border-color);
}
.custom-table td {
padding: 12px 14px;
border-bottom: 1px solid rgba(31, 41, 61, 0.4);
font-size: 0.88rem;
color: var(--text-primary);
}
.custom-table tr:hover td {
background-color: var(--bg-card-hover);
}
BIN
View File
Binary file not shown.