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;
}
?>