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

View File

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

View File

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

View File

@ -25,6 +25,7 @@ import {
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from "@/components/ui/select"; } from "@/components/ui/select";
import { Textarea } from "@/components/ui/textarea";
import { useToast } from "@/hooks/use-toast"; import { useToast } from "@/hooks/use-toast";
import { Zap, Clock, Check } from "lucide-react"; import { Zap, Clock, Check } from "lucide-react";
import { format } from "date-fns"; import { format } from "date-fns";
@ -38,14 +39,14 @@ export function QuickEntryButton() {
return ( return (
<> <>
<Button <button
onClick={() => setOpen(true)} 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" data-testid="button-quick-entry"
> >
<Zap className="h-4 w-4" /> <Zap className="h-4 w-4" />
Saisie rapide Saisie rapide
</Button> </button>
<QuickEntryDialog open={open} onOpenChange={setOpen} /> <QuickEntryDialog open={open} onOpenChange={setOpen} />
</> </>
); );
@ -61,6 +62,7 @@ function QuickEntryDialog({
const [projectId, setProjectId] = useState<string>(""); const [projectId, setProjectId] = useState<string>("");
const [date, setDate] = useState(format(new Date(), "yyyy-MM-dd")); const [date, setDate] = useState(format(new Date(), "yyyy-MM-dd"));
const [hours, setHours] = useState<number>(1); const [hours, setHours] = useState<number>(1);
const [description, setDescription] = useState("");
const [success, setSuccess] = useState(false); const [success, setSuccess] = useState(false);
const [lastEntry, setLastEntry] = useState<{ const [lastEntry, setLastEntry] = useState<{
projectName: string; projectName: string;
@ -92,6 +94,7 @@ function QuickEntryDialog({
date, date,
hours, hours,
collaborator: COLLABORATOR, collaborator: COLLABORATOR,
description: description.trim() || undefined,
}, },
}, },
{ {
@ -133,6 +136,7 @@ function QuickEntryDialog({
setSuccess(false); setSuccess(false);
setProjectId(""); setProjectId("");
setHours(1); setHours(1);
setDescription("");
setDate(format(new Date(), "yyyy-MM-dd")); setDate(format(new Date(), "yyyy-MM-dd"));
onOpenChange(false); onOpenChange(false);
}; };
@ -141,6 +145,7 @@ function QuickEntryDialog({
setSuccess(false); setSuccess(false);
setProjectId(""); setProjectId("");
setHours(1); setHours(1);
setDescription("");
}; };
const activeProjects = projects?.filter((p) => p.isActive) ?? []; const activeProjects = projects?.filter((p) => p.isActive) ?? [];
@ -258,9 +263,22 @@ function QuickEntryDialog({
</div> </div>
</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 && ( {selectedProject && (
<div className="bg-muted/50 rounded-lg p-3 text-sm"> <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"> <p className="font-medium mt-1">
{hours}h sur{" "} {hours}h sur{" "}
<Badge variant="secondary" className="font-mono text-xs"> <Badge variant="secondary" className="font-mono text-xs">
@ -271,6 +289,11 @@ function QuickEntryDialog({
<p className="text-muted-foreground capitalize"> <p className="text-muted-foreground capitalize">
{formattedDate} {formattedDate}
</p> </p>
{description.trim() && (
<p className="text-muted-foreground mt-1 italic">
« {description.trim()} »
</p>
)}
</div> </div>
)} )}
</div> </div>

View File

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

View File

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

View File

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

View File

@ -11,4 +11,5 @@ export interface QuickAddTimeBody {
date: Date; date: Date;
hours: number; hours: number;
collaborator: string; 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 { createInsertSchema } from "drizzle-zod";
import { z } from "zod/v4"; import { z } from "zod/v4";
import { timesheetLinesTable } from "./timesheetLines"; 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" }), timesheetLineId: integer("timesheet_line_id").notNull().references(() => timesheetLinesTable.id, { onDelete: "cascade" }),
date: date("date").notNull(), date: date("date").notNull(),
hours: real("hours").notNull().default(0), hours: real("hours").notNull().default(0),
description: text("description"),
}); });
export const insertTimeEntrySchema = createInsertSchema(timeEntriesTable).omit({ id: true }); export const insertTimeEntrySchema = createInsertSchema(timeEntriesTable).omit({ id: true });