Add project and todo management

This commit is contained in:
michael committed 2026-09-02 11:03:50 +02:00
1 parent 2792051a16
commit 3da436423f
12 files changed
+1865 -43

No files matched your search

+35 -6
View File
@@ -6,8 +6,10 @@ import {
ChartNoAxesCombined,
CircleHelp,
FileText,
FolderKanban,
GripVertical,
LayoutGrid,
ListTodo,
LogOut,
Moon,
Pause,
@@ -65,11 +67,13 @@ import { FaqPage } from "./views/FaqPage";
import { LoginPage } from "./views/LoginPage";
import { MonthlyClosePage } from "./views/MonthlyClosePage";
import { ProfilePage } from "./views/ProfilePage";
import { ProjectsPage } from "./views/ProjectsPage";
import { RecurringBillingsPage } from "./views/RecurringBillingsPage";
import { StatisticsPage } from "./views/StatisticsPage";
import { SetupPage } from "./views/SetupPage";
import { TicketDetailPage } from "./views/TicketDetailPage";
import { TimerPage } from "./views/TimerPage";
import { TodosPage } from "./views/TodosPage";
import type { AuthUser, PeriodType, TicketDetailPeriodType, TicketSearchResult } from "./types";
import type { Dispatch, DragEvent, SetStateAction } from "react";
@@ -177,6 +181,14 @@ function routeFromPath(pathWithSearch: string) {
return { page: "statistics" as const };
}
if (pathname.startsWith("/projects")) {
return { page: "projects" as const };
}
if (pathname.startsWith("/todos")) {
return { page: "todos" as const };
}
if (pathname.startsWith("/faq")) {
return { page: "faq" as const };
}
@@ -387,11 +399,15 @@ function HeaderTicketSearch({ className, onNavigate }: HeaderTicketSearchProps)
}}
>
<span className="flex min-w-0 items-center justify-between gap-3">
<span className="truncate text-sm font-medium">{ticket.ticket_number}</span>
<span className="flex min-w-0 items-center gap-2">
<span className="truncate text-sm font-medium">{ticket.ticket_number}</span>
{ticket.is_project ? <span className="rounded-full border px-1.5 py-0.5 text-[10px] uppercase text-muted-foreground">Projekt</span> : null}
</span>
<span className="shrink-0 text-xs text-muted-foreground">{formatMinutes(ticket.total_minutes)}</span>
</span>
<span className="truncate text-xs text-muted-foreground">
{ticket.organization_name || ticket.customer_name || "Keine Organisation"} · {ticket.session_count} Session(s) · zuletzt {formatDateTime(ticket.latest_started_at)}
{ticket.project_name || ticket.organization_name || ticket.customer_name || "Keine Organisation"} · {ticket.session_count} Session(s)
{ticket.latest_started_at ? ` · zuletzt ${formatDateTime(ticket.latest_started_at)}` : ""}
</span>
</button>
))
@@ -489,7 +505,8 @@ function SidebarTimers({ timers, selectedTimerId, setSelectedTimerId, setTimers,
</span>
{timer.organizationName ? (
<span className="block truncate text-[11px] leading-4 text-muted-foreground">
{timer.organizationName}
{timer.isProject ? "Projekt · " : ""}
{timer.projectName || timer.organizationName}
</span>
) : null}
<span className="block font-mono text-[11px] font-semibold leading-4 tracking-normal text-muted-foreground">
@@ -631,13 +648,17 @@ export function App() {
return {
id: timer.id,
organizationName: result.ticket?.organization_name ?? result.ticket?.customer_name ?? null,
workType: result.ticket?.work_type ?? null
workType: result.ticket?.work_type ?? null,
isProject: Boolean(result.ticket?.is_project),
projectName: result.ticket?.project_name ?? null
};
} catch {
return {
id: timer.id,
organizationName: null,
workType: null
workType: null,
isProject: false,
projectName: null
};
}
})
@@ -659,6 +680,8 @@ export function App() {
...timer,
organizationName: result.organizationName,
workType: result.workType,
isProject: result.isProject,
projectName: result.projectName,
ticketLookupDone: true
};
})
@@ -727,6 +750,8 @@ export function App() {
ticketNumber,
organizationName: ticketResult.ticket?.organization_name ?? ticketResult.ticket?.customer_name ?? null,
workType: ticketResult.ticket?.work_type ?? null,
isProject: Boolean(ticketResult.ticket?.is_project),
projectName: ticketResult.ticket?.project_name ?? null,
ticketLookupDone: true,
startedAt: now,
pausedTotalMs: 0,
@@ -737,7 +762,7 @@ export function App() {
setTimers((current) => [...current.map((timer) => pauseEntry(timer, now)), newTimer]);
setSelectedTimerId(newTimer.id);
toast.success("Timer gestartet", {
description: ticketNumber
description: ticketResult.ticket?.is_project ? `${ticketNumber} ist ein Projekt` : ticketNumber
});
return true;
}
@@ -870,6 +895,8 @@ export function App() {
{ type: "item" as const, href: "/monthly-close", label: "Monatsabschluss", icon: ClipboardCheck, active: path.startsWith("/monthly-close") },
{ type: "separator" as const, key: "after-monthly-close" },
{ type: "item" as const, href: "/statistics", label: "Statistiken", icon: ChartNoAxesCombined, active: path.startsWith("/statistics") },
{ type: "item" as const, href: "/projects", label: "Projekte", icon: FolderKanban, active: path.startsWith("/projects") },
{ type: "item" as const, href: "/todos", label: "ToDos", icon: ListTodo, active: path.startsWith("/todos") },
{ type: "item" as const, href: "/recurring", label: "Fixe Abrechnung", icon: Repeat, active: path.startsWith("/recurring") },
{ type: "separator" as const, key: "before-profile" },
{ type: "item" as const, href: "/profile", label: "Profil", icon: UserCog, active: path.startsWith("/profile") },
@@ -1130,6 +1157,8 @@ export function App() {
{!isAdmin && route.page === "analysis" ? <AnalysisPage onNavigate={navigate} /> : null}
{!isAdmin && route.page === "monthly-close" ? <MonthlyClosePage onNavigate={navigate} /> : null}
{!isAdmin && route.page === "statistics" ? <StatisticsPage onNavigate={navigate} /> : null}
{!isAdmin && route.page === "projects" ? <ProjectsPage onNavigate={navigate} onStartTimer={startQuickTimer} /> : null}
{!isAdmin && route.page === "todos" ? <TodosPage onNavigate={navigate} /> : null}
{!isAdmin && route.page === "recurring" ? <RecurringBillingsPage /> : null}
{route.page === "faq" ? <FaqPage /> : null}
{route.page === "profile" ? <ProfilePage currentUser={currentUser} onUserUpdated={setCurrentUser} /> : null}