From 4ac0fd22e5824d350f97d4d5c24949f5c16bf951 Mon Sep 17 00:00:00 2001 From: Amirhossein Khalili Date: Wed, 29 Apr 2026 12:17:08 +0330 Subject: [PATCH] feat(timesheet): improve empty state copy and layout --- src/locales/en.ts | 10 +- src/locales/fa.ts | 2 + src/pages/Timesheet.tsx | 963 ++++++++++++++++++++-------------------- 3 files changed, 497 insertions(+), 478 deletions(-) diff --git a/src/locales/en.ts b/src/locales/en.ts index 8582b7a..df3a35c 100644 --- a/src/locales/en.ts +++ b/src/locales/en.ts @@ -408,10 +408,12 @@ export const en = { runningBadge: "Running", noRunningEntry: "No running entry", searchPlaceholder: "Search time entries...", - orderingNewest: "Newest first", - orderingOldest: "Oldest first", - emptyState: "No time entries found", - emptyDescription: "No description", + orderingNewest: "Newest first", + orderingOldest: "Oldest first", + emptyState: "No time entries found", + emptyStateDescription: "Start the timer or add a manual entry to get started.", + noEntriesSearch: "Try adjusting your search query or filters.", + emptyDescription: "No description", createTitle: "Add Time Entry", startTitle: "Start Timer", editTitle: "Edit Time Entry", diff --git a/src/locales/fa.ts b/src/locales/fa.ts index e7f0de0..d194758 100644 --- a/src/locales/fa.ts +++ b/src/locales/fa.ts @@ -409,6 +409,8 @@ export const fa = { orderingOldest: "قدیمی‌ترین", emptyState: "ورودی زمانی یافت نشد", emptyDescription: "بدون توضیح", + emptyStateDescription: "برای شروع، تایمر را اجرا کنید یا یک ورودی دستی اضافه کنید.", + noEntriesSearch: "عبارت جست‌وجو یا فیلترهای خود را تغییر دهید.", createTitle: "افزودن ورودی زمان", startTitle: "شروع تایمر", editTitle: "ویرایش ورودی زمان", diff --git a/src/pages/Timesheet.tsx b/src/pages/Timesheet.tsx index 07f92f2..4696294 100644 --- a/src/pages/Timesheet.tsx +++ b/src/pages/Timesheet.tsx @@ -1,6 +1,6 @@ -import { useCallback, useEffect, useMemo, useRef, useState } from "react"; -import { createPortal } from "react-dom"; -import { useSearchParams } from "react-router-dom"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { createPortal } from "react-dom"; +import { useSearchParams } from "react-router-dom"; import { CalendarDays, Check, ChevronDown, Clock3, DollarSign, MoreVertical, Pencil, Play, Plus, Search, Square, Tag as TagIcon, Trash2 } from "lucide-react"; import { toast } from "sonner"; @@ -17,20 +17,21 @@ import { updateTimeEntry, } from "../api/timeEntries"; import { getTags, type Tag } from "../api/tags"; -import { Modal } from "../components/Modal"; -import { InfiniteScroll } from "../components/InfiniteScroll"; -import TimesheetFilterBar, { type TimeEntryFilters } from "../components/timesheet/TimesheetFilterBar"; -import JalaliDatePicker from "../components/ui/JalaliDatePicker"; -import { Button } from "../components/ui/button"; -import { Input } from "../components/ui/input"; -import { SearchableSelect } from "../components/ui/SearchableSelect"; -import { useWorkspace } from "../context/WorkspaceContext"; -import { useTranslation } from "../hooks/useTranslation"; -import { - readArrayParam, - readStringParam, - updateQueryParams, -} from "../lib/queryParams"; +import { Modal } from "../components/Modal"; +import EmptyStateCard from "../components/EmptyStateCard"; +import { InfiniteScroll } from "../components/InfiniteScroll"; +import TimesheetFilterBar, { type TimeEntryFilters } from "../components/timesheet/TimesheetFilterBar"; +import JalaliDatePicker from "../components/ui/JalaliDatePicker"; +import { Button } from "../components/ui/button"; +import { Input } from "../components/ui/input"; +import { SearchableSelect } from "../components/ui/SearchableSelect"; +import { useWorkspace } from "../context/WorkspaceContext"; +import { useTranslation } from "../hooks/useTranslation"; +import { + readArrayParam, + readStringParam, + updateQueryParams, +} from "../lib/queryParams"; type EntryModalMode = "manual" | "edit" | null; @@ -864,35 +865,35 @@ function TagMultiSelect({ ); } -function ProjectInlineSelect({ - projects, - value, - onChange, - placeholder, - searchPlaceholder, - emptyLabel, - portalOwnerId, - className = "", - dropdownClassName = "", - disabled = false, -}: { - projects: Project[]; - value: string; - onChange: (projectId: string) => void; - placeholder: string; - searchPlaceholder: string; - emptyLabel: string; - portalOwnerId?: string; - className?: string; - dropdownClassName?: string; - disabled?: boolean; -}) { - const [isOpen, setIsOpen] = useState(false); - const [query, setQuery] = useState(""); - const wrapperRef = useRef(null); - const buttonRef = useRef(null); - const dropdownRef = useRef(null); - const [dropdownStyle, setDropdownStyle] = useState({}); +function ProjectInlineSelect({ + projects, + value, + onChange, + placeholder, + searchPlaceholder, + emptyLabel, + portalOwnerId, + className = "", + dropdownClassName = "", + disabled = false, +}: { + projects: Project[]; + value: string; + onChange: (projectId: string) => void; + placeholder: string; + searchPlaceholder: string; + emptyLabel: string; + portalOwnerId?: string; + className?: string; + dropdownClassName?: string; + disabled?: boolean; +}) { + const [isOpen, setIsOpen] = useState(false); + const [query, setQuery] = useState(""); + const wrapperRef = useRef(null); + const buttonRef = useRef(null); + const dropdownRef = useRef(null); + const [dropdownStyle, setDropdownStyle] = useState({}); useEffect(() => { if (!isOpen) return; @@ -907,9 +908,9 @@ function ProjectInlineSelect({ } }; - document.addEventListener("mousedown", handleClickOutside); - return () => document.removeEventListener("mousedown", handleClickOutside); - }, [isOpen]); + document.addEventListener("mousedown", handleClickOutside); + return () => document.removeEventListener("mousedown", handleClickOutside); + }, [isOpen]); useEffect(() => { if (!isOpen || !buttonRef.current) return; @@ -939,27 +940,27 @@ function ProjectInlineSelect({ return () => { window.removeEventListener("resize", closeOnViewportChange); window.removeEventListener("scroll", closeOnViewportChange, true); - }; - }, [isOpen]); - - useEffect(() => { - if (!isOpen) { - setQuery(""); - } - }, [isOpen]); - - const selectedProject = projects.find((project) => project.id === value); - const label = selectedProject?.name || placeholder; - const filteredProjects = useMemo(() => { - const needle = query.trim().toLowerCase(); - if (!needle) return projects; - return projects.filter((project) => { - const clientName = project.client?.name || ""; - return `${project.name} ${clientName}`.toLowerCase().includes(needle); - }); - }, [projects, query]); - - return ( + }; + }, [isOpen]); + + useEffect(() => { + if (!isOpen) { + setQuery(""); + } + }, [isOpen]); + + const selectedProject = projects.find((project) => project.id === value); + const label = selectedProject?.name || placeholder; + const filteredProjects = useMemo(() => { + const needle = query.trim().toLowerCase(); + if (!needle) return projects; + return projects.filter((project) => { + const clientName = project.client?.name || ""; + return `${project.name} ${clientName}`.toLowerCase().includes(needle); + }); + }, [projects, query]); + + return (
- {filteredProjects.map((project) => { - const selected = project.id === value; - const unavailable = Boolean(project.is_deleted) && !selected; - return ( + {filteredProjects.map((project) => { + const selected = project.id === value; + const unavailable = Boolean(project.is_deleted) && !selected; + return ( - ); - })} - {filteredProjects.length === 0 && ( -
{emptyLabel}
- )} -
- , - document.body, + title={project.name} + > + {project.name} + + ); + })} + {filteredProjects.length === 0 && ( +
{emptyLabel}
+ )} + + , + document.body, )} ); @@ -1269,16 +1270,16 @@ function EntryEditorFields({
- (onProjectChange ? onProjectChange(projectId) : onChange({ projectId }))} - placeholder={t.timesheet?.projectLabel || "Project"} - searchPlaceholder={t.timesheet?.searchProjectsLabel || t.projects?.searchPlaceholder || "Search projects..."} - emptyLabel={t.timesheet?.noProjectsFoundLabel || "No projects found."} - portalOwnerId={portalOwnerId} - className="min-w-0 max-w-fit flex-1" - /> + (onProjectChange ? onProjectChange(projectId) : onChange({ projectId }))} + placeholder={t.timesheet?.projectLabel || "Project"} + searchPlaceholder={t.timesheet?.searchProjectsLabel || t.projects?.searchPlaceholder || "Search projects..."} + emptyLabel={t.timesheet?.noProjectsFoundLabel || "No projects found."} + portalOwnerId={portalOwnerId} + className="min-w-0 max-w-fit flex-1" + /> {selectedProject?.client?.name && ( @@ -1349,28 +1350,28 @@ function EntryEditorFields({ />
-
- - onChange({ projectId: String(value) })} - options={[ - { value: "", label: t.timesheet?.noProject || "No project" }, - ...projects.map((project) => ({ - value: project.id, - label: project.name, - searchText: project.client?.name || "", - })), - ]} - placeholder={t.timesheet?.noProject || "No project"} - searchPlaceholder={t.timesheet?.searchProjectsLabel || t.projects?.searchPlaceholder || "Search projects..."} - emptyLabel={t.timesheet?.noProjectsFoundLabel || "No projects found."} - className="w-full" - buttonClassName={compact ? "w-full h-9 px-2 text-xs" : "w-full"} - /> -
+
+ + onChange({ projectId: String(value) })} + options={[ + { value: "", label: t.timesheet?.noProject || "No project" }, + ...projects.map((project) => ({ + value: project.id, + label: project.name, + searchText: project.client?.name || "", + })), + ]} + placeholder={t.timesheet?.noProject || "No project"} + searchPlaceholder={t.timesheet?.searchProjectsLabel || t.projects?.searchPlaceholder || "Search projects..."} + emptyLabel={t.timesheet?.noProjectsFoundLabel || "No projects found."} + className="w-full" + buttonClassName={compact ? "w-full h-9 px-2 text-xs" : "w-full"} + /> +
@@ -1589,7 +1590,7 @@ function RecordedEntryCard({
-
+
{formatDateTime(entry.start_time, lang)}
@@ -1640,7 +1641,7 @@ function RecordedEntryCard({ } return ( -
+
-
+
{formatDuration(entry)}
-
+
onDelete(entry)} />
@@ -1682,7 +1683,7 @@ function RecordedEntryCard({ ); } -function MobileRecordedEntryCard({ +function MobileRecordedEntryCard({ entry, t, projects, @@ -1797,7 +1798,7 @@ function MobileRecordedEntryCard({ }; return ( -
+
@@ -1806,7 +1807,7 @@ function MobileRecordedEntryCard({
- ); -} - -function TimesheetSkeleton({ loadingLabel }: { loadingLabel: string }) { - return ( -
-
-
-
-
-
-
-
-
-
-
- -
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- -
-
-
-
-
-
-
-
-
-
-
-
-
-
- -
- - {loadingLabel} -
- -
- {[0, 1].map((weekIndex) => ( -
-
-
-
-
- {[0, 1].map((dayIndex) => ( -
-
-
-
-
-
- {[0, 1].map((entryIndex) => ( -
-
-
-
-
-
-
-
- ))} -
-
- ))} -
- ))} -
-
- ); -} - -export default function Timesheet() { - const { t, lang } = useTranslation(); - const { activeWorkspace } = useWorkspace(); - const [searchParams, setSearchParams] = useSearchParams(); - const isRtl = lang === "fa"; + ); +} + +function TimesheetSkeleton({ loadingLabel }: { loadingLabel: string }) { + return ( +
+
+
+
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+ + {loadingLabel} +
+ +
+ {[0, 1].map((weekIndex) => ( +
+
+
+
+
+ {[0, 1].map((dayIndex) => ( +
+
+
+
+
+
+ {[0, 1].map((entryIndex) => ( +
+
+
+
+
+
+
+
+ ))} +
+
+ ))} +
+ ))} +
+
+ ); +} + +export default function Timesheet() { + const { t, lang } = useTranslation(); + const { activeWorkspace } = useWorkspace(); + const [searchParams, setSearchParams] = useSearchParams(); + const isRtl = lang === "fa"; const extendedTimesheet = (t.timesheet as { deleteTitle?: string; deleteConfirmMessage?: string; @@ -2034,39 +2035,47 @@ export default function Timesheet() { hideFiltersLabel?: string; applyFiltersLabel?: string; clientFilterPrefix?: string; - projectFilterPrefix?: string; - tagFilterPrefix?: string; - fromFilterPrefix?: string; - toFilterPrefix?: string; - restartConfirmMessage?: string; - discardConfirmMessage?: string; - deletedProjectLabel?: string; - deletedTagLabel?: string; - searchTagsLabel?: string; - noTagsFoundLabel?: string; - searchProjectsLabel?: string; - noProjectsFoundLabel?: string; - }) || {}; + projectFilterPrefix?: string; + tagFilterPrefix?: string; + fromFilterPrefix?: string; + toFilterPrefix?: string; + restartConfirmMessage?: string; + discardConfirmMessage?: string; + deletedProjectLabel?: string; + deletedTagLabel?: string; + searchTagsLabel?: string; + noTagsFoundLabel?: string; + searchProjectsLabel?: string; + noProjectsFoundLabel?: string; + }) || {}; const [projects, setProjects] = useState([]); const [tags, setTags] = useState([]); - const [groupedHistory, setGroupedHistory] = useState([]); - const [activeRunningEntry, setActiveRunningEntry] = useState(null); - const [isLoading, setIsLoading] = useState(false); - const [isLoadingMore, setIsLoadingMore] = useState(false); - const [debouncedSearchQuery, setDebouncedSearchQuery] = useState(""); - const searchQuery = readStringParam(searchParams, "search", ""); - const filters = useMemo( - () => ({ - projectId: readStringParam(searchParams, "project", DEFAULT_ENTRY_FILTERS.projectId), - clientId: readStringParam(searchParams, "client", DEFAULT_ENTRY_FILTERS.clientId), - tagIds: readArrayParam(searchParams, "tags"), - startedAfter: readStringParam(searchParams, "from", DEFAULT_ENTRY_FILTERS.startedAfter), - startedBefore: readStringParam(searchParams, "to", DEFAULT_ENTRY_FILTERS.startedBefore), - }), - [searchParams], - ); - const [hasMoreHistory, setHasMoreHistory] = useState(false); + const [groupedHistory, setGroupedHistory] = useState([]); + const [activeRunningEntry, setActiveRunningEntry] = useState(null); + const [isLoading, setIsLoading] = useState(false); + const [isLoadingMore, setIsLoadingMore] = useState(false); + const [debouncedSearchQuery, setDebouncedSearchQuery] = useState(""); + const searchQuery = readStringParam(searchParams, "search", ""); + const filters = useMemo( + () => ({ + projectId: readStringParam(searchParams, "project", DEFAULT_ENTRY_FILTERS.projectId), + clientId: readStringParam(searchParams, "client", DEFAULT_ENTRY_FILTERS.clientId), + tagIds: readArrayParam(searchParams, "tags"), + startedAfter: readStringParam(searchParams, "from", DEFAULT_ENTRY_FILTERS.startedAfter), + startedBefore: readStringParam(searchParams, "to", DEFAULT_ENTRY_FILTERS.startedBefore), + }), + [searchParams], + ); + const hasActiveHistoryFilters = Boolean( + searchQuery || + filters.projectId || + filters.clientId || + filters.tagIds.length || + filters.startedAfter || + filters.startedBefore, + ); + const [hasMoreHistory, setHasMoreHistory] = useState(false); const [nextOffset, setNextOffset] = useState(0); const [limit] = useState(20); const [ticker, setTicker] = useState(Date.now()); @@ -2133,41 +2142,41 @@ export default function Timesheet() { const loadOptions = async () => { try { - const [projectsData, tagsData] = await Promise.all([ - getProjects(activeWorkspace.id, { limit: 100, offset: 0, ordering: "name" }), - getTags(activeWorkspace.id, { limit: 100, offset: 0, ordering: "name" }), - ]); - - setProjects((projectsData.results || []).filter((project: Project) => !project.is_archived)); - setTags(tagsData.results || []); - } catch (error) { - console.error(error); - toast.error(t.timesheet?.optionsError || "Failed to load projects and tags"); - } + const [projectsData, tagsData] = await Promise.all([ + getProjects(activeWorkspace.id, { limit: 100, offset: 0, ordering: "name" }), + getTags(activeWorkspace.id, { limit: 100, offset: 0, ordering: "name" }), + ]); + + setProjects((projectsData.results || []).filter((project: Project) => !project.is_archived)); + setTags(tagsData.results || []); + } catch (error) { + console.error(error); + toast.error(t.timesheet?.optionsError || "Failed to load projects and tags"); + } }; void loadOptions(); }, [activeWorkspace?.id, t.timesheet?.optionsError]); - useEffect(() => { - setGroupedHistory([]); - setNextOffset(0); - setHasMoreHistory(false); - }, [activeWorkspace?.id]); - - useEffect(() => { - const timeoutId = window.setTimeout(() => { - setDebouncedSearchQuery(searchQuery); - }, 350); - - return () => window.clearTimeout(timeoutId); - }, [searchQuery]); - - useEffect(() => { - setGroupedHistory([]); - setNextOffset(0); - setHasMoreHistory(false); - }, [debouncedSearchQuery, filters]); + useEffect(() => { + setGroupedHistory([]); + setNextOffset(0); + setHasMoreHistory(false); + }, [activeWorkspace?.id]); + + useEffect(() => { + const timeoutId = window.setTimeout(() => { + setDebouncedSearchQuery(searchQuery); + }, 350); + + return () => window.clearTimeout(timeoutId); + }, [searchQuery]); + + useEffect(() => { + setGroupedHistory([]); + setNextOffset(0); + setHasMoreHistory(false); + }, [debouncedSearchQuery, filters]); useEffect(() => { if (!filters.clientId || !filters.projectId) return; @@ -2176,14 +2185,14 @@ export default function Timesheet() { (project) => project.id === filters.projectId && project.client?.id === filters.clientId, ); - if (!projectStillMatchesClient) { - setSearchParams( - (current) => - updateQueryParams(current, { project: "" }, { project: "" }), - { replace: true }, - ); - } - }, [filters.clientId, filters.projectId, projects, setSearchParams]); + if (!projectStillMatchesClient) { + setSearchParams( + (current) => + updateQueryParams(current, { project: "" }, { project: "" }), + { replace: true }, + ); + } + }, [filters.clientId, filters.projectId, projects, setSearchParams]); const loadHistory = useCallback(async ({ offset = 0, append = false }: { offset?: number; append?: boolean } = {}) => { if (!activeWorkspace?.id) return; @@ -2194,11 +2203,11 @@ export default function Timesheet() { } else { setIsLoading(true); } - const params: TimeEntryListParams = { - limit, - offset, - search: debouncedSearchQuery, - status: "ended", + const params: TimeEntryListParams = { + limit, + offset, + search: debouncedSearchQuery, + status: "ended", project: filters.projectId || undefined, client: filters.clientId || undefined, tags: filters.tagIds, @@ -2219,7 +2228,7 @@ export default function Timesheet() { setIsLoading(false); } } - }, [activeWorkspace?.id, debouncedSearchQuery, filters, limit, t.timesheet?.fetchError]); + }, [activeWorkspace?.id, debouncedSearchQuery, filters, limit, t.timesheet?.fetchError]); const loadRunningEntry = useCallback(async () => { if (!activeWorkspace?.id) { @@ -2240,15 +2249,15 @@ export default function Timesheet() { } }, [activeWorkspace?.id]); - useEffect(() => { - if (!activeWorkspace?.id) return; - - const timeoutId = window.setTimeout(() => { - void loadHistory(); - }, 250); - - return () => window.clearTimeout(timeoutId); - }, [activeWorkspace?.id, debouncedSearchQuery, limit, loadHistory, filters]); + useEffect(() => { + if (!activeWorkspace?.id) return; + + const timeoutId = window.setTimeout(() => { + void loadHistory(); + }, 250); + + return () => window.clearTimeout(timeoutId); + }, [activeWorkspace?.id, debouncedSearchQuery, limit, loadHistory, filters]); useEffect(() => { void loadRunningEntry(); @@ -2539,54 +2548,54 @@ export default function Timesheet() { } }, []); - const handleApplyFilters = useCallback((nextFilters: TimeEntryFilters) => { - setSearchParams( - (current) => - updateQueryParams( - current, - { - project: nextFilters.projectId, - client: nextFilters.clientId, - tags: nextFilters.tagIds, - from: nextFilters.startedAfter, - to: nextFilters.startedBefore, - }, - { - project: "", - client: "", - from: "", - to: "", - }, - ), - { replace: true }, - ); - }, [setSearchParams]); - - const handleClearFilters = useCallback(() => { - setSearchParams( - (current) => - updateQueryParams( - current, - { - search: "", - project: "", - client: "", - tags: [], - from: "", - to: "", - }, - { - search: "", - project: "", - client: "", - from: "", - to: "", - }, - ), - { replace: true }, - ); - setDebouncedSearchQuery(""); - }, [setSearchParams]); + const handleApplyFilters = useCallback((nextFilters: TimeEntryFilters) => { + setSearchParams( + (current) => + updateQueryParams( + current, + { + project: nextFilters.projectId, + client: nextFilters.clientId, + tags: nextFilters.tagIds, + from: nextFilters.startedAfter, + to: nextFilters.startedBefore, + }, + { + project: "", + client: "", + from: "", + to: "", + }, + ), + { replace: true }, + ); + }, [setSearchParams]); + + const handleClearFilters = useCallback(() => { + setSearchParams( + (current) => + updateQueryParams( + current, + { + search: "", + project: "", + client: "", + tags: [], + from: "", + to: "", + }, + { + search: "", + project: "", + client: "", + from: "", + to: "", + }, + ), + { replace: true }, + ); + setDebouncedSearchQuery(""); + }, [setSearchParams]); const handleLoadMore = useCallback(() => { if (!hasMoreHistory || nextOffset === null || isLoadingMore) return; @@ -2620,7 +2629,7 @@ export default function Timesheet() { } return ( -
+

{t.timesheet?.title || 'Timesheets'}

@@ -2629,9 +2638,9 @@ export default function Timesheet() {
@@ -2644,26 +2653,26 @@ export default function Timesheet() { />
-
- setTimerDraft((current) => ({ ...current, projectId: String(value) }))} - options={[ - { value: "", label: t.timesheet?.projectLabel || "Project" }, - ...runningTimerProjects.map((project) => ({ - value: project.id, - label: project.name, - searchText: project.client?.name || "", - })), - ]} - placeholder={t.timesheet?.projectLabel || "Project"} - searchPlaceholder={extendedTimesheet.searchProjectsLabel || t.projects?.searchPlaceholder || "Search projects..."} - emptyLabel={extendedTimesheet.noProjectsFoundLabel || "No projects found."} - className="min-w-[190px] max-w-[220px]" - buttonClassName="h-12 w-full rounded-none border-0 bg-transparent px-3 text-sm text-sky-600 shadow-none outline-none dark:bg-transparent dark:text-sky-400 focus:ring-0 focus-visible:ring-0 focus-visible:ring-offset-0" - disabled={isStartingTimer} - /> -
+
+ setTimerDraft((current) => ({ ...current, projectId: String(value) }))} + options={[ + { value: "", label: t.timesheet?.projectLabel || "Project" }, + ...runningTimerProjects.map((project) => ({ + value: project.id, + label: project.name, + searchText: project.client?.name || "", + })), + ]} + placeholder={t.timesheet?.projectLabel || "Project"} + searchPlaceholder={extendedTimesheet.searchProjectsLabel || t.projects?.searchPlaceholder || "Search projects..."} + emptyLabel={extendedTimesheet.noProjectsFoundLabel || "No projects found."} + className="min-w-[190px] max-w-[220px]" + buttonClassName="h-12 w-full rounded-none border-0 bg-transparent px-3 text-sm text-sky-600 shadow-none outline-none dark:bg-transparent dark:text-sky-400 focus:ring-0 focus-visible:ring-0 focus-visible:ring-offset-0" + disabled={isStartingTimer} + /> +
-
- setTimerDraft((current) => ({ ...current, projectId: String(value) }))} - options={[ - { value: "", label: t.timesheet?.projectLabel || "Project" }, - ...runningTimerProjects.map((project) => ({ - value: project.id, - label: project.name, - searchText: project.client?.name || "", - })), - ]} - placeholder={t.timesheet?.projectLabel || "Project"} - searchPlaceholder={extendedTimesheet.searchProjectsLabel || t.projects?.searchPlaceholder || "Search projects..."} - emptyLabel={extendedTimesheet.noProjectsFoundLabel || "No projects found."} - className="w-full" - buttonClassName="h-10 w-full rounded-md border border-slate-200 bg-slate-50 px-3 text-sm shadow-none outline-none dark:border-slate-700 dark:bg-slate-900 focus:ring-0 focus-visible:ring-0 focus-visible:ring-offset-0" - disabled={isStartingTimer} - /> +
+ setTimerDraft((current) => ({ ...current, projectId: String(value) }))} + options={[ + { value: "", label: t.timesheet?.projectLabel || "Project" }, + ...runningTimerProjects.map((project) => ({ + value: project.id, + label: project.name, + searchText: project.client?.name || "", + })), + ]} + placeholder={t.timesheet?.projectLabel || "Project"} + searchPlaceholder={extendedTimesheet.searchProjectsLabel || t.projects?.searchPlaceholder || "Search projects..."} + emptyLabel={extendedTimesheet.noProjectsFoundLabel || "No projects found."} + className="w-full" + buttonClassName="h-10 w-full rounded-md border border-slate-200 bg-slate-50 px-3 text-sm shadow-none outline-none dark:border-slate-700 dark:bg-slate-900 focus:ring-0 focus-visible:ring-0 focus-visible:ring-offset-0" + disabled={isStartingTimer} + />
{runningEntry ? formatDuration(runningEntry, ticker) : "00:00:00"} @@ -2842,20 +2851,20 @@ export default function Timesheet() {
- - setSearchParams( - (current) => - updateQueryParams(current, { search: value }, { search: "" }), - { replace: true }, - ) - } - onApply={handleApplyFilters} - onClearFilters={handleClearFilters} - projects={projects} - tags={tags} + + setSearchParams( + (current) => + updateQueryParams(current, { search: value }, { search: "" }), + { replace: true }, + ) + } + onApply={handleApplyFilters} + onClearFilters={handleClearFilters} + projects={projects} + tags={tags} searchPlaceholder={t.timesheet?.searchPlaceholder || "Search time entries..."} labels={{ project: t.timesheet?.projectLabel || "Project", @@ -2872,20 +2881,20 @@ export default function Timesheet() { apply: extendedTimesheet.applyFiltersLabel || "Apply", clientPrefix: extendedTimesheet.clientFilterPrefix || "Client", projectPrefix: extendedTimesheet.projectFilterPrefix || "Project", - tagPrefix: extendedTimesheet.tagFilterPrefix || "Tag", - fromPrefix: extendedTimesheet.fromFilterPrefix || "From", - toPrefix: extendedTimesheet.toFilterPrefix || "To", - searchTags: extendedTimesheet.searchTagsLabel || t.tags?.searchPlaceholder || "Search tags...", - noTagsFound: extendedTimesheet.noTagsFoundLabel || "No tags found.", - }} - /> -
+ tagPrefix: extendedTimesheet.tagFilterPrefix || "Tag", + fromPrefix: extendedTimesheet.fromFilterPrefix || "From", + toPrefix: extendedTimesheet.toFilterPrefix || "To", + searchTags: extendedTimesheet.searchTagsLabel || t.tags?.searchPlaceholder || "Search tags...", + noTagsFound: extendedTimesheet.noTagsFoundLabel || "No tags found.", + }} + /> +
- {isLoading ? ( - - ) : ( - + ) : ( + {week.days.map((day) => ( -
-
+
+

{formatDayLabel(new Date(`${day.date}T00:00:00`), lang)}

-

- {t.reports?.total || "Total"}: {formatDurationMs(day.total_ms)} -

+

+ {t.reports?.total || "Total"}: {formatDurationMs(day.total_ms)} +

@@ -2948,11 +2957,17 @@ export default function Timesheet() {
))} - {groupedHistory.length === 0 && ( -
- -

{t.timesheet?.emptyState || "No time entries found"}

-
+ {groupedHistory.length === 0 && ( + )}
@@ -2986,10 +3001,10 @@ export default function Timesheet() { {deleteModal.entry && ( - @@ -3003,10 +3018,10 @@ export default function Timesheet() { } >
-

- {extendedTimesheet.deleteConfirmMessage || t.timesheet?.deleteConfirmMessage || "Are you sure you want to delete this time entry?"} -

-
+

+ {extendedTimesheet.deleteConfirmMessage || t.timesheet?.deleteConfirmMessage || "Are you sure you want to delete this time entry?"} +

+

{deleteModal.entry.description || t.timesheet?.emptyDescription || "No description"}

@@ -3037,10 +3052,10 @@ export default function Timesheet() { } >
-

- {extendedTimesheet.restartConfirmMessage || t.timesheet?.restartConfirmMessage || "Start a new running timer from this entry?"} -

-
+

+ {extendedTimesheet.restartConfirmMessage || t.timesheet?.restartConfirmMessage || "Start a new running timer from this entry?"} +

+

{restartModal.entry.description || t.timesheet?.emptyDescription || "No description"}

@@ -3071,10 +3086,10 @@ export default function Timesheet() { } >
-

- {extendedTimesheet.discardConfirmMessage || t.timesheet?.discardConfirmMessage || "Are you sure you want to discard this running timer?"} -

-
+

+ {extendedTimesheet.discardConfirmMessage || t.timesheet?.discardConfirmMessage || "Are you sure you want to discard this running timer?"} +

+

{discardTimerModal.entry.description || t.timesheet?.emptyDescription || "No description"}