Skip to content
Merged
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 @@ -202,6 +202,7 @@ const AddTime = ({
const { data: projects, isLoading: isProjectLoading } = useFrappeGetCall("frappe.client.get_list", {
doctype: "Project",
fields: ["name", "project_name"],
filters: window.frappe?.boot?.global_filters.project,
limit_page_length: "null",
});

Expand Down
17 changes: 10 additions & 7 deletions frontend/packages/app/src/app/layout/sidebar/index.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/**
* External dependencies.
*/
import React, { useEffect, useState } from "react";
import React, { useCallback, useEffect, useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import { NavLink } from "react-router-dom";
import { useLocation } from "react-router-dom";
Expand Down Expand Up @@ -84,7 +84,9 @@ const Sidebar = () => {
key: "task",
isPmRoute: false,
},
{
];
if (!user.roles.includes("Contractor") || user.userName == "Administrator") {
routes.push({
to: RESOURCE_MANAGEMENT,
label: "Resource Management",
key: "resource-management",
Expand All @@ -109,15 +111,16 @@ const Sidebar = () => {
icon: FolderKanban,
},
],
},
];
});
}
const toggleNestedRoutes = (key: string) => {
setOpenRoutes((prev) => ({ ...prev, [key]: !prev[key] }));
};

const handleSidebarCollapse = () => {
const handleSidebarCollapse = useCallback(() => {
dispatch(setSidebarCollapsed(checkIsMobile()));
};
}, [dispatch]);

useEffect(() => {
setLocalStorage("next-pms:isSidebarCollapsed", user.isSidebarCollapsed);
}, [user.isSidebarCollapsed]);
Expand All @@ -127,7 +130,7 @@ const Sidebar = () => {
}
window.addEventListener("resize", handleSidebarCollapse);
return () => window.removeEventListener("resize", handleSidebarCollapse);
}, []);
}, [dispatch, handleSidebarCollapse]);

