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;
|
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 };
|
||||||
|
|||||||
@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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">
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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({
|
||||||
|
|||||||
@ -11,4 +11,5 @@ export interface QuickAddTimeBody {
|
|||||||
date: Date;
|
date: Date;
|
||||||
hours: number;
|
hours: number;
|
||||||
collaborator: string;
|
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 { 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 });
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user