Add optional description field for time entries and improve button accessibility

Update API and frontend to include an optional description field for time entries and move the quick entry button to the sidebar for better visibility.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 55837015-10e9-4be9-b857-7f5e6be73772
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 734a5162-3dd4-4103-b626-ee12b22fd002
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/1cc377db-7ea0-49f2-97ce-c3e87e0228cc/55837015-10e9-4be9-b857-7f5e6be73772/1SIrmNK
Replit-Helium-Checkpoint-Created: true
This commit is contained in:
SylvainP1 2026-04-14 08:19:30 +00:00
parent 7707be4eab
commit 1df1e34c21
9 changed files with 45 additions and 14 deletions

View File

@ -15,7 +15,7 @@ router.post("/quick-entry", async (req, res): Promise<void> => {
return;
}
const { projectId, hours, collaborator } = parsed.data;
const { projectId, hours, collaborator, description } = parsed.data;
const rawDate = parsed.data.date;
const dateStr = rawDate instanceof Date
? rawDate.toISOString().split("T")[0]
@ -87,14 +87,14 @@ router.post("/quick-entry", async (req, res): Promise<void> => {
} else {
[entry] = await db
.update(timeEntriesTable)
.set({ hours })
.set({ hours, description: description ?? existing.description })
.where(eq(timeEntriesTable.id, existing.id))
.returning();
}
} else if (hours > 0) {
[entry] = await db
.insert(timeEntriesTable)
.values({ timesheetLineId: line.id, date: dateStr, hours })
.values({ timesheetLineId: line.id, date: dateStr, hours, description: description ?? null })
.returning();
} else {
entry = { id: 0, timesheetLineId: line.id, date: dateStr, hours: 0 };

View File

@ -1,17 +1,13 @@
import { AppSidebar } from "./sidebar";
import { QuickEntryButton } from "@/components/quick-entry";
export function AppLayout({ children }: { children: React.ReactNode }) {
return (
<div className="flex h-screen w-full bg-background overflow-hidden">
<AppSidebar />
<main className="flex-1 flex flex-col min-w-0 overflow-y-auto relative">
<main className="flex-1 flex flex-col min-w-0 overflow-y-auto">
<div className="flex-1 p-8 container mx-auto max-w-7xl">
{children}
</div>
<div className="fixed bottom-6 right-6 z-50">
<QuickEntryButton />
</div>
</main>
</div>
);

View File

@ -1,6 +1,7 @@
import { Link, useLocation } from "wouter";
import { LayoutDashboard, Clock, FolderKanban, Settings } from "lucide-react";
import { LayoutDashboard, Clock, FolderKanban, Zap } from "lucide-react";
import { cn } from "@/lib/utils";
import { QuickEntryButton } from "@/components/quick-entry";
const navItems = [
{
@ -51,6 +52,10 @@ export function AppSidebar() {
</Link>
);
})}
<div className="pt-3">
<QuickEntryButton />
</div>
</nav>
<div className="p-4 border-t border-sidebar-border">

View File

@ -25,6 +25,7 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Textarea } from "@/components/ui/textarea";
import { useToast } from "@/hooks/use-toast";
import { Zap, Clock, Check } from "lucide-react";
import { format } from "date-fns";
@ -38,14 +39,14 @@ export function QuickEntryButton() {
return (
<>
<Button
<button
onClick={() => setOpen(true)}
className="gap-2 bg-primary shadow-lg hover:shadow-xl transition-all"
className="flex items-center gap-3 rounded-md px-3 py-2.5 text-sm font-medium transition-colors cursor-pointer w-full bg-primary/10 text-primary hover:bg-primary/20"
data-testid="button-quick-entry"
>
<Zap className="h-4 w-4" />
Saisie rapide
</Button>
</button>
<QuickEntryDialog open={open} onOpenChange={setOpen} />
</>
);
@ -61,6 +62,7 @@ function QuickEntryDialog({
const [projectId, setProjectId] = useState<string>("");
const [date, setDate] = useState(format(new Date(), "yyyy-MM-dd"));
const [hours, setHours] = useState<number>(1);
const [description, setDescription] = useState("");
const [success, setSuccess] = useState(false);
const [lastEntry, setLastEntry] = useState<{
projectName: string;
@ -92,6 +94,7 @@ function QuickEntryDialog({
date,
hours,
collaborator: COLLABORATOR,
description: description.trim() || undefined,
},
},
{
@ -133,6 +136,7 @@ function QuickEntryDialog({
setSuccess(false);
setProjectId("");
setHours(1);
setDescription("");
setDate(format(new Date(), "yyyy-MM-dd"));
onOpenChange(false);
};
@ -141,6 +145,7 @@ function QuickEntryDialog({
setSuccess(false);
setProjectId("");
setHours(1);
setDescription("");
};
const activeProjects = projects?.filter((p) => p.isActive) ?? [];
@ -258,9 +263,22 @@ function QuickEntryDialog({
</div>
</div>
<div className="space-y-2">
<label className="text-sm font-medium">
Description <span className="text-muted-foreground font-normal">(optionnel)</span>
</label>
<Textarea
value={description}
onChange={(e) => setDescription(e.target.value)}
placeholder="Ex: Développement de la page d'accueil, réunion client..."
className="resize-none h-20"
data-testid="input-quick-description"
/>
</div>
{selectedProject && (
<div className="bg-muted/50 rounded-lg p-3 text-sm">
<p className="text-muted-foreground">Récapitulatif:</p>
<p className="text-muted-foreground">Récapitulatif :</p>
<p className="font-medium mt-1">
{hours}h sur{" "}
<Badge variant="secondary" className="font-mono text-xs">
@ -271,6 +289,11 @@ function QuickEntryDialog({
<p className="text-muted-foreground capitalize">
{formattedDate}
</p>
{description.trim() && (
<p className="text-muted-foreground mt-1 italic">
« {description.trim()} »
</p>
)}
</div>
)}
</div>

View File

@ -181,6 +181,7 @@ export interface QuickAddTimeBody {
date: string;
hours: number;
collaborator: string;
description?: string | null;
}
export interface QuickAddTimeResponse {

View File

@ -727,6 +727,9 @@ components:
type: number
collaborator:
type: string
description:
type: string
nullable: true
required:
- projectId
- date

View File

@ -265,6 +265,7 @@ export const QuickAddTimeBody = zod.object({
date: zod.coerce.date(),
hours: zod.number(),
collaborator: zod.string(),
description: zod.string().nullish(),
});
export const QuickAddTimeResponse = zod.object({

View File

@ -11,4 +11,5 @@ export interface QuickAddTimeBody {
date: Date;
hours: number;
collaborator: string;
description?: string | null;
}

View File

@ -1,4 +1,4 @@
import { pgTable, serial, integer, real, date } from "drizzle-orm/pg-core";
import { pgTable, serial, integer, real, date, text } from "drizzle-orm/pg-core";
import { createInsertSchema } from "drizzle-zod";
import { z } from "zod/v4";
import { timesheetLinesTable } from "./timesheetLines";
@ -8,6 +8,7 @@ export const timeEntriesTable = pgTable("time_entries", {
timesheetLineId: integer("timesheet_line_id").notNull().references(() => timesheetLinesTable.id, { onDelete: "cascade" }),
date: date("date").notNull(),
hours: real("hours").notNull().default(0),
description: text("description"),
});
export const insertTimeEntrySchema = createInsertSchema(timeEntriesTable).omit({ id: true });