Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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 = "";
Expand All @@ -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;
Expand Down Expand Up @@ -104,14 +104,24 @@ export const Cell = ({ date, data, isHoliday, onCellClick, disabled, className }
/>
</span>
</HoverCardTrigger>
{description && (
{description && !isBackdatedDisabled && (
<HoverCardContent
className="text-left whitespace-pre text-wrap w-full max-w-96 max-h-52 overflow-auto hover-content p-0"
onClick={(e) => e.stopPropagation()}
>
<TextEditor onChange={() => {}} hideToolbar={true} readOnly={true} value={description} />
</HoverCardContent>
)}
{isBackdatedDisabled && (
<HoverCardContent
className="text-left whitespace-pre text-wrap w-full max-w-96 hover-content p-2"
onClick={(e) => e.stopPropagation()}
>
<Typography variant="p" className="text-sm text-muted-foreground">
Backdated time entry limit exceeded
</Typography>
</HoverCardContent>
)}
</TableCell>
</HoverCard>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ const Row = ({
totalCellClassName,
showEmptyCell,
hideLikeButton,
isDateBackdatedDisabled,
}: RowProps) => {
return (
<>
Expand Down Expand Up @@ -86,6 +87,7 @@ const Row = ({
isHoliday={result.isHoliday && !result.weekly_off}
onCellClick={onCellClick}
disabled={disabled}
isBackdatedDisabled={isDateBackdatedDisabled?.(date)}
/>
);
})}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export interface RowProps {
totalCellClassName?: string;
showEmptyCell?: boolean;
hideLikeButton?: boolean;
isDateBackdatedDisabled?: (date: string) => boolean;
}

export interface leaveRowProps {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export type cellProps = {
onCellClick?: (val) => void;
disabled?: boolean;
className?: string;
isBackdatedDisabled?: boolean;
};

export type HeaderProps = {
Expand Down Expand Up @@ -79,4 +80,5 @@ export type timesheetTableProps = {
likedTaskData?: Array<object>;
getLikedTaskData?: () => void;
hideLikeButton?: boolean;
oldestAllowedDate?: string | null;
};
11 changes: 11 additions & 0 deletions frontend/packages/app/src/app/components/timesheet-table/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,23 @@ export const TimesheetTable = ({
likedTaskData,
getLikedTaskData,
hideLikeButton,
oldestAllowedDate,
}: timesheetTableProps) => {
const holidayList = getHolidayList(holidays);
const [isTaskLogDialogBoxOpen, setIsTaskLogDialogBoxOpen] = useState(false);
const [selectedTask, setSelectedTask] = useState<string>("");
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(
Expand Down Expand Up @@ -146,6 +156,7 @@ export const TimesheetTable = ({
workingFrequency={workingFrequency}
workingHour={workingHour}
hideLikeButton={hideLikeButton}
isDateBackdatedDisabled={isDateBackdatedDisabled}
/>
</TableBody>
</Table>
Expand Down
20 changes: 19 additions & 1 deletion frontend/packages/app/src/app/layout/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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 = {
Expand All @@ -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 (
<ErrorFallback>
<div className="flex flex-row h-screen w-full">
Expand Down
1 change: 1 addition & 0 deletions frontend/packages/app/src/app/pages/timesheet/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,7 @@ function Timesheet() {
loadingLikedTasks={loadingLikedTasks}
likedTaskData={likedTaskData}
getLikedTaskData={getLikedTaskData}
oldestAllowedDate={user.backdatedSettings?.oldest_allowed_date}
/>
</AccordionContent>
</AccordionItem>
Expand Down
15 changes: 15 additions & 0 deletions frontend/packages/app/src/store/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -36,6 +45,7 @@ export interface UserState {
currencies: Array<string>;
hasBuField: boolean;
hasIndustryField: boolean;
backdatedSettings: BackdatedSettings | null;
}

const initialState: UserState = {
Expand All @@ -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({
Expand Down Expand Up @@ -102,6 +113,9 @@ const userSlice = createSlice({
state.reportsTo = action.payload.reportsTo;
state.employeeName = action.payload.employeeName;
},
setBackdatedSettings: (state, action: PayloadAction<BackdatedSettings>) => {
state.backdatedSettings = action.payload;
},
},
});

Expand All @@ -116,6 +130,7 @@ export const {
setCurrency,
setHasBuField,
setHasIndustryField,
setBackdatedSettings,
} = userSlice.actions;

export default userSlice.reducer;
77 changes: 77 additions & 0 deletions next_pms/timesheet/api/employee.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}