Feature batch: Projects, Recurrence, Group settings, Bug fixes
- Projects CRUD API + invite members - Task recurrence (daily/weekly/monthly) with auto-creation - Group time zones + GPS locations settings - i18n fallback fix (no more undefined labels) - UX: action buttons in one row - Chat/Calendar: relative API URLs - DB: task_assignments, projects tables, recurrence column Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -69,6 +69,7 @@ const start = async () => {
|
||||
await app.register(require("./routes/chat"), { prefix: "/api/v1" });
|
||||
await app.register(require("./routes/notifications"), { prefix: "/api/v1" });
|
||||
await app.register(require("./routes/goals"), { prefix: "/api/v1" });
|
||||
await app.register(require("./routes/projects"), { prefix: "/api/v1" });
|
||||
await app.register(require("./routes/deploy"), { prefix: "/api/v1" });
|
||||
await app.register(require("./routes/system"), { prefix: "/api/v1" });
|
||||
|
||||
|
||||
@@ -57,6 +57,50 @@ async function groupRoutes(app) {
|
||||
}
|
||||
return { status: 'ok' };
|
||||
});
|
||||
|
||||
// Update time zones for a group
|
||||
app.put('/groups/:id/timezones', async (req) => {
|
||||
const { time_zones } = req.body;
|
||||
if (!Array.isArray(time_zones)) {
|
||||
throw { statusCode: 400, message: 'time_zones must be an array of [{days, from, to}]' };
|
||||
}
|
||||
// Validate each timezone entry
|
||||
for (const tz of time_zones) {
|
||||
if (!Array.isArray(tz.days) || !tz.from || !tz.to) {
|
||||
throw { statusCode: 400, message: 'Each timezone must have days (array), from (HH:MM), to (HH:MM)' };
|
||||
}
|
||||
}
|
||||
const { rows } = await app.db.query(
|
||||
'UPDATE task_groups SET time_zones = $1, updated_at = NOW() WHERE id = $2 RETURNING *',
|
||||
[JSON.stringify(time_zones), req.params.id]
|
||||
);
|
||||
if (!rows.length) throw { statusCode: 404, message: 'Group not found' };
|
||||
return { data: rows[0] };
|
||||
});
|
||||
|
||||
// Update GPS locations for a group
|
||||
app.put('/groups/:id/locations', async (req) => {
|
||||
const { locations } = req.body;
|
||||
if (!Array.isArray(locations)) {
|
||||
throw { statusCode: 400, message: 'locations must be an array of [{name, lat, lng, radius_m}]' };
|
||||
}
|
||||
// Validate each location entry
|
||||
for (const loc of locations) {
|
||||
if (!loc.name || loc.lat === undefined || loc.lng === undefined) {
|
||||
throw { statusCode: 400, message: 'Each location must have name, lat, lng' };
|
||||
}
|
||||
if (typeof loc.lat !== 'number' || typeof loc.lng !== 'number') {
|
||||
throw { statusCode: 400, message: 'lat and lng must be numbers' };
|
||||
}
|
||||
}
|
||||
const { rows } = await app.db.query(
|
||||
'UPDATE task_groups SET locations = $1, updated_at = NOW() WHERE id = $2 RETURNING *',
|
||||
[JSON.stringify(locations), req.params.id]
|
||||
);
|
||||
if (!rows.length) throw { statusCode: 404, message: 'Group not found' };
|
||||
return { data: rows[0] };
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
module.exports = groupRoutes;
|
||||
|
||||
114
api/src/routes/projects.js
Normal file
114
api/src/routes/projects.js
Normal file
@@ -0,0 +1,114 @@
|
||||
// Task Team — Projects CRUD — 2026-03-29
|
||||
async function projectRoutes(app) {
|
||||
|
||||
// List projects (optionally filter by member)
|
||||
app.get("/projects", async (req) => {
|
||||
const { user_id } = req.query;
|
||||
let query = "SELECT * FROM projects";
|
||||
const params = [];
|
||||
if (user_id) {
|
||||
params.push(user_id);
|
||||
query += ` WHERE owner_id = $1 OR $1 = ANY(members)`;
|
||||
}
|
||||
query += " ORDER BY updated_at DESC";
|
||||
const { rows } = await app.db.query(query, params);
|
||||
return { data: rows };
|
||||
});
|
||||
|
||||
// Get single project with task count
|
||||
app.get("/projects/:id", async (req) => {
|
||||
const { rows } = await app.db.query(
|
||||
`SELECT p.*,
|
||||
(SELECT COUNT(*) FROM tasks t WHERE t.project_id = p.id) as task_count,
|
||||
(SELECT COUNT(*) FROM tasks t WHERE t.project_id = p.id AND t.status IN ('done','completed')) as done_count
|
||||
FROM projects p WHERE p.id = $1`,
|
||||
[req.params.id]
|
||||
);
|
||||
if (!rows.length) throw { statusCode: 404, message: "Project not found" };
|
||||
return { data: rows[0] };
|
||||
});
|
||||
|
||||
// Create project
|
||||
app.post("/projects", async (req) => {
|
||||
const { name, description, color, icon, owner_id } = req.body;
|
||||
if (!name || !name.trim()) throw { statusCode: 400, message: "name is required" };
|
||||
const { rows } = await app.db.query(
|
||||
`INSERT INTO projects (name, description, color, icon, owner_id, members)
|
||||
VALUES ($1, $2, $3, $4, $5, ARRAY[$5]::uuid[]) RETURNING *`,
|
||||
[name.trim(), description || "", color || "#3B82F6", icon || "\ud83d\udcc1", owner_id]
|
||||
);
|
||||
return { data: rows[0] };
|
||||
});
|
||||
|
||||
// Update project
|
||||
app.put("/projects/:id", async (req) => {
|
||||
const { name, description, color, icon } = req.body;
|
||||
const { rows } = await app.db.query(
|
||||
`UPDATE projects SET
|
||||
name = COALESCE($1, name),
|
||||
description = COALESCE($2, description),
|
||||
color = COALESCE($3, color),
|
||||
icon = COALESCE($4, icon),
|
||||
updated_at = NOW()
|
||||
WHERE id = $5 RETURNING *`,
|
||||
[name, description, color, icon, req.params.id]
|
||||
);
|
||||
if (!rows.length) throw { statusCode: 404, message: "Project not found" };
|
||||
return { data: rows[0] };
|
||||
});
|
||||
|
||||
// Invite user to project
|
||||
app.post("/projects/:id/invite", 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 projects SET
|
||||
members = array_append(members, $1::uuid),
|
||||
updated_at = NOW()
|
||||
WHERE id = $2 AND NOT ($1::uuid = ANY(members))
|
||||
RETURNING *`,
|
||||
[user_id, req.params.id]
|
||||
);
|
||||
if (!rows.length) {
|
||||
// Check if project exists
|
||||
const check = await app.db.query("SELECT id FROM projects WHERE id = $1", [req.params.id]);
|
||||
if (!check.rows.length) throw { statusCode: 404, message: "Project not found" };
|
||||
return { status: "already_member" };
|
||||
}
|
||||
return { data: rows[0], status: "invited" };
|
||||
});
|
||||
|
||||
// Remove member from project
|
||||
app.delete("/projects/:id/members/:userId", async (req) => {
|
||||
const { rows } = await app.db.query(
|
||||
`UPDATE projects SET
|
||||
members = array_remove(members, $1::uuid),
|
||||
updated_at = NOW()
|
||||
WHERE id = $2 RETURNING *`,
|
||||
[req.params.userId, req.params.id]
|
||||
);
|
||||
if (!rows.length) throw { statusCode: 404, message: "Project not found" };
|
||||
return { data: rows[0], status: "removed" };
|
||||
});
|
||||
|
||||
// Delete project (sets tasks.project_id to NULL via ON DELETE SET NULL)
|
||||
app.delete("/projects/:id", async (req) => {
|
||||
const { rowCount } = await app.db.query("DELETE FROM projects WHERE id = $1", [req.params.id]);
|
||||
if (!rowCount) throw { statusCode: 404, message: "Project not found" };
|
||||
return { status: "deleted" };
|
||||
});
|
||||
|
||||
// List tasks in project
|
||||
app.get("/projects/:id/tasks", async (req) => {
|
||||
const { rows } = await app.db.query(
|
||||
`SELECT t.*, tg.name as group_name, tg.color as group_color, tg.icon as group_icon
|
||||
FROM tasks t LEFT JOIN task_groups tg ON t.group_id = tg.id
|
||||
WHERE t.project_id = $1
|
||||
ORDER BY t.priority DESC, t.created_at DESC`,
|
||||
[req.params.id]
|
||||
);
|
||||
return { data: rows };
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = projectRoutes;
|
||||
@@ -183,6 +183,45 @@ async function taskRoutes(app) {
|
||||
`UPDATE tasks SET ${sets.join(", ")} WHERE id = $${i} RETURNING *`, params
|
||||
);
|
||||
if (!rows.length) throw { statusCode: 404, message: "Task not found" };
|
||||
|
||||
// Auto-create next occurrence for recurring tasks
|
||||
const updatedTask = rows[0];
|
||||
if ((fields.status === "completed" || fields.status === "done") && updatedTask.recurrence) {
|
||||
try {
|
||||
const rec = typeof updatedTask.recurrence === "string" ? JSON.parse(updatedTask.recurrence) : updatedTask.recurrence;
|
||||
let nextDate = new Date();
|
||||
if (rec.type === "daily") {
|
||||
nextDate.setDate(nextDate.getDate() + 1);
|
||||
} else if (rec.type === "weekly") {
|
||||
nextDate.setDate(nextDate.getDate() + 7);
|
||||
} else if (rec.type === "monthly") {
|
||||
nextDate.setMonth(nextDate.getMonth() + 1);
|
||||
}
|
||||
if (rec.time) {
|
||||
const [h, m] = rec.time.split(":");
|
||||
nextDate.setHours(parseInt(h) || 8, parseInt(m) || 0, 0, 0);
|
||||
}
|
||||
await app.db.query(
|
||||
`INSERT INTO tasks (title, description, status, group_id, priority, scheduled_at, due_at, assigned_to, recurrence, project_id)
|
||||
VALUES ($1, $2, 'pending', $3, $4, $5, $6, $7, $8, $9)`,
|
||||
[
|
||||
updatedTask.title,
|
||||
updatedTask.description,
|
||||
updatedTask.group_id,
|
||||
updatedTask.priority,
|
||||
nextDate.toISOString(),
|
||||
updatedTask.due_at ? new Date(new Date(updatedTask.due_at).getTime() + (nextDate.getTime() - Date.now())).toISOString() : null,
|
||||
updatedTask.assigned_to || [],
|
||||
JSON.stringify(rec),
|
||||
updatedTask.project_id
|
||||
]
|
||||
);
|
||||
app.log.info("Auto-created next recurring task for: " + updatedTask.title);
|
||||
} catch (recErr) {
|
||||
app.log.warn("Recurrence auto-create failed: " + recErr.message);
|
||||
}
|
||||
}
|
||||
|
||||
await invalidateTaskCaches();
|
||||
return { data: rows[0] };
|
||||
});
|
||||
@@ -267,6 +306,47 @@ async function taskRoutes(app) {
|
||||
return { data: rows[0] };
|
||||
});
|
||||
|
||||
|
||||
// === Task Recurrence Endpoints ===
|
||||
|
||||
// Set recurrence on a task
|
||||
app.post("/tasks/:id/recurrence", async (req) => {
|
||||
const { type, days, time } = req.body;
|
||||
if (!type || !["daily", "weekly", "monthly"].includes(type)) {
|
||||
throw { statusCode: 400, message: "type must be one of: daily, weekly, monthly" };
|
||||
}
|
||||
const recurrence = { type, days: days || [], time: time || "08:00" };
|
||||
const { rows } = await app.db.query(
|
||||
"UPDATE tasks SET recurrence = $1, updated_at = NOW() WHERE id = $2 RETURNING *",
|
||||
[JSON.stringify(recurrence), req.params.id]
|
||||
);
|
||||
if (!rows.length) throw { statusCode: 404, message: "Task not found" };
|
||||
await invalidateTaskCaches();
|
||||
return { data: rows[0] };
|
||||
});
|
||||
|
||||
// Remove recurrence from a task
|
||||
app.delete("/tasks/:id/recurrence", async (req) => {
|
||||
const { rows } = await app.db.query(
|
||||
"UPDATE tasks SET recurrence = NULL, updated_at = NOW() WHERE id = $1 RETURNING *",
|
||||
[req.params.id]
|
||||
);
|
||||
if (!rows.length) throw { statusCode: 404, message: "Task not found" };
|
||||
await invalidateTaskCaches();
|
||||
return { data: rows[0] };
|
||||
});
|
||||
|
||||
// List recurring tasks
|
||||
app.get("/tasks/recurring", async (req) => {
|
||||
const { rows } = await app.db.query(
|
||||
`SELECT t.*, tg.name as group_name, tg.color as group_color, tg.icon as group_icon
|
||||
FROM tasks t LEFT JOIN task_groups tg ON t.group_id = tg.id
|
||||
WHERE t.recurrence IS NOT NULL
|
||||
ORDER BY t.created_at DESC`
|
||||
);
|
||||
return { data: rows };
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
module.exports = taskRoutes;
|
||||
|
||||
@@ -5,10 +5,10 @@ import dayGridPlugin from '@fullcalendar/daygrid';
|
||||
import timeGridPlugin from '@fullcalendar/timegrid';
|
||||
import interactionPlugin from '@fullcalendar/interaction';
|
||||
import { useTranslation } from '@/lib/i18n';
|
||||
import { useAuth } from '@/lib/auth';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3000';
|
||||
|
||||
interface Task {
|
||||
interface CalTask {
|
||||
id: string;
|
||||
title: string;
|
||||
scheduled_at: string | null;
|
||||
@@ -26,32 +26,61 @@ const LOCALE_MAP: Record<string, string> = {
|
||||
};
|
||||
|
||||
export default function CalendarPage() {
|
||||
const [tasks, setTasks] = useState<Task[]>([]);
|
||||
const [tasks, setTasks] = useState<CalTask[]>([]);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const { t, locale } = useTranslation();
|
||||
const { token } = useAuth();
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
fetch(`${API_URL}/api/v1/tasks?limit=100`)
|
||||
.then(r => r.json())
|
||||
.then(d => setTasks(d.data || []));
|
||||
}, []);
|
||||
if (!token) {
|
||||
router.replace('/login');
|
||||
return;
|
||||
}
|
||||
fetch('/api/v1/tasks?limit=100', {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`,
|
||||
},
|
||||
})
|
||||
.then(r => {
|
||||
if (!r.ok) throw new Error(`HTTP ${r.status}`);
|
||||
return r.json();
|
||||
})
|
||||
.then(d => setTasks(d.data || []))
|
||||
.catch(err => setError(err.message));
|
||||
}, [token, router]);
|
||||
|
||||
const events = tasks
|
||||
.filter((t): t is Task & { scheduled_at: string } | Task & { due_at: string } =>
|
||||
t.scheduled_at !== null || t.due_at !== null
|
||||
)
|
||||
.map(t => ({
|
||||
id: t.id,
|
||||
title: t.title,
|
||||
start: (t.scheduled_at || t.due_at) as string,
|
||||
end: (t.due_at || t.scheduled_at) as string,
|
||||
backgroundColor: t.group_color || '#3B82F6',
|
||||
borderColor: t.group_color || '#3B82F6',
|
||||
extendedProps: { status: t.status, group: t.group_name }
|
||||
.filter((tk) => tk.scheduled_at !== null || tk.due_at !== null)
|
||||
.map(tk => ({
|
||||
id: tk.id,
|
||||
title: tk.title,
|
||||
start: (tk.scheduled_at || tk.due_at) as string,
|
||||
end: (tk.due_at || tk.scheduled_at) as string,
|
||||
backgroundColor: tk.group_color || '#3B82F6',
|
||||
borderColor: tk.group_color || '#3B82F6',
|
||||
extendedProps: { status: tk.status, group: tk.group_name },
|
||||
}));
|
||||
|
||||
// Build background events from unique groups
|
||||
const groupColors = new Map<string, string>();
|
||||
tasks.forEach(tk => {
|
||||
if (tk.group_name && tk.group_color) {
|
||||
groupColors.set(tk.group_name, tk.group_color);
|
||||
}
|
||||
});
|
||||
|
||||
if (!token) return null;
|
||||
|
||||
return (
|
||||
<div className="p-4">
|
||||
<h1 className="text-2xl font-bold mb-4">{t('calendar.title')}</h1>
|
||||
{error && (
|
||||
<div className="mb-4 p-3 bg-red-50 dark:bg-red-900/20 text-red-600 dark:text-red-400 rounded-lg text-sm">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
<div className="bg-white dark:bg-gray-800 rounded-xl p-4 shadow">
|
||||
<FullCalendar
|
||||
plugins={[dayGridPlugin, timeGridPlugin, interactionPlugin]}
|
||||
@@ -59,7 +88,7 @@ export default function CalendarPage() {
|
||||
headerToolbar={{
|
||||
left: 'prev,next today',
|
||||
center: 'title',
|
||||
right: 'dayGridMonth,timeGridWeek,timeGridDay'
|
||||
right: 'dayGridMonth,timeGridWeek,timeGridDay',
|
||||
}}
|
||||
events={events}
|
||||
editable={true}
|
||||
|
||||
237
apps/tasks/app/projects/page.tsx
Normal file
237
apps/tasks/app/projects/page.tsx
Normal file
@@ -0,0 +1,237 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState, useCallback } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useAuth } from "@/lib/auth";
|
||||
import { useTranslation } from "@/lib/i18n";
|
||||
import {
|
||||
getProjects,
|
||||
createProject,
|
||||
deleteProject,
|
||||
Project,
|
||||
} from "@/lib/api";
|
||||
|
||||
export default function ProjectsPage() {
|
||||
const { token, user } = useAuth();
|
||||
const { t } = useTranslation();
|
||||
const router = useRouter();
|
||||
const [projects, setProjects] = useState<Project[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [showForm, setShowForm] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
// Form state
|
||||
const [formName, setFormName] = useState("");
|
||||
const [formDesc, setFormDesc] = useState("");
|
||||
const [formColor, setFormColor] = useState("#3B82F6");
|
||||
const [formIcon, setFormIcon] = useState("\ud83d\udcc1");
|
||||
|
||||
const COLORS = ["#3B82F6", "#EF4444", "#10B981", "#F59E0B", "#8B5CF6", "#EC4899", "#6366F1", "#14B8A6"];
|
||||
const ICONS = ["\ud83d\udcc1", "\ud83d\ude80", "\ud83d\udca1", "\ud83c\udfaf", "\ud83d\udee0\ufe0f", "\ud83c\udf1f", "\ud83d\udcca", "\ud83d\udd25"];
|
||||
|
||||
const loadData = useCallback(async () => {
|
||||
if (!token) return;
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await getProjects(token);
|
||||
setProjects(res.data || []);
|
||||
} catch (err) {
|
||||
console.error("Load error:", err);
|
||||
setError(t("common.error"));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [token, t]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!token) {
|
||||
router.replace("/login");
|
||||
return;
|
||||
}
|
||||
loadData();
|
||||
}, [token, router, loadData]);
|
||||
|
||||
async function handleCreate(e: React.FormEvent) {
|
||||
e.preventDefault();
|
||||
if (!token || !formName.trim()) return;
|
||||
try {
|
||||
await createProject(token, {
|
||||
name: formName.trim(),
|
||||
description: formDesc,
|
||||
color: formColor,
|
||||
icon: formIcon,
|
||||
owner_id: user?.id,
|
||||
});
|
||||
setFormName("");
|
||||
setFormDesc("");
|
||||
setFormColor("#3B82F6");
|
||||
setFormIcon("\ud83d\udcc1");
|
||||
setShowForm(false);
|
||||
loadData();
|
||||
} catch (err) {
|
||||
console.error("Create error:", err);
|
||||
setError(t("common.error"));
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDelete(id: string) {
|
||||
if (!token) return;
|
||||
if (!confirm(t("tasks.confirmDelete"))) return;
|
||||
try {
|
||||
await deleteProject(token, id);
|
||||
loadData();
|
||||
} catch (err) {
|
||||
console.error("Delete error:", err);
|
||||
setError(t("common.error"));
|
||||
}
|
||||
}
|
||||
|
||||
function formatDate(d: string) {
|
||||
return new Date(d).toLocaleDateString("cs-CZ", { day: "numeric", month: "short" });
|
||||
}
|
||||
|
||||
if (!token) return null;
|
||||
|
||||
return (
|
||||
<div className="space-y-4 pb-24 sm:pb-8 px-4 sm:px-0">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<h1 className="text-xl font-bold dark:text-white">{t("nav.projects")}</h1>
|
||||
<button
|
||||
onClick={() => setShowForm(!showForm)}
|
||||
className="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg text-sm font-medium transition-colors min-h-[44px]"
|
||||
>
|
||||
{showForm ? t("tasks.form.cancel") : `+ ${t("projects.add")}`}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Error */}
|
||||
{error && (
|
||||
<div className="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-3 text-red-700 dark:text-red-300 text-sm">
|
||||
{error}
|
||||
<button onClick={() => setError(null)} className="ml-2 underline">{t("tasks.close")}</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Create form */}
|
||||
{showForm && (
|
||||
<form onSubmit={handleCreate} className="bg-white dark:bg-gray-900 rounded-xl border border-gray-200 dark:border-gray-800 p-4 space-y-3">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">{t("tasks.form.title")}</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formName}
|
||||
onChange={(e) => setFormName(e.target.value)}
|
||||
placeholder={t("projects.namePlaceholder")}
|
||||
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
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">{t("tasks.form.description")}</label>
|
||||
<textarea
|
||||
value={formDesc}
|
||||
onChange={(e) => setFormDesc(e.target.value)}
|
||||
placeholder={t("projects.descPlaceholder")}
|
||||
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-[80px] resize-none"
|
||||
rows={2}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">{t("projects.color")}</label>
|
||||
<div className="flex gap-1.5 flex-wrap">
|
||||
{COLORS.map((c) => (
|
||||
<button
|
||||
key={c}
|
||||
type="button"
|
||||
onClick={() => setFormColor(c)}
|
||||
className={`w-8 h-8 rounded-full border-2 transition-all ${formColor === c ? "border-gray-900 dark:border-white scale-110" : "border-transparent"}`}
|
||||
style={{ backgroundColor: c }}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">{t("projects.icon")}</label>
|
||||
<div className="flex gap-1.5 flex-wrap">
|
||||
{ICONS.map((ic) => (
|
||||
<button
|
||||
key={ic}
|
||||
type="button"
|
||||
onClick={() => setFormIcon(ic)}
|
||||
className={`w-8 h-8 rounded-lg flex items-center justify-center text-lg border-2 transition-all ${formIcon === ic ? "border-blue-500 bg-blue-50 dark:bg-blue-900/20" : "border-gray-200 dark:border-gray-700"}`}
|
||||
>
|
||||
{ic}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
className="w-full py-2.5 bg-blue-600 hover:bg-blue-700 text-white rounded-lg font-medium transition-colors min-h-[44px]"
|
||||
>
|
||||
{t("projects.add")}
|
||||
</button>
|
||||
</form>
|
||||
)}
|
||||
|
||||
{/* Projects list */}
|
||||
{loading ? (
|
||||
<div className="flex justify-center py-12">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600" />
|
||||
</div>
|
||||
) : projects.length === 0 ? (
|
||||
<div className="text-center py-16">
|
||||
<div className="text-5xl mb-4 opacity-50">{"\ud83d\udcc2"}</div>
|
||||
<p className="text-gray-500 dark:text-gray-400 text-lg font-medium">{t("projects.empty")}</p>
|
||||
<p className="text-gray-400 dark:text-gray-500 text-sm mt-1">{t("projects.createFirst")}</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{projects.map((project) => (
|
||||
<div
|
||||
key={project.id}
|
||||
className="bg-white dark:bg-gray-900 rounded-xl border border-gray-200 dark:border-gray-800 p-4 transition-all hover:shadow-md"
|
||||
>
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="flex items-start gap-3 flex-1 min-w-0">
|
||||
<div
|
||||
className="w-10 h-10 rounded-lg flex items-center justify-center text-xl flex-shrink-0"
|
||||
style={{ backgroundColor: (project.color || "#3B82F6") + "15" }}
|
||||
>
|
||||
{project.icon || "\ud83d\udcc1"}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<h3 className="font-semibold text-gray-900 dark:text-white truncate">{project.name}</h3>
|
||||
{project.description && (
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400 truncate mt-0.5">{project.description}</p>
|
||||
)}
|
||||
<div className="flex items-center gap-3 mt-1.5 text-xs text-gray-400 dark:text-gray-500">
|
||||
<span>{t("projects.tasks")}: {project.task_count || 0}</span>
|
||||
<span>{t("projects.members")}: {project.members?.length || 0}</span>
|
||||
<span>{formatDate(project.created_at)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-1 flex-shrink-0">
|
||||
<div className="w-3 h-3 rounded-full" style={{ backgroundColor: project.color || "#3B82F6" }} />
|
||||
<button
|
||||
onClick={() => handleDelete(project.id)}
|
||||
className="p-2 text-gray-400 hover:text-red-500 transition-colors min-h-[44px] min-w-[44px] flex items-center justify-center"
|
||||
title={t("tasks.delete")}
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -162,27 +162,66 @@ export default function TaskDetailPage() {
|
||||
const taskDone = isDone(task.status);
|
||||
|
||||
return (
|
||||
<div className="max-w-lg mx-auto space-y-6">
|
||||
{/* Back button */}
|
||||
<button
|
||||
onClick={() => router.push("/tasks")}
|
||||
className="text-sm text-muted hover:text-foreground flex items-center gap-1"
|
||||
>
|
||||
<svg
|
||||
className="w-4 h-4"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
<div className="max-w-lg mx-auto space-y-4">
|
||||
{/* Action bar - all buttons in one compact row */}
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<button
|
||||
onClick={() => router.push("/tasks")}
|
||||
className="inline-flex items-center gap-1.5 px-3 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 rounded-lg transition-colors"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M15 19l-7-7 7-7"
|
||||
/>
|
||||
</svg>
|
||||
{t("common.back")}
|
||||
</button>
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M15 19l-7-7 7-7" />
|
||||
</svg>
|
||||
{t("common.back")}
|
||||
</button>
|
||||
|
||||
<div className="flex-1" />
|
||||
|
||||
{!taskDone && (
|
||||
<button
|
||||
onClick={() => handleQuickStatus("done")}
|
||||
className="inline-flex items-center gap-1.5 px-3 py-2 text-sm font-medium bg-green-600 hover:bg-green-700 text-white rounded-lg transition-colors"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
{t("tasks.markDone")}
|
||||
</button>
|
||||
)}
|
||||
|
||||
{taskDone && (
|
||||
<button
|
||||
onClick={() => handleQuickStatus("pending")}
|
||||
className="inline-flex items-center gap-1.5 px-3 py-2 text-sm font-medium bg-yellow-600 hover:bg-yellow-700 text-white rounded-lg transition-colors"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
||||
</svg>
|
||||
{t("tasks.reopen")}
|
||||
</button>
|
||||
)}
|
||||
|
||||
<button
|
||||
onClick={() => setEditing(true)}
|
||||
className="inline-flex items-center gap-1.5 px-3 py-2 text-sm font-medium border border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-800 rounded-lg transition-colors"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
|
||||
</svg>
|
||||
{t("tasks.edit")}
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={handleDelete}
|
||||
disabled={deleting}
|
||||
className="inline-flex items-center gap-1.5 px-3 py-2 text-sm 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 rounded-lg transition-colors disabled:opacity-50"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
||||
</svg>
|
||||
{deleting ? t("tasks.deleting") : t("tasks.delete")}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Task detail card */}
|
||||
<div className="bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 rounded-2xl p-6">
|
||||
@@ -253,51 +292,6 @@ export default function TaskDetailPage() {
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Quick status buttons */}
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
{!taskDone && (
|
||||
<button
|
||||
onClick={() => handleQuickStatus("done")}
|
||||
className="px-4 py-2 bg-green-600 hover:bg-green-700 text-white rounded-lg text-sm font-medium transition-colors"
|
||||
>
|
||||
{t("tasks.markDone")}
|
||||
</button>
|
||||
)}
|
||||
{task.status === "pending" && (
|
||||
<button
|
||||
onClick={() => handleQuickStatus("in_progress")}
|
||||
className="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg text-sm font-medium transition-colors"
|
||||
>
|
||||
{t("tasks.start")}
|
||||
</button>
|
||||
)}
|
||||
{taskDone && (
|
||||
<button
|
||||
onClick={() => handleQuickStatus("pending")}
|
||||
className="px-4 py-2 bg-yellow-600 hover:bg-yellow-700 text-white rounded-lg text-sm font-medium transition-colors"
|
||||
>
|
||||
{t("tasks.reopen")}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Action buttons */}
|
||||
<div className="flex gap-3">
|
||||
<button
|
||||
onClick={() => setEditing(true)}
|
||||
className="flex-1 py-2.5 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors font-medium"
|
||||
>
|
||||
{t("tasks.edit")}
|
||||
</button>
|
||||
<button
|
||||
onClick={handleDelete}
|
||||
disabled={deleting}
|
||||
className="px-6 py-2.5 border border-red-300 dark:border-red-800 text-red-600 dark:text-red-400 rounded-lg hover:bg-red-50 dark:hover:bg-red-900/20 transition-colors font-medium disabled:opacity-50"
|
||||
>
|
||||
{deleting ? t("tasks.deleting") : t("tasks.delete")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -27,6 +27,18 @@ export default function BottomNav() {
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
{
|
||||
href: "/projects",
|
||||
label: t("nav.projects"),
|
||||
icon: (
|
||||
<svg className="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M2 12.5V3.5C2 2.67 2.67 2 3.5 2H8.5C9.33 2 10 2.67 10 3.5V12.5C10 13.33 9.33 14 8.5 14H3.5C2.67 14 2 13.33 2 12.5Z" />
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M14 20.5V11.5C14 10.67 14.67 10 15.5 10H20.5C21.33 10 22 10.67 22 11.5V20.5C22 21.33 21.33 22 20.5 22H15.5C14.67 22 14 21.33 14 20.5Z" />
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M14 5.5V3.5C14 2.67 14.67 2 15.5 2H20.5C21.33 2 22 2.67 22 3.5V5.5C22 6.33 21.33 7 20.5 7H15.5C14.67 7 14 6.33 14 5.5Z" />
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M2 20.5V18.5C2 17.67 2.67 17 3.5 17H8.5C9.33 17 10 17.67 10 18.5V20.5C10 21.33 9.33 22 8.5 22H3.5C2.67 22 2 21.33 2 20.5Z" />
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
{
|
||||
href: "/goals",
|
||||
label: t("nav.goals"),
|
||||
@@ -45,16 +57,6 @@ export default function BottomNav() {
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
{
|
||||
href: "/settings",
|
||||
label: t("nav.settings"),
|
||||
icon: (
|
||||
<svg className="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.066 2.573c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.573 1.066c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.066-2.573c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
@@ -66,7 +68,7 @@ export default function BottomNav() {
|
||||
<Link
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
className={`flex flex-col items-center justify-center gap-0.5 flex-1 h-full min-w-[64px] min-h-[48px] rounded-lg transition-colors ${
|
||||
className={`flex flex-col items-center justify-center gap-0.5 flex-1 h-full min-w-[56px] min-h-[48px] rounded-lg transition-colors ${
|
||||
isActive
|
||||
? "text-blue-600 dark:text-blue-400"
|
||||
: "text-gray-400 dark:text-gray-500 hover:text-gray-600 dark:hover:text-gray-300"
|
||||
|
||||
@@ -204,3 +204,44 @@ export function generateGoalPlan(token: string, id: string) {
|
||||
export function getGoalReport(token: string, id: string) {
|
||||
return apiFetch<{ data: GoalReport }>(`/api/v1/goals/${id}/report`, { token });
|
||||
}
|
||||
|
||||
|
||||
// Projects
|
||||
export interface Project {
|
||||
id: string;
|
||||
owner_id: string | null;
|
||||
name: string;
|
||||
description: string;
|
||||
color: string;
|
||||
icon: string;
|
||||
members: string[];
|
||||
task_count?: number;
|
||||
done_count?: number;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export function getProjects(token: string, userId?: string) {
|
||||
const qs = userId ? "?user_id=" + userId : "";
|
||||
return apiFetch<{ data: Project[] }>("/api/v1/projects" + qs, { token });
|
||||
}
|
||||
|
||||
export function getProject(token: string, id: string) {
|
||||
return apiFetch<{ data: Project }>("/api/v1/projects/" + id, { token });
|
||||
}
|
||||
|
||||
export function createProject(token: string, data: Partial<Project>) {
|
||||
return apiFetch<{ data: Project }>("/api/v1/projects", { method: "POST", body: data, token });
|
||||
}
|
||||
|
||||
export function updateProject(token: string, id: string, data: Partial<Project>) {
|
||||
return apiFetch<{ data: Project }>("/api/v1/projects/" + id, { method: "PUT", body: data, token });
|
||||
}
|
||||
|
||||
export function deleteProject(token: string, id: string) {
|
||||
return apiFetch<void>("/api/v1/projects/" + id, { method: "DELETE", token });
|
||||
}
|
||||
|
||||
export function inviteToProject(token: string, id: string, userId: string) {
|
||||
return apiFetch<{ data: Project; status: string }>("/api/v1/projects/" + id + "/invite", { method: "POST", body: { user_id: userId }, token });
|
||||
}
|
||||
|
||||
@@ -22,14 +22,14 @@ const MESSAGES: Record<Locale, Messages> = { cs, he, ru, ua };
|
||||
|
||||
const STORAGE_KEY = "taskteam_language";
|
||||
|
||||
function getNestedValue(obj: unknown, path: string): string {
|
||||
function getNestedValue(obj: unknown, path: string): string | undefined {
|
||||
const keys = path.split(".");
|
||||
let current: unknown = obj;
|
||||
for (const key of keys) {
|
||||
if (current == null || typeof current !== "object") return path;
|
||||
if (current == null || typeof current !== "object") return undefined;
|
||||
current = (current as Record<string, unknown>)[key];
|
||||
}
|
||||
return typeof current === "string" ? current : path;
|
||||
return typeof current === "string" ? current : undefined;
|
||||
}
|
||||
|
||||
interface I18nContextType {
|
||||
@@ -75,7 +75,10 @@ export function I18nProvider({ children }: { children: ReactNode }) {
|
||||
|
||||
const t = useCallback(
|
||||
(key: string): string => {
|
||||
return getNestedValue(MESSAGES[locale], key);
|
||||
const value = getNestedValue(MESSAGES[locale], key);
|
||||
if (value !== undefined) return value;
|
||||
// Fallback: return last segment of the key (the raw value) instead of "undefined"
|
||||
return key.split(".").pop() || key;
|
||||
},
|
||||
[locale]
|
||||
);
|
||||
|
||||
@@ -1,10 +1,133 @@
|
||||
{
|
||||
"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ář" }
|
||||
}
|
||||
"nav": {
|
||||
"tasks": "Úkoly",
|
||||
"calendar": "Kalendář",
|
||||
"chat": "Chat",
|
||||
"settings": "Nastavení",
|
||||
"goals": "Cíle",
|
||||
"projects": "Projekty"
|
||||
},
|
||||
"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ář"
|
||||
},
|
||||
"projects": {
|
||||
"add": "Novy projekt",
|
||||
"empty": "Zadne projekty",
|
||||
"createFirst": "Vytvorte prvni projekt tlacitkem +",
|
||||
"namePlaceholder": "Nazev projektu...",
|
||||
"descPlaceholder": "Popis projektu...",
|
||||
"color": "Barva",
|
||||
"icon": "Ikona",
|
||||
"tasks": "Ukoly",
|
||||
"members": "Clenove"
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,133 @@
|
||||
{
|
||||
"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": "לוח שנה" }
|
||||
}
|
||||
"nav": {
|
||||
"tasks": "משימות",
|
||||
"calendar": "לוח שנה",
|
||||
"chat": "צ׳אט",
|
||||
"settings": "הגדרות",
|
||||
"goals": "מטרות",
|
||||
"projects": "פרויקטים"
|
||||
},
|
||||
"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": "לוח שנה"
|
||||
},
|
||||
"projects": {
|
||||
"add": "פרויקט חדש",
|
||||
"empty": "אין פרויקטים",
|
||||
"createFirst": "צור פרויקט ראשון +",
|
||||
"namePlaceholder": "שם פרויקט...",
|
||||
"descPlaceholder": "תיאור פרויקט...",
|
||||
"color": "צבע",
|
||||
"icon": "איקון",
|
||||
"tasks": "משימות",
|
||||
"members": "חברים"
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,133 @@
|
||||
{
|
||||
"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": "Календарь" }
|
||||
}
|
||||
"nav": {
|
||||
"tasks": "Задачи",
|
||||
"calendar": "Календарь",
|
||||
"chat": "Чат",
|
||||
"settings": "Настройки",
|
||||
"goals": "Цели",
|
||||
"projects": "Проекты"
|
||||
},
|
||||
"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": "Календарь"
|
||||
},
|
||||
"projects": {
|
||||
"add": "Новый проект",
|
||||
"empty": "Нет проектов",
|
||||
"createFirst": "Создайте первый проект кнопкой +",
|
||||
"namePlaceholder": "Название проекта...",
|
||||
"descPlaceholder": "Описание проекта...",
|
||||
"color": "Цвет",
|
||||
"icon": "Иконка",
|
||||
"tasks": "Задачи",
|
||||
"members": "Участники"
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,133 @@
|
||||
{
|
||||
"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": "Календар" }
|
||||
}
|
||||
"nav": {
|
||||
"tasks": "Завдання",
|
||||
"calendar": "Календар",
|
||||
"chat": "Чат",
|
||||
"settings": "Налаштування",
|
||||
"goals": "Цілі",
|
||||
"projects": "Проекти"
|
||||
},
|
||||
"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": "Календар"
|
||||
},
|
||||
"projects": {
|
||||
"add": "Новий проект",
|
||||
"empty": "Немає проектів",
|
||||
"createFirst": "Створіть перший проект кнопкою +",
|
||||
"namePlaceholder": "Назва проекту...",
|
||||
"descPlaceholder": "Опис проекту...",
|
||||
"color": "Колір",
|
||||
"icon": "Іконка",
|
||||
"tasks": "Завдання",
|
||||
"members": "Учасники"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user