From fea4d38ce8f414fccd5ca2a285186d6f680b40c2 Mon Sep 17 00:00:00 2001 From: Claude CLI Agent Date: Sun, 29 Mar 2026 13:12:19 +0000 Subject: [PATCH] Phase 3-4: Goals AI planner, Team tasks, Push notifications, i18n (CZ/HE/RU/UA) - Goals CRUD API + AI study plan generator + progress reports - Goals frontend page with progress bars - Team tasks: assign, transfer, collaborate endpoints - Push notifications: web-push, VAPID, subscribe/send - i18n: 4 languages (cs, he, ru, ua) translation files - notifications.js + goals.js routes Co-Authored-By: Claude Opus 4.6 (1M context) --- api/package-lock.json | 89 ++++- api/package.json | 3 +- api/src/index.js | 2 + api/src/routes/goals.js | 155 +++++++++ api/src/routes/notifications.js | 77 +++++ api/src/routes/tasks.js | 56 ++++ apps/tasks/app/goals/page.tsx | 482 ++++++++++++++++++++++++++++ apps/tasks/app/layout.tsx | 21 +- apps/tasks/app/login/page.tsx | 16 +- apps/tasks/app/register/page.tsx | 22 +- apps/tasks/app/settings/page.tsx | 50 ++- apps/tasks/components/BottomNav.tsx | 91 +++--- apps/tasks/components/Header.tsx | 30 +- apps/tasks/lib/api.ts | 55 ++++ apps/tasks/lib/i18n.tsx | 99 ++++++ apps/tasks/messages/cs.json | 10 + apps/tasks/messages/he.json | 10 + apps/tasks/messages/ru.json | 10 + apps/tasks/messages/ua.json | 10 + 19 files changed, 1176 insertions(+), 112 deletions(-) create mode 100644 api/src/routes/goals.js create mode 100644 api/src/routes/notifications.js create mode 100644 apps/tasks/app/goals/page.tsx create mode 100644 apps/tasks/lib/i18n.tsx create mode 100644 apps/tasks/messages/cs.json create mode 100644 apps/tasks/messages/he.json create mode 100644 apps/tasks/messages/ru.json create mode 100644 apps/tasks/messages/ua.json diff --git a/api/package-lock.json b/api/package-lock.json index 8a4892a..be3d4e4 100644 --- a/api/package-lock.json +++ b/api/package-lock.json @@ -21,7 +21,8 @@ "ioredis": "^5.10.1", "pg": "^8.20.0", "redis": "^5.11.0", - "uuid": "^13.0.0" + "uuid": "^13.0.0", + "web-push": "^3.6.7" }, "devDependencies": { "nodemon": "^3.1.14" @@ -435,6 +436,15 @@ "integrity": "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==", "license": "MIT" }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, "node_modules/ajv": { "version": "8.18.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", @@ -590,6 +600,12 @@ "node": ">=8" } }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, "node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", @@ -978,6 +994,15 @@ "node": ">=18.0.0" } }, + "node_modules/http_ece": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http_ece/-/http_ece-1.2.0.tgz", + "integrity": "sha512-JrF8SSLVmcvc5NducxgyOrKXe3EsyHMgBFgSaIUGmArKe+rwr0uphRkRXvwiom3I+fpIfoItveHrfudL8/rxuA==", + "license": "MIT", + "engines": { + "node": ">=16" + } + }, "node_modules/http-errors": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", @@ -998,6 +1023,19 @@ "url": "https://opencollective.com/express" } }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/ignore-by-default": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", @@ -1145,6 +1183,27 @@ "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "license": "MIT" }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, "node_modules/light-my-request": { "version": "6.6.0", "resolved": "https://registry.npmjs.org/light-my-request/-/light-my-request-6.6.0.tgz", @@ -1236,6 +1295,15 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/minipass": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", @@ -1905,6 +1973,25 @@ "uuid": "dist-node/bin/uuid" } }, + "node_modules/web-push": { + "version": "3.6.7", + "resolved": "https://registry.npmjs.org/web-push/-/web-push-3.6.7.tgz", + "integrity": "sha512-OpiIUe8cuGjrj3mMBFWY+e4MMIkW3SVT+7vEIjvD9kejGUypv8GPDf84JdPWskK8zMRIJ6xYGm+Kxr8YkPyA0A==", + "license": "MPL-2.0", + "dependencies": { + "asn1.js": "^5.3.0", + "http_ece": "1.2.0", + "https-proxy-agent": "^7.0.0", + "jws": "^4.0.0", + "minimist": "^1.2.5" + }, + "bin": { + "web-push": "src/cli.js" + }, + "engines": { + "node": ">= 16" + } + }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", diff --git a/api/package.json b/api/package.json index 5663dc6..9d5234b 100644 --- a/api/package.json +++ b/api/package.json @@ -25,7 +25,8 @@ "ioredis": "^5.10.1", "pg": "^8.20.0", "redis": "^5.11.0", - "uuid": "^13.0.0" + "uuid": "^13.0.0", + "web-push": "^3.6.7" }, "devDependencies": { "nodemon": "^3.1.14" diff --git a/api/src/index.js b/api/src/index.js index 6702a68..5309afa 100644 --- a/api/src/index.js +++ b/api/src/index.js @@ -48,6 +48,8 @@ app.register(require("./routes/connectors/odoo"), { prefix: "/api/v1" }); app.register(require("./routes/connectors/moodle"), { prefix: "/api/v1" }); app.register(require("./routes/connectors/pohoda"), { prefix: "/api/v1" }); app.register(require("./routes/chat"), { prefix: "/api/v1" }); +app.register(require("./routes/notifications"), { prefix: "/api/v1" }); +app.register(require("./routes/goals"), { prefix: "/api/v1" }); // Graceful shutdown const shutdown = async (signal) => { diff --git a/api/src/routes/goals.js b/api/src/routes/goals.js new file mode 100644 index 0000000..4060fd8 --- /dev/null +++ b/api/src/routes/goals.js @@ -0,0 +1,155 @@ +// Task Team — Goals API + AI Planner — 2026-03-29 +const Anthropic = require("@anthropic-ai/sdk"); + +async function goalRoutes(app) { + const anthropic = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY }); + + // List goals + app.get("/goals", async (req) => { + const { rows } = await app.db.query( + "SELECT g.*, tg.name as group_name, tg.color as group_color, tg.icon as group_icon FROM goals g LEFT JOIN task_groups tg ON g.group_id = tg.id ORDER BY g.target_date ASC NULLS LAST" + ); + return { data: rows }; + }); + + // Get goal with related tasks + app.get("/goals/:id", async (req) => { + const { rows } = await app.db.query("SELECT * FROM goals WHERE id = $1", [req.params.id]); + if (!rows.length) throw { statusCode: 404, message: "Goal not found" }; + const { rows: tasks } = await app.db.query( + "SELECT * FROM tasks WHERE external_id LIKE $1 ORDER BY scheduled_at ASC", + ["goal:" + req.params.id + "%"] + ); + return { data: { ...rows[0], tasks } }; + }); + + // Create goal + app.post("/goals", async (req) => { + const { title, target_date, group_id, user_id, plan } = req.body; + const { rows } = await app.db.query( + "INSERT INTO goals (title, target_date, group_id, user_id, plan) VALUES ($1, $2, $3, $4, $5) RETURNING *", + [title, target_date || null, group_id || null, user_id || null, JSON.stringify(plan || {})] + ); + return { data: rows[0] }; + }); + + // Update goal progress + app.put("/goals/:id", async (req) => { + const { title, target_date, progress_pct, plan } = req.body; + const { rows } = await app.db.query( + "UPDATE goals SET title=COALESCE($1,title), target_date=COALESCE($2,target_date), progress_pct=COALESCE($3,progress_pct), plan=COALESCE($4,plan), updated_at=NOW() WHERE id=$5 RETURNING *", + [title, target_date, progress_pct, plan ? JSON.stringify(plan) : null, req.params.id] + ); + if (!rows.length) throw { statusCode: 404, message: "Goal not found" }; + return { data: rows[0] }; + }); + + // Delete goal + app.delete("/goals/:id", async (req) => { + const { rowCount } = await app.db.query("DELETE FROM goals WHERE id = $1", [req.params.id]); + if (!rowCount) throw { statusCode: 404, message: "Goal not found" }; + return { status: "deleted" }; + }); + + // AI: Generate study plan for a goal + app.post("/goals/:id/plan", async (req) => { + const { rows } = await app.db.query("SELECT * FROM goals WHERE id = $1", [req.params.id]); + if (!rows.length) throw { statusCode: 404, message: "Goal not found" }; + const goal = rows[0]; + + const { rows: groups } = await app.db.query( + "SELECT name, time_zones FROM task_groups WHERE id = $1", [goal.group_id] + ); + const groupInfo = groups[0] || {}; + + const response = await anthropic.messages.create({ + model: "claude-sonnet-4-20250514", + max_tokens: 2048, + system: "Jsi planovaci AI asistent. Generujes detailni studijni/pracovni plany v JSON formatu. Odpovidej v cestine.", + messages: [{ + role: "user", + content: "Vytvor tydenni plan pro tento cil:\nCil: " + goal.title + + "\nTermin: " + (goal.target_date || "bez terminu") + + "\nAktualni progres: " + goal.progress_pct + "%" + + "\nSkupina: " + (groupInfo.name || "obecna") + + "\nCasove zony skupiny: " + JSON.stringify(groupInfo.time_zones || []) + + "\n\nVrat JSON s polem \"weeks\" kde kazdy tyden ma \"week_number\", \"focus\", \"tasks\" (pole s title, description, duration_hours, day_of_week).\nZahrn spaced repetition pro opakovani." + }] + }); + + let plan; + try { + const text = response.content[0].text; + const jsonMatch = text.match(/\{[\s\S]*\}/); + plan = jsonMatch ? JSON.parse(jsonMatch[0]) : { raw: text }; + } catch (e) { + plan = { raw: response.content[0].text }; + } + + // Save plan + await app.db.query( + "UPDATE goals SET plan = $1, updated_at = NOW() WHERE id = $2", + [JSON.stringify(plan), goal.id] + ); + + // Create tasks from plan + let tasksCreated = 0; + if (plan.weeks) { + for (const week of plan.weeks) { + for (const task of (week.tasks || [])) { + await app.db.query( + "INSERT INTO tasks (title, description, group_id, external_id, external_source, status, priority) VALUES ($1, $2, $3, $4, $5, $6, $7)", + [ + task.title, + task.description || "", + goal.group_id, + "goal:" + goal.id + ":w" + week.week_number, + "goal", + "pending", + "medium" + ] + ); + tasksCreated++; + } + } + } + + return { data: { plan, tasks_created: tasksCreated } }; + }); + + // AI: Progress report for a goal + app.get("/goals/:id/report", async (req) => { + const { rows } = await app.db.query("SELECT * FROM goals WHERE id = $1", [req.params.id]); + if (!rows.length) throw { statusCode: 404, message: "Goal not found" }; + + const { rows: tasks } = await app.db.query( + "SELECT title, status, completed_at FROM tasks WHERE external_id LIKE $1", + ["goal:" + req.params.id + "%"] + ); + + const done = tasks.filter(t => t.status === "completed" || t.status === "done").length; + const total = tasks.length; + + const response = await anthropic.messages.create({ + model: "claude-sonnet-4-20250514", + max_tokens: 1024, + system: "Jsi AI coach. Davas motivacni zpetnou vazbu k plneni cilu. Odpovidej v cestine, strucne.", + messages: [{ + role: "user", + content: "Cil: " + rows[0].title + + "\nSplneno: " + done + "/" + total + " ukolu (" + (total ? Math.round(done / total * 100) : 0) + "%)" + + "\nProgres: " + rows[0].progress_pct + "%" + + "\nDej zpetnou vazbu a doporuceni." + }] + }); + + return { + data: { + report: response.content[0].text, + stats: { done, total, pct: total ? Math.round(done / total * 100) : 0 } + } + }; + }); +} + +module.exports = goalRoutes; diff --git a/api/src/routes/notifications.js b/api/src/routes/notifications.js new file mode 100644 index 0000000..afd2743 --- /dev/null +++ b/api/src/routes/notifications.js @@ -0,0 +1,77 @@ +// Task Team — Push Notifications — 2026-03-29 +const webpush = require("web-push"); + +async function notificationRoutes(app) { + // Generate VAPID keys if not exist + const vapidKeys = { + publicKey: process.env.VAPID_PUBLIC_KEY || "", + privateKey: process.env.VAPID_PRIVATE_KEY || "" + }; + + if (!vapidKeys.publicKey) { + const keys = webpush.generateVAPIDKeys(); + vapidKeys.publicKey = keys.publicKey; + vapidKeys.privateKey = keys.privateKey; + console.log("VAPID keys generated. Add to env:", JSON.stringify(keys)); + } + + webpush.setVapidDetails("mailto:admin@hasdo.info", vapidKeys.publicKey, vapidKeys.privateKey); + + // Create subscriptions table if not exists + await app.db.query(` + CREATE TABLE IF NOT EXISTS push_subscriptions ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + user_id UUID REFERENCES users(id) ON DELETE CASCADE, + subscription JSONB NOT NULL, + created_at TIMESTAMPTZ DEFAULT NOW() + ) + `); + + // Get VAPID public key + app.get("/notifications/vapid-key", async () => { + return { data: { publicKey: vapidKeys.publicKey } }; + }); + + // Subscribe + app.post("/notifications/subscribe", async (req) => { + const { subscription, user_id } = req.body; + await app.db.query( + "INSERT INTO push_subscriptions (user_id, subscription) VALUES ($1, $2)", + [user_id, JSON.stringify(subscription)] + ); + return { status: "subscribed" }; + }); + + // Send notification (internal use) + app.post("/notifications/send", async (req) => { + const { user_id, title, body, url } = req.body; + const { rows } = await app.db.query( + "SELECT subscription FROM push_subscriptions WHERE user_id = $1", [user_id] + ); + + let sent = 0; + for (const row of rows) { + try { + await webpush.sendNotification( + JSON.parse(row.subscription), + JSON.stringify({ title, body, url: url || "/tasks" }) + ); + sent++; + } catch (e) { + if (e.statusCode === 410) { + await app.db.query("DELETE FROM push_subscriptions WHERE subscription = $1", [row.subscription]); + } + } + } + return { status: "sent", count: sent }; + }); + + // Unsubscribe + app.delete("/notifications/unsubscribe", async (req) => { + const { user_id } = req.body; + await app.db.query("DELETE FROM push_subscriptions WHERE user_id = $1", [user_id]); + return { status: "unsubscribed" }; + }); +} + +module.exports = notificationRoutes; diff --git a/api/src/routes/tasks.js b/api/src/routes/tasks.js index a9261df..2955e70 100644 --- a/api/src/routes/tasks.js +++ b/api/src/routes/tasks.js @@ -153,6 +153,62 @@ async function taskRoutes(app) { ); return { data: rows[0] }; }); + + // === Team Collaboration Endpoints === + + // Assign task to user + app.post("/tasks/:id/assign", async (req) => { + const { user_id } = req.body; + if (!user_id) throw { statusCode: 400, message: "user_id is required" }; + const { rows } = await app.db.query( + "UPDATE tasks SET assigned_to = array_append(COALESCE(assigned_to, ARRAY[]::uuid[]), $1::uuid), updated_at = NOW() WHERE id = $2 RETURNING *", + [user_id, req.params.id] + ); + if (!rows.length) throw { statusCode: 404, message: "Task not found" }; + await invalidateTaskCaches(); + return { data: rows[0] }; + }); + + // Transfer task (creates pending transfer) + app.post("/tasks/:id/transfer", async (req) => { + const { to_user_id, message } = req.body; + if (!to_user_id) throw { statusCode: 400, message: "to_user_id is required" }; + await app.db.query( + "INSERT INTO task_comments (task_id, content, is_ai) VALUES ($1, $2, true)", + [req.params.id, JSON.stringify({ type: "transfer_request", to: to_user_id, message: message || "", status: "pending" })] + ); + return { status: "transfer_requested" }; + }); + + // Accept/reject transfer + app.post("/tasks/:id/transfer/respond", async (req) => { + const { accept, user_id } = req.body; + if (accept) { + if (!user_id) throw { statusCode: 400, message: "user_id is required" }; + const { rows } = await app.db.query( + "UPDATE tasks SET user_id = $1, updated_at = NOW() WHERE id = $2 RETURNING *", + [user_id, req.params.id] + ); + if (!rows.length) throw { statusCode: 404, message: "Task not found" }; + await invalidateTaskCaches(); + return { data: rows[0], status: "transferred" }; + } + return { status: "rejected" }; + }); + + // Collaborate on task + app.post("/tasks/:id/collaborate", async (req) => { + const { user_id } = req.body; + if (!user_id) throw { statusCode: 400, message: "user_id is required" }; + const { rows } = await app.db.query( + "UPDATE tasks SET assigned_to = array_append(COALESCE(assigned_to, ARRAY[]::uuid[]), $1::uuid), updated_at = NOW() WHERE id = $2 RETURNING *", + [user_id, req.params.id] + ); + if (!rows.length) throw { statusCode: 404, message: "Task not found" }; + await invalidateTaskCaches(); + return { data: rows[0] }; + }); + } module.exports = taskRoutes; diff --git a/apps/tasks/app/goals/page.tsx b/apps/tasks/app/goals/page.tsx new file mode 100644 index 0000000..aa6ab60 --- /dev/null +++ b/apps/tasks/app/goals/page.tsx @@ -0,0 +1,482 @@ +"use client"; + +import { useEffect, useState, useCallback } from "react"; +import { useRouter } from "next/navigation"; +import { useAuth } from "@/lib/auth"; +import { + getGoals, + getGoal, + createGoal, + updateGoal, + deleteGoal, + generateGoalPlan, + getGoalReport, + getGroups, + Goal, + GoalPlanResult, + GoalReport, + Group, +} from "@/lib/api"; + +export default function GoalsPage() { + const { token } = useAuth(); + const router = useRouter(); + const [goals, setGoals] = useState([]); + const [groups, setGroups] = useState([]); + const [loading, setLoading] = useState(true); + const [showForm, setShowForm] = useState(false); + const [selectedGoal, setSelectedGoal] = useState<(Goal & { tasks?: unknown[] }) | null>(null); + const [planResult, setPlanResult] = useState(null); + const [report, setReport] = useState(null); + const [aiLoading, setAiLoading] = useState(null); + const [error, setError] = useState(null); + + // Form state + const [formTitle, setFormTitle] = useState(""); + const [formDate, setFormDate] = useState(""); + const [formGroup, setFormGroup] = useState(""); + + const loadData = useCallback(async () => { + if (!token) return; + setLoading(true); + try { + const [goalsRes, groupsRes] = await Promise.all([ + getGoals(token), + getGroups(token), + ]); + setGoals(goalsRes.data || []); + setGroups(groupsRes.data || []); + } catch (err) { + console.error("Chyba pri nacitani:", err); + setError("Nepodarilo se nacist data"); + } finally { + setLoading(false); + } + }, [token]); + + useEffect(() => { + if (!token) { + router.replace("/login"); + return; + } + loadData(); + }, [token, router, loadData]); + + async function handleCreate(e: React.FormEvent) { + e.preventDefault(); + if (!token || !formTitle.trim()) return; + try { + await createGoal(token, { + title: formTitle.trim(), + target_date: formDate || null, + group_id: formGroup || null, + }); + setFormTitle(""); + setFormDate(""); + setFormGroup(""); + setShowForm(false); + loadData(); + } catch (err) { + console.error("Chyba pri vytvareni:", err); + setError("Nepodarilo se vytvorit cil"); + } + } + + async function handleSelectGoal(goal: Goal) { + if (!token) return; + try { + const res = await getGoal(token, goal.id); + setSelectedGoal(res.data); + setPlanResult(null); + setReport(null); + } catch (err) { + console.error("Chyba pri nacitani cile:", err); + } + } + + async function handleGeneratePlan(goalId: string) { + if (!token) return; + setAiLoading("plan"); + setError(null); + try { + const res = await generateGoalPlan(token, goalId); + setPlanResult(res.data); + // Reload goal to get updated plan + const updated = await getGoal(token, goalId); + setSelectedGoal(updated.data); + loadData(); + } catch (err) { + console.error("Chyba pri generovani planu:", err); + setError("Nepodarilo se vygenerovat plan. Zkuste to znovu."); + } finally { + setAiLoading(null); + } + } + + async function handleGetReport(goalId: string) { + if (!token) return; + setAiLoading("report"); + setError(null); + try { + const res = await getGoalReport(token, goalId); + setReport(res.data); + } catch (err) { + console.error("Chyba pri ziskavani reportu:", err); + setError("Nepodarilo se ziskat report. Zkuste to znovu."); + } finally { + setAiLoading(null); + } + } + + async function handleUpdateProgress(goalId: string, pct: number) { + if (!token) return; + try { + await updateGoal(token, goalId, { progress_pct: pct } as Partial); + loadData(); + if (selectedGoal && selectedGoal.id === goalId) { + setSelectedGoal({ ...selectedGoal, progress_pct: pct }); + } + } catch (err) { + console.error("Chyba pri aktualizaci:", err); + } + } + + async function handleDelete(goalId: string) { + if (!token) return; + if (!confirm("Opravdu chcete smazat tento cil?")) return; + try { + await deleteGoal(token, goalId); + setSelectedGoal(null); + loadData(); + } catch (err) { + console.error("Chyba pri mazani:", err); + } + } + + function formatDate(d: string | null) { + if (!d) return "Bez terminu"; + return new Date(d).toLocaleDateString("cs-CZ", { day: "numeric", month: "short", year: "numeric" }); + } + + function progressColor(pct: number) { + if (pct >= 80) return "bg-green-500"; + if (pct >= 50) return "bg-blue-500"; + if (pct >= 20) return "bg-yellow-500"; + return "bg-gray-400"; + } + + if (!token) return null; + + return ( +
+ {/* Header */} +
+

