This commit is contained in:
simon.franken
2026-02-18 16:08:42 +01:00
parent f5c0a0b2f7
commit 0f6e55302a
10 changed files with 29 additions and 14 deletions

View File

@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "users" ADD COLUMN "full_name" VARCHAR(255);

View File

@@ -10,6 +10,7 @@ datasource db {
model User { model User {
id String @id @db.VarChar(255) id String @id @db.VarChar(255)
username String @db.VarChar(255) username String @db.VarChar(255)
fullName String? @db.VarChar(255) @map("full_name")
email String @db.VarChar(255) email String @db.VarChar(255)
createdAt DateTime @default(now()) @map("created_at") createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at") updatedAt DateTime @updatedAt @map("updated_at")

View File

@@ -92,6 +92,7 @@ export async function getUserInfo(tokenSet: TokenSet): Promise<AuthenticatedUser
const id = String(claims.sub); const id = String(claims.sub);
const username = String(userInfo.preferred_username || claims.preferred_username || claims.name || id); const username = String(userInfo.preferred_username || claims.preferred_username || claims.name || id);
const email = String(userInfo.email || claims.email || ''); const email = String(userInfo.email || claims.email || '');
const fullName = String(userInfo.name || claims.name || '') || null;
if (!email) { if (!email) {
throw new Error('Email not provided by OIDC provider'); throw new Error('Email not provided by OIDC provider');
@@ -100,6 +101,7 @@ export async function getUserInfo(tokenSet: TokenSet): Promise<AuthenticatedUser
return { return {
id, id,
username, username,
fullName,
email, email,
}; };
} }

View File

@@ -32,11 +32,13 @@ export async function syncUser(user: AuthenticatedUser): Promise<void> {
where: { id: user.id }, where: { id: user.id },
update: { update: {
username: user.username, username: user.username,
fullName: user.fullName,
email: user.email, email: user.email,
}, },
create: { create: {
id: user.id, id: user.id,
username: user.username, username: user.username,
fullName: user.fullName,
email: user.email, email: user.email,
}, },
}); });

View File

@@ -3,6 +3,7 @@ import { Request } from 'express';
export interface AuthenticatedUser { export interface AuthenticatedUser {
id: string; id: string;
username: string; username: string;
fullName: string | null;
email: string; email: string;
} }

View File

@@ -6,8 +6,8 @@ export function Layout() {
return ( return (
<div className="min-h-screen bg-gray-50"> <div className="min-h-screen bg-gray-50">
<Navbar /> <Navbar />
<main className="pt-4 pb-24 px-4 sm:px-6 lg:px-8"> <main className="pt-4 pb-24">
<div className="max-w-7xl mx-auto"> <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<Outlet /> <Outlet />
</div> </div>
</main> </main>

View File

@@ -114,8 +114,8 @@ export function Navbar() {
</div> </div>
</div> </div>
<div className="flex items-center space-x-4"> <div className="flex items-center space-x-4">
<span className="text-sm text-gray-600 hidden sm:block"> <span className="text-sm font-medium text-gray-600 hidden sm:block">
{user?.username} {user?.fullName || user?.username}
</span> </span>
<button <button
onClick={logout} onClick={logout}

View File

@@ -80,8 +80,8 @@ export function TimerWidget() {
if (isLoading) { if (isLoading) {
return ( return (
<div className="fixed bottom-0 left-0 right-0 bg-white border-t border-gray-200 p-4 shadow-lg"> <div className="fixed bottom-0 left-0 right-0 bg-white border-t border-gray-200 py-4 shadow-lg">
<div className="max-w-7xl mx-auto flex items-center justify-center"> <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 flex items-center justify-center">
<div className="animate-spin rounded-full h-6 w-6 border-b-2 border-primary-600"></div> <div className="animate-spin rounded-full h-6 w-6 border-b-2 border-primary-600"></div>
</div> </div>
</div> </div>
@@ -89,8 +89,8 @@ export function TimerWidget() {
} }
return ( return (
<div className="fixed bottom-0 left-0 right-0 bg-white border-t border-gray-200 p-4 shadow-lg z-50"> <div className="fixed bottom-0 left-0 right-0 bg-white border-t border-gray-200 py-4 shadow-lg z-50">
<div className="max-w-7xl mx-auto flex flex-wrap sm:flex-nowrap items-center gap-2 sm:justify-between"> <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 flex flex-wrap sm:flex-nowrap items-center gap-2 sm:justify-between">
{ongoingTimer ? ( {ongoingTimer ? (
<> <>
{/* Row 1 (mobile): timer + stop side by side. On sm+ dissolves into the parent flex row via contents. */} {/* Row 1 (mobile): timer + stop side by side. On sm+ dissolves into the parent flex row via contents. */}
@@ -183,7 +183,7 @@ export function TimerWidget() {
</div> </div>
{error && ( {error && (
<div className="max-w-7xl mx-auto mt-2"> <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 mt-2">
<p className="text-sm text-red-600">{error}</p> <p className="text-sm text-red-600">{error}</p>
</div> </div>
)} )}

View File

@@ -19,14 +19,20 @@ export function StatisticsPage() {
const { clients } = useClients(); const { clients } = useClients();
const { projects } = useProjects(); const { projects } = useProjects();
const filteredProjects = filters.clientId
? (projects ?? []).filter((p) => p.clientId === filters.clientId)
: projects;
const handleFilterChange = ( const handleFilterChange = (
key: keyof StatisticsFilters, key: keyof StatisticsFilters,
value: string | undefined, value: string | undefined,
) => { ) => {
setFilters((prev) => ({ setFilters((prev) => {
...prev, const next = { ...prev, [key]: value || undefined };
[key]: value || undefined, // When client changes, clear any project selection that may belong to a different client
})); if (key === 'clientId') next.projectId = undefined;
return next;
});
}; };
const clearFilters = () => { const clearFilters = () => {
@@ -132,7 +138,7 @@ export function StatisticsPage() {
className="input" className="input"
> >
<option value="">All Projects</option> <option value="">All Projects</option>
{projects?.map((project) => ( {filteredProjects?.map((project) => (
<option key={project.id} value={project.id}> <option key={project.id} value={project.id}>
{project.name} {project.name}
</option> </option>

View File

@@ -1,6 +1,7 @@
export interface User { export interface User {
id: string; id: string;
username: string; username: string;
fullName: string | null;
email: string; email: string;
} }