feat(frontend): settings window, i18n, admin UI, books, tasks, calendar, timer
- Settings window with tabs: profile (name, email, avatar, password), appearance (light/dark/system), language (de/en/zh), background (gradient presets, custom URL, blur toggle); persisted server-side - i18n dictionaries (de/en/zh) applied across nav, sidebar, tasks, calendar, time tracking, admin, invites and comments - Greeting header with time-of-day message, live clock and date; cooler theme, cards, kanban, calendar grid and data-table styling - Shared sidebar: nav, collapsible books with per-book pages, top-level pages, search; user chip opens settings - Tasks kanban with priorities, overdue flags and book/page links; calendar month view fed by task deadlines; time view with entries, durations and totals; top-right live timer with optional book link - User discovery autocomplete in the invite card; editor gains tables and checklists; standalone theme buttons removed
This commit is contained in:
@@ -0,0 +1,52 @@
|
||||
import { defineStore } from 'pinia';
|
||||
import api from '../api';
|
||||
|
||||
export function formatDuration(totalSeconds) {
|
||||
const s = Math.max(0, Math.floor(totalSeconds || 0));
|
||||
const h = String(Math.floor(s / 3600)).padStart(2, '0');
|
||||
const m = String(Math.floor((s % 3600) / 60)).padStart(2, '0');
|
||||
const sec = String(s % 60).padStart(2, '0');
|
||||
return `${h}:${m}:${sec}`;
|
||||
}
|
||||
|
||||
export const useTimeStore = defineStore('time', {
|
||||
state: () => ({
|
||||
entries: [],
|
||||
open: null, // running timer entry (ended_at == null)
|
||||
workspaceId: null,
|
||||
}),
|
||||
getters: {
|
||||
elapsed: (s) => {
|
||||
if (!s.open) return 0;
|
||||
return (Date.now() - new Date(s.open.started_at).getTime()) / 1000;
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
async fetch(workspaceId) {
|
||||
this.workspaceId = workspaceId;
|
||||
const { data } = await api.get(`/time/by-workspace/${workspaceId}`);
|
||||
this.entries = data.entries;
|
||||
this.open = data.open;
|
||||
},
|
||||
async start(description = '', book_id = null) {
|
||||
const { data } = await api.post('/time/start', {
|
||||
workspace_id: this.workspaceId, description, book_id,
|
||||
});
|
||||
this.open = data.entry;
|
||||
this.entries.unshift(data.entry);
|
||||
return data.entry;
|
||||
},
|
||||
async stop() {
|
||||
const { data } = await api.post('/time/stop', { workspace_id: this.workspaceId });
|
||||
this.open = null;
|
||||
const idx = this.entries.findIndex((e) => e.id === data.entry.id);
|
||||
if (idx >= 0) this.entries[idx] = { ...data.entry, user_name: this.entries[idx].user_name };
|
||||
else this.entries.unshift(data.entry);
|
||||
return data.entry;
|
||||
},
|
||||
async remove(id) {
|
||||
await api.delete(`/time/${id}`);
|
||||
this.entries = this.entries.filter((e) => e.id !== id);
|
||||
},
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user