diff --git a/frontend/packages/app/src/app/components/timesheet-table/components/dataCell.tsx b/frontend/packages/app/src/app/components/timesheet-table/components/dataCell.tsx index 6e7833a95..b3392f0d9 100644 --- a/frontend/packages/app/src/app/components/timesheet-table/components/dataCell.tsx +++ b/frontend/packages/app/src/app/components/timesheet-table/components/dataCell.tsx @@ -31,7 +31,7 @@ import type { cellProps } from "./types"; * @param {string} props.className - Class name for the cell */ -export const Cell = ({ date, data, isHoliday, onCellClick, disabled, className }: cellProps) => { +export const Cell = ({ date, data, isHoliday, onCellClick, disabled, className, isBackdatedDisabled }: cellProps) => { const { hours, description, isTimeBothBillableAndNonBillable, isTimeBillable } = useMemo(() => { let hours = 0; let description = ""; @@ -50,7 +50,7 @@ export const Cell = ({ date, data, isHoliday, onCellClick, disabled, className } return { hours, description, isTimeBothBillableAndNonBillable, isTimeBillable }; }, [data]); - const isDisabled = useMemo(() => disabled || data?.[0]?.docstatus === 1, [disabled, data]); + const isDisabled = useMemo(() => disabled || data?.[0]?.docstatus === 1 || isBackdatedDisabled, [disabled, data, isBackdatedDisabled]); const handleClick = useCallback(() => { if (isDisabled) return; @@ -104,7 +104,7 @@ export const Cell = ({ date, data, isHoliday, onCellClick, disabled, className } /> - {description && ( + {description && !isBackdatedDisabled && ( e.stopPropagation()} @@ -112,6 +112,16 @@ export const Cell = ({ date, data, isHoliday, onCellClick, disabled, className } {}} hideToolbar={true} readOnly={true} value={description} /> )} + {isBackdatedDisabled && ( + e.stopPropagation()} + > + + Backdated time entry limit exceeded + + + )} ); diff --git a/frontend/packages/app/src/app/components/timesheet-table/components/row/index.tsx b/frontend/packages/app/src/app/components/timesheet-table/components/row/index.tsx index 6624d82ef..efa01a855 100644 --- a/frontend/packages/app/src/app/components/timesheet-table/components/row/index.tsx +++ b/frontend/packages/app/src/app/components/timesheet-table/components/row/index.tsx @@ -30,6 +30,7 @@ const Row = ({ totalCellClassName, showEmptyCell, hideLikeButton, + isDateBackdatedDisabled, }: RowProps) => { return ( <> @@ -86,6 +87,7 @@ const Row = ({ isHoliday={result.isHoliday && !result.weekly_off} onCellClick={onCellClick} disabled={disabled} + isBackdatedDisabled={isDateBackdatedDisabled?.(date)} /> ); })} diff --git a/frontend/packages/app/src/app/components/timesheet-table/components/row/types.ts b/frontend/packages/app/src/app/components/timesheet-table/components/row/types.ts index 2cb7db6cf..6455b62cd 100644 --- a/frontend/packages/app/src/app/components/timesheet-table/components/row/types.ts +++ b/frontend/packages/app/src/app/components/timesheet-table/components/row/types.ts @@ -51,6 +51,7 @@ export interface RowProps { totalCellClassName?: string; showEmptyCell?: boolean; hideLikeButton?: boolean; + isDateBackdatedDisabled?: (date: string) => boolean; } export interface leaveRowProps { diff --git a/frontend/packages/app/src/app/components/timesheet-table/components/types.ts b/frontend/packages/app/src/app/components/timesheet-table/components/types.ts index 15b77304b..b9e16352a 100644 --- a/frontend/packages/app/src/app/components/timesheet-table/components/types.ts +++ b/frontend/packages/app/src/app/components/timesheet-table/components/types.ts @@ -19,6 +19,7 @@ export type cellProps = { onCellClick?: (val) => void; disabled?: boolean; className?: string; + isBackdatedDisabled?: boolean; }; export type HeaderProps = { @@ -79,4 +80,5 @@ export type timesheetTableProps = { likedTaskData?: Array; getLikedTaskData?: () => void; hideLikeButton?: boolean; + oldestAllowedDate?: string | null; }; diff --git a/frontend/packages/app/src/app/components/timesheet-table/index.tsx b/frontend/packages/app/src/app/components/timesheet-table/index.tsx index 6617db0e8..a47550478 100644 --- a/frontend/packages/app/src/app/components/timesheet-table/index.tsx +++ b/frontend/packages/app/src/app/components/timesheet-table/index.tsx @@ -34,6 +34,7 @@ export const TimesheetTable = ({ likedTaskData, getLikedTaskData, hideLikeButton, + oldestAllowedDate, }: timesheetTableProps) => { const holidayList = getHolidayList(holidays); const [isTaskLogDialogBoxOpen, setIsTaskLogDialogBoxOpen] = useState(false); @@ -41,6 +42,15 @@ export const TimesheetTable = ({ const task_date_range_key = dates[0] + "-" + dates[dates.length - 1]; const has_liked_task = hasKeyInLocalStorage(LIKED_TASK_KEY); + // Helper function to check if a date is backdated and disabled + const isDateBackdatedDisabled = useCallback( + (date: string) => { + if (!oldestAllowedDate) return false; + return new Date(date) < new Date(oldestAllowedDate); + }, + [oldestAllowedDate] + ); + const setTaskInLocalStorage = () => { setLikedTask(LIKED_TASK_KEY, task_date_range_key, likedTaskData!); setFilteredLikedTasks( @@ -146,6 +156,7 @@ export const TimesheetTable = ({ workingFrequency={workingFrequency} workingHour={workingHour} hideLikeButton={hideLikeButton} + isDateBackdatedDisabled={isDateBackdatedDisabled} /> diff --git a/frontend/packages/app/src/app/layout/index.tsx b/frontend/packages/app/src/app/layout/index.tsx index 5b3d39921..1a63e2efa 100644 --- a/frontend/packages/app/src/app/layout/index.tsx +++ b/frontend/packages/app/src/app/layout/index.tsx @@ -13,7 +13,7 @@ import { useFrappeGetCall } from "frappe-react-sdk"; import Sidebar from "@/app/layout/sidebar"; import { parseFrappeErrorMsg } from "@/lib/utils"; import type { RootState } from "@/store"; -import { setInitialData } from "@/store/user"; +import { setInitialData, setBackdatedSettings } from "@/store/user"; const Layout = ({ children }: { children: React.ReactNode }) => { const user = useSelector((state: RootState) => state.user); @@ -25,6 +25,17 @@ const Layout = ({ children }: { children: React.ReactNode }) => { errorRetryCount: 1, }); + const { data: backdatedData } = useFrappeGetCall( + "next_pms.timesheet.api.employee.get_backdated_settings", + {}, + undefined, + { + revalidateOnFocus: false, + revalidateIfStale: false, + errorRetryCount: 1, + } + ); + useEffect(() => { if (data) { const info = { @@ -46,6 +57,13 @@ const Layout = ({ children }: { children: React.ReactNode }) => { // eslint-disable-next-line react-hooks/exhaustive-deps }, [data, error]); + useEffect(() => { + if (backdatedData?.message) { + dispatch(setBackdatedSettings(backdatedData.message)); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [backdatedData]); + return (
diff --git a/frontend/packages/app/src/app/pages/timesheet/index.tsx b/frontend/packages/app/src/app/pages/timesheet/index.tsx index cd656e40d..a45f36885 100644 --- a/frontend/packages/app/src/app/pages/timesheet/index.tsx +++ b/frontend/packages/app/src/app/pages/timesheet/index.tsx @@ -281,6 +281,7 @@ function Timesheet() { loadingLikedTasks={loadingLikedTasks} likedTaskData={likedTaskData} getLikedTaskData={getLikedTaskData} + oldestAllowedDate={user.backdatedSettings?.oldest_allowed_date} /> diff --git a/frontend/packages/app/src/store/user.ts b/frontend/packages/app/src/store/user.ts index 0d883a10c..3ae2b66a7 100644 --- a/frontend/packages/app/src/store/user.ts +++ b/frontend/packages/app/src/store/user.ts @@ -22,6 +22,15 @@ interface InitiDataProps { employeeName: string; } +export interface BackdatedSettings { + allow_backdated_entries: boolean; + allowed_days_employee: number; + allowed_days_manager: number; + allowed_days: number; + oldest_allowed_date: string | null; + has_manager_access: boolean; +} + export interface UserState { userName: string; image: string; @@ -36,6 +45,7 @@ export interface UserState { currencies: Array; hasBuField: boolean; hasIndustryField: boolean; + backdatedSettings: BackdatedSettings | null; } const initialState: UserState = { @@ -52,6 +62,7 @@ const initialState: UserState = { currencies: window.frappe?.boot?.currencies ?? [], hasBuField: window.frappe?.boot?.has_business_unit ?? false, hasIndustryField: window.frappe?.boot?.has_industry ?? false, + backdatedSettings: null, }; const userSlice = createSlice({ @@ -102,6 +113,9 @@ const userSlice = createSlice({ state.reportsTo = action.payload.reportsTo; state.employeeName = action.payload.employeeName; }, + setBackdatedSettings: (state, action: PayloadAction) => { + state.backdatedSettings = action.payload; + }, }, }); @@ -116,6 +130,7 @@ export const { setCurrency, setHasBuField, setHasIndustryField, + setBackdatedSettings, } = userSlice.actions; export default userSlice.reducer; diff --git a/next_pms/timesheet/api/employee.py b/next_pms/timesheet/api/employee.py index e8e746546..6c7e9acae 100644 --- a/next_pms/timesheet/api/employee.py +++ b/next_pms/timesheet/api/employee.py @@ -173,3 +173,80 @@ def wrapper(*args, **kwargs): return wrapper return decorator + + +@frappe.whitelist() +def get_backdated_settings(employee: str = None): + """ + Get backdated time entry settings for the current user/employee. + Returns the allowed backdated days limit based on user role. + """ + from frappe import get_roles + from frappe.utils import add_days, getdate, today + from hrms.hr.utils import get_holiday_dates_for_employee + + from next_pms.resource_management.api.utils.query import get_employee_leaves + + if not employee: + employee = get_employee_from_user() + + if not employee: + return { + "allow_backdated_entries": False, + "allowed_days_employee": 0, + "allowed_days_manager": 0, + "oldest_allowed_date": None, + } + + # Get settings from Timesheet Settings + allow_backdated_entries = frappe.db.get_single_value("Timesheet Settings", "allow_backdated_entries") + allowed_days_employee = frappe.db.get_single_value("Timesheet Settings", "allow_backdated_entries_till_employee") or 0 + allowed_days_manager = frappe.db.get_single_value("Timesheet Settings", "allow_backdated_entries_till_manager") or 0 + + # Check if user has manager/higher access roles + ROLES = {"Projects Manager", "HR User", "HR Manager", "Projects User"} + frappe_roles = set(get_roles()) + has_access = bool(ROLES.intersection(frappe_roles)) + + # Determine which limit applies + allowed_days = allowed_days_manager if has_access else allowed_days_employee + + # Calculate oldest allowed date considering holidays and leaves + today_date = getdate(today()) + oldest_date = add_days(today_date, -allowed_days) + + # Get holidays and leaves to adjust the calculation + holidays = get_holiday_dates_for_employee(employee, oldest_date, today_date) + leaves = get_employee_leaves( + start_date=add_days(oldest_date, -28), + end_date=add_days(today_date, 28), + employee=employee, + ) + + # Add leave dates to holidays list + for leave in leaves: + from_date = getdate(leave.from_date) + to_date = getdate(leave.to_date) + current_date = from_date + while current_date <= to_date: + holidays.append(str(current_date)) + current_date = add_days(current_date, 1) + + # Count holidays between oldest_date and today + holiday_counter = 0 + holidays = set(holidays) + for holiday in holidays: + if oldest_date <= getdate(holiday) < today_date: + holiday_counter += 1 + + # Adjust oldest date by subtracting holidays + adjusted_oldest_date = add_days(oldest_date, -holiday_counter) + + return { + "allow_backdated_entries": allow_backdated_entries, + "allowed_days_employee": allowed_days_employee, + "allowed_days_manager": allowed_days_manager, + "allowed_days": allowed_days, + "oldest_allowed_date": str(adjusted_oldest_date) if allow_backdated_entries else str(today_date), + "has_manager_access": has_access, + }