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:
2026-03-29 13:49:25 +00:00
parent fc39029ce3
commit b4b8439f80
14 changed files with 1173 additions and 136 deletions

View File

@@ -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 });
}

View File

@@ -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]
);