function sendMessage($chatId, $text, $keyboard = null) { $data = [ 'chat_id' => $chatId, 'text' => $text, 'parse_mode' => 'HTML' ]; if ($keyboard !== null) { $data['reply_markup'] = json_encode( $keyboard, JSON_UNESCAPED_UNICODE ); } return botRequest('sendMessage', $data); } function editMessage($chatId, $messageId, $text, $keyboard = null) { $data = [ 'chat_id' => $chatId, 'message_id' => $messageId, 'text' => $text, 'parse_mode' => 'HTML' ]; if ($keyboard !== null) { $data['reply_markup'] = json_encode( $keyboard, JSON_UNESCAPED_UNICODE ); } return botRequest('editMessageText', $data); } function answerCallback($callbackId, $text = '') { $data = [ 'callback_query_id' => $callbackId ]; if ($text !== '') { $data['text'] = $text; } return botRequest('answerCallbackQuery', $data); } /* ========================================================= General Helpers ========================================================= */ function html($text) { return htmlspecialchars( (string)$text, ENT_QUOTES, 'UTF-8' ); } function money($number) { return number_format( (float)$number, 0, '.', ',' ) . ' تومان'; } function faNumber($number) { return strtr( (string)$number, [ '0' => '۰', '1' => '۱', '2' => '۲', '3' => '۳', '4' => '۴', '5' => '۵', '6' => '۶', '7' => '۷', '8' => '۸', '9' => '۹' ] ); } function normalizeNumber($value) { $value = strtr( trim((string)$value), [ '۰' => '0', '۱' => '1', '۲' => '2', '۳' => '3', '۴' => '4', '۵' => '5', '۶' => '6', '۷' => '7', '۸' => '8', '۹' => '9', '٠' => '0', '١' => '1', '٢' => '2', '٣' => '3', '٤' => '4', '٥' => '5', '٦' => '6', '٧' => '7', '٨' => '8', '٩' => '9', ',' => '', '٬' => '', ' ' => '' ] ); return $value; } function cleanText($value) { return trim( preg_replace( '/\s+/u', ' ', (string)$value ) ); } /* ========================================================= Settings ========================================================= */ function getSetting($key, $default = 0) { global $pdo; $stmt = $pdo->prepare( "SELECT setting_value FROM settings WHERE setting_key = ? LIMIT 1" ); $stmt->execute([$key]); $row = $stmt->fetch(); if (!$row) { return $default; } return $row['setting_value']; } function setSetting($key, $value) { global $pdo; $stmt = $pdo->prepare( "INSERT INTO settings (setting_key, setting_value) VALUES (?, ?) ON DUPLICATE KEY UPDATE setting_value = VALUES(setting_value)" ); return $stmt->execute([ $key, $value ]); } /* ========================================================= Database Compatibility ========================================================= */ function ensureColumn($table, $column, $definition) { global $pdo; $allowedTables = [ 'orders', 'customers', 'products', 'settings' ]; if (!in_array($table, $allowedTables, true)) { return; } // SHOW COLUMNS ... LIKE ? در بعضی نسخه‌های MariaDB // با prepared statement خطای SQL می‌دهد. // بنابراین ستون‌ها را بدون placeholder بررسی می‌کنیم. $table = preg_replace('/[^a-zA-Z0-9_]/', '', $table); $column = preg_replace('/[^a-zA-Z0-9_]/', '', $column); $stmt = $pdo->query( "SHOW COLUMNS FROM `$table`" ); $columns = $stmt->fetchAll(PDO::FETCH_ASSOC); foreach ($columns as $row) { if (isset($row['Field']) && $row['Field'] === $column) { return; } } $pdo->exec( "ALTER TABLE `$table` ADD COLUMN `$column` $definition" ); } function initializeDatabase() { global $pdo; ensureColumn( 'orders', 'spring_type', "VARCHAR(30) NULL" ); ensureColumn( 'orders', 'staple_type', "VARCHAR(30) NULL" ); ensureColumn( 'orders', 'extra_amount', "BIGINT UNSIGNED NOT NULL DEFAULT 0" ); ensureColumn( 'orders', 'extra_reason', "TEXT NULL" ); $settings = [ 'spring_with_cover_price' => 0, 'spring_without_cover_price' => 0, 'staple_double_price' => 0, 'staple_triple_price' => 0, 'label_price' => 0, 'low_stock' => 5 ]; foreach ($settings as $key => $value) { $stmt = $pdo->prepare( "INSERT IGNORE INTO settings (setting_key, setting_value) VALUES (?, ?)" ); $stmt->execute([ $key, $value ]); } } initializeDatabase(); /* ========================================================= Admin State ========================================================= */ function getState($adminId) { global $pdo; $stmt = $pdo->prepare( "SELECT * FROM admin_state WHERE admin_id = ? LIMIT 1" ); $stmt->execute([ $adminId ]); $row = $stmt->fetch(); if (!$row) { return null; } $data = []; if (!empty($row['data'])) { $decoded = json_decode( $row['data'], true ); if (is_array($decoded)) { $data = $decoded; } } return [ 'state' => $row['state'], 'data' => $data ]; } function setState($adminId, $state, $data = []) { global $pdo; $json = json_encode( $data, JSON_UNESCAPED_UNICODE ); $stmt = $pdo->prepare( "INSERT INTO admin_state (admin_id, state, data) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE state = VALUES(state), data = VALUES(data), updated_at = CURRENT_TIMESTAMP" ); return $stmt->execute([ $adminId, $state, $json ]); } function clearState($adminId) { global $pdo; $stmt = $pdo->prepare( "DELETE FROM admin_state WHERE admin_id = ?" ); return $stmt->execute([ $adminId ]); } /* ========================================================= Keyboards ========================================================= */ function mainKeyboard() { return [ 'keyboard' => [ [ ['text' => '📝 ورودی جدید'], ['text' => '🧾 فاکتور جدید'] ], [ ['text' => '📊 آمار و گزارشات'], ['text' => '⚙️ تنظیمات کلی'] ], [ ['text' => '👥 مشتریان'], ['text' => '📦 انبار'] ], [ ['text' => '💰 دخل و پرداخت‌ها'], ['text' => '📈 سود و زیان'] ], [ ['text' => '🗃 سفارشات'], ['text' => '🖨 گزارش A4'] ] ], 'resize_keyboard' => true ]; } function backKeyboard() { return [ 'keyboard' => [ [ ['text' => '🔙 بازگشت'] ] ], 'resize_keyboard' => true ]; } function settingsKeyboard() { return [ 'keyboard' => [ [ ['text' => '📕 فنربندی با جلد'], ['text' => '📗 فنربندی بی‌جلد'] ], [ ['text' => '📎 منگنه دوتایی'], ['text' => '📎 منگنه سه‌تایی'] ], [ ['text' => '🏷 قیمت لیبل'], ['text' => '📦 حداقل موجودی'] ], [ ['text' => '🔙 بازگشت'] ] ], 'resize_keyboard' => true ]; } function inventoryKeyboard() { return [ 'keyboard' => [ [ ['text' => '📋 مشاهده موجودی'] ], [ ['text' => '➕ ثبت خرید/ورودی'] ], [ ['text' => '🔙 بازگشت'] ] ], 'resize_keyboard' => true ]; } function cashKeyboard() { return [ 'keyboard' => [ [ ['text' => '💵 ثبت دریافت'], ['text' => '💸 ثبت هزینه'] ], [ ['text' => '📊 دخل امروز'] ], [ ['text' => '🔙 بازگشت'] ] ], 'resize_keyboard' => true ]; } function reportKeyboard() { return [ 'keyboard' => [ [ ['text' => '📅 امروز'], ['text' => '📅 دیروز'] ], [ ['text' => '📅 ۷ روز اخیر'], ['text' => '📅 ۳۰ روز اخیر'] ], [ ['text' => '🗓 انتخاب بازه'] ], [ ['text' => '🔙 بازگشت'] ] ], 'resize_keyboard' => true ]; } /* ========================================================= Main Menu ========================================================= */ function showMainMenu($chatId) { clearState($chatId); sendMessage( $chatId, '🏠 دفترفنی جهان' . "\n\n" . 'لطفاً یکی از گزینه‌های زیر را انتخاب کنید:', mainKeyboard() ); } /* ========================================================= Settings ========================================================= */ function showSettings($chatId) { $springCover = getSetting( 'spring_with_cover_price' ); $springNoCover = getSetting( 'spring_without_cover_price' ); $stapleDouble = getSetting( 'staple_double_price' ); $stapleTriple = getSetting( 'staple_triple_price' ); $label = getSetting( 'label_price' ); $lowStock = getSetting( 'low_stock' ); $text = "⚙️ تنظیمات کلی\n\n" . "📕 فنربندی با جلد: " . html(money($springCover)) . "\n\n" . "📗 فنربندی بی‌جلد: " . html(money($springNoCover)) . "\n\n" . "📎 منگنه دوتایی: " . html(money($stapleDouble)) . "\n\n" . "📎 منگنه سه‌تایی: " . html(money($stapleTriple)) . "\n\n" . "🏷 قیمت هر برگه لیبل: " . html(money($label)) . "\n\n" . "📦 حداقل موجودی: " . faNumber($lowStock) . ""; sendMessage( $chatId, $text, settingsKeyboard() ); } /* ========================================================= Order Calculation ========================================================= */ function calculateOrderTotal($data) { $springCoverPrice = (int)getSetting( 'spring_with_cover_price' ); $springNoCoverPrice = (int)getSetting( 'spring_without_cover_price' ); $stapleDoublePrice = (int)getSetting( 'staple_double_price' ); $stapleTriplePrice = (int)getSetting( 'staple_triple_price' ); $labelPrice = (int)getSetting( 'label_price' ); $springs = (int)($data['springs'] ?? 0); $staples = (int)($data['staples'] ?? 0); $labelSheets = (int)($data['label_sheets'] ?? 0); $extraAmount = (int)($data['extra_amount'] ?? 0); $springUnit = 0; if ( !empty($data['spring_type']) && $data['spring_type'] === 'with_cover' ) { $springUnit = $springCoverPrice; } if ( !empty($data['spring_type']) && $data['spring_type'] === 'without_cover' ) { $springUnit = $springNoCoverPrice; } $stapleUnit = 0; if ( !empty($data['staple_type']) && $data['staple_type'] === 'double' ) { $stapleUnit = $stapleDoublePrice; } if ( !empty($data['staple_type']) && $data['staple_type'] === 'triple' ) { $stapleUnit = $stapleTriplePrice; } $springCost = $springs * $springUnit; $stapleCost = $staples * $stapleUnit; /* * اولین برگه لیبل رایگان است */ $chargeableLabels = max( 0, $labelSheets - 1 ); $labelCost = $chargeableLabels * $labelPrice; $total = $springCost + $stapleCost + $labelCost + $extraAmount; return [ 'spring_unit' => $springUnit, 'spring_cost' => $springCost, 'staple_unit' => $stapleUnit, 'staple_cost' => $stapleCost, 'chargeable_labels' => $chargeableLabels, 'label_cost' => $labelCost, 'extra_amount' => $extraAmount, 'total' => $total ]; } /* ========================================================= New Order ========================================================= */ function startNewOrder($chatId) { clearState($chatId); setState( $chatId, 'order_name', [] ); sendMessage( $chatId, '📝 ورودی جدید' . "\n\n" . 'نام مشتری را وارد کنید:', backKeyboard() ); } /* ========================================================= Order Confirmation ========================================================= */ function showOrderConfirmation($chatId, $data) { $calc = calculateOrderTotal($data); $springType = 'ندارد'; if ( ($data['spring_type'] ?? '') === 'with_cover' ) { $springType = 'فنربندی با جلد'; } if ( ($data['spring_type'] ?? '') === 'without_cover' ) { $springType = 'فنربندی بی‌جلد'; } $stapleType = 'ندارد'; if ( ($data['staple_type'] ?? '') === 'double' ) { $stapleType = 'منگنه دوتایی'; } if ( ($data['staple_type'] ?? '') === 'triple' ) { $stapleType = 'منگنه سه‌تایی'; } $deposit = (int)( $data['deposit'] ?? 0 ); $remaining = max( 0, $calc['total'] - $deposit ); $text = "🧾 پیش‌نمایش فاکتور\n\n" . "👤 نام: " . html($data['name']) . "\n" . "📱 تلفن: " . html($data['phone']) . "\n" . "🎓 تحصیلات: " . html($data['education']) . "\n\n" . "📚 تعداد کتاب: " . faNumber($data['books']) . "\n\n" . "📕 فنربندی: " . faNumber($data['springs']) . "\n" . "نوع فنربندی: " . html($springType) . "\n" . "قیمت واحد فنر: " . money($calc['spring_unit']) . "\n" . "هزینه فنربندی: " . money($calc['spring_cost']) . "\n\n" . "📎 منگنه: " . faNumber($data['staples']) . "\n" . "نوع منگنه: " . html($stapleType) . "\n" . "قیمت واحد منگنه: " . money($calc['staple_unit']) . "\n" . "هزینه منگنه: " . money($calc['staple_cost']) . "\n"; if (!empty($data['staple_names'])) { $text .= "اسامی منگنه: " . html($data['staple_names']) . "\n"; } $text .= "\n🏷 شماره لیبل: " . html($data['label_number']) . "\n" . "🏷 تعداد برگه لیبل: " . faNumber($data['label_sheets']) . "\n" . "برگه مشمول هزینه: " . faNumber($calc['chargeable_labels']) . "\n" . "هزینه لیبل: " . money($calc['label_cost']) . "\n"; if ($calc['extra_amount'] > 0) { $text .= "\n➕ مبلغ اضافی: " . money($calc['extra_amount']) . "\n" . "بابت: " . html( $data['extra_reason'] ?? '' ) . "\n"; } $text .= "\n💰 مجموع: " . money($calc['total']) . "\n" . "💵 دریافتی: " . money($deposit) . "\n" . "💳 مانده: " . money($remaining) . "\n"; if (!empty($data['description'])) { $text .= "\n📝 توضیحات:\n" . html($data['description']) . "\n"; } $keyboard = [ 'keyboard' => [ [ ['text' => '✅ ثبت سفارش'], ['text' => '❌ لغو'] ] ], 'resize_keyboard' => true ]; sendMessage( $chatId, $text, $keyboard ); } /* ========================================================= Save Order ========================================================= */ function saveOrder($chatId, $data) { global $pdo; $calc = calculateOrderTotal($data); $deposit = (int)( $data['deposit'] ?? 0 ); $remaining = max( 0, $calc['total'] - $deposit ); try { $pdo->beginTransaction(); /* * Customer */ $stmt = $pdo->prepare( "SELECT id FROM customers WHERE phone = ? LIMIT 1" ); $stmt->execute([ $data['phone'] ]); $customer = $stmt->fetch(); if ($customer) { $customerId = (int)$customer['id']; $stmt = $pdo->prepare( "UPDATE customers SET name = ?, education = ?, order_count = order_count + 1, sales_total = sales_total + ?, debt = debt + ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?" ); $stmt->execute([ $data['name'], $data['education'], $calc['total'], $remaining, $customerId ]); } else { $stmt = $pdo->prepare( "INSERT INTO customers ( name, phone, education, order_count, sales_total, debt ) VALUES (?, ?, ?, 1, ?, ?)" ); $stmt->execute([ $data['name'], $data['phone'], $data['education'], $calc['total'], $remaining ]); $customerId = (int)$pdo->lastInsertId(); } /* * Order */ $stmt = $pdo->prepare( "INSERT INTO orders ( customer_id, name, phone, education, books, springs, spring_size, spring_type, staples, staple_type, staple_names, label_number, label_sheets, total, deposit, remaining, extra_amount, extra_reason, description, status ) VALUES ( ?, ?, ?, ?, ?, ?, '', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'pending' )" ); $stmt->execute([ $customerId, $data['name'], $data['phone'], $data['education'], (int)$data['books'], (int)$data['springs'], $data['spring_type'] ?? null, (int)$data['staples'], $data['staple_type'] ?? null, $data['staple_names'] ?? '', $data['label_number'] ?? '', (int)$data['label_sheets'], $calc['total'], $deposit, $remaining, $calc['extra_amount'], $data['extra_reason'] ?? '', $data['description'] ?? '' ]); $orderId = (int)$pdo->lastInsertId(); /* * Payment */ if ($deposit > 0) { $stmt = $pdo->prepare( "INSERT INTO payments ( order_id, customer_id, type, amount, description ) VALUES (?, ?, 'income', ?, ?)" ); $stmt->execute([ $orderId, $customerId, $deposit, 'دریافت بابت سفارش #' . $orderId ]); } /* * Stock movement */ $productMap = [ 'فنر' => (int)$data['springs'], 'منگنه دوتایی' => ( ($data['staple_type'] ?? '') === 'double' ? (int)$data['staples'] : 0 ), 'منگنه سه‌تایی' => ( ($data['staple_type'] ?? '') === 'triple' ? (int)$data['staples'] : 0 ), 'لیبل' => max( 0, (int)$data['label_sheets'] - 1 ) ]; foreach ($productMap as $productName => $qty) { if ($qty <= 0) { continue; } $stmt = $pdo->prepare( "SELECT id FROM products WHERE name = ? LIMIT 1" ); $stmt->execute([ $productName ]); $product = $stmt->fetch(); if (!$product) { continue; } $productId = (int)$product['id']; $stmt = $pdo->prepare( "UPDATE products SET stock = GREATEST( 0, stock - ? ) WHERE id = ?" ); $stmt->execute([ $qty, $productId ]); $stmt = $pdo->prepare( "INSERT INTO stock_movements ( product_id, type, quantity, description ) VALUES (?, 'out', ?, ?)" ); $stmt->execute([ $productId, $qty, 'مصرف سفارش #' . $orderId ]); } $pdo->commit(); clearState($chatId); sendMessage( $chatId, "✅ سفارش با موفقیت ثبت شد.\n\n" . "شماره سفارش: #" . faNumber($orderId) . "\n" . "مبلغ کل: " . money($calc['total']) . "\n" . "دریافتی: " . money($deposit) . "\n" . "مانده: " . money($remaining) . "", mainKeyboard() ); } catch (Throwable $e) { if ($pdo->inTransaction()) { $pdo->rollBack(); } error_log( 'Save order error: ' . $e->getMessage() ); sendMessage( $chatId, '❌ خطا در ثبت سفارش.' . "\n\n" . 'خطا در سیستم ثبت شده است.' ); } } /* ========================================================= Report Date Helpers ========================================================= */ function reportDateRange($type) { $today = new DateTime( 'today', new DateTimeZone('Asia/Tehran') ); if ($type === 'today') { $from = clone $today; $to = clone $today; } elseif ($type === 'yesterday') { $from = clone $today; $from->modify('-1 day'); $to = clone $from; } elseif ($type === '7days') { $from = clone $today; $from->modify('-6 days'); $to = clone $today; } elseif ($type === '30days') { $from = clone $today; $from->modify('-29 days'); $to = clone $today; } else { $from = clone $today; $to = clone $today; } return [ 'from' => $from->format('Y-m-d'), 'to' => $to->format('Y-m-d') ]; } /* * ورودی تاریخ را قبول می‌کند: * * 1405/06/24 * 1405-06-24 * 2026/09/15 * 2026-09-15 * * اگر شمسی باشد به میلادی تبدیل می‌شود. */ function gregorianToJalali( $gy, $gm, $gd ) { $g_d_m = [ 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334 ]; if ($gy > 1600) { $jy = 979; $gy -= 1600; $gm -= 1; $gd -= 1; } else { $jy = 0; $gy -= 621; $gm -= 1; $gd -= 1; } $gy2 = ($gm > 1) ? $gy + 1 : $gy; $days = 365 * $gy + floor(($gy2 + 3) / 4) - floor(($gy2 + 99) / 100) + floor(($gy2 + 399) / 400) - 80 + $gd + $g_d_m[$gm]; $jy += 33 * floor($days / 12053); $days %= 12053; $jy += 4 * floor($days / 1461); $days %= 1461; if ($days > 365) { $jy += floor( ($days - 1) / 365 ); $days = ($days - 1) % 365; } if ($days < 186) { $jm = 1 + floor($days / 31); $jd = 1 + ($days % 31); } else { $jm = 7 + floor( ($days - 186) / 30 ); $jd = 1 + (($days - 186) % 30); } return [ $jy, $jm, $jd ]; } function jalaliToGregorian( $jy, $jm, $jd ) { $jy += 1595; $days = -355668 + (365 * $jy) + floor($jy / 33) * 8 + floor( (($jy % 33) + 3) / 4 ) + $jd; if ($jm < 7) { $days += ($jm - 1) * 31; } else { $days += (($jm - 7) * 30) + 186; } $gy = 400 * floor( $days / 146097 ); $days %= 146097; if ($days > 36524) { $gy += 100 * floor( --$days / 36524 ); $days %= 36524; if ($days >= 365) { $days++; } } $gy += 4 * floor( $days / 1461 ); $days %= 1461; if ($days > 365) { $gy += floor( ($days - 1) / 365 ); $days = ($days - 1) % 365; } $gd = $days + 1; $sal_a = [ 0, 31, ( ($gy % 4 == 0 && $gy % 100 != 0) || ($gy % 400 == 0) ) ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 ]; $gm = 0; while ( $gm < 13 && $gd > $sal_a[$gm] ) { $gd -= $sal_a[$gm]; $gm++; } return [ $gy, $gm, $gd ]; } function normalizeReportDate($value) { $value = normalizeNumber($value); $value = str_replace( '/', '-', $value ); if ( !preg_match( '/^(\d{4})-(\d{1,2})-(\d{1,2})$/', $value, $m ) ) { return false; } $year = (int)$m[1]; $month = (int)$m[2]; $day = (int)$m[3]; /* * سال‌های شمسی */ if ($year >= 1300 && $year <= 1500) { [$gy, $gm, $gd] = jalaliToGregorian( $year, $month, $day ); return sprintf( '%04d-%02d-%02d', $gy, $gm, $gd ); } /* * سال میلادی */ if ($year >= 1900 && $year <= 2200) { if (!checkdate( $month, $day, $year )) { return false; } return sprintf( '%04d-%02d-%02d', $year, $month, $day ); } return false; } function formatJalaliDate($date) { $timestamp = strtotime( $date ); if (!$timestamp) { return $date; } [$jy, $jm, $jd] = gregorianToJalali( (int)date('Y', $timestamp), (int)date('n', $timestamp), (int)date('j', $timestamp) ); return faNumber( sprintf( '%04d/%02d/%02d', $jy, $jm, $jd ) ); } /* ========================================================= REPORT DATA ========================================================= */ function getSalesReport( $from, $to ) { global $pdo; $fromDate = $from . ' 00:00:00'; $toDate = $to . ' 23:59:59'; /* * سفارش‌ها */ $stmt = $pdo->prepare( "SELECT * FROM orders WHERE created_at >= ? AND created_at <= ? AND status <> 'cancelled' ORDER BY created_at ASC" ); $stmt->execute([ $fromDate, $toDate ]); $orders = $stmt->fetchAll(); /* * خلاصه */ $summary = [ 'orders' => count($orders), 'sales' => 0, 'received' => 0, 'remaining' => 0, 'spring_with_cover_count' => 0, 'spring_with_cover_amount' => 0, 'spring_without_cover_count' => 0, 'spring_without_cover_amount' => 0, 'staple_double_count' => 0, 'staple_double_amount' => 0, 'staple_triple_count' => 0, 'staple_triple_amount' => 0, 'label_count' => 0, 'label_amount' => 0, 'extra_amount' => 0, 'books' => 0 ]; foreach ($orders as $order) { $summary['sales'] += (int)$order['total']; $summary['received'] += (int)$order['deposit']; $summary['remaining'] += (int)$order['remaining']; $summary['books'] += (int)$order['books']; $springs = (int)$order['springs']; $staples = (int)$order['staples']; $labels = max( 0, (int)$order['label_sheets'] - 1 ); $extra = (int)$order['extra_amount']; /* * فنربندی */ if ( $springs > 0 && $order['spring_type'] === 'with_cover' ) { $unit = (int)getSetting( 'spring_with_cover_price' ); $summary[ 'spring_with_cover_count' ] += $springs; $summary[ 'spring_with_cover_amount' ] += $springs * $unit; } if ( $springs > 0 && $order['spring_type'] === 'without_cover' ) { $unit = (int)getSetting( 'spring_without_cover_price' ); $summary[ 'spring_without_cover_count' ] += $springs; $summary[ 'spring_without_cover_amount' ] += $springs * $unit; } /* * منگنه */ if ( $staples > 0 && $order['staple_type'] === 'double' ) { $unit = (int)getSetting( 'staple_double_price' ); $summary[ 'staple_double_count' ] += $staples; $summary[ 'staple_double_amount' ] += $staples * $unit; } if ( $staples > 0 && $order['staple_type'] === 'triple' ) { $unit = (int)getSetting( 'staple_triple_price' ); $summary[ 'staple_triple_count' ] += $staples; $summary[ 'staple_triple_amount' ] += $staples * $unit; } /* * لیبل */ if ($labels > 0) { $unit = (int)getSetting( 'label_price' ); $summary[ 'label_count' ] += $labels; $summary[ 'label_amount' ] += $labels * $unit; } /* * مبلغ اضافی */ $summary[ 'extra_amount' ] += $extra; } /* * هزینه‌ها */ $stmt = $pdo->prepare( "SELECT COALESCE( SUM(amount), 0 ) AS total FROM expenses WHERE created_at >= ? AND created_at <= ?" ); $stmt->execute([ $fromDate, $toDate ]); $expenses = (int)$stmt->fetchColumn(); /* * خرید انبار */ $stmt = $pdo->prepare( "SELECT COALESCE( SUM(total), 0 ) AS total FROM purchases WHERE created_at >= ? AND created_at <= ?" ); $stmt->execute([ $fromDate, $toDate ]); $purchases = (int)$stmt->fetchColumn(); /* * سود تقریبی */ $profit = $summary['sales'] - $expenses - $purchases; return [ 'from' => $from, 'to' => $to, 'orders' => $orders, 'summary' => $summary, 'expenses' => $expenses, 'purchases' => $purchases, 'profit' => $profit ]; } /* ========================================================= Report Telegram Summary ========================================================= */ function sendReportSummary( $chatId, $report ) { $s = $report['summary']; $text = "📊 گزارش فروش\n\n" . "📅 از " . formatJalaliDate($report['from']) . " تا " . formatJalaliDate($report['to']) . "\n\n" . "🧾 تعداد سفارش‌ها: " . faNumber($s['orders']) . "\n" . "📚 تعداد کتاب‌ها: " . faNumber($s['books']) . "\n\n" . "💰 مجموع فروش: " . money($s['sales']) . "\n" . "💵 دریافت‌شده: " . money($s['received']) . "\n" . "💳 مانده: " . money($s['remaining']) . "\n\n" . "📕 فنربندی با جلد\n" . "تعداد: " . faNumber( $s['spring_with_cover_count'] ) . "\n" . "مبلغ: " . money( $s['spring_with_cover_amount'] ) . "\n\n" . "📗 فنربندی بی‌جلد\n" . "تعداد: " . faNumber( $s['spring_without_cover_count'] ) . "\n" . "مبلغ: " . money( $s['spring_without_cover_amount'] ) . "\n\n" . "📎 منگنه دوتایی\n" . "تعداد: " . faNumber( $s['staple_double_count'] ) . "\n" . "مبلغ: " . money( $s['staple_double_amount'] ) . "\n\n" . "📎 منگنه سه‌تایی\n" . "تعداد: " . faNumber( $s['staple_triple_count'] ) . "\n" . "مبلغ: " . money( $s['staple_triple_amount'] ) . "\n\n" . "🏷 لیبل\n" . "برگه مشمول هزینه: " . faNumber( $s['label_count'] ) . "\n" . "مبلغ: " . money( $s['label_amount'] ) . "\n\n" . "➕ مبالغ اضافی\n" . money( $s['extra_amount'] ) . "\n\n" . "💸 هزینه‌ها: " . money($report['expenses']) . "\n" . "📦 خرید انبار: " . money($report['purchases']) . "\n\n" . "📈 سود تقریبی: " . money($report['profit']) . "\n\n" . "برای دریافت گزارش کامل A4، " . "دکمه «🖨 دریافت گزارش A4» را بزنید."; $keyboard = [ 'keyboard' => [ [ [ 'text' => '🖨 دریافت گزارش A4' ] ], [ [ 'text' => '🔙 بازگشت' ] ] ], 'resize_keyboard' => true ]; sendMessage( $chatId, $text, $keyboard ); } /* ========================================================= Report Word / A4 ========================================================= */ function createA4ReportFile($report) { $s = $report['summary']; $fileName = 'gozaresh-' . $report['from'] . '-to-' . $report['to'] . '.doc'; $path = sys_get_temp_dir() . DIRECTORY_SEPARATOR . $fileName; $rows = ''; foreach ( $report['orders'] as $order ) { $types = []; if ( (int)$order['springs'] > 0 ) { if ( $order['spring_type'] === 'with_cover' ) { $types[] = 'فنربندی با جلد'; } elseif ( $order['spring_type'] === 'without_cover' ) { $types[] = 'فنربندی بی‌جلد'; } } if ( (int)$order['staples'] > 0 ) { if ( $order['staple_type'] === 'double' ) { $types[] = 'منگنه دوتایی'; } elseif ( $order['staple_type'] === 'triple' ) { $types[] = 'منگنه سه‌تایی'; } } if ( (int)$order['label_sheets'] > 0 ) { $types[] = 'لیبل'; } if ( (int)$order['extra_amount'] > 0 ) { $types[] = 'مبلغ اضافی'; } $typeText = implode( ' + ', $types ); $rows .= '' . '' . html($order['id']) . '' . '' . html($order['name']) . '' . '' . html($order['phone']) . '' . '' . html($typeText) . '' . '' . faNumber( $order['books'] ) . '' . '' . money( $order['total'] ) . '' . '' . money( $order['deposit'] ) . '' . '' . money( $order['remaining'] ) . '' . '' . formatJalaliDate( substr( $order['created_at'], 0, 10 ) ) . '' . ''; } $html = '

گزارش فروش دفترفنی جهان

گزارش از ' . formatJalaliDate( $report['from'] ) . ' تا ' . formatJalaliDate( $report['to'] ) . '

خلاصه مالی

عنوان مبلغ
مجموع فروش ' . money($s['sales']) . '
دریافت‌شده ' . money($s['received']) . '
مانده ' . money($s['remaining']) . '
هزینه‌ها ' . money( $report['expenses'] ) . '
خرید انبار ' . money( $report['purchases'] ) . '
سود تقریبی ' . money( $report['profit'] ) . '

آمار نوع فروش

نوع فروش تعداد مبلغ
فنربندی با جلد ' . faNumber( $s['spring_with_cover_count'] ) . ' ' . money( $s['spring_with_cover_amount'] ) . '
فنربندی بی‌جلد ' . faNumber( $s['spring_without_cover_count'] ) . ' ' . money( $s['spring_without_cover_amount'] ) . '
منگنه دوتایی ' . faNumber( $s['staple_double_count'] ) . ' ' . money( $s['staple_double_amount'] ) . '
منگنه سه‌تایی ' . faNumber( $s['staple_triple_count'] ) . ' ' . money( $s['staple_triple_amount'] ) . '
لیبل ' . faNumber( $s['label_count'] ) . ' ' . money( $s['label_amount'] ) . '
مبلغ اضافی - ' . money( $s['extra_amount'] ) . '

آمار کلی

تعداد سفارش‌ها ' . faNumber( $s['orders'] ) . '
تعداد کتاب‌ها ' . faNumber( $s['books'] ) . '

جزئیات سفارش‌ها

' . $rows . '
شماره مشتری تلفن نوع فروش کتاب کل دریافتی مانده تاریخ
'; file_put_contents( $path, $html ); return [ 'path' => $path, 'name' => $fileName ]; } /* ========================================================= Send A4 Report ========================================================= */ function sendA4Report( $chatId, $report ) { $file = createA4ReportFile( $report ); if ( !file_exists( $file['path'] ) ) { sendMessage( $chatId, '❌ خطا در ساخت فایل گزارش.' ); return; } $caption = "🖨 گزارش A4 دفترفنی جهان\n\n" . "بازه: " . formatJalaliDate( $report['from'] ) . " تا " . formatJalaliDate( $report['to'] ) . "\n\n" . "💰 فروش: " . money( $report['summary']['sales'] ) . "\n" . "📈 سود تقریبی: " . money( $report['profit'] ); botRequest( 'sendDocument', [ 'chat_id' => $chatId, 'document' => new CURLFile( $file['path'], 'application/msword', $file['name'] ), 'caption' => $caption, 'parse_mode' => 'HTML' ] ); @unlink( $file['path'] ); } /* ========================================================= Inventory ========================================================= */ function showInventory($chatId) { global $pdo; $stmt = $pdo->query( "SELECT * FROM products ORDER BY id ASC" ); $products = $stmt->fetchAll(); $text = "📦 موجودی انبار\n\n"; if (!$products) { $text .= 'هیچ کالایی در انبار ثبت نشده است.'; } else { foreach ($products as $product) { $stock = (int)$product['stock']; $min = (int)$product['min_stock']; $icon = $stock <= $min ? '⚠️' : '✅'; $text .= $icon . ' ' . '' . html($product['name']) . '' . "\n" . 'موجودی: ' . faNumber($stock) . ' ' . html( $product['unit'] ?? '' ) . "\n\n"; } } sendMessage( $chatId, $text, inventoryKeyboard() ); } /* ========================================================= Cash ========================================================= */ function showTodayCash($chatId) { global $pdo; $today = date('Y-m-d'); $stmt = $pdo->prepare( "SELECT COALESCE( SUM( CASE WHEN type = 'income' THEN amount ELSE 0 END ), 0 ) AS income, COALESCE( SUM( CASE WHEN type = 'expense' THEN amount ELSE 0 END ), 0 ) AS expense FROM payments WHERE created_at >= ? AND created_at < DATE_ADD(?, INTERVAL 1 DAY)" ); $stmt->execute([ $today . ' 00:00:00', $today ]); $row = $stmt->fetch(); $income = (int)$row['income']; $expense = (int)$row['expense']; $balance = $income - $expense; sendMessage( $chatId, "📊 دخل امروز\n\n" . "💵 دریافت‌ها: " . money($income) . "\n\n" . "💸 هزینه‌ها: " . money($expense) . "\n\n" . "💰 مانده دخل: " . money($balance) . "", cashKeyboard() ); } /* ========================================================= Customers ========================================================= */ function showCustomers($chatId) { global $pdo; $stmt = $pdo->query( "SELECT * FROM customers ORDER BY id DESC LIMIT 30" ); $customers = $stmt->fetchAll(); $text = "👥 مشتریان\n\n"; if (!$customers) { $text .= 'هنوز مشتری ثبت نشده است.'; } else { foreach ($customers as $customer) { $text .= "👤 " . html($customer['name']) . "\n" . "📱 " . html($customer['phone']) . "\n" . "🧾 سفارش‌ها: " . faNumber( $customer['order_count'] ) . "\n" . "💰 خرید: " . money( $customer['sales_total'] ) . "\n" . "💳 بدهی: " . money( $customer['debt'] ) . "\n\n"; } } sendMessage( $chatId, $text, backKeyboard() ); } /* ========================================================= Orders ========================================================= */ function showOrders($chatId) { global $pdo; $stmt = $pdo->query( "SELECT * FROM orders ORDER BY id DESC LIMIT 20" ); $orders = $stmt->fetchAll(); $text = "🗃 آخرین سفارشات\n\n"; if (!$orders) { $text .= 'هنوز سفارشی ثبت نشده است.'; } else { foreach ($orders as $order) { $status = $order['status']; $text .= "🧾 #" . faNumber($order['id']) . "\n" . "👤 " . html($order['name']) . "\n" . "📱 " . html($order['phone']) . "\n" . "💰 " . money($order['total']) . "\n" . "💵 دریافت: " . money($order['deposit']) . "\n" . "💳 مانده: " . money($order['remaining']) . "\n" . "📌 وضعیت: " . html($status) . "\n\n"; } } sendMessage( $chatId, $text, backKeyboard() ); } /* ========================================================= Order State Handler ========================================================= */ function handleState( $chatId, $text, $stateData ) { global $pdo; $state = $stateData['state']; $data = $stateData['data']; /* * لغو */ if ($text === '❌ لغو') { clearState($chatId); sendMessage( $chatId, '❌ عملیات لغو شد.', mainKeyboard() ); return; } /* * بازگشت */ if ($text === '🔙 بازگشت') { clearState($chatId); showMainMenu($chatId); return; } /* * سفارش جدید */ switch ($state) { case 'order_name': $data['name'] = cleanText($text); setState( $chatId, 'order_phone', $data ); sendMessage( $chatId, '📱 شماره تلفن مشتری را وارد کنید:', backKeyboard() ); break; case 'order_phone': $data['phone'] = cleanText($text); setState( $chatId, 'order_education', $data ); sendMessage( $chatId, '🎓 مقطع / رشته تحصیلی را وارد کنید:', backKeyboard() ); break; case 'order_education': $data['education'] = cleanText($text); setState( $chatId, 'order_books', $data ); sendMessage( $chatId, '📚 تعداد کتاب‌ها را وارد کنید:', backKeyboard() ); break; case 'order_books': $number = normalizeNumber($text); if ( !ctype_digit($number) ) { sendMessage( $chatId, '❌ لطفاً فقط عدد وارد کنید.' ); return; } $data['books'] = (int)$number; setState( $chatId, 'order_springs', $data ); sendMessage( $chatId, '📕 تعداد فنربندی را وارد کنید:', backKeyboard() ); break; case 'order_springs': $number = normalizeNumber($text); if ( !ctype_digit($number) ) { sendMessage( $chatId, '❌ لطفاً فقط عدد وارد کنید.' ); return; } $data['springs'] = (int)$number; if ( $data['springs'] > 0 ) { setState( $chatId, 'order_spring_type', $data ); $keyboard = [ 'keyboard' => [ [ [ 'text' => '📕 فنربندی با جلد' ] ], [ [ 'text' => '📗 فنربندی بی‌جلد' ] ], [ [ 'text' => '🔙 بازگشت' ] ] ], 'resize_keyboard' => true ]; sendMessage( $chatId, 'نوع فنربندی را انتخاب کنید:', $keyboard ); } else { setState( $chatId, 'order_staples', $data ); sendMessage( $chatId, '📎 تعداد منگنه را وارد کنید:', backKeyboard() ); } break; case 'order_spring_type': if ( $text === '📕 فنربندی با جلد' ) { $data['spring_type'] = 'with_cover'; } elseif ( $text === '📗 فنربندی بی‌جلد' ) { $data['spring_type'] = 'without_cover'; } else { sendMessage( $chatId, '❌ لطفاً یکی از گزینه‌ها را انتخاب کنید.' ); return; } setState( $chatId, 'order_staples', $data ); sendMessage( $chatId, '📎 تعداد منگنه را وارد کنید:', backKeyboard() ); break; case 'order_staples': $number = normalizeNumber($text); if ( !ctype_digit($number) ) { sendMessage( $chatId, '❌ لطفاً فقط عدد وارد کنید.' ); return; } $data['staples'] = (int)$number; if ( $data['staples'] > 0 ) { setState( $chatId, 'order_staple_type', $data ); $keyboard = [ 'keyboard' => [ [ [ 'text' => '📎 منگنه دوتایی' ], [ 'text' => '📎 منگنه سه‌تایی' ] ], [ [ 'text' => '🔙 بازگشت' ] ] ], 'resize_keyboard' => true ]; sendMessage( $chatId, 'نوع منگنه را انتخاب کنید:', $keyboard ); } else { setState( $chatId, 'order_label_number', $data ); sendMessage( $chatId, '🏷 شماره لیبل را وارد کنید:', backKeyboard() ); } break; case 'order_staple_type': if ( $text === '📎 منگنه دوتایی' ) { $data['staple_type'] = 'double'; } elseif ( $text === '📎 منگنه سه‌تایی' ) { $data['staple_type'] = 'triple'; } else { sendMessage( $chatId, '❌ لطفاً نوع منگنه را انتخاب کنید.' ); return; } setState( $chatId, 'order_staple_names', $data ); sendMessage( $chatId, '✍️ اسامی مربوط به منگنه را وارد کنید:', backKeyboard() ); break; case 'order_staple_names': $data['staple_names'] = cleanText($text); setState( $chatId, 'order_label_number', $data ); sendMessage( $chatId, '🏷 شماره لیبل را وارد کنید:', backKeyboard() ); break; case 'order_label_number': $data['label_number'] = cleanText($text); setState( $chatId, 'order_label_sheets', $data ); sendMessage( $chatId, '🏷 تعداد برگه‌های لیبل را وارد کنید:', backKeyboard() ); break; case 'order_label_sheets': $number = normalizeNumber($text); if ( !ctype_digit($number) ) { sendMessage( $chatId, '❌ لطفاً فقط عدد وارد کنید.' ); return; } $data['label_sheets'] = (int)$number; setState( $chatId, 'order_deposit', $data ); sendMessage( $chatId, '💵 مبلغ دریافتی / بیعانه را وارد کنید:', backKeyboard() ); break; case 'order_deposit': $number = normalizeNumber($text); if ( !ctype_digit($number) ) { sendMessage( $chatId, '❌ لطفاً فقط عدد وارد کنید.' ); return; } $data['deposit'] = (int)$number; setState( $chatId, 'order_extra_amount', $data ); sendMessage( $chatId, '➕ مبلغ اضافی را وارد کنید.' . "\n\n" . 'اگر مبلغ اضافی ندارید، عدد ۰ را وارد کنید.', backKeyboard() ); break; case 'order_extra_amount': $number = normalizeNumber($text); if ( !ctype_digit($number) ) { sendMessage( $chatId, '❌ لطفاً فقط عدد وارد کنید.' ); return; } $data['extra_amount'] = (int)$number; if ( $data['extra_amount'] > 0 ) { setState( $chatId, 'order_extra_reason', $data ); sendMessage( $chatId, '📝 مبلغ اضافی بابت چی است؟', backKeyboard() ); } else { setState( $chatId, 'order_description', $data ); sendMessage( $chatId, '📝 توضیحات سفارش را وارد کنید.' . "\n\n" . 'اگر توضیحی ندارید بنویسید: ندارد', backKeyboard() ); } break; case 'order_extra_reason': $data['extra_reason'] = cleanText($text); setState( $chatId, 'order_description', $data ); sendMessage( $chatId, '📝 توضیحات سفارش را وارد کنید.' . "\n\n" . 'اگر توضیحی ندارید بنویسید: ندارد', backKeyboard() ); break; case 'order_description': $data['description'] = cleanText($text); setState( $chatId, 'order_confirm', $data ); showOrderConfirmation( $chatId, $data ); break; case 'order_confirm': if ( $text === '✅ ثبت سفارش' ) { saveOrder( $chatId, $data ); } else { clearState($chatId); sendMessage( $chatId, '❌ سفارش ثبت نشد.', mainKeyboard() ); } break; /* * تنظیمات */ case 'setting_spring_cover': $number = normalizeNumber($text); if (!ctype_digit($number)) { sendMessage( $chatId, '❌ فقط عدد وارد کنید.' ); return; } setSetting( 'spring_with_cover_price', (int)$number ); clearState($chatId); showSettings($chatId); break; case 'setting_spring_no_cover': $number = normalizeNumber($text); if (!ctype_digit($number)) { sendMessage( $chatId, '❌ فقط عدد وارد کنید.' ); return; } setSetting( 'spring_without_cover_price', (int)$number ); clearState($chatId); showSettings($chatId); break; case 'setting_staple_double': $number = normalizeNumber($text); if (!ctype_digit($number)) { sendMessage( $chatId, '❌ فقط عدد وارد کنید.' ); return; } setSetting( 'staple_double_price', (int)$number ); clearState($chatId); showSettings($chatId); break; case 'setting_staple_triple': $number = normalizeNumber($text); if (!ctype_digit($number)) { sendMessage( $chatId, '❌ فقط عدد وارد کنید.' ); return; } setSetting( 'staple_triple_price', (int)$number ); clearState($chatId); showSettings($chatId); break; case 'setting_label': $number = normalizeNumber($text); if (!ctype_digit($number)) { sendMessage( $chatId, '❌ فقط عدد وارد کنید.' ); return; } setSetting( 'label_price', (int)$number ); clearState($chatId); showSettings($chatId); break; case 'setting_low_stock': $number = normalizeNumber($text); if (!ctype_digit($number)) { sendMessage( $chatId, '❌ فقط عدد وارد کنید.' ); return; } setSetting( 'low_stock', (int)$number ); clearState($chatId); showSettings($chatId); break; /* * گزارش بازه */ case 'report_from': $from = normalizeReportDate($text); if ($from === false) { sendMessage( $chatId, "❌ تاریخ صحیح نیست.\n\n" . "مثال:\n" . "۱۴۰۵/۰۶/۲۴" ); return; } $data['from'] = $from; setState( $chatId, 'report_to', $data ); sendMessage( $chatId, "🗓 تاریخ تا را وارد کنید:\n\n" . "مثال: ۱۴۰۵/۰۶/۲۴", backKeyboard() ); break; case 'report_to': $to = normalizeReportDate($text); if ($to === false) { sendMessage( $chatId, "❌ تاریخ صحیح نیست.\n\n" . "مثال:\n" . "۱۴۰۵/۰۶/۲۴" ); return; } if ( $to < $data['from'] ) { sendMessage( $chatId, '❌ تاریخ پایان نمی‌تواند قبل از تاریخ شروع باشد.' ); return; } $data['to'] = $to; clearState($chatId); $report = getSalesReport( $data['from'], $data['to'] ); sendReportSummary( $chatId, $report ); break; /* * ثبت خرید */ case 'purchase_product': $data['product_name'] = cleanText($text); setState( $chatId, 'purchase_quantity', $data ); sendMessage( $chatId, '📦 تعداد / مقدار خرید را وارد کنید:', backKeyboard() ); break; case 'purchase_quantity': $number = normalizeNumber($text); if (!ctype_digit($number)) { sendMessage( $chatId, '❌ فقط عدد وارد کنید.' ); return; } $data['quantity'] = (int)$number; setState( $chatId, 'purchase_unit_price', $data ); sendMessage( $chatId, '💰 قیمت خرید هر واحد را وارد کنید:', backKeyboard() ); break; case 'purchase_unit_price': $number = normalizeNumber($text); if (!ctype_digit($number)) { sendMessage( $chatId, '❌ فقط عدد وارد کنید.' ); return; } $data['unit_price'] = (int)$number; global $pdo; $pdo->beginTransaction(); try { $stmt = $pdo->prepare( "SELECT id FROM products WHERE name = ? LIMIT 1" ); $stmt->execute([ $data['product_name'] ]); $product = $stmt->fetch(); if ($product) { $productId = (int)$product['id']; $stmt = $pdo->prepare( "UPDATE products SET stock = stock + ?, last_buy_price = ? WHERE id = ?" ); $stmt->execute([ $data['quantity'], $data['unit_price'], $productId ]); } else { $stmt = $pdo->prepare( "INSERT INTO products ( name, unit, stock, min_stock, last_buy_price ) VALUES (?, 'عدد', ?, ?, ?)" ); $stmt->execute([ $data['product_name'], $data['quantity'], getSetting( 'low_stock', 5 ), $data['unit_price'] ]); $productId = (int)$pdo->lastInsertId(); } $total = $data['quantity'] * $data['unit_price']; $stmt = $pdo->prepare( "INSERT INTO purchases (total, description) VALUES (?, ?)" ); $stmt->execute([ $total, 'خرید ' . $data['product_name'] ]); $purchaseId = (int)$pdo->lastInsertId(); $stmt = $pdo->prepare( "INSERT INTO purchase_items ( purchase_id, product_id, quantity, unit_price, total ) VALUES (?, ?, ?, ?, ?)" ); $stmt->execute([ $purchaseId, $productId, $data['quantity'], $data['unit_price'], $total ]); $stmt = $pdo->prepare( "INSERT INTO stock_movements ( product_id, type, quantity, description ) VALUES (?, 'in', ?, ?)" ); $stmt->execute([ $productId, $data['quantity'], 'خرید جدید' ]); $pdo->commit(); clearState($chatId); sendMessage( $chatId, "✅ خرید با موفقیت ثبت شد.\n\n" . "محصول: " . html( $data['product_name'] ) . "\n" . "مقدار: " . faNumber( $data['quantity'] ) . "\n" . "مبلغ کل: " . money($total) . "", inventoryKeyboard() ); } catch (Throwable $e) { if ( $pdo->inTransaction() ) { $pdo->rollBack(); } error_log( 'Purchase error: ' . $e->getMessage() ); sendMessage( $chatId, '❌ خطا در ثبت خرید.' ); } break; /* * ثبت دریافت */ case 'cash_income_amount': $number = normalizeNumber($text); if (!ctype_digit($number)) { sendMessage( $chatId, '❌ فقط عدد وارد کنید.' ); return; } $data['amount'] = (int)$number; setState( $chatId, 'cash_income_description', $data ); sendMessage( $chatId, '📝 بابت چه چیزی دریافت شده است؟', backKeyboard() ); break; case 'cash_income_description': $description = cleanText($text); $stmt = $pdo->prepare( "INSERT INTO payments ( type, amount, description ) VALUES ('income', ?, ?)" ); $stmt->execute([ $data['amount'], $description ]); clearState($chatId); sendMessage( $chatId, '✅ دریافت با موفقیت ثبت شد.', cashKeyboard() ); break; /* * ثبت هزینه */ case 'cash_expense_amount': $number = normalizeNumber($text); if (!ctype_digit($number)) { sendMessage( $chatId, '❌ فقط عدد وارد کنید.' ); return; } $data['amount'] = (int)$number; setState( $chatId, 'cash_expense_title', $data ); sendMessage( $chatId, '💸 عنوان هزینه را وارد کنید:', backKeyboard() ); break; case 'cash_expense_title': $data['title'] = cleanText($text); setState( $chatId, 'cash_expense_description', $data ); sendMessage( $chatId, '📝 توضیحات هزینه را وارد کنید:', backKeyboard() ); break; case 'cash_expense_description': $description = cleanText($text); $stmt = $pdo->prepare( "INSERT INTO expenses ( amount, title, description ) VALUES (?, ?, ?)" ); $stmt->execute([ $data['amount'], $data['title'], $description ]); $stmt = $pdo->prepare( "INSERT INTO payments ( type, amount, description ) VALUES ('expense', ?, ?)" ); $stmt->execute([ $data['amount'], $data['title'] . ' - ' . $description ]); clearState($chatId); sendMessage( $chatId, '✅ هزینه با موفقیت ثبت شد.', cashKeyboard() ); break; } } /* ========================================================= Callback Handler ========================================================= */ function handleCallback($callback) { $callbackId = $callback['id']; $chatId = $callback['message']['chat']['id']; $data = $callback['data'] ?? ''; answerCallback( $callbackId ); if ( $data === 'noop' ) { return; } } /* ========================================================= Text Router ========================================================= */ function handleText( $chatId, $text ) { /* * اگر State فعال باشد */ $state = getState($chatId); /* * دکمه‌های عمومی باید قبل از State * بررسی شوند. */ if ( $text === '🔙 بازگشت' ) { clearState($chatId); showMainMenu($chatId); return; } /* * منوی اصلی */ if ( $text === '📝 ورودی جدید' || $text === '/start' ) { startNewOrder($chatId); return; } if ( $text === '🧾 فاکتور جدید' ) { startNewOrder($chatId); return; } /* * گزارشات */ if ( $text === '📊 آمار و گزارشات' ) { clearState($chatId); sendMessage( $chatId, "📊 آمار و گزارشات\n\n" . "بازه موردنظر را انتخاب کنید:", reportKeyboard() ); return; } if ( $text === '📅 امروز' ) { $range = reportDateRange( 'today' ); $report = getSalesReport( $range['from'], $range['to'] ); sendReportSummary( $chatId, $report ); return; } if ( $text === '📅 دیروز' ) { $range = reportDateRange( 'yesterday' ); $report = getSalesReport( $range['from'], $range['to'] ); sendReportSummary( $chatId, $report ); return; } if ( $text === '📅 ۷ روز اخیر' ) { $range = reportDateRange( '7days' ); $report = getSalesReport( $range['from'], $range['to'] ); sendReportSummary( $chatId, $report ); return; } if ( $text === '📅 ۳۰ روز اخیر' ) { $range = reportDateRange( '30days' ); $report = getSalesReport( $range['from'], $range['to'] ); sendReportSummary( $chatId, $report ); return; } if ( $text === '🗓 انتخاب بازه' ) { setState( $chatId, 'report_from', [] ); sendMessage( $chatId, "🗓 تاریخ شروع را وارد کنید:\n\n" . "مثال:\n" . "۱۴۰۵/۰۶/۲۴", backKeyboard() ); return; } if ( $text === '🖨 دریافت گزارش A4' ) { $stateData = getState($chatId); /* * اگر گزارش سفارشی باشد، * تاریخ‌ها در State ذخیره می‌شوند. */ if ( $stateData && isset( $stateData['data']['from'] ) && isset( $stateData['data']['to'] ) ) { $report = getSalesReport( $stateData['data']['from'], $stateData['data']['to'] ); sendA4Report( $chatId, $report ); return; } /* * اگر State گزارش موجود نیست، * پیش‌فرض امروز. */ $range = reportDateRange( 'today' ); $report = getSalesReport( $range['from'], $range['to'] ); sendA4Report( $chatId, $report ); return; } /* * تنظیمات */ if ( $text === '⚙️ تنظیمات کلی' ) { clearState($chatId); showSettings($chatId); return; } if ( $text === '📕 فنربندی با جلد' ) { setState( $chatId, 'setting_spring_cover', [] ); sendMessage( $chatId, '💰 قیمت هر فنربندی با جلد را وارد کنید:', backKeyboard() ); return; } if ( $text === '📗 فنربندی بی‌جلد' ) { setState( $chatId, 'setting_spring_no_cover', [] ); sendMessage( $chatId, '💰 قیمت هر فنربندی بی‌جلد را وارد کنید:', backKeyboard() ); return; } if ( $text === '📎 منگنه دوتایی' ) { setState( $chatId, 'setting_staple_double', [] ); sendMessage( $chatId, '💰 قیمت هر منگنه دوتایی را وارد کنید:', backKeyboard() ); return; } if ( $text === '📎 منگنه سه‌تایی' ) { setState( $chatId, 'setting_staple_triple', [] ); sendMessage( $chatId, '💰 قیمت هر منگنه سه‌تایی را وارد کنید:', backKeyboard() ); return; } if ( $text === '🏷 قیمت لیبل' ) { setState( $chatId, 'setting_label', [] ); sendMessage( $chatId, '💰 قیمت هر برگه لیبل را وارد کنید:', backKeyboard() ); return; } if ( $text === '📦 حداقل موجودی' ) { setState( $chatId, 'setting_low_stock', [] ); sendMessage( $chatId, '📦 حداقل موجودی را وارد کنید:', backKeyboard() ); return; } /* * انبار */ if ( $text === '📦 انبار' ) { clearState($chatId); showInventory($chatId); return; } if ( $text === '📋 مشاهده موجودی' ) { showInventory($chatId); return; } if ( $text === '➕ ثبت خرید/ورودی' ) { setState( $chatId, 'purchase_product', [] ); sendMessage( $chatId, '📦 نام محصول را وارد کنید:', backKeyboard() ); return; } /* * دخل */ if ( $text === '💰 دخل و پرداخت‌ها' ) { clearState($chatId); sendMessage( $chatId, '💰 دخل و پرداخت‌ها', cashKeyboard() ); return; } if ( $text === '💵 ثبت دریافت' ) { setState( $chatId, 'cash_income_amount', [] ); sendMessage( $chatId, '💵 مبلغ دریافت را وارد کنید:', backKeyboard() ); return; } if ( $text === '💸 ثبت هزینه' ) { setState( $chatId, 'cash_expense_amount', [] ); sendMessage( $chatId, '💸 مبلغ هزینه را وارد کنید:', backKeyboard() ); return; } if ( $text === '📊 دخل امروز' ) { showTodayCash($chatId); return; } /* * مشتریان */ if ( $text === '👥 مشتریان' ) { clearState($chatId); showCustomers($chatId); return; } /* * سفارشات */ if ( $text === '🗃 سفارشات' ) { clearState($chatId); showOrders($chatId); return; } /* * سود و زیان */ if ( $text === '📈 سود و زیان' ) { $range = reportDateRange( 'today' ); $report = getSalesReport( $range['from'], $range['to'] ); sendMessage( $chatId, "📈 سود و زیان امروز\n\n" . "فروش: " . money( $report['summary']['sales'] ) . "\n\n" . "هزینه‌ها: " . money( $report['expenses'] ) . "\n\n" . "خرید انبار: " . money( $report['purchases'] ) . "\n\n" . "📈 سود تقریبی: " . money( $report['profit'] ) . "", mainKeyboard() ); return; } /* * گزارش A4 */ if ( $text === '🖨 گزارش A4' ) { sendMessage( $chatId, "🖨 گزارش A4\n\n" . "بازه گزارش را انتخاب کنید:", reportKeyboard() ); return; } /* * اگر State فعال بود */ if ($state) { handleState( $chatId, $text, $state ); return; } /* * دستورهای متنی */ if ( $text === '/report' ) { sendMessage( $chatId, "📊 آمار و گزارشات", reportKeyboard() ); return; } if ( $text === '/orders' ) { showOrders($chatId); return; } if ( $text === '/customers' ) { showCustomers($chatId); return; } /* * پاسخ پیش‌فرض */ sendMessage( $chatId, 'لطفاً یکی از گزینه‌های منو را انتخاب کنید.', mainKeyboard() ); } /* ========================================================= Telegram Update ========================================================= */ $raw = file_get_contents( 'php://input' ); if (!$raw) { exit; } $update = json_decode( $raw, true ); if (!is_array($update)) { exit; } /* ========================================================= Manager Only ========================================================= */ $chatId = null; if ( isset( $update['message']['chat']['id'] ) ) { $chatId = (string)$update['message']['chat']['id']; } elseif ( isset( $update['callback_query']['message']['chat']['id'] ) ) { $chatId = (string)$update[ 'callback_query' ]['message']['chat']['id']; } if ( $chatId === null || (string)$chatId !== (string)ADMIN_ID ) { if ( isset( $update['message']['chat']['id'] ) ) { sendMessage( $chatId, '⛔ دسترسی شما به این ربات مجاز نیست.' ); } exit; } /* ========================================================= Callback ========================================================= */ if ( isset( $update['callback_query'] ) ) { handleCallback( $update['callback_query'] ); exit; } /* ========================================================= Message ========================================================= */ if ( isset( $update['message']['text'] ) ) { $text = trim( $update['message']['text'] ); handleText( $chatId, $text ); exit; } ?>