💰 Додавання витрат
/add 150 їжаЗагальна витрата (÷2 між командами)
/d1 200 готельВитрата тільки Команди 1 (Даша & Віталік)
/d2 180 кафеВитрата тільки Команди 2 (Денис & Інна)
/паливо 320Швидка витрата на паливо (загальна)
📊 Статистика
/statПоточний стан бюджету обох команд
/settleРозрахунок — хто кому винен
/dayЩо на сьогодні по маршруту
/progressПрогрес маршруту у %
✅ Маршрут
/done Tre CimeПозначити етап пройденим
/nextНаступний крок маршруту
/mapПосилання на Google Maps
📨 Автоматичні нотифікації
⏰Щодня о 7:00 — план дня, погода, нагадування
✅При позначенні пройдено — прогрес + баланс команд
💰При додаванні витрати — оновлений баланс
⚖️В кінці поїздки — повний фінальний розрахунок
📋 Google Apps Script — повний код бота
const BOT_TOKEN = 'ВАШ_TOKEN';
const CHAT_ID = 'ВАШ_CHAT_ID';
const SHEET_ID = 'ID_GOOGLE_SHEETS'; // Файл → Поділитись → Скопіювати ID
// ─── ЩОДЕННІ НОТИФІКАЦІЇ ───
const TRIP_DAYS = {
'2026-06-28': { emoji:'🚗', msg:'Виїзд з Варшави! Ціль — Нюрнберг (~10 год)', tip:'Заправся в Польщі 98-м — дешевше!' },
'2026-06-30': { emoji:'🥾', msg:'Хайк Schäfler–Saxer Lücke–Säntis!', tip:'Раннє виходження — туман розходиться до 9:00' },
'2026-07-04': { emoji:'🧗', msg:'ДЕНЬ Х! Gemmi Pass + Daubenhorn via ferrata!', tip:'Перевір спорядження ввечері. Удачі!' },
'2026-07-08': { emoji:'🏆', msg:'TRE CIME DI LAVAREDO! Найкращий хайк!', tip:'⚠️ ВИЇЖДЖАЙТЕ О 6:00! Паркінг повний до 9:00' },
'2026-07-11': { emoji:'🏠', msg:'Повертаємось до Варшави! Яка була поїздка?!' },
};
function sendDailyNotification() {
const today = Utilities.formatDate(new Date(), 'Europe/Warsaw', 'yyyy-MM-dd');
const day = TRIP_DAYS[today];
if (!day) return;
const ss = SpreadsheetApp.openById(SHEET_ID);
const stats = getStats(ss);
const text = [
`${day.emoji} *Швейцарія & Італія 2026 · ${today}*`,
``,
day.msg,
day.tip ? `\n💡 _${day.tip}_` : '',
``,
`💰 *Бюджет:*`,
`🟣 Команда 1: ${stats.t1} zł витрачено`,
`🔴 Команда 2: ${stats.t2} zł витрачено`,
`📊 Загалом: ${stats.total} zł з 14 000 zł`,
].join('\n');
sendMsg(text);
}
// ─── ОБРОБНИК КОМАНД ───
function doPost(e) {
const update = JSON.parse(e.postData.contents);
const msg = update.message;
if (!msg || !msg.text) return;
const text = msg.text.trim();
const ss = SpreadsheetApp.openById(SHEET_ID);
// /add 150 їжа — загальна витрата
if (text.match(/^\/add\s+(\d+)\s*(.*)/i)) {
const [, amt, desc] = text.match(/^\/add\s+(\d+)\s*(.*)/i);
const who = msg.from.first_name;
addExpense(ss, 'ЗАГАЛЬНА', desc || 'витрата', Number(amt), who);
const half = Math.round(amt/2);
sendMsg(`✅ Додано: *${desc || 'витрата'}* — ${amt} zł\n🟣 К1: −${half} zł 🔴 К2: −${half} zł\nДодав: ${who}`);
return;
}
// /d1 200 готель — тільки Команда 1
if (text.match(/^\/d1\s+(\d+)\s*(.*)/i)) {
const [, amt, desc] = text.match(/^\/d1\s+(\d+)\s*(.*)/i);
addExpense(ss, 'КОМАНДА_1', desc || 'витрата', Number(amt), msg.from.first_name);
sendMsg(`✅ 🟣 Команда 1: *${desc || 'витрата'}* — ${amt} zł`);
return;
}
// /d2 200 кафе — тільки Команда 2
if (text.match(/^\/d2\s+(\d+)\s*(.*)/i)) {
const [, amt, desc] = text.match(/^\/d2\s+(\d+)\s*(.*)/i);
addExpense(ss, 'КОМАНДА_2', desc || 'витрата', Number(amt), msg.from.first_name);
sendMsg(`✅ 🔴 Команда 2: *${desc || 'витрата'}* — ${amt} zł`);
return;
}
// /stat — поточний стан
if (text === '/stat') {
const s = getStats(ss);
const bar = '█'.repeat(Math.round(s.pct/10)) + '░'.repeat(10-Math.round(s.pct/10));
sendMsg([
`📊 *Бюджет поїздки*`,
``,
`🟣 Команда 1 (Даша & Віталік)`,
` Витрачено: ${s.t1} zł | На особу: ${Math.round(s.t1/2)} zł`,
``,
`🔴 Команда 2 (Денис & Інна)`,
` Витрачено: ${s.t2} zł | На особу: ${Math.round(s.t2/2)} zł`,
``,
`💰 Загалом: ${s.total} / 14000 zł (${s.pct}%)`,
`${bar}`,
`💚 Залишок: ${14000 - s.total} zł`,
].join('\n'));
return;
}
// /settle — розрахунок
if (text === '/settle') {
const s = getStats(ss);
const diff = s.t1 - s.t2;
const absDiff = Math.abs(diff);
const who = diff > 0 ? 'Команда 2 (Денис & Інна)' : 'Команда 1 (Даша & Віталік)';
const owes = diff > 0 ? 'Команда 1' : 'Команда 2';
sendMsg([
`⚖️ *Фінальний розрахунок*`,
``,
`🟣 Команда 1: ${s.t1} zł`,
`🔴 Команда 2: ${s.t2} zł`,
``,
absDiff < 10
? `✅ Рахунок рівний! Нікому нічого не винні 🎉`
: `💸 *${owes} повинна сплатити ${who}: ${absDiff} zł*`,
``,
`Тобто: ${Math.round(absDiff/2)} zł з кожного учасника`,
].join('\n'));
return;
}
}
// ─── ДОПОМІЖНІ ФУНКЦІЇ ───
function addExpense(ss, team, desc, amount, who) {
const sheet = ss.getSheetByName('Витрати') || ss.insertSheet('Витрати');
const date = Utilities.formatDate(new Date(), 'Europe/Warsaw', 'dd.MM.yyyy');
sheet.appendRow([date, team, desc, amount, who]);
}
function getStats(ss) {
const sheet = ss.getSheetByName('Витрати');
if (!sheet) return { t1:0, t2:0, total:0, pct:0 };
const rows = sheet.getDataRange().getValues().slice(1);
let t1=0, t2=0, common=0;
rows.forEach(r => {
const team = String(r[1]).toUpperCase();
const amt = Number(r[3]) || 0;
if (team === 'КОМАНДА_1') t1 += amt;
else if (team === 'КОМАНДА_2') t2 += amt;
else { common += amt; t1 += amt/2; t2 += amt/2; }
});
const total = t1 + t2;
return { t1: Math.round(t1), t2: Math.round(t2), total: Math.round(total), pct: Math.round(total/140) };
}
function sendMsg(text) {
UrlFetchApp.fetch(`https://api.telegram.org/bot${BOT_TOKEN}/sendMessage`, {
method:'post', contentType:'application/json',
payload: JSON.stringify({ chat_id: CHAT_ID, text, parse_mode:'Markdown' })
});
}
// Встановити вебхук (запусти один раз вручну):
function setWebhook() {
const url = `https://script.google.com/macros/s/ВАШ_SCRIPT_ID/exec`;
UrlFetchApp.fetch(
`https://api.telegram.org/bot${BOT_TOKEN}/setWebhook?url=${url}`
);
}