Cile

+ +
+ + {/* Error */} + {error && ( +
+ {error} + +
+ )} + + {/* Create form */} + {showForm && ( +
+
+ + setFormTitle(e.target.value)} + placeholder="Napr. Naucit se TypeScript" + className="w-full px-3 py-2 border rounded-lg dark:bg-gray-800 dark:border-gray-700 dark:text-white focus:ring-2 focus:ring-blue-500 focus:border-blue-500 min-h-[44px]" + required + /> +
+
+
+ + setFormDate(e.target.value)} + className="w-full px-3 py-2 border rounded-lg dark:bg-gray-800 dark:border-gray-700 dark:text-white focus:ring-2 focus:ring-blue-500 min-h-[44px]" + /> +
+
+ + +
+
+ +
+ )} + + {/* Goals list */} + {loading ? ( +
+
+
+ ) : goals.length === 0 ? ( +
+
🎯
+

Zadne cile

+

Vytvorte svuj prvni cil

+
+ ) : ( +
+ {goals.map((goal) => ( + + ))} +
+ )} + + {/* Goal detail panel */} + {selectedGoal && ( +
+
+
+

{selectedGoal.title}

+

+ {formatDate(selectedGoal.target_date)} | Progres: {selectedGoal.progress_pct}% +

+
+ +
+ + {/* Progress slider */} +
+ + handleUpdateProgress(selectedGoal.id, parseInt(e.target.value))} + className="w-full h-2 bg-gray-200 dark:bg-gray-700 rounded-lg appearance-none cursor-pointer accent-blue-600" + /> +
+ + {/* AI Action buttons */} +
+ + +
+ + {/* Plan result */} + {planResult && ( +
+

+ Vygenerovany plan ({planResult.tasks_created} ukolu vytvoreno) +

+ {planResult.plan.weeks ? ( +
+ {(planResult.plan.weeks as Array<{ week_number: number; focus: string; tasks: Array<{ title: string; duration_hours?: number; day_of_week?: string }> }>).map((week, i) => ( +
+

+ Tyden {week.week_number}: {week.focus} +

+
    + {(week.tasks || []).map((t, j) => ( +
  • + + {t.title} + {t.duration_hours && ({t.duration_hours}h)} + {t.day_of_week && [{t.day_of_week}]} +
  • + ))} +
+
+ ))} +
+ ) : ( +
+                  {JSON.stringify(planResult.plan, null, 2)}
+                
+ )} +
+ )} + + {/* AI Report */} + {report && ( +
+
+

AI Report

+ + {report.stats.done}/{report.stats.total} splneno ({report.stats.pct}%) + +
+

{report.report}

+
+ )} + + {/* Existing plan */} + {selectedGoal.plan && Object.keys(selectedGoal.plan).length > 0 && !planResult && ( +
+

Ulozeny plan

+ {(selectedGoal.plan as Record).weeks ? ( +
+ {((selectedGoal.plan as Record).weeks as Array<{ week_number: number; focus: string; tasks: Array<{ title: string; duration_hours?: number; day_of_week?: string }> }>).map((week, i) => ( +
+

+ Tyden {week.week_number}: {week.focus} +

+
    + {(week.tasks || []).map((t, j) => ( +
  • + + {t.title} +
  • + ))} +
+
+ ))} +
+ ) : ( +
+                  {JSON.stringify(selectedGoal.plan, null, 2)}
+                
+ )} +
+ )} + + {/* Related tasks */} + {selectedGoal.tasks && selectedGoal.tasks.length > 0 && ( +
+

+ Souvisejici ukoly ({selectedGoal.tasks.length}) +

+
+ {(selectedGoal.tasks as Array<{ id: string; title: string; status: string }>).map((task) => ( +
+ + {task.title} + {task.status} +
+ ))} +
+
+ )} + + {/* Delete button */} + +
+ )} +
+ ); +} diff --git a/apps/tasks/app/layout.tsx b/apps/tasks/app/layout.tsx index 82d51bd..1bb1d97 100644 --- a/apps/tasks/app/layout.tsx +++ b/apps/tasks/app/layout.tsx @@ -4,6 +4,7 @@ import ThemeProvider from "@/components/ThemeProvider"; import AuthProvider from "@/components/AuthProvider"; import Header from "@/components/Header"; import BottomNav from "@/components/BottomNav"; +import { I18nProvider } from "@/lib/i18n"; export const metadata: Metadata = { title: "Task Team", @@ -35,15 +36,17 @@ export default function RootLayout({ return ( - - -
-
- {children} -
- - - + + + +
+
+ {children} +
+ + + + ); diff --git a/apps/tasks/app/login/page.tsx b/apps/tasks/app/login/page.tsx index 7ed620b..d27584f 100644 --- a/apps/tasks/app/login/page.tsx +++ b/apps/tasks/app/login/page.tsx @@ -5,18 +5,20 @@ import { useRouter } from "next/navigation"; import Link from "next/link"; import { login } from "@/lib/api"; import { useAuth } from "@/lib/auth"; +import { useTranslation } from "@/lib/i18n"; export default function LoginPage() { const [email, setEmail] = useState(""); const [loading, setLoading] = useState(false); const [error, setError] = useState(""); const { setAuth } = useAuth(); + const { t } = useTranslation(); const router = useRouter(); async function handleSubmit(e: React.FormEvent) { e.preventDefault(); if (!email.trim()) { - setError("Zadejte email"); + setError(t("auth.email")); return; } setLoading(true); @@ -26,7 +28,7 @@ export default function LoginPage() { setAuth(result.data.token, result.data.user); router.push("/tasks"); } catch (err) { - setError(err instanceof Error ? err.message : "Chyba prihlaseni"); + setError(err instanceof Error ? err.message : t("common.error")); } finally { setLoading(false); } @@ -36,7 +38,7 @@ export default function LoginPage() {
-

Prihlaseni

+

{t("auth.login")}

{error && (
@@ -46,7 +48,7 @@ export default function LoginPage() {
- + - {loading ? "Prihlasuji..." : "Prihlasit se"} + {loading ? t("common.loading") : t("auth.submit")}

- Nemate ucet?{" "} + {t("auth.noAccount")}{" "} - Registrovat se + {t("auth.registerBtn")}

diff --git a/apps/tasks/app/register/page.tsx b/apps/tasks/app/register/page.tsx index e32bfe7..088d92c 100644 --- a/apps/tasks/app/register/page.tsx +++ b/apps/tasks/app/register/page.tsx @@ -5,6 +5,7 @@ import { useRouter } from "next/navigation"; import Link from "next/link"; import { register } from "@/lib/api"; import { useAuth } from "@/lib/auth"; +import { useTranslation } from "@/lib/i18n"; export default function RegisterPage() { const [email, setEmail] = useState(""); @@ -13,12 +14,13 @@ export default function RegisterPage() { const [loading, setLoading] = useState(false); const [error, setError] = useState(""); const { setAuth } = useAuth(); + const { t } = useTranslation(); const router = useRouter(); async function handleSubmit(e: React.FormEvent) { e.preventDefault(); if (!email.trim() || !name.trim()) { - setError("Email a jmeno jsou povinne"); + setError(t("common.error")); return; } setLoading(true); @@ -32,7 +34,7 @@ export default function RegisterPage() { setAuth(result.data.token, result.data.user); router.push("/tasks"); } catch (err) { - setError(err instanceof Error ? err.message : "Chyba registrace"); + setError(err instanceof Error ? err.message : t("common.error")); } finally { setLoading(false); } @@ -42,7 +44,7 @@ export default function RegisterPage() {
-

Registrace

+

{t("auth.register")}

{error && (
@@ -52,31 +54,29 @@ export default function RegisterPage() {
- + setName(e.target.value)} className="w-full px-3 py-2.5 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 focus:ring-2 focus:ring-blue-500 focus:border-transparent outline-none" - placeholder="Vase jmeno" autoFocus />
- + setEmail(e.target.value)} className="w-full px-3 py-2.5 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 focus:ring-2 focus:ring-blue-500 focus:border-transparent outline-none" - placeholder="vas@email.cz" autoComplete="email" />
- + - {loading ? "Registruji..." : "Registrovat se"} + {loading ? t("common.loading") : t("auth.registerBtn")}

- Jiz mate ucet?{" "} + {t("auth.hasAccount")}{" "} - Prihlasit se + {t("auth.submit")}

diff --git a/apps/tasks/app/settings/page.tsx b/apps/tasks/app/settings/page.tsx index 61902c6..ec94cb6 100644 --- a/apps/tasks/app/settings/page.tsx +++ b/apps/tasks/app/settings/page.tsx @@ -4,19 +4,14 @@ import { useState, useEffect } from "react"; import { useRouter } from "next/navigation"; import { useAuth } from "@/lib/auth"; import { useTheme } from "@/components/ThemeProvider"; - -const LANGUAGES = [ - { code: "cs", label: "Čeština", flag: "🇨🇿" }, - { code: "he", label: "עברית", flag: "🇮🇱" }, - { code: "ru", label: "Русский", flag: "🇷🇺" }, - { code: "ua", label: "Українська", flag: "🇺🇦" }, -]; +import { useTranslation, LOCALES } from "@/lib/i18n"; +import type { Locale } from "@/lib/i18n"; export default function SettingsPage() { const { token, user, logout } = useAuth(); const { theme, toggleTheme } = useTheme(); + const { t, locale, setLocale } = useTranslation(); const router = useRouter(); - const [language, setLanguage] = useState("cs"); const [notifications, setNotifications] = useState({ push: true, email: false, @@ -31,8 +26,6 @@ export default function SettingsPage() { } // Load saved preferences if (typeof window !== "undefined") { - const savedLang = localStorage.getItem("taskteam_language"); - if (savedLang) setLanguage(savedLang); const savedNotifs = localStorage.getItem("taskteam_notifications"); if (savedNotifs) { try { @@ -46,7 +39,6 @@ export default function SettingsPage() { function handleSave() { if (typeof window !== "undefined") { - localStorage.setItem("taskteam_language", language); localStorage.setItem("taskteam_notifications", JSON.stringify(notifications)); } setSaved(true); @@ -62,17 +54,17 @@ export default function SettingsPage() { return (
-

Nastavení

+

{t("settings.title")}

{/* Profile section */}
-

Profil

+

{t("settings.profile")}

{(user?.name || user?.email || "?").charAt(0).toUpperCase()}
-

{user?.name || "Uživatel"}

+

{user?.name || t("settings.user")}

{user?.email}

@@ -80,7 +72,7 @@ export default function SettingsPage() { {/* Appearance */}
-

Vzhled

+

{t("settings.appearance")}

{/* Theme toggle */}
@@ -95,7 +87,7 @@ export default function SettingsPage() { )} - {theme === "dark" ? "Tmavý režim" : "Světlý režim"} + {theme === "dark" ? t("settings.dark") : t("settings.light")}
{/* Logout */} @@ -187,7 +179,7 @@ export default function SettingsPage() { onClick={handleLogout} className="w-full py-3 rounded-xl font-medium border border-red-300 dark:border-red-800 text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/20 transition-colors" > - Odhlásit se + {t("auth.logout")} {/* App info */} diff --git a/apps/tasks/components/BottomNav.tsx b/apps/tasks/components/BottomNav.tsx index dc8f6c2..d155c58 100644 --- a/apps/tasks/components/BottomNav.tsx +++ b/apps/tasks/components/BottomNav.tsx @@ -2,49 +2,60 @@ import Link from "next/link"; import { usePathname } from "next/navigation"; - -const NAV_ITEMS = [ - { - href: "/tasks", - label: "Ukoly", - icon: ( - - - - ), - }, - { - href: "/calendar", - label: "Kalendar", - icon: ( - - - - ), - }, - { - href: "/chat", - label: "Chat", - icon: ( - - - - ), - }, - { - href: "/settings", - label: "Nastaveni", - icon: ( - - - - - ), - }, -]; +import { useTranslation } from "@/lib/i18n"; export default function BottomNav() { const pathname = usePathname(); + const { t } = useTranslation(); + + const NAV_ITEMS = [ + { + href: "/tasks", + label: t("nav.tasks"), + icon: ( + + + + ), + }, + { + href: "/calendar", + label: t("nav.calendar"), + icon: ( + + + + ), + }, + { + href: "/goals", + label: t("nav.goals"), + icon: ( + + + + ), + }, + { + href: "/chat", + label: t("nav.chat"), + icon: ( + + + + ), + }, + { + href: "/settings", + label: t("nav.settings"), + icon: ( + + + + + ), + }, + ]; return (
) : ( @@ -90,7 +92,7 @@ export default function Header() { href="/login" className="text-sm px-4 py-2 rounded-lg bg-blue-600 text-white hover:bg-blue-700 transition-colors min-h-[44px] flex items-center" > - Přihlásit + {t("auth.submit")} )}
@@ -101,7 +103,7 @@ export default function Header() { @@ -109,7 +111,7 @@ export default function Header() {
-

{user.name || "Uživatel"}

+

{user.name || t("settings.user")}

{user.email}

@@ -169,7 +171,7 @@ export default function Header() { - Úkoly + {t("nav.tasks")} - Kalendář + {t("nav.calendar")} - Chat + {t("nav.chat")} - Nastavení + {t("nav.settings")} {/* Theme toggle */} @@ -221,7 +223,7 @@ export default function Header() { )} - {theme === "dark" ? "Světlý režim" : "Tmavý režim"} + {theme === "dark" ? t("settings.light") : t("settings.dark")}
@@ -236,7 +238,7 @@ export default function Header() { - Odhlásit se + {t("auth.logout")}
)} diff --git a/apps/tasks/lib/api.ts b/apps/tasks/lib/api.ts index 80e78e4..84496d8 100644 --- a/apps/tasks/lib/api.ts +++ b/apps/tasks/lib/api.ts @@ -149,3 +149,58 @@ export interface Connector { type: string; config: Record; } + +// Goals +export interface Goal { + id: string; + user_id: string | null; + title: string; + target_date: string | null; + progress_pct: number; + group_id: string | null; + plan: Record | null; + created_at: string; + updated_at: string; + group_name: string | null; + group_color: string | null; + group_icon: string | null; + tasks?: Task[]; +} + +export interface GoalPlanResult { + plan: Record; + tasks_created: number; +} + +export interface GoalReport { + report: string; + stats: { done: number; total: number; pct: number }; +} + +export function getGoals(token: string) { + return apiFetch<{ data: Goal[] }>("/api/v1/goals", { token }); +} + +export function getGoal(token: string, id: string) { + return apiFetch<{ data: Goal }>(`/api/v1/goals/${id}`, { token }); +} + +export function createGoal(token: string, data: Partial) { + return apiFetch<{ data: Goal }>("/api/v1/goals", { method: "POST", body: data, token }); +} + +export function updateGoal(token: string, id: string, data: Partial) { + return apiFetch<{ data: Goal }>(`/api/v1/goals/${id}`, { method: "PUT", body: data, token }); +} + +export function deleteGoal(token: string, id: string) { + return apiFetch(`/api/v1/goals/${id}`, { method: "DELETE", token }); +} + +export function generateGoalPlan(token: string, id: string) { + return apiFetch<{ data: GoalPlanResult }>(`/api/v1/goals/${id}/plan`, { method: "POST", token }); +} + +export function getGoalReport(token: string, id: string) { + return apiFetch<{ data: GoalReport }>(`/api/v1/goals/${id}/report`, { token }); +} diff --git a/apps/tasks/lib/i18n.tsx b/apps/tasks/lib/i18n.tsx new file mode 100644 index 0000000..ab9f553 --- /dev/null +++ b/apps/tasks/lib/i18n.tsx @@ -0,0 +1,99 @@ +"use client"; + +import { createContext, useContext, useState, useEffect, useCallback, ReactNode } from "react"; + +import cs from "@/messages/cs.json"; +import he from "@/messages/he.json"; +import ru from "@/messages/ru.json"; +import ua from "@/messages/ua.json"; + +export type Locale = "cs" | "he" | "ru" | "ua"; + +export const LOCALES: { code: Locale; label: string; flag: string; dir: "ltr" | "rtl" }[] = [ + { code: "cs", label: "\u010ce\u0161tina", flag: "\ud83c\udde8\ud83c\uddff", dir: "ltr" }, + { code: "he", label: "\u05e2\u05d1\u05e8\u05d9\u05ea", flag: "\ud83c\uddee\ud83c\uddf1", dir: "rtl" }, + { code: "ru", label: "\u0420\u0443\u0441\u0441\u043a\u0438\u0439", flag: "\ud83c\uddf7\ud83c\uddfa", dir: "ltr" }, + { code: "ua", label: "\u0423\u043a\u0440\u0430\u0457\u043d\u0441\u044c\u043a\u0430", flag: "\ud83c\uddfa\ud83c\udde6", dir: "ltr" }, +]; + +type Messages = typeof cs; + +const MESSAGES: Record = { cs, he, ru, ua }; + +const STORAGE_KEY = "taskteam_language"; + +function getNestedValue(obj: unknown, path: string): string { + const keys = path.split("."); + let current: unknown = obj; + for (const key of keys) { + if (current == null || typeof current !== "object") return path; + current = (current as Record)[key]; + } + return typeof current === "string" ? current : path; +} + +interface I18nContextType { + locale: Locale; + setLocale: (locale: Locale) => void; + t: (key: string) => string; + dir: "ltr" | "rtl"; + isRTL: boolean; +} + +const I18nContext = createContext({ + locale: "cs", + setLocale: () => {}, + t: (key: string) => key, + dir: "ltr", + isRTL: false, +}); + +export function useTranslation() { + return useContext(I18nContext); +} + +export function I18nProvider({ children }: { children: ReactNode }) { + const [locale, setLocaleState] = useState("cs"); + const [mounted, setMounted] = useState(false); + + useEffect(() => { + setMounted(true); + if (typeof window !== "undefined") { + const stored = localStorage.getItem(STORAGE_KEY) as Locale | null; + if (stored && MESSAGES[stored]) { + setLocaleState(stored); + } + } + }, []); + + const setLocale = useCallback((newLocale: Locale) => { + setLocaleState(newLocale); + if (typeof window !== "undefined") { + localStorage.setItem(STORAGE_KEY, newLocale); + } + }, []); + + const t = useCallback( + (key: string): string => { + return getNestedValue(MESSAGES[locale], key); + }, + [locale] + ); + + const localeInfo = LOCALES.find((l) => l.code === locale) || LOCALES[0]; + const dir = localeInfo.dir; + const isRTL = dir === "rtl"; + + // Update html attributes when locale changes + useEffect(() => { + if (!mounted) return; + document.documentElement.lang = locale; + document.documentElement.dir = dir; + }, [locale, dir, mounted]); + + return ( + + {children} + + ); +} diff --git a/apps/tasks/messages/cs.json b/apps/tasks/messages/cs.json new file mode 100644 index 0000000..29ec36c --- /dev/null +++ b/apps/tasks/messages/cs.json @@ -0,0 +1,10 @@ +{ + "nav": { "tasks": "Úkoly", "calendar": "Kalendář", "chat": "Chat", "settings": "Nastavení", "goals": "Cíle" }, + "auth": { "login": "Přihlášení", "register": "Registrace", "email": "Email", "name": "Jméno", "phone": "Telefon", "submit": "Přihlásit se", "registerBtn": "Registrovat se", "noAccount": "Nemáte účet?", "hasAccount": "Máte účet?", "logout": "Odhlásit se" }, + "tasks": { "title": "Úkoly", "add": "Nový úkol", "edit": "Upravit", "delete": "Smazat", "noTasks": "Žádné úkoly", "all": "Vše", "status": { "pending": "Čeká", "in_progress": "Probíhá", "done": "Hotovo", "completed": "Hotovo", "cancelled": "Zrušeno" }, "priority": { "urgent": "Urgentní", "high": "Vysoká", "medium": "Střední", "low": "Nízká" }, "form": { "title": "Název", "description": "Popis", "group": "Skupina", "priority": "Priorita", "status": "Status", "dueDate": "Termín", "save": "Uložit", "cancel": "Zrušit", "titleRequired": "Název je povinný", "saveError": "Chyba při ukládání", "saving": "Ukládám...", "noGroup": "-- Bez skupiny --", "placeholder": "Co je třeba udělat...", "descPlaceholder": "Podrobnosti..." }, "noDue": "Bez termínu", "createFirst": "Vytvořte první úkol pomocí tlačítka +", "newTask": "Nový úkol", "close": "Zavřít", "markDone": "Označit jako hotové", "start": "Zahájit", "reopen": "Znovu otevřít", "confirmDelete": "Opravdu smazat tento úkol?", "editTask": "Upravit úkol", "saveChanges": "Uložit změny", "deleting": "Mažu...", "created": "Vytvořeno", "completed": "Dokončeno", "loadError": "Chyba při načítání úkolu", "notFound": "Úkol nenalezen", "backToTasks": "Zpět na úkoly" }, + "chat": { "title": "AI Asistent", "placeholder": "Napište zprávu...", "send": "Odeslat", "empty": "Zeptejte se na cokoliv...", "subtitle": "Zeptejte se na cokoliv ohledně vašich úkolů", "startConversation": "Začněte konverzaci", "helpText": "Napište zprávu a AI asistent vám pomůže s úkoly", "unavailable": "Chat asistent je momentálně nedostupný. Zkuste to prosím později.", "processError": "Omlouvám se, nemohl jsem zpracovat vaši zprávu." }, + "settings": { "title": "Nastavení", "language": "Jazyk", "theme": "Motiv", "dark": "Tmavý režim", "light": "Světlý režim", "notifications": "Oznámení", "push": "Push oznámení", "email": "E-mailová oznámení", "taskReminders": "Připomenutí úkolů", "dailySummary": "Denní souhrn", "save": "Uložit nastavení", "saved": "Uloženo!", "profile": "Profil", "appearance": "Vzhled", "user": "Uživatel" }, + "goals": { "title": "Cíle", "add": "Nový cíl", "progress": "Progres", "plan": "Generovat plán", "report": "AI Report" }, + "common": { "back": "Zpět", "loading": "Načítání...", "error": "Chyba", "confirm": "Potvrdit", "menu": "Menu", "closeMenu": "Zavřít menu", "toggleTheme": "Přepnout téma" }, + "calendar": { "title": "Kalendář" } +} diff --git a/apps/tasks/messages/he.json b/apps/tasks/messages/he.json new file mode 100644 index 0000000..21d824c --- /dev/null +++ b/apps/tasks/messages/he.json @@ -0,0 +1,10 @@ +{ + "nav": { "tasks": "משימות", "calendar": "לוח שנה", "chat": "צ׳אט", "settings": "הגדרות", "goals": "מטרות" }, + "auth": { "login": "התחברות", "register": "הרשמה", "email": "אימייל", "name": "שם", "phone": "טלפון", "submit": "התחבר", "registerBtn": "הירשם", "noAccount": "אין לך חשבון?", "hasAccount": "יש לך חשבון?", "logout": "התנתק" }, + "tasks": { "title": "משימות", "add": "משימה חדשה", "edit": "ערוך", "delete": "מחק", "noTasks": "אין משימות", "all": "הכל", "status": { "pending": "ממתין", "in_progress": "בתהליך", "done": "הושלם", "completed": "הושלם", "cancelled": "בוטל" }, "priority": { "urgent": "דחוף", "high": "גבוה", "medium": "בינוני", "low": "נמוך" }, "form": { "title": "כותרת", "description": "תיאור", "group": "קבוצה", "priority": "עדיפות", "status": "סטטוס", "dueDate": "תאריך יעד", "save": "שמור", "cancel": "ביטול", "titleRequired": "כותרת חובה", "saveError": "שגיאה בשמירה", "saving": "שומר...", "noGroup": "-- ללא קבוצה --", "placeholder": "מה צריך לעשות...", "descPlaceholder": "פרטים..." }, "noDue": "ללא תאריך", "createFirst": "צור משימה ראשונה בעזרת הכפתור +", "newTask": "משימה חדשה", "close": "סגור", "markDone": "סמן כהושלם", "start": "התחל", "reopen": "פתח מחדש", "confirmDelete": "למחוק משימה זו?", "editTask": "ערוך משימה", "saveChanges": "שמור שינויים", "deleting": "מוחק...", "created": "נוצר", "completed": "הושלם", "loadError": "שגיאה בטעינת המשימה", "notFound": "משימה לא נמצאה", "backToTasks": "חזרה למשימות" }, + "chat": { "title": "עוזר AI", "placeholder": "כתוב הודעה...", "send": "שלח", "empty": "שאל כל דבר...", "subtitle": "שאל כל שאלה לגבי המשימות שלך", "startConversation": "התחל שיחה", "helpText": "כתוב הודעה ועוזר ה-AI יעזור לך עם משימות", "unavailable": "עוזר הצ׳אט אינו זמין כרגע. נסה שוב מאוחר יותר.", "processError": "מצטער, לא הצלחתי לעבד את ההודעה שלך." }, + "settings": { "title": "הגדרות", "language": "שפה", "theme": "ערכת נושא", "dark": "מצב כהה", "light": "מצב בהיר", "notifications": "התראות", "push": "התראות פוש", "email": "התראות אימייל", "taskReminders": "תזכורות משימות", "dailySummary": "סיכום יומי", "save": "שמור הגדרות", "saved": "נשמר!", "profile": "פרופיל", "appearance": "מראה", "user": "משתמש" }, + "goals": { "title": "מטרות", "add": "מטרה חדשה", "progress": "התקדמות", "plan": "צור תוכנית", "report": "דוח AI" }, + "common": { "back": "חזרה", "loading": "טוען...", "error": "שגיאה", "confirm": "אישור", "menu": "תפריט", "closeMenu": "סגור תפריט", "toggleTheme": "החלף ערכת נושא" }, + "calendar": { "title": "לוח שנה" } +} diff --git a/apps/tasks/messages/ru.json b/apps/tasks/messages/ru.json new file mode 100644 index 0000000..bb41813 --- /dev/null +++ b/apps/tasks/messages/ru.json @@ -0,0 +1,10 @@ +{ + "nav": { "tasks": "Задачи", "calendar": "Календарь", "chat": "Чат", "settings": "Настройки", "goals": "Цели" }, + "auth": { "login": "Вход", "register": "Регистрация", "email": "Email", "name": "Имя", "phone": "Телефон", "submit": "Войти", "registerBtn": "Зарегистрироваться", "noAccount": "Нет аккаунта?", "hasAccount": "Есть аккаунт?", "logout": "Выйти" }, + "tasks": { "title": "Задачи", "add": "Новая задача", "edit": "Редактировать", "delete": "Удалить", "noTasks": "Нет задач", "all": "Все", "status": { "pending": "Ожидает", "in_progress": "В работе", "done": "Готово", "completed": "Готово", "cancelled": "Отменено" }, "priority": { "urgent": "Срочно", "high": "Высокий", "medium": "Средний", "low": "Низкий" }, "form": { "title": "Название", "description": "Описание", "group": "Группа", "priority": "Приоритет", "status": "Статус", "dueDate": "Срок", "save": "Сохранить", "cancel": "Отмена", "titleRequired": "Название обязательно", "saveError": "Ошибка при сохранении", "saving": "Сохраняю...", "noGroup": "-- Без группы --", "placeholder": "Что нужно сделать...", "descPlaceholder": "Подробности..." }, "noDue": "Без срока", "createFirst": "Создайте первую задачу кнопкой +", "newTask": "Новая задача", "close": "Закрыть", "markDone": "Отметить готовой", "start": "Начать", "reopen": "Открыть заново", "confirmDelete": "Удалить эту задачу?", "editTask": "Редактировать задачу", "saveChanges": "Сохранить изменения", "deleting": "Удаляю...", "created": "Создано", "completed": "Завершено", "loadError": "Ошибка при загрузке задачи", "notFound": "Задача не найдена", "backToTasks": "Назад к задачам" }, + "chat": { "title": "AI Ассистент", "placeholder": "Напишите сообщение...", "send": "Отправить", "empty": "Спросите что угодно...", "subtitle": "Задайте любой вопрос о ваших задачах", "startConversation": "Начните разговор", "helpText": "Напишите сообщение, и AI ассистент поможет вам с задачами", "unavailable": "Чат ассистент сейчас недоступен. Попробуйте позже.", "processError": "Извините, не удалось обработать ваше сообщение." }, + "settings": { "title": "Настройки", "language": "Язык", "theme": "Тема", "dark": "Тёмный режим", "light": "Светлый режим", "notifications": "Уведомления", "push": "Push уведомления", "email": "E-mail уведомления", "taskReminders": "Напоминания о задачах", "dailySummary": "Ежедневная сводка", "save": "Сохранить настройки", "saved": "Сохранено!", "profile": "Профиль", "appearance": "Внешний вид", "user": "Пользователь" }, + "goals": { "title": "Цели", "add": "Новая цель", "progress": "Прогресс", "plan": "Создать план", "report": "AI Отчёт" }, + "common": { "back": "Назад", "loading": "Загрузка...", "error": "Ошибка", "confirm": "Подтвердить", "menu": "Меню", "closeMenu": "Закрыть меню", "toggleTheme": "Переключить тему" }, + "calendar": { "title": "Календарь" } +} diff --git a/apps/tasks/messages/ua.json b/apps/tasks/messages/ua.json new file mode 100644 index 0000000..c7913bf --- /dev/null +++ b/apps/tasks/messages/ua.json @@ -0,0 +1,10 @@ +{ + "nav": { "tasks": "Завдання", "calendar": "Календар", "chat": "Чат", "settings": "Налаштування", "goals": "Цілі" }, + "auth": { "login": "Вхід", "register": "Реєстрація", "email": "Email", "name": "Ім'я", "phone": "Телефон", "submit": "Увійти", "registerBtn": "Зареєструватися", "noAccount": "Немає акаунту?", "hasAccount": "Є акаунт?", "logout": "Вийти" }, + "tasks": { "title": "Завдання", "add": "Нове завдання", "edit": "Редагувати", "delete": "Видалити", "noTasks": "Немає завдань", "all": "Усі", "status": { "pending": "Очікує", "in_progress": "В роботі", "done": "Готово", "completed": "Готово", "cancelled": "Скасовано" }, "priority": { "urgent": "Терміново", "high": "Високий", "medium": "Середній", "low": "Низький" }, "form": { "title": "Назва", "description": "Опис", "group": "Група", "priority": "Пріоритет", "status": "Статус", "dueDate": "Термін", "save": "Зберегти", "cancel": "Скасувати", "titleRequired": "Назва обов'язкова", "saveError": "Помилка при збереженні", "saving": "Зберігаю...", "noGroup": "-- Без групи --", "placeholder": "Що треба зробити...", "descPlaceholder": "Подробиці..." }, "noDue": "Без терміну", "createFirst": "Створіть перше завдання кнопкою +", "newTask": "Нове завдання", "close": "Закрити", "markDone": "Позначити готовим", "start": "Розпочати", "reopen": "Відкрити знову", "confirmDelete": "Видалити це завдання?", "editTask": "Редагувати завдання", "saveChanges": "Зберегти зміни", "deleting": "Видаляю...", "created": "Створено", "completed": "Завершено", "loadError": "Помилка при завантаженні завдання", "notFound": "Завдання не знайдено", "backToTasks": "Назад до завдань" }, + "chat": { "title": "AI Асистент", "placeholder": "Напишіть повідомлення...", "send": "Надіслати", "empty": "Запитайте будь-що...", "subtitle": "Задайте будь-яке питання щодо ваших завдань", "startConversation": "Почніть розмову", "helpText": "Напишіть повідомлення, і AI асистент допоможе вам із завданнями", "unavailable": "Чат асистент зараз недоступний. Спробуйте пізніше.", "processError": "Вибачте, не вдалося обробити ваше повідомлення." }, + "settings": { "title": "Налаштування", "language": "Мова", "theme": "Тема", "dark": "Темний режим", "light": "Світлий режим", "notifications": "Сповіщення", "push": "Push сповіщення", "email": "E-mail сповіщення", "taskReminders": "Нагадування про завдання", "dailySummary": "Щоденний підсумок", "save": "Зберегти налаштування", "saved": "Збережено!", "profile": "Профіль", "appearance": "Зовнішній вигляд", "user": "Користувач" }, + "goals": { "title": "Цілі", "add": "Нова ціль", "progress": "Прогрес", "plan": "Створити план", "report": "AI Звіт" }, + "common": { "back": "Назад", "loading": "Завантаження...", "error": "Помилка", "confirm": "Підтвердити", "menu": "Меню", "closeMenu": "Закрити меню", "toggleTheme": "Перемкнути тему" }, + "calendar": { "title": "Календар" } +}