From 7395d3e048a5c3a4205c35a0699646966fb8b90f Mon Sep 17 00:00:00 2001 From: Casey Date: Tue, 20 Jan 2026 17:06:28 -0600 Subject: [PATCH] update calendar functionality and holidays --- custom_ui/api/db/employees.py | 49 + custom_ui/api/db/estimates.py | 8 +- custom_ui/api/db/general.py | 24 +- custom_ui/db_utils.py | 13 + custom_ui/hooks.py | 4 + custom_ui/install.py | 56 +- frontend/src/api.js | 26 +- .../calendar/CalendarNavigation.vue | 13 +- ...ctsCalendar.vue => SNWProjectCalendar.vue} | 1106 ++++++++++------- 9 files changed, 859 insertions(+), 440 deletions(-) create mode 100644 custom_ui/api/db/employees.py rename frontend/src/components/calendar/jobs/{ProjectsCalendar.vue => SNWProjectCalendar.vue} (51%) diff --git a/custom_ui/api/db/employees.py b/custom_ui/api/db/employees.py new file mode 100644 index 0000000..1263c19 --- /dev/null +++ b/custom_ui/api/db/employees.py @@ -0,0 +1,49 @@ +import frappe, json +from custom_ui.db_utils import build_success_response, build_error_response +# =============================================================================== +# EMPLOYEE API METHODS +# =============================================================================== + +@frappe.whitelist() +def get_employees(company: str, roles=[]): + """Get a list of employees for a given company. Can be filtered by role.""" + roles = json.loads(roles) if isinstance(roles, str) else roles + filters = {"company": company} + if roles: + filters["designation"] = ["in", roles] + try: + employee_names = frappe.get_all( + "Employee", + filters=filters, + pluck="name" + ) + employees = [frappe.get_doc("Employee", name).as_dict() for name in employee_names] + return build_success_response(employees) + except Exception as e: + return build_error_response(str(e), 500) + +@frappe.whitelist() +def get_employees_organized(company: str, roles=[]): + """Get all employees for a company organized by designation.""" + roles = json.loads(roles) if isinstance(roles, str) else roles + try: + filters = {"company": company} + if roles: + filters["designation"] = ["in", roles] + employee_names = frappe.get_all( + "Employee", + filters=filters, + pluck="name" + ) + employees = [frappe.get_doc("Employee", name).as_dict() for name in employee_names] + + organized = {} + for emp in employees: + designation = emp.get("designation", "Unassigned") + if designation not in organized: + organized[designation] = [] + organized[designation].append(emp) + + return build_success_response(organized) + except Exception as e: + return build_error_response(str(e), 500) \ No newline at end of file diff --git a/custom_ui/api/db/estimates.py b/custom_ui/api/db/estimates.py index bf7e260..dcfc579 100644 --- a/custom_ui/api/db/estimates.py +++ b/custom_ui/api/db/estimates.py @@ -1,7 +1,7 @@ import frappe, json from frappe.utils.pdf import get_pdf from custom_ui.api.db.general import get_doc_history -from custom_ui.db_utils import process_query_conditions, build_datatable_dict, get_count_or_filters, build_success_response, build_error_response +from custom_ui.db_utils import DbUtils, process_query_conditions, build_datatable_dict, get_count_or_filters, build_success_response, build_error_response from werkzeug.wrappers import Response from custom_ui.api.db.clients import check_if_customer, convert_lead_to_customer from custom_ui.services import DbService, ClientService, AddressService, ContactService @@ -10,6 +10,12 @@ from custom_ui.services import DbService, ClientService, AddressService, Contact # ESTIMATES & INVOICES API METHODS # =============================================================================== +@frappe.whitelist() +def get_estimate_table_data_v2(filters={}, sortings=[], page=1, page_size=10): + """Get paginated estimate table data with filtering and sorting.""" + print("DEBUG: Raw estimate options received:", filters, sortings, page, page_size) + filters, sortings, page, page_size = DbUtils.process_query_conditions(filters, sortings, page, page_size) + @frappe.whitelist() def get_estimate_table_data(filters={}, sortings=[], page=1, page_size=10): diff --git a/custom_ui/api/db/general.py b/custom_ui/api/db/general.py index 8e2524c..5aed7e1 100644 --- a/custom_ui/api/db/general.py +++ b/custom_ui/api/db/general.py @@ -1,5 +1,6 @@ import frappe -from custom_ui.db_utils import build_history_entries +from custom_ui.db_utils import build_history_entries, build_success_response, build_error_response +from datetime import datetime, timedelta def get_doc_history(doctype, docname): """Get the history of changes for a specific document.""" @@ -56,4 +57,23 @@ def search_any_field(doctype, text): query, [like] * len(conditions), as_dict=True - ) \ No newline at end of file + ) + +@frappe.whitelist() +def get_week_holidays(week_start_date: str): + """Get holidays within a week starting from the given date.""" + + start_date = datetime.strptime(week_start_date, "%Y-%m-%d").date() + end_date = start_date + timedelta(days=6) + + holidays = frappe.get_all( + "Holiday", + filters={ + "holiday_date": ["between", (start_date, end_date)] + }, + fields=["holiday_date", "description"], + order_by="holiday_date asc" + ) + + print(f"DEBUG: Retrieved holidays from {start_date} to {end_date}: {holidays}") + return build_success_response(holidays) \ No newline at end of file diff --git a/custom_ui/db_utils.py b/custom_ui/db_utils.py index 0bf9915..e9be1be 100644 --- a/custom_ui/db_utils.py +++ b/custom_ui/db_utils.py @@ -233,3 +233,16 @@ def build_history_entries(comments, versions): def normalize_name(name: str, split_target: str = "_") -> str: """Normalize a name by splitting off anything after and including the split_target.""" return name.split(split_target)[0] if split_target in name else name + +class DbUtils: + + @staticmethod + def process_datatable_request(filters, sortings, page, page_size): + # turn filters and sortings from json strings to dicts/lists + if isinstance(filters, str): + filters = json.loads(filters) + if isinstance(sortings, str): + sortings = json.loads(sortings) + page = int(page) + page_size = int(page_size) + return filters, sortings,page, page_size \ No newline at end of file diff --git a/custom_ui/hooks.py b/custom_ui/hooks.py index e7ec24b..04541cf 100644 --- a/custom_ui/hooks.py +++ b/custom_ui/hooks.py @@ -26,6 +26,10 @@ add_to_apps_screen = [ # "has_permission": "custom_ui.api.permission.has_app_permission" } ] + +requires = [ + "holidays==0.89" +] # Apps # ------------------ diff --git a/custom_ui/install.py b/custom_ui/install.py index 0453f49..d264307 100644 --- a/custom_ui/install.py +++ b/custom_ui/install.py @@ -4,6 +4,8 @@ import subprocess import sys import frappe from .utils import create_module +import holidays +from datetime import date, timedelta def after_install(): create_module() @@ -29,6 +31,8 @@ def after_migrate(): for doctype in doctypes_to_refresh: frappe.clear_cache(doctype=doctype) frappe.reload_doctype(doctype) + + check_and_create_holiday_list() # update_address_fields() # build_frontend() @@ -951,4 +955,54 @@ def build_missing_field_specs(custom_fields, missing_fields): missing_field_specs[doctype].append(field_spec) break - return missing_field_specs \ No newline at end of file + return missing_field_specs + +def check_and_create_holiday_list(year=2026, country="US", weekly_off="Sunday"): + """Check if Holiday List for the given year exists, if not create it.""" + print(f"\nšŸ”§ Checking for Holiday List for {country} in {year}...") + holiday_list_name = f"{country} Holidays {year}" + + if frappe.db.exists("Holiday List", holiday_list_name): + print(f"āœ… Holiday List '{holiday_list_name}' already exists.") + return + else: + print(f"āŒ Holiday List '{holiday_list_name}' does not exist. Creating...") + us_holidays = holidays.US(years=[year]) + sundays = get_all_sundays(year) + hl = frappe.get_doc({ + "doctype": "Holiday List", + "holiday_list_name": holiday_list_name, + "country": country, + "year": year, + "from_date": f"{year}-01-01", + "to_date": f"{year}-12-31", + "weekly_off": weekly_off, + "holidays": [ + { + "holiday_date": holiday_date, + "description": holiday_name + } for holiday_date, holiday_name in us_holidays.items() + ] + }) + for sunday in sundays: + hl.append("holidays", { + "holiday_date": sunday, + "description": "Sunday" + }) + hl.insert() + # hl.make_holiday_entries() + frappe.db.commit() + print(f"āœ… Holiday List '{holiday_list_name}' created successfully.") + +def get_all_sundays(year): + sundays = [] + d = date(year, 1, 1) + + while d.weekday() != 6: + d += timedelta(days=1) + + while d.year == year: + sundays.append(d) + d += timedelta(days=7) + + return sundays \ No newline at end of file diff --git a/frontend/src/api.js b/frontend/src/api.js index 3663e52..18c1ae0 100644 --- a/frontend/src/api.js +++ b/frontend/src/api.js @@ -47,7 +47,11 @@ const FRAPPE_GET_CLIENT_TABLE_DATA_METHOD = "custom_ui.api.db.clients.get_client const FRAPPE_GET_CLIENT_TABLE_DATA_V2_METHOD = "custom_ui.api.db.clients.get_clients_table_data_v2"; const FRAPPE_GET_CLIENT_METHOD = "custom_ui.api.db.clients.get_client_v2"; const FRAPPE_GET_CLIENT_NAMES_METHOD = "custom_ui.api.db.clients.get_client_names"; - +// Employee methods +const FRAPPE_GET_EMPLOYEES_METHOD = "custom_ui.api.db.employees.get_employees"; +const FRAPPE_GET_EMPLOYEES_ORGANIZED_METHOD = "custom_ui.api.db.employees.get_employees_organized"; +// Other methods +const FRAPPE_GET_WEEK_HOLIDAYS_METHOD = "custom_ui.api.db.general.get_week_holidays"; class Api { // ============================================================================ // CORE REQUEST METHOPD @@ -589,6 +593,26 @@ class Api { return data; } + // ============================================================================ + // EMPLOYEE METHODS + // ============================================================================ + + static async getEmployees(company, roles = []) { + return await this.request(FRAPPE_GET_EMPLOYEES_METHOD, { company, roles }); + } + + static async getEmployeesOrganized (company, roles = []) { + return await this.request(FRAPPE_GET_EMPLOYEES_ORGANIZED_METHOD, { company, roles }); + } + + // ============================================================================ + // OTHER METHODS + // ============================================================================ + + static async getWeekHolidays(startDate) { + return await this.request(FRAPPE_GET_WEEK_HOLIDAYS_METHOD, { weekStartDate: startDate }); + } + // ============================================================================ // GENERIC DOCTYPE METHODS // ============================================================================ diff --git a/frontend/src/components/calendar/CalendarNavigation.vue b/frontend/src/components/calendar/CalendarNavigation.vue index d16cee5..2413851 100644 --- a/frontend/src/components/calendar/CalendarNavigation.vue +++ b/frontend/src/components/calendar/CalendarNavigation.vue @@ -1,6 +1,6 @@ @@ -38,10 +41,12 @@ import TabPanel from 'primevue/tabpanel'; import TabPanels from 'primevue/tabpanels'; import ScheduleBid from '../calendar/bids/ScheduleBid.vue'; import JobsCalendar from '../calendar/jobs/JobsCalendar.vue'; -import InstallsCalendar from './jobs/ProjectsCalendar.vue'; +import SNWProjectCalendar from './jobs/SNWProjectCalendar.vue'; import { useNotificationStore } from '../../stores/notifications-primevue'; +import { useCompanyStore } from '../../stores/company'; const notifications = useNotificationStore(); +const companyStore = useCompanyStore(); \ No newline at end of file