diff --git a/analytics.php b/analytics.php new file mode 100644 index 0000000..9d671a1 --- /dev/null +++ b/analytics.php @@ -0,0 +1,542 @@ +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 + ]; + } +} diff --git a/api.php b/api.php index bfe6d6d..3655a47 100644 --- a/api.php +++ b/api.php @@ -1,12 +1,12 @@ 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, ' $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('#]*>(.*?)#is', $content, $trMatches); + foreach ($trMatches[1] as $trHtml) { + preg_match_all('#]*>(.*?)#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); +} diff --git a/database.py b/database.py deleted file mode 100644 index 394e6df..0000000 --- a/database.py +++ /dev/null @@ -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)) diff --git a/db.php b/db.php index fcc524b..8685c9e 100644 --- a/db.php +++ b/db.php @@ -1,273 +1,299 @@ 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' + ]); } } diff --git a/forex_journal.db b/forex_journal.db new file mode 100644 index 0000000..84020d4 Binary files /dev/null and b/forex_journal.db differ diff --git a/index.php b/index.php index 7246437..13181e2 100644 --- a/index.php +++ b/index.php @@ -1,3 +1,3 @@ 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")) diff --git a/router.php b/router.php index 9f5f395..7fb2672 100644 --- a/router.php +++ b/router.php @@ -1,45 +1,62 @@ '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'); diff --git a/static/index.html b/static/index.html index 3dacfab..c0825c7 100644 --- a/static/index.html +++ b/static/index.html @@ -1,763 +1,803 @@ - - - شهرنگار | سامانه جامع مدیریت و رسیدگی به مسائل شهری - - - - - - - - - - - - - - - - - - - - - - - - + + + FX Journal Pro • ژورنال تخصصی و تحلیلی فارکس + + + + + + + + + - + - -
-
- سامانه برخط ۱۳۷ - همراهی شما برای شهری زیباتر، ایمن‌تر و پاک‌تر • پاسخگویی ۲۴ ساعته -
- -
- - -
-
-
- - -
-
- -
-
-
- شهرنگار - نسخه هوشمند + +
+
+ + +
+
+ +
+
+

+ FX Journal Pro + مای‌اف‌ایکس‌بوک +

+

سامانه جامع ژورنال‌نویسی و تحلیل پیشرفته معاملات

+
-

سامانه یکپارچه گزارش و نظارت مردمی بر مسائل شهری

-
+ + +
+
+ + حساب فعال: + + +
+ + +
+ + + + + + + +
+
+ +
+
+ + +
+ + +
+
- - - - -
- -
- - - - - -
- - - - -
- - - -
-
-
- - -
-
- - صدای شما، اقدام سریع شهرداری و بازرسان شهری -
-

- شهر خود را با مشارکت هوشمند، - زیباتر و امن‌تر بسازید -

-

- خرابی آسفالت، سد معبر، نقص روشنایی پارک‌ها، انباشت پسماند یا مشکلات ترافیکی را در کمتر از ۱ دقیقه گزارش کنید و وضعیت رسیدگی را مرحله به مرحله رصد نمایید. -

- - -
-
- - -
- -
-
- - -
-
-
- کل موارد ثبت‌شده -
- -
-
-
--
-
گزارش‌های شهروندی
-
- -
-
- مسائل رفع شده -
- -
-
-
--
-
نرخ تکمیل: --٪
-
- -
-
- در حال عملیات و اجرا -
- -
-
-
--
-
اکیپ مستقر در محل
-
- -
-
- تایید و همراهی شهروندان -
- -
-
-
--
-
رأی و حمایت مردمی
-
-
- -
-
-
- - -
-
- دسته‌بندی‌ها: - - - - - - - -
-
- - -
- - -
- - -
- -
- -
- - -
- - -
- - -
- - -
- - -
-
- - -
- - -
- -
- - -
- -
- - -
-
- -
-

در حال بارگذاری گزارش‌های شهری...

-
- - - -
- - - - - - - - -
- - -
+ + + + + - - -