feat(timesheet): add manual time entry action
This commit is contained in:
@@ -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.",
|
||||||
|
|||||||
@@ -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: "تایمر با موفقیت شروع شد.",
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user