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:
parent
7707be4eab
commit
1df1e34c21
@ -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 };
|
||||
|
||||
@ -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>
|
||||
);
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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,6 +263,19 @@ 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>
|
||||
@ -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>
|
||||
|
||||
@ -181,6 +181,7 @@ export interface QuickAddTimeBody {
|
||||
date: string;
|
||||
hours: number;
|
||||
collaborator: string;
|
||||
description?: string | null;
|
||||
}
|
||||
|
||||
export interface QuickAddTimeResponse {
|
||||
|
||||
@ -727,6 +727,9 @@ components:
|
||||
type: number
|
||||
collaborator:
|
||||
type: string
|
||||
description:
|
||||
type: string
|
||||
nullable: true
|
||||
required:
|
||||
- projectId
|
||||
- date
|
||||
|
||||
@ -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({
|
||||
|
||||
@ -11,4 +11,5 @@ export interface QuickAddTimeBody {
|
||||
date: Date;
|
||||
hours: number;
|
||||
collaborator: string;
|
||||
description?: string | null;
|
||||
}
|
||||
|
||||
@ -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 });
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user