feat(timesheet): add manual time entry action
Some checks failed
Frontend CI/CD / build (push) Has been cancelled
Frontend CI/CD / deploy (push) Has been cancelled

This commit is contained in:
2026-06-18 22:59:04 +03:30
parent 55ba274346
commit c7ede31b68
3 changed files with 65 additions and 36 deletions

View File

@@ -681,7 +681,8 @@ export const en = {
title: "Timesheet", title: "Timesheet",
description: (workspaceName: string) => `Track time inside ${workspaceName}`, description: (workspaceName: string) => `Track time inside ${workspaceName}`,
selectWorkspace: "Please select a workspace first.", selectWorkspace: "Please select a workspace first.",
addEntry: "Add Entry", addEntry: "Add Entry",
addManualEntry: "Add manual entry",
startTimer: "Start Timer", startTimer: "Start Timer",
stopTimer: "Stop Timer", stopTimer: "Stop Timer",
timerRunning: "Timer Running", timerRunning: "Timer Running",
@@ -695,7 +696,8 @@ export const en = {
emptyStateDescription: "Start the timer or add a manual entry to get started.", emptyStateDescription: "Start the timer or add a manual entry to get started.",
noEntriesSearch: "Try adjusting your search query or filters.", noEntriesSearch: "Try adjusting your search query or filters.",
emptyDescription: "No description", emptyDescription: "No description",
createTitle: "Add Time Entry", createTitle: "Add Time Entry",
manualCreateTitle: "Add Manual Time Entry",
startTitle: "Start Timer", startTitle: "Start Timer",
editTitle: "Edit Time Entry", editTitle: "Edit Time Entry",
createSuccess: "Time entry created successfully.", createSuccess: "Time entry created successfully.",

View File

@@ -677,9 +677,10 @@ export const fa = {
timesheet: { timesheet: {
title: "تایم‌شیت", title: "تایم‌شیت",
description: (workspaceName: string) => `ثبت زمان در ${workspaceName}`, description: (workspaceName: string) => `ثبت زمان در ${workspaceName}`,
selectWorkspace: "لطفاً ابتدا یک ورک‌اسپیس انتخاب کنید.", selectWorkspace: "لطفاً ابتدا یک ورک‌اسپیس انتخاب کنید.",
addEntry: "افزودن ورودی", addEntry: "افزودن ورودی",
startTimer: "شروع تایمر", addManualEntry: "افزودن دستی زمان",
startTimer: "شروع تایمر",
stopTimer: "توقف تایمر", stopTimer: "توقف تایمر",
timerRunning: "تایمر فعال است", timerRunning: "تایمر فعال است",
runningLabel: "تایمر فعلی", runningLabel: "تایمر فعلی",
@@ -692,8 +693,9 @@ export const fa = {
emptyDescription: "بدون توضیح", emptyDescription: "بدون توضیح",
emptyStateDescription: "برای شروع، تایمر را اجرا کنید یا یک ورودی دستی اضافه کنید.", emptyStateDescription: "برای شروع، تایمر را اجرا کنید یا یک ورودی دستی اضافه کنید.",
noEntriesSearch: "عبارت جست‌وجو یا فیلترهای خود را تغییر دهید.", noEntriesSearch: "عبارت جست‌وجو یا فیلترهای خود را تغییر دهید.",
createTitle: "افزودن ورودی زمان", createTitle: "افزودن ورودی زمان",
startTitle: "شروع تایمر", manualCreateTitle: "افزودن دستی زمان",
startTitle: "شروع تایمر",
editTitle: "ویرایش ورودی زمان", editTitle: "ویرایش ورودی زمان",
createSuccess: "ورودی زمان با موفقیت ایجاد شد.", createSuccess: "ورودی زمان با موفقیت ایجاد شد.",
startSuccess: "تایمر با موفقیت شروع شد.", startSuccess: "تایمر با موفقیت شروع شد.",

View File

@@ -402,15 +402,21 @@ const updateGroupedHistoryEntry = (
return merged; return merged;
}; };
const buildEntryFormState = (entry?: TimeEntry | null): EntryFormState => { const buildEntryFormState = (entry?: TimeEntry | null): EntryFormState => {
if (!entry) { if (!entry) {
const now = getLocalDateParts(new Date().toISOString()); const endDate = new Date();
return { const startDate = new Date(endDate);
...EMPTY_FORM, startDate.setHours(startDate.getHours() - 1);
startDate: now.date, const start = getLocalDateParts(startDate.toISOString());
startTime: now.time, const end = getLocalDateParts(endDate.toISOString());
}; return {
} ...EMPTY_FORM,
startDate: start.date,
startTime: start.time,
endDate: end.date,
endTime: end.time,
};
}
const start = getLocalDateParts(entry.start_time); const start = getLocalDateParts(entry.start_time);
const end = getLocalDateParts(entry.end_time); const end = getLocalDateParts(entry.end_time);
@@ -459,7 +465,12 @@ const toggleTagId = (currentTags: string[], tagId: string) =>
const buildPayloadFromState = ( const buildPayloadFromState = (
state: EntryFormState, state: EntryFormState,
options: { includeWorkspace: boolean; workspaceId?: string; messages?: Partial<Record<"startRequired" | "endRequired" | "invalidEndTime" | "endBeforeStart", string>> }, options: {
includeWorkspace: boolean;
workspaceId?: string;
requireEnd?: boolean;
messages?: Partial<Record<"startRequired" | "endRequired" | "invalidEndTime" | "endBeforeStart", string>>;
},
): { payload?: TimeEntryPayload; error?: string } => { ): { payload?: TimeEntryPayload; error?: string } => {
const messages = { const messages = {
startRequired: "Start date and time are required.", startRequired: "Start date and time are required.",
@@ -473,9 +484,12 @@ const buildPayloadFromState = (
return { error: messages.startRequired }; return { error: messages.startRequired };
} }
let endDateTime: string | null = null; let endDateTime: string | null = null;
const hasEndValue = Boolean(state.endDate || state.endTime); const hasEndValue = Boolean(state.endDate || state.endTime);
if (hasEndValue) { if (options.requireEnd && !hasEndValue) {
return { error: messages.endRequired };
}
if (hasEndValue) {
if (!state.endDate || !state.endTime) { if (!state.endDate || !state.endTime) {
return { error: messages.endRequired }; return { error: messages.endRequired };
} }
@@ -2587,6 +2601,7 @@ export default function Timesheet() {
const { payload, error } = buildPayloadFromState(formState, { const { payload, error } = buildPayloadFromState(formState, {
includeWorkspace: modalMode === "manual", includeWorkspace: modalMode === "manual",
workspaceId: activeWorkspace?.id, workspaceId: activeWorkspace?.id,
requireEnd: modalMode === "manual",
messages: entryValidationMessages, messages: entryValidationMessages,
}); });
@@ -2870,11 +2885,17 @@ export default function Timesheet() {
<h1 className="text-2xl font-bold text-slate-900 dark:text-white">{t.timesheet?.title || 'Timesheets'}</h1> <h1 className="text-2xl font-bold text-slate-900 dark:text-white">{t.timesheet?.title || 'Timesheets'}</h1>
<p className="text-slate-500 dark:text-slate-400 mt-1">{t.timesheet?.description(activeWorkspace?.name || "-") || 'Manage your Timesheet'}</p> <p className="text-slate-500 dark:text-slate-400 mt-1">{t.timesheet?.description(activeWorkspace?.name || "-") || 'Manage your Timesheet'}</p>
</div> </div>
<Button type="button" variant="secondary" onClick={() => void openRatesPanel()} className="gap-2"> <div className="flex flex-wrap gap-2">
<Banknote className="h-4 w-4" /> <Button type="button" onClick={openCreateModal} className="gap-2">
{t.rates?.myRatesTitle || "My rates"} <Plus className="h-4 w-4" />
</Button> {t.timesheet?.addManualEntry || t.timesheet?.addEntry || "Add manual entry"}
</div> </Button>
<Button type="button" variant="secondary" onClick={() => void openRatesPanel()} className="gap-2">
<Banknote className="h-4 w-4" />
{t.rates?.myRatesTitle || "My rates"}
</Button>
</div>
</div>
<div <div
ref={desktopTimerRef} ref={desktopTimerRef}
@@ -3210,21 +3231,25 @@ export default function Timesheet() {
</InfiniteScroll> </InfiniteScroll>
)} )}
<Modal <Modal
isOpen={modalMode === "manual" || modalMode === "edit"} isOpen={modalMode === "manual" || modalMode === "edit"}
onClose={closeCreateModal} onClose={closeCreateModal}
title={modalMode === "edit" ? (t.timesheet?.editTitle || "Edit Time Entry") : (t.timesheet?.createTitle || "Add Time Entry")} title={
maxWidth="max-w-2xl" modalMode === "edit"
footer={ ? (t.timesheet?.editTitle || "Edit Time Entry")
: (t.timesheet?.manualCreateTitle || t.timesheet?.createTitle || "Add Manual Time Entry")
}
maxWidth="max-w-2xl"
footer={
<> <>
<Button variant="secondary" onClick={closeCreateModal}> <Button variant="secondary" onClick={closeCreateModal}>
{t.actions?.cancel || "Cancel"} {t.actions?.cancel || "Cancel"}
</Button>
<Button type="submit" form="time-entry-modal-form" disabled={isSaving}>
{isSaving ? "..." : (modalMode === "edit" ? (t.save || "Save") : (t.create || "Create"))}
</Button> </Button>
</> <Button type="submit" form="time-entry-modal-form" disabled={isSaving}>
} {isSaving ? "..." : (modalMode === "edit" ? (t.save || "Save") : (t.timesheet?.addManualEntry || t.create || "Add manual entry"))}
</Button>
</>
}
> >
<form id="time-entry-modal-form" onSubmit={handleSaveEntryModal}> <form id="time-entry-modal-form" onSubmit={handleSaveEntryModal}>
<EntryEditorFields <EntryEditorFields