diff --git a/resources/commands/expenses/breakdown.lua b/resources/commands/expenses/breakdown.lua new file mode 100644 index 0000000..ba8f698 --- /dev/null +++ b/resources/commands/expenses/breakdown.lua @@ -0,0 +1,55 @@ +-- Per-category breakdown for the last 7 days, sorted by amount desc. + +local lang = jarvis.context.language +local raw_state = jarvis.state.get("entries") or "" +if raw_state == "" then + return jarvis.cmd.not_found(lang == "ru" + and "Нечего разбивать, сэр." + or "Nothing to break down, sir.") +end + +local now = jarvis.context.time +local now_ts = os.time({ + year = now.year, month = now.month, day = now.day, + hour = now.hour, min = now.minute, sec = now.second or 0, +}) +local cutoff = now_ts - 7 * 86400 + +local by_cat = {} +for chunk in raw_state:gmatch("([^|]+)") do + local ts, am, ca = chunk:match("^(%d+),([%-%d%.]+),(.+)$") + ts = tonumber(ts); am = tonumber(am) + if ts and am and ts >= cutoff then + by_cat[ca] = (by_cat[ca] or 0) + am + end +end + +local rows = {} +for ca, total in pairs(by_cat) do + table.insert(rows, { ca = ca, total = total }) +end +table.sort(rows, function(a, b) return a.total > b.total end) + +if #rows == 0 then + return jarvis.cmd.ok(lang == "ru" + and "На этой неделе тишина по тратам, сэр." + or "Quiet week on spending, sir.") +end + +-- Top 5 categories spoken; rest collapses into "и другое" +local top = math.min(5, #rows) +local parts = {} +for i = 1, top do + table.insert(parts, string.format("%s %.0f", rows[i].ca, rows[i].total)) +end +local tail = "" +if #rows > top then + local rest_total = 0 + for i = top + 1, #rows do rest_total = rest_total + rows[i].total end + tail = string.format(lang == "ru" + and " и другое %.0f" or " and other %.0f", rest_total) +end + +return jarvis.cmd.ok(string.format(lang == "ru" + and "По категориям: %s%s." + or "By category: %s%s.", table.concat(parts, ", "), tail)) diff --git a/resources/commands/expenses/command.toml b/resources/commands/expenses/command.toml new file mode 100644 index 0000000..5dbc168 --- /dev/null +++ b/resources/commands/expenses/command.toml @@ -0,0 +1,91 @@ +# Personal-finance tracker — voice-log expenses by category, get totals. +# Storage is per-pack via `jarvis.state` (auto-namespaced JSON), so it doesn't +# bleed into other packs' data. +# +# Voice flow: +# "потратил 500 на еду" → log entry (amount=500, category="еда") +# "сколько я потратил сегодня" → speak today's total +# "сколько на этой неделе" → speak 7-day total +# "куда ушли деньги" → speak per-category breakdown for the week + +[[commands]] +id = "expenses.log" +type = "lua" +script = "log.lua" +sandbox = "standard" +timeout = 3000 + +[commands.phrases] +ru = [ + "потратил", + "записать трату", + "запиши трату", + "купил за", + "заплатил за", +] +en = [ + "spent", + "log expense", + "paid for", + "bought", +] + + +[[commands]] +id = "expenses.today" +type = "lua" +script = "today.lua" +sandbox = "standard" +timeout = 3000 + +[commands.phrases] +ru = [ + "сколько я потратил сегодня", + "траты сегодня", + "сколько ушло сегодня", +] +en = [ + "spending today", + "expenses today", + "how much spent today", +] + + +[[commands]] +id = "expenses.week" +type = "lua" +script = "week.lua" +sandbox = "standard" +timeout = 3000 + +[commands.phrases] +ru = [ + "сколько я потратил на этой неделе", + "траты на неделе", + "недельные траты", +] +en = [ + "spending this week", + "weekly expenses", +] + + +[[commands]] +id = "expenses.breakdown" +type = "lua" +script = "breakdown.lua" +sandbox = "standard" +timeout = 3000 + +[commands.phrases] +ru = [ + "куда ушли деньги", + "разбивка по тратам", + "на что я тратил", + "разбивка по категориям", +] +en = [ + "where did the money go", + "expense breakdown", + "spending by category", +] diff --git a/resources/commands/expenses/log.lua b/resources/commands/expenses/log.lua new file mode 100644 index 0000000..807b713 --- /dev/null +++ b/resources/commands/expenses/log.lua @@ -0,0 +1,74 @@ +-- Voice "потратил 500 на еду" → append {amount, category, timestamp} to the +-- pack's state JSON. +-- +-- Parsing the phrase: +-- 1. Strip the trigger ("потратил" / "spent" / ...) +-- 2. First number = amount +-- 3. After the number: optional "на <слово>" → category (RU) or "for " +-- / "on " (EN). Otherwise category defaults to "разное" / "misc". + +local lang = jarvis.context.language +local raw = (jarvis.context.phrase or ""):lower() + +local triggers = { + "потратил", "записать трату", "запиши трату", "купил за", "заплатил за", + "spent", "log expense", "paid for", "bought", +} +local rest = jarvis.text.strip_trigger(raw, triggers) or raw +rest = rest:gsub("^%s+", ""):gsub("%s+$", "") + +-- Pull the first integer-like token. Allows comma decimals ("12,50" → 12.5). +local amount_str, after = rest:match("(%-?[%d]+[%.,]?[%d]*)%s*(.*)") +if not amount_str then + return jarvis.cmd.not_found(lang == "ru" + and "Не понял сумму, сэр." + or "I didn't catch the amount, sir.") +end +amount_str = amount_str:gsub(",", ".") +local amount = tonumber(amount_str) +if not amount or amount <= 0 then + return jarvis.cmd.not_found(lang == "ru" + and "Сумма не похожа на число." + or "That doesn't look like an amount.") +end + +local category = "разное" +local cat_match = (after or ""):match("на%s+(%S+)") or (after or ""):match("for%s+(%S+)") + or (after or ""):match("on%s+(%S+)") +if cat_match and cat_match ~= "" then + category = cat_match +end + +-- Load existing log, append, save. +local raw_state = jarvis.state.get("entries") or "" +local entries = {} +if raw_state ~= "" then + -- Encoded as "ts1,amount1,cat1|ts2,amount2,cat2|..." + for chunk in raw_state:gmatch("([^|]+)") do + local ts, am, ca = chunk:match("^(%d+),([%-%d%.]+),(.+)$") + if ts and am and ca then + table.insert(entries, { ts = tonumber(ts), am = tonumber(am), ca = ca }) + end + end +end + +local now = jarvis.context.time +-- chrono::Local timestamp is exposed via {year, month, day, hour, minute, second} +-- but not as a unix epoch. Approximate via os.time({...}) — sandbox standard +-- gives us os.time/date. +local ts = os.time({ + year = now.year, month = now.month, day = now.day, + hour = now.hour, min = now.minute, sec = now.second or 0, +}) +table.insert(entries, { ts = ts, am = amount, ca = category }) + +local out = {} +for _, e in ipairs(entries) do + table.insert(out, string.format("%d,%s,%s", e.ts, tostring(e.am), e.ca)) +end +jarvis.state.set("entries", table.concat(out, "|")) + +return jarvis.cmd.ok(string.format(lang == "ru" + and "Записал: %s на %s." + or "Logged: %s on %s.", + amount_str, category)) diff --git a/resources/commands/expenses/today.lua b/resources/commands/expenses/today.lua new file mode 100644 index 0000000..8269d22 --- /dev/null +++ b/resources/commands/expenses/today.lua @@ -0,0 +1,36 @@ +-- Sum of today's expenses (local calendar day). + +local lang = jarvis.context.language +local raw_state = jarvis.state.get("entries") or "" + +if raw_state == "" then + return jarvis.cmd.not_found(lang == "ru" + and "Сегодня ничего не тратили, сэр." + or "No expenses logged today, sir.") +end + +local now = jarvis.context.time +local day_start = os.time({ + year = now.year, month = now.month, day = now.day, + hour = 0, min = 0, sec = 0, +}) +local day_end = day_start + 86400 + +local total = 0 +for chunk in raw_state:gmatch("([^|]+)") do + local ts, am = chunk:match("^(%d+),([%-%d%.]+),") + ts = tonumber(ts); am = tonumber(am) + if ts and am and ts >= day_start and ts < day_end then + total = total + am + end +end + +if total == 0 then + return jarvis.cmd.ok(lang == "ru" + and "Сегодня ничего не тратили, сэр." + or "Nothing spent today, sir.") +end + +return jarvis.cmd.ok(string.format(lang == "ru" + and "Сегодня потратили %.0f." + or "Today's spending: %.0f.", total)) diff --git a/resources/commands/expenses/week.lua b/resources/commands/expenses/week.lua new file mode 100644 index 0000000..25d6e41 --- /dev/null +++ b/resources/commands/expenses/week.lua @@ -0,0 +1,37 @@ +-- Sum of the last 7 days' expenses (rolling window). + +local lang = jarvis.context.language +local raw_state = jarvis.state.get("entries") or "" +if raw_state == "" then + return jarvis.cmd.not_found(lang == "ru" + and "На этой неделе ничего не тратили." + or "Nothing logged this week.") +end + +local now = jarvis.context.time +local now_ts = os.time({ + year = now.year, month = now.month, day = now.day, + hour = now.hour, min = now.minute, sec = now.second or 0, +}) +local cutoff = now_ts - 7 * 86400 + +local total = 0 +local count = 0 +for chunk in raw_state:gmatch("([^|]+)") do + local ts, am = chunk:match("^(%d+),([%-%d%.]+),") + ts = tonumber(ts); am = tonumber(am) + if ts and am and ts >= cutoff then + total = total + am + count = count + 1 + end +end + +if total == 0 then + return jarvis.cmd.ok(lang == "ru" + and "На этой неделе тратами не похваляешься, сэр." + or "Nothing tracked this week, sir.") +end + +return jarvis.cmd.ok(string.format(lang == "ru" + and "За неделю %d трат на сумму %.0f." + or "This week: %d expenses totalling %.0f.", count, total))