diff --git a/src/pages/Timesheet.tsx b/src/pages/Timesheet.tsx
index b8d2634..6cdf8fd 100644
--- a/src/pages/Timesheet.tsx
+++ b/src/pages/Timesheet.tsx
@@ -1,12 +1,12 @@
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { createPortal } from "react-dom";
import { useSearchParams } from "react-router-dom";
-import { Banknote, CalendarDays, Check, ChevronDown, Clock3, DollarSign, MoreVertical, Pencil, Play, Plus, Search, Square, Tag as TagIcon, Trash2 } from "lucide-react";
+import { Banknote, CalendarDays, Check, ChevronDown, Clock3, DollarSign, MoreVertical, Pencil, Play, Plus, Search, Square, Tag as TagIcon, Trash2 } from "lucide-react";
import { toast } from "sonner";
-import { getProjects, type Project } from "../api/projects";
-import { getMyWorkspaceRates, type MyWorkspaceRatesResponse } from "../api/rates";
-import {
+import { getProjects, type Project } from "../api/projects";
+import { getMyWorkspaceRates, type MyWorkspaceRatesResponse } from "../api/rates";
+import {
createTimeEntry,
deleteTimeEntry,
getTimeEntries,
@@ -19,10 +19,10 @@ import {
} from "../api/timeEntries";
import { getTags, type Tag } from "../api/tags";
import { Modal } from "../components/Modal";
-import EmptyStateCard from "../components/EmptyStateCard";
-import { InfiniteScroll } from "../components/InfiniteScroll";
-import { WorkspaceRatesPanel } from "../components/rates/WorkspaceRatesPanel";
-import TimesheetFilterBar, { type TimeEntryFilters } from "../components/timesheet/TimesheetFilterBar";
+import EmptyStateCard from "../components/EmptyStateCard";
+import { InfiniteScroll } from "../components/InfiniteScroll";
+import { WorkspaceRatesPanel } from "../components/rates/WorkspaceRatesPanel";
+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";
@@ -1262,10 +1262,10 @@ function EntryEditorFields({
onChange({ description: event.target.value })}
- placeholder={t.timesheet?.descriptionPlaceholder || "What are you working on?"}
- className="h-12 w-[200px] 2xl:w-[400px] shrink-0 rounded-none border-0 bg-transparent px-0 pe-3 text-sm shadow-none placeholder:text-slate-300 focus-visible:ring-0 focus-visible:ring-offset-0 truncate dark:bg-transparent dark:text-slate-100 dark:placeholder:text-slate-600"
+ value={state.description}
+ onChange={(event) => onChange({ description: event.target.value })}
+ placeholder={t.timesheet?.descriptionPlaceholder || "What are you working on?"}
+ className="h-12 w-[200px] 2xl:w-[400px] shrink-0 rounded-none border-0 bg-transparent px-0 pe-3 text-sm shadow-none placeholder:text-slate-300 focus-visible:ring-0 focus-visible:ring-offset-0 truncate dark:bg-transparent dark:text-slate-100 dark:placeholder:text-slate-600"
/>
•
@@ -1344,11 +1344,11 @@ function EntryEditorFields({
- onChange({ description: event.target.value })}
- placeholder={t.timesheet?.descriptionPlaceholder || "What are you working on?"}
- className={compact ? "h-9 px-2 text-xs placeholder:text-slate-300 dark:placeholder:text-slate-600" : "placeholder:text-slate-300 dark:placeholder:text-slate-600"}
+ onChange({ description: event.target.value })}
+ placeholder={t.timesheet?.descriptionPlaceholder || "What are you working on?"}
+ className={compact ? "h-9 px-2 text-xs placeholder:text-slate-300 dark:placeholder:text-slate-600" : "placeholder:text-slate-300 dark:placeholder:text-slate-600"}
/>
@@ -1837,7 +1837,7 @@ function MobileRecordedEntryCard({
{project && (
- {"\u2022"}
+ {"•"}
{project.isDeleted ? buildDeletedProjectLabel(project.name, deletedProjectLabel) : project.name}
@@ -2017,13 +2017,13 @@ function TimesheetSkeleton({ loadingLabel }: { loadingLabel: string }) {
);
}
-export default function Timesheet() {
- const { t, lang } = useTranslation();
- const { activeWorkspace } = useWorkspace();
- const [isRatesPanelOpen, setIsRatesPanelOpen] = useState(false);
- const [isRatesPanelLoading, setIsRatesPanelLoading] = useState(false);
- const [myRates, setMyRates] = useState(null);
- const [searchParams, setSearchParams] = useSearchParams();
+export default function Timesheet() {
+ const { t, lang } = useTranslation();
+ const { activeWorkspace } = useWorkspace();
+ const [isRatesPanelOpen, setIsRatesPanelOpen] = useState(false);
+ const [isRatesPanelLoading, setIsRatesPanelLoading] = useState(false);
+ const [myRates, setMyRates] = useState(null);
+ const [searchParams, setSearchParams] = useSearchParams();
const isRtl = lang === "fa";
const extendedTimesheet = (t.timesheet as {
deleteTitle?: string;
@@ -2163,17 +2163,17 @@ export default function Timesheet() {
void loadOptions();
}, [activeWorkspace?.id, t.timesheet?.optionsError]);
- useEffect(() => {
- setGroupedHistory([]);
- setNextOffset(0);
- setHasMoreHistory(false);
- }, [activeWorkspace?.id]);
-
- useEffect(() => {
- setIsRatesPanelOpen(false);
- setIsRatesPanelLoading(false);
- setMyRates(null);
- }, [activeWorkspace?.id]);
+ useEffect(() => {
+ setGroupedHistory([]);
+ setNextOffset(0);
+ setHasMoreHistory(false);
+ }, [activeWorkspace?.id]);
+
+ useEffect(() => {
+ setIsRatesPanelOpen(false);
+ setIsRatesPanelLoading(false);
+ setMyRates(null);
+ }, [activeWorkspace?.id]);
useEffect(() => {
const timeoutId = window.setTimeout(() => {
@@ -2606,29 +2606,29 @@ export default function Timesheet() {
setDebouncedSearchQuery("");
}, [setSearchParams]);
- const handleLoadMore = useCallback(() => {
- if (!hasMoreHistory || nextOffset === null || isLoadingMore) return;
- void loadHistory({ offset: nextOffset, append: true });
- }, [hasMoreHistory, isLoadingMore, loadHistory, nextOffset]);
-
- const openRatesPanel = useCallback(async () => {
- if (!activeWorkspace?.id) return;
-
- setIsRatesPanelOpen(true);
- if (myRates || isRatesPanelLoading) return;
-
- try {
- setIsRatesPanelLoading(true);
- const response = await getMyWorkspaceRates(activeWorkspace.id);
- setMyRates(response);
- } catch (error) {
- console.error(error);
- toast.error(t.rates?.projectSaveError || "Failed to load rates.");
- setIsRatesPanelOpen(false);
- } finally {
- setIsRatesPanelLoading(false);
- }
- }, [activeWorkspace?.id, isRatesPanelLoading, myRates, t.rates?.projectSaveError]);
+ const handleLoadMore = useCallback(() => {
+ if (!hasMoreHistory || nextOffset === null || isLoadingMore) return;
+ void loadHistory({ offset: nextOffset, append: true });
+ }, [hasMoreHistory, isLoadingMore, loadHistory, nextOffset]);
+
+ const openRatesPanel = useCallback(async () => {
+ if (!activeWorkspace?.id) return;
+
+ setIsRatesPanelOpen(true);
+ if (myRates || isRatesPanelLoading) return;
+
+ try {
+ setIsRatesPanelLoading(true);
+ const response = await getMyWorkspaceRates(activeWorkspace.id);
+ setMyRates(response);
+ } catch (error) {
+ console.error(error);
+ toast.error(t.rates?.projectSaveError || "Failed to load rates.");
+ setIsRatesPanelOpen(false);
+ } finally {
+ setIsRatesPanelLoading(false);
+ }
+ }, [activeWorkspace?.id, isRatesPanelLoading, myRates, t.rates?.projectSaveError]);
const handleDiscardTimerDraft = useCallback(async () => {
if (!discardTimerModal.entry || isDiscardingTimer) return;
@@ -2658,16 +2658,16 @@ export default function Timesheet() {
return (
-
-
-
{t.timesheet?.title || 'Timesheets'}
-
{t.timesheet?.description(activeWorkspace?.name || "-") || 'Manage your Timesheet'}
-
-
-
+
+
+
{t.timesheet?.title || 'Timesheets'}
+
{t.timesheet?.description(activeWorkspace?.name || "-") || 'Manage your Timesheet'}
+
+
+
setTimerDraft((current) => ({ ...current, description: event.target.value }))}
- disabled={isStartingTimer}
- className="h-12 rounded-none border-0 bg-transparent px-5 text-sm shadow-none placeholder:text-slate-300 focus-visible:ring-0 focus-visible:ring-offset-0 dark:bg-transparent dark:placeholder:text-slate-600"
+ value={timerDraft.description}
+ placeholder={t.timesheet?.descriptionPlaceholder || "What are you working on?"}
+ onChange={(event) => setTimerDraft((current) => ({ ...current, description: event.target.value }))}
+ disabled={isStartingTimer}
+ className="h-12 rounded-none border-0 bg-transparent px-5 text-sm shadow-none placeholder:text-slate-300 focus-visible:ring-0 focus-visible:ring-offset-0 dark:bg-transparent dark:placeholder:text-slate-600"
/>
@@ -2785,11 +2785,11 @@ export default function Timesheet() {
>
-
- )}
-
-
setIsRatesPanelOpen(false)}
- />
-
- );
-}
+
+ )}
+
+
setIsRatesPanelOpen(false)}
+ />
+
+ );
+}
diff --git a/src/pages/auth/utils.ts b/src/pages/auth/utils.ts
index fa8339d..fae9597 100644
--- a/src/pages/auth/utils.ts
+++ b/src/pages/auth/utils.ts
@@ -3,7 +3,7 @@ import { toast } from "sonner"
import { ApiError } from "../../api/client"
import { setSessionTokens } from "../../lib/session"
-const PERSIAN_DIGITS = ["\u06f0", "\u06f1", "\u06f2", "\u06f3", "\u06f4", "\u06f5", "\u06f6", "\u06f7", "\u06f8", "\u06f9"]
+const PERSIAN_DIGITS = ["۰", "۱", "۲", "۳", "۴", "۵", "۶", "۷", "۸", "۹"]
export const localizeDigits = (value: string, isRtl: boolean) =>
isRtl ? value.replace(/\d/g, (digit) => PERSIAN_DIGITS[Number.parseInt(digit, 10)] ?? digit) : value