return (
<ErrorFallback>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ const ProjectComboBox = ({
...(debouncedSearch ? [["project_name", "like", `%${debouncedSearch}%`]] : []),
...(status ? [["status", "=", status]] : []),
...(value && !debouncedSearch ? [["name", "=", value]] : []),
...(window.frappe?.boot?.global_filters.project || []),
],
limit: length,
orderBy: { field: "creation", order: "desc" },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ export const Header = ({
filters: {
doctype: "Project",
fields: ["name", "project_name as label"],
filters: window.frappe?.boot?.global_filters.project,
or_filters: [
["name", "like", `%${projectSearch}%`],
["project_name", "like", `%${projectSearch}%`],
Expand Down
39 changes: 22 additions & 17 deletions frontend/packages/app/src/app/pages/timesheet/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -173,27 +173,32 @@ function Timesheet() {
return (
<>
<Header className="justify-end gap-x-3">
<Button variant="outline" onClick={handleAddLeave} title="Add Time">
<Plus />
Leave
</Button>
{window.frappe?.boot?.user?.can_create.includes("Leave Application") && (
<Button variant="outline" onClick={handleAddLeave} title="Add Time">
<Plus />
Leave
</Button>
)}

<Button onClick={handleAddTime} title="Add Time">
<Plus />
Time
</Button>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline">
<EllipsisVertical />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="mr-2 [&_div]:cursor-pointer [&_div]:gap-x-2">
<DropdownMenuItem onClick={handleImportTaskFromGoogleCalendar}>
<CalendarArrowDown />
<Typography variant="p">Import Events From Google Calendar</Typography>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
{window.frappe?.boot?.is_calendar_setup && (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline">
<EllipsisVertical />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="mr-2 [&_div]:cursor-pointer [&_div]:gap-x-2">
<DropdownMenuItem onClick={handleImportTaskFromGoogleCalendar}>
<CalendarArrowDown />
<Typography variant="p">Import Events From Google Calendar</Typography>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)}
</Header>

{isLoading && Object.keys(timesheet.data?.data).length == 0 ? (
Expand Down
12 changes: 12 additions & 0 deletions frontend/packages/app/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,18 @@ import "@next-pms/design-system/index.css";
import "@next-pms/resource-management/index.css";
import "./global.css";

if (import.meta.env.DEV) {
fetch("/api/method/next_pms.www.next-pms.index.get_context_for_dev", {
method: "POST",
})
.then((response) => response.json())
.then((values) => {
const v = JSON.parse(values.message);
if (!window.frappe) window.frappe = {};
window.frappe.boot = v;
});
}

ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<App />
Expand Down
3 changes: 1 addition & 2 deletions frontend/packages/app/src/route.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,7 @@ const AuthenticatedRoute = () => {
dispatch(setViews(res.message));
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
}, [call, dispatch, user.roles.length, views.views.length]);

if (isLoading) {
return <></>;
Expand Down
9 changes: 7 additions & 2 deletions frontend/packages/app/src/types/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { TaskStatusType } from "./task";

import { type ViewState } from "../store/view";
export type Employee = {
name: string;
image: string;
Expand Down Expand Up @@ -45,15 +45,20 @@ export interface DocMetaProps {
}
declare global {
interface Window {
frappe?: {
frappe: {
boot?: {
user?: {
roles?: string[];
can_create: string[];
};
currencies?: string[];
has_business_unit?: boolean;
has_industry?: boolean;
desk_theme?: string;
views: ViewState["views"];
is_calendar_setup: boolean;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
global_filters: { [key: string]: Array<any> };
};
};
}
Expand Down
5 changes: 5 additions & 0 deletions next_pms/resource_management/api/utils/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,11 @@ def filter_project_list(project_name=None, customer=None, billing_type=None, pag


def resource_api_permissions_check():
if "Contractor" in frappe.get_roles() and frappe.session.user != "Administrator":
frappe.throw(
frappe._("You don't have permission to access this resource"),
frappe.PermissionError,
)
frappe.only_for(["Projects Manager", "Projects User", "Employee"], message=True)

roles = frappe.get_roles()
Expand Down
27 changes: 24 additions & 3 deletions next_pms/timesheet/api/project.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import json

import frappe
from erpnext.accounts.report.utils import get_rate_as_at
from frappe import get_list, get_meta, whitelist
from frappe.utils import flt, getdate
Expand All @@ -13,6 +14,7 @@ def get_projects(
currency=None,
fields=None,
filters=None,
or_filters=None,
start=0,
order_by="modified desc",
):
Expand All @@ -28,19 +30,28 @@ def get_projects(
if "custom_currency" not in fields:
fields.append("custom_currency")

if not filters:
filters = get_project_filter_for_contractor()
else:
filters += get_project_filter_for_contractor()
project_lists = get_list(
"Project",
fields=fields,
filters=filters,
limit_start=start,
limit=limit,
order_by=order_by,
or_filters=or_filters,
)
count = get_count("Project", filters=filters)
count = get_count("Project", filters=filters, or_filters=or_filters)
has_more = int(start) + int(limit) < count

if not limit:
has_more = False
if not currency or len(currency) == 0:
return {
"data": project_lists,
"has_more": int(start) + int(limit) < count,
"has_more": has_more,
"total_count": count,
}

Expand All @@ -58,7 +69,7 @@ def get_projects(

return {
"data": project_lists,
"has_more": int(start) + int(limit) < count,
"has_more": has_more,
"total_count": count,
}

Expand All @@ -75,3 +86,13 @@ def get_currency_fields(meta_fields):
def convert(value, rate):
converted_value = flt(value) * (rate or 1)
return converted_value


def get_project_filter_for_contractor(only_list=False):
if "Contractor" in frappe.get_roles() and frappe.session.user != "Administrator":
names = frappe.share.get_shared("Project", frappe.session.user, filters=[["everyone", "!=", True]])
if only_list:
return names
return [["name", "in", names]]

return []
15 changes: 11 additions & 4 deletions next_pms/timesheet/api/task.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from pypika import Criterion, Order

from . import get_count
from .project import get_project_filter_for_contractor


@frappe.whitelist()
Expand Down Expand Up @@ -46,10 +47,16 @@ def get_task_list(
if "project_name" in field_list:
field_list.remove("project_name")
# Limit the task fetched based on the projects the user has access to.
if projects:
projects = frappe.get_list("Project", pluck="name", filters={"name": ["in", projects]})
else:
projects = frappe.get_list("Project", pluck="name")
allowed_projects = get_project_filter_for_contractor(only_list=True)
project_filters = {}

if allowed_projects:
if projects:
project_filters["name"] = ["in", list(set(allowed_projects).intersection(set(projects)))]
else:
project_filters["name"] = ["in", allowed_projects]

projects = frappe.get_list("Project", pluck="name", filters=project_filters)

filter = {"project": ["in", projects]}
if not projects:
Expand Down
13 changes: 12 additions & 1 deletion next_pms/timesheet/doc_events/timesheet.py
Original file line number Diff line number Diff line change
Expand Up @@ -287,7 +287,12 @@ def publish_timesheet_update(employee, start_date):
after_commit=True,
room=get_site_room(),
)

publish_realtime(
f"timesheet_update::{employee}",
{"message": data},
after_commit=True,
user=frappe.session.user,
)
res = get_compact_view_data(
date=get_date_str(start_date),
max_week=2,
Expand All @@ -300,6 +305,12 @@ def publish_timesheet_update(employee, start_date):
after_commit=True,
room=get_site_room(),
)
publish_realtime(
"timesheet_info",
{"message": res, "employee": employee, "date": get_date_str(start_date)},
after_commit=True,
user=frappe.session.user,
)


def validate_start_date(doc):
Expand Down
37 changes: 37 additions & 0 deletions next_pms/www/next-pms/index.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import frappe

from next_pms.timesheet.api.app import has_bu_field, has_industry_field
from next_pms.timesheet.api.project import get_project_filter_for_contractor
from next_pms.timesheet.doctype.pms_view_setting.pms_view_setting import get_views

no_cache = 1
Expand Down Expand Up @@ -35,6 +36,8 @@ def get_context(context):
boot["currencies"] = frappe.get_all("Currency", pluck="name", filters={"enabled": 1})
boot["has_business_unit"] = has_bu_field()
boot["has_industry"] = has_industry_field()
boot["is_calendar_setup"] = is_google_calendar_enabled()
boot["global_filters"] = get_global_filters()
boot_json = frappe.as_json(boot, indent=None, separators=(",", ":"))
boot_json = SCRIPT_TAG_PATTERN.sub("", boot_json)

Expand All @@ -52,3 +55,37 @@ def get_context(context):
context["app_name"] = "Next PMS"

return context


@frappe.whitelist(methods=["POST"], allow_guest=True)
def get_context_for_dev():
if not frappe.conf.developer_mode:
frappe.throw(frappe._("This method is only meant for developer mode"))
return json.loads(get_boot())


def get_boot():
boot = frappe.sessions.get()
boot["views"] = get_views()
boot["currencies"] = frappe.get_all("Currency", pluck="name", filters={"enabled": 1})
boot["has_business_unit"] = has_bu_field()
boot["has_industry"] = has_industry_field()
boot["is_calendar_setup"] = is_google_calendar_enabled()
boot["app_name"] = "Next PMS"
boot["global_filters"] = get_global_filters()
boot_json = frappe.as_json(boot, indent=None, separators=(",", ":"))
boot_json = SCRIPT_TAG_PATTERN.sub("", boot_json)

boot_json = CLOSING_SCRIPT_TAG_PATTERN.sub("", boot_json)
boot_json = json.dumps(boot_json)
return boot_json


def is_google_calendar_enabled():
return bool(frappe.db.exists("Google Calendar", {"user": frappe.session.user, "enable": 1}))


def get_global_filters():
filters = {}
filters["project"] = get_project_filter_for_contractor()
return filters
Loading