Работа с QR-кодами
This commit is contained in:
parent
5e2072062d
commit
79e10b4556
144
bot.py
144
bot.py
|
|
@ -72,29 +72,30 @@ async def cmd_start(message: Message):
|
||||||
@dp.message(Command("get"))
|
@dp.message(Command("get"))
|
||||||
@allowed_only
|
@allowed_only
|
||||||
async def cmd_get(message: Message):
|
async def cmd_get(message: Message):
|
||||||
result = db.get_random_from_oldest()
|
record = db.get_random_record()
|
||||||
if not result:
|
if not record:
|
||||||
await message.answer("❌ База пуста, брат. Добавь номера!")
|
await message.answer("❌ База пуста, брат!")
|
||||||
return
|
return
|
||||||
|
|
||||||
phone = result['phone']
|
record_type = record['type']
|
||||||
bonus_card = result.get('bonus_card')
|
phone = record.get('phone')
|
||||||
|
qr_code = record.get('qr_code')
|
||||||
|
|
||||||
if bonus_card:
|
if record_type == 'both' and qr_code:
|
||||||
await message.answer_photo(
|
await message.answer_photo(
|
||||||
photo=bonus_card,
|
photo=qr_code,
|
||||||
caption=f"🎲 *МракоЗвон*\n"
|
caption=f"🎲 *МракоЗвон*\n📞 Номер: `{phone}`\n🃏 Бонусная карта привязана\n🔄 Использован: {record['usage_count']} раз",
|
||||||
f"📞 Номер: `{phone}`\n"
|
|
||||||
f"🃏 Бонусная карта привязана\n"
|
|
||||||
f"🔄 Использован: {result['usage_count']} раз",
|
|
||||||
parse_mode="Markdown"
|
parse_mode="Markdown"
|
||||||
)
|
)
|
||||||
else:
|
elif record_type == 'phone':
|
||||||
await message.answer(
|
await message.answer(
|
||||||
f"🎲 *МракоЗвон*\n"
|
f"🎲 *МракоЗвон*\n📞 Номер: `{phone}`\n🔄 Использован: {record['usage_count']} раз",
|
||||||
f"📞 `{phone}`\n"
|
parse_mode="Markdown"
|
||||||
f"🔄 Использован: {result['usage_count']} раз\n"
|
)
|
||||||
f"❌ Бонусная карта отсутствует",
|
elif record_type == 'qr' and qr_code:
|
||||||
|
await message.answer_photo(
|
||||||
|
photo=qr_code,
|
||||||
|
caption=f"🎲 *МракоЗвон*\n🃏 Бонусная карта\n🔄 Использована: {record['usage_count']} раз",
|
||||||
parse_mode="Markdown"
|
parse_mode="Markdown"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -217,104 +218,61 @@ async def cmd_whitelist(message: Message):
|
||||||
async def handle_message(message: Message):
|
async def handle_message(message: Message):
|
||||||
phone = None
|
phone = None
|
||||||
qr_raw_data = None
|
qr_raw_data = None
|
||||||
|
qr_image = None
|
||||||
|
|
||||||
# 1. Если есть фото - пытаемся распознать QR
|
# 1. Распознаём QR из фото
|
||||||
if message.photo:
|
if message.photo:
|
||||||
photo = message.photo[-1]
|
photo = message.photo[-1]
|
||||||
file = await bot.get_file(photo.file_id)
|
file = await bot.get_file(photo.file_id)
|
||||||
file_bytes = await bot.download_file(file.file_path)
|
file_bytes = await bot.download_file(file.file_path)
|
||||||
qr_raw_data = decode_qr_from_image(file_bytes.read())
|
qr_raw_data = decode_qr_from_image(file_bytes.read())
|
||||||
|
if qr_raw_data:
|
||||||
|
qr_image = generate_qr_code(qr_raw_data)
|
||||||
|
|
||||||
# 2. Если есть текст - пробуем извлечь номер
|
# 2. Извлекаем номер из текста
|
||||||
if message.text:
|
if message.text:
|
||||||
phone = normalize_phone(message.text.strip())
|
phone = normalize_phone(message.text.strip())
|
||||||
|
|
||||||
# 3. Если нет ничего
|
# 3. Определяем тип записи и сохраняем
|
||||||
if not phone and not qr_raw_data:
|
|
||||||
await message.answer(
|
|
||||||
"⚠️ Не распознал.\n\n"
|
|
||||||
"Отправь:\n"
|
|
||||||
"• номер телефона текстом\n"
|
|
||||||
"• бонусную карту (QR-код)\n"
|
|
||||||
"• номер + карту вместе"
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
# 4. Только номер
|
|
||||||
if phone and not qr_raw_data:
|
|
||||||
if db.add_number(phone):
|
|
||||||
await message.answer(f"✅ Сохранён номер: `{phone}`", parse_mode="Markdown")
|
|
||||||
else:
|
|
||||||
await message.answer(f"⚠️ Номер `{phone}` уже есть в базе", parse_mode="Markdown")
|
|
||||||
return
|
|
||||||
|
|
||||||
# 5. Только QR (без номера)
|
|
||||||
if qr_raw_data and not phone:
|
|
||||||
await message.answer(
|
|
||||||
f"🃏 Распознана бонусная карта:\n"
|
|
||||||
f"`{qr_raw_data[:50]}{'...' if len(qr_raw_data) > 50 else ''}`\n\n"
|
|
||||||
f"⚠️ Чтобы привязать карту к номеру, отправь и номер тоже.\n"
|
|
||||||
f"Пример: номер и карту в одном сообщении"
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
# 6. Номер + QR вместе
|
|
||||||
if phone and qr_raw_data:
|
if phone and qr_raw_data:
|
||||||
qr_base64 = generate_qr_code(qr_raw_data)
|
# Тип: номер + QR
|
||||||
|
db.add_record('both', phone=phone, qr_code=qr_image, qr_data=qr_raw_data)
|
||||||
if db.add_number(phone, qr_base64):
|
await message.answer(f"✅ Сохранено: номер `{phone}` + бонусная карта", parse_mode="Markdown")
|
||||||
await message.answer(
|
elif phone:
|
||||||
f"✅ Сохранено:\n"
|
# Тип: только номер
|
||||||
f"📞 Номер: `{phone}`\n"
|
db.add_record('phone', phone=phone)
|
||||||
f"🃏 Бонусная карта привязана",
|
await message.answer(f"✅ Сохранён номер: `{phone}`", parse_mode="Markdown")
|
||||||
parse_mode="Markdown"
|
elif qr_raw_data:
|
||||||
)
|
# Тип: только QR
|
||||||
|
db.add_record('qr', qr_code=qr_image, qr_data=qr_raw_data)
|
||||||
|
await message.answer(f"🃏 Сохранена бонусная карта: `{qr_raw_data[:50]}...`", parse_mode="Markdown")
|
||||||
else:
|
else:
|
||||||
# Если номер уже есть - обновляем карту
|
await message.answer("⚠️ Не распознано. Отправь номер, QR-код или оба вместе.")
|
||||||
if db.update_bonus_card(phone, qr_base64):
|
|
||||||
await message.answer(
|
|
||||||
f"🔄 Обновлена бонусная карта для номера `{phone}`",
|
|
||||||
parse_mode="Markdown"
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
await message.answer(f"⚠️ Ошибка сохранения", parse_mode="Markdown")
|
|
||||||
return
|
|
||||||
|
|
||||||
|
|
||||||
# ========== ВЕБ-СЕРВЕР ==========
|
# ========== ВЕБ-СЕРВЕР ==========
|
||||||
routes = web.RouteTableDef()
|
routes = web.RouteTableDef()
|
||||||
|
|
||||||
|
|
||||||
@routes.get("/get_webapp")
|
@routes.get("/get_records")
|
||||||
async def serve_webapp(request):
|
async def get_records(request):
|
||||||
try:
|
records = db.get_all_records()
|
||||||
with open("web_app.html", "r", encoding="utf-8") as f:
|
return web.json_response({"records": records})
|
||||||
html = f.read()
|
|
||||||
return web.Response(text=html, content_type="text/html")
|
|
||||||
except FileNotFoundError:
|
|
||||||
return web.Response(text="web_app.html not found", status=404)
|
|
||||||
|
|
||||||
|
@routes.get("/get_qr_code/{record_id}")
|
||||||
|
async def get_qr_code(request):
|
||||||
|
record_id = int(request.match_info.get('record_id', 0))
|
||||||
|
with sqlite3.connect(DB_NAME) as conn:
|
||||||
|
conn.row_factory = sqlite3.Row
|
||||||
|
result = conn.execute("SELECT qr_code FROM records WHERE id = ?", (record_id,)).fetchone()
|
||||||
|
if result and result['qr_code']:
|
||||||
|
return web.json_response({"qr_code": result['qr_code']})
|
||||||
|
return web.json_response({"qr_code": None})
|
||||||
|
|
||||||
@routes.get("/get_numbers")
|
@routes.post("/delete_record")
|
||||||
async def get_numbers(request):
|
async def delete_record(request):
|
||||||
numbers = db.get_all_numbers()
|
|
||||||
safe_numbers = []
|
|
||||||
for num in numbers:
|
|
||||||
safe_numbers.append({
|
|
||||||
"id": num["id"],
|
|
||||||
"phone": num["phone"],
|
|
||||||
"has_bonus_card": bool(num.get("bonus_card")),
|
|
||||||
"created_at": num["created_at"],
|
|
||||||
"used_at": num["used_at"],
|
|
||||||
"usage_count": num["usage_count"]
|
|
||||||
})
|
|
||||||
return web.json_response({"numbers": safe_numbers})
|
|
||||||
|
|
||||||
|
|
||||||
@routes.post("/delete_number")
|
|
||||||
async def delete_number(request):
|
|
||||||
data = await request.json()
|
data = await request.json()
|
||||||
db.delete_number(data["id"])
|
db.delete_record(data["id"])
|
||||||
return web.json_response({"status": "ok"})
|
return web.json_response({"status": "ok"})
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
87
database.py
87
database.py
|
|
@ -7,19 +7,21 @@ DB_NAME = "bot.db"
|
||||||
|
|
||||||
def init_db():
|
def init_db():
|
||||||
with sqlite3.connect(DB_NAME) as conn:
|
with sqlite3.connect(DB_NAME) as conn:
|
||||||
# Основная таблица с номерами и бонусными картами
|
# Единая таблица для всех записей
|
||||||
conn.execute("""
|
conn.execute("""
|
||||||
CREATE TABLE IF NOT EXISTS numbers (
|
CREATE TABLE IF NOT EXISTS records (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
phone TEXT UNIQUE NOT NULL,
|
type TEXT NOT NULL,
|
||||||
bonus_card TEXT,
|
phone TEXT,
|
||||||
|
qr_code TEXT,
|
||||||
|
qr_data TEXT,
|
||||||
created_at TEXT NOT NULL,
|
created_at TEXT NOT NULL,
|
||||||
used_at TEXT,
|
used_at TEXT,
|
||||||
usage_count INTEGER DEFAULT 0
|
usage_count INTEGER DEFAULT 0
|
||||||
)
|
)
|
||||||
""")
|
""")
|
||||||
|
|
||||||
# Таблица белого списка пользователей
|
# Таблица белого списка
|
||||||
conn.execute("""
|
conn.execute("""
|
||||||
CREATE TABLE IF NOT EXISTS allowed_users (
|
CREATE TABLE IF NOT EXISTS allowed_users (
|
||||||
user_id INTEGER PRIMARY KEY,
|
user_id INTEGER PRIMARY KEY,
|
||||||
|
|
@ -29,89 +31,66 @@ def init_db():
|
||||||
)
|
)
|
||||||
""")
|
""")
|
||||||
|
|
||||||
conn.execute("CREATE INDEX IF NOT EXISTS idx_created ON numbers(created_at)")
|
# Индексы
|
||||||
conn.execute("CREATE INDEX IF NOT EXISTS idx_used ON numbers(used_at)")
|
conn.execute("CREATE INDEX IF NOT EXISTS idx_records_used ON records(used_at)")
|
||||||
|
conn.execute("CREATE INDEX IF NOT EXISTS idx_records_type ON records(type)")
|
||||||
|
|
||||||
|
|
||||||
# ========== Функции для работы с номерами и бонусными картами ==========
|
def add_record(record_type: str, phone: str = None, qr_code: str = None, qr_data: str = None) -> bool:
|
||||||
def add_number(phone: str, bonus_card: str = None) -> bool:
|
"""Добавляет запись"""
|
||||||
"""Добавляет номер и опционально бонусную карту"""
|
|
||||||
with sqlite3.connect(DB_NAME) as conn:
|
with sqlite3.connect(DB_NAME) as conn:
|
||||||
try:
|
try:
|
||||||
conn.execute(
|
conn.execute("""
|
||||||
"INSERT INTO numbers (phone, bonus_card, created_at) VALUES (?, ?, ?)",
|
INSERT INTO records (type, phone, qr_code, qr_data, created_at)
|
||||||
(phone, bonus_card, datetime.now().isoformat())
|
VALUES (?, ?, ?, ?, ?)
|
||||||
)
|
""", (record_type, phone, qr_code, qr_data, datetime.now().isoformat()))
|
||||||
return True
|
return True
|
||||||
except sqlite3.IntegrityError:
|
except sqlite3.IntegrityError:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def update_bonus_card(phone: str, bonus_card: str) -> bool:
|
def get_random_record():
|
||||||
"""Обновляет бонусную карту для существующего номера"""
|
"""Возвращает случайную запись"""
|
||||||
with sqlite3.connect(DB_NAME) as conn:
|
|
||||||
cursor = conn.execute(
|
|
||||||
"UPDATE numbers SET bonus_card = ? WHERE phone = ?",
|
|
||||||
(bonus_card, phone)
|
|
||||||
)
|
|
||||||
return cursor.rowcount > 0
|
|
||||||
|
|
||||||
|
|
||||||
def get_random_from_oldest():
|
|
||||||
with sqlite3.connect(DB_NAME) as conn:
|
with sqlite3.connect(DB_NAME) as conn:
|
||||||
conn.row_factory = sqlite3.Row
|
conn.row_factory = sqlite3.Row
|
||||||
|
|
||||||
total_count = conn.execute("SELECT COUNT(*) as count FROM numbers").fetchone()["count"]
|
total_count = conn.execute("SELECT COUNT(*) as count FROM records").fetchone()["count"]
|
||||||
|
|
||||||
if total_count <= 50:
|
if total_count <= 50:
|
||||||
limit = 10
|
limit = 10
|
||||||
else:
|
else:
|
||||||
limit = max(10, min(50, int(total_count * 0.2)))
|
limit = max(10, min(50, int(total_count * 0.2)))
|
||||||
|
|
||||||
oldest = conn.execute(
|
oldest = conn.execute("""
|
||||||
"SELECT *, COALESCE(used_at, created_at) as sort_date FROM numbers ORDER BY sort_date ASC LIMIT ?",
|
SELECT *, COALESCE(used_at, created_at) as sort_date
|
||||||
(limit,)
|
FROM records
|
||||||
).fetchall()
|
ORDER BY sort_date ASC
|
||||||
|
LIMIT ?
|
||||||
|
""", (limit,)).fetchall()
|
||||||
|
|
||||||
if not oldest:
|
if not oldest:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
selected = random.choice(oldest)
|
selected = random.choice(oldest)
|
||||||
|
|
||||||
conn.execute(
|
conn.execute("""
|
||||||
"UPDATE numbers SET usage_count = usage_count + 1, used_at = ? WHERE id = ?",
|
UPDATE records SET usage_count = usage_count + 1, used_at = ?
|
||||||
(datetime.now().isoformat(), selected["id"])
|
WHERE id = ?
|
||||||
)
|
""", (datetime.now().isoformat(), selected["id"]))
|
||||||
|
|
||||||
return dict(selected)
|
return dict(selected)
|
||||||
|
|
||||||
|
|
||||||
def get_all_numbers():
|
def get_all_records():
|
||||||
with sqlite3.connect(DB_NAME) as conn:
|
with sqlite3.connect(DB_NAME) as conn:
|
||||||
conn.row_factory = sqlite3.Row
|
conn.row_factory = sqlite3.Row
|
||||||
rows = conn.execute("SELECT * FROM numbers ORDER BY created_at DESC").fetchall()
|
rows = conn.execute("SELECT * FROM records ORDER BY created_at DESC").fetchall()
|
||||||
return [dict(row) for row in rows]
|
return [dict(row) for row in rows]
|
||||||
|
|
||||||
|
|
||||||
def delete_number(record_id: int):
|
def delete_record(record_id: int):
|
||||||
with sqlite3.connect(DB_NAME) as conn:
|
with sqlite3.connect(DB_NAME) as conn:
|
||||||
conn.execute("DELETE FROM numbers WHERE id = ?", (record_id,))
|
conn.execute("DELETE FROM records WHERE id = ?", (record_id,))
|
||||||
|
|
||||||
|
|
||||||
def get_bonus_card(record_id: int) -> str | None:
|
|
||||||
"""Получить бонусную карту по ID записи"""
|
|
||||||
with sqlite3.connect(DB_NAME) as conn:
|
|
||||||
conn.row_factory = sqlite3.Row
|
|
||||||
result = conn.execute("SELECT bonus_card FROM numbers WHERE id = ?", (record_id,)).fetchone()
|
|
||||||
return result["bonus_card"] if result else None
|
|
||||||
|
|
||||||
|
|
||||||
def get_bonus_card_by_phone(phone: str) -> str | None:
|
|
||||||
"""Получить бонусную карту по номеру телефона"""
|
|
||||||
with sqlite3.connect(DB_NAME) as conn:
|
|
||||||
conn.row_factory = sqlite3.Row
|
|
||||||
result = conn.execute("SELECT bonus_card FROM numbers WHERE phone = ?", (phone,)).fetchone()
|
|
||||||
return result["bonus_card"] if result else None
|
|
||||||
|
|
||||||
|
|
||||||
# ========== Функции для белого списка ==========
|
# ========== Функции для белого списка ==========
|
||||||
|
|
|
||||||
371
web_app.html
371
web_app.html
|
|
@ -3,7 +3,7 @@
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
|
||||||
<title>МракоЗвон — управление номерами</title>
|
<title>МракоЗвон — управление</title>
|
||||||
<style>
|
<style>
|
||||||
* {
|
* {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
|
@ -18,7 +18,7 @@
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
.container {
|
.container {
|
||||||
max-width: 600px;
|
max-width: 700px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
.card {
|
.card {
|
||||||
|
|
@ -35,8 +35,14 @@
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
h2 {
|
||||||
|
font-size: 22px;
|
||||||
|
color: #6ab3f3;
|
||||||
|
}
|
||||||
h3 {
|
h3 {
|
||||||
font-size: 20px;
|
font-size: 18px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
color: #aaa;
|
||||||
}
|
}
|
||||||
.total {
|
.total {
|
||||||
background: #2d7a2d;
|
background: #2d7a2d;
|
||||||
|
|
@ -45,55 +51,88 @@
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
.number-card {
|
.record-card {
|
||||||
background: #2a2a2a;
|
background: #2a2a2a;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
padding: 14px 16px;
|
padding: 14px 16px;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
display: flex;
|
border-left: 4px solid;
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
}
|
||||||
.phone {
|
.record-card.phone {
|
||||||
|
border-left-color: #2d7a2d;
|
||||||
|
}
|
||||||
|
.record-card.qr {
|
||||||
|
border-left-color: #6ab3f3;
|
||||||
|
}
|
||||||
|
.record-card.both {
|
||||||
|
border-left-color: #f3b33d;
|
||||||
|
}
|
||||||
|
.phone-number {
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
font-family: monospace;
|
font-family: monospace;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.qr-data {
|
||||||
|
font-size: 12px;
|
||||||
|
font-family: monospace;
|
||||||
|
color: #888;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
.badge {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
.badge-phone {
|
||||||
|
background: #2d7a2d;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.badge-qr {
|
||||||
|
background: #6ab3f3;
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
.badge-both {
|
||||||
|
background: #f3b33d;
|
||||||
|
color: #000;
|
||||||
}
|
}
|
||||||
.meta {
|
.meta {
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
color: #888;
|
color: #888;
|
||||||
margin-top: 4px;
|
margin-top: 6px;
|
||||||
|
}
|
||||||
|
.btn-group {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
.show-qr-btn {
|
||||||
|
background: #6ab3f3;
|
||||||
|
border: none;
|
||||||
|
color: #000;
|
||||||
|
padding: 6px 12px;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-size: 12px;
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
.delete-btn {
|
.delete-btn {
|
||||||
background: #dc2626;
|
background: #dc2626;
|
||||||
border: none;
|
border: none;
|
||||||
color: white;
|
color: white;
|
||||||
padding: 8px 16px;
|
padding: 6px 12px;
|
||||||
border-radius: 20px;
|
border-radius: 20px;
|
||||||
font-size: 13px;
|
font-size: 12px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-weight: 500;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
}
|
||||||
.bonus-btn {
|
.show-qr-btn:active {
|
||||||
background: #2d7a2d;
|
background: #4a8bc0;
|
||||||
border: none;
|
|
||||||
color: white;
|
|
||||||
padding: 8px 16px;
|
|
||||||
border-radius: 20px;
|
|
||||||
font-size: 13px;
|
|
||||||
cursor: pointer;
|
|
||||||
font-weight: 500;
|
|
||||||
flex-shrink: 0;
|
|
||||||
margin-right: 8px;
|
|
||||||
}
|
}
|
||||||
.delete-btn:active {
|
.delete-btn:active {
|
||||||
background: #b91c1c;
|
background: #b91c1c;
|
||||||
}
|
}
|
||||||
.bonus-btn:active {
|
|
||||||
background: #1e5e1e;
|
|
||||||
}
|
|
||||||
.loading {
|
.loading {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 60px 20px;
|
padding: 60px 20px;
|
||||||
|
|
@ -104,6 +143,31 @@
|
||||||
padding: 40px;
|
padding: 40px;
|
||||||
color: #666;
|
color: #666;
|
||||||
}
|
}
|
||||||
|
.tabs {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.tab-btn {
|
||||||
|
flex: 1;
|
||||||
|
background: #2d2d2d;
|
||||||
|
border: none;
|
||||||
|
color: white;
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
.tab-btn.active {
|
||||||
|
background: #2d7a2d;
|
||||||
|
}
|
||||||
|
.tab-content {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.tab-content.active {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
<script src="https://telegram.org/js/telegram-web-app.js"></script>
|
<script src="https://telegram.org/js/telegram-web-app.js"></script>
|
||||||
</head>
|
</head>
|
||||||
|
|
@ -111,10 +175,29 @@
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<h3>🌑 МракоЗвон</h3>
|
<h2>🌑 МракоЗвон</h2>
|
||||||
<div class="total" id="totalCount">Загрузка...</div>
|
<div class="total" id="totalCount">Загрузка...</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="list"></div>
|
|
||||||
|
<div class="tabs">
|
||||||
|
<button class="tab-btn active" onclick="showTab('all')">📋 Все</button>
|
||||||
|
<button class="tab-btn" onclick="showTab('phones')">📞 Номера</button>
|
||||||
|
<button class="tab-btn" onclick="showTab('qrs')">🃏 Карты</button>
|
||||||
|
<button class="tab-btn" onclick="showTab('both')">🔗 Номер+Карта</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="tab-all" class="tab-content active">
|
||||||
|
<div id="list-all"></div>
|
||||||
|
</div>
|
||||||
|
<div id="tab-phones" class="tab-content">
|
||||||
|
<div id="list-phones"></div>
|
||||||
|
</div>
|
||||||
|
<div id="tab-qrs" class="tab-content">
|
||||||
|
<div id="list-qrs"></div>
|
||||||
|
</div>
|
||||||
|
<div id="tab-both" class="tab-content">
|
||||||
|
<div id="list-both"></div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -122,83 +205,118 @@
|
||||||
const tg = window.Telegram.WebApp;
|
const tg = window.Telegram.WebApp;
|
||||||
tg.expand();
|
tg.expand();
|
||||||
|
|
||||||
async function loadNumbers() {
|
let currentData = [];
|
||||||
const listDiv = document.getElementById('list');
|
|
||||||
const totalSpan = document.getElementById('totalCount');
|
|
||||||
listDiv.innerHTML = '<div class="loading">⏳ Загрузка...</div>';
|
|
||||||
|
|
||||||
|
async function loadData() {
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/get_numbers');
|
const response = await fetch('/get_records');
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
currentData = data.records || [];
|
||||||
if (!data.numbers || data.numbers.length === 0) {
|
updateCounters();
|
||||||
listDiv.innerHTML = '<div class="empty">📭 Нет номеров</div>';
|
renderAll();
|
||||||
totalSpan.innerText = 'Всего: 0';
|
renderPhones();
|
||||||
return;
|
renderQRs();
|
||||||
}
|
renderBoth();
|
||||||
|
|
||||||
totalSpan.innerText = `Всего: ${data.numbers.length}`;
|
|
||||||
listDiv.innerHTML = '';
|
|
||||||
|
|
||||||
data.numbers.forEach(num => {
|
|
||||||
const card = document.createElement('div');
|
|
||||||
card.className = 'number-card';
|
|
||||||
const created = new Date(num.created_at).toLocaleString('ru');
|
|
||||||
const used = num.used_at ? new Date(num.used_at).toLocaleString('ru') : '—';
|
|
||||||
const hasBonus = num.has_bonus_card ? '✅ Есть' : '❌ Нет';
|
|
||||||
|
|
||||||
const bonusButton = num.has_bonus_card ?
|
|
||||||
`<button class="bonus-btn" data-id="${num.id}">🃏 Карта</button>` : '';
|
|
||||||
|
|
||||||
card.innerHTML = `
|
|
||||||
<div style="flex:1">
|
|
||||||
<div class="phone">📱 ${num.phone}</div>
|
|
||||||
<div class="meta">🃏 Бонусная карта: ${hasBonus}</div>
|
|
||||||
<div class="meta">➕ Добавлен: ${created}</div>
|
|
||||||
<div class="meta">🔄 Использован: ${num.usage_count} раз, последний: ${used}</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
${bonusButton}
|
|
||||||
<button class="delete-btn" data-id="${num.id}">🗑 Удалить</button>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
listDiv.appendChild(card);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Вешаем обработчики на кнопки удаления
|
|
||||||
document.querySelectorAll('.delete-btn').forEach(btn => {
|
|
||||||
btn.addEventListener('click', async (e) => {
|
|
||||||
const id = btn.getAttribute('data-id');
|
|
||||||
if (confirm('Удалить номер?')) {
|
|
||||||
await fetch('/delete_number', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {'Content-Type': 'application/json'},
|
|
||||||
body: JSON.stringify({id: parseInt(id)})
|
|
||||||
});
|
|
||||||
loadNumbers();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Вешаем обработчики на кнопки бонусных карт
|
|
||||||
document.querySelectorAll('.bonus-btn').forEach(btn => {
|
|
||||||
btn.addEventListener('click', async (e) => {
|
|
||||||
const id = btn.getAttribute('data-id');
|
|
||||||
await showBonusCard(id);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
listDiv.innerHTML = '<div class="empty">⚠️ Ошибка загрузки</div>';
|
document.getElementById('list-all').innerHTML = '<div class="empty">⚠️ Ошибка загрузки</div>';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function showBonusCard(id) {
|
function updateCounters() {
|
||||||
|
const total = currentData.length;
|
||||||
|
const phones = currentData.filter(r => r.type === 'phone').length;
|
||||||
|
const qrs = currentData.filter(r => r.type === 'qr').length;
|
||||||
|
const both = currentData.filter(r => r.type === 'both').length;
|
||||||
|
document.getElementById('totalCount').innerHTML = `📊 ${total} | 📞 ${phones} | 🃏 ${qrs} | 🔗 ${both}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTypeBadge(type) {
|
||||||
|
if (type === 'phone') return '<span class="badge badge-phone">📞 Только номер</span>';
|
||||||
|
if (type === 'qr') return '<span class="badge badge-qr">🃏 Только карта</span>';
|
||||||
|
return '<span class="badge badge-both">🔗 Номер + карта</span>';
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRecordHTML(record) {
|
||||||
|
const created = new Date(record.created_at).toLocaleString('ru');
|
||||||
|
const used = record.used_at ? new Date(record.used_at).toLocaleString('ru') : '—';
|
||||||
|
const badge = getTypeBadge(record.type);
|
||||||
|
|
||||||
|
let content = '';
|
||||||
|
if (record.type === 'phone' || record.type === 'both') {
|
||||||
|
content += `<div class="phone-number">📱 ${record.phone}</div>`;
|
||||||
|
}
|
||||||
|
if (record.type === 'qr' || record.type === 'both') {
|
||||||
|
const shortData = record.qr_data ? record.qr_data.slice(0, 50) + (record.qr_data.length > 50 ? '...' : '') : '—';
|
||||||
|
content += `<div class="qr-data">🃏 Данные карты: ${shortData}</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const showQRBtn = (record.type === 'qr' || record.type === 'both') && record.qr_code ?
|
||||||
|
`<button class="show-qr-btn" data-id="${record.id}">🖼️ Показать QR</button>` : '';
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="record-card ${record.type}">
|
||||||
|
${badge}
|
||||||
|
${content}
|
||||||
|
<div class="meta">➕ Добавлен: ${created}</div>
|
||||||
|
<div class="meta">🔄 Использован: ${record.usage_count} раз, последний: ${used}</div>
|
||||||
|
<div class="btn-group">
|
||||||
|
${showQRBtn}
|
||||||
|
<button class="delete-btn" data-id="${record.id}">🗑 Удалить</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderAll() {
|
||||||
|
const container = document.getElementById('list-all');
|
||||||
|
if (currentData.length === 0) {
|
||||||
|
container.innerHTML = '<div class="empty">📭 Нет записей</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
container.innerHTML = currentData.map(r => getRecordHTML(r)).join('');
|
||||||
|
attachEvents();
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderPhones() {
|
||||||
|
const container = document.getElementById('list-phones');
|
||||||
|
const phones = currentData.filter(r => r.type === 'phone');
|
||||||
|
if (phones.length === 0) {
|
||||||
|
container.innerHTML = '<div class="empty">📭 Нет номеров</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
container.innerHTML = phones.map(r => getRecordHTML(r)).join('');
|
||||||
|
attachEvents();
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderQRs() {
|
||||||
|
const container = document.getElementById('list-qrs');
|
||||||
|
const qrs = currentData.filter(r => r.type === 'qr');
|
||||||
|
if (qrs.length === 0) {
|
||||||
|
container.innerHTML = '<div class="empty">🃏 Нет бонусных карт</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
container.innerHTML = qrs.map(r => getRecordHTML(r)).join('');
|
||||||
|
attachEvents();
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderBoth() {
|
||||||
|
const container = document.getElementById('list-both');
|
||||||
|
const both = currentData.filter(r => r.type === 'both');
|
||||||
|
if (both.length === 0) {
|
||||||
|
container.innerHTML = '<div class="empty">🔗 Нет связок номер+карта</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
container.innerHTML = both.map(r => getRecordHTML(r)).join('');
|
||||||
|
attachEvents();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function showQRModal(recordId) {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/get_bonus_card/${id}`);
|
const response = await fetch(`/get_qr_code/${recordId}`);
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
if (data.bonus_card) {
|
if (data.qr_code) {
|
||||||
const modal = document.createElement('div');
|
const modal = document.createElement('div');
|
||||||
modal.style.position = 'fixed';
|
modal.style.position = 'fixed';
|
||||||
modal.style.top = '0';
|
modal.style.top = '0';
|
||||||
|
|
@ -213,24 +331,75 @@
|
||||||
modal.style.cursor = 'pointer';
|
modal.style.cursor = 'pointer';
|
||||||
|
|
||||||
const img = document.createElement('img');
|
const img = document.createElement('img');
|
||||||
img.src = data.bonus_card;
|
img.src = data.qr_code;
|
||||||
img.style.maxWidth = '85%';
|
img.style.maxWidth = '85%';
|
||||||
img.style.maxHeight = '85%';
|
img.style.maxHeight = '85%';
|
||||||
img.style.borderRadius = '12px';
|
img.style.borderRadius = '12px';
|
||||||
img.style.boxShadow = '0 4px 20px rgba(0,0,0,0.5)';
|
|
||||||
|
|
||||||
modal.onclick = () => modal.remove();
|
modal.onclick = () => modal.remove();
|
||||||
modal.appendChild(img);
|
modal.appendChild(img);
|
||||||
document.body.appendChild(modal);
|
document.body.appendChild(modal);
|
||||||
} else {
|
} else {
|
||||||
alert('Бонусная карта отсутствует для этого номера');
|
alert('QR-код отсутствует');
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
alert('Ошибка загрузки бонусной карты');
|
alert('Ошибка загрузки QR');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
loadNumbers();
|
async function deleteRecord(recordId) {
|
||||||
|
if (!confirm('Удалить запись?')) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/delete_record', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {'Content-Type': 'application/json'},
|
||||||
|
body: JSON.stringify({id: recordId})
|
||||||
|
});
|
||||||
|
if (response.ok) {
|
||||||
|
loadData();
|
||||||
|
} else {
|
||||||
|
alert('Ошибка при удалении');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
alert('Ошибка');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function attachEvents() {
|
||||||
|
document.querySelectorAll('.show-qr-btn').forEach(btn => {
|
||||||
|
btn.removeEventListener('click', btn._handler);
|
||||||
|
btn._handler = () => showQRModal(btn.dataset.id);
|
||||||
|
btn.addEventListener('click', btn._handler);
|
||||||
|
});
|
||||||
|
|
||||||
|
document.querySelectorAll('.delete-btn').forEach(btn => {
|
||||||
|
btn.removeEventListener('click', btn._handler);
|
||||||
|
btn._handler = () => deleteRecord(btn.dataset.id);
|
||||||
|
btn.addEventListener('click', btn._handler);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function showTab(tabName) {
|
||||||
|
document.querySelectorAll('.tab-btn').forEach(btn => btn.classList.remove('active'));
|
||||||
|
document.querySelectorAll('.tab-content').forEach(content => content.classList.remove('active'));
|
||||||
|
|
||||||
|
if (tabName === 'all') {
|
||||||
|
document.querySelector('.tab-btn:first-child').classList.add('active');
|
||||||
|
document.getElementById('tab-all').classList.add('active');
|
||||||
|
} else if (tabName === 'phones') {
|
||||||
|
document.querySelectorAll('.tab-btn')[1].classList.add('active');
|
||||||
|
document.getElementById('tab-phones').classList.add('active');
|
||||||
|
} else if (tabName === 'qrs') {
|
||||||
|
document.querySelectorAll('.tab-btn')[2].classList.add('active');
|
||||||
|
document.getElementById('tab-qrs').classList.add('active');
|
||||||
|
} else if (tabName === 'both') {
|
||||||
|
document.querySelectorAll('.tab-btn')[3].classList.add('active');
|
||||||
|
document.getElementById('tab-both').classList.add('active');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loadData();
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
Loading…
Reference in New Issue