feat(timesheet): add live search and searchable project selectors

This commit is contained in:
2026-04-29 01:25:05 +03:30
parent 8868b7d1cc
commit 05f2b4a4bb
5 changed files with 342 additions and 244 deletions

View File

@@ -16,15 +16,15 @@ 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 { Select } from "../components/ui/Select";
import { useWorkspace } from "../context/WorkspaceContext";
import { useTranslation } from "../hooks/useTranslation";
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";
type EntryModalMode = "manual" | "edit" | null;
@@ -858,30 +858,35 @@ function TagMultiSelect({
);
}
function ProjectInlineSelect({
projects,
value,
onChange,
placeholder,
portalOwnerId,
className = "",
dropdownClassName = "",
disabled = false,
}: {
projects: Project[];
value: string;
onChange: (projectId: string) => void;
placeholder: string;
portalOwnerId?: string;
className?: string;
dropdownClassName?: string;
disabled?: boolean;
}) {
const [isOpen, setIsOpen] = useState(false);
const wrapperRef = useRef<HTMLDivElement>(null);
const buttonRef = useRef<HTMLButtonElement>(null);
const dropdownRef = useRef<HTMLDivElement>(null);
const [dropdownStyle, setDropdownStyle] = useState<React.CSSProperties>({});
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<HTMLDivElement>(null);
const buttonRef = useRef<HTMLButtonElement>(null);
const dropdownRef = useRef<HTMLDivElement>(null);
const [dropdownStyle, setDropdownStyle] = useState<React.CSSProperties>({});
useEffect(() => {
if (!isOpen) return;
@@ -896,9 +901,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;
@@ -928,13 +933,27 @@ function ProjectInlineSelect({
return () => {
window.removeEventListener("resize", closeOnViewportChange);
window.removeEventListener("scroll", closeOnViewportChange, true);
};
}, [isOpen]);
const selectedProject = projects.find((project) => project.id === value);
const label = selectedProject?.name || placeholder;
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 (
<div ref={wrapperRef} className={`relative min-w-0 ${className}`}>
<button
ref={buttonRef}
@@ -958,11 +977,23 @@ function ProjectInlineSelect({
style={dropdownStyle}
data-entry-editor-owner={portalOwnerId}
className={`rounded-2xl border border-slate-200 bg-white p-2 shadow-xl dark:border-slate-700 dark:bg-slate-800 ${dropdownClassName}`}
>
<div className="max-h-64 space-y-1 overflow-y-auto">
<button
type="button"
onMouseDown={(event) => event.preventDefault()}
>
<div className="border-b border-slate-200 p-2 dark:border-slate-700">
<div className="relative">
<Search className="pointer-events-none absolute left-3 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-slate-400" />
<input
type="text"
value={query}
onChange={(event) => setQuery(event.target.value)}
placeholder={searchPlaceholder}
className="h-8 w-full rounded-md border border-slate-200 bg-slate-50 pl-8 pr-2 text-xs text-slate-900 outline-none transition focus:border-sky-400 focus:bg-white focus:ring-2 focus:ring-sky-500/20 dark:border-slate-700 dark:bg-slate-800 dark:text-white dark:focus:border-sky-500 dark:focus:bg-slate-800"
/>
</div>
</div>
<div className="max-h-64 space-y-1 overflow-y-auto p-2">
<button
type="button"
onMouseDown={(event) => event.preventDefault()}
onClick={() => {
onChange("");
setIsOpen(false);
@@ -976,10 +1007,10 @@ function ProjectInlineSelect({
{placeholder}
</button>
{projects.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 (
<button
key={project.id}
type="button"
@@ -989,22 +1020,25 @@ function ProjectInlineSelect({
onChange(project.id);
setIsOpen(false);
}}
className={`flex w-full items-center rounded-xl px-3 py-2 text-sm transition-colors ${
className={`flex w-full items-center rounded-xl px-3 py-2 text-sm transition-colors ${
selected
? "bg-sky-50 text-sky-700 dark:bg-sky-500/15 dark:text-sky-300"
: unavailable
? "cursor-not-allowed text-slate-400 opacity-70 dark:text-slate-500"
: "text-slate-700 hover:bg-slate-100 dark:text-slate-200 dark:hover:bg-slate-700/70"
}`}
title={project.name}
>
<span className={`truncate ${project.is_deleted ? "italic" : ""}`}>{project.name}</span>
</button>
);
})}
</div>
</div>,
document.body,
title={project.name}
>
<span className={`truncate ${project.is_deleted ? "italic" : ""}`}>{project.name}</span>
</button>
);
})}
{filteredProjects.length === 0 && (
<div className="px-3 py-3 text-xs text-slate-500 dark:text-slate-400">{emptyLabel}</div>
)}
</div>
</div>,
document.body,
)}
</div>
);
@@ -1229,14 +1263,16 @@ function EntryEditorFields({
<div className="flex min-w-0 flex-1 items-center">
<div className="flex min-w-0 flex-1 items-center gap-1">
<ProjectInlineSelect
projects={projects}
value={state.projectId}
onChange={(projectId) => (onProjectChange ? onProjectChange(projectId) : onChange({ projectId }))}
placeholder={t.timesheet?.projectLabel || "Project"}
portalOwnerId={portalOwnerId}
className="min-w-0 max-w-fit flex-1"
/>
<ProjectInlineSelect
projects={projects}
value={state.projectId}
onChange={(projectId) => (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 && (
<span className="min-w-0 max-w-[120px] 2xl:max-w-fit shrink truncate text-sm text-slate-400 dark:text-slate-500" title={selectedProject.client.name}>
@@ -1307,21 +1343,28 @@ function EntryEditorFields({
/>
</div>
<div>
<label className={`block font-medium text-slate-700 dark:text-slate-300 ${compact ? "mb-0.5 text-[11px]" : "mb-1 text-sm"}`}>
{t.timesheet?.projectLabel || "Project"}
</label>
<Select
value={state.projectId}
onChange={(value) => onChange({ projectId: String(value) })}
options={[
{ value: "", label: t.timesheet?.noProject || "No project" },
...projects.map((project) => ({ value: project.id, label: project.name })),
]}
className="w-full"
buttonClassName={compact ? "w-full h-9 px-2 text-xs" : "w-full"}
/>
</div>
<div>
<label className={`block font-medium text-slate-700 dark:text-slate-300 ${compact ? "mb-0.5 text-[11px]" : "mb-1 text-sm"}`}>
{t.timesheet?.projectLabel || "Project"}
</label>
<SearchableSelect
value={state.projectId}
onChange={(value) => 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"}
/>
</div>
<div className="grid gap-4 md:grid-cols-2">
<div className={compact ? "" : "space-y-4 rounded-2xl border border-slate-200 bg-slate-50 p-4 dark:border-slate-800 dark:bg-slate-900/60"}>
@@ -1893,23 +1936,29 @@ export default function Timesheet() {
hideFiltersLabel?: string;
applyFiltersLabel?: string;
clientFilterPrefix?: string;
projectFilterPrefix?: string;
tagFilterPrefix?: string;
fromFilterPrefix?: string;
toFilterPrefix?: string;
restartConfirmMessage?: string;
deletedProjectLabel?: string;
deletedTagLabel?: 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<Project[]>([]);
const [tags, setTags] = useState<Tag[]>([]);
const [groupedHistory, setGroupedHistory] = useState<TimeEntryGroupWeek[]>([]);
const [activeRunningEntry, setActiveRunningEntry] = useState<TimeEntry | null>(null);
const [isLoading, setIsLoading] = useState(false);
const [isLoadingMore, setIsLoadingMore] = useState(false);
const [searchQuery, setSearchQuery] = useState("");
const [filters, setFilters] = useState<TimeEntryFilters>(DEFAULT_ENTRY_FILTERS);
const [groupedHistory, setGroupedHistory] = useState<TimeEntryGroupWeek[]>([]);
const [activeRunningEntry, setActiveRunningEntry] = useState<TimeEntry | null>(null);
const [isLoading, setIsLoading] = useState(false);
const [isLoadingMore, setIsLoadingMore] = useState(false);
const [searchQuery, setSearchQuery] = useState("");
const [debouncedSearchQuery, setDebouncedSearchQuery] = useState("");
const [filters, setFilters] = useState<TimeEntryFilters>(DEFAULT_ENTRY_FILTERS);
const [hasMoreHistory, setHasMoreHistory] = useState(false);
const [nextOffset, setNextOffset] = useState<number | null>(0);
const [limit] = useState(20);
@@ -1977,35 +2026,44 @@ 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 || []);
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(() => {
setSearchQuery("");
setFilters(DEFAULT_ENTRY_FILTERS);
setGroupedHistory([]);
setNextOffset(0);
setHasMoreHistory(false);
}, [activeWorkspace?.id]);
useEffect(() => {
setGroupedHistory([]);
setNextOffset(0);
setHasMoreHistory(false);
}, [filters, searchQuery]);
useEffect(() => {
setSearchQuery("");
setDebouncedSearchQuery("");
setFilters(DEFAULT_ENTRY_FILTERS);
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;
@@ -2028,11 +2086,11 @@ export default function Timesheet() {
} else {
setIsLoading(true);
}
const params: TimeEntryListParams = {
limit,
offset,
search: searchQuery,
status: "ended",
const params: TimeEntryListParams = {
limit,
offset,
search: debouncedSearchQuery,
status: "ended",
project: filters.projectId || undefined,
client: filters.clientId || undefined,
tags: filters.tagIds,
@@ -2053,7 +2111,7 @@ export default function Timesheet() {
setIsLoading(false);
}
}
}, [activeWorkspace?.id, filters, limit, searchQuery, t.timesheet?.fetchError]);
}, [activeWorkspace?.id, debouncedSearchQuery, filters, limit, t.timesheet?.fetchError]);
const loadRunningEntry = useCallback(async () => {
if (!activeWorkspace?.id) {
@@ -2074,15 +2132,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, limit, loadHistory, filters, searchQuery]);
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();
@@ -2373,15 +2431,15 @@ export default function Timesheet() {
}
}, []);
const handleApplyFilters = useCallback((nextSearchQuery: string, nextFilters: TimeEntryFilters) => {
setSearchQuery(nextSearchQuery);
setFilters(nextFilters);
}, []);
const handleClearFilters = useCallback(() => {
setSearchQuery("");
setFilters(DEFAULT_ENTRY_FILTERS);
}, []);
const handleApplyFilters = useCallback((nextFilters: TimeEntryFilters) => {
setFilters(nextFilters);
}, []);
const handleClearFilters = useCallback(() => {
setSearchQuery("");
setDebouncedSearchQuery("");
setFilters(DEFAULT_ENTRY_FILTERS);
}, []);
const handleLoadMore = useCallback(() => {
if (!hasMoreHistory || nextOffset === null || isLoadingMore) return;
@@ -2439,20 +2497,26 @@ export default function Timesheet() {
/>
</div>
<div className="flex shrink-0 items-center">
<Select
value={timerDraft.projectId}
onChange={(value) => setTimerDraft((current) => ({ ...current, projectId: String(value) }))}
options={[
{ value: "", label: t.timesheet?.projectLabel || "Project" },
...runningTimerProjects.map((project) => ({ value: project.id, label: project.name })),
]}
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}
portalOwnerId={timerEditorOwnerId}
/>
</div>
<div className="flex shrink-0 items-center">
<SearchableSelect
value={timerDraft.projectId}
onChange={(value) => 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}
/>
</div>
<div className="flex min-w-0 shrink items-center">
<TagMultiSelect
@@ -2540,19 +2604,25 @@ export default function Timesheet() {
className="h-10 border-slate-200 bg-slate-50 text-sm dark:border-slate-700 dark:bg-slate-900"
/>
<div className="grid grid-cols-[minmax(0,1fr)_auto] gap-2">
<Select
value={timerDraft.projectId}
onChange={(value) => setTimerDraft((current) => ({ ...current, projectId: String(value) }))}
options={[
{ value: "", label: t.timesheet?.projectLabel || "Project" },
...runningTimerProjects.map((project) => ({ value: project.id, label: project.name })),
]}
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}
portalOwnerId={timerEditorOwnerId}
/>
<div className="grid grid-cols-[minmax(0,1fr)_auto] gap-2">
<SearchableSelect
value={timerDraft.projectId}
onChange={(value) => 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}
/>
<div className="flex h-10 min-w-[110px] items-center justify-center rounded-md border border-slate-200 bg-slate-50 px-3 text-sm font-semibold text-slate-900 dark:border-slate-700 dark:bg-slate-900 dark:text-white">
{runningEntry ? formatDuration(runningEntry, ticker) : "00:00:00"}
@@ -2625,13 +2695,14 @@ export default function Timesheet() {
</div>
<div className="mb-4">
<TimesheetFilterBar
searchQuery={searchQuery}
filters={filters}
onApply={handleApplyFilters}
onClearFilters={handleClearFilters}
projects={projects}
tags={tags}
<TimesheetFilterBar
searchQuery={searchQuery}
filters={filters}
onSearchChange={setSearchQuery}
onApply={handleApplyFilters}
onClearFilters={handleClearFilters}
projects={projects}
tags={tags}
searchPlaceholder={t.timesheet?.searchPlaceholder || "Search time entries..."}
labels={{
project: t.timesheet?.projectLabel || "Project",
@@ -2648,12 +2719,14 @@ 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",
}}
/>
</div>
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.",
}}
/>
</div>
{isLoading ? (
<div className="flex justify-center p-12 text-slate-500">{t.loading || "Loading..."}</div>
@@ -2682,9 +2755,9 @@ export default function Timesheet() {
<p className="text-xs font-medium text-slate-500 dark:text-slate-400">
{formatDayLabel(new Date(`${day.date}T00:00:00`), lang)}
</p>
<p className="text-xs text-slate-500 dark:text-slate-400">
Total: <span className="font-semibold text-slate-700 dark:text-slate-200">{formatDurationMs(day.total_ms)}</span>
</p>
<p className="text-xs text-slate-500 dark:text-slate-400">
{t.reports?.total || "Total"}: <span className="font-semibold text-slate-700 dark:text-slate-200">{formatDurationMs(day.total_ms)}</span>
</p>
</div>
<div>
@@ -2760,10 +2833,10 @@ export default function Timesheet() {
</Modal>
{deleteModal.entry && (
<Modal
isOpen={deleteModal.isOpen}
onClose={closeDeleteModal}
title={extendedTimesheet.deleteTitle || "Delete Time Entry"}
<Modal
isOpen={deleteModal.isOpen}
onClose={closeDeleteModal}
title={extendedTimesheet.deleteTitle || t.timesheet?.deleteTitle || "Delete Time Entry"}
maxWidth="max-w-md"
footer={
<>
@@ -2777,9 +2850,9 @@ export default function Timesheet() {
}
>
<div className="space-y-3">
<p className="text-sm leading-relaxed text-slate-600 dark:text-slate-400">
{extendedTimesheet.deleteConfirmMessage || "Are you sure you want to delete this time entry?"}
</p>
<p className="text-sm leading-relaxed text-slate-600 dark:text-slate-400">
{extendedTimesheet.deleteConfirmMessage || t.timesheet?.deleteConfirmMessage || "Are you sure you want to delete this time entry?"}
</p>
<div className="rounded-2xl border border-slate-200 bg-slate-50 p-4 dark:border-slate-800 dark:bg-slate-900/60">
<p className="font-medium text-slate-900 dark:text-white">
{deleteModal.entry.description || t.timesheet?.emptyDescription || "No description"}
@@ -2811,9 +2884,9 @@ export default function Timesheet() {
}
>
<div className="space-y-3">
<p className="text-sm leading-relaxed text-slate-600 dark:text-slate-400">
{(extendedTimesheet.restartConfirmMessage || "Start a new running timer from this entry?")}
</p>
<p className="text-sm leading-relaxed text-slate-600 dark:text-slate-400">
{extendedTimesheet.restartConfirmMessage || t.timesheet?.restartConfirmMessage || "Start a new running timer from this entry?"}
</p>
<div className="rounded-2xl border border-slate-200 bg-slate-50 p-4 dark:border-slate-800 dark:bg-slate-900/60">
<p className="font-medium text-slate-900 dark:text-white">
{restartModal.entry.description || t.timesheet?.emptyDescription || "No description"}
@@ -2845,9 +2918,9 @@ export default function Timesheet() {
}
>
<div className="space-y-3">
<p className="text-sm leading-relaxed text-slate-600 dark:text-slate-400">
{extendedTimesheet.deleteConfirmMessage || "Are you sure you want to delete this time entry?"}
</p>
<p className="text-sm leading-relaxed text-slate-600 dark:text-slate-400">
{extendedTimesheet.discardConfirmMessage || t.timesheet?.discardConfirmMessage || "Are you sure you want to discard this running timer?"}
</p>
<div className="rounded-2xl border border-slate-200 bg-slate-50 p-4 dark:border-slate-800 dark:bg-slate-900/60">
<p className="font-medium text-slate-900 dark:text-white">
{discardTimerModal.entry.description || t.timesheet?.emptyDescription || "No description"}