Compare commits

...

610 Commits

Author SHA1 Message Date
Chamika J
11a6224fb3 fix(holiday-api): add error handling and fallback for countries with states API
- Implemented try-catch block in `getCountriesWithStates` to handle API errors gracefully.
- Added logging for errors when fetching countries, with a fallback to static data if the API call fails.
- Updated the `SettingsPage` to utilize the selected country code state for improved state selection logic.
2025-08-01 17:17:57 +05:30
Chamika J
8f407b45a9 feat(holiday-calendar): implement holiday calendar component with CRUD functionality
- Added a new `HolidayCalendar` component for managing custom and official holidays.
- Integrated holiday type fetching and population logic to streamline holiday management.
- Enhanced UI with modals for creating, editing, and deleting holidays, including validation and user feedback.
- Updated styles for improved visual presentation and user interaction.
- Refactored sidebar and routes to accommodate new component structure and localization updates.
2025-08-01 16:54:58 +05:30
Chamika J
1a64115063 Merge branch 'release-v2.1.4' of https://github.com/Worklenz/worklenz into feature/team-utilization 2025-08-01 09:31:38 +05:30
Chamika J
7c42087854 feat(update-notification): implement update notification system for new versions
- Added a service worker message handler to check for updates and notify users.
- Created `UpdateNotification` component to display update prompts with options to reload or dismiss.
- Introduced `UpdateNotificationProvider` to manage update state and notifications globally.
- Implemented `useUpdateChecker` hook for periodic update checks and user notification management.
- Updated localization files to include new strings related to update notifications.
- Enhanced service worker functionality to support hard reloads and update checks.
2025-07-31 16:12:04 +05:30
Chamika J
14c89dec24 chore(tests): remove obsolete SQL test scripts for sort order validation
- Deleted `test_sort_fix.sql` and `test_sort_orders.sql` as they are no longer needed for the current implementation.
- These scripts were previously used to verify sort order constraints and helper functions, but have been deemed unnecessary.
2025-07-31 15:51:43 +05:30
Chamika J
b1bdf0ac11 feat(hubspot): add dark mode support and light color scheme for chat widget
- Removed production check to ensure HubSpot script loads in all environments.
- Implemented dark mode CSS injection when the dark theme is active.
- Added a style block to enforce a light color scheme for the HubSpot chat widget, improving visibility and user experience.
2025-07-31 15:44:04 +05:30
Chamika J
7635676289 feat(trial-user-limits): implement trial member limit checks in project and team controllers
- Added TRIAL_MEMBER_LIMIT constant to enforce a maximum number of trial users in project and team member controllers.
- Implemented logic to check current trial members against the limit during user addition, providing appropriate responses for exceeding limits.
- Updated relevant controllers to utilize the new trial member limit functionality, enhancing subscription management for trial users.
- Enhanced error messaging to guide users on upgrading their subscription for additional members.
2025-07-31 12:56:28 +05:30
Chamika J
903a9475b1 feat(sri-lankan-holidays): add comprehensive holiday management for Sri Lanka
- Introduced a new SQL migration file to populate Sri Lankan holidays from verified sources for the years 2020-2050.
- Implemented holiday data fetching and population logic in AdminCenterController and HolidayController to automatically sync holidays for Sri Lanka.
- Added a holiday data provider service to centralize holiday data retrieval and storage.
- Created a script for updating Sri Lankan holidays, including generating SQL for new years and validating holiday data.
- Developed documentation outlining the annual update process for Sri Lankan holidays, ensuring accurate data management.
- Included a JSON file containing detailed holiday information and metadata for Sri Lankan public holidays.
2025-07-31 10:43:49 +05:30
chamiakJ
13984fcfd4 Merge branch 'release-v2.1.4' of https://github.com/Worklenz/worklenz into feature/team-utilization 2025-07-31 06:57:50 +05:30
chamikaJ
2bd6c19c13 refactor(layouts): simplify MainLayout and enhance styling
- Removed unused imports and performance monitoring hooks from MainLayout.
- Updated layout structure to improve responsiveness and styling, including sticky header and optimized content padding.
- Adjusted home page layout to reduce margin and improve spacing for better visual consistency.
- Enhanced TodoList component with collapsible sections for improved user interaction and task management.
- Streamlined project and schedule pages by removing unnecessary margin adjustments, ensuring a cleaner layout.
2025-07-30 17:20:20 +05:30
chamikaJ
374595261f feat(task-list-v2): enhance sticky column behavior and dark mode support
- Updated DropSpacer and EmptyGroupMessage components to accept an optional isDarkMode prop for improved styling in dark mode.
- Enhanced task rendering in TaskRow to dynamically adjust background colors based on dark mode and drag states.
- Refactored useTaskRowColumns to support sticky column positioning and hover effects, ensuring a consistent user experience across different themes.
- Improved overall visual feedback during task interactions, including drag-and-drop operations.
2025-07-30 16:25:29 +05:30
chamikaJ
b6c056dd1a feat(task-status-dropdown): enhance task status update and group movement handling
- Added logic to optimistically update task status in Redux for immediate feedback.
- Implemented group movement handling when tasks are updated based on their status, ensuring tasks are moved between groups as needed.
- Improved socket event emissions for real-time updates, including parent task handling.
- Refactored group selection logic to streamline target group identification based on status ID and group value.
2025-07-30 15:26:27 +05:30
chamikaJ
81e1872c1f refactor(task-list): simplify drag-and-drop functionality and enhance task rendering
- Removed droppable functionality from TaskGroupHeader and replaced it with a more streamlined approach in TaskListV2Table.
- Introduced DropSpacer component to improve visual feedback during task dragging.
- Updated task rendering logic in TaskRow to enhance user experience with clearer drop indicators.
- Refactored useDragAndDrop hook to manage drop positions more effectively, ensuring tasks can only be reordered within the same group.
- Improved socket event handling for task sorting to ensure accurate updates during drag-and-drop operations.
2025-07-30 15:08:28 +05:30
chamikaJ
f085f87107 feat(timer): enhance timer conflict resolution and state management
- Updated `useTaskTimerWithConflictCheck` to immediately update Redux state when stopping a conflicting timer, ensuring synchronization between task management and time tracking slices.
- Adjusted the timer start logic to trigger immediately after state updates, improving user experience during timer transitions.
- Added necessary imports for Redux dispatch and state management to support the new functionality.
2025-07-30 13:27:41 +05:30
chamikaJ
d9700a9b2c refactor(assignee-selector, kanban-group): clean up code formatting and improve modal confirmation handling
- Removed unnecessary line breaks and improved code readability in AssigneeSelector and KanbanGroup components.
- Refactored modal confirmation logic in KanbanGroup to streamline delete operations.
- Enhanced the dropdown and member list rendering in AssigneeSelector for better performance and user experience.
2025-07-30 13:09:43 +05:30
chamikaJ
9da6dced01 Merge branch 'release-v2.1.4' of https://github.com/Worklenz/worklenz into feature/team-utilization 2025-07-30 12:56:56 +05:30
chamikaJ
9dfc1fa375 feat(admin-center): implement admin center settings retrieval and enhance holiday population logic
- Added a new API endpoint in AdminCenterController to fetch admin center settings, including organization details.
- Updated the admin center API router to include the new settings route.
- Enhanced the holiday controller to check for recent holiday population before attempting to repopulate, preventing duplicate entries.
- Improved the holiday calendar component to manage holiday population attempts and display loading states.
- Updated localization files to support new messages related to calculation methods and holiday settings.
2025-07-30 11:35:12 +05:30
chamikaJ
5cce3bc613 feat(localization): add timer conflict handling and update translations
- Introduced a new hook `useTaskTimerWithConflictCheck` to manage timer conflicts, prompting users when a timer is already running for a different task.
- Updated localization files for Albanian, German, English, Spanish, Portuguese, and Chinese to include new translation keys related to timer conflict handling and cancellation.
- Refactored components to utilize the new timer hook, enhancing user experience by preventing overlapping timers.
2025-07-30 10:13:08 +05:30
chamikaJ
c53ab511bf Merge branch 'main' of https://github.com/Worklenz/worklenz into release-v2.1.4 2025-07-29 14:19:44 +05:30
Chamika J
7b9a16fd72 Merge pull request #307 from Worklenz/development
refactor: update .gitignore and remove obsolete files
2025-07-29 14:16:55 +05:30
chamikaJ
8830af2cbb refactor: update .gitignore and remove obsolete files
- Added .cursor and .claude directories to .gitignore to prevent tracking of temporary files.
- Deleted obsolete .claude/settings.local.json and .cursor/rules/antd-components.mdc files to clean up the repository and remove unnecessary configurations.
2025-07-29 14:16:52 +05:30
chamikaJ
b915de2b93 feat(reporting): enhance date handling and export functionality
- Improved date range handling in ReportingControllerBaseWithTimezone to support various date formats from the frontend, ensuring robust parsing and timezone conversion.
- Updated SQL queries to use consistent table aliases for clarity.
- Added export functionality for project members and tasks in ProjectReportsDrawer, allowing users to download relevant data.
- Enhanced the Excel export handler in ProjectsReports to streamline project data exports based on the current session's team name.
2025-07-29 13:05:55 +05:30
chamikaJ
29b8c1b2af feat(task-context-menu): add copy link functionality and update translations
- Implemented a new "Copy link to task" feature in the task context menu, allowing users to easily copy task links to the clipboard.
- Added corresponding success and error messages for link copying.
- Updated localization files for Albanian, German, English, Spanish, Portuguese, and Chinese to include new translation keys for the copy link feature.
2025-07-29 12:49:51 +05:30
chamikaJ
c2b231d5cc feat(survey-modal): add survey modal configuration to environment files
- Introduced a new environment variable VITE_ENABLE_SURVEY_MODAL to control the visibility of the survey modal.
- Updated .env.example to include the new variable with a default value of true.
- Modified SurveyPromptModal component to check the environment variable before displaying the modal, enhancing user experience by allowing toggling of the feature.
2025-07-29 11:13:08 +05:30
chamikaJ
53a28cf489 refactor(localization): update task-related translations and improve user activity feed
- Added new translation keys for recent tasks and time logged tasks in Albanian, German, English, Spanish, Portuguese, and Chinese localization files.
- Enhanced user activity feed to switch between recent tasks and time logged tasks, improving user experience.
- Updated the date formatting utility to support locale-specific formatting for better internationalization.
- Refactored task activity list and time logged task list components to utilize a table layout for improved readability.
2025-07-29 10:19:28 +05:30
Chamika J
e8ccc2a533 Merge pull request #306 from Worklenz/feature/task-activities-by-user
Feature/task activities by user
2025-07-29 08:46:13 +05:30
Chamika J
f24c0d8955 Merge branch 'release-v2.1.4' into feature/task-activities-by-user 2025-07-29 08:46:05 +05:30
chamiakJ
069ae6ccb1 feat(projects-controller): add currency and budget fields to project details 2025-07-28 18:31:30 +05:30
Chamika J
01a580d992 Merge pull request #304 from Worklenz/fix/reporting-sidebar-style-fix
feat(survey-localization): add survey localization files for multiple…
2025-07-28 16:57:33 +05:30
chamikaJ
c2e670c9a2 feat(survey-localization): add survey localization files for multiple languages
- Introduced new localization JSON files for Albanian, German, English, Spanish, Portuguese, and Chinese to support the survey feature.
- Each file includes translations for modal titles, button texts, and feedback messages to enhance user experience across different languages.
- Updated the SurveyPromptModal component to utilize these localization entries for improved accessibility and user engagement.
2025-07-28 16:57:40 +05:30
Chamika J
25042baf71 Merge pull request #303 from Worklenz/fix/reporting-sidebar-style-fix
feat(account-setup): implement skip functionality and update localiza…
2025-07-28 16:31:53 +05:30
chamikaJ
e8d21ee187 feat(account-setup): implement skip functionality and update localization
- Added a state to manage the skipping process during account setup, enhancing user experience.
- Updated button behavior to reflect the skipping state and provide feedback to users.
- Included new localization entries for the "skipping" status in multiple languages (Albanian, German, English, Spanish, Portuguese, Chinese).
- Refined HubSpot widget styling to ensure better integration with the app's UI.
2025-07-28 16:30:21 +05:30
Chamika J
a8d1446b0d Merge pull request #302 from Worklenz/fix/reporting-sidebar-style-fix
feat(hubspot-integration): refine HubSpot widget styling and add acco…
2025-07-28 16:14:03 +05:30
chamikaJ
2082934cd5 feat(hubspot-integration): refine HubSpot widget styling and add account setup skip functionality
- Enhanced CSS targeting for HubSpot widget elements to prevent interference with the Worklenz app UI.
- Introduced a new function to allow users to bypass team member validation during account setup, improving user experience.
- Updated the button click handler to utilize the new skip functionality for a smoother setup process.
2025-07-28 16:12:12 +05:30
Chamika J
4debcd6aa5 Merge pull request #301 from Worklenz/fix/reporting-sidebar-style-fix
Fix/reporting sidebar style fix
2025-07-28 15:45:34 +05:30
chamikaJ
76adb89caf feat(task-filters): enhance sorting functionality and localization updates
- Added sorting options to task filters, including clear sort, sort ascending, sort descending, and sort by field.
- Updated localization files for multiple languages (Albanian, German, English, Spanish, Portuguese, Chinese) to include new sorting terms.
- Implemented a SortDropdown component for improved user experience in task management.
- Integrated sorting state management in the task management slice for better data handling.
2025-07-28 15:45:12 +05:30
chamikaJ
703a6425fe feat(surveys): add survey tables and initial data for account setup questionnaire
- Created tables for surveys, survey questions, survey responses, and survey answers to support the account setup process.
- Added default account setup survey and corresponding questions to the database.
- Implemented necessary indexes and constraints for data integrity and performance.
2025-07-28 15:17:21 +05:30
Chamika J
e2c9e19b83 Merge pull request #300 from Worklenz/fix/reporting-sidebar-style-fix
refactor(survey-submission): update validation logic and submission d…
2025-07-28 15:08:43 +05:30
chamikaJ
e2a749e0b6 refactor(survey-submission): update validation logic and submission data handling
- Modified the survey submission validator to make both answer_text and answer_json optional, allowing users to submit empty answers.
- Refactored the SurveyPromptModal component to only include answered questions in the submission data, improving data handling and clarity.
2025-07-28 15:07:09 +05:30
Chamika J
2c0b0ac4c5 Merge pull request #299 from Worklenz/fix/reporting-sidebar-style-fix
Fix/reporting sidebar style fix
2025-07-28 14:55:11 +05:30
chamikaJ
dd511b236f refactor(reporting-layout): streamline sidebar and content layout
- Replaced the existing sidebar implementation with a new ReportingSider component that accepts collapse state and toggle function as props.
- Simplified the ReportingCollapsedButton component for better readability and functionality.
- Updated layout styles to enhance responsiveness and maintain consistent margins.
- Removed unused CSS styles related to the sidebar for cleaner code.
2025-07-28 14:54:54 +05:30
chamikaJ
2c860b0cc8 feat(localization): update password-related translations in German and Spanish signup forms
- Translated password labels, guidelines, placeholders, and validation messages to improve user experience in both languages.
- Ensured consistency in terminology and clarity in password requirements for better user understanding.
2025-07-28 14:17:41 +05:30
chamikaJ
f81d0f9594 feat(holiday-settings): implement organization holiday settings management
- Added SQL migration for creating organization holiday settings and state holidays tables with necessary constraints and indexes.
- Implemented API endpoints in AdminCenterController for retrieving and updating organization holiday settings.
- Updated admin-center API router to include routes for holiday settings management.
- Enhanced localization files to support new holiday settings UI elements in multiple languages.
- Improved holiday calendar component to display working days and integrate holiday settings.
2025-07-28 13:07:15 +05:30
chamikaJ
c18b289e4f feat(reporting): enhance reporting allocation and members controllers
- Added a helper method to build billable queries with custom table aliases in the ReportingAllocationController.
- Updated the logic for filtering projects and categories in the ReportingAllocationController to improve data retrieval based on user selections.
- Refactored the query structure in both ReportingAllocationController and ReportingMembersController to streamline data processing and enhance performance.
- Improved handling of utilization calculations and added new filters for team members in the ReportingMembersController.
- Enhanced Excel export functionality for member reports, ensuring accurate data representation and improved formatting.
2025-07-28 10:31:08 +05:30
Chamika J
b762bb5b18 Merge pull request #298 from Worklenz/feature/holiday-calendar-integration
feat(localization): update and enhance localization files for multipl…
2025-07-28 10:01:33 +05:30
Chamika J
7c7f955bb5 Merge branch 'feature/team-utilization' into feature/holiday-calendar-integration 2025-07-28 10:01:24 +05:30
Chamika J
1e6045c534 Merge pull request #297 from Worklenz/fix/task-time-log-timezone-fix
feat(task-time-logs): enhance time log retrieval and formatting with …
2025-07-28 09:48:08 +05:30
chamikaJ
2a9e12a495 feat(task-time-logs): enhance time log retrieval and formatting with user timezone
- Integrated user timezone handling in the task time logs API service to ensure accurate time representation.
- Added a new utility function to format date/time strings according to the user's profile timezone.
- Updated the TimeLogItem component to utilize the new formatting function for displaying timestamps.
2025-07-28 09:44:59 +05:30
chamiakJ
e0f268e4a1 fix(holiday-controller): specify error type in catch block for better type safety 2025-07-28 07:41:29 +05:30
chamiakJ
d39bddc22f feat(project-view): add user permission-based tab filtering
- Implemented a new function to filter project view tabs based on user finance permissions, enhancing the user experience by conditionally displaying the finance tab.
- Updated the project view component to utilize the new filtering function, ensuring that only authorized users can access finance-related features.
- Refactored URL parameter handling to accommodate the filtered tab items, improving state management and user navigation.
2025-07-28 07:36:25 +05:30
chamiakJ
591d348ae5 feat(localization): update and enhance localization files for multiple languages
- Updated localization files for various languages, including English, German, Spanish, Portuguese, and Chinese, to ensure consistency and accuracy across the application.
- Added new keys and updated existing ones to support recent UI changes and features, particularly in project views, task lists, and admin center settings.
- Enhanced the structure of localization files to improve maintainability and facilitate future updates.
- Implemented performance optimizations in the frontend components to better handle localization data.
2025-07-28 07:19:55 +05:30
chamiakJ
fc88c14b94 Merge branch 'feature/add-calender-and-holidays' of https://github.com/Worklenz/worklenz into feature/team-utilization 2025-07-26 12:31:35 +05:30
Chamika J
fd2fc793df Merge pull request #295 from Worklenz/chore/added-sign-up-survey
Chore/added sign up survey
2025-07-25 15:23:03 +05:30
chamikaJ
8380b354cc refactor(template-drawer): remove unused parameters from TemplateDrawer component
- Eliminated unused parameters in the TemplateDrawer component to clean up the code and improve readability.
2025-07-25 15:23:02 +05:30
chamikaJ
2aaf0fc19a feat(account-setup): refactor language handling in account setup process
- Updated language keys to use constants from the Language enum for better maintainability.
- Changed the language state selector to use 'lng' instead of 'language' for consistency.
- Adjusted the language change handler to accept ILanguageType for type safety.
2025-07-25 15:22:51 +05:30
Chamika J
f3b7479770 Merge pull request #291 from Worklenz/chore/added-sign-up-survey
Chore/added sign up survey
2025-07-25 13:03:16 +05:30
Chamika J
65745e368f Merge branch 'release-v2.1.4' into chore/added-sign-up-survey 2025-07-25 13:03:03 +05:30
Chamika J
cabd05e0da Merge pull request #290 from Worklenz/imp/invite--improvement
Imp/invite  improvement
2025-07-25 13:01:29 +05:30
Chamika J
6e71a91d6c Merge pull request #288 from shancds/test/invitation-process
feat(assignee-selector): enhance member invitation functionality and …
2025-07-25 13:01:01 +05:30
chamikaJ
7dc3dedda5 feat(account-setup): enhance localization and UI for account setup process
- Added new language support and improved translations for account setup steps across multiple languages.
- Updated the organization step to streamline user input and enhance suggestions for organization names.
- Refactored task management components to improve user experience when adding and managing tasks.
- Removed outdated CSS for admin center components to simplify styling and improve maintainability.
- Introduced new UI elements and transitions for a more engaging account setup experience.
- Enhanced Redux state management to accommodate new features and localization updates.
2025-07-25 12:50:19 +05:30
shancds
944acf99db feat(project-member-drawer): filter out already invited members from the selection list
- Implemented logic to filter available members by excluding those already part of the current project.
- Updated the member selection dropdown to display only non-invited members, enhancing the user experience during the invitation process.
2025-07-25 12:07:43 +05:30
shancds
a9d0244ca2 fix(update-member-drawer): correct job title assignment in member update request
- Updated the job title assignment in the member update request to use the value from the form field instead of a previously hardcoded variable.
2025-07-25 11:31:36 +05:30
chamikaJ
b688f8e114 feat(account-setup): enhance account setup process with new survey and task management features
- Expanded localization files to include additional text for account setup steps in multiple languages.
- Introduced new components for the survey step, allowing users to provide feedback on their needs and preferences.
- Implemented task management features, enabling users to add and manage tasks during the account setup process.
- Enhanced the organization step with suggestions for organization names based on industry categories.
- Improved UI/UX with new design elements and transitions for a smoother user experience.
- Updated Redux state management to handle new survey and task data effectively.
- Added language switcher functionality to support multilingual users during the setup process.
2025-07-25 10:52:07 +05:30
shancds
e7e9cfce8c Merge branch 'release-v2.1.4' of https://github.com/Worklenz/worklenz into test/invitation-process 2025-07-25 09:48:29 +05:30
shancds
27605b4d68 feat(assignee-selector): enhance member invitation functionality and integrate project manager checks
- Added hooks for project manager status and authentication to manage member invitation visibility.
- Refactored dropdown toggle logic for improved readability and performance.
- Updated UI to conditionally render the invite member button based on user roles (admin or project manager).
- Cleaned up code formatting for better consistency and maintainability.
2025-07-25 09:47:09 +05:30
Chamika J
ff4b0ed315 Merge pull request #287 from Worklenz/imp/invite--improvement
Imp/invite  improvement
2025-07-25 08:52:44 +05:30
chamikaJ
fe7c15ced1 feat(surveys): implement account setup survey functionality
- Added new database migration to create survey-related tables for storing questions and responses.
- Developed SurveyController to handle fetching and submitting survey data.
- Created survey API routes for account setup, including endpoints for retrieving the survey and submitting responses.
- Implemented frontend components for displaying the survey and capturing user responses, integrating with Redux for state management.
- Enhanced localization files to include survey-related text for multiple languages.
- Added validation middleware for survey submissions to ensure data integrity.
2025-07-24 17:12:47 +05:30
chamikaJ
15ff69a031 refactor(styles): remove outdated HubSpot widget dark mode CSS overrides
- Eliminated unnecessary CSS rules for the HubSpot widget's dark mode styling to streamline the stylesheet.
- Cleaned up the customOverrides.css file by removing redundant background and box-shadow properties for improved maintainability.
2025-07-24 16:08:11 +05:30
chamikaJ
22d78222d3 feat(localization): update tab labels to include finance
- Added the "finance" tab label to the project-view constants, ensuring it is available for localization.
- Updated the tab label retrieval function and the tab label update function to support the new finance key, enhancing the user interface for finance-related features.
2025-07-24 16:05:23 +05:30
chamikaJ
3a6af8bd07 feat(localization): add finance key to project-view localization files
- Added a new "finance" key to project-view localization files for multiple languages, enhancing support for finance-related UI elements.
- Ensured consistency across all language files by updating the structure and maintaining formatting.
2025-07-24 16:05:11 +05:30
chamikaJ
4ffc3465e3 feat(finance): implement project finance and rate card management features
- Added new controllers for managing project finance and rate cards, including CRUD operations for rate card roles and project finance tasks.
- Introduced API routes for project finance and rate card functionalities, enhancing the backend structure.
- Developed frontend components for displaying and managing project finance data, including a finance drawer and rate card settings.
- Enhanced localization files to support new UI elements and ensure consistency across multiple languages.
- Implemented utility functions for handling man-days and financial calculations, improving overall functionality.
2025-07-24 15:23:34 +05:30
chamikaJ
4b54f2cc17 feat(admin-center): implement organization calculation method settings
- Added functionality to update the organization's calculation method (hourly or man-days) in the Admin Center.
- Created a new component for managing the calculation method, including UI elements for selection and saving changes.
- Updated API service to handle the new endpoint for updating the calculation method.
- Enhanced localization files to support new keys related to the calculation method settings.
- Introduced a settings page to manage organization working days and hours alongside the calculation method.
2025-07-24 12:53:46 +05:30
chamikaJ
67a75685a9 feat(reporting): add total time utilization component and enhance localization
- Introduced a new TotalTimeUtilization component to display total time logged, expected capacity, and team utilization metrics.
- Updated existing time-report localization files to include new keys for total time logged, expected capacity, and utilization states across multiple languages.
- Refactored MembersTimeReports to integrate the new TotalTimeUtilization component, improving the reporting interface.
- Enhanced the overall structure and organization of the reporting components for better maintainability.
2025-07-24 11:41:04 +05:30
chamikaJ
20ce0c9687 feat(reporting): enhance time reports page with new filters and components
- Added new components for filtering by billable status, categories, projects, members, and teams in the time reports overview.
- Implemented a new header component to manage the layout and functionality of the time reports page.
- Refactored existing components to improve organization and maintainability, including the removal of deprecated files.
- Updated localization files to support new UI elements and ensure consistency across languages.
- Adjusted the language selector to reflect the correct language codes for Chinese.
2025-07-24 11:40:39 +05:30
Chamika J
070c643105 Merge pull request #283 from shancds/test/invitation-process
Test/invitation process
2025-07-24 10:30:54 +05:30
Chamika J
980af8bd4f Merge branch 'imp/invite--improvement' into test/invitation-process 2025-07-24 10:30:46 +05:30
chamikaJ
1931856d31 Merge branch 'main' of https://github.com/Worklenz/worklenz into fix/timelog-timezone-fix 2025-07-24 10:18:52 +05:30
Chamika J
fe3fb5e627 Merge pull request #285 from Worklenz/development
Development
2025-07-24 10:17:22 +05:30
Chamika J
ef47df804f Merge pull request #284 from Worklenz/fix/release-v2.1.3
feat(database): add performance indexes for optimized task queries
2025-07-24 10:16:34 +05:30
chamikaJ
7ea05d2982 feat(database): add performance indexes for optimized task queries
- Introduced multiple new indexes in the SQL schema to enhance query performance for tasks, including composite indexes for filtering and sorting.
- Added indexes for various task-related entities such as assignees, phases, labels, comments, and attachments to improve data retrieval efficiency.
- Implemented covering indexes to optimize task aggregation and search functionalities, ensuring faster access to frequently queried data.
2025-07-24 10:16:09 +05:30
chamikaJ
daa65465dd feat(reporting): add utility methods for date and time calculations in ReportingMembersController
- Introduced methods to calculate percentage, convert time formats, and determine date-related metrics such as days left, overdue status, and if a date is today.
- Updated existing logic in getSingleMemberProjects to utilize the new utility methods for improved clarity and maintainability.
2025-07-24 09:56:23 +05:30
chamikaJ
de26417247 refactor(reporting): enhance timezone handling and clean up migration
- Updated SQL queries in reporting controllers to join with the timezones table for accurate timezone retrieval.
- Refactored ReportingMembersController to extend ReportingControllerBaseWithTimezone for centralized timezone logic.
- Removed obsolete migration file that added a timezone column to the users table, as it is no longer needed.
2025-07-24 09:25:50 +05:30
chamiakJ
69b2fe1a90 feat(reporting): implement timezone support in reporting allocation and related components
- Added timezone handling in the getMemberTimeSheets method to ensure accurate date calculations based on user timezone.
- Created ReportingControllerBaseWithTimezone to centralize timezone-related logic for reporting.
- Introduced a migration to add a timezone column to the users table for better user experience.
- Updated frontend API services and hooks to include user's timezone in requests.
- Enhanced members time reports page to display time logs in the user's local timezone.
2025-07-24 07:50:01 +05:30
Chamika J
5c327a3a21 Merge pull request #281 from Worklenz/development
Development
2025-07-23 16:07:57 +05:30
Chamika J
123a912e64 Merge pull request #280 from Worklenz/fix/release-v2.1.3
Fix/release v2.1.3
2025-07-23 15:09:27 +05:30
chamikaJ
78516d8d6c feat(analytics-hubspot): modularize analytics and HubSpot integration
- Moved Google Analytics and HubSpot integration scripts to separate modules for better organization and maintainability.
- Implemented an AnalyticsManager class to handle Google Analytics initialization and privacy notice display.
- Created a HubSpotManager class to manage HubSpot widget loading and dark mode theming.
- Updated index.html to reference the new modules, improving code clarity and separation of concerns.
2025-07-23 14:36:50 +05:30
chamikaJ
9946c9a00e fix(upgrade-plans): adjust minimum seat value logic and enhance HubSpot widget dark mode styles
- Updated the minimum seat value logic in the upgrade plans component to allow the current seat count.
- Added CSS overrides for the HubSpot widget to ensure proper display in dark mode, removing unwanted backgrounds and shadows.
2025-07-23 13:24:58 +05:30
chamikaJ
4887383dc4 feat(upgrade-plans): add responsive styles and improve layout for upgrade plans component
- Introduced a new CSS file for responsive design in the upgrade plans component, enhancing mobile usability.
- Updated the upgrade plans component to utilize the new styles, ensuring a better layout on various screen sizes.
- Refactored seat count options logic for improved clarity and functionality.
2025-07-23 13:03:00 +05:30
chamikaJ
a6863d8280 refactor: update code to use centralized Ant Design imports
- Replaced direct import of '@ant-design/icons' with centralized import from '@/shared/antd-imports'
2025-07-23 12:07:48 +05:30
chamikaJ
edf81dbe57 refactor: update Ant Design imports to centralized path
- Replaced direct imports from 'antd' with centralized imports from '@/shared/antd-imports' to align with new import rules and improve maintainability.
2025-07-23 11:05:39 +05:30
shancds
300d4763f5 Enhance project member management with search functionality and localization updates
- Implemented search functionality for project members in the backend, allowing users to filter members by name or email.
- Updated frontend components to include a search input for members, improving user experience.
- Added localization strings for the search placeholder in multiple languages (Albanian, German, English, Spanish, Portuguese).
- Refactored SQL queries for better performance and clarity in fetching project members.
2025-07-23 10:49:00 +05:30
chamikaJ
80f5febb51 feat(antd-imports): establish centralized import rules for Ant Design components
- Introduced a new rules file for Ant Design component imports, enforcing the use of centralized imports from `@antd-imports` to enhance tree-shaking, maintainability, and performance.
- Updated various components to replace direct imports from 'antd' and '@ant-design/icons' with the centralized import path.
- Refactored existing components to utilize memoization and callbacks for improved performance and readability.
- Enhanced the billing and configuration components with updated styles and improved user experience.
2025-07-23 10:33:55 +05:30
shancds
aaaac09212 Merge branch 'fix/release-v2.1.3' of https://github.com/Worklenz/worklenz into test/invitation-process 2025-07-23 08:50:54 +05:30
shancds
c4400d178f Refactor invite components to comment out unused UI elements
- Commented out the button for copying team and project links in the invite team members and project member invite drawer components, respectively, to streamline the UI.
- Adjusted conditional rendering for the checkbox related to project invitations to improve code clarity and maintainability.
2025-07-23 08:48:01 +05:30
chamiakJ
a6286eb2b8 feat(migrations): add README for node-pg-migrate and enhance frontend configuration
- Created a README file for database migrations using node-pg-migrate, detailing commands, file format, best practices, and an example migration.
- Added a Vite configuration file for the frontend, including plugin setup, alias resolution, build optimizations, and responsive design adjustments for task list components.
- Updated i18n configuration to preload the 'home' namespace for improved localization.
- Enhanced task list styling with responsive design features for better mobile usability.
2025-07-23 07:46:39 +05:30
Chamika J
ee75aead78 Merge pull request #278 from Worklenz/fix/release-v2.1.3
Fix/release v2.1.3
2025-07-22 17:26:45 +05:30
chamikaJ
e3c002b088 style(task-list): enhance styling for task list components
- Updated the EmptyGroupDropZone component to include top and bottom borders for better visual separation.
- Adjusted the AddTaskRow component by removing the bottom border for a cleaner appearance.
2025-07-22 17:25:37 +05:30
chamikaJ
3beed3dae6 feat(auth): update password requirements and localization for signup
- Updated password label and placeholder to "Password" and "Enter your password" across multiple languages.
- Added password guideline for minimum requirements in English, German, Spanish, Portuguese, Albanian, and Chinese.
- Introduced maximum character requirement for passwords in all supported languages.
- Enhanced password validation messages to improve user experience during signup.
2025-07-22 17:25:25 +05:30
chamikaJ
33aace71c8 refactor(tasks): improve code readability and formatting in tasks-controller-v2
- Refactored import statements for better organization and clarity.
- Enhanced formatting of methods and conditionals for improved readability.
- Updated task filtering methods to maintain consistent formatting and style.
- Added performance logging for deprecated methods to assist in monitoring usage.
- Cleaned up unnecessary comments and improved inline documentation for better understanding.
2025-07-22 17:12:06 +05:30
shancds
da791e2cb7 Enhance project member invitation feature with new localization and UI updates
- Added new localization strings for "invite member" and "also invite to project" across multiple languages (Albanian, German, English, Spanish, Portuguese, Chinese).
- Updated the project member invite drawer to conditionally display UI elements based on the context of the invitation (from assigner or not).
- Introduced a new state management feature to track if the invitation is initiated from the assignee selector, improving user experience.
2025-07-22 16:21:18 +05:30
chamikaJ
354b9422ed feat(tasks): add color_code_dark to task groups and enhance task list display
- Introduced color_code_dark property to task groups for improved theming support.
- Updated task list components to utilize color_code_dark based on the current theme mode.
- Enhanced empty state handling in task list to dynamically create an unmapped group for better user experience.
- Refactored task management slice to support dynamic group creation for unmapped tasks.
2025-07-22 16:08:41 +05:30
shancds
3373dccc58 Update localization and UI for team member invitation feature
- Changed button labels for inviting team members to "Send Invitation" across multiple languages (Albanian, German, English, Spanish, Portuguese, Chinese).
- Updated the invite team members component to use a modal instead of a drawer for a better user experience.
- Added a new button for copying the team link in the modal footer, enhancing functionality.
2025-07-22 15:21:13 +05:30
shancds
06da0d20b9 Enhance project member drawer localization and UI
- Added new localization strings for "members" and "copy project link" in multiple languages (Albanian, German, English, Spanish, Portuguese, Chinese).
- Updated the project member invite drawer to use a modal instead of a drawer for improved user experience.
- Included a button in the modal footer for copying the project link, enhancing functionality for users.
2025-07-22 14:43:04 +05:30
chamikaJ
256f1eb3a9 feat(task-list): enhance empty state display in task list component
- Added EmptyListPlaceholder component to visually represent the empty state in the task list.
- Adjusted styling and layout for the empty group drop zone to improve user experience.
- Removed unused progress properties from the task group data structure for cleaner code.
2025-07-22 12:46:09 +05:30
chamikaJ
5f86ba6b13 feat(auth): implement new authentication pages and refactor routes
- Added new authentication pages: LoginPage, SignupPage, ForgotPasswordPage, AuthenticatingPage, LoggingOutPage, and VerifyResetEmailPage.
- Refactored auth routes to use updated component names for better consistency and clarity.
- Enhanced user experience with improved loading states and error handling across authentication processes.
2025-07-22 12:27:05 +05:30
shancds
5addcee0b2 Merge branch 'release/v2.1.2' of https://github.com/Worklenz/worklenz into test/invitation-process 2025-07-21 09:54:09 +05:30
shancds
3419d7e81d Merge branch 'feature/project-gantt-chart' of https://github.com/Worklenz/worklenz into test/invitation-process 2025-07-21 09:50:23 +05:30
chamikaJ
78d960bf01 feat(gantt): introduce advanced Gantt chart components and demo page
- Added new components for an advanced Gantt chart, including AdvancedGanttChart, GanttGrid, DraggableTaskBar, and TimelineMarkers.
- Implemented a demo page (GanttDemoPage) to showcase the functionality of the new Gantt chart components.
- Enhanced project roadmap features with ProjectRoadmapGantt and related components for better project management visualization.
- Introduced sample data for testing and demonstration purposes, improving the user experience in the Gantt chart interface.
- Updated main routes to include the new Gantt demo page for easy access.
2025-07-20 22:05:42 +05:30
Chamika J
8dc3133814 Merge pull request #275 from shancds/test/kanban-order-v1.2.3
Enhance EnhancedKanbanBoardNativeDnD to support phase reordering
2025-07-17 16:30:39 +05:30
shancds
1709fad733 Add drag-and-drop data transfer support in EnhancedKanbanBoardNativeDnD 2025-07-17 15:51:23 +05:30
shancds
7f71e8952b Enhance EnhancedKanbanBoardNativeDnD with task priority updates and socket integration
- Added functionality to handle task priority changes, emitting updates via socket for real-time synchronization.
- Updated the EnhancedKanbanBoardNativeDnD component to include new logic for managing task priorities within the drag-and-drop interface.
- Cleaned up console log statements in the useTaskSocketHandlers hook for improved performance monitoring.
2025-07-17 12:52:40 +05:30
shancds
22d2023e2a Update phase handling in EnhancedKanbanBoardNativeDnD component
- Modified phase update logic to prevent setting phase_id for 'Unmapped' phases, ensuring only valid phases are processed.
- Cleaned up unnecessary whitespace in the task reordering section for improved code clarity.
2025-07-17 11:54:33 +05:30
chamikaJ
f73c151da2 feat(holiday-system): add holiday population functionality and API integration
- Implemented a new API endpoint to populate country holidays in the database.
- Enhanced the HolidayController to handle holiday population logic for multiple countries.
- Updated the holiday API router to include the new populate endpoint.
- Added a service method in the frontend to trigger holiday population.
- Integrated a button in the admin center overview for easy access to populate holidays.
- Improved error handling and user feedback during the holiday population process.
2025-07-17 11:27:29 +05:30
shancds
fa08463e65 Enhance localization support in Kanban board
- Added new localized messages for error handling, task management, and filter loading across multiple languages (Albanian, German, English, Spanish, Portuguese, Chinese).
- Updated the EnhancedKanbanBoardNativeDnD component to utilize these localized messages for improved user experience during task operations and error notifications.
2025-07-17 11:10:23 +05:30
shancds
7226932247 Enhance EnhancedKanbanBoardNativeDnD to support phase reordering
- Integrated phase reordering functionality within the EnhancedKanbanBoardNativeDnD component.
- Added logic to fetch phases by project ID and update phase order through API calls.
- Updated drag-and-drop handling to accommodate reordering of phases alongside existing status-based reordering.
2025-07-17 10:56:06 +05:30
Chamika J
6adf40f5a6 Merge pull request #274 from shancds/test/kanban-order-v1.2.2
Test/kanban order v1.2.2
2025-07-16 09:35:12 +05:30
chamiakJ
5214368354 feat(holiday-system): implement comprehensive holiday management features
- Added holiday types and organization holidays management with CRUD operations.
- Introduced country holidays import functionality using the date-holidays npm package.
- Created database migrations for holiday types and organization holidays tables.
- Developed a holiday calendar component for visual representation and management of holidays.
- Enhanced API routes for holiday-related operations and integrated them into the admin center.
- Updated frontend localization for holiday management features.
- Implemented scripts for populating holidays in the database for 200+ countries.
2025-07-16 07:59:27 +05:30
shancds
f03f6e6f5d Implement task order updates and socket emissions in EnhancedKanbanBoardNativeDnD
- Added a utility function to recalculate task orders for all groups based on the specified grouping criteria (status, priority, phase).
- Updated task drag-and-drop logic to handle reordering within the same group and across different groups.
- Enhanced socket emissions to send full task order updates, including task details and indices, improving synchronization with the backend.
2025-07-15 18:17:32 +05:30
Chamika J
a112d39321 Merge pull request #272 from Worklenz/development
Development
2025-07-15 17:20:59 +05:30
Chamika J
4788294bc4 Merge pull request #271 from Worklenz/release/v2.1.2
Release/v2.1.2
2025-07-15 17:20:25 +05:30
shancds
d7416ff793 Merge branch 'test/kanban-right-click-feature-v1.2.1' of https://github.com/shancds/worklenz-open-source into test/kanban-order-v1.2.2 2025-07-15 16:46:43 +05:30
shancds
d89247eb02 Add delete phase confirmation modal with localization support
- Implemented a confirmation modal for deleting phases in the Kanban board.
- Integrated localized messages for delete phase prompts in multiple languages, enhancing user experience.
- Updated KanbanGroup component to utilize the new confirmation modal for phase deletions.
2025-07-15 16:45:57 +05:30
shancds
5318f95037 Merge branch 'release/v2.1.2' of https://github.com/Worklenz/worklenz into test/kanban-order-v1.2.2 2025-07-15 16:39:35 +05:30
shancds
c80b00ec76 Add delete status confirmation modal with localization support
- Implemented a confirmation modal for deleting statuses in the Kanban board.
- Integrated localized messages for delete status prompts in multiple languages, enhancing user experience.
- Removed the previous portal-based confirmation approach in favor of Ant Design's Modal component for better consistency and usability.
2025-07-15 16:34:03 +05:30
shancds
f48476478a Implement task deletion functionality in TaskCard component
- Added context menu for task deletion with confirmation modal.
- Integrated localization for delete task prompts in multiple languages.
- Updated TaskCard to handle task deletion logic, including dispatching actions to update the state and emit socket events for task progress.
2025-07-15 16:19:40 +05:30
chamikaJ
737f7cada2 refactor(App): streamline cache cleanup process on logout
- Removed dynamic import of CacheCleanup in the App component, directly utilizing the imported utility for clearing caches and reloading the application on user logout.
- This change simplifies the logout process and improves code readability.
2025-07-15 16:11:30 +05:30
chamikaJ
833879e0e8 feat(logout): implement cache cleanup and service worker unregistration on logout
- Added a new utility, CacheCleanup, to handle clearing caches and unregistering the service worker during user logout.
- Enhanced the LoggingOutPage to utilize CacheCleanup for clearing local session and caches before redirecting to the login page.
- Introduced ModuleErrorBoundary to manage module loading errors, providing user feedback and options to retry or reload the application.
- Updated App component to include global error handlers for improved error management related to module loading issues.
2025-07-15 16:08:07 +05:30
chamikaJ
cb5610d99b feat(timer): add useTimerInitialization hook for managing running timers
- Introduced a new custom hook, useTimerInitialization, to fetch and initialize running timers from the backend when the project view loads.
- Integrated the hook into the ProjectView component to update Redux state with active timers and their corresponding task details.
- Enhanced error handling and logging for timer initialization to improve debugging and user experience.
2025-07-15 15:57:01 +05:30
chamikaJ
0434bbb73b refactor(account-setup): update ProjectStep to use custom dispatch and refresh user session
- Replaced useDispatch with useAppDispatch for improved state management in ProjectStep component.
- Added logic to refresh user session and update user state after template setup completion.
- Enhanced error handling for session refresh to ensure robustness in user experience.
2025-07-15 15:40:06 +05:30
chamikaJ
6e911d79fc feat(auth): add combined AuthAndSetupGuard for route protection
- Introduced AuthAndSetupGuard to enforce both authentication and setup completion for protected routes.
- Updated main routes to utilize the new guard, ensuring users are redirected appropriately based on their authentication and setup status.
- Adjusted setup route to only require authentication, allowing access without completed setup.
- Refactored account setup component to use custom dispatch hook for improved state management and added session refresh after setup completion.
2025-07-15 15:34:43 +05:30
chamikaJ
0bb748cf89 feat(database-migrations): implement grouping-specific sort orders and constraints
- Added new sort order columns for tasks based on different grouping types (status, priority, phase, member).
- Updated database functions to handle these new sort orders, ensuring proper task updates during sorting operations.
- Implemented a migration to fix existing sort order constraint violations, ensuring data consistency across projects.
- Enhanced functions to avoid sort order conflicts and maintain unique sort orders within each project.
2025-07-15 15:19:16 +05:30
Chamika J
ba5d4975af Merge pull request #270 from shancds/test/row-kanban-board-v1.2.0
Enhance TaskCard accessibility by adding title attribute to task name
2025-07-15 15:09:57 +05:30
shancds
d4620148bd Update TaskProgressCircle to visually indicate task completion
- Changed the stroke color of the progress circle to green when progress reaches 100%.
- Added a green checkmark icon for completed tasks, enhancing visual feedback.
- Retained progress percentage display for tasks that are not yet complete, ensuring clarity in task status.
2025-07-15 14:48:26 +05:30
shancds
8d7d54be78 Enhance TaskCard accessibility by adding title attribute to task name
- Updated TaskCard component to include a title attribute on the task name span for improved accessibility and better user experience when hovering over task names.
2025-07-15 14:28:45 +05:30
Chamika J
c34b94c7db Merge pull request #269 from shancds/test/row-kanban-board-v1.2.0
Test/row kanban board v1.2.0
2025-07-15 14:24:24 +05:30
chamikaJ
55a0028e26 refactor(drag-and-drop): replace console logging with error logging
- Integrated an error logging utility to replace console warnings and errors in the `useDragAndDrop` hook, enhancing error tracking and debugging.
- Removed unnecessary console logs related to drag-and-drop operations, streamlining the code and improving performance.
2025-07-15 14:24:24 +05:30
chamikaJ
17371200ca feat(drag-and-drop): enhance task grouping updates and socket event handling
- Updated the `useDragAndDrop` hook to emit specific grouping field change events (phase, priority, status) when tasks are moved between groups.
- Refactored the task management slice to prevent direct updates to task grouping fields during drag-and-drop operations, ensuring these updates are handled via socket events after backend confirmation.
- Introduced a new socket handler for task sort order changes to update task properties based on backend responses, improving synchronization between frontend and backend task states.
2025-07-15 14:22:27 +05:30
shancds
83044077d3 Enhance task status handling and progress visualization in Kanban board
- Introduced a mechanism to emit a socket event when a task's status changes during drag-and-drop operations.
- Updated TaskCard to consistently display the progress circle regardless of task completion state.
- Improved TaskProgressCircle to handle complete_ratio and progress more robustly, ensuring accurate visual representation.
2025-07-15 14:13:37 +05:30
Chamika J
a03d9ef6a4 Merge pull request #268 from Worklenz/fix/task-sort-order
Fix/task sort order
2025-07-15 13:38:49 +05:30
Chamika J
fca8ace10d Merge pull request #267 from Worklenz/feature/show-multiples-levels-of-subtaks-in-task-list
Feature/show multiples levels of subtaks in task list
2025-07-15 13:31:10 +05:30
chamikaJ
d970cbb626 feat(task-management): enhance task timer synchronization and color handling
- Updated `CustomColordLabel` and `CustomNumberLabel` components to improve color handling by removing the alpha channel logic and implementing a dynamic text color based on background luminance.
- Enhanced task management slice to preserve timer state when fetching tasks, ensuring active timers are maintained across updates.
- Modified socket handlers to synchronize timer state between task slices, improving consistency in task time tracking.
- Refactored `useTaskTimer` hook to streamline local and Redux state synchronization for timer management.
2025-07-15 13:30:59 +05:30
chamikaJ
6d8c475e67 refactor(sort-orders): remove outdated deployment and implementation guides
- Deleted the `DEPLOYMENT_GUIDE_SORT_ORDERS.md` and `SEPARATE_SORT_ORDERS_IMPLEMENTATION.md` files as they are no longer relevant following the recent updates to the sort orders feature.
- Introduced new migration scripts to address duplicate sort orders and ensure data integrity across the updated task sorting system.
- Updated database schema to include new sort order columns and constraints for improved performance and organization.
- Enhanced backend functions and frontend components to support the new sorting logic and maintain user experience during task organization.
2025-07-15 13:18:51 +05:30
shancds
a1c0cef149 Remove PerformanceMonitor and VirtualizedTaskList components along with their associated styles to streamline the enhanced Kanban board. Update TaskCard to include a progress circle for better task visualization. 2025-07-15 12:17:16 +05:30
shancds
8f098143fd Merge branch 'main' of https://github.com/Worklenz/worklenz into test/row-kanban-board-v1.2.0 2025-07-15 09:00:38 +05:30
chamiakJ
407dc416ec feat(sort-orders): implement separate sort orders for task groupings
- Introduced new columns for `status_sort_order`, `priority_sort_order`, `phase_sort_order`, and `member_sort_order` in the tasks table to maintain distinct sort orders for each grouping type.
- Updated database functions to handle grouping-specific sort orders and avoid unique constraint violations.
- Enhanced backend socket handlers to emit changes based on the selected grouping.
- Modified frontend components to support drag-and-drop functionality with the new sort order fields, ensuring task organization is preserved across different views.
- Added comprehensive migration scripts and verification steps to ensure smooth deployment and backward compatibility.
2025-07-15 07:44:15 +05:30
chamikaJ
61461bb776 feat(user-activity): enhance user activity logs with additional data and improved queries
- Added optional fields for project color, task status, and status color in IUserRecentTask and IUserTimeLoggedTask interfaces.
- Optimized SQL queries to include team filtering and additional data such as project color and task status.
- Updated frontend components to support new data fields and improved styling for better user experience.
- Enhanced dark mode detection and styling in task activity lists.
- Implemented refetching of data on tab change in the user activity feed.
2025-07-14 13:26:28 +05:30
chamikaJ
2a7019c64c refactor(home-page): simplify layout by removing unnecessary Card component
- Removed the Card wrapper around the TasksList for a cleaner layout.
- Cleaned up whitespace in the home-page.tsx file for improved readability.
2025-07-14 12:48:13 +05:30
chamikaJ
5b1cbb0c46 Merge branch 'main' of https://github.com/Worklenz/worklenz into feature/task-activities-by-user 2025-07-14 12:46:18 +05:30
chamikaJ
3d67145af7 refactor(task-list): adjust subtask indentation for improved visual hierarchy
- Reduced spacing for level 1 subtasks and increased spacing for deeper levels in TaskRowWithSubtasks and TitleColumn components.
- Enhanced comments to clarify indentation logic for better maintainability.
2025-07-14 12:37:54 +05:30
chamikaJ
1c981312d4 feat(task-drawer): enhance task navigation and hierarchy display
- Added functionality to navigate back to parent tasks for sub-tasks in the task drawer.
- Introduced a breadcrumb component to visually represent the hierarchy of tasks, improving user navigation.
- Updated task drawer header to display the current task name with truncation and tooltip for better readability.
- Enhanced styling for task name display and breadcrumb for improved user experience.
2025-07-14 12:28:04 +05:30
chamikaJ
02d814b935 refactor(task-list): enhance task row components with depth handling
- Added depth and maxDepth props to TaskRow, TaskRowWithSubtasks, and TitleColumn components to manage nested subtasks more effectively.
- Updated AddSubtaskRow to support depth for proper indentation and visual hierarchy.
- Improved styling for subtasks based on their depth level, ensuring better visual distinction.
- Adjusted task management slice to utilize actual subtask counts from the backend for accurate display.
2025-07-14 12:04:31 +05:30
Chamika J
e87f33dcc8 Merge pull request #265 from Worklenz/development
Development
2025-07-14 11:37:47 +05:30
Chamika J
6286d4315d Merge pull request #262 from Worklenz/fix/home-page-tasks
fix(home-page): exclude archived projects from task queries and updat…
2025-07-14 11:37:22 +05:30
Chamika J
a1234b8af0 Merge pull request #264 from Worklenz/development
Development
2025-07-14 08:57:44 +05:30
Chamika J
bc0a62002b Merge pull request #263 from Worklenz/chore/updated-readme.md
Update README and setup documentation for improved clarity and requir…
2025-07-14 07:54:44 +05:30
chamikaJ
52eca27619 Update README and setup documentation for improved clarity and requirements
- Added license and version badges to the README for better visibility.
- Updated Node.js version requirement from v16 to v20 in both README and setup documentation.
- Enhanced the "Getting Started" section with clearer instructions for Docker setup and manual installation.
- Removed Grunt-related files and dependencies to streamline the backend setup.
- Updated deployment instructions for local and remote environments, including new access URLs and video guides.
2025-07-14 07:53:42 +05:30
chamiakJ
e4c9e22972 refactor(tasks-list): update column widths for improved layout and responsiveness
- Adjusted column widths in the TasksList component to use percentage-based values for better adaptability across different screen sizes.
- Enhanced text styling for task names and project names to improve readability and visual consistency.
2025-07-12 07:27:32 +05:30
chamiakJ
20e7d3c51a fix(home-page): exclude archived projects from task queries and update project color display
- Added a condition to exclude archived projects from the task retrieval queries in the HomePageController.
- Updated the TasksList component to display the project color instead of the phase color for better visual representation.
2025-07-12 06:46:23 +05:30
Chamika J
6d5aa0ccab Merge pull request #261 from Worklenz/development
Development
2025-07-11 21:55:19 +05:30
Chamika J
7618ae7c6a Merge pull request #260 from Worklenz/fix/release-v.2.1.1
Fix/release v.2.1.1
2025-07-11 21:54:39 +05:30
chamikaJ
808731387b feat(localization): update task and project view translations across multiple languages
- Enhanced localization files for task management and project view headers in Albanian, German, Spanish, Portuguese, Chinese, and others.
- Standardized translations for terms related to tasks, project settings, and notifications, including default task names.
- Improved clarity and consistency in language usage to enhance user experience across different locales.
2025-07-11 18:29:15 +05:30
chamikaJ
502726cd83 chore(release-notes): update version to 2.1.1 and enhance feature descriptions
- Changed release version from 2.1.0 to 2.1.1 in the email template.
- Updated feature descriptions to reflect new functionalities, including easier management of statuses and phases, a task progress bar, and a right-click context menu.
- Improved UI elements for clarity and added images to enhance visual representation of new features.
2025-07-11 18:21:03 +05:30
chamikaJ
a26d8d0f90 feat(task-management): enhance task localization and progress visualization
- Added localization entries for task statuses (To Do, In Progress, Done) across multiple languages including Albanian, German, Spanish, Portuguese, and Chinese.
- Updated the GroupProgressBar component to improve visual representation of task progress with distinct color coding for each status.
- Enhanced TaskGroupHeader to calculate and display group progress dynamically based on task completion and status distribution.
- Integrated a new Convert To Subtask Drawer for improved task management functionality.
2025-07-11 18:09:03 +05:30
chamikaJ
747088e7cc refactor(task-management): enhance empty state visuals and improve layout
- Updated the empty state message styling in VirtualizedTaskList and TaskGroup components for better visibility and user experience.
- Adjusted padding and height in various components to create a more consistent and visually appealing layout.
- Removed console log statements from TaskPhaseDropdown to clean up the codebase.
2025-07-11 17:37:22 +05:30
chamikaJ
affbbbffbf feat(task-phases): enhance phase creation with custom naming and localization updates
- Updated the phase creation logic to allow custom names, defaulting to a generated name if none is provided.
- Modified localization files for multiple languages to improve phase-related text consistency and clarity.
- Enhanced the UI components to reflect the new phase naming functionality and ensure proper integration with the task management system.
- Added dark mode styling for confirmation modals to improve user experience across themes.
2025-07-11 17:26:21 +05:30
shancds
d3023618e1 fix(enhanced-kanban): remove unused Skeleton import in EnhancedKanbanBoardNativeDnD 2025-07-11 17:00:48 +05:30
chamikaJ
12b430a349 feat(task-context-menu): implement context menu for task actions
- Added TaskContextMenu component to provide a context menu for task-related actions such as assigning, archiving, deleting, and moving tasks.
- Integrated context menu into TitleColumn component, allowing users to access task actions via right-click.
- Enhanced user experience by providing immediate feedback for actions like assigning tasks and archiving.
- Improved code organization by separating context menu logic into its own component.
2025-07-11 16:41:30 +05:30
Chamika J
2f3e555b5a Merge pull request #259 from shancds/test/row-kanban-board-v1.1.9
fix(enhanced-kanban): integrate socket event handling for real-time u…
2025-07-11 16:23:42 +05:30
shancds
2498effce3 fix(enhanced-kanban): integrate socket event handling for real-time updates
- Added useTaskSocketHandlers hook to manage socket event handlers for real-time task updates, improving code organization and readability.
- Removed inline socket handling logic to streamline the component and enhance maintainability.
2025-07-11 16:20:21 +05:30
Chamika J
2ad3c2dcd4 Merge pull request #258 from shancds/test/row-kanban-board-v1.1.8
fix(enhanced-kanban): refine loading state visuals in EnhancedKanbanB…
2025-07-11 16:10:49 +05:30
shancds
6226ae35ff fix(task-card): add title attribute for better accessibility
- Added a title attribute to the task name div in TaskCard component to improve accessibility and provide additional context on hover.
2025-07-11 16:01:01 +05:30
chamikaJ
26de439fab feat(task-management): add progress statistics and visual representation for task groups
- Implemented progress calculations for tasks grouped by priority and phase, including todo, doing, and done counts.
- Introduced a new GroupProgressBar component to visually represent task progress in the TaskGroupHeader.
- Updated TaskGroupHeader and TaskListV2Table to integrate progress data and display the progress bar conditionally based on grouping.
- Enhanced local storage handling for grouping preferences in the task management feature.
2025-07-11 15:54:43 +05:30
shancds
295d7a92df fix(kanban-group): update empty drop zone behavior and visuals
- Modified the condition for rendering the empty drop zone to exclude the currently hovered group.
- Updated the background gradient colors for the empty drop zone to enhance visual appeal and consistency with the theme.
2025-07-11 15:45:47 +05:30
shancds
e20ab86d6e fix(enhanced-kanban): refine loading state visuals in EnhancedKanbanBoardNativeDnD
- Removed unnecessary Card wrapper around loading skeletons for a cleaner layout.
- Adjusted loading state visuals to enhance user experience during data fetching.
2025-07-11 15:12:22 +05:30
Chamika J
5c938586b8 Merge pull request #257 from shancds/test/row-kanban-board-v1.1.8
Test/row kanban board v1.1.8
2025-07-11 15:10:36 +05:30
shancds
93b67fba07 fix(enhanced-kanban): improve code readability and loading state visuals
- Refactored conditional statements for better clarity and consistency.
- Enhanced loading state visuals by replacing static text with a dynamic skeleton loader, improving user experience during data fetching.
2025-07-11 15:07:33 +05:30
chamikaJ
e4dfae9f1d feat(database): optimize task sorting functions and introduce bulk update capability
- Added new SQL migration to fix window function errors in task sorting functions, replacing CTEs with direct updates for better performance.
- Introduced a bulk update function for task sort orders, allowing multiple updates in a single call to improve efficiency.
- Updated socket command to support bulk updates, enhancing the task sorting experience in the frontend.
- Simplified task update handling in the frontend to utilize the new bulk update feature, improving overall performance and user experience.
2025-07-11 14:46:07 +05:30
shancds
0efcbf448b fix(task-card): improve loading state visual feedback
- Replaced loading text with a skeleton loader for subtasks in TaskCard component, enhancing user experience during data fetching.
2025-07-11 14:34:49 +05:30
Chamika J
f2f12a2dfa Merge pull request #256 from shancds/test/row-kanban-board-v1.1.8
feat(enhanced-kanban): enhance section creation with category selecti…
2025-07-11 14:07:09 +05:30
shancds
ea37b55078 fix(kanban-group): update section name handling in status update
- Removed unused section name generation and replaced it with trimmed name input for better consistency.
- Ensured that the updated name is set correctly after a successful status update, improving data integrity.
2025-07-11 14:06:11 +05:30
chamikaJ
cc0ff20ca1 feat(localization): update translation keys for phase management
- Revised translation keys in Albanian, German, English, Spanish, Portuguese, and Chinese for improved clarity and consistency in phase management.
- Adjusted phrases related to phase naming, options, and actions to enhance user experience across multiple languages.
- Added new keys for creating and canceling phases to align with recent UI updates in the ManagePhaseModal component.
2025-07-11 14:04:02 +05:30
chamikaJ
6b58709848 feat(task-management): enhance status management with drag-and-drop functionality
- Updated ManageStatusModal to support drag-and-drop for reordering statuses and moving them between categories.
- Introduced CategorySection component for better organization of statuses by category.
- Added validation to prevent moving the last status out of a category, ensuring each category retains at least one status.
- Enhanced localization for task management, updating translation keys across multiple languages for improved clarity and consistency.
2025-07-11 13:50:35 +05:30
shancds
f2b1262e3d feat(enhanced-kanban): enhance section creation with category selection and input handling
- Added state management for section creation, including input focus and category selection.
- Implemented dropdown for category selection with visual feedback and improved accessibility.
- Refactored section creation logic to handle user input and category assignment more effectively.
- Enhanced user experience by managing input focus and handling outside clicks to close dropdowns.
2025-07-11 13:45:13 +05:30
Chamika J
7c04598264 Merge pull request #253 from OminduHirushka/imp/user-activity-feed
user activity feed
2025-07-11 11:27:42 +05:30
Chamika J
7def564950 Merge pull request #255 from shancds/test/row-kanban-board-v1.1.7
feat(assignee-selector): add kanbanMode prop and enhance styling
2025-07-11 11:19:45 +05:30
chamikaJ
278e221c75 feat(task-list): add TaskListSkeleton component for improved loading state
- Introduced TaskListSkeleton to provide a visual loading state for the task list, enhancing user experience during data fetching.
- Updated TaskListV2Table to utilize TaskListSkeleton instead of a generic Skeleton component, allowing for a more tailored loading interface.
- The new skeleton component includes customizable column widths and multiple rows to better represent the task list structure while loading.
2025-07-11 11:18:30 +05:30
shancds
d9a5f76449 feat(assignee-selector): add kanbanMode prop and enhance styling
- Introduced kanbanMode prop to AssigneeSelector for improved functionality in kanban view.
- Updated styling in AssigneeSelector to adjust z-index for better overlay management.
- Enhanced TaskCard to include LazyAssigneeSelectorWrapper, integrating the new prop for task assignment in kanban mode.
2025-07-11 11:17:40 +05:30
Chamika J
b9b707410d Merge pull request #254 from shancds/test/row-kanban-board-v1.1.7
feat(kanban): implement portal for delete confirmation modal
2025-07-11 10:42:39 +05:30
shancds
87675cc73c feat(kanban): implement portal for delete confirmation modal
- Introduced a Portal component to render the delete confirmation modal outside the main DOM hierarchy, improving UI responsiveness.
- Updated the delete confirmation modal to utilize the Portal for better overlay management and user experience.
- Enhanced styling and interaction handling for the modal, ensuring it aligns with the application's theme and accessibility standards.
2025-07-11 10:37:53 +05:30
chamikaJ
0e083868cb chore: moved locale files 2025-07-11 09:37:50 +05:30
chamiakJ
94977f7255 feat(performance): enhance application performance with optimizations and monitoring
- Updated package dependencies for improved localization support and performance.
- Introduced CSS performance optimizations to prevent layout shifts and enhance rendering efficiency.
- Implemented asset preloading and lazy loading strategies for critical components to improve load times.
- Enhanced translation loading with optimized caching and background loading strategies.
- Added performance monitoring utilities to track key metrics and improve user experience.
- Refactored task management components to utilize new performance features and ensure efficient rendering.
- Introduced new utility functions for asset and CSS optimizations to streamline resource management.
2025-07-10 20:39:15 +05:30
chamiakJ
cf686ef8c5 feat(task-management): introduce modals for managing phases and statuses
- Added CreateTaskModal for task creation with integrated status management.
- Implemented ManagePhaseModal and ManageStatusModal for phase and status management, respectively, featuring drag-and-drop functionality.
- Enhanced UI with new CSS styles for dark mode and improved accessibility.
- Updated filter data loading to include phases and statuses for better task management experience.
- Ensured all new components are responsive and align with existing design patterns.
2025-07-10 18:13:41 +05:30
chamiakJ
857b48e225 feat(localization): add new translation keys for task management
- Updated localization JSON files for Albanian, German, English, Spanish, Portuguese, and Chinese to include new keys for managing statuses and phases.
- Enhanced existing translations for clarity and consistency across multiple languages.
- Ensured that new keys align with recent UI changes to improve user experience in task management features.
2025-07-10 16:25:13 +05:30
chamiakJ
f846230d59 feat(localization): update project list translations and add new keys
- Enhanced localization JSON files for multiple languages, including Albanian, German, English, Spanish, Portuguese, and Chinese.
- Updated existing translation keys for consistency and clarity, particularly for task progress, archive confirmations, and filtering instructions.
- Introduced new keys for improved user experience, such as "yes", "no", "list", "group", and "noPermission".
- Ensured all translations align with the latest UI changes for better internationalization support.
2025-07-10 14:16:51 +05:30
chamiakJ
bcfa18b1e8 feat(pwa): implement service worker and PWA enhancements
- Added service worker (sw.js) for offline functionality, caching strategies, and performance improvements.
- Registered service worker in App component to manage updates and offline readiness.
- Introduced ServiceWorkerStatus component to display connection status and provide cache management controls.
- Created manifest.json for PWA configuration, including app name, icons, and display settings.
- Updated index.html with PWA meta tags and links to support mobile web app capabilities.
- Refactored authentication guards to utilize useAuthStatus hook for improved user state management.
- Removed deprecated unregister-sw.js file to streamline service worker management.
2025-07-10 14:07:03 +05:30
chamiakJ
bb8e6ee60f refactor(email): enhance email validation and improve bounced email handling
- Added isValidateEmail utility function to validate email addresses before sending.
- Updated email filtering logic to remove empty, null, undefined, and invalid emails from the recipient list.
- Reversed the iteration order in removeMails function to prevent index issues while splicing bounced emails.
- Ensured that valid emails are present after filtering before proceeding with the email sending process.
2025-07-10 12:28:52 +05:30
chamiakJ
6ebdd78855 feat(task-timer): add timer start and stop handlers for task management
- Implemented handleTimerStart and handleTimerStop functions to manage task timer state via socket events.
- Updated useTaskSocketHandlers to register new socket event listeners for timer actions.
- Enhanced useTaskTimer to retrieve active timer state from both the old and new task management slices.
- Added activeTimer property to Task type for tracking the start timestamp of active timers.
2025-07-10 12:27:15 +05:30
chamiakJ
70cca5d4c0 refactor(task-list): restructure TaskRow and introduce new column components
- Refactored TaskRow to simplify state management and enhance readability by extracting logic into custom hooks.
- Introduced new components for rendering task columns, including DatePickerColumn, TitleColumn, and various column types for better modularity.
- Improved task name editing functionality with better state handling and click outside detection.
- Streamlined date handling and formatting within the task row for improved user experience.
2025-07-10 12:17:22 +05:30
chamiakJ
6448d24e20 refactor(task-list): simplify subtask handling in AddSubtaskRow and TaskRowWithSubtasks
- Updated AddSubtaskRow to remove rowId dependency from onSubtaskAdded and onActivate callbacks, streamlining the subtask addition process.
- Enhanced input handling to maintain focus and visibility after adding a subtask.
- Refactored TaskRowWithSubtasks to consolidate add subtask row management, ensuring a single add subtask row is displayed when not loading.
2025-07-10 12:00:56 +05:30
chamiakJ
5fb2633bc5 refactor(task-drawer): update localization keys for created and updated timestamps
- Modified localization JSON files for multiple languages to use double curly braces for variable interpolation in the createdBy and updatedTime fields.
- Ensured consistency across English, German, Spanish, Portuguese, Albanian, and Chinese translations for better formatting of dynamic content.
2025-07-09 22:51:20 +05:30
chamiakJ
75c55fff21 refactor(search): improve SQL search handling and optimize project list component
- Enhanced search handling in WorklenzControllerBase to properly escape single quotes, preventing SQL syntax errors.
- Refactored search logic in ProjectList to maintain reference stability and improve performance during debounced searches.
- Removed unnecessary console logs and optimized loading state management for better user experience.
2025-07-09 22:38:58 +05:30
chamikaJ
8f5de8f1a1 refactor(task-management): update search handling and improve task filtering
- Modified search handling to utilize the taskManagement slice for consistent state management across components.
- Enhanced placeholder text in search filters for better user guidance.
- Updated task fetching logic to ensure accurate search value retrieval from the correct state slice.
2025-07-09 17:11:15 +05:30
chamikaJ
db9b481e8d refactor(task-list): enhance task addition functionality in TaskListV2Table and AddTaskRow
- Introduced state management for dynamic add task rows in TaskListV2Table, allowing real-time updates when tasks are added.
- Updated handleTaskAdded to manage new task row creation based on group ID.
- Enhanced AddTaskRow to support auto-focus functionality and unique row identification for improved user experience during task addition.
- Refactored input handling in AddTaskRow to maintain focus and streamline task creation process.
2025-07-09 17:02:37 +05:30
Chamika J
cdd22e5f2f Merge pull request #252 from shancds/test/row-kanban-board-v1.1.7
Test/row kanban board v1.1.7
2025-07-09 16:58:46 +05:30
shancds
635b5ce8e1 feat(task-drawer): add functionality to hide task drawer on task deletion
- Imported setShowTaskDrawer action to manage task drawer visibility.
- Updated TaskDrawerHeader to dispatch setShowTaskDrawer(false) after task deletion, improving user experience by closing the drawer automatically.
2025-07-09 16:55:03 +05:30
shancds
1a476a0e3c Merge branch 'test/row-kanban-board-v1.1.6' of https://github.com/shancds/worklenz-open-source into test/row-kanban-board-v1.1.7 2025-07-09 16:41:51 +05:30
shancds
80b1d6c292 Merge branch 'fix/release-v.2.1.1' of https://github.com/Worklenz/worklenz into test/row-kanban-board-v1.1.7 2025-07-09 16:36:59 +05:30
chamikaJ
deb0f3f602 refactor(task-list): enhance task rendering and editing functionality in TaskRow and TaskListV2Table
- Updated TaskListV2Table to pass isFirstInGroup prop to renderTask for improved task grouping logic.
- Enhanced TaskRow to support inline editing of task names with a new input field and associated state management.
- Implemented click outside detection to save task name changes when editing is complete.
- Improved layout and styling for better user experience during task editing and display.
2025-07-09 16:32:28 +05:30
shancds
71f168f8fa feat(kanban-board): add subtasks localization and enhance task drawer functionality
- Added new localization keys for subtasks in the kanban board JSON file to improve user experience.
- Updated the SubTaskTable component to dispatch enhanced kanban subtask updates upon deletion.
- Enhanced the TaskDrawerHeader to handle deletion of subtasks with appropriate state updates in the kanban context.
2025-07-09 16:27:48 +05:30
chamikaJ
6f63041148 refactor(task-list): update status handling and enhance styling in TaskListV2Table
- Modified status assignment in useTaskSocketHandlers to utilize actual status_id from the response for improved accuracy.
- Simplified status logic by directly using data.status in task creation.
- Enhanced styling in TaskListV2Table by adding border styles for better visual separation of elements.
2025-07-09 15:57:08 +05:30
chamikaJ
399a01904a refactor(task-list): enhance styling and structure in TaskListV2 and TaskRow components
- Consolidated import statements for better readability.
- Improved layout and styling consistency by adding border styles to various elements in TaskRow and AddTaskRow components.
- Updated TaskListV2Table to enhance the rendering logic and maintainability.
- Adjusted custom column handling and task estimation display for improved user experience.
2025-07-09 14:58:54 +05:30
chamikaJ
9cc19460bd Merge branch 'fix/release-v.0.1.1' of https://github.com/Worklenz/worklenz into fix/release-v.2.1.1 2025-07-09 14:51:26 +05:30
Chamika J
2920f131f8 Merge pull request #251 from shancds/test/row-kanban-board-v1.1.5
refactor(task-list): streamline TaskListV2 component and improve stru…
2025-07-09 14:30:08 +05:30
shancds
04f622a7f0 refactor(task-list): streamline TaskListV2 component and improve structure
- Removed unused imports and consolidated task list logic for better readability.
- Introduced TaskListV2Section for improved organization and separation of concerns.
- Enhanced task filtering and rendering logic to optimize performance and maintainability.
- Updated styling and layout for a cleaner user interface and better usability.
2025-07-09 14:21:10 +05:30
chamikaJ
fadc115412 feat(task-reporter): add reporter field to task data structure
- Introduced a new 'reporter' field in the task data structure for both backend and frontend task management.
- Updated the tasks-controller to include the reporter information when transforming task data.
- Modified the fetchTasks and fetchTasksV3 functions to handle the reporter field, ensuring it defaults to undefined when not present.
2025-07-09 13:01:51 +05:30
chamikaJ
10c53d954e refactor(task-list): unify date handling and enhance column widths
- Updated TaskRow component to handle both camelCase and snake_case date fields for created and updated timestamps.
- Adjusted column widths for due date, start date, completed date, created date, and last updated fields for better layout consistency.
- Ensured whitespace handling in date display spans for improved UI presentation.
2025-07-09 12:32:17 +05:30
chamikaJ
29a09ec500 refactor(task-drawer): enhance task deletion handling and update imports
- Updated task deletion logic to ensure consistency across task management slices, including clearing selections and updating the board state.
- Refactored imports to streamline task management functionality and improve code clarity.
- Added new Ant Design icon import for enhanced UI options.
2025-07-09 12:22:37 +05:30
chamikaJ
6dba080ade refactor(task-drawer): streamline task name handling and enhance socket management
- Removed local socket listener for task name changes, relying on global socket handlers for real-time updates.
- Simplified task name change handling logic to improve clarity and maintainability.
- Enhanced task status group matching logic in useTaskSocketHandlers for better accuracy with multiple matching strategies.
- Added detailed logging for task movement and status changes to aid in debugging and tracking.
2025-07-09 12:11:11 +05:30
chamikaJ
ab7ca33ac1 feat(localization): improve translation handling and update UI labels
- Updated the 'board' label in project-view translations for consistency.
- Enhanced the getTabLabel function to include fallback labels for better user experience when translations are not available.
- Implemented translation loading checks in ProjectView to ensure labels are updated correctly based on the selected language.
- Refactored tab label updates to handle translation loading errors gracefully.
2025-07-09 11:58:40 +05:30
chamikaJ
bc6a15de8f feat(localization): add 'share' label translations for multiple languages
- Added the 'share' label to project view headers in Albanian, German, Spanish, Portuguese, Chinese, and English to enhance user interaction.
- Updated corresponding button icons and labels in the project view header for improved functionality and consistency.
2025-07-09 11:41:09 +05:30
chamikaJ
a47a9045e6 feat(localization): add new label management translations for multiple languages
- Introduced new translation keys for label management features in Albanian, German, Spanish, Portuguese, Chinese, and English.
- Updated placeholder texts, button labels, and paths to enhance user experience in the task list component.
2025-07-09 11:31:00 +05:30
Tharindu shan
b6e92b4211 Merge branch 'Worklenz:main' into test/row-kanban-board-v1.1.4 2025-07-09 08:58:51 +05:30
Omindu Hirushka
5222d75064 user activity feed - frontend 2025-07-09 07:44:23 +05:30
Omindu Hirushka
2587b8afd9 user activity feed - backend 2025-07-09 07:43:44 +05:30
Chamika J
6c08f10e9d Merge pull request #249 from Worklenz/release/v2.1.0
Release/v2.1.0
2025-07-09 06:37:37 +05:30
Chamika J
6c620d6878 Merge pull request #247 from Worklenz/release/v2.0.4-bug-fix
Release/v2.0.4 bug fix
2025-07-09 06:22:56 +05:30
chamikaJ
072c1a6a3b refactor(task-list): improve layout and styling for task list and rows
- Adjusted padding and alignment in TaskListV2 for better visual consistency.
- Enhanced styling in TaskRow to ensure uniformity across various task elements.
- Updated flex properties for improved responsiveness and usability in task display.
2025-07-08 17:10:56 +05:30
Chamika J
78e14d6378 Merge pull request #245 from shancds/test/row-kanban-board-v1.1.4
Test/row kanban board v1.1.4
2025-07-08 17:00:19 +05:30
chamikaJ
68e71d09ea refactor(task-list): enhance layout and styling for improved usability
- Adjusted heights and padding in the task list and filters for better space optimization.
- Updated styling in TaskRow for consistent alignment of task elements.
- Refined padding in ImprovedTaskFilters for a cleaner appearance.
- Removed unnecessary margin in ProjectViewHeader to streamline layout.
2025-07-08 16:51:44 +05:30
chamikaJ
6ac2a0c888 refactor(task-list): improve layout and styling for better usability
- Removed unnecessary padding from task filters for a cleaner look.
- Adjusted height calculations to optimize space usage in the task list.
- Added padding to the content area to ensure the horizontal scrollbar is visible.
- Updated subtask count checks for clarity and consistency.
- Modified gap and margin values in project view header for improved alignment.
2025-07-08 16:13:56 +05:30
chamikaJ
66e01119d2 refactor(task-drawer): update tab behavior and enhance link handling in description editor
- Changed tab component property from 'destroyInactiveTabPane' to 'destroyOnHidden' for improved tab management.
- Added CSS styles for links in the description editor to enhance visibility based on theme mode.
- Implemented link click handling to open links in a new tab while preventing default editor behavior, improving user experience.
2025-07-08 16:02:12 +05:30
chamikaJ
8fb33e311d feat(localization): enhance task drawer translations and UI consistency
- Updated translations for task drawer components in Albanian, German, Spanish, Portuguese, Chinese, and English to improve clarity and consistency.
- Added new keys for task progress, activity log, and time log forms to support enhanced user interactions.
- Improved placeholder texts, error messages, and button labels for better user experience across multiple languages.
- Refactored activity log component to utilize translation keys for dynamic content rendering.
2025-07-08 15:51:47 +05:30
chamikaJ
f06851fa37 feat(localization): add and update translations for multiple languages
- Introduced new localization files for Albanian, German, Spanish, Portuguese, and Chinese, enhancing the application's multilingual support.
- Added new keys and updated existing translations in project-view, task-list-table, and settings files to improve user experience across different languages.
- Enhanced error handling and empty state messages in task management components to provide clearer feedback to users.
- Updated tooltip texts and button labels for better clarity and consistency in the user interface.
2025-07-08 15:26:55 +05:30
chamikaJ
e750023fdc Merge branch 'development' of https://github.com/Worklenz/worklenz into release/v2.0.4-bug-fix 2025-07-08 13:12:22 +05:30
shancds
e2e57fbf26 fix(KanbanGroup): enhance input focus behavior to select text on focus
- Updated the focus handling in KanbanGroup to select all text in the input field when focused, improving user experience during task editing.
2025-07-08 12:59:31 +05:30
Chamika J
56d6a53a54 Merge pull request #208 from jiuhao47/main
add support for zh_cn
2025-07-08 12:57:07 +05:30
shancds
ee6055934c refactor(AssigneeSelector): streamline component logic and enhance dropdown behavior
- Removed unused kanbanMode prop and related logic to simplify the AssigneeSelector component.
- Updated dropdown position handling to improve visibility and responsiveness during interactions.
- Optimized member selection logic for better performance and user feedback.
- Enhanced the rendering of team members with improved visual feedback for pending changes.
2025-07-08 12:49:49 +05:30
chamikaJ
03b3f55400 fix(project-list): enhance grouped request parameters handling
- Updated the initialization of grouped request parameters to use a proper groupBy value.
- Improved the effect dependencies to include groupBy, ensuring correct state management.
- Enhanced the logic for fetching grouped projects, ensuring parameters are set correctly and data is retrieved when necessary.
- Added comments for clarity on the conditions for fetching grouped projects.
2025-07-08 12:43:00 +05:30
shancds
2aab2a21b6 feat(EnhancedKanbanBoard): implement drag end handling for improved task interaction
- Added handleDragEnd function to reset hovered task and group states after drag operations in EnhancedKanbanBoard.
- Updated KanbanGroup and TaskCard components to support onDragEnd prop, enhancing drag-and-drop functionality and user experience during task management.
2025-07-08 12:13:23 +05:30
chamikaJ
a44b276269 feat(email-templates): update release note template for Worklenz 2.1.0
- Added a title and meta subject for the release note template.
- Enhanced styling for better readability and user experience, including background color, font adjustments, and button styles.
- Introduced new sections for features, including a new tasks list, kanban board, group view, language support, and bug fixes.
- Improved responsiveness and dark mode support for the email template.
2025-07-08 11:59:17 +05:30
shancds
d150747f83 refactor(ProjectViewEnhancedBoard): remove unused EnhancedKanbanBoard import 2025-07-08 10:57:34 +05:30
shancds
fa9e765e37 refactor(KanbanGroup, TaskCard): enhance drag-and-drop indicators and task rendering
- Added visual drop indicators before and after task cards in the KanbanGroup component to improve user experience during drag-and-drop operations.
- Removed the isDropIndicator prop from TaskCard as the drop indicator logic is now handled within the KanbanGroup, simplifying the TaskCard component.
- Updated drag-and-drop event handling in TaskCard to better manage task positioning during drag operations, enhancing overall functionality.
2025-07-07 17:20:34 +05:30
chamikaJ
b0253135e5 feat(project-drawer): enhance project data fetching and error handling
- Updated project data fetching logic in the project drawer and related components to ensure the drawer opens only after successful data retrieval.
- Added detailed logging for successful and failed fetch attempts to improve debugging and user feedback.
- Introduced error handling to maintain user experience by allowing the drawer to open even if data fetching fails, displaying an error state.
- Refactored project list and project view components to optimize search functionality and improve loading states.
- Removed deprecated components related to task management to streamline the project view.
2025-07-07 17:07:45 +05:30
shancds
8e62594eff fix(TaskCard): update background color for improved UI consistency
- Changed the background color of the TaskCard component from '#f0f0f0' to '#E2EAF4' in light mode to enhance visual consistency and user experience.
2025-07-07 15:17:33 +05:30
chamikaJ
978d9158c0 feat(database): add performance indexes and materialized view for optimization
- Created multiple new indexes in the performance-indexes.sql file to enhance query performance for tasks, team members, and related tables.
- Added a materialized view for team member information in 3_views.sql to pre-calculate expensive joins, improving data retrieval efficiency.
- Introduced a function to refresh the materialized view, ensuring up-to-date information while optimizing performance.
- Implemented an optimized method in tasks-controller-v2.ts to split complex queries into focused segments, significantly improving response times and overall performance.
2025-07-07 15:01:11 +05:30
chamikaJ
134899114d refactor(index.html): update production tracking ID logic
- Simplified the logic for determining the production tracking ID by removing the check for the 'worklenz.com' hostname.
- Ensured that the tracking ID is now solely based on the 'app.worklenz.com' hostname, streamlining the analytics loading process.
2025-07-07 14:50:01 +05:30
chamikaJ
8533a440bc refactor(tasks-controller): enhance getTasksV3 method for performance and clarity
- Improved logging for performance tracking in the getTasksV3 method.
- Streamlined progress refresh logic and removed unnecessary calculations to optimize initial load times.
- Unified query handling by aligning with the getList method for consistency.
- Transformed response structure to maintain compatibility with V3 format while ensuring efficient data processing.
- Added memoized selectors in the frontend for better performance and reduced re-renders.
2025-07-07 14:30:19 +05:30
chamikaJ
9ec422c6e2 Merge branch 'release/v2.0.4-bug-fix' of https://github.com/Worklenz/worklenz into release/v2.0.4-bug-fix 2025-07-07 14:10:36 +05:30
Chamika J
6c03bf71c2 Merge pull request #243 from shancds/test/row-kanban-board-v1.1.3
Test/row kanban board v1.1.3
2025-07-07 14:10:06 +05:30
chamikaJ
3887cc477d refactor(tasks-controller): enhance SQL query structure and improve task filtering
- Updated SQL queries in TasksControllerV2 to use table aliases for clarity and consistency.
- Improved filtering logic for project IDs and assignees to ensure accurate task retrieval.
- Refactored JSON object construction in SQL queries for better readability and maintainability.
- Adjusted parameter handling in query execution to streamline data retrieval processes.
2025-07-07 14:10:04 +05:30
chamikaJ
0b96d59285 refactor(tasks-controller): remove Redis caching logic for task retrieval
- Eliminated caching logic from the TasksControllerV2 to streamline task retrieval and improve performance.
- Updated response handling to directly return task data without caching, enhancing clarity and reducing complexity.
- Removed cache invalidation method to simplify the controller's responsibilities.
2025-07-07 13:22:17 +05:30
chamikaJ
a3f317cbeb refactor(vite.config): simplify chunking strategy and optimize asset naming
- Updated the chunking strategy to keep React and related libraries together, improving compatibility and reducing context issues.
- Simplified asset naming conventions for better caching and consistency, removing unnecessary hash lengths.
- Adjusted optimization settings to ensure critical libraries are included while excluding heavy libraries for lazy loading.
2025-07-07 13:16:26 +05:30
shancds
5a9ceb4a94 feat(EnhancedKanbanBoard): add task dependency check during drag-and-drop
- Implemented a check for task dependencies when moving tasks between groups in the EnhancedKanbanBoard component.
- Integrated alert notifications to inform users if a task cannot be moved due to incomplete dependencies, enhancing user experience and task management integrity.
2025-07-07 13:12:50 +05:30
chamikaJ
bdc3050a5e feat(database): add performance indexes for optimized task queries
- Introduced a new SQL migration file to create various performance indexes on tasks, task_assignees, task_phase, and related tables.
- These indexes aim to enhance query performance for task filtering, status joins, assignees lookup, and other operations, improving overall application efficiency.
2025-07-07 13:10:27 +05:30
chamikaJ
bc085926a6 refactor(vite.config): clean up unnecessary whitespace in configuration file 2025-07-07 12:47:19 +05:30
chamikaJ
aa1fb1c6f5 feat(performance): optimize resource loading and initialization
- Added resource hints in index.html for improved loading performance, including preconnect and dns-prefetch links.
- Implemented preload for critical JSON resources to enhance initial load times.
- Optimized Google Analytics and HubSpot script loading to defer execution and reduce blocking during initial render.
- Refactored app initialization in App.tsx to defer non-critical operations, improving perceived performance.
- Introduced lazy loading for chart components and TinyMCE editor to minimize initial bundle size and enhance user experience.
- Enhanced Vite configuration for optimized chunking strategy and improved caching with shorter hash lengths.
2025-07-07 12:41:23 +05:30
chamikaJ
26b47aac53 refactor(i18n): optimize translation loading and initialization
- Updated ensureTranslationsLoaded function to prevent duplicate requests by caching loaded translations and managing loading promises.
- Simplified translation preloading on app startup to only load essential namespaces for the current language.
- Adjusted useTranslationPreloader hook to avoid multiple requests for translations and ensure efficient loading state management.
2025-07-07 12:31:11 +05:30
shancds
8dcd0295e5 refactor(EnhancedKanbanBoard): improve drag-and-drop handling and task index management
- Updated drag-and-drop event handlers in EnhancedKanbanBoard to support null task indices, enhancing flexibility during task interactions.
- Adjusted KanbanGroup component to reflect changes in task index handling, ensuring consistent behavior when dragging tasks over empty drop zones.
- Enhanced the visual structure of the KanbanGroup to improve user experience during task creation and management.
2025-07-07 12:05:05 +05:30
shancds
3206af160a refactor(TaskCard): enhance subtask visibility animation and styling
- Updated the TaskCard component to improve the visibility and animation of subtasks, adding a smooth transition effect for better user experience.
- Adjusted the rendering logic to conditionally display the subtasks container based on the task's state, enhancing the overall UI interaction.
2025-07-07 10:31:33 +05:30
shancds
b500c801ee refactor(KanbanGroup, TaskCard): simplify card creation logic and enhance drag-and-drop functionality
- Removed conditional checks for user roles when displaying the new task card options in KanbanGroup, streamlining the UI for task creation.
- Updated TaskCard component to improve the drag-and-drop indicator styling and structure, enhancing the user experience during task interactions.
2025-07-07 10:11:56 +05:30
Chamika J
3f1b8762dd Merge pull request #241 from Worklenz/release/v2.0.4-bug-fix
feat(task-management): add all_labels support and improve label handling
2025-07-07 09:41:24 +05:30
chamikaJ
a6f9046b42 feat(task-management): add all_labels support and improve label handling
- Introduced all_labels property in task management to provide a complete list of labels for selection logic.
- Updated TasksControllerV2, TaskRow, and LabelsSelector components to utilize all_labels for enhanced label management.
- Improved checkbox handling in LabelsSelector to prevent event propagation and ensure better user interaction.
- Enhanced useTaskSocketHandlers to manage temporary subtasks effectively, preventing duplication during optimistic updates.
2025-07-07 09:40:56 +05:30
Chamika J
2cf91bddea Merge pull request #240 from Worklenz/fix/task-drag-and-drop-improvement
refactor(TaskRow): improve layout and styling for task indicators and…
2025-07-07 07:22:13 +05:30
chamiakJ
e1e4187ded refactor(TaskRow): improve layout and styling for task indicators and subtasks
- Enhanced the layout of the TaskRow component to ensure better spacing and alignment for task indicators and subtasks.
- Updated CSS classes to prevent compression of elements and maintain consistent visual presentation.
- Improved tooltip functionality for task display names and indicators, ensuring better accessibility and user experience.
2025-07-07 07:20:45 +05:30
Chamika J
e02796c310 Merge pull request #239 from Worklenz/fix/task-drag-and-drop-improvement
refactor(components): enhance component structure and add forwardRef …
2025-07-07 07:16:57 +05:30
chamiakJ
5d9e96033e refactor(components): enhance component structure and add forwardRef support
- Refactored CustomAvatar, CustomColordLabel, CustomNumberLabel, and ProjectStatusIcon components to utilize React.forwardRef for improved ref handling.
- Introduced TooltipWrapper component to avoid findDOMNode warnings in React StrictMode, ensuring better compatibility with Ant Design's Tooltip.
- Updated TaskRow component to enhance layout and tooltip functionality for task display names, improving user experience and accessibility.
2025-07-07 07:16:10 +05:30
Chamika J
cc618960e6 Merge pull request #238 from Worklenz/fix/task-drag-and-drop-improvement
Fix/task drag and drop improvement
2025-07-07 07:05:59 +05:30
chamiakJ
f9926e7a5d feat(task-list): add tooltips for task indicators and enhance localization
- Introduced tooltips for subtasks, comments, attachments, subscribers, dependencies, and recurring tasks across various components to improve user experience.
- Enhanced localization by adding new translation keys for these indicators in multiple languages, ensuring consistent messaging for users.
- Updated components such as TaskRow, KanbanTaskCard, and EnhancedKanbanTaskCard to utilize the new tooltip functionality, improving clarity and accessibility.
2025-07-07 07:05:29 +05:30
chamiakJ
03fc2fb7ee refactor(task-list): update subtask and indicator visibility logic
- Modified subtask visibility conditions across TaskCard, EnhancedKanbanTaskCard, KanbanTaskCard, and TaskRow components to only display when the count is greater than 1.
- Enhanced task indicators for comments and attachments to follow the same visibility logic.
- Added new dependencies for task management types to support subscriber and attachment features.
2025-07-07 06:33:22 +05:30
chamiakJ
b6efa3f37e feat(task-filters): enhance search filter functionality and UI interactions
- Implemented synchronization of local search value with external value prop to maintain expanded state during searches.
- Updated button styles for better visibility in dark mode and improved user experience.
- Refactored search handling to ensure consistent use of taskReducer for search actions across different views.
- Enhanced filter section visibility logic to improve user interaction during data loading.
2025-07-07 05:52:07 +05:30
chamiakJ
85f20eaf1c refactor(task-list): update custom column handling and improve UI interactions
- Renamed CSS class for focused custom column cells to enhance clarity and consistency.
- Implemented optimistic updates in TaskListV2 for immediate UI feedback when updating custom column values.
- Adjusted rendering logic to support sticky headers and improved layout for task list components.
- Enhanced loading state management in SelectionCustomColumnCell for better user experience during updates.
2025-07-07 05:39:09 +05:30
chamiakJ
411147efce fix(task-list): update styling and improve task handling
- Adjusted color styling for the CreateStatusButton based on theme mode.
- Enhanced TaskGroupHeader with improved border styling and spacing for better visual consistency.
- Increased width and padding for the AddCustomColumnButton to improve usability.
- Updated TaskRow to include additional dependencies for task labels and phase updates in socket handling.
- Refactored task management slice to ensure accurate label and phase updates during real-time interactions.
- Removed unnecessary debug logging from CustomColumnModal and SelectionTypeColumn components for cleaner code.
2025-07-07 05:07:05 +05:30
chamiakJ
48c3d58f7e feat(task-list): enhance TaskListV2 with scroll synchronization and custom column handling
- Added scroll synchronization between header and content sections using refs for improved user experience.
- Refactored custom column handling to prioritize UUIDs for API interactions, ensuring accurate updates and data retrieval.
- Introduced a message display for empty task groups, enhancing user feedback when no tasks are available.
- Updated rendering logic for task groups and tasks to improve performance and maintainability.
2025-07-07 04:21:09 +05:30
chamiakJ
746d38017f feat(task-list): refine task list components and improve UI consistency
- Updated SubtaskLoadingSkeleton and TaskRow components for better spacing and visual consistency.
- Simplified TaskGroupHeader by removing unnecessary elements and enhancing the display of group names.
- Adjusted TaskListV2 to improve column rendering and added state management for field visibility synchronization with the database.
- Enhanced AddTaskRow and AddSubtaskRow components for improved user interaction and layout.
- Updated placeholder texts in CustomColumnComponents for better clarity.
2025-07-07 03:39:39 +05:30
chamiakJ
01298928c7 feat(task-filters): enhance localization and UI for task filters
- Added new translation keys for task filter functionalities across multiple languages, improving user experience and accessibility.
- Updated the ImprovedTaskFilters component to utilize localized strings for search placeholders, loading states, and filter messages.
- Enhanced styling for filter buttons and dropdowns to ensure consistency in both light and dark modes.
- Introduced a description column in task management components, improving the display of task details.
2025-07-07 03:04:16 +05:30
Chamika J
13ee16452b Merge pull request #237 from Worklenz/fix/task-drag-and-drop-improvement
Fix/task drag and drop improvement
2025-07-07 02:40:15 +05:30
chamiakJ
9a57413624 feat(task-filters): enhance archived task handling in task management
- Introduced state management for archived tasks in the task management slice, allowing for better control over task visibility.
- Updated ImprovedTaskFilters component to utilize the new archived state, enabling toggling of archived tasks based on the current view (list or kanban).
- Refactored related functions to ensure proper dispatching of archived state changes, improving user experience and task management efficiency.
2025-07-07 02:38:50 +05:30
chamiakJ
8d8250bc17 feat(task-list): enhance bulk action functionality with improved task handling
- Updated TaskListV2 to pass selected task IDs to bulk action handlers, improving functionality and user experience.
- Refactored useBulkActions hook to implement detailed handling for bulk status, priority, phase changes, and other actions, ensuring proper task management.
- Added loading states for individual bulk actions to provide visual feedback during processing.
- Implemented error handling and alerts for task dependency checks before executing bulk actions, enhancing reliability.
- Introduced new methods for bulk assigning members, adding labels, archiving, deleting, and duplicating tasks, streamlining task management processes.
2025-07-07 02:38:42 +05:30
chamiakJ
174c6bcedf feat(custom-columns): enhance task management with custom column support
- Added custom column values to task responses in the API for better task management flexibility.
- Implemented custom column components in the frontend, including dropdowns and date pickers, to improve user interaction.
- Updated TaskListV2 and TaskRow components to handle custom columns, ensuring proper rendering and functionality.
- Introduced a new PeopleDropdown component for selecting team members in custom columns, enhancing usability.
- Enhanced styling for custom column components to support both light and dark modes, improving visual consistency.
2025-07-07 02:04:05 +05:30
chamiakJ
c70f8e7b6d feat(task-statuses): add category update functionality and enhance localization
- Implemented updateCategory method in TaskStatusesController to allow updating task status categories.
- Added corresponding route for category updates in statusesApiRouter.
- Enhanced task management localization by adding new translation keys for category-related actions in multiple languages.
- Updated TaskGroupHeader component to support category changes with a modal for selecting categories.
2025-07-06 16:54:11 +05:30
chamiakJ
6ba1ff57b2 refactor(task-list): streamline task addition and socket handling
- Removed local socket listener from AddTaskRow, delegating task addition to the global socket handler for real-time updates.
- Simplified the handleTaskAdded function in TaskListV2, eliminating the need for refetching tasks upon addition.
- Updated useTaskSocketHandlers to handle task data more efficiently, ensuring proper integration with the Redux store.
2025-07-06 15:42:12 +05:30
chamiakJ
a5291483f7 feat(task-list): enhance task creation and UI components
- Improved the on_quick_task function to handle empty task names and emit null when no task is created, ensuring better user feedback.
- Updated SubtaskLoadingSkeleton and TaskRow components for improved styling and spacing, enhancing visual consistency.
- Introduced AddTaskRow component for streamlined task addition, integrating socket communication for real-time updates.
- Refactored TaskListV2 to optimize rendering and improve performance, including adjustments to column headers and task display.
- Added custom column components for enhanced task management flexibility and user interaction.
2025-07-06 14:51:22 +05:30
chamikaJ
e3a9618dc9 feat(task-list): improve layout and scrolling behavior in TaskListV2 component
- Adjusted the layout to ensure fixed heights for the task list and table container, enhancing usability.
- Implemented horizontal scrolling for the table container and ensured the bulk action bar is positioned absolutely for better accessibility.
- Updated styles for column headers and task list content to improve visual consistency and user experience.
2025-07-04 20:54:11 +05:30
chamikaJ
f30fde553d feat(task-management): enhance task grouping and localization support
- Implemented unmapped task grouping for better organization of tasks without valid phases.
- Updated task distribution logic to handle unmapped tasks and added a corresponding group in the response.
- Enhanced localization by adding translations for "noTasksInGroup" in multiple languages.
- Improved task list components to support custom columns and better task management features.
- Refactored task management slice to include loading states for columns and custom columns.
2025-07-04 20:41:03 +05:30
Chamika J
f9c1537ca0 Merge pull request #236 from shancds/test/row-kanban-board-v1.1.2
Test/row kanban board v1.1.2
2025-07-04 17:31:02 +05:30
shancds
208a6db1a6 feat(AssigneeSelector): add kanbanMode prop for enhanced task assignment
- Introduced kanbanMode prop to AssigneeSelector and LazyAssigneeSelectorWrapper for improved functionality in Kanban view.
- Updated EnhancedKanbanTaskCard and TaskCard components to utilize the new kanbanMode prop.
- Adjusted task sorting logic to handle cases where sort_order may be undefined, ensuring robust behavior during task updates.
2025-07-04 17:13:56 +05:30
shancds
9e1798cc3e fix(TaskCard): improve UI and interaction for subtasks
- Adjusted styling for task and subtask elements to enhance visual consistency.
- Updated subtask rendering to include priority color indicators based on theme mode.
- Added click handling for subtask items to improve user interaction.
- Refined layout and spacing for better usability and readability.
2025-07-04 15:41:52 +05:30
chamikaJ
9e29031703 feat(task-list): update column labels for localization and improve task count display
- Changed column labels to be more descriptive for localization support.
- Integrated translation functionality for column headers using `useTranslation`.
- Updated task count display to reflect actual task counts instead of collapsed counts.
2025-07-04 14:38:26 +05:30
shancds
3626192f31 Merge branch 'release/v2.0.4' of https://github.com/Worklenz/worklenz into test/row-kanban-board-v1.1.2 2025-07-04 14:23:44 +05:30
shancds
d246f8e3ed feat(TaskCard): add subtask expansion functionality and improve UI interactions
- Implemented subtask expansion logic with dispatch actions for toggling visibility and fetching subtasks.
- Enhanced UI to include a button for showing/hiding subtasks, improving user interaction.
- Updated task display to show subtask counts and loading states, ensuring better feedback during data fetching.
2025-07-04 14:23:23 +05:30
Chamika J
3ddf6900c9 Merge pull request #235 from Worklenz/fix/task-drag-and-drop-improvement
feat(task-management): enhance task management UI with subtask functi…
2025-07-04 13:25:42 +05:30
chamikaJ
aab3ffe262 feat(task-management): enhance task management UI with subtask functionality
- Updated task list components to support subtasks, including TaskRowWithSubtasks for rendering tasks with their subtasks.
- Introduced AddSubtaskRow for adding new subtasks directly within the task list.
- Enhanced TaskRow to handle task expansion and display subtask counts.
- Implemented optimistic updates for subtask creation to improve user experience.
- Added loading states for subtasks to provide visual feedback during data fetching.
- Refactored task management slice to manage subtasks and their loading states effectively.
2025-07-04 13:24:41 +05:30
shancds
56f129d784 feat(TaskCard): implement end date selection and refine label styling
- Added useEffect to set the selected date based on task's end date for improved date management.
- Adjusted label styling for better visual consistency, including reduced border radius, padding, and updated color scheme based on theme mode.
2025-07-04 12:31:25 +05:30
shancds
7fe35d646a style(TaskCard): refine date picker UI and adjust styling for better usability
- Reduced dimensions and padding of the date picker for a more compact design.
- Updated button sizes and text styles for improved readability and interaction.
- Adjusted layout spacing to enhance overall visual consistency and user experience.
2025-07-04 12:22:01 +05:30
shancds
31891fae6e feat(task-management): integrate date picker functionality in TaskCard component
- Added a date picker to the TaskCard for selecting and updating task due dates.
- Implemented real-time updates for due date changes using socket communication.
- Enhanced UI with a custom calendar popup and improved date selection options.
- Updated localization files to include new strings related to date management in multiple languages.
2025-07-04 12:16:10 +05:30
Chamika J
33ee3a521c Merge pull request #234 from shancds/test/row-kanban-board-v1.1.1
feat(localization): add new translations for Kanban board sections
2025-07-04 11:14:08 +05:30
shancds
df581b965a feat(localization): add new translations for Kanban board sections
- Introduced translations for "untitledSection" and "unmapped" in multiple languages including Albanian, German, English, Spanish, and Portuguese.
- Updated KanbanGroup component to utilize these new translations for improved internationalization support.
2025-07-04 11:10:16 +05:30
Chamika J
6cd7500073 Merge pull request #232 from shancds/test/row-kanban-board-v1.1.1
feat(enhanced-kanban): enhance KanbanGroup component with editing and…
2025-07-04 11:03:03 +05:30
Chamika J
20b9251eab Merge pull request #233 from Worklenz/fix/task-drag-and-drop-improvement
feat(task-management): improve dropdown behavior and enhance task row…
2025-07-04 11:02:26 +05:30
chamikaJ
6f66367282 feat(task-management): improve dropdown behavior and enhance task row components
- Updated AssigneeSelector and LabelsSelector to close dropdowns only when scrolling outside the dropdown area, enhancing user experience.
- Introduced TaskLabelsCell component in TaskRow for better label rendering and organization.
- Refactored date handling in TaskRow to consolidate formatted date logic and improve clarity.
- Memoized date picker handlers for better performance and cleaner code.
2025-07-04 11:01:21 +05:30
shancds
e566514ac0 feat(enhanced-kanban): enhance KanbanGroup component with editing and deletion features
- Added functionality for renaming Kanban groups with unique name generation.
- Implemented status updates and deletion logic for Kanban groups based on user interactions.
- Introduced loading states and dropdown menus for better user experience when managing groups.
- Enhanced UI elements for task creation and deletion confirmations, improving overall usability.
2025-07-04 10:57:02 +05:30
Chamika J
02db84e7f2 Merge pull request #231 from Worklenz/fix/task-drag-and-drop-improvement
Fix/task drag and drop improvement
2025-07-04 10:34:30 +05:30
chamikaJ
8adeabce61 feat(task-management): integrate TaskPhaseDropdown in TaskRow for enhanced task phase management
- Added TaskPhaseDropdown component to TaskRow for improved phase selection.
- Replaced static phase display with a dropdown for better interactivity and user experience.
2025-07-04 10:33:48 +05:30
chamikaJ
7e6d7d8580 feat(task-management): enhance task date handling and UI components in TaskListV2
- Added startDate and dueDate fields to task data structure for improved date management.
- Updated TaskRow to include date pickers for start and due dates with clear functionality.
- Enhanced LabelsSelector to support dynamic label rendering and improved visual feedback.
- Refactored AssigneeSelector and CustomColordLabel components for better integration with task data.
- Improved dropdown positioning logic in LabelsSelector for better user experience.
- Added translations for new date-related UI elements in multiple languages.
2025-07-04 10:29:51 +05:30
Chamika J
0781f3e13d Merge pull request #230 from Worklenz/fix/task-drag-and-drop-improvement
Fix/task drag and drop improvement
2025-07-04 09:00:38 +05:30
chamiakJ
64f1e5831a feat(task-management): enhance TaskGroupHeader and TaskListV2 for improved task display and reordering
- Updated TaskGroupHeader to display task count alongside group name for better clarity.
- Refactored task reordering logic in TaskListV2 to streamline drag-and-drop functionality across groups.
- Added TypeScript definitions for Heroicons to improve type safety and component usage.
- Adjusted styling in TaskGroupHeader for a more consistent visual presentation.
2025-07-04 07:50:16 +05:30
chamikaJ
551924c384 feat(task-management): integrate real-time updates and enhance task row components
- Added useTaskSocketHandlers to TaskListV2 for real-time task updates.
- Updated TaskRow to accept projectId prop for better context handling.
- Replaced static status and priority displays with TaskStatusDropdown and TaskPriorityDropdown for improved interactivity.
- Enhanced dropdown positioning logic in TaskStatusDropdown and TaskPriorityDropdown to handle scrollable containers effectively.
2025-07-03 20:02:00 +05:30
Chamika J
c889f8e9c8 Merge pull request #229 from shancds/test/row-kanban-board-v1.1.1
Test/row kanban board v1.1.1
2025-07-03 19:51:23 +05:30
Chamika J
86b5ec0afd Merge pull request #228 from Worklenz/fix/task-drag-and-drop-improvement
feat(task-management): enhance task assignment handling and UI feedba…
2025-07-03 19:29:05 +05:30
chamikaJ
6bf98b787e feat(task-management): enhance task assignment handling and UI feedback in AssigneeSelector
- Introduced optimistic updates for assignee selection to improve UI responsiveness.
- Updated AssigneeSelector to initialize optimistic assignees from task data on mount.
- Refactored task assignment logic to ensure unique assignee IDs and improved state management.
- Enhanced TaskGroupHeader and TaskListV2 to support bulk actions and selection state.
- Integrated a new bulk action bar for managing selected tasks efficiently.
2025-07-03 19:28:14 +05:30
shancds
3532b0bbfb feat(enhanced-kanban): enhance task card styling for improved UI
- Added a light border, box shadow, and background color to the EnhancedKanbanTaskCard for a more visually appealing design.
- Refactored the rendering of TaskCard components in KanbanGroup to eliminate unnecessary React.Fragment wrappers, improving code clarity.
2025-07-03 19:24:17 +05:30
shancds
6d4d851f1d feat(enhanced-kanban): replace loading spinner with skeleton loader
- Updated the loading state in the EnhancedKanbanBoardNativeDnD component to use a Skeleton loader instead of a Spin component for improved user experience during data fetching.
2025-07-03 19:13:03 +05:30
shancds
fb9e430ba0 feat(enhanced-kanban): update TaskCard to display task due date
- Added a new due date display within the TaskCard component, enhancing task visibility.
- Removed the previous due date element to streamline the layout and improve UI consistency.
2025-07-03 19:05:40 +05:30
shancds
73c78dd28f feat(enhanced-kanban): enhance TaskCard with task selection and improved UI
- Added task selection functionality by dispatching actions on card click to show the task drawer.
- Introduced labels display for tasks, enhancing visual organization.
- Improved task content layout with priority indicators and due date formatting.
- Integrated AvatarGroup and LazyAssigneeSelector for better assignee management and visibility.
2025-07-03 18:51:48 +05:30
Chamika J
509e654123 Merge pull request #227 from Worklenz/fix/task-drag-and-drop-improvement
Fix/task drag and drop improvement
2025-07-03 18:02:34 +05:30
chamikaJ
6b7f412341 feat(task-management): enhance task list with improved drag-and-drop functionality and visual feedback
- Integrated droppable areas in TaskGroupHeader for better task organization.
- Implemented drag-over visual feedback to indicate valid drop zones.
- Enhanced TaskListV2 to handle cross-group task movement and reordering.
- Optimized TaskRow for better rendering performance and added memoization for task details.
- Improved Redux state management for task reordering and group handling.
2025-07-03 18:02:00 +05:30
chamikaJ
edf051adc7 feat(task-management): enhance task list with drag-and-drop functionality and improved styling
- Integrated drag-and-drop capabilities using @dnd-kit for task reordering within the TaskListV2 component.
- Updated TaskGroupHeader to dynamically adjust colors based on group properties, improving visual clarity.
- Refactored TaskRow to support drag-and-drop interactions and optimized rendering of task details.
- Enhanced Redux state management for collapsed groups, transitioning from Set to array for better compatibility.
- Improved task display logic to handle optional fields and ensure consistent rendering across components.
2025-07-03 17:34:31 +05:30
shancds
aee09aeb0d feat(enhanced-kanban): implement real-time task updates via socket integration
- Added a useEffect hook to handle incoming tasks through a socket connection.
- Implemented logic to differentiate between regular tasks and subtasks, updating the state accordingly.
- Enhanced the EnhancedKanbanBoardNativeDnD component to support real-time task management, improving user interactivity.
2025-07-03 15:39:11 +05:30
chamikaJ
d15c00c29b feat(task-management): enhance task list with new components and improved state management
- Introduced TaskListV2 and TaskGroupHeader components for a more organized task display.
- Implemented virtualized rendering using react-virtuoso for efficient task list handling.
- Updated Redux state management to include new selectors and improved task grouping logic.
- Added task filtering capabilities with TaskListFilters component for better user experience.
- Enhanced task selection handling and integrated drag-and-drop functionality for task rows.
- Updated package dependencies to include new libraries for icons and forms.
2025-07-03 15:31:54 +05:30
shancds
6c4bcbe300 feat(enhanced-kanban): allow dynamic grouping in task reordering
- Integrated dynamic grouping by utilizing the `groupBy` state from the enhancedKanbanReducer.
- Updated the task reordering logic to respect the selected grouping, defaulting to 'status' when no group is specified.
- Enhanced the overall flexibility of the EnhancedKanbanBoardNativeDnD component for improved user experience.
2025-07-03 15:17:19 +05:30
shancds
2ff0555493 feat(enhanced-kanban): implement real-time task and group reordering with socket integration
- Added socket functionality to emit task sort order changes during drag-and-drop operations.
- Enhanced group drop handling to include API calls for updating group order, with error handling and alerts for failed updates.
- Integrated authentication and socket context to manage project and team information effectively.
2025-07-03 14:52:42 +05:30
shancds
e84ab43b36 refactor(enhanced-kanban): clean up TaskCard component structure
- Reformatted the TaskCard component for improved readability by adjusting indentation and spacing.
- Ensured consistent styling and structure for the drop indicator and task card elements.
- Maintained existing functionality while enhancing code clarity and maintainability.
2025-07-03 14:08:09 +05:30
shancds
8134c6af35 feat(enhanced-kanban): add task creation functionality to Kanban groups
- Introduced EnhancedKanbanCreateTaskCard component for adding tasks directly within Kanban groups.
- Implemented conditional rendering of task creation buttons based on user roles (owner/admin or project manager).
- Enhanced the KanbanGroup component to support task creation at both the top and bottom of the task list, improving user experience.
2025-07-03 12:31:51 +05:30
shancds
e05169b7b4 refactor(enhanced-kanban): simplify EnhancedKanbanBoardNativeDnD component
- Removed unused imports and commented-out code to clean up the component.
- Consolidated task and group rendering logic by importing KanbanGroup directly.
- Streamlined the code structure for better readability and maintainability.
2025-07-03 11:33:31 +05:30
shancds
df62f15734 refactor(enhanced-kanban): improve task reordering logic in EnhancedKanbanBoardNativeDnD
- Enhanced drag-and-drop functionality to handle both same-group and cross-group task reordering more efficiently.
- Simplified the task update process by consolidating logic for updating task arrays.
- Ensured proper index adjustments during reordering to maintain task integrity and prevent errors.
2025-07-03 11:22:52 +05:30
chamikaJ
e26f16bbc2 feat(routes): implement lazy loading and suspense fallback for route components
- Refactored account setup, admin center, reporting, and settings routes to utilize React's lazy loading for improved performance.
- Wrapped route components in Suspense with a fallback UI to enhance user experience during loading states.
- Removed unused drag-and-drop CSS file to clean up the codebase.
2025-07-03 10:34:06 +05:30
Chamika J
7623ea2f7f Merge pull request #225 from Worklenz/fix/WB-705-task-list-timer-cell
Fix/wb 705 task list timer cell
2025-07-03 09:46:09 +05:30
chamikaJ
c19c1c2f34 refactor(task-management): optimize task selection handling in TaskListBoard
- Improved task selection logic by utilizing a Set for efficient addition and removal of selected task IDs.
- Streamlined the dispatch of selected tasks and IDs to Redux state, enhancing performance and readability.
- Removed unused context menu logic from TaskRow component, simplifying the codebase and focusing on subtask expansion management.
2025-07-03 09:44:16 +05:30
shancds
6443a03afd Merge branch 'release/v2.0.4' of https://github.com/Worklenz/worklenz into test/row-kanban-board-v1 2025-07-03 08:10:29 +05:30
chamiakJ
bb4229a82d Merge branch 'release/v2.0.4' of https://github.com/Worklenz/worklenz into fix/WB-705-task-list-timer-cell 2025-07-03 07:48:00 +05:30
chamiakJ
e41cead10b feat(task-management): implement context menu for task actions
- Added a context menu to the TaskRow component, allowing users to perform actions such as assigning tasks, archiving, deleting, and moving tasks between statuses, priorities, and phases.
- Introduced TaskContextMenu component to handle context menu logic and interactions.
- Enhanced task row styling for improved hover effects and visibility in both light and dark modes.
- Updated task management slice to include new actions for handling task assignments and conversions.
2025-07-03 07:42:43 +05:30
chamiakJ
ecd4d29a38 expand sub tasks 2025-07-03 01:31:05 +05:30
shancds
7dfaacd28e refactor(enhanced-kanban): update drag-and-drop functionality in EnhancedKanbanBoardNativeDnD
- Added `idx` prop to TaskCard for better task index management during drag-and-drop.
- Adjusted drop indicator styling for improved visibility.
- Commented out unused drag-and-drop handlers in KanbanGroup for clarity.
2025-07-02 18:11:31 +05:30
shancds
775a91889f refactor(enhanced-kanban): relocate EnhancedKanbanBoardNativeDnD component
- Moved EnhancedKanbanBoardNativeDnD to a new directory for better organization.
- Updated import paths in ProjectViewEnhancedBoard to reflect the new location.
2025-07-02 16:36:11 +05:30
Chamika J
3159ba14b9 Merge pull request #224 from Worklenz/fix/WB-705-task-list-timer-cell
feat(task-management): add configuration buttons and permission check…
2025-07-02 16:12:12 +05:30
chamikaJ
3bef18901a feat(task-management): add configuration buttons and permission checks to task filters
- Introduced ConfigPhaseButton and CreateStatusButton components for enhanced task filtering options.
- Implemented permission checks to conditionally render configuration buttons based on user roles (owner/admin or project manager).
- Removed the TaskManagementDemo component as it was no longer needed, streamlining the codebase.
2025-07-02 16:11:48 +05:30
Chamika J
a2395f121b Merge pull request #223 from Worklenz/fix/WB-705-task-list-timer-cell
Fix/wb 705 task list timer cell
2025-07-02 16:01:29 +05:30
chamikaJ
a1e8a4c464 feat(task-management): enhance bulk action bar and localization updates
- Added new features to the OptimizedBulkActionBar, including dropdowns for labels and assignees, improving task management capabilities.
- Integrated task template creation functionality for owners/admins, allowing users to create templates from selected tasks.
- Updated localization files for multiple languages, adding new strings for label searching and template name requirements to enhance user experience.
- Refactored LabelsDropdown component to support label filtering and improved UI feedback for label creation.
2025-07-02 16:01:04 +05:30
shancds
11e5a6d379 feat(enhanced-kanban): enhance Kanban board with improved task filtering and loading states
- Integrated ImprovedTaskFilters component for better task management.
- Added loading and error handling states to the Kanban board for improved user experience.
- Updated drag-and-drop functionality to dispatch actions for reordering tasks and groups directly from Redux state.
2025-07-02 15:42:53 +05:30
chamikaJ
365369cc31 feat(i18n): enhance translation loading and preloading mechanism
- Introduced a utility function `ensureTranslationsLoaded` to preload essential translation namespaces, improving app initialization.
- Updated `App` component to initialize translations alongside CSRF token on startup.
- Created custom hooks `useTranslationPreloader`, `useBulkActionTranslations`, and `useTaskManagementTranslations` to manage translation readiness and prevent Suspense issues.
- Refactored components to utilize new translation hooks, ensuring translations are ready before rendering.
- Enhanced `OptimizedBulkActionBar` and `TaskListBoard` components to improve user experience during language switching.
2025-07-02 15:37:24 +05:30
chamikaJ
0452dbd179 feat(task-management): implement task reordering and group updates via API
- Added API methods for reordering tasks and updating task groups (status, priority, phase).
- Enhanced task management slice with async thunks for handling task reordering and group movements.
- Updated task list board to support real-time updates during drag-and-drop operations, emitting socket events for task sort order changes.
- Refactored task-related components to utilize shared Ant Design imports for consistency and maintainability.
- Removed unused Ant Design imports and optimized drag-and-drop CSS for improved performance.
2025-07-02 15:17:21 +05:30
shancds
d70fb133b7 feat(enhanced-kanban): integrate native drag-and-drop functionality
- Replaced the existing EnhancedKanbanBoard component with EnhancedKanbanBoardNativeDnD to support native drag-and-drop interactions.
- Commented out the previous EnhancedKanbanBoard usage for potential future reference.
2025-07-02 14:33:51 +05:30
chamikaJ
2064c0833c feat(task-management): enhance task details and subtask handling
- Added subtask-related properties to the Task interface for better management of subtasks.
- Implemented functionality to show and add subtasks directly within the task list, improving user interaction.
- Updated task rendering logic to accommodate new subtask features, enhancing overall task management experience.
- Removed unused components and optimized imports across various task management files for cleaner code.
2025-07-02 12:38:24 +05:30
Chamika J
d0947112eb Merge pull request #222 from shancds/fix/project-drawer-issues
refactor(enhanced-kanban): update state management for editable section
2025-07-02 12:34:14 +05:30
shancds
c9d9134049 refactor(enhanced-kanban): update state management for editable section
- Changed the source of `editableSectionId` from `boardReducer` to `enhancedKanbanReducer` for improved state organization.
- Added `setEditableSection` and `deleteSection` actions to manage the editable section state within the enhanced kanban slice.
2025-07-02 12:28:34 +05:30
Chamika J
91b8f4ca2b Merge pull request #221 from shancds/fix/project-drawer-issues
fix(enhanced-kanban): update subtask placeholder text for clarity
2025-07-02 12:23:30 +05:30
shancds
d56eaa9f02 fix(enhanced-kanban): prevent unnecessary state updates on blur event 2025-07-02 12:05:54 +05:30
shancds
71e1d58ec6 refactor(enhanced-kanban): simplify task creation event handling
- Updated the event handler in EnhancedKanbanCreateTaskCard to clarify that the form reset is separate from the global task addition to Redux, improving code readability and maintainability.
2025-07-02 11:41:43 +05:30
shancds
382283d0ce fix(enhanced-kanban): update subtask placeholder text for clarity 2025-07-02 11:20:56 +05:30
chamikaJ
c29ba6ea69 refactor(task-row): clean up imports and remove unused components
- Removed unused imports and components from task-row.tsx
2025-07-02 09:34:02 +05:30
chamikaJ
cf5f5c1449 feat(task-row): integrate task timer functionality into task management
- Added TaskTimer component to manage task time tracking.
- Updated TaskTimeTracking to utilize the new timer functionality, enhancing user interaction with task timing.
- Refactored props to pass task ID for timer management, improving state handling.
2025-07-02 09:27:36 +05:30
Chamika J
d5796b2cb5 Merge pull request #219 from Worklenz/fix/WB-708-task-progress
feat(task-row): add progress indicator with CheckCircle icon
2025-07-02 09:21:38 +05:30
chamikaJ
dd8bfe9fce feat(task-row): add progress indicator with CheckCircle icon
- Introduced a visual indicator for task progress, displaying a CheckCircle icon when progress reaches 100%.
- Updated the rendering logic to show a progress bar for incomplete tasks, enhancing user feedback on task status.
2025-07-02 09:21:27 +05:30
Chamika J
f80ec9797e Merge pull request #216 from Dev-Tanaay/bug/Documentation
Solved the documentation error with issue number
2025-07-02 09:09:27 +05:30
Chamika J
eb158678d4 Merge pull request #217 from shancds/fix/enhanced-board-assignees
Fix/enhanced board assignees
2025-07-02 09:08:55 +05:30
Chamika J
865502a796 Merge pull request #218 from Worklenz/fix/bulk-action-bar
feat(task-management): enhance bulk action bar with new features and …
2025-07-02 09:08:36 +05:30
chamikaJ
7a7856bc36 Merge branch 'release/v2.0.4' of https://github.com/Worklenz/worklenz into fix/bulk-action-bar 2025-07-02 09:07:09 +05:30
chamikaJ
756c9b892f feat(suspense-fallback): replace Spin with Skeleton for improved loading experience
- Updated the SuspenseFallback component to use Ant Design's Skeleton instead of Spin for a more visually appealing loading state.
- Enhanced the layout with padding and responsive width settings for better presentation.
- Adjusted InlineSuspenseFallback to implement similar Skeleton loading structure, improving consistency across components.
2025-07-02 08:59:45 +05:30
chamiakJ
ccde08b700 feat(task-management): enhance bulk action bar with new features and translations
- Added new bulk action options for changing status, priority, and phase, as well as adding labels and assigning tasks.
- Integrated translations for English, German, Spanish, and Portuguese in the task table bulk actions.
- Updated the `OptimizedBulkActionBar` to utilize Redux state for status, priority, and phase selections.
- Improved user experience with dynamic dropdown menus for bulk actions and confirmation prompts.
- Refactored task selection handling to ensure proper state management during bulk actions.
2025-07-02 07:59:34 +05:30
shancds
87f73ee4c2 feat(assignee-selector): enhance real-time updates and UI interactions
- Added socket event handling for quick assignee updates, dispatching changes to the enhanced Kanban state.
- Updated AssigneeSelector component to prevent event propagation on dropdown interactions.
- Integrated AvatarGroup and LazyAssigneeSelectorWrapper in EnhancedKanbanTaskCard for improved assignee display and selection.
2025-07-01 18:21:52 +05:30
Dev-Tanaay
fbbd820512 Updation in Documentation 2025-07-01 17:29:02 +05:30
shancds
0a92d38ccf refactor(task-sort-order): optimize access check and clean up code
- Improved the access check logic by incorporating team member validation in the SQL query, enhancing security and accuracy.
- Removed unnecessary whitespace for cleaner code formatting.
- Updated socket event emission for consistency in response structure.
2025-07-01 16:57:07 +05:30
Chamika J
e4e6d3c74d Merge pull request #213 from Worklenz/fix/task-list-realtime-update
chore(dependencies): update package-lock.json and package.json for de…
2025-07-01 15:22:59 +05:30
chamikaJ
f352d823a8 Merge branch 'main' of https://github.com/Worklenz/worklenz into development 2025-07-01 15:22:41 +05:30
chamikaJ
98a96b4fcc chore(dependencies): update package-lock.json and package.json for dependency management
- Removed `pg-native` from dependencies in both package files.
- Updated several AWS SDK and Smithy packages to their latest versions for improved functionality and security.
- Added new dependencies for emoji-regex and string-width to enhance task management features.
- Adjusted various package versions to ensure compatibility and performance optimizations across the application.
2025-07-01 15:19:21 +05:30
shancds
63483e01c2 Merge branch 'release/v2.0.4' of https://github.com/Worklenz/worklenz into fix/enhanced-board-sub-task-section 2025-07-01 12:42:05 +05:30
Chamika J
b247186a0a Merge pull request #212 from Worklenz/fix/task-list-realtime-update
Fix/task list realtime update
2025-07-01 12:31:36 +05:30
shancds
4304ebf7b1 refactor(task-socket-handlers): remove unnecessary console logs for cleaner code
- Eliminated various console log statements from the task socket handlers to enhance code clarity and performance.
- Maintained functionality while reducing noise in the console output during task status, priority, and phase changes.
2025-07-01 12:24:32 +05:30
Chamika J
4d229c79d5 Merge pull request #211 from shancds/fix/enhanced-board-sub-task-section
Fix/enhanced board sub task section
2025-07-01 12:11:18 +05:30
Chamika J
6e995e7fc2 Merge pull request #209 from shancds/fix/task-manual-progress-update
Fix/task manual progress update
2025-07-01 12:10:59 +05:30
Chamika J
eec100dfe8 Merge pull request #207 from shancds/fix/kanban-board-enhanced-feat
feat(enhanced-kanban): enhance drag-and-drop overlays with theme-awar…
2025-07-01 12:10:01 +05:30
shancds
10d64c88e3 feat(enhanced-kanban): implement real-time updates and task expansion handling
- Integrated socket event handlers for real-time updates in the enhanced Kanban board, improving task management responsiveness.
- Added functionality to toggle task expansion for subtasks, enhancing user interaction with task details.
- Updated state management to handle subtasks more effectively, including loading states and counts.
- Refactored subtask fetching logic to utilize a dedicated API endpoint, streamlining data retrieval.
2025-07-01 12:04:30 +05:30
shancds
165a87ce69 feat(enhanced-kanban): add start and end date updates for tasks in task drawer
- Implemented actions to update task start and end dates within the enhanced Kanban feature, ensuring real-time synchronization when the task drawer is used.
- Enhanced task drawer components to dispatch updates to the enhanced Kanban state based on the current tab, specifically when on the board tab.
- Improved state management for task date properties, allowing for better handling of task updates across different views.
2025-07-01 11:02:04 +05:30
shancds
e5ff036d81 feat(task-status): enhance task status change handling to reset manual progress
- Added logic to reset manual_progress to FALSE when a task transitions from "done" to "todo" or "doing", allowing for accurate progress recalculation based on subtasks.
- Improved logging for task status changes to provide better insights into task management actions.
- Ensured parent task progress updates are triggered appropriately for subtasks during status changes.
2025-07-01 10:18:56 +05:30
chamiakJ
326f283d4e feat(task-management): introduce optimized bulk action bar component
- Added a new `OptimizedBulkActionBar` component for enhanced task management.
- Implemented performance optimizations, including memoization and smooth animations.
- Integrated bulk action handlers for status, priority, phase changes, and more.
- Updated `TaskListBoard` to utilize the new bulk action bar, improving user experience for task selection and actions.
- Included responsive design adjustments and accessibility features.
2025-07-01 10:11:39 +05:30
shancds
c048085c8a feat(enhanced-kanban): enhance task progress and name updates in task drawer
- Integrated updates for task progress and name within the enhanced Kanban feature, allowing for real-time synchronization based on the selected tab (tasks-list or board).
- Added new actions to handle task progress updates specifically for the enhanced Kanban view.
- Updated task drawer components to dispatch the appropriate actions based on the current tab, improving user experience and state management.
2025-07-01 09:58:49 +05:30
shancds
8fcd4d0d53 feat(enhanced-kanban): integrate task assignee, label, and priority updates
- Added actions to update task assignees, labels, and priority within the enhanced Kanban feature, enhancing task management capabilities.
- Updated task drawer components to utilize new actions for real-time updates based on user interactions.
- Improved state management for better handling of task properties across different views.
2025-07-01 09:42:47 +05:30
chamiakJ
30bdaf1ed5 chore(dependencies): update Tailwind CSS and related configurations
- Removed @tailwindcss/postcss from devDependencies and updated tailwindcss to version 3.4.15 for improved styling.
- Added autoprefixer as a new dependency to enhance CSS compatibility.
- Updated postcss.config.js to reflect the new Tailwind CSS plugin structure.
- Created a new tailwind.config.js file to define Tailwind's configuration.
- Adjusted CSS imports in index.css to utilize Tailwind's utility classes.
- Refined focus styles across various components for better accessibility and visual consistency.
2025-07-01 09:23:10 +05:30
jiuhao47
39e09bedd3 add support for zh_cn 2025-06-30 20:52:49 +08:00
shancds
487fb76776 feat(enhanced-kanban): enhance drag-and-drop overlays with theme-aware styling
- Added theme mode support to the drag-and-drop overlays for tasks and groups, improving visual consistency in dark and light modes.
- Updated the display of active tasks and groups during drag operations to enhance user experience and clarity.
2025-06-30 17:09:17 +05:30
chamikaJ
41e563297a Merge branch 'release/v2.0.4' of https://github.com/Worklenz/worklenz into fix/task-list-realtime-update 2025-06-30 16:30:51 +05:30
Chamika J
9743adaed5 Merge pull request #206 from shancds/fix/kanban-board-enhanced-feat
refactor(enhanced-kanban): remove unnecessary div wrapper in Virtuali…
2025-06-30 15:25:30 +05:30
shancds
b179a0274f refactor(enhanced-kanban): remove unnecessary div wrapper in VirtualizedTaskList component
- Eliminated the redundant div element surrounding the EnhancedKanbanTaskCard to streamline the component structure and improve rendering efficiency.
2025-06-30 15:22:34 +05:30
chamikaJ
61574c847f chore(dependencies): update Tailwind CSS and related packages for improved styling
- Added @tailwindcss/postcss as a new dependency.
- Updated tailwindcss to version 4.1.11 for enhanced features and performance.
- Upgraded prettier-plugin-tailwindcss to version 0.6.13 for better formatting support.
- Adjusted postcss.config.js to use the new Tailwind CSS plugin structure.
- Removed the obsolete tailwind.config.js file as it is no longer needed.
2025-06-30 15:11:30 +05:30
Chamika J
2eee15be03 Merge pull request #205 from shancds/fix/kanban-board-enhanced-feat
feat(enhanced-kanban): add task assignees and labels fetching with im…
2025-06-30 14:58:24 +05:30
Chamika J
0ae615cc77 Merge branch 'release/v2.0.4' into fix/kanban-board-enhanced-feat 2025-06-30 14:58:15 +05:30
shancds
7f46b10a42 feat(enhanced-kanban): add updateEnhancedKanbanTaskStatus action for task status management
- Introduced a new action to update task status within the enhanced Kanban feature, allowing for dynamic task movement between groups based on status changes.
- Updated the task drawer status dropdown to utilize the new action for improved task management experience.
2025-06-30 14:53:00 +05:30
shancds
dee385c6db refactor(enhanced-kanban): remove console log from handleSubTaskExpand function
- Removed debugging console log from the handleSubTaskExpand function to clean up the code and improve performance.
2025-06-30 13:42:35 +05:30
Chamika J
207e038315 Merge pull request #204 from Worklenz/fix/task-list-realtime-update
Fix/task list realtime update
2025-06-30 12:49:17 +05:30
shancds
dc3433a036 feat(enhanced-kanban): add task assignees and labels fetching with improved filter management
- Implemented async thunks to fetch task assignees and labels for enhanced Kanban board.
- Updated state management to store original and current data for task assignees and labels.
- Enhanced filter selection actions to update both original and current data seamlessly.
- Integrated filter data loader for improved user experience in task management.
2025-06-30 12:25:22 +05:30
chamikaJ
14c5c148b9 refactor(task-management): optimize task management components with performance enhancements
- Updated import statements for consistency and clarity.
- Refined task sorting and update logic to improve responsiveness.
- Enhanced error logging for better debugging during task sort order changes.
- Increased overscan count in virtualized task lists for smoother scrolling experience.
- Introduced lazy loading for heavy components to reduce initial load times.
- Improved CSS styles for better responsiveness and user interaction across task management components.
2025-06-30 11:02:41 +05:30
chamiakJ
7fdea2a285 feat(performance): implement extensive performance optimizations across task management components
- Introduced batching and optimized query handling in SQL functions for improved performance during large updates.
- Enhanced task sorting functions with batching to reduce load times and improve responsiveness.
- Implemented performance monitoring utilities to track render times, memory usage, and long tasks, providing insights for further optimizations.
- Added performance analysis component to visualize metrics and identify bottlenecks in task management.
- Optimized drag-and-drop functionality with CSS enhancements to ensure smooth interactions and reduce layout thrashing.
- Refined task row rendering logic to minimize DOM updates and improve loading behavior for large lists.
- Introduced aggressive virtualization and memoization strategies to enhance rendering performance in task lists.
2025-06-30 07:48:32 +05:30
chamikaJ
e3324f0707 feat(task-management): enhance priority and status dropdowns with fallback rendering
- Added helper functions to display names and colors for raw priority and status values, improving user experience.
- Implemented fallback rendering for dropdowns to handle cases where the priority or status is not found in the list.
- Updated task row to display formatted priority and status values, ensuring consistency across the UI.
- Enhanced error handling in task list rendering to provide meaningful feedback when data is unavailable.
2025-06-27 17:39:47 +05:30
Chamika J
0336715103 Merge pull request #200 from Worklenz/fix/task-list-realtime-update
feat(assignee-selector): implement optimistic updates for assignee ma…
2025-06-27 15:58:54 +05:30
chamikaJ
c37ffd6991 feat(assignee-selector): implement optimistic updates for assignee management
- Added optimistic UI updates for assignee selection, improving user experience with immediate feedback.
- Introduced state management for pending changes to visually indicate ongoing updates.
- Enhanced member toggle functionality to reflect changes instantly in the UI while maintaining socket communication for backend updates.
- Improved checkbox behavior to prevent interaction during pending state, ensuring clarity in user actions.
2025-06-27 15:58:19 +05:30
Chamika J
5a07bcce77 Merge pull request #199 from Worklenz/fix/task-list-realtime-update
feat(task-management): improve hover state management and performance…
2025-06-27 15:28:26 +05:30
chamikaJ
ceb962a92a feat(task-management): improve hover state management and performance in task rows
- Enhanced CSS styles to ensure proper hover state resets and visibility of task action buttons.
- Implemented optimizations for hover effects to prevent flickering and improve user interaction.
- Adjusted containment properties to enhance rendering performance during hover states.
- Refined transition effects for smoother visibility changes of task-related elements.
2025-06-27 15:27:33 +05:30
Chamika J
4af204daec Merge pull request #198 from Worklenz/fix/task-list-realtime-update
feat(task-management): enhance real-time updates and performance opti…
2025-06-27 15:18:13 +05:30
chamikaJ
30edda1762 feat(task-management): enhance real-time updates and performance optimizations
- Implemented CSS styles to prevent flickering during socket updates, ensuring stable content visibility.
- Modified `TaskRow` component to improve loading behavior and prevent blank content during real-time updates.
- Enhanced socket handlers to update task management state immediately upon receiving real-time data, reducing the need for unnecessary refetches.
- Introduced logic to track loading state, ensuring consistent rendering and improved user experience during task updates.
2025-06-27 15:17:29 +05:30
Chamika J
5bd06a12dd Merge pull request #197 from Worklenz/fix/task-list-realtime-update
feat(task-management): enhance task row functionality and URL synchro…
2025-06-27 13:57:41 +05:30
chamikaJ
8b63c1cf9e feat(task-management): enhance task row functionality and URL synchronization
- Integrated Redux for managing task drawer state, allowing for task selection and data fetching when opening the task drawer.
- Improved URL synchronization logic to handle task ID updates more effectively, ensuring proper state management during drawer interactions.
- Updated task indicators to use type-safe access for subtasks, comments, and attachments counts, enhancing code reliability and readability.
- Refactored URL clearing logic to prevent unnecessary updates when closing the task drawer, improving user experience.
2025-06-27 13:56:46 +05:30
Chamika J
1e6b1b7d96 Merge pull request #196 from Worklenz/fix/task-list-realtime-update
feat(localization): update project view header translations and enhan…
2025-06-27 13:24:43 +05:30
chamikaJ
e74668c389 feat(localization): update project view header translations and enhance UI functionality
- Added new translations for "Import task", "Refresh project", "Save as template", and "Invite" in Albanian, German, English, Spanish, and Portuguese.
- Refactored `ProjectViewHeader` component to optimize rendering with memoization and improved state management.
- Enhanced task creation and subscription handling with better performance and error management.
- Improved dropdown and button actions for a more intuitive user experience.
2025-06-27 13:24:01 +05:30
Chamika J
cf52140bca Merge pull request #195 from Worklenz/fix/task-list-realtime-update
Fix/task list realtime update
2025-06-27 13:13:04 +05:30
chamikaJ
7e44d53bb3 feat(performance): implement various performance optimizations across components
- Added a new `usePerformanceOptimization` hook for tracking render performance, debouncing, throttling, and optimized selectors to reduce unnecessary re-renders.
- Enhanced `ProjectGroupList` and `ProjectList` components with preloading of project view and task management components on hover for smoother navigation.
- Updated `TaskListBoard` to import `ImprovedTaskFilters` synchronously, avoiding suspense issues.
- Introduced a `resetTaskDrawer` action in the task drawer slice for better state management.
- Improved layout and positioning in `SuspenseFallback` for better user experience during loading states.
- Documented performance optimizations in `PERFORMANCE_OPTIMIZATIONS.md` outlining key improvements and metrics.
2025-06-27 13:12:47 +05:30
chamikaJ
fdb485614f feat(task-management): enhance task management UI with subtask functionality
- Added localization support for task management messages in multiple languages, including Albanian, German, English, Spanish, and Portuguese.
- Implemented subtask addition feature in the `TaskRow` component, allowing users to create and manage subtasks directly within the task interface.
- Introduced hover effects and improved styling for task rows to enhance user experience and interaction.
- Updated task indicators to display counts for subtasks, comments, and attachments, providing better visibility of task details.
2025-06-27 10:59:21 +05:30
Chamika J
6b35ffe930 Merge pull request #194 from Worklenz/fix/task-list-realtime-update
Fix/task list realtime update
2025-06-27 08:50:02 +05:30
chamiakJ
9a254105fb feat(task-management): add task phase and priority dropdown components
- Introduced `TaskPhaseDropdown` and `TaskPriorityDropdown` components for managing task phases and priorities within the task management interface.
- Integrated these components into the `TaskRow` to enhance user interaction and streamline task updates.
- Updated socket handlers to handle phase and priority changes, ensuring real-time updates and improved task organization.
- Enhanced dropdown functionality with animations and improved accessibility features.
2025-06-27 07:28:47 +05:30
chamiakJ
e73196a249 feat(task-management): implement task movement between groups
- Added `moveTaskBetweenGroups` action to facilitate moving tasks across different groups while maintaining state integrity.
- Enhanced task management slice to support task updates during group transitions, including logging for better debugging.
- Updated socket handlers to utilize the new action for moving tasks based on status, priority, and phase changes, improving task organization and user experience.
2025-06-27 07:06:14 +05:30
chamiakJ
84f77940fd feat(task-management): add functionality to assign tasks to specific groups
- Introduced `addTaskToGroup` action to allow tasks to be added to designated groups based on group IDs.
- Enhanced task management slice to support group assignment for better organization and compatibility with V3 API.
- Updated socket handlers to dispatch `addTaskToGroup` with appropriate group IDs extracted from backend responses.
2025-06-27 07:06:02 +05:30
chamiakJ
3d1cb29a67 feat(tasks): optimize task retrieval and performance metrics logging
- Updated `getList` and `getTasksOnly` methods to skip expensive progress calculations by default, enhancing performance.
- Introduced logging for performance metrics, including method execution times and warnings for deprecated methods.
- Added new `getTaskProgressStatus` endpoint to provide basic progress stats without heavy calculations.
- Implemented performance optimizations in the frontend, including lazy loading and improved rendering for task rows.
- Enhanced task management slice with reset actions for better state management.
- Added localization support for task management messages in multiple languages.
2025-06-26 12:26:50 +05:30
Chamika J
345b8500cd Merge pull request #188 from shancds/fix/get-pull-6-26
Fix/get pull 6 26
2025-06-26 12:26:28 +05:30
shancds
3672d02d6f Merge branch 'fix/enhanced-task-board' into fix/get-pull-6-26 2025-06-26 12:18:29 +05:30
shancds
efbfe77deb feat(enhanced-kanban): integrate status categories fetching and improve task card behavior
- Added fetching of status categories in EnhancedKanbanBoard to ensure data availability.
- Enhanced task creation flow in EnhancedKanbanCreateTaskCard by resetting input state and managing focus more effectively.
- Improved cleanup in useEffect hooks to prevent memory leaks and ensure proper resource management.
- Streamlined imports and updated logic in DeleteStatusDrawer for better task group fetching and error handling.
2025-06-26 12:02:21 +05:30
Chamika J
09cf5d8990 Merge pull request #186 from shancds/refact/board-task-card-performance
feat(task-filters): enhance ImprovedTaskFilters for Kanban integration
2025-06-26 09:50:44 +05:30
Chamika J
1e15630708 Merge branch 'release/v2.0.4' into refact/board-task-card-performance 2025-06-26 09:50:30 +05:30
chamiakJ
8c02ad9291 feat(task-filters): enhance performance and debounce functionality in task filters
- Introduced performance constants to limit filter options and improve UI responsiveness.
- Implemented an enhanced debounced function with cancellation support for filter and search changes, reducing unnecessary API calls.
- Optimized filter data retrieval and state updates using memoization to prevent redundant calculations.
- Improved the clear all filters functionality to batch state updates and prevent multiple re-renders, enhancing user experience.
- Updated the handling of search input to immediately clear and dispatch actions, ensuring efficient task fetching.
2025-06-26 00:07:19 +05:30
chamiakJ
4c34a01729 refactor(task-filters): update priority handling in task filters
- Replaced the use of selectedPriorities from taskManagement slice with priorities from taskReducer for consistency across the application.
- Updated dispatch calls in ImprovedTaskFilters to utilize the new setPriorities action, enhancing clarity and maintainability.
- Removed unnecessary imports and streamlined the selector logic for improved performance and readability.
2025-06-25 23:51:59 +05:30
chamiakJ
19cd0e577c feat(task-filters): implement comprehensive filter clearing functionality
- Added logic to clear label, assignee, and priority filters in ImprovedTaskFilters, enhancing user experience by allowing users to reset all filters at once.
- Updated the dependency array in the useEffect hook to include currentTaskLabels and currentTaskAssignees, ensuring proper updates on filter changes.
- Modified task management slice to change the delimiter for selected labels, assignees, and priorities from commas to spaces for improved readability.
2025-06-25 23:46:38 +05:30
chamiakJ
e096bc66ab feat(task-filters): implement search functionality for task filtering
- Added search state management to ImprovedTaskFilters, allowing users to filter tasks based on search input.
- Integrated search actions for both list and board views, ensuring appropriate task fetching based on the current view.
- Updated task management slice to include a search field, enhancing the overall task filtering capabilities.
2025-06-25 23:40:41 +05:30
chamiakJ
f22caea1e5 refactor(task-filters, project-view-header): streamline imports and optimize task fetching
- Removed unused imports and optimized the import structure in ImprovedTaskFilters for better readability and performance.
- Updated ProjectViewHeader to include a call to fetchTasksV3, ensuring enhanced task data is refreshed appropriately.
- Adjusted task creation request type to Partial for improved type safety.
2025-06-25 23:33:32 +05:30
shancds
208d1ad5d4 feat(task-filters): enhance ImprovedTaskFilters for Kanban integration
- Updated useFilterData to support both board and list views, utilizing enhanced Kanban state for filtering.
- Integrated new selectors for Kanban-specific filters including priorities, labels, and assignees.
- Refactored handleSelectionChange and handleSearchChange to accommodate Kanban logic, ensuring proper dispatch of actions based on the selected view.
- Improved overall filter functionality and user experience in task management.
2025-06-25 17:10:38 +05:30
chamikaJ
44527f68cf feat(assignee-selector, suspense-fallback, project-view): optimize component loading and enhance user experience
- Integrated synchronous imports for TaskListFilters and filter dropdowns to improve performance and reduce loading times.
- Refactored AssigneeSelector to include a new invite member drawer functionality, enhancing user interaction.
- Simplified SuspenseFallback components for better loading experiences, ensuring they do not block the main UI.
- Updated project view constants to utilize InlineSuspenseFallback for lazy-loaded components, improving rendering efficiency.
2025-06-25 17:08:40 +05:30
Chamika J
3c7cacc46f Merge pull request #185 from shancds/refact/board-task-card-performance
Refact/board task card performance
2025-06-25 16:25:32 +05:30
chamikaJ
bbd602a297 feat(assignee-selector, labels-selector): improve dropdown visibility handling
- Enhanced dropdown behavior by checking button visibility in the viewport before updating position.
- Added logic to hide the dropdown if the button is not visible, improving user experience during scrolling.
2025-06-25 15:43:24 +05:30
chamikaJ
df2a40b861 feat(assignee-selector): enhance dropdown functionality and position handling
- Added button reference and dropdown position state to improve dropdown positioning.
- Implemented useCallback for updating dropdown position on scroll and resize events.
- Enhanced click outside handling to close the dropdown correctly.
- Utilized createPortal for rendering the dropdown, ensuring it overlays correctly in the DOM.
- Improved styling and behavior of the dropdown button based on its open state.
2025-06-25 15:40:20 +05:30
shancds
e29e5ed0a4 feat(enhanced-kanban): integrate ImprovedTaskFilters and fetchBoardSubTasks for enhanced task management
- Replaced the existing TaskListFilters with ImprovedTaskFilters in EnhancedKanbanBoard for better filtering capabilities.
- Updated EnhancedKanbanTaskCard to handle subtask expansion and fetching using the new fetchBoardSubTasks action.
- Added sectionId prop to EnhancedKanbanTaskCard and EnhancedKanbanGroup for improved task organization.
- Refactored project-view-header to utilize fetchEnhancedKanbanGroups for loading task groups.
2025-06-25 15:24:44 +05:30
chamikaJ
734b5f807b feat(task-management): enhance task components with performance optimizations and new status field
- Rearranged the order of task fields in the dropdown for better usability.
- Introduced a new CSS file for task row optimizations, improving rendering performance and responsiveness.
- Added utility functions for date formatting and performance monitoring to enhance task management efficiency.
- Updated TaskRow and TaskStatusDropdown components to improve rendering and user experience, including better handling of status display and dark mode support.
- Integrated new status field into various task management components, ensuring consistent visibility and functionality across the application.
2025-06-25 15:22:38 +05:30
chamikaJ
85cce6e707 refactor(vite.config.ts): enhance build configuration and chunking strategy
- Updated Vite configuration to ensure a single React instance and improve Ant Design integration by refining the deduplication process.
- Enhanced build settings, including target specification and sourcemap handling, to optimize performance.
- Streamlined manual chunking strategy for better React context management and improved file naming conventions for assets.
- Adjusted CSS optimization settings and introduced experimental features for improved build performance.
2025-06-25 13:24:41 +05:30
chamikaJ
a4da6cdf3a refactor(vite.config.ts): simplify chunking strategy and enhance Ant Design integration
- Streamlined manual chunking strategy in Vite config to improve React context sharing and Ant Design component loading.
- Increased chunk size warning limit to accommodate larger Ant Design chunks.
- Updated Ant Design imports in `antd-imports.ts` for better tree-shaking and consistent React context availability across components.
2025-06-25 13:16:58 +05:30
chamikaJ
f837ca6b23 feat(configuration): enhance Vite config for React and Ant Design integration
- Updated Vite configuration to ensure a single React instance and prevent context issues by adding 'react/jsx-runtime' to deduplication.
- Improved manual chunking strategy to keep React core together and optimize Ant Design component loading.
- Adjusted build settings for better source map handling and chunk size warnings.
- Centralized Ant Design imports in `antd-imports.ts` to ensure React is available for all components.
- Removed exclusions for Ant Design to enhance compatibility and performance.
2025-06-25 13:10:19 +05:30
chamikaJ
7b326e8ff0 feat(project-view): optimize component loading and enhance configuration
- Introduced lazy loading for project view components and chart components to reduce initial bundle size.
- Centralized Ant Design imports in a new `antd-imports.ts` file for better tree-shaking and maintainability.
- Updated project view header and task list components to utilize centralized imports, improving consistency and performance.
- Enhanced project view constants to streamline component rendering and improve user experience.
2025-06-25 13:05:38 +05:30
chamikaJ
680e84d19b feat(task-management): centralize Ant Design imports and enhance task components
- Introduced a new `antd-imports.ts` file to centralize Ant Design component imports, optimizing tree-shaking and improving maintainability.
- Updated various task management components (e.g., TaskRow, TaskGroup, VirtualizedTaskList) to utilize centralized imports, ensuring consistent styling and configuration.
- Enhanced task filtering and display features by adding new fields (e.g., start date, due date, estimation) to task components for improved usability.
- Refactored date handling in task components to utilize memoization for performance optimization.
- Improved overall styling and responsiveness of task management components, particularly in dark mode.
2025-06-25 12:31:39 +05:30
chamikaJ
cf5919a3a0 feat(task-management): enhance Checkbox component and task selection functionality
- Added `indeterminate` state to Checkbox component for better visual representation of partial selections.
- Updated TaskGroup and VirtualizedTaskList components to utilize the new Checkbox features, allowing for group selection with indeterminate states.
- Implemented custom debounce function for saving task fields to localStorage, improving performance during user interactions.
- Enhanced task row styling for better visibility and user experience, particularly in dark mode.
2025-06-25 10:48:01 +05:30
shancds
9ce6cd63d1 refactor(enhanced-kanban): remove inline style from VirtualizedTaskList for cleaner markup 2025-06-25 09:53:41 +05:30
shancds
6f5e5f5c30 style(enhanced-kanban): comment out unused styles in EnhancedKanbanGroup for cleaner code 2025-06-25 09:08:06 +05:30
chamiakJ
a25fcf209a feat(task-management): implement customizable task list fields and configuration modal
- Added a new slice for managing task list fields, allowing users to toggle visibility and order of fields in the task list.
- Introduced a ColumnConfigurationModal for users to configure which fields appear in the dropdown and their order.
- Updated ShowFieldsFilterDropdown to integrate the new configuration modal and manage field visibility.
- Enhanced task management components to utilize the new field visibility settings, improving the overall user experience and customization options.
2025-06-25 07:57:53 +05:30
Chamika J
5d0777f67c Merge pull request #183 from Worklenz/fix/added-i18n-alb-and-de
Update LANGUAGE_TYPE enum to include 'alb' and 'de' for Albanian and …
2025-06-24 22:39:29 +05:30
chamiakJ
f1d504f985 Update LANGUAGE_TYPE enum to include 'alb' and 'de' for Albanian and German languages 2025-06-24 22:28:33 +05:30
chamiakJ
9a070ef5d3 chore: update package-lock.json and refactor index.html for improved readability
- Updated dependencies in package-lock.json, downgrading several @esbuild packages to version 0.17.19 and adjusting node engine requirements to >=12.
- Refactored index.html to enhance readability by consolidating script loading logic and improving formatting.
- Removed unnecessary comments and whitespace for cleaner code structure.
2025-06-24 22:21:13 +05:30
chamiakJ
3e5bc71535 Merge branch 'main' of https://github.com/Worklenz/worklenz into release/v2.0.4 2025-06-24 21:49:12 +05:30
Chamika J
6a4d77d904 Merge pull request #182 from Worklenz/feature/german-i18n
Feature/german i18n
2025-06-24 21:47:45 +05:30
Chamika J
c35d53266a Merge pull request #179 from OminduHirushka/language/de
German Language
2025-06-24 21:47:06 +05:30
Chamika J
ea79270bff Merge pull request #180 from shancds/refact/board-task-card-performance
Refact/board task card performance
2025-06-24 21:46:15 +05:30
chamiakJ
975e5c4faf Merge branch 'imp/task-list-performance-fixes' of https://github.com/Worklenz/worklenz into release/v2.0.4 2025-06-24 21:45:44 +05:30
chamiakJ
f405777463 feat(task-management): enhance task filtering and UI components for improved usability
- Updated AssigneeSelector and LabelsSelector components to include text color adjustments for better visibility in dark mode.
- Introduced ImprovedTaskFilters component for a more efficient task filtering experience, integrating Redux state management for selected priorities and labels.
- Refactored task management slice to support new filtering capabilities, including selected priorities and improved task fetching logic.
- Enhanced TaskGroup and TaskRow components to accommodate new filtering features and improve overall layout consistency.
2025-06-24 21:40:01 +05:30
shancds
217ec39503 feat(enhanced-kanban): enhance EnhancedKanbanTaskCard with improved task interaction and display
- Refactored EnhancedKanbanTaskCard to include new features such as task labels rendering, progress display, and a custom due date picker.
- Implemented click handling to open a task drawer for detailed task management.
- Enhanced UI with Ant Design components for better visual feedback and user interaction.
2025-06-24 17:08:18 +05:30
shancds
e89f81152e style(enhanced-kanban): remove background color from drag-over state in EnhancedKanbanGroup 2025-06-24 16:09:07 +05:30
Chamika J
a34b9a8fb0 Merge pull request #178 from shancds/imp/board-task-list-performance-fix
Imp/board task list performance fix
2025-06-24 14:51:32 +05:30
Omindu Hirushka
dc096f5e12 german language switch 2025-06-24 14:17:23 +05:30
Omindu Hirushka
a681aadcfa german language 2025-06-24 14:17:08 +05:30
shancds
29618660aa style(enhanced-kanban): enhance styling for EnhancedKanbanCreateTaskCard
- Added minimum height, z-index, box shadow, and margin adjustments to the EnhancedKanbanCreateTaskCard component for improved visual appearance.
- Updated class names to include hover effects, enhancing user interaction feedback.
2025-06-24 12:50:51 +05:30
shancds
d3c4fdef9d fix(enhanced-kanban): correct background color assignment in EnhancedKanbanCreateTaskCard
- Updated the background color logic in the EnhancedKanbanCreateTaskCard component to ensure proper color assignment based on theme mode.
- Refactored import statement for ITaskCreateRequest to use the correct path, improving code clarity and consistency.
2025-06-24 12:37:02 +05:30
shancds
4f7cbf3527 feat(enhanced-kanban): enhance EnhancedKanbanBoard with new task creation options and layout adjustments
- Updated the EnhancedKanbanBoard component to include a new section for creating tasks at both the top and bottom of each group.
- Adjusted the CSS for the kanban groups container to improve layout responsiveness.
- Refactored EnhancedKanbanGroup to manage task creation visibility and interactions more effectively, enhancing user experience during task management.
2025-06-24 12:24:54 +05:30
shancds
ad76563543 feat(enhanced-kanban): implement API call for column order updates during group reordering
- Added functionality to prevent reordering of groups when grouped by phases.
- Implemented API call to update the status order after reordering groups, with error handling to revert changes if the API call fails.
- Enhanced user feedback with alert notifications for failed updates, improving overall user experience during drag-and-drop operations.
2025-06-24 09:55:17 +05:30
Chamika J
4e973f3d51 Merge pull request #177 from shancds/imp/board-task-list-performance-fix
Imp/board task list performance fix
2025-06-24 08:19:33 +05:30
shancds
17bcf8c41f fix(enhanced-kanban): correct sort order handling for task drop positions
- Updated the logic for determining the sort order when tasks are dropped in the EnhancedKanbanBoard component.
- Added handling for dropping tasks at the end of a group and ensured proper assignment of sort orders, improving task organization during drag-and-drop operations.
2025-06-23 17:05:35 +05:30
shancds
a8bf4671fa feat(enhanced-kanban): add socket integration for task and column reordering
- Integrated socket functionality to emit events for task and column sort order changes during drag-and-drop operations in the EnhancedKanbanBoard component.
- Enhanced the handleDragEnd function to prepare and send the updated column order to the backend or via socket, improving real-time synchronization.
- Updated error handling for socket emissions to ensure robustness during reordering processes.
2025-06-23 16:57:23 +05:30
chamikaJ
95d0985f3d feat(task-management): enhance task row and virtualized list components for improved layout and performance
- Added support for customizable columns in `TaskRow` component, allowing for fixed and scrollable columns.
- Implemented synchronized scrolling between header and body in `VirtualizedTaskList` for better user experience.
- Refactored column header rendering to dynamically generate based on column definitions, improving maintainability.
- Enhanced styles for task group headers and column headers to ensure consistent appearance and responsiveness.
2025-06-23 16:49:57 +05:30
chamikaJ
2dd756bbb8 feat(tasks): implement V3 API for task management and enhance UI components
- Introduced `getTasksV3` and `refreshTaskProgress` methods in `TasksControllerV2` to optimize task retrieval and progress refreshing.
- Updated API routes to include new endpoints for V3 task management.
- Enhanced frontend components to utilize the new V3 API, improving performance by reducing frontend processing.
- Added `VirtualizedTaskList` and `VirtualizedTaskGroup` components for efficient rendering of task lists.
- Updated task management slice to support new V3 data structure and improved state management.
- Refactored styles for better dark mode support and overall UI consistency.
2025-06-23 16:34:57 +05:30
shancds
3be97b1da2 feat(enhanced-kanban): enhance EnhancedKanbanGroup with editable section names and status management
- Implemented functionality to edit section names directly within the EnhancedKanbanGroup component, allowing for a more dynamic user experience.
- Added unique name generation for sections to prevent duplicates.
- Integrated status update and deletion capabilities, enabling users to manage task statuses effectively.
- Enhanced UI with new Ant Design components for better interaction and visual feedback during editing and deletion processes.
2025-06-23 16:02:50 +05:30
shancds
b436db183f feat(enhanced-kanban): implement synchronous reordering for tasks and groups
- Added synchronous state updates for task and group reordering in the EnhancedKanbanBoard component, improving UI responsiveness during drag-and-drop operations.
- Introduced new actions `reorderTasks` and `reorderGroups` in the enhanced-kanban slice for better state management.
- Updated EnhancedKanbanGroup and EnhancedKanbanTaskCard components to utilize the new layout change animations, enhancing the user experience during reordering.
2025-06-23 14:08:32 +05:30
shancds
6508dc6c64 fix(enhanced-kanban): remove background color from drop preview in drag-over state
- Eliminated the background color for the drop preview when a kanban group is dragged over, enhancing visual clarity during drag-and-drop operations.
2025-06-23 12:03:46 +05:30
shancds
b3d39b65b0 feat(enhanced-kanban): implement group reordering and improve drag-and-drop functionality
- Added support for reordering kanban groups via drag-and-drop, enhancing user experience.
- Updated EnhancedKanbanBoard and EnhancedKanbanGroup components to handle group dragging and state management.
- Introduced visual feedback for dragging groups and tasks, improving usability.
- Refined CSS styles for better layout and responsiveness during drag operations.
2025-06-23 11:37:40 +05:30
shancds
67c26a973e refactor(enhanced-kanban): improve code readability and integrate TaskListFilters component
- Refactored EnhancedKanbanBoard and EnhancedKanbanGroup components for better code organization and readability.
- Integrated TaskListFilters component to enhance task filtering capabilities within the kanban board.
- Cleaned up unnecessary whitespace and improved formatting for consistency across the codebase.
2025-06-23 10:13:47 +05:30
chamiakJ
687fff9c74 feat(task-management): optimize task components for performance and usability
- Refactored TaskGroup and TaskRow components to improve rendering efficiency by utilizing memoization and callbacks.
- Moved color mappings for group statuses and priorities outside of components to prevent unnecessary re-creations.
- Enhanced drag-and-drop functionality with optimistic updates and throttling for smoother user experience.
- Updated task management slice to support new properties and batch updates for better performance.
- Simplified selectors and improved error handling in the task management slice.
2025-06-23 07:29:50 +05:30
Chamika J
f15f3f5110 Merge pull request #176 from Worklenz/feature/albanian-i18n
Feature/albanian i18n
2025-06-23 07:24:12 +05:30
Chamika J
07ae71fd23 Merge pull request #168 from OminduHirushka/upstream/language/alb
albanian language
2025-06-23 07:23:42 +05:30
Chamika J
9c7fad790f Merge pull request #151 from kithmina1999/feat/docker-db-backup
Feat/Add auto backup and manual backup functionality  to docker-compose.yaml
2025-06-23 06:57:24 +05:30
Chamika J
26270b2842 Merge pull request #166 from kithmina1999/chore/docker-image-size-reduction
Chore/docker image size reduction
2025-06-23 06:56:14 +05:30
chamiakJ
05729285af feat(components): introduce new UI components and enhance Vite configuration
- Added AssigneeSelector, Avatar, AvatarGroup, Button, Checkbox, CustomColordLabel, CustomNumberLabel, LabelsSelector, Progress, Tag, and Tooltip components for improved UI functionality.
- Updated Vite configuration to change the development server port to 5173 and removed unnecessary interop settings for module compatibility.
- Enhanced task management components to utilize new task structure and improve performance.
2025-06-22 14:16:39 +05:30
Chamika J
d713ed5900 Merge pull request #175 from Worklenz/imp/task-list-performance-fixes
feat(config): refine Vite configuration for improved chunking and mod…
2025-06-21 19:25:28 +05:30
chamiakJ
cfbb4534d8 feat(config): refine Vite configuration for improved chunking and module interop
- Added deduplication for React and React DOM to ensure a single instance.
- Simplified the chunking strategy to group React-related libraries together, enhancing dependency management.
- Introduced interop settings for better module compatibility.
2025-06-21 19:24:41 +05:30
Chamika J
67cff68581 Merge pull request #174 from Worklenz/imp/task-list-performance-fixes
fix(index.html): change script type for env-config.js to improve comp…
2025-06-21 19:21:02 +05:30
chamiakJ
b63df394cc fix(index.html): change script type for env-config.js to improve compatibility
feat(env-config): add env-config.js for development environment setup

- Updated index.html to use a standard script tag for env-config.js.
- Introduced env-config.js as a development placeholder for environment variables, allowing fallback to build-time env vars.
2025-06-21 19:20:36 +05:30
Chamika J
2a96e61a97 Merge pull request #173 from Worklenz/imp/task-list-performance-fixes
feat(lazy-loading): implement lazy loading and suspense for improved …
2025-06-21 19:17:54 +05:30
Chamika J
be26d241c0 Merge branch 'release/v2.0.4' into imp/task-list-performance-fixes 2025-06-21 19:16:00 +05:30
chamiakJ
2670eb2925 feat(lazy-loading): implement lazy loading and suspense for improved performance
- Added lazy loading for NotFoundPage and TaskListFilters components to enhance initial load times.
- Wrapped lazy-loaded components in Suspense boundaries to provide loading states and improve user experience.
- Updated Vite configuration to optimize chunking strategy and preserve module signatures for better dependency management.
2025-06-21 19:15:13 +05:30
Chamika J
75c8e678bf Merge pull request #172 from Worklenz/imp/task-list-performance-fixes
refactor(config): switch configuration files to CommonJS module syntax
2025-06-21 19:10:05 +05:30
chamiakJ
ddb3e2bc17 refactor(config): switch configuration files to CommonJS module syntax
- Updated jest.config.js, postcss.config.js, and tailwind.config.js to use CommonJS module.exports syntax for compatibility with Node.js environments.
2025-06-21 19:09:30 +05:30
Chamika J
613d7aba71 Merge pull request #171 from Worklenz/imp/task-list-performance-fixes
refactor(config): migrate configuration files to ES module syntax
2025-06-21 19:06:49 +05:30
chamiakJ
7a7eeefe3b refactor(config): migrate configuration files to ES module syntax
- Updated jest.config.js, postcss.config.js, and tailwind.config.js to use ES module export syntax.
- Removed unused tsconfigPaths import from vite.config.ts to streamline the configuration.
2025-06-21 19:05:48 +05:30
Chamika J
1c306c571b Merge pull request #170 from Worklenz/imp/task-list-performance-fixes
feat(config): enhance Vite configuration for improved build performan…
2025-06-21 19:03:21 +05:30
chamiakJ
fb56a12297 feat(config): enhance Vite configuration for improved build performance and development experience
- Updated Vite configuration to include production checks for sourcemaps and minification.
- Added development server settings for automatic opening and HMR overlay customization.
- Optimized chunking strategy for better caching and organization of dependencies.
- Enhanced asset file naming strategies for better categorization of images and fonts.
- Introduced experimental features for performance improvements and CSS optimization.
- Adjusted task-row component styling for improved font size consistency.
2025-06-21 19:02:35 +05:30
Chamika J
26171fd846 Merge pull request #169 from Worklenz/imp/task-list-performance-fixes
Imp/task list performance fixes
2025-06-21 18:53:55 +05:30
chamiakJ
5a475a84b5 feat(performance): add Redux performance monitoring and memoized selectors
- Introduced a Redux performance monitoring system to log action metrics, including duration and state size.
- Implemented middleware for tracking performance of Redux actions and logging slow actions in development.
- Added utility functions for analyzing performance metrics and generating recommendations for optimization.
- Created memoized selectors to enhance performance and prevent unnecessary re-renders across various application states.
2025-06-21 18:40:57 +05:30
chamiakJ
b617d15c62 feat(performance): enhance routing and component loading efficiency
- Implemented lazy loading for all route components to improve initial load times and reduce bundle size.
- Added Suspense boundaries around lazy-loaded components for better loading states and user experience.
- Memoized guard components to prevent unnecessary re-renders and optimize performance.
- Introduced defensive programming practices in guard components to handle potential errors gracefully.
- Updated routing structure to utilize React Router's future features for enhanced performance.
2025-06-21 18:32:41 +05:30
shancds
f7ba4f202b feat(enhanced-kanban): integrate react-window-infinite-loader and update project view
- Added react-window-infinite-loader to improve performance in rendering large lists.
- Integrated enhancedKanbanReducer into the Redux store for state management.
- Updated ProjectViewEnhancedBoard to utilize EnhancedKanbanBoard for better project visualization.
2025-06-21 18:24:09 +05:30
chamiakJ
bb57280c8c feat(performance): implement comprehensive performance improvements for Worklenz frontend
- Introduced a new document summarizing performance optimizations across the application.
- Applied React.memo(), useMemo(), and useCallback() to key components to minimize unnecessary re-renders and optimize rendering performance.
- Implemented a route preloading system to enhance navigation speed and user experience.
- Added performance monitoring utilities for development to track component render times and function execution.
- Enhanced lazy loading and suspense boundaries for better loading states.
- Conducted production optimizations, including TypeScript error fixes and memory management improvements.
- Memoized style and configuration objects to reduce garbage collection pressure and improve memory usage.
2025-06-21 18:16:13 +05:30
shancds
bbca644b40 feat(project-view-constants): add enhanced board view to project tabs
- Introduced ProjectViewEnhancedBoard to the project view constants.
- Added a new tab item for the enhanced board view, improving project management options.
- Updated tab items structure to include the new board variant for better user navigation.
2025-06-20 17:13:37 +05:30
chamikaJ
5221061241 feat(task-management): implement new task management features with BulkActionBar and task grouping
- Introduced BulkActionBar component for bulk actions on selected tasks, including status, priority, and assignee changes.
- Added TaskGroup and TaskRow components to enhance task organization and display.
- Implemented grouping functionality with GroupingSelector for improved task categorization.
- Enhanced drag-and-drop capabilities for task reordering within groups.
- Updated styling and responsiveness across task management components for better user experience.
2025-06-20 10:56:48 +05:30
kithmina1999
0d0596b767 ci(docker): update postgres healthcheck and retry settings
- Modify healthcheck command to use DB_NAME and DB_USER environment variables
- Reduce retries from 10 to 5 to fail faster if database is unavailable
2025-06-20 08:48:51 +05:30
Chamika J
dfb360733e Merge pull request #165 from Worklenz/imp/task-list-performance-fixes
Imp/task list performance fixes
2025-06-20 08:40:14 +05:30
chamiakJ
c1e6689beb refactor(task-management): improve layout and styling for TaskGroup and TaskRow components
- Updated TaskGroup and TaskRow to enhance column widths and visibility.
- Adjusted styling for task headers and buttons for better user experience.
- Improved overflow handling and responsiveness in task management components.
- Streamlined CSS for consistency across task-related components.
2025-06-20 07:33:14 +05:30
chamiakJ
4e1c6fb333 refactor(task-management): enhance TaskGroup and TaskRow components with column visibility and improved layout
- Integrated Redux for column visibility management in TaskGroup and TaskRow components.
- Simplified the rendering of task details based on column visibility settings.
- Updated styling for better consistency and responsiveness across task rows and groups.
- Removed unused imports and components to streamline the codebase.
2025-06-20 06:44:47 +05:30
kithmina1999
eca7af2d6f chore: remove obsolete git file with process information
The file contained outdated process information that was no longer relevant to the project. This cleanup helps maintain a cleaner codebase.
2025-06-19 22:32:17 +05:30
kithmina1999
3ace14fcdb build(docker): optimize docker setup and compose configuration
- Split Dockerfile into multi-stage build to reduce final image size
- Update docker-compose.yml with restart policies and health checks
- Improve .dockerignore files for both frontend and backend
- Fix MinIO bucket creation script to use 'alias set' instead of deprecated command
- Enhance PostgreSQL healthcheck configuration
2025-06-19 22:26:20 +05:30
Chamika J
69cd40dc95 Merge pull request #163 from shancds/fix/customer-feedback
refactor(project-view-updates): optimize comment handling and renderi…
2025-06-19 13:47:55 +05:30
shancds
ece614941e feat(task-comments): enhance comment processing with linkification and sanitization
- Added a utility to linkify URLs in comments, allowing users to click and open links.
- Refactored comment content processing to handle both mentions and links, improving user interaction.
- Updated the sanitizeHtml function to allow <span> tags for mentions, enhancing content display.
2025-06-19 13:23:40 +05:30
shancds
b47b3253f6 feat(project-view-updates): add linkification and context menu for comments
- Implemented linkification for URLs in comments, allowing users to click and open links in a new tab.
- Introduced a context menu for each comment with an option to delete, enhancing user interaction.
- Refactored comment rendering to include link handling and improved code organization.
2025-06-19 12:39:32 +05:30
shancds
889335c579 refactor(info-tab-footer): remove console log for member selection 2025-06-19 11:27:18 +05:30
shancds
7b657120e9 refactor(project-view-updates): optimize comment handling and rendering logic
- Introduced useMemo and useCallback hooks to enhance performance and prevent unnecessary re-renders.
- Refactored comment rendering logic into a separate function for better readability and maintainability.
- Updated mentionsOptions to utilize useMemo for efficient computation based on members.
- Improved comment change handling and member selection logic for a smoother user experience.
- Cleaned up code by removing redundant comments and optimizing dependencies in useEffect hooks.
2025-06-19 10:40:40 +05:30
Chamika J
a0cf5099f8 Merge pull request #162 from Worklenz/imp/kanban-performance
Imp/kanban performance
2025-06-19 10:35:44 +05:30
chamiakJ
82aa207e0d refactor(task-management): enhance TaskGroup and TaskRow components for improved functionality and styling
- Updated TaskGroup to include new props for task selection and toggling subtasks.
- Refactored TaskRow to improve layout and styling, including fixed and scrollable columns.
- Replaced drag handle icon and adjusted task metadata display for better clarity.
- Enhanced overall styling for better responsiveness and dark mode support.
2025-06-19 09:21:55 +05:30
Chamika J
301b58f0ba Merge pull request #161 from shancds/release/v2.0.3-kanban-handle-drag-over
Release/v2.0.3 kanban handle drag over
2025-06-19 08:31:43 +05:30
shancds
4c4a860c76 feat(board): enhance task and subtask management in board components
- Updated boardSlice to allow updating task assignees and names for both main tasks and subtasks.
- Improved BoardSubTaskCard to include context menu options for assigning tasks, deleting subtasks, and handling errors.
- Refactored BoardViewTaskCard to integrate dropdown menus for better task interaction and organization.
- Enhanced user experience by adding loading states and error handling for task actions.
2025-06-18 17:11:39 +05:30
chamikaJ
d0310ded28 refactor(project-view-enhanced-tasks): update project ID handling and improve error messaging
- Replaced use of `useParams` with `useAppSelector` to retrieve project information from the Redux store.
- Updated error message from "Project ID not found" to "Project not found" for better clarity.
- Adjusted the way the project ID is passed to the `TaskListBoard` component.
2025-06-18 17:10:14 +05:30
chamikaJ
c01ef4579a feat(project-view): add Enhanced Tasks tab and component
- Introduced a new tab for Enhanced Tasks in the project view.
- Created ProjectViewEnhancedTasks component to display task management features.
- Updated project-view-constants to include the new tab and adjusted indices for existing tabs.
- Enhanced task management styles for improved dark mode support.
2025-06-18 17:07:26 +05:30
Omindu Hirushka
99bec6c7f9 albanian language switch 2025-06-18 17:04:58 +05:30
chamikaJ
c1a303e78c feat(store): integrate task management reducers into the store
- Added taskManagementReducer, groupingReducer, and selectionReducer to the Redux store.
- Organized imports and store configuration for better clarity and maintainability.
2025-06-18 17:02:23 +05:30
shancds
193288013e refactor(priority-section): remove task name display from PrioritySection
- Eliminated the task name display from the PrioritySection component for a cleaner layout.
- Updated BoardViewTaskCard to include task name alongside the PrioritySection for improved organization.
2025-06-18 15:31:00 +05:30
shancds
39e8add103 feat(filters): enhance labels and members filter dropdowns
- Added useEffect to fetch labels when the component mounts or projectId changes in LabelsFilterDropdown.
- Improved members filter logic to only sync board members when the board task assignees are empty.
- Cleaned up redundant checks and optimized dependencies in the members filter dropdown.
2025-06-18 12:56:24 +05:30
shancds
0f82c9738b feat(project-view-board): implement task priority change handling
- Added a new function to handle task priority changes via socket events.
- Integrated priority change logic into the drag-and-drop functionality for improved task management.
- Cleaned up unused imports and improved code organization for better readability.
2025-06-18 12:18:58 +05:30
shancds
a4237a6f17 refactor(project-view-board): update collision detection strategy for drag-and-drop
- Replaced `closestCorners` with `collisionDetectionStrategy` to enhance drag-and-drop functionality.
- Aims to improve performance and accuracy during task movement on the project board.
2025-06-18 09:45:37 +05:30
Chamika J
20039a07ff Merge pull request #160 from Worklenz/imp/kanban-performance
Imp/kanban performance
2025-06-18 08:37:17 +05:30
Chamika J
dfc38a6829 Merge pull request #159 from shancds/release/v2.0.3-kanban-handle-drag-over
Release/v2.0.3 kanban handle drag over
2025-06-18 08:35:17 +05:30
shancds
0e0d1a5f11 refactor(project-view-board): clean up code and improve task handling logic
- Removed unnecessary conditional checks and whitespace for better readability.
- Streamlined task movement logic to enhance performance during drag-and-drop operations.
- Improved socket event emission for task sort order changes, ensuring more reliable updates.
- Cleaned up comments and organized code structure for clarity.
2025-06-17 16:46:36 +05:30
Omindu Hirushka
ef299f1f4a albanian language 2025-06-17 16:03:08 +05:30
shancds
4dbaab060a Merge branch 'release/v2.0.3' of https://github.com/Worklenz/worklenz into release/v2.0.3-kanban-handle-drag-over 2025-06-17 09:54:07 +05:30
shancds
b8811ab5b6 refactor(board-section): optimize component rendering and enhance task card functionality
- Imported React to ensure proper usage of hooks.
- Wrapped `BoardSectionCardContainer` in `React.memo` for performance optimization.
- Integrated `useAuthService` to manage user session within `BoardViewTaskCard`.
- Replaced priority icon rendering with a dedicated `PrioritySection` component for cleaner code and improved readability.
- Cleaned up unused code and improved overall structure of task card rendering.
2025-06-16 17:13:36 +05:30
Chamika J
5248c26b76 Merge pull request #157 from Worklenz/feature/project-list-grouping
Feature/project list grouping
2025-06-16 10:05:54 +05:30
chamikaJ
eed0fb6eca chore(deps): update brace-expansion to version 2.0.2 in package-lock.json 2025-06-16 09:51:03 +05:30
Chamika J
2a9447b506 Merge pull request #156 from Worklenz/imp/kanban-performance
Imp/kanban performance
2025-06-16 08:58:49 +05:30
Chamika J
fb94028410 Merge pull request #155 from shancds/release/v2.0.3-kanban-handle-drag-over
Release/v2.0.3 kanban handle drag over
2025-06-16 08:46:14 +05:30
chamikaJ
25639afe1a refactor(reporting): optimize project reports components with memoization
- Refactored ProjectsReports, ProjectsReportsFilters, and ProjectsReportsTable components to utilize React.memo, useCallback, and useMemo for improved performance and reduced unnecessary re-renders.
- Memoized various handlers and configurations to enhance rendering efficiency and maintain responsiveness.
- Updated component exports to use memoization, ensuring optimal performance during re-renders.
2025-06-13 16:37:03 +05:30
chamikaJ
4426b5f3ef feat(reporting): enhance overview reports with memoization and dark mode support
- Refactored components in the reporting section to utilize React.memo, useCallback, and useMemo for improved performance and reduced unnecessary re-renders.
- Updated the OverviewStatCard to support dark mode styling and added enhanced hover effects.
- Improved the Avatars component by memoizing rendering logic and preventing event propagation.
- Enhanced OverviewReportsTable with memoized columns and row props for better performance.
- Applied consistent styling adjustments across various components to ensure a cohesive user experience.
2025-06-13 16:04:32 +05:30
chamikaJ
3cae2771de feat(projects): implement grouped project retrieval and UI enhancements
- Added a new endpoint for retrieving projects grouped by category, client, or status.
- Enhanced the ProjectsController with a method to handle grouped project queries.
- Updated the projects API router to include the new grouped endpoint.
- Improved the frontend to support displaying grouped projects with pagination and filtering options.
- Updated localization files for English, Spanish, and Portuguese to include new grouping options.
- Refactored project list components to accommodate the new grouped view and improved UI elements.
2025-06-13 15:46:03 +05:30
chamikaJ
81f55adb41 feat(projects): enhance project selection and grouping functionality
- Added grouping options for projects by category, team, and status in the project list.
- Implemented search functionality with a clear search option.
- Improved UI with expandable/collapsible project groups and selection summary.
- Updated localization files for English, Spanish, and Portuguese to include new grouping and UI strings.
- Enhanced project type definitions to support additional grouping properties.
2025-06-13 13:16:25 +05:30
chamikaJ
bd4c88833d Merge branch 'release/v2.0.3' of https://github.com/Worklenz/worklenz into feature/project-list-grouping 2025-06-13 13:02:57 +05:30
Chamika J
2374d7a357 Merge pull request #152 from OminduHirushka/upstream/feature/project-groupby
Projects - List / Group View
2025-06-13 13:02:30 +05:30
Chamika J
91730026fd Merge branch 'feature/project-list-grouping' into upstream/feature/project-groupby 2025-06-13 13:02:17 +05:30
shancds
9d10b23ba7 Merge branch 'release/v2.0.3' of https://github.com/Worklenz/worklenz into release/v2.0.3-kanban-handle-drag-over 2025-06-13 08:40:27 +05:30
chamiakJ
d0c231ee43 fix(account-storage): update billingInfo property name for progress percentage
- Changed the property name from `usedPercentage` to `used_percent` in the billing account info interface and updated the corresponding usage in the AccountStorage component to ensure consistency.
2025-06-12 14:28:05 +05:30
chamiakJ
58ce8e40c7 fix(migrations): correct progress_mode type casting in SQL migration
- Updated the SQL migration to ensure proper type casting for progress_mode when updating progress values based on project_id.
2025-06-12 14:11:04 +05:30
Chamika J
66b0709e6e Merge pull request #154 from Worklenz/chore/added-google-analytics
Chore/added google analytics
2025-06-12 10:10:00 +05:30
chamiakJ
a2ed33214d Merge branch 'main' of https://github.com/Worklenz/worklenz into chore/added-google-analytics 2025-06-12 09:49:32 +05:30
Chamika J
a3dccd690d Merge pull request #141 from gdevenyi/patch-1
Fix use of deprecated mc command for bucket creation
2025-06-12 09:46:45 +05:30
chamiakJ
69313fba34 Enhance privacy notice functionality in index.html
- Updated the privacy notice button to use an ID for easier event handling.
- Added an event listener to the button to manage the display of the notice and store user consent in localStorage.
2025-06-12 09:38:24 +05:30
chamiakJ
1889c58598 Refactor privacy notice implementation in index.html
- Introduced a dedicated function to display the privacy notice for users of the open source version.
- Updated the styling and content of the privacy notice for improved visibility and clarity.
- Added a DOMContentLoaded event listener to conditionally show the notice based on the environment and previous interactions.
2025-06-12 09:34:53 +05:30
chamiakJ
e9f0162439 Enhance documentation and integrate Google Analytics
- Added an Analytics section to the README.md, detailing what is tracked, privacy measures, and opt-out instructions.
- Implemented Google Analytics in index.html, including a privacy notice for open source users and environment-specific tracking IDs.
- Updated worklenz-frontend README.md to include a License section.
2025-06-12 09:26:00 +05:30
shancds
2aa4fe9673 feat(project-view-board): enhance drag-and-drop functionality and optimize task handling
- Added debounced task movement to prevent rapid updates during drag-and-drop operations.
- Implemented a custom collision detection strategy for improved task placement logic.
- Introduced new refs and state management for better handling of drag events and task cloning.
- Refactored drag event handlers to streamline task movement between groups and sections.
- Enhanced loading state management and cleanup for better user experience during task interactions.
2025-06-11 14:50:48 +05:30
chamikaJ
ccb50e3c62 feat(navbar): add HelpButton to the navbar and comment out TimerButton
- Temporarily commented out TimerButton for future adjustments.
2025-06-11 12:59:17 +05:30
chamikaJ
5ce9e66fea feat(timer-button): enhance error handling and improve timer updates
- Added error state management and logging for API calls and timer updates.
- Refactored timer update logic to handle invalid data and improve robustness.
- Updated dropdown rendering to display error messages and handle empty states more gracefully.
- Improved socket event handling with error logging for better debugging.
2025-06-11 12:55:53 +05:30
chamikaJ
6492a4672b Merge branch 'feature/recurring-tasks' of https://github.com/Worklenz/worklenz into release/v2.0.3 2025-06-11 12:41:04 +05:30
Chamika J
46acb26c42 Merge pull request #153 from shancds/fix/project-list-issues
feat(locales): add 'enterProjectKey' translation to project drawer fo…
2025-06-11 12:36:49 +05:30
shancds
c9aab73a2a Merge branch 'release/v2.0.3' of https://github.com/Worklenz/worklenz into fix/release/v2.0.3-update 2025-06-11 12:01:45 +05:30
shancds
13a202cca4 feat(locales): add 'enterProjectKey' translation to project drawer for en, es, and pt 2025-06-09 14:40:46 +05:30
chamikaJ
bdb9c9ca28 feat(project-subscribers): implement project subscriber management and loading state
- Added `getProjectSubscribers` method in `TasksControllerV2` to retrieve project subscribers with user details.
- Updated socket command to handle project subscription changes, ensuring no duplicate entries on conflict.
- Enhanced `ProjectViewHeader` to manage subscription loading state, providing user feedback during subscription updates.
- Implemented error handling and timeout for subscription requests to improve user experience.
2025-06-09 13:13:45 +05:30
chamikaJ
5ed5a86bad refactor(tasks-controller): enhance progress calculation logic for tasks without subtasks
- Updated progress calculation to consider project settings for time-based progress.
- Implemented a cap on progress to prevent exceeding 100%.
- Defaulted progress to 0% when time-based calculation is not enabled, improving accuracy in task status representation.
2025-06-09 13:12:24 +05:30
chamikaJ
520888988e feat(task-list): implement drag-and-drop functionality for task reordering
- Integrated drag-and-drop capabilities in the task list using `@dnd-kit` for improved user experience.
- Created a `DraggableRow` component to handle individual task dragging and dropping.
- Updated task list rendering to support dynamic reordering and socket integration for backend persistence.
- Enhanced task selection and hover effects for better visual feedback during drag operations.
- Refactored task list components to streamline rendering and improve performance.
2025-06-09 09:58:28 +05:30
chamiakJ
de28f87c62 refactor(task-drag-and-drop): remove unused drag-and-drop hook and simplify task group handling
- Deleted `useTaskDragAndDrop` hook to streamline drag-and-drop functionality.
- Updated `TaskGroupWrapperOptimized` to remove drag-and-drop context and simplify rendering.
- Refactored `TaskListTable` to integrate drag-and-drop directly, enhancing performance and maintainability.
- Adjusted task rendering logic to ensure proper handling of task states during drag operations.
2025-06-09 07:19:15 +05:30
chamikaJ
81a6c44090 refactor(info-tab-footer): enhance member selection and comment handling
- Updated member selection to use member names as values for better display.
- Improved handling of selected members to ensure correct identification using member IDs.
- Reset selected members and comment value upon comment submission.
- Enhanced comment input with filtering options for mentions, improving user experience.
2025-06-06 17:03:31 +05:30
kithmina1999
f142046dcc fix(docker): correct bash syntax in postgres backup command
The command was using incorrect quote escaping which could cause issues with variable expansion and file deletion. Fixed by using single quotes for the outer string and proper escaping for date command substitution.
2025-06-06 16:32:11 +05:30
kithmina1999
c5e480af52 fix(db): correct variable syntax in pg_dump command
The previous version used $$ for variable expansion which doesn't work in this context. Changed to single $ for proper shell variable expansion in the database backup command.
2025-06-06 16:30:46 +05:30
kithmina1999
f89e3e8554 refactor(database): improve database initialization and backup handling
- Rename init script from init.sh to 00_init.sh for better ordering
- Rewrite database initialization script to support backup restoration
- Add proper migration handling with schema_migrations table
- Fix indentation and quoting issues in docker-compose.yml
- Remove old 00-init-db.sh script as it's replaced by new implementation
2025-06-06 16:26:11 +05:30
kithmina1999
1442c57e18 refactor(docker): improve postgres container initialization script
The command for the postgres service was restructured to:
1. Use more readable multi-line formatting
2. Replace the find -exec with a more efficient for loop
3. Maintain the same functionality while improving maintainability
2025-06-06 16:06:42 +05:30
kithmina1999
0987fb14b2 refactor(docker): improve command formatting and fix shell script issues
- Fix shell script syntax in db service command by using proper quoting and loop structure
- Clean up indentation and formatting in both db and db-backup services
- Ensure consistent command structure while maintaining the same functionality
2025-06-06 15:34:18 +05:30
kithmina1999
dc22d1e6cb fix(db): improve database initialization script and docker setup
- Rename init.sh to 00_init.sh for better ordering
- Format docker-compose command for better readability
- Add comprehensive database initialization script with backup restoration
- Implement proper migration handling with schema_migrations table
2025-06-06 15:12:06 +05:30
Omindu Hirushka
e9e9bffd9a group by client / category 2025-06-06 13:23:23 +05:30
kithmina1999
11694de4e6 feat(db): add database backup and initialization system
- Add backup.sh script for manual PostgreSQL database backups
- Update .gitignore to exclude pg_backups directory
- Modify docker-compose.yml to include backup service and volume mounts
- Add init.sh script for automated database initialization with backup restoration
2025-06-06 12:38:46 +05:30
Chamika J
8f181c687b Merge pull request #150 from Worklenz/feature/recurring-tasks
Feature/recurring tasks
2025-06-06 12:09:03 +05:30
Chamika J
926c058d1e Merge pull request #149 from shancds/fix/home-task-list-status-update
Fix/home task list status update
2025-06-06 12:06:26 +05:30
shancds
1583221232 fix: enable automatic refetching for task queries in dropdown and date picker components 2025-06-06 11:45:22 +05:30
Omindu Hirushka
585a65be31 current updates 2025-06-06 09:47:42 +05:30
Tharindu Kosgahakumbura
2de9b7f6b7 Merge branch 'Worklenz:main' into fix/kanban-board-optimization 2025-06-06 09:10:32 +05:30
Chamika J
323b17185c Merge pull request #148 from kithmina1999/fix/postgres-password-auth
fix: change DB_PASSWORD to static value
2025-06-05 13:18:41 +05:30
chamiakJ
6002ab7c50 refactor(task-list): optimize selector usage and update component properties
- Changed `name` property in `ITaskAssignee` interface from optional to required for better type safety.
- Updated `Dropdown` component's prop from `dropdownRender` to `popupRender` for consistency.
- Renamed `destroyPopupOnHide` to `destroyOnHidden` in relevant components for clarity.
- Split combined selectors in `ProjectViewTaskList` to prevent unnecessary re-renders, enhancing performance.
2025-06-05 11:24:07 +05:30
chamiakJ
bd77733935 feat(timers): add running timers feature to the navbar
- Introduced a new `TimerButton` component to display and manage running timers.
- Implemented API service method `getRunningTimers` to fetch active timers.
- Updated the navbar to replace the HelpButton with the TimerButton for better functionality.
- Enhanced timer display with real-time updates and socket event handling for timer start/stop actions.
2025-06-05 11:11:16 +05:30
kithmina1999
09f44a5685 fix: change DB_PASSWORD to static value for development
Using a static password simplifies development environment setup. The previous random password generation caused issues during local testing and debugging.
2025-06-05 10:40:06 +05:30
Omindu Hirushka
0e67434515 list/group view toggle button 2025-06-02 15:38:50 +05:30
chamikaJ
cfa0af24ae refactor(session-middleware): improve cookie handling and security settings
- Updated session middleware to use secure cookies in production environments.
- Adjusted sameSite attribute to "lax" for standard handling of same-origin requests.
- Removed unnecessary comments and streamlined cookie settings for clarity.
2025-06-02 13:29:05 +05:30
chamikaJ
69f5009579 refactor(auth): remove debug logging and enhance session middleware
- Eliminated extensive debug logging from the login strategy and verification endpoint to streamline the authentication process.
- Updated session middleware to improve cookie handling, enabling proxy support and adjusting session creation behavior.
- Ensured secure cookie settings for cross-origin requests in production environments.
2025-06-02 13:20:40 +05:30
chamikaJ
24fa837a39 feat(auth): enhance login and verification processes with detailed debug logging
- Added comprehensive debug logging to the login strategy and verification endpoint to track authentication flow and errors.
- Improved title determination logic for login and signup success/failure messages based on authentication status.
- Implemented middleware for logging request details on the login route to aid in debugging.
2025-06-02 13:07:50 +05:30
chamikaJ
5e4d78c6f5 refactor(task-details-form): enhance progress input handling and improve assignee rendering
- Added `InlineMember` type import for better type management.
- Enhanced the `Avatars` component to handle multiple sources for assignee names, improving flexibility in data handling.
2025-06-02 09:19:58 +05:30
chamikaJ
837692e808 feat(task-list): implement optimized task group handling and filter data loading
- Introduced `useFilterDataLoader` hook to manage asynchronous loading of filter data without blocking the main UI.
- Created `TaskGroupWrapperOptimized` for improved rendering of task groups with drag-and-drop functionality.
- Refactored `ProjectViewTaskList` to utilize the new optimized components and enhance loading state management.
- Added `TaskGroup` component for better organization and interaction with task groups.
- Updated `TaskListFilters` to leverage the new filter data loading mechanism, ensuring a smoother user experience.
2025-05-30 11:40:27 +05:30
chamikaJ
6ffdbc64d0 refactor(navbar): comment out license expiry alert for future implementation
- Commented out the conditional rendering of the license expiry alert in the Navbar component for future adjustments.
2025-05-30 11:10:11 +05:30
chamikaJ
65af5f659e refactor(build): remove Gruntfile and transition to npm scripts for build process
- Deleted Gruntfile.js to streamline the build process.
- Updated package.json to include new npm scripts for build, clean, and watch tasks.
- Added dependencies for concurrent execution and CSRF token management.
- Integrated csrf-sync for improved CSRF protection in the application.
- Refactored app and API client to utilize the new CSRF token management approach.
2025-05-30 10:56:19 +05:30
chamikaJ
f38a7b4d56 Merge branch 'feature/recurring-tasks' of https://github.com/Worklenz/worklenz into fix/performance-improvements 2025-05-30 10:55:28 +05:30
Chamika J
f4ab7841fb Merge pull request #105 from MRNafisiA/main
increase the memory limit to prevent crashing during build time.
2025-05-30 10:15:47 +05:30
Chamika J
3de4f69a62 Merge pull request #142 from gdevenyi/patch-2
Generate random passwords in update-docker-env.sh
2025-05-30 09:42:46 +05:30
Gabriel A. Devenyi
102be2c24a Generate random passwords in update-docker-env.sh 2025-05-29 15:56:56 -04:00
Omindu Hirushka
378dc22bb0 setting up 2025-05-29 11:10:22 +05:30
Chamika J
3a39b25e64 Merge pull request #144 from kithmina1999/docs/add-video-guides
Update README.md to include video guides for local and remote deployment
2025-05-28 13:01:16 +05:30
kithmina1999
32248f8424 Update README.md to include video guides for local and remote deployment
- Added a section for a video guide on local Docker deployment.
- Included a video guide for deploying Worklenz to a remote server.
2025-05-28 09:32:32 +05:30
Gabriel A. Devenyi
a1f8776743 Fix use of deprecated mc command for bucket creation 2025-05-27 22:32:36 -04:00
chamiakJ
80797e043c Merge branch 'development' of https://github.com/Worklenz/worklenz into feature/recurring-tasks 2025-05-27 17:14:43 +05:30
chamiakJ
312c6b5be8 feat(settings): add project templates settings to the configuration
- Restored the project templates settings in the settings constants file, making it accessible for admin users.
2025-05-27 17:13:04 +05:30
chamikaJ
c18889a127 refactor(task-drawer): remove unused imports and add edit mode for task name
Removed unused `useEffect` import in `task-drawer-status-dropdown.tsx` and unused `connected` variable. Added edit mode for task name in `task-drawer-header.tsx` to improve user interaction by allowing inline editing of the task name.
2025-05-22 09:39:53 +05:30
chamiakJ
c1e923c703 feat(ownership-transfer): implement transfer_team_ownership function for team ownership management
- Added a new PostgreSQL function to handle the transfer of team ownership between users
2025-05-21 21:42:16 +05:30
chamiakJ
f716971654 feat(hubspot-integration): dynamically load HubSpot script in production environment
- Added a script to conditionally load the HubSpot tracking script in the index.html file when the hostname matches 'app.worklenz.com'.
- Removed the HubSpot component from MainLayout to streamline the integration process.
2025-05-21 18:50:18 +05:30
chamiakJ
d7ca1d8bd2 style: remove HubSpot chat widget styles from global CSS
- Deleted specific styles that prevented global styles from affecting the HubSpot chat widget, streamlining the CSS file.
2025-05-21 18:39:28 +05:30
chamiakJ
8704b6a8c8 style: adjust font-family formatting and add styles for HubSpot chat widget
- Reformatted the font-family declaration for improved readability.
- Added specific styles to prevent global styles from affecting the HubSpot chat widget, ensuring consistent appearance.
2025-05-21 15:45:50 +05:30
chamiakJ
4687478704 fix: update empty list image source to use S3 URL for consistency across components 2025-05-21 15:09:42 +05:30
chamiakJ
2bdae400ac feat(hubspot-integration): add HubSpot script component for production environment
- Introduced a new HubSpot component that dynamically loads the HubSpot tracking script when in production.
- Updated MainLayout to replace TawkTo with HubSpot for improved customer engagement tracking.
2025-05-21 11:42:42 +05:30
chamiakJ
0cb0efe43e feat(cron-jobs): conditionally enable recurring tasks based on environment variable
- Updated the cron job initialization to start recurring tasks only if the ENABLE_RECURRING_JOBS environment variable is set to "true". This allows for more flexible job management based on deployment configurations.
2025-05-21 11:28:10 +05:30
Chamika J
7e431d645a Merge pull request #134 from Worklenz/chamikaJ-patch-1
Update README.md
2025-05-21 08:28:32 +05:30
Chamika J
cef4bffd69 Update README.md
updated logo URL
2025-05-21 08:28:09 +05:30
chamiakJ
84c7428fed feat(recurring-tasks): enhance recurring task functionality and documentation
- Expanded schedule options for recurring tasks, including new intervals for every X days, weeks, and months.
- Added future task creation logic to ensure tasks are created within defined limits based on their schedule type.
- Updated user guide to reflect new scheduling options and future task creation details.
- Improved backend logic for recurring task creation, including batch processing and future limit calculations.
- Added environment configuration for enabling recurring jobs.
- Enhanced frontend localization for recurring task configuration labels.
2025-05-21 08:07:59 +05:30
chamikaJ
a568ee808f Merge branch 'feature/member-time-progress-and-utilization' of https://github.com/Worklenz/worklenz into feature/recurring-tasks 2025-05-19 13:39:13 +05:30
chamiakJ
69b910f2a4 refactor(sql-functions): enhance SQL functions with COALESCE for better null handling
- Updated various SQL queries to use COALESCE, ensuring that null values are replaced with defaults for improved data integrity.
- Modified the handling of schedule_id for recurring tasks to return a JSON object or 'null' as appropriate.
- Improved the return structure of task-related JSON objects to prevent null values in the response.
2025-05-19 06:28:03 +05:30
chamiakJ
f9858fbd4b refactor(task-list): enhance performance with useMemo and useCallback
- Introduced useMemo to optimize loading state and empty state calculations.
- Added useMemo for socket event handler functions to prevent unnecessary re-renders.
- Refactored data fetching logic to improve initial data load handling.
- Improved drag-and-drop functionality with memoized handlers for better performance.
2025-05-18 20:58:20 +05:30
chamiakJ
f3a7fd8be5 refactor(project-view): optimize component with useMemo and useCallback for performance improvements
- Introduced useMemo and useCallback to memoize tab menu items and callback functions, enhancing performance.
- Added resetProjectData function to clean up project state on component unmount.
- Refactored the component to use React.memo for preventing unnecessary re-renders.
2025-05-18 20:15:40 +05:30
chamikaJ
49bdd00dac fix(todo-list): update empty list image source to use relative path 2025-05-16 14:45:47 +05:30
chamikaJ
2e985bd051 feat(recurring-tasks): implement recurring task scheduling and API integration 2025-05-16 14:32:45 +05:30
chamikaJ
8e74f1ddb5 Merge branch 'chore/add-google-analytics' of https://github.com/Worklenz/worklenz into feature/recurring-tasks 2025-05-16 12:43:06 +05:30
Chamika J
753e3be83f Merge pull request #131 from chamikaJ/feature/recurring-tasks
Feature/recurring tasks
2025-05-16 12:36:23 +05:30
chamikaJ
ebd0f66768 feat(task-drawer): integrate socket handling for recurring task updates
Enhance the TaskDrawerRecurringConfig component to include socket communication for handling recurring task changes. This update introduces the use of Redux for managing state updates related to recurring schedules, ensuring real-time synchronization of task configurations. Additionally, the code has been refactored for improved readability and maintainability.
2025-05-16 12:37:02 +05:30
chamiakJ
d333104f43 feat(i18n): add recurring task translation keys
Add new localization entries for the recurring task feature in English, Spanish, and Portuguese. This update includes the addition of the "recurring" key to the task drawer JSON files, enhancing support for recurring task configurations across multiple languages.
2025-05-16 07:21:57 +05:30
chamikaJ
62548e5c37 feat(task-drawer): add recurring task configuration
Add support for configuring recurring tasks in the task drawer. This includes adding a new `schedule_id` field to the task type, creating a new `TaskDrawerRecurringConfig` component, and updating localization files for English, Spanish, and Portuguese. The configuration allows setting repeat intervals, days of the week, and monthly recurrence options.
2025-05-14 15:41:09 +05:30
MRNafisiA
75391641fd increase the memory limit to prevent crashing during build time. 2025-05-02 15:53:48 +03:30
1620 changed files with 120414 additions and 17809 deletions

2
.gitignore vendored
View File

@@ -36,6 +36,8 @@ lerna-debug.log*
.vscode/*
!.vscode/extensions.json
.idea/
.cursor/
.claude/
.DS_Store
*.suo
*.ntvs*

422
README.md
View File

@@ -1,11 +1,29 @@
<h1 align="center">
<a href="https://worklenz.com" target="_blank" rel="noopener noreferrer">
<img src="https://app.worklenz.com/assets/icons/icon-144x144.png" alt="Worklenz Logo" width="75">
<img src="https://s3.us-west-2.amazonaws.com/worklenz.com/assets/icon-144x144.png" alt="Worklenz Logo" width="75">
</a>
<br>
Worklenz
</h1>
<p align="center">
<a href="https://github.com/Worklenz/worklenz/blob/main/LICENSE">
<img src="https://img.shields.io/badge/license-AGPL--3.0-blue.svg" alt="License">
</a>
<a href="https://github.com/Worklenz/worklenz/releases">
<img src="https://img.shields.io/github/v/release/Worklenz/worklenz" alt="Release">
</a>
<a href="https://github.com/Worklenz/worklenz/stargazers">
<img src="https://img.shields.io/github/stars/Worklenz/worklenz" alt="Stars">
</a>
<a href="https://github.com/Worklenz/worklenz/network/members">
<img src="https://img.shields.io/github/forks/Worklenz/worklenz" alt="Forks">
</a>
<a href="https://github.com/Worklenz/worklenz/issues">
<img src="https://img.shields.io/github/issues/Worklenz/worklenz" alt="Issues">
</a>
</p>
<p align="center">
<a href="https://worklenz.com/task-management/">Task Management</a> |
<a href="https://worklenz.com/time-tracking/">Time Tracking</a> |
@@ -27,6 +45,24 @@
Worklenz is a project management tool designed to help organizations improve their efficiency. It provides a
comprehensive solution for managing projects, tasks, and collaboration within teams.
## Table of Contents
- [Features](#features)
- [Tech Stack](#tech-stack)
- [Getting Started](#getting-started)
- [Quick Start (Docker)](#-quick-start-docker---recommended)
- [Manual Installation](#-manual-installation-for-development)
- [Deployment](#deployment)
- [Local Development](#local-development-with-docker)
- [Remote Server Deployment](#remote-server-deployment)
- [Configuration](#configuration)
- [MinIO Integration](#minio-integration)
- [Security](#security)
- [Analytics](#analytics)
- [Screenshots](#screenshots)
- [Contributing](#contributing)
- [License](#license)
## Features
- **Project Planning**: Create and organize projects, assign tasks to team members.
@@ -50,42 +86,80 @@ This repository contains the frontend and backend code for Worklenz.
## Getting Started
These instructions will help you set up and run the Worklenz project on your local machine for development and testing purposes.
Choose your preferred setup method below. Docker is recommended for quick setup and testing.
### Prerequisites
### 🚀 Quick Start (Docker - Recommended)
- Node.js (version 18 or higher)
- PostgreSQL database
- An S3-compatible storage service (like MinIO) or Azure Blob Storage
The fastest way to get Worklenz running locally with all dependencies included.
### Option 1: Manual Installation
**Prerequisites:**
- Docker and Docker Compose installed on your system
- Git
1. Clone the repository
**Steps:**
1. Clone the repository:
```bash
git clone https://github.com/Worklenz/worklenz.git
cd worklenz
```
2. Set up environment variables
- Copy the example environment files
```bash
cp .env.example .env
cp worklenz-backend/.env.example worklenz-backend/.env
```
- Update the environment variables with your configuration
3. Install dependencies
2. Start the Docker containers:
```bash
# Install backend dependencies
docker-compose up -d
```
3. Access the application:
- **Frontend**: http://localhost:5000
- **Backend API**: http://localhost:3000
- **MinIO Console**: http://localhost:9001 (login: minioadmin/minioadmin)
4. To stop the services:
```bash
docker-compose down
```
**Alternative startup methods:**
- **Windows**: Run `start.bat`
- **Linux/macOS**: Run `./start.sh`
**Video Guide**: For a visual walkthrough of the local Docker deployment process, check out our [step-by-step video guide](https://www.youtube.com/watch?v=AfwAKxJbqLg).
### 🛠️ Manual Installation (For Development)
For developers who want to run the services individually or customize the setup.
**Prerequisites:**
- Node.js (version 18 or higher)
- PostgreSQL (version 15 or higher)
- An S3-compatible storage service (like MinIO) or Azure Blob Storage
**Steps:**
1. Clone the repository:
```bash
git clone https://github.com/Worklenz/worklenz.git
cd worklenz
```
2. Set up environment variables:
```bash
cp worklenz-backend/.env.template worklenz-backend/.env
# Update the environment variables with your configuration
```
3. Install dependencies:
```bash
# Backend dependencies
cd worklenz-backend
npm install
# Install frontend dependencies
# Frontend dependencies
cd ../worklenz-frontend
npm install
```
4. Set up the database
4. Set up the database:
```bash
# Create a PostgreSQL database named worklenz_db
cd worklenz-backend
@@ -101,49 +175,47 @@ psql -U your_username -d worklenz_db -f database/sql/2_dml.sql
psql -U your_username -d worklenz_db -f database/sql/5_database_user.sql
```
5. Start the development servers
5. Start the development servers:
```bash
# In one terminal, start the backend
# Terminal 1: Start the backend
cd worklenz-backend
npm run dev
# In another terminal, start the frontend
# Terminal 2: Start the frontend
cd worklenz-frontend
npm run dev
```
6. Access the application at http://localhost:5000
### Option 2: Docker Setup
## Deployment
The project includes a fully configured Docker setup with:
- Frontend React application
- Backend server
- PostgreSQL database
- MinIO for S3-compatible storage
For local development, follow the [Quick Start (Docker)](#-quick-start-docker---recommended) section above.
1. Clone the repository:
```bash
git clone https://github.com/Worklenz/worklenz.git
cd worklenz
```
### Remote Server Deployment
2. Start the Docker containers (choose one option):
When deploying to a remote server:
**Using Docker Compose directly**
```bash
docker-compose up -d
```
1. Set up the environment files with your server's hostname:
```bash
# For HTTP/WS
./update-docker-env.sh your-server-hostname
# For HTTPS/WSS
./update-docker-env.sh your-server-hostname true
```
3. The application will be available at:
- Frontend: http://localhost:5000
- Backend API: http://localhost:3000
- MinIO Console: http://localhost:9001 (login with minioadmin/minioadmin)
2. Pull and run the latest Docker images:
```bash
docker-compose pull
docker-compose up -d
```
4. To stop the services:
```bash
docker-compose down
```
3. Access the application through your server's hostname:
- Frontend: http://your-server-hostname:5000
- Backend API: http://your-server-hostname:3000
4. **Video Guide**: For a complete walkthrough of deploying Worklenz to a remote server, check out our [deployment video guide](https://www.youtube.com/watch?v=CAZGu2iOXQs&t=10s).
## Configuration
@@ -158,16 +230,46 @@ Worklenz requires several environment variables to be configured for proper oper
Please refer to the `.env.example` files for a full list of required variables.
### MinIO Integration
The Docker setup uses environment variables to configure the services:
- **Frontend:**
- `VITE_API_URL`: URL of the backend API (default: http://backend:3000 for container networking)
- `VITE_SOCKET_URL`: WebSocket URL for real-time communication (default: ws://backend:3000)
- **Backend:**
- Database connection parameters
- Storage configuration
- Other backend settings
For custom configuration, edit the `.env` file or the `update-docker-env.sh` script.
## MinIO Integration
The project uses MinIO as an S3-compatible object storage service, which provides an open-source alternative to AWS S3 for development and production.
### Working with MinIO
MinIO provides an S3-compatible API, so any code that works with S3 will work with MinIO by simply changing the endpoint URL. The backend has been configured to use MinIO by default, with no additional configuration required.
- **MinIO Console**: http://localhost:9001
- Username: minioadmin
- Password: minioadmin
- **Default Bucket**: worklenz-bucket (created automatically when the containers start)
### Backend Storage Configuration
The backend is pre-configured to use MinIO with the following settings:
```javascript
// S3 credentials with MinIO defaults
export const REGION = process.env.AWS_REGION || "us-east-1";
export const BUCKET = process.env.AWS_BUCKET || "worklenz-bucket";
export const S3_URL = process.env.S3_URL || "http://minio:9000/worklenz-bucket";
export const S3_ACCESS_KEY_ID = process.env.AWS_ACCESS_KEY_ID || "minioadmin";
export const S3_SECRET_ACCESS_KEY = process.env.AWS_SECRET_ACCESS_KEY || "minioadmin";
```
### Security Considerations
For production deployments:
@@ -178,19 +280,32 @@ For production deployments:
4. Enable HTTPS for all public endpoints
5. Review and update dependencies regularly
## Contributing
We welcome contributions from the community! If you'd like to contribute, please follow our [contributing guidelines](CONTRIBUTING.md).
## Security
If you believe you have found a security vulnerability in Worklenz, we encourage you to responsibly disclose this and not open a public issue. We will investigate all legitimate reports.
Email [info@worklenz.com](mailto:info@worklenz.com) to disclose any security vulnerabilities.
## License
## Analytics
This project is licensed under the [MIT License](LICENSE).
Worklenz uses Google Analytics to understand how the application is being used. This helps us improve the application and make better decisions about future development.
### What We Track
- Anonymous usage statistics
- Page views and navigation patterns
- Feature usage
- Browser and device information
### Privacy
- Analytics is opt-in only
- No personal information is collected
- Users can opt-out at any time
- Data is stored according to Google's privacy policy
### How to Opt-Out
If you've previously opted in and want to opt-out:
1. Clear your browser's local storage for the Worklenz domain
2. Or click the "Decline" button in the analytics notice if it appears
## Screenshots
@@ -240,206 +355,13 @@ This project is licensed under the [MIT License](LICENSE).
</a>
</p>
### Contributing
## Contributing
We welcome contributions from the community! If you'd like to contribute, please follow
our [contributing guidelines](CONTRIBUTING.md).
We welcome contributions from the community! If you'd like to contribute, please follow our [contributing guidelines](CONTRIBUTING.md).
### License
## License
Worklenz is open source and released under the [GNU Affero General Public License Version 3 (AGPLv3)](LICENSE).
By contributing to Worklenz, you agree that your contributions will be licensed under its AGPL.
# Worklenz React
This repository contains the React version of Worklenz with a Docker setup for easy development and deployment.
## Getting Started with Docker
The project includes a fully configured Docker setup with:
- Frontend React application
- Backend server
- PostgreSQL database
- MinIO for S3-compatible storage
### Prerequisites
- Docker and Docker Compose installed on your system
- Git
### Quick Start
1. Clone the repository:
```bash
git clone https://github.com/Worklenz/worklenz.git
cd worklenz
```
2. Start the Docker containers (choose one option):
**Option 1: Using the provided scripts (easiest)**
- On Windows:
```
start.bat
```
- On Linux/macOS:
```bash
./start.sh
```
**Option 2: Using Docker Compose directly**
```bash
docker-compose up -d
```
3. The application will be available at:
- Frontend: http://localhost:5000
- Backend API: http://localhost:3000
- MinIO Console: http://localhost:9001 (login with minioadmin/minioadmin)
4. To stop the services (choose one option):
**Option 1: Using the provided scripts**
- On Windows:
```
stop.bat
```
- On Linux/macOS:
```bash
./stop.sh
```
**Option 2: Using Docker Compose directly**
```bash
docker-compose down
```
## MinIO Integration
The project uses MinIO as an S3-compatible object storage service, which provides an open-source alternative to AWS S3 for development and production.
### Working with MinIO
MinIO provides an S3-compatible API, so any code that works with S3 will work with MinIO by simply changing the endpoint URL. The backend has been configured to use MinIO by default, with no additional configuration required.
- **MinIO Console**: http://localhost:9001
- Username: minioadmin
- Password: minioadmin
- **Default Bucket**: worklenz-bucket (created automatically when the containers start)
### Backend Storage Configuration
The backend is pre-configured to use MinIO with the following settings:
```javascript
// S3 credentials with MinIO defaults
export const REGION = process.env.AWS_REGION || "us-east-1";
export const BUCKET = process.env.AWS_BUCKET || "worklenz-bucket";
export const S3_URL = process.env.S3_URL || "http://minio:9000/worklenz-bucket";
export const S3_ACCESS_KEY_ID = process.env.AWS_ACCESS_KEY_ID || "minioadmin";
export const S3_SECRET_ACCESS_KEY = process.env.AWS_SECRET_ACCESS_KEY || "minioadmin";
```
The S3 client is initialized with special MinIO configuration:
```javascript
const s3Client = new S3Client({
region: REGION,
credentials: {
accessKeyId: S3_ACCESS_KEY_ID || "",
secretAccessKey: S3_SECRET_ACCESS_KEY || "",
},
endpoint: getEndpointFromUrl(), // Extracts endpoint from S3_URL
forcePathStyle: true, // Required for MinIO
});
```
### Environment Configuration
The project uses the following environment file structure:
- **Frontend**:
- `worklenz-frontend/.env.development` - Development environment variables
- `worklenz-frontend/.env.production` - Production build variables
- **Backend**:
- `worklenz-backend/.env` - Backend environment variables
### Setting Up Environment Files
The Docker environment script will create or overwrite all environment files:
```bash
# For HTTP/WS
./update-docker-env.sh your-hostname
# For HTTPS/WSS
./update-docker-env.sh your-hostname true
```
This script generates properly configured environment files for both development and production environments.
## Docker Deployment
### Local Development with Docker
1. Set up the environment files:
```bash
# For HTTP/WS
./update-docker-env.sh
# For HTTPS/WSS
./update-docker-env.sh localhost true
```
2. Run the application using Docker Compose:
```bash
docker-compose up -d
```
3. Access the application:
- Frontend: http://localhost:5000
- Backend API: http://localhost:3000 (or https://localhost:3000 with SSL)
### Remote Server Deployment
When deploying to a remote server:
1. Set up the environment files with your server's hostname:
```bash
# For HTTP/WS
./update-docker-env.sh your-server-hostname
# For HTTPS/WSS
./update-docker-env.sh your-server-hostname true
```
This ensures that the frontend correctly connects to the backend API.
2. Pull and run the latest Docker images:
```bash
docker-compose pull
docker-compose up -d
```
3. Access the application through your server's hostname:
- Frontend: http://your-server-hostname:5000
- Backend API: http://your-server-hostname:3000
### Environment Configuration
The Docker setup uses environment variables to configure the services:
- Frontend:
- `VITE_API_URL`: URL of the backend API (default: http://backend:3000 for container networking)
- `VITE_SOCKET_URL`: WebSocket URL for real-time communication (default: ws://backend:3000)
- Backend:
- Database connection parameters
- Storage configuration
- Other backend settings
For custom configuration, edit the `.env` file or the `update-docker-env.sh` script.

View File

@@ -4,7 +4,7 @@ Getting started with development is a breeze! Follow these steps and you'll be c
## Requirements
- Node.js version v16 or newer - [Node.js](https://nodejs.org/en/download/)
- Node.js version v20 or newer - [Node.js](https://nodejs.org/en/download/)
- PostgreSQL version v15 or newer - [PostgreSQL](https://www.postgresql.org/download/)
- S3-compatible storage (like MinIO) for file storage
@@ -38,7 +38,7 @@ Getting started with development is a breeze! Follow these steps and you'll be c
npm start
```
4. Navigate to [http://localhost:5173](http://localhost:5173)
4. Navigate to [http://localhost:5173](http://localhost:5173) (development server)
### Backend installation
@@ -126,7 +126,7 @@ For an easier setup, you can use Docker and Docker Compose:
```
3. Access the application:
- Frontend: http://localhost:5000
- Frontend: http://localhost:5000 (Docker production build)
- Backend API: http://localhost:3000
- MinIO Console: http://localhost:9001 (login with minioadmin/minioadmin)

16
backup.sh Normal file
View File

@@ -0,0 +1,16 @@
#!/bin/bash
set -eu
# Adjust these as needed:
CONTAINER=worklenz_db
DB_NAME=worklenz_db
DB_USER=postgres
BACKUP_DIR=./pg_backups
mkdir -p "$BACKUP_DIR"
timestamp=$(date +%Y-%m-%d_%H-%M-%S)
outfile="${BACKUP_DIR}/${DB_NAME}_${timestamp}.sql"
echo "Creating backup $outfile ..."
docker exec -t "$CONTAINER" pg_dump -U "$DB_USER" -d "$DB_NAME" > "$outfile"
echo "Backup saved to $outfile"

View File

@@ -7,8 +7,8 @@ services:
ports:
- "5000:5000"
depends_on:
backend:
condition: service_started
- backend
restart: unless-stopped
env_file:
- ./worklenz-frontend/.env.production
networks:
@@ -26,6 +26,7 @@ services:
condition: service_healthy
minio:
condition: service_started
restart: unless-stopped
env_file:
- ./worklenz-backend/.env
networks:
@@ -37,6 +38,7 @@ services:
ports:
- "9000:9000"
- "9001:9001"
restart: unless-stopped
environment:
MINIO_ROOT_USER: ${S3_ACCESS_KEY_ID:-minioadmin}
MINIO_ROOT_PASSWORD: ${S3_SECRET_ACCESS_KEY:-minioadmin}
@@ -52,13 +54,14 @@ services:
container_name: worklenz_createbuckets
depends_on:
- minio
restart: on-failure
entrypoint: >
/bin/sh -c '
echo "Waiting for MinIO to start...";
sleep 15;
for i in 1 2 3 4 5; do
echo "Attempt $i to connect to MinIO...";
if /usr/bin/mc config host add myminio http://minio:9000 minioadmin minioadmin; then
if /usr/bin/mc alias set myminio http://minio:9000 minioadmin minioadmin; then
echo "Successfully connected to MinIO!";
/usr/bin/mc mb --ignore-existing myminio/worklenz-bucket;
/usr/bin/mc policy set public myminio/worklenz-bucket;
@@ -80,32 +83,79 @@ services:
POSTGRES_DB: ${DB_NAME:-worklenz_db}
POSTGRES_PASSWORD: ${DB_PASSWORD:-password}
healthcheck:
test: [ "CMD-SHELL", "pg_isready -d ${DB_NAME:-worklenz_db} -U ${DB_USER:-postgres}" ]
test:
[
"CMD-SHELL",
"pg_isready -d ${DB_NAME:-worklenz_db} -U ${DB_USER:-postgres}",
]
interval: 10s
timeout: 5s
retries: 5
restart: unless-stopped
networks:
- worklenz
volumes:
- worklenz_postgres_data:/var/lib/postgresql/data
- type: bind
source: ./worklenz-backend/database
target: /docker-entrypoint-initdb.d
source: ./worklenz-backend/database/sql
target: /docker-entrypoint-initdb.d/sql
consistency: cached
- type: bind
source: ./worklenz-backend/database/migrations
target: /docker-entrypoint-initdb.d/migrations
consistency: cached
- type: bind
source: ./worklenz-backend/database/00_init.sh
target: /docker-entrypoint-initdb.d/00_init.sh
consistency: cached
- type: bind
source: ./pg_backups
target: /docker-entrypoint-initdb.d/pg_backups
command: >
bash -c ' if command -v apt-get >/dev/null 2>&1; then
apt-get update && apt-get install -y dos2unix
elif command -v apk >/dev/null 2>&1; then
apk add --no-cache dos2unix
fi && find /docker-entrypoint-initdb.d -type f -name "*.sh" -exec sh -c '\''
dos2unix "{}" 2>/dev/null || true
chmod +x "{}"
'\'' \; && exec docker-entrypoint.sh postgres '
bash -c '
if command -v apt-get >/dev/null 2>&1; then
apt-get update && apt-get install -y dos2unix
elif command -v apk >/dev/null 2>&1; then
apk add --no-cache dos2unix
fi
find /docker-entrypoint-initdb.d -type f -name "*.sh" -exec sh -c '"'"'
for f; do
dos2unix "$f" 2>/dev/null || true
chmod +x "$f"
done
'"'"' sh {} +
exec docker-entrypoint.sh postgres
'
db-backup:
image: postgres:15
container_name: worklenz_db_backup
environment:
POSTGRES_USER: ${DB_USER:-postgres}
POSTGRES_DB: ${DB_NAME:-worklenz_db}
POSTGRES_PASSWORD: ${DB_PASSWORD:-password}
depends_on:
db:
condition: service_healthy
volumes:
- ./pg_backups:/pg_backups #host dir for backups files
#setup bassh loop to backup data evey 24h
command: >
bash -c 'while true; do
sleep 86400;
PGPASSWORD=$$POSTGRES_PASSWORD pg_dump -h worklenz_db -U $$POSTGRES_USER -d $$POSTGRES_DB \
> /pg_backups/worklenz_db_$$(date +%Y-%m-%d_%H-%M-%S).sql;
find /pg_backups -type f -name "*.sql" -mtime +30 -delete;
done'
restart: unless-stopped
networks:
- worklenz
volumes:
worklenz_postgres_data:
worklenz_minio_data:
pgdata:
networks:
worklenz:

View File

@@ -0,0 +1,429 @@
# Enhanced Task Management: Technical Guide
## Overview
The Enhanced Task Management system is a comprehensive React-based interface built on top of WorkLenz's existing task infrastructure. It provides a modern, grouped view with drag-and-drop functionality, bulk operations, and responsive design.
## Architecture
### Component Structure
```
src/components/task-management/
├── TaskListBoard.tsx # Main container with DnD context
├── TaskGroup.tsx # Individual group with collapse/expand
├── TaskRow.tsx # Task display with rich metadata
├── GroupingSelector.tsx # Grouping method switcher
└── BulkActionBar.tsx # Bulk operations toolbar
```
### Integration Points
The system integrates with existing WorkLenz infrastructure:
- **Redux Store:** Uses `tasks.slice.ts` for state management
- **Types:** Leverages existing TypeScript interfaces
- **API Services:** Works with existing task API endpoints
- **WebSocket:** Supports real-time updates via existing socket system
## Core Components
### TaskListBoard.tsx
Main orchestrator component that provides:
- **DnD Context:** @dnd-kit drag-and-drop functionality
- **State Management:** Redux integration for task data
- **Event Handling:** Drag events and bulk operations
- **Layout Structure:** Header controls and group container
#### Key Props
```typescript
interface TaskListBoardProps {
projectId: string; // Required: Project identifier
className?: string; // Optional: Additional CSS classes
}
```
#### Redux Selectors Used
```typescript
const {
taskGroups, // ITaskListGroup[] - Grouped task data
loadingGroups, // boolean - Loading state
error, // string | null - Error state
groupBy, // IGroupBy - Current grouping method
search, // string | null - Search filter
archived, // boolean - Show archived tasks
} = useSelector((state: RootState) => state.taskReducer);
```
### TaskGroup.tsx
Renders individual task groups with:
- **Collapsible Headers:** Expand/collapse functionality
- **Progress Indicators:** Visual completion progress
- **Drop Zones:** Accept dropped tasks from other groups
- **Group Statistics:** Task counts and completion rates
#### Key Props
```typescript
interface TaskGroupProps {
group: ITaskListGroup; // Group data with tasks
projectId: string; // Project context
currentGrouping: IGroupBy; // Current grouping mode
selectedTaskIds: string[]; // Selected task IDs
onAddTask?: (groupId: string) => void;
onToggleCollapse?: (groupId: string) => void;
}
```
### TaskRow.tsx
Individual task display featuring:
- **Rich Metadata:** Progress, assignees, labels, due dates
- **Drag Handles:** Sortable within and between groups
- **Selection:** Multi-select with checkboxes
- **Subtask Support:** Expandable hierarchy display
#### Key Props
```typescript
interface TaskRowProps {
task: IProjectTask; // Task data
projectId: string; // Project context
groupId: string; // Parent group ID
currentGrouping: IGroupBy; // Current grouping mode
isSelected: boolean; // Selection state
isDragOverlay?: boolean; // Drag overlay rendering
index?: number; // Position in group
onSelect?: (taskId: string, selected: boolean) => void;
onToggleSubtasks?: (taskId: string) => void;
}
```
## State Management
### Redux Integration
The system uses existing WorkLenz Redux patterns:
```typescript
// Primary slice used
import {
fetchTaskGroups, // Async thunk for loading data
reorderTasks, // Update task order/group
setGroup, // Change grouping method
updateTaskStatus, // Update individual task status
updateTaskPriority, // Update individual task priority
// ... other existing actions
} from '@/features/tasks/tasks.slice';
```
### Data Flow
1. **Component Mount:** `TaskListBoard` dispatches `fetchTaskGroups(projectId)`
2. **Group Changes:** `setGroup(newGroupBy)` triggers data reorganization
3. **Drag Operations:** `reorderTasks()` updates task positions and properties
4. **Real-time Updates:** WebSocket events update Redux state automatically
## Drag and Drop Implementation
### DnD Kit Integration
Uses @dnd-kit for modern, accessible drag-and-drop:
```typescript
// Sensors for different input methods
const sensors = useSensors(
useSensor(PointerSensor, {
activationConstraint: { distance: 8 }
}),
useSensor(KeyboardSensor, {
coordinateGetter: sortableKeyboardCoordinates
})
);
```
### Drag Event Handling
```typescript
const handleDragEnd = (event: DragEndEvent) => {
const { active, over } = event;
// Determine source and target
const sourceGroup = findTaskGroup(active.id);
const targetGroup = findTargetGroup(over?.id);
// Update task arrays and dispatch changes
dispatch(reorderTasks({
activeGroupId: sourceGroup.id,
overGroupId: targetGroup.id,
fromIndex: sourceIndex,
toIndex: targetIndex,
task: movedTask,
updatedSourceTasks,
updatedTargetTasks,
}));
};
```
### Smart Property Updates
When tasks are moved between groups, properties update automatically:
- **Status Grouping:** Moving to "Done" group sets task status to "done"
- **Priority Grouping:** Moving to "High" group sets task priority to "high"
- **Phase Grouping:** Moving to "Testing" group sets task phase to "testing"
## Bulk Operations
### Selection State Management
```typescript
// Local state for task selection
const [selectedTaskIds, setSelectedTaskIds] = useState<string[]>([]);
// Selection handlers
const handleTaskSelect = (taskId: string, selected: boolean) => {
if (selected) {
setSelectedTaskIds(prev => [...prev, taskId]);
} else {
setSelectedTaskIds(prev => prev.filter(id => id !== taskId));
}
};
```
### Context-Aware Actions
Bulk actions adapt to current grouping:
```typescript
// Only show status changes when not grouped by status
{currentGrouping !== 'status' && (
<Dropdown overlay={statusMenu}>
<Button>Change Status</Button>
</Dropdown>
)}
```
## Performance Optimizations
### Memoized Selectors
```typescript
// Expensive group calculations are memoized
const taskGroups = useMemo(() => {
return createGroupsFromTasks(tasks, currentGrouping);
}, [tasks, currentGrouping]);
```
### Virtual Scrolling Ready
For large datasets, the system is prepared for react-window integration:
```typescript
// Large group detection
const shouldVirtualize = group.tasks.length > 100;
return shouldVirtualize ? (
<VirtualizedTaskList tasks={group.tasks} />
) : (
<StandardTaskList tasks={group.tasks} />
);
```
### Optimistic Updates
UI updates immediately while API calls process in background:
```typescript
// Immediate UI update
dispatch(updateTaskStatusOptimistically(taskId, newStatus));
// API call with rollback on error
try {
await updateTaskStatus(taskId, newStatus);
} catch (error) {
dispatch(rollbackTaskStatusUpdate(taskId));
}
```
## Responsive Design
### Breakpoint Strategy
```css
/* Mobile-first responsive design */
.task-row {
padding: 12px;
}
@media (min-width: 768px) {
.task-row {
padding: 16px;
}
}
@media (min-width: 1024px) {
.task-row {
padding: 20px;
}
}
```
### Progressive Enhancement
- **Mobile:** Essential information only
- **Tablet:** Additional metadata visible
- **Desktop:** Full feature set with optimal layout
## Accessibility
### ARIA Implementation
```typescript
// Proper ARIA labels for screen readers
<div
role="button"
aria-label={`Move task ${task.name}`}
tabIndex={0}
{...dragHandleProps}
>
<DragOutlined />
</div>
```
### Keyboard Navigation
- **Tab:** Navigate between elements
- **Space:** Select/deselect tasks
- **Enter:** Activate buttons
- **Arrows:** Navigate sortable lists with keyboard sensor
### Focus Management
```typescript
// Maintain focus during dynamic updates
useEffect(() => {
if (shouldFocusTask) {
taskRef.current?.focus();
}
}, [taskGroups]);
```
## WebSocket Integration
### Real-time Updates
The system subscribes to existing WorkLenz WebSocket events:
```typescript
// Socket event handlers (existing WorkLenz patterns)
socket.on('TASK_STATUS_CHANGED', (data) => {
dispatch(updateTaskStatus(data));
});
socket.on('TASK_PROGRESS_UPDATED', (data) => {
dispatch(updateTaskProgress(data));
});
```
### Live Collaboration
- Multiple users can work simultaneously
- Changes appear in real-time
- Conflict resolution through server-side validation
## API Integration
### Existing Endpoints Used
```typescript
// Uses existing WorkLenz API services
import { tasksApiService } from '@/api/tasks/tasks.api.service';
// Task data fetching
tasksApiService.getTaskList(config);
// Task updates
tasksApiService.updateTask(taskId, changes);
// Bulk operations
tasksApiService.bulkUpdateTasks(taskIds, changes);
```
### Error Handling
```typescript
try {
await dispatch(fetchTaskGroups(projectId));
} catch (error) {
// Display user-friendly error message
message.error('Failed to load tasks. Please try again.');
logger.error('Task loading error:', error);
}
```
## Testing Strategy
### Component Testing
```typescript
// Example test structure
describe('TaskListBoard', () => {
it('should render task groups correctly', () => {
const mockTasks = generateMockTasks(10);
render(<TaskListBoard projectId="test-project" />);
expect(screen.getByText('Tasks (10)')).toBeInTheDocument();
});
it('should handle drag and drop operations', async () => {
// Test drag and drop functionality
});
});
```
### Integration Testing
- Redux state management
- API service integration
- WebSocket event handling
- Drag and drop operations
## Development Guidelines
### Code Organization
- Follow existing WorkLenz patterns
- Use TypeScript strictly
- Implement proper error boundaries
- Maintain accessibility standards
### Performance Considerations
- Memoize expensive calculations
- Implement virtual scrolling for large datasets
- Debounce user input operations
- Optimize re-render cycles
### Styling Standards
- Use existing Ant Design components
- Follow WorkLenz design system
- Implement responsive breakpoints
- Maintain dark mode compatibility
## Future Enhancements
### Planned Features
- Custom column integration
- Advanced filtering capabilities
- Kanban board view
- Enhanced time tracking
- Task templates
### Extension Points
The system is designed for easy extension:
```typescript
// Plugin architecture ready
interface TaskViewPlugin {
name: string;
component: React.ComponentType;
supportedGroupings: IGroupBy[];
}
const plugins: TaskViewPlugin[] = [
{ name: 'kanban', component: KanbanView, supportedGroupings: ['status'] },
{ name: 'timeline', component: TimelineView, supportedGroupings: ['phase'] },
];
```
## Deployment Considerations
### Bundle Size
- Tree-shake unused dependencies
- Code-split large components
- Optimize asset loading
### Browser Compatibility
- Modern browsers (ES2020+)
- Graceful degradation for older browsers
- Progressive enhancement approach
### Performance Monitoring
- Track component render times
- Monitor API response times
- Measure user interaction latency

View File

@@ -0,0 +1,275 @@
# Enhanced Task Management: User Guide
## What Is Enhanced Task Management?
The Enhanced Task Management system provides a modern, grouped view of your tasks with advanced features like drag-and-drop, bulk operations, and dynamic grouping. This system builds on WorkLenz's existing task infrastructure while offering improved productivity and organization tools.
## Why Use Enhanced Task Management?
- **Better Organization:** Group tasks by Status, Priority, or Phase for clearer project overview
- **Increased Productivity:** Bulk operations let you update multiple tasks at once
- **Intuitive Interface:** Drag-and-drop functionality makes task management feel natural
- **Rich Task Display:** See progress, assignees, labels, and due dates at a glance
- **Responsive Design:** Works seamlessly on desktop, tablet, and mobile devices
## Getting Started
### Accessing Enhanced Task Management
1. Navigate to your project workspace
2. Look for the enhanced task view option in your project interface
3. The system will display your tasks grouped by the current grouping method (default: Status)
### Understanding the Interface
The enhanced task management interface consists of several key areas:
- **Header Controls:** Task count, grouping selector, and action buttons
- **Task Groups:** Collapsible sections containing related tasks
- **Individual Tasks:** Rich task cards with metadata and actions
- **Bulk Action Bar:** Appears when multiple tasks are selected (blue bar)
## Task Grouping
### Available Grouping Options
You can organize your tasks using three different grouping methods:
#### 1. Status Grouping (Default)
Groups tasks by their current status:
- **To Do:** Tasks not yet started
- **Doing:** Tasks currently in progress
- **Done:** Completed tasks
#### 2. Priority Grouping
Groups tasks by their priority level:
- **Critical:** Highest priority, urgent tasks
- **High:** Important tasks requiring attention
- **Medium:** Standard priority tasks
- **Low:** Tasks that can be addressed later
#### 3. Phase Grouping
Groups tasks by project phases:
- **Planning:** Tasks in the planning stage
- **Development:** Implementation and development tasks
- **Testing:** Quality assurance and testing tasks
- **Deployment:** Release and deployment tasks
### Switching Between Groupings
1. Locate the "Group by" dropdown in the header controls
2. Select your preferred grouping method (Status, Priority, or Phase)
3. Tasks will automatically reorganize into the new groups
4. Your grouping preference is saved for future sessions
### Group Features
Each task group includes:
- **Color-coded headers** with visual indicators
- **Task count badges** showing the number of tasks in each group
- **Progress indicators** showing completion percentage
- **Collapse/expand functionality** to hide or show group contents
- **Add task buttons** to quickly create tasks in specific groups
## Drag and Drop
### Moving Tasks Within Groups
1. Hover over a task to reveal the drag handle (⋮⋮ icon)
2. Click and hold the drag handle
3. Drag the task to your desired position within the same group
4. Release to drop the task in its new position
### Moving Tasks Between Groups
1. Click and hold the drag handle on any task
2. Drag the task over a different group
3. The target group will highlight to show it can accept the task
4. Release to drop the task into the new group
5. The task's properties (status, priority, or phase) will automatically update
### Drag and Drop Benefits
- **Instant Updates:** Task properties change automatically when moved between groups
- **Visual Feedback:** Clear indicators show where tasks can be dropped
- **Keyboard Accessible:** Alternative keyboard controls for accessibility
- **Mobile Friendly:** Touch-friendly drag operations on mobile devices
## Multi-Select and Bulk Operations
### Selecting Tasks
You can select multiple tasks using several methods:
#### Individual Selection
- Click the checkbox next to any task to select it
- Click again to deselect
#### Range Selection
- Select the first task in your desired range
- Hold Shift and click the last task in the range
- All tasks between the first and last will be selected
#### Multiple Selection
- Hold Ctrl (or Cmd on Mac) while clicking tasks
- This allows you to select non-consecutive tasks
### Bulk Actions
When you have tasks selected, a blue bulk action bar appears with these options:
#### Change Status (when not grouped by Status)
- Update the status of all selected tasks at once
- Choose from available status options in your project
#### Set Priority (when not grouped by Priority)
- Assign the same priority level to all selected tasks
- Options include Critical, High, Medium, and Low
#### More Actions
Additional bulk operations include:
- **Assign to Member:** Add team members to multiple tasks
- **Add Labels:** Apply labels to selected tasks
- **Archive Tasks:** Move multiple tasks to archive
#### Delete Tasks
- Permanently remove multiple tasks at once
- Confirmation dialog prevents accidental deletions
### Bulk Action Tips
- The bulk action bar only shows relevant options based on your current grouping
- You can clear your selection at any time using the "Clear" button
- Bulk operations provide immediate feedback and can be undone if needed
## Task Display Features
### Rich Task Information
Each task displays comprehensive information:
#### Basic Information
- **Task Key:** Unique identifier (e.g., PROJ-123)
- **Task Name:** Clear, descriptive title
- **Description:** Additional details when available
#### Visual Indicators
- **Progress Bar:** Shows completion percentage (0-100%)
- **Priority Indicator:** Color-coded dot showing task importance
- **Status Color:** Left border color indicates current status
#### Team and Collaboration
- **Assignee Avatars:** Profile pictures of assigned team members (up to 3 visible)
- **Labels:** Color-coded tags for categorization
- **Comment Count:** Number of comments and discussions
- **Attachment Count:** Number of files attached to the task
#### Timing Information
- **Due Dates:** When tasks are scheduled to complete
- Red text: Overdue tasks
- Orange text: Due today or within 3 days
- Gray text: Future due dates
- **Time Tracking:** Estimated vs. logged time when available
### Subtask Support
Tasks with subtasks include additional features:
#### Expanding Subtasks
- Click the "+X" button next to task names to expand subtasks
- Subtasks appear indented below the parent task
- Click "X" to collapse subtasks
#### Subtask Progress
- Parent task progress reflects completion of all subtasks
- Individual subtask progress is visible when expanded
- Subtask counts show total number of child tasks
## Advanced Features
### Real-time Updates
- Changes made by team members appear instantly
- Live collaboration with multiple users
- WebSocket connections ensure data synchronization
### Search and Filtering
- Use existing project search and filter capabilities
- Enhanced task management respects current filter settings
- Search results maintain grouping organization
### Responsive Design
The interface adapts to different screen sizes:
#### Desktop (Large Screens)
- Full feature set with all metadata visible
- Optimal drag-and-drop experience
- Multi-column layouts where appropriate
#### Tablet (Medium Screens)
- Condensed but functional interface
- Touch-friendly interactions
- Simplified metadata display
#### Mobile (Small Screens)
- Stacked layout for easy navigation
- Large touch targets for selections
- Essential information prioritized
## Best Practices
### Organizing Your Tasks
1. **Choose the Right Grouping:** Select the grouping method that best fits your workflow
2. **Use Labels Consistently:** Apply meaningful labels for better categorization
3. **Keep Groups Balanced:** Avoid having too many tasks in a single group
4. **Regular Maintenance:** Review and update task organization periodically
### Collaboration Tips
1. **Clear Task Names:** Use descriptive titles that everyone understands
2. **Proper Assignment:** Assign tasks to appropriate team members
3. **Progress Updates:** Keep progress percentages current for accurate project tracking
4. **Use Comments:** Communicate about tasks using the comment system
### Productivity Techniques
1. **Batch Similar Operations:** Use bulk actions for efficiency
2. **Prioritize Effectively:** Use priority grouping during planning phases
3. **Track Progress:** Monitor completion rates using group progress indicators
4. **Plan Ahead:** Use due dates and time estimates for better scheduling
## Keyboard Shortcuts
### Navigation
- **Tab:** Move focus between elements
- **Enter:** Activate focused button or link
- **Esc:** Close open dialogs or clear selections
### Selection
- **Space:** Select/deselect focused task
- **Shift + Click:** Range selection
- **Ctrl + Click:** Multi-selection (Cmd + Click on Mac)
### Actions
- **Delete:** Remove selected tasks (with confirmation)
- **Ctrl + A:** Select all visible tasks (Cmd + A on Mac)
## Troubleshooting
### Common Issues
#### Tasks Not Moving Between Groups
- Ensure you have edit permissions for the tasks
- Check that you're dragging from the drag handle (⋮⋮ icon)
- Verify the target group allows the task type
#### Bulk Actions Not Working
- Confirm tasks are actually selected (checkboxes checked)
- Ensure you have appropriate permissions
- Check that the action is available for your current grouping
#### Missing Task Information
- Some metadata may be hidden on smaller screens
- Try expanding to full screen or using desktop view
- Check that task has the required information (assignees, labels, etc.)
### Performance Tips
- For projects with hundreds of tasks, consider using filters to reduce visible tasks
- Collapse groups you're not actively working with
- Clear selections when not performing bulk operations
## Getting Help
- Contact your workspace administrator for permission-related issues
- Check the main WorkLenz documentation for general task management help
- Report bugs or feature requests through your organization's support channels
## What's New
This enhanced task management system builds on WorkLenz's solid foundation while adding:
- Modern drag-and-drop interfaces
- Flexible grouping options
- Powerful bulk operation capabilities
- Rich visual task displays
- Mobile-responsive design
- Improved accessibility features

View File

@@ -16,24 +16,45 @@ Recurring tasks are tasks that repeat automatically on a schedule you choose. Th
5. Save the task. It will now be created automatically based on your chosen schedule.
## Schedule Options
You can choose how often your task repeats. Here are the most common options:
You can choose how often your task repeats. Here are the available options:
- **Daily:** The task is created every day.
- **Weekly:** The task is created once a week. You can pick the day (e.g., every Monday).
- **Monthly:** The task is created once a month. You can pick the date (e.g., the 1st of every month).
- **Weekdays:** The task is created every Monday to Friday.
- **Custom:** Set your own schedule, such as every 2 days, every 3 weeks, or only on certain days.
- **Weekly:** The task is created once a week. You can pick one or more days (e.g., every Monday and Thursday).
- **Monthly:** The task is created once a month. You have two options:
- **On a specific date:** Choose a date from 1 to 28 (limited to 28 to ensure consistency across all months)
- **On a specific day:** Choose a week (first, second, third, fourth, or last) and a day of the week
- **Every X Days:** The task is created every specified number of days (e.g., every 3 days)
- **Every X Weeks:** The task is created every specified number of weeks (e.g., every 2 weeks)
- **Every X Months:** The task is created every specified number of months (e.g., every 3 months)
### Examples
- "Send team update" every Friday (weekly)
- "Submit expense report" on the 1st of each month (monthly)
- "Submit expense report" on the 15th of each month (monthly, specific date)
- "Monthly team meeting" on the first Monday of each month (monthly, specific day)
- "Check backups" every day (daily)
- "Review project status" every Monday and Thursday (custom)
- "Review project status" every Monday and Thursday (weekly, multiple days)
- "Quarterly report" every 3 months (every X months)
## Future Task Creation
The system automatically creates tasks up to a certain point in the future to ensure timely scheduling:
- **Daily Tasks:** Created up to 7 days in advance
- **Weekly Tasks:** Created up to 2 weeks in advance
- **Monthly Tasks:** Created up to 2 months in advance
- **Every X Days/Weeks/Months:** Created up to 2 intervals in advance
This ensures that:
- You always have upcoming tasks visible in your schedule
- Tasks are created at appropriate intervals
- The system maintains a reasonable number of future tasks
## Tips
- You can edit or stop a recurring task at any time.
- Assign team members and labels to recurring tasks for better organization.
- Check your task list regularly to see newly created recurring tasks.
- For monthly tasks, dates are limited to 1-28 to ensure the task occurs on the same date every month.
- Tasks are created automatically within the future limit window - you don't need to manually create them.
- If you need to see tasks further in the future, they will be created automatically as the current tasks are completed.
## Need Help?
If you have questions or need help setting up recurring tasks, contact your workspace admin or support team.

View File

@@ -17,6 +17,51 @@ The recurring tasks cron job automates the creation of tasks based on predefined
3. Checks if a task for the next occurrence already exists.
4. Creates a new task if it does not exist and the next occurrence is within the allowed future window.
## Future Limit Logic
The system implements different future limits based on the schedule type to maintain an appropriate number of future tasks:
```typescript
const FUTURE_LIMITS = {
daily: moment.duration(7, 'days'),
weekly: moment.duration(2, 'weeks'),
monthly: moment.duration(2, 'months'),
every_x_days: (interval: number) => moment.duration(interval * 2, 'days'),
every_x_weeks: (interval: number) => moment.duration(interval * 2, 'weeks'),
every_x_months: (interval: number) => moment.duration(interval * 2, 'months')
};
```
### Implementation Details
- **Base Calculation:**
```typescript
const futureLimit = moment(template.last_checked_at || template.created_at)
.add(getFutureLimit(schedule.schedule_type, schedule.interval), 'days');
```
- **Task Creation Rules:**
1. Only create tasks if the next occurrence is before the future limit
2. Skip creation if a task already exists for that date
3. Update `last_checked_at` after processing
- **Benefits:**
- Prevents excessive task creation
- Maintains system performance
- Ensures timely task visibility
- Allows for schedule modifications
## Date Handling
- **Monthly Tasks:**
- Dates are limited to 1-28 to ensure consistency across all months
- This prevents issues with months having different numbers of days
- No special handling needed for February or months with 30/31 days
- **Weekly Tasks:**
- Supports multiple days of the week (0-6, where 0 is Sunday)
- Tasks are created for each selected day
- **Interval-based Tasks:**
- Every X days/weeks/months from the last task's end date
- Minimum interval is 1 day/week/month
- No maximum limit, but tasks are only created up to the future limit
## Database Interactions
- **Templates and Schedules:**
- Templates are stored in `task_recurring_templates`.
@@ -27,6 +72,7 @@ The recurring tasks cron job automates the creation of tasks based on predefined
- Assigns team members and labels by calling appropriate functions/controllers.
- **State Tracking:**
- Updates `last_checked_at` and `last_created_task_end_date` in the schedule after processing.
- Maintains future limits based on schedule type.
## Task Creation Process
1. **Fetch Templates:** Retrieve all templates and their associated schedules.
@@ -41,10 +87,12 @@ The recurring tasks cron job automates the creation of tasks based on predefined
- **Cron Expression:** Modify the `TIME` constant in the code to change the schedule.
- **Task Template Structure:** Extend the template or schedule interfaces to support additional fields.
- **Task Creation Logic:** Customize the task creation process or add new assignment/labeling logic as needed.
- **Future Window:** Adjust the future limits by modifying the `FUTURE_LIMITS` configuration.
## Error Handling
- Errors are logged using the `log_error` utility.
- The job continues processing other templates even if one fails.
- Failed task creations are not retried automatically.
## References
- Source: `src/cron_jobs/recurring-tasks.ts`

6
package-lock.json generated Normal file
View File

@@ -0,0 +1,6 @@
{
"name": "worklenz",
"lockfileVersion": 3,
"requires": true,
"packages": {}
}

View File

@@ -73,8 +73,8 @@ cat > worklenz-backend/.env << EOL
NODE_ENV=production
PORT=3000
SESSION_NAME=worklenz.sid
SESSION_SECRET=change_me_in_production
COOKIE_SECRET=change_me_in_production
SESSION_SECRET=$(openssl rand -base64 48)
COOKIE_SECRET=$(openssl rand -base64 48)
# CORS
SOCKET_IO_CORS=${FRONTEND_URL}
@@ -123,7 +123,7 @@ SLACK_WEBHOOK=
COMMIT_BUILD_IMMEDIATELY=true
# JWT Secret
JWT_SECRET=change_me_in_production
JWT_SECRET=$(openssl rand -base64 48)
EOL
echo "Environment configuration updated for ${HOSTNAME} with" $([ "$USE_SSL" = "true" ] && echo "HTTPS/WSS" || echo "HTTP/WS")
@@ -138,4 +138,4 @@ echo "Frontend URL: ${FRONTEND_URL}"
echo "API URL: ${HTTP_PREFIX}${HOSTNAME}:3000"
echo "Socket URL: ${WS_PREFIX}${HOSTNAME}:3000"
echo "MinIO Dashboard URL: ${MINIO_DASHBOARD_URL}"
echo "CORS is configured to allow requests from: ${FRONTEND_URL}"
echo "CORS is configured to allow requests from: ${FRONTEND_URL}"

View File

@@ -1,5 +1,9 @@
node_modules
npm-debug.log
build
.scannerwork
coverage
.dockerignore
.git
*.md
tests

View File

@@ -78,4 +78,8 @@ GOOGLE_CAPTCHA_SECRET_KEY=your_captcha_secret_key
GOOGLE_CAPTCHA_PASS_SCORE=0.8
# Email Cronjobs
ENABLE_EMAIL_CRONJOBS=true
ENABLE_EMAIL_CRONJOBS=true
# RECURRING_JOBS
ENABLE_RECURRING_JOBS=true
RECURRING_JOBS_INTERVAL="0 11 */1 * 1-5"

View File

@@ -20,9 +20,6 @@ coverage
# nyc test coverage
.nyc_output
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components

View File

@@ -1,26 +1,39 @@
# Use the official Node.js 20 image as a base
FROM node:20
# --- Stage 1: Build ---
FROM node:20-slim AS builder
ARG RELEASE_VERSION
RUN apt-get update && apt-get install -y \
python3 \
make \
g++ \
curl \
postgresql-server-dev-all \
&& rm -rf /var/lib/apt/lists/*
# Create and set the working directory
WORKDIR /usr/src/app
# Install global dependencies
RUN npm install -g ts-node typescript grunt grunt-cli
# Copy package.json and package-lock.json (if available)
COPY package*.json ./
# Install app dependencies
RUN npm ci
# Copy the rest of the application code
COPY . .
# Run the build script to compile TypeScript to JavaScript
RUN npm run build
# Expose the port the app runs on
EXPOSE 3000
RUN echo "$RELEASE_VERSION" > release
# --- Stage 2: Production Image ---
FROM node:20-slim
RUN apt-get update && apt-get install -y libpq5 && rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY --from=builder /usr/src/app/package*.json ./
COPY --from=builder /usr/src/app/build ./build
COPY --from=builder /usr/src/app/node_modules ./node_modules
COPY --from=builder /usr/src/app/release ./release
COPY --from=builder /usr/src/app/worklenz-email-templates ./worklenz-email-templates
EXPOSE 3000
CMD ["node", "build/bin/www"]
# Start the application
CMD ["npm", "start"]

View File

@@ -1,131 +0,0 @@
module.exports = function (grunt) {
// Project configuration.
grunt.initConfig({
pkg: grunt.file.readJSON("package.json"),
clean: {
dist: "build"
},
compress: require("./grunt/grunt-compress"),
copy: {
main: {
files: [
{expand: true, cwd: "src", src: ["public/**"], dest: "build"},
{expand: true, cwd: "src", src: ["views/**"], dest: "build"},
{expand: true, cwd: "landing-page-assets", src: ["**"], dest: "build/public/assets"},
{expand: true, cwd: "src", src: ["shared/sample-data.json"], dest: "build", filter: "isFile"},
{expand: true, cwd: "src", src: ["shared/templates/**"], dest: "build", filter: "isFile"},
{expand: true, cwd: "src", src: ["shared/postgresql-error-codes.json"], dest: "build", filter: "isFile"},
]
},
packages: {
files: [
{expand: true, cwd: "", src: [".env"], dest: "build", filter: "isFile"},
{expand: true, cwd: "", src: [".gitignore"], dest: "build", filter: "isFile"},
{expand: true, cwd: "", src: ["release"], dest: "build", filter: "isFile"},
{expand: true, cwd: "", src: ["jest.config.js"], dest: "build", filter: "isFile"},
{expand: true, cwd: "", src: ["package.json"], dest: "build", filter: "isFile"},
{expand: true, cwd: "", src: ["package-lock.json"], dest: "build", filter: "isFile"},
{expand: true, cwd: "", src: ["common_modules/**"], dest: "build"}
]
}
},
sync: {
main: {
files: [
{cwd: "src", src: ["views/**", "public/**"], dest: "build/"}, // makes all src relative to cwd
],
verbose: true,
failOnError: true,
compareUsing: "md5"
}
},
uglify: {
all: {
files: [{
expand: true,
cwd: "build",
src: "**/*.js",
dest: "build"
}]
},
controllers: {
files: [{
expand: true,
cwd: "build",
src: "controllers/*.js",
dest: "build"
}]
},
routes: {
files: [{
expand: true,
cwd: "build",
src: "routes/**/*.js",
dest: "build"
}]
},
assets: {
files: [{
expand: true,
cwd: "build",
src: "public/assets/**/*.js",
dest: "build"
}]
}
},
shell: {
tsc: {
command: "tsc --build tsconfig.prod.json"
},
esbuild: {
// command: "esbuild `find src -type f -name '*.ts'` --platform=node --minify=false --target=esnext --format=cjs --tsconfig=tsconfig.prod.json --outdir=build"
command: "node esbuild && node cli/esbuild-patch"
},
tsc_dev: {
command: "tsc --build tsconfig.json"
},
swagger: {
command: "node ./cli/swagger"
},
inline_queries: {
command: "node ./cli/inline-queries"
}
},
watch: {
scripts: {
files: ["src/**/*.ts"],
tasks: ["shell:tsc_dev"],
options: {
debounceDelay: 250,
spawn: false,
}
},
other: {
files: ["src/**/*.pug", "landing-page-assets/**"],
tasks: ["sync"]
}
}
});
grunt.registerTask("clean", ["clean"]);
grunt.registerTask("copy", ["copy:main"]);
grunt.registerTask("swagger", ["shell:swagger"]);
grunt.registerTask("build:tsc", ["shell:tsc"]);
grunt.registerTask("build", ["clean", "shell:tsc", "copy:main", "compress"]);
grunt.registerTask("build:es", ["clean", "shell:esbuild", "copy:main", "uglify:assets", "compress"]);
grunt.registerTask("build:strict", ["clean", "shell:tsc", "copy:packages", "uglify:all", "copy:main", "compress"]);
grunt.registerTask("dev", ["clean", "copy:main", "shell:tsc_dev", "shell:inline_queries", "watch"]);
// Load the plugin that provides the "uglify" task.
grunt.loadNpmTasks("grunt-contrib-watch");
grunt.loadNpmTasks("grunt-contrib-clean");
grunt.loadNpmTasks("grunt-contrib-copy");
grunt.loadNpmTasks("grunt-contrib-uglify");
grunt.loadNpmTasks("grunt-contrib-compress");
grunt.loadNpmTasks("grunt-shell");
grunt.loadNpmTasks("grunt-sync");
// Default task(s).
grunt.registerTask("default", []);
};

View File

@@ -1,55 +0,0 @@
#!/bin/bash
set -e
# This script controls the order of SQL file execution during database initialization
echo "Starting database initialization..."
# Check if we have SQL files in expected locations
if [ -f "/docker-entrypoint-initdb.d/sql/0_extensions.sql" ]; then
SQL_DIR="/docker-entrypoint-initdb.d/sql"
echo "Using SQL files from sql/ subdirectory"
elif [ -f "/docker-entrypoint-initdb.d/0_extensions.sql" ]; then
# First time setup - move files to subdirectory
echo "Moving SQL files to sql/ subdirectory..."
mkdir -p /docker-entrypoint-initdb.d/sql
# Move all SQL files (except this script) to the subdirectory
for f in /docker-entrypoint-initdb.d/*.sql; do
if [ -f "$f" ]; then
cp "$f" /docker-entrypoint-initdb.d/sql/
echo "Copied $f to sql/ subdirectory"
fi
done
SQL_DIR="/docker-entrypoint-initdb.d/sql"
else
echo "SQL files not found in expected locations!"
exit 1
fi
# Execute SQL files in the correct order
echo "Executing 0_extensions.sql..."
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" -f "$SQL_DIR/0_extensions.sql"
echo "Executing 1_tables.sql..."
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" -f "$SQL_DIR/1_tables.sql"
echo "Executing indexes.sql..."
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" -f "$SQL_DIR/indexes.sql"
echo "Executing 4_functions.sql..."
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" -f "$SQL_DIR/4_functions.sql"
echo "Executing triggers.sql..."
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" -f "$SQL_DIR/triggers.sql"
echo "Executing 3_views.sql..."
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" -f "$SQL_DIR/3_views.sql"
echo "Executing 2_dml.sql..."
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" -f "$SQL_DIR/2_dml.sql"
echo "Executing 5_database_user.sql..."
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" -f "$SQL_DIR/5_database_user.sql"
echo "Database initialization completed successfully"

View File

@@ -0,0 +1,88 @@
#!/bin/bash
set -e
echo "Starting database initialization..."
SQL_DIR="/docker-entrypoint-initdb.d/sql"
MIGRATIONS_DIR="/docker-entrypoint-initdb.d/migrations"
BACKUP_DIR="/docker-entrypoint-initdb.d/pg_backups"
# --------------------------------------------
# 🗄️ STEP 1: Attempt to restore latest backup
# --------------------------------------------
if [ -d "$BACKUP_DIR" ]; then
LATEST_BACKUP=$(ls -t "$BACKUP_DIR"/*.sql 2>/dev/null | head -n 1)
else
LATEST_BACKUP=""
fi
if [ -f "$LATEST_BACKUP" ]; then
echo "🗄️ Found latest backup: $LATEST_BACKUP"
echo "⏳ Restoring from backup..."
psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" < "$LATEST_BACKUP"
echo "✅ Backup restoration complete. Skipping schema and migrations."
exit 0
else
echo " No valid backup found. Proceeding with base schema and migrations."
fi
# --------------------------------------------
# 🏗️ STEP 2: Continue with base schema setup
# --------------------------------------------
# Create migrations table if it doesn't exist
psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" -c "
CREATE TABLE IF NOT EXISTS schema_migrations (
version TEXT PRIMARY KEY,
applied_at TIMESTAMP DEFAULT now()
);
"
# List of base schema files to execute in order
BASE_SQL_FILES=(
"0_extensions.sql"
"1_tables.sql"
"indexes.sql"
"4_functions.sql"
"triggers.sql"
"3_views.sql"
"2_dml.sql"
"5_database_user.sql"
)
echo "Running base schema SQL files in order..."
for file in "${BASE_SQL_FILES[@]}"; do
full_path="$SQL_DIR/$file"
if [ -f "$full_path" ]; then
echo "Executing $file..."
psql -v ON_ERROR_STOP=1 -U "$POSTGRES_USER" -d "$POSTGRES_DB" -f "$full_path"
else
echo "WARNING: $file not found, skipping."
fi
done
echo "✅ Base schema SQL execution complete."
# --------------------------------------------
# 🚀 STEP 3: Apply SQL migrations
# --------------------------------------------
if [ -d "$MIGRATIONS_DIR" ] && compgen -G "$MIGRATIONS_DIR/*.sql" > /dev/null; then
echo "Applying migrations..."
for f in "$MIGRATIONS_DIR"/*.sql; do
version=$(basename "$f")
if ! psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" -tAc "SELECT 1 FROM schema_migrations WHERE version = '$version'" | grep -q 1; then
echo "Applying migration: $version"
psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" -f "$f"
psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" -c "INSERT INTO schema_migrations (version) VALUES ('$version');"
else
echo "Skipping already applied migration: $version"
fi
done
else
echo "No migration files found or directory is empty, skipping migrations."
fi
echo "🎉 Database initialization completed successfully."

View File

@@ -0,0 +1,135 @@
-- Performance indexes for optimized tasks queries
-- Migration: 20250115000000-performance-indexes.sql
-- Composite index for main task filtering
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_tasks_project_archived_parent
ON tasks(project_id, archived, parent_task_id)
WHERE archived = FALSE;
-- Index for status joins
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_tasks_status_project
ON tasks(status_id, project_id)
WHERE archived = FALSE;
-- Index for assignees lookup
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_tasks_assignees_task_member
ON tasks_assignees(task_id, team_member_id);
-- Index for phase lookup
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_task_phase_task_phase
ON task_phase(task_id, phase_id);
-- Index for subtask counting
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_tasks_parent_archived
ON tasks(parent_task_id, archived)
WHERE parent_task_id IS NOT NULL AND archived = FALSE;
-- Index for labels
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_task_labels_task_label
ON task_labels(task_id, label_id);
-- Index for comments count
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_task_comments_task
ON task_comments(task_id);
-- Index for attachments count
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_task_attachments_task
ON task_attachments(task_id);
-- Index for work log aggregation
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_task_work_log_task
ON task_work_log(task_id);
-- Index for subscribers check
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_task_subscribers_task
ON task_subscribers(task_id);
-- Index for dependencies check
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_task_dependencies_task
ON task_dependencies(task_id);
-- Index for timers lookup
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_task_timers_task_user
ON task_timers(task_id, user_id);
-- Index for custom columns
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_cc_column_values_task
ON cc_column_values(task_id);
-- Index for team member info view optimization
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_team_members_team_user
ON team_members(team_id, user_id)
WHERE active = TRUE;
-- Index for notification settings
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_notification_settings_user_team
ON notification_settings(user_id, team_id);
-- Index for task status categories
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_task_statuses_category
ON task_statuses(category_id, project_id);
-- Index for project phases
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_project_phases_project_sort
ON project_phases(project_id, sort_index);
-- Index for task priorities
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_task_priorities_value
ON task_priorities(value);
-- Index for team labels
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_team_labels_team
ON team_labels(team_id);
-- NEW INDEXES FOR PERFORMANCE OPTIMIZATION --
-- Composite index for task main query optimization (covers most WHERE conditions)
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_tasks_performance_main
ON tasks(project_id, archived, parent_task_id, status_id, priority_id)
WHERE archived = FALSE;
-- Index for sorting by sort_order with project filter
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_tasks_project_sort_order
ON tasks(project_id, sort_order)
WHERE archived = FALSE;
-- Index for email_invitations to optimize team_member_info_view
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_email_invitations_team_member
ON email_invitations(team_member_id);
-- Covering index for task status with category information
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_task_statuses_covering
ON task_statuses(id, category_id, project_id);
-- Index for task aggregation queries (parent task progress calculation)
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_tasks_parent_status_archived
ON tasks(parent_task_id, status_id, archived)
WHERE archived = FALSE;
-- Index for project team member filtering
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_team_members_project_lookup
ON team_members(team_id, active, user_id)
WHERE active = TRUE;
-- Covering index for tasks with frequently accessed columns
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_tasks_covering_main
ON tasks(id, project_id, archived, parent_task_id, status_id, priority_id, sort_order, name)
WHERE archived = FALSE;
-- Index for task search functionality
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_tasks_name_search
ON tasks USING gin(to_tsvector('english', name))
WHERE archived = FALSE;
-- Index for date-based filtering (if used)
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_tasks_dates
ON tasks(project_id, start_date, end_date)
WHERE archived = FALSE;
-- Index for task timers with user filtering
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_task_timers_user_task
ON task_timers(user_id, task_id);
-- Index for sys_task_status_categories lookups
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_sys_task_status_categories_covering
ON sys_task_status_categories(id, color_code, color_code_dark, is_done, is_doing, is_todo);

View File

@@ -0,0 +1,143 @@
-- Fix window function error in task sort optimized functions
-- Error: window functions are not allowed in UPDATE
-- Replace the optimized sort functions to avoid CTE usage in UPDATE statements
CREATE OR REPLACE FUNCTION handle_task_list_sort_between_groups_optimized(_from_index integer, _to_index integer, _task_id uuid, _project_id uuid, _batch_size integer DEFAULT 100) RETURNS void
LANGUAGE plpgsql
AS
$$
DECLARE
_offset INT := 0;
_affected_rows INT;
BEGIN
-- PERFORMANCE OPTIMIZATION: Use direct updates without CTE in UPDATE
IF (_to_index = -1)
THEN
_to_index = COALESCE((SELECT MAX(sort_order) + 1 FROM tasks WHERE project_id = _project_id), 0);
END IF;
-- PERFORMANCE OPTIMIZATION: Batch updates for large datasets
IF _to_index > _from_index
THEN
LOOP
UPDATE tasks
SET sort_order = sort_order - 1
WHERE project_id = _project_id
AND sort_order > _from_index
AND sort_order < _to_index
AND sort_order > _offset
AND sort_order <= _offset + _batch_size;
GET DIAGNOSTICS _affected_rows = ROW_COUNT;
EXIT WHEN _affected_rows = 0;
_offset := _offset + _batch_size;
END LOOP;
UPDATE tasks SET sort_order = _to_index - 1 WHERE id = _task_id AND project_id = _project_id;
END IF;
IF _to_index < _from_index
THEN
_offset := 0;
LOOP
UPDATE tasks
SET sort_order = sort_order + 1
WHERE project_id = _project_id
AND sort_order > _to_index
AND sort_order < _from_index
AND sort_order > _offset
AND sort_order <= _offset + _batch_size;
GET DIAGNOSTICS _affected_rows = ROW_COUNT;
EXIT WHEN _affected_rows = 0;
_offset := _offset + _batch_size;
END LOOP;
UPDATE tasks SET sort_order = _to_index + 1 WHERE id = _task_id AND project_id = _project_id;
END IF;
END
$$;
-- Replace the second optimized sort function
CREATE OR REPLACE FUNCTION handle_task_list_sort_inside_group_optimized(_from_index integer, _to_index integer, _task_id uuid, _project_id uuid, _batch_size integer DEFAULT 100) RETURNS void
LANGUAGE plpgsql
AS
$$
DECLARE
_offset INT := 0;
_affected_rows INT;
BEGIN
-- PERFORMANCE OPTIMIZATION: Batch updates for large datasets without CTE in UPDATE
IF _to_index > _from_index
THEN
LOOP
UPDATE tasks
SET sort_order = sort_order - 1
WHERE project_id = _project_id
AND sort_order > _from_index
AND sort_order <= _to_index
AND sort_order > _offset
AND sort_order <= _offset + _batch_size;
GET DIAGNOSTICS _affected_rows = ROW_COUNT;
EXIT WHEN _affected_rows = 0;
_offset := _offset + _batch_size;
END LOOP;
END IF;
IF _to_index < _from_index
THEN
_offset := 0;
LOOP
UPDATE tasks
SET sort_order = sort_order + 1
WHERE project_id = _project_id
AND sort_order >= _to_index
AND sort_order < _from_index
AND sort_order > _offset
AND sort_order <= _offset + _batch_size;
GET DIAGNOSTICS _affected_rows = ROW_COUNT;
EXIT WHEN _affected_rows = 0;
_offset := _offset + _batch_size;
END LOOP;
END IF;
UPDATE tasks SET sort_order = _to_index WHERE id = _task_id AND project_id = _project_id;
END
$$;
-- Add simple bulk update function as alternative
CREATE OR REPLACE FUNCTION update_task_sort_orders_bulk(_updates json) RETURNS void
LANGUAGE plpgsql
AS
$$
DECLARE
_update_record RECORD;
BEGIN
-- Simple approach: update each task's sort_order from the provided array
FOR _update_record IN
SELECT
(item->>'task_id')::uuid as task_id,
(item->>'sort_order')::int as sort_order,
(item->>'status_id')::uuid as status_id,
(item->>'priority_id')::uuid as priority_id,
(item->>'phase_id')::uuid as phase_id
FROM json_array_elements(_updates) as item
LOOP
UPDATE tasks
SET
sort_order = _update_record.sort_order,
status_id = COALESCE(_update_record.status_id, status_id),
priority_id = COALESCE(_update_record.priority_id, priority_id)
WHERE id = _update_record.task_id;
-- Handle phase updates separately since it's in a different table
IF _update_record.phase_id IS NOT NULL THEN
INSERT INTO task_phase (task_id, phase_id)
VALUES (_update_record.task_id, _update_record.phase_id)
ON CONFLICT (task_id) DO UPDATE SET phase_id = _update_record.phase_id;
END IF;
END LOOP;
END
$$;

View File

@@ -0,0 +1,85 @@
-- Create holiday types table
CREATE TABLE IF NOT EXISTS holiday_types (
id UUID DEFAULT uuid_generate_v4() NOT NULL,
name TEXT NOT NULL,
description TEXT,
color_code WL_HEX_COLOR NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL
);
ALTER TABLE holiday_types
ADD CONSTRAINT holiday_types_pk
PRIMARY KEY (id);
-- Insert default holiday types
INSERT INTO holiday_types (name, description, color_code) VALUES
('Public Holiday', 'Official public holidays', '#f37070'),
('Company Holiday', 'Company-specific holidays', '#70a6f3'),
('Personal Holiday', 'Personal or optional holidays', '#75c997'),
('Religious Holiday', 'Religious observances', '#fbc84c')
ON CONFLICT DO NOTHING;
-- Create organization holidays table
CREATE TABLE IF NOT EXISTS organization_holidays (
id UUID DEFAULT uuid_generate_v4() NOT NULL,
organization_id UUID NOT NULL,
holiday_type_id UUID NOT NULL,
name TEXT NOT NULL,
description TEXT,
date DATE NOT NULL,
is_recurring BOOLEAN DEFAULT FALSE NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL
);
ALTER TABLE organization_holidays
ADD CONSTRAINT organization_holidays_pk
PRIMARY KEY (id);
ALTER TABLE organization_holidays
ADD CONSTRAINT organization_holidays_organization_id_fk
FOREIGN KEY (organization_id) REFERENCES organizations
ON DELETE CASCADE;
ALTER TABLE organization_holidays
ADD CONSTRAINT organization_holidays_holiday_type_id_fk
FOREIGN KEY (holiday_type_id) REFERENCES holiday_types
ON DELETE RESTRICT;
-- Add unique constraint to prevent duplicate holidays on the same date for an organization
ALTER TABLE organization_holidays
ADD CONSTRAINT organization_holidays_organization_date_unique
UNIQUE (organization_id, date);
-- Create country holidays table for predefined holidays
CREATE TABLE IF NOT EXISTS country_holidays (
id UUID DEFAULT uuid_generate_v4() NOT NULL,
country_code CHAR(2) NOT NULL,
name TEXT NOT NULL,
description TEXT,
date DATE NOT NULL,
is_recurring BOOLEAN DEFAULT TRUE NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL
);
ALTER TABLE country_holidays
ADD CONSTRAINT country_holidays_pk
PRIMARY KEY (id);
ALTER TABLE country_holidays
ADD CONSTRAINT country_holidays_country_code_fk
FOREIGN KEY (country_code) REFERENCES countries(code)
ON DELETE CASCADE;
-- Add unique constraint to prevent duplicate holidays for the same country, name, and date
ALTER TABLE country_holidays
ADD CONSTRAINT country_holidays_country_name_date_unique
UNIQUE (country_code, name, date);
-- Create indexes for better performance
CREATE INDEX IF NOT EXISTS idx_organization_holidays_organization_id ON organization_holidays(organization_id);
CREATE INDEX IF NOT EXISTS idx_organization_holidays_date ON organization_holidays(date);
CREATE INDEX IF NOT EXISTS idx_country_holidays_country_code ON country_holidays(country_code);
CREATE INDEX IF NOT EXISTS idx_country_holidays_date ON country_holidays(date);

View File

@@ -0,0 +1,60 @@
-- ================================================================
-- Sri Lankan Holidays Migration
-- ================================================================
-- This migration populates Sri Lankan holidays from verified sources
--
-- SOURCES & VERIFICATION:
-- - 2025 data: Verified from official government sources
-- - Fixed holidays: Independence Day, May Day, Christmas (all years)
-- - Variable holidays: Added only when officially verified
--
-- MAINTENANCE:
-- - Use scripts/update-sri-lankan-holidays.js for updates
-- - See docs/sri-lankan-holiday-update-process.md for process
-- ================================================================
-- Insert fixed holidays for multiple years (these never change dates)
DO $$
DECLARE
current_year INT;
BEGIN
FOR current_year IN 2020..2050 LOOP
INSERT INTO country_holidays (country_code, name, description, date, is_recurring)
VALUES
('LK', 'Independence Day', 'Commemorates the independence of Sri Lanka from British rule in 1948',
make_date(current_year, 2, 4), true),
('LK', 'May Day', 'International Workers'' Day',
make_date(current_year, 5, 1), true),
('LK', 'Christmas Day', 'Christian celebration of the birth of Jesus Christ',
make_date(current_year, 12, 25), true)
ON CONFLICT (country_code, name, date) DO NOTHING;
END LOOP;
END $$;
-- Insert specific holidays for years 2025-2028 (from our JSON data)
-- 2025 holidays
INSERT INTO country_holidays (country_code, name, description, date, is_recurring)
VALUES
('LK', 'Duruthu Full Moon Poya Day', 'Commemorates the first visit of Buddha to Sri Lanka', '2025-01-13', false),
('LK', 'Navam Full Moon Poya Day', 'Commemorates the appointment of Sariputta and Moggallana as Buddha''s chief disciples', '2025-02-12', false),
('LK', 'Medin Full Moon Poya Day', 'Commemorates Buddha''s first visit to his father''s palace after enlightenment', '2025-03-14', false),
('LK', 'Eid al-Fitr', 'Festival marking the end of Ramadan', '2025-03-31', false),
('LK', 'Bak Full Moon Poya Day', 'Commemorates Buddha''s second visit to Sri Lanka', '2025-04-12', false),
('LK', 'Good Friday', 'Christian commemoration of the crucifixion of Jesus Christ', '2025-04-18', false),
('LK', 'Vesak Full Moon Poya Day', 'Most sacred day for Buddhists - commemorates birth, enlightenment and passing of Buddha', '2025-05-12', false),
('LK', 'Day after Vesak Full Moon Poya Day', 'Additional day for Vesak celebrations', '2025-05-13', false),
('LK', 'Eid al-Adha', 'Islamic festival of sacrifice', '2025-06-07', false),
('LK', 'Poson Full Moon Poya Day', 'Commemorates the introduction of Buddhism to Sri Lanka by Arahat Mahinda', '2025-06-11', false),
('LK', 'Esala Full Moon Poya Day', 'Commemorates Buddha''s first sermon and the arrival of the Sacred Tooth Relic', '2025-07-10', false),
('LK', 'Nikini Full Moon Poya Day', 'Commemorates the first Buddhist council', '2025-08-09', false),
('LK', 'Binara Full Moon Poya Day', 'Commemorates Buddha''s visit to heaven to preach to his mother', '2025-09-07', false),
('LK', 'Vap Full Moon Poya Day', 'Marks the end of Buddhist Lent and Buddha''s return from heaven', '2025-10-07', false),
('LK', 'Deepavali', 'Hindu Festival of Lights', '2025-10-20', false),
('LK', 'Il Full Moon Poya Day', 'Commemorates Buddha''s ordination of sixty disciples', '2025-11-05', false),
('LK', 'Unduvap Full Moon Poya Day', 'Commemorates the arrival of Sanghamitta Theri with the Sacred Bo sapling', '2025-12-04', false)
ON CONFLICT (country_code, name, date) DO NOTHING;
-- NOTE: Data for 2026+ should be added only after verification from official sources
-- Use the holiday management script to generate templates for new years:
-- node update-sri-lankan-holidays.js --poya-template YYYY

View File

@@ -145,7 +145,7 @@ BEGIN
SET progress_value = NULL,
progress_mode = NULL
WHERE project_id = _project_id
AND progress_mode = _old_mode;
AND progress_mode::text::progress_mode_type = _old_mode;
END IF;
RETURN NEW;

View File

@@ -0,0 +1,63 @@
-- Create organization holiday settings table
CREATE TABLE IF NOT EXISTS organization_holiday_settings (
id UUID DEFAULT uuid_generate_v4() NOT NULL,
organization_id UUID NOT NULL,
country_code CHAR(2),
state_code TEXT,
auto_sync_holidays BOOLEAN DEFAULT TRUE NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL
);
ALTER TABLE organization_holiday_settings
ADD CONSTRAINT organization_holiday_settings_pk
PRIMARY KEY (id);
ALTER TABLE organization_holiday_settings
ADD CONSTRAINT organization_holiday_settings_organization_id_fk
FOREIGN KEY (organization_id) REFERENCES organizations
ON DELETE CASCADE;
ALTER TABLE organization_holiday_settings
ADD CONSTRAINT organization_holiday_settings_country_code_fk
FOREIGN KEY (country_code) REFERENCES countries(code)
ON DELETE SET NULL;
-- Ensure one settings record per organization
ALTER TABLE organization_holiday_settings
ADD CONSTRAINT organization_holiday_settings_organization_unique
UNIQUE (organization_id);
-- Create index for better performance
CREATE INDEX IF NOT EXISTS idx_organization_holiday_settings_organization_id ON organization_holiday_settings(organization_id);
-- Add state holidays table for more granular holiday data
CREATE TABLE IF NOT EXISTS state_holidays (
id UUID DEFAULT uuid_generate_v4() NOT NULL,
country_code CHAR(2) NOT NULL,
state_code TEXT NOT NULL,
name TEXT NOT NULL,
description TEXT,
date DATE NOT NULL,
is_recurring BOOLEAN DEFAULT TRUE NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL
);
ALTER TABLE state_holidays
ADD CONSTRAINT state_holidays_pk
PRIMARY KEY (id);
ALTER TABLE state_holidays
ADD CONSTRAINT state_holidays_country_code_fk
FOREIGN KEY (country_code) REFERENCES countries(code)
ON DELETE CASCADE;
-- Add unique constraint to prevent duplicate holidays for the same state, name, and date
ALTER TABLE state_holidays
ADD CONSTRAINT state_holidays_state_name_date_unique
UNIQUE (country_code, state_code, name, date);
-- Create indexes for better performance
CREATE INDEX IF NOT EXISTS idx_state_holidays_country_state ON state_holidays(country_code, state_code);
CREATE INDEX IF NOT EXISTS idx_state_holidays_date ON state_holidays(date);

View File

@@ -0,0 +1,300 @@
-- Fix Duplicate Sort Orders Script
-- This script detects and fixes duplicate sort order values that break task ordering
-- 1. DETECTION QUERIES - Run these first to see the scope of the problem
-- Check for duplicates in main sort_order column
SELECT
project_id,
sort_order,
COUNT(*) as duplicate_count,
STRING_AGG(id::text, ', ') as task_ids
FROM tasks
WHERE project_id IS NOT NULL
GROUP BY project_id, sort_order
HAVING COUNT(*) > 1
ORDER BY project_id, sort_order;
-- Check for duplicates in status_sort_order
SELECT
project_id,
status_sort_order,
COUNT(*) as duplicate_count,
STRING_AGG(id::text, ', ') as task_ids
FROM tasks
WHERE project_id IS NOT NULL
GROUP BY project_id, status_sort_order
HAVING COUNT(*) > 1
ORDER BY project_id, status_sort_order;
-- Check for duplicates in priority_sort_order
SELECT
project_id,
priority_sort_order,
COUNT(*) as duplicate_count,
STRING_AGG(id::text, ', ') as task_ids
FROM tasks
WHERE project_id IS NOT NULL
GROUP BY project_id, priority_sort_order
HAVING COUNT(*) > 1
ORDER BY project_id, priority_sort_order;
-- Check for duplicates in phase_sort_order
SELECT
project_id,
phase_sort_order,
COUNT(*) as duplicate_count,
STRING_AGG(id::text, ', ') as task_ids
FROM tasks
WHERE project_id IS NOT NULL
GROUP BY project_id, phase_sort_order
HAVING COUNT(*) > 1
ORDER BY project_id, phase_sort_order;
-- Note: member_sort_order removed - no longer used
-- 2. CLEANUP FUNCTIONS
-- Fix duplicates in main sort_order column
CREATE OR REPLACE FUNCTION fix_sort_order_duplicates() RETURNS void
LANGUAGE plpgsql
AS
$$
DECLARE
_project RECORD;
_task RECORD;
_counter INTEGER;
BEGIN
-- For each project, reassign sort_order values to ensure uniqueness
FOR _project IN
SELECT DISTINCT project_id
FROM tasks
WHERE project_id IS NOT NULL
LOOP
_counter := 0;
-- Reassign sort_order values sequentially for this project
FOR _task IN
SELECT id
FROM tasks
WHERE project_id = _project.project_id
ORDER BY sort_order, created_at
LOOP
UPDATE tasks
SET sort_order = _counter
WHERE id = _task.id;
_counter := _counter + 1;
END LOOP;
END LOOP;
RAISE NOTICE 'Fixed sort_order duplicates for all projects';
END
$$;
-- Fix duplicates in status_sort_order column
CREATE OR REPLACE FUNCTION fix_status_sort_order_duplicates() RETURNS void
LANGUAGE plpgsql
AS
$$
DECLARE
_project RECORD;
_task RECORD;
_counter INTEGER;
BEGIN
FOR _project IN
SELECT DISTINCT project_id
FROM tasks
WHERE project_id IS NOT NULL
LOOP
_counter := 0;
FOR _task IN
SELECT id
FROM tasks
WHERE project_id = _project.project_id
ORDER BY status_sort_order, created_at
LOOP
UPDATE tasks
SET status_sort_order = _counter
WHERE id = _task.id;
_counter := _counter + 1;
END LOOP;
END LOOP;
RAISE NOTICE 'Fixed status_sort_order duplicates for all projects';
END
$$;
-- Fix duplicates in priority_sort_order column
CREATE OR REPLACE FUNCTION fix_priority_sort_order_duplicates() RETURNS void
LANGUAGE plpgsql
AS
$$
DECLARE
_project RECORD;
_task RECORD;
_counter INTEGER;
BEGIN
FOR _project IN
SELECT DISTINCT project_id
FROM tasks
WHERE project_id IS NOT NULL
LOOP
_counter := 0;
FOR _task IN
SELECT id
FROM tasks
WHERE project_id = _project.project_id
ORDER BY priority_sort_order, created_at
LOOP
UPDATE tasks
SET priority_sort_order = _counter
WHERE id = _task.id;
_counter := _counter + 1;
END LOOP;
END LOOP;
RAISE NOTICE 'Fixed priority_sort_order duplicates for all projects';
END
$$;
-- Fix duplicates in phase_sort_order column
CREATE OR REPLACE FUNCTION fix_phase_sort_order_duplicates() RETURNS void
LANGUAGE plpgsql
AS
$$
DECLARE
_project RECORD;
_task RECORD;
_counter INTEGER;
BEGIN
FOR _project IN
SELECT DISTINCT project_id
FROM tasks
WHERE project_id IS NOT NULL
LOOP
_counter := 0;
FOR _task IN
SELECT id
FROM tasks
WHERE project_id = _project.project_id
ORDER BY phase_sort_order, created_at
LOOP
UPDATE tasks
SET phase_sort_order = _counter
WHERE id = _task.id;
_counter := _counter + 1;
END LOOP;
END LOOP;
RAISE NOTICE 'Fixed phase_sort_order duplicates for all projects';
END
$$;
-- Note: fix_member_sort_order_duplicates() removed - no longer needed
-- Master function to fix all sort order duplicates
CREATE OR REPLACE FUNCTION fix_all_duplicate_sort_orders() RETURNS void
LANGUAGE plpgsql
AS
$$
BEGIN
RAISE NOTICE 'Starting sort order cleanup for all columns...';
PERFORM fix_sort_order_duplicates();
PERFORM fix_status_sort_order_duplicates();
PERFORM fix_priority_sort_order_duplicates();
PERFORM fix_phase_sort_order_duplicates();
RAISE NOTICE 'Completed sort order cleanup for all columns';
END
$$;
-- 3. VERIFICATION FUNCTION
-- Verify that duplicates have been fixed
CREATE OR REPLACE FUNCTION verify_sort_order_integrity() RETURNS TABLE(
column_name text,
project_id uuid,
duplicate_count bigint,
status text
)
LANGUAGE plpgsql
AS
$$
BEGIN
-- Check sort_order duplicates
RETURN QUERY
SELECT
'sort_order'::text as column_name,
t.project_id,
COUNT(*) as duplicate_count,
CASE WHEN COUNT(*) > 1 THEN 'DUPLICATES FOUND' ELSE 'OK' END as status
FROM tasks t
WHERE t.project_id IS NOT NULL
GROUP BY t.project_id, t.sort_order
HAVING COUNT(*) > 1;
-- Check status_sort_order duplicates
RETURN QUERY
SELECT
'status_sort_order'::text as column_name,
t.project_id,
COUNT(*) as duplicate_count,
CASE WHEN COUNT(*) > 1 THEN 'DUPLICATES FOUND' ELSE 'OK' END as status
FROM tasks t
WHERE t.project_id IS NOT NULL
GROUP BY t.project_id, t.status_sort_order
HAVING COUNT(*) > 1;
-- Check priority_sort_order duplicates
RETURN QUERY
SELECT
'priority_sort_order'::text as column_name,
t.project_id,
COUNT(*) as duplicate_count,
CASE WHEN COUNT(*) > 1 THEN 'DUPLICATES FOUND' ELSE 'OK' END as status
FROM tasks t
WHERE t.project_id IS NOT NULL
GROUP BY t.project_id, t.priority_sort_order
HAVING COUNT(*) > 1;
-- Check phase_sort_order duplicates
RETURN QUERY
SELECT
'phase_sort_order'::text as column_name,
t.project_id,
COUNT(*) as duplicate_count,
CASE WHEN COUNT(*) > 1 THEN 'DUPLICATES FOUND' ELSE 'OK' END as status
FROM tasks t
WHERE t.project_id IS NOT NULL
GROUP BY t.project_id, t.phase_sort_order
HAVING COUNT(*) > 1;
-- Note: member_sort_order verification removed - column no longer used
END
$$;
-- 4. USAGE INSTRUCTIONS
/*
USAGE:
1. First, run the detection queries to see which projects have duplicates
2. Then run this to fix all duplicates:
SELECT fix_all_duplicate_sort_orders();
3. Finally, verify the fix worked:
SELECT * FROM verify_sort_order_integrity();
If verification returns no rows, all duplicates have been fixed successfully.
WARNING: This will reassign sort order values based on current order + creation time.
Make sure to backup your database before running these functions.
*/

View File

@@ -0,0 +1,37 @@
-- Migration: Add separate sort order columns for different grouping types
-- This allows users to maintain different task orders when switching between grouping views
-- Add new sort order columns
ALTER TABLE tasks ADD COLUMN IF NOT EXISTS status_sort_order INTEGER DEFAULT 0;
ALTER TABLE tasks ADD COLUMN IF NOT EXISTS priority_sort_order INTEGER DEFAULT 0;
ALTER TABLE tasks ADD COLUMN IF NOT EXISTS phase_sort_order INTEGER DEFAULT 0;
ALTER TABLE tasks ADD COLUMN IF NOT EXISTS member_sort_order INTEGER DEFAULT 0;
-- Initialize new columns with current sort_order values
UPDATE tasks SET
status_sort_order = sort_order,
priority_sort_order = sort_order,
phase_sort_order = sort_order,
member_sort_order = sort_order
WHERE status_sort_order = 0
OR priority_sort_order = 0
OR phase_sort_order = 0
OR member_sort_order = 0;
-- Add constraints to ensure non-negative values
ALTER TABLE tasks ADD CONSTRAINT tasks_status_sort_order_check CHECK (status_sort_order >= 0);
ALTER TABLE tasks ADD CONSTRAINT tasks_priority_sort_order_check CHECK (priority_sort_order >= 0);
ALTER TABLE tasks ADD CONSTRAINT tasks_phase_sort_order_check CHECK (phase_sort_order >= 0);
ALTER TABLE tasks ADD CONSTRAINT tasks_member_sort_order_check CHECK (member_sort_order >= 0);
-- Add indexes for performance (since these will be used for ordering)
CREATE INDEX IF NOT EXISTS idx_tasks_status_sort_order ON tasks(project_id, status_sort_order);
CREATE INDEX IF NOT EXISTS idx_tasks_priority_sort_order ON tasks(project_id, priority_sort_order);
CREATE INDEX IF NOT EXISTS idx_tasks_phase_sort_order ON tasks(project_id, phase_sort_order);
CREATE INDEX IF NOT EXISTS idx_tasks_member_sort_order ON tasks(project_id, member_sort_order);
-- Update comments for documentation
COMMENT ON COLUMN tasks.status_sort_order IS 'Sort order when grouped by status';
COMMENT ON COLUMN tasks.priority_sort_order IS 'Sort order when grouped by priority';
COMMENT ON COLUMN tasks.phase_sort_order IS 'Sort order when grouped by phase';
COMMENT ON COLUMN tasks.member_sort_order IS 'Sort order when grouped by members/assignees';

View File

@@ -0,0 +1,172 @@
-- Migration: Update database functions to handle grouping-specific sort orders
-- Function to get the appropriate sort column name based on grouping type
CREATE OR REPLACE FUNCTION get_sort_column_name(_group_by TEXT) RETURNS TEXT
LANGUAGE plpgsql
AS
$$
BEGIN
CASE _group_by
WHEN 'status' THEN RETURN 'status_sort_order';
WHEN 'priority' THEN RETURN 'priority_sort_order';
WHEN 'phase' THEN RETURN 'phase_sort_order';
WHEN 'members' THEN RETURN 'member_sort_order';
ELSE RETURN 'sort_order'; -- fallback to general sort_order
END CASE;
END;
$$;
-- Updated bulk sort order function to handle different sort columns
CREATE OR REPLACE FUNCTION update_task_sort_orders_bulk(_updates json, _group_by text DEFAULT 'status') RETURNS void
LANGUAGE plpgsql
AS
$$
DECLARE
_update_record RECORD;
_sort_column TEXT;
_sql TEXT;
BEGIN
-- Get the appropriate sort column based on grouping
_sort_column := get_sort_column_name(_group_by);
-- Simple approach: update each task's sort_order from the provided array
FOR _update_record IN
SELECT
(item->>'task_id')::uuid as task_id,
(item->>'sort_order')::int as sort_order,
(item->>'status_id')::uuid as status_id,
(item->>'priority_id')::uuid as priority_id,
(item->>'phase_id')::uuid as phase_id
FROM json_array_elements(_updates) as item
LOOP
-- Update the appropriate sort column and other fields using dynamic SQL
-- Only update sort_order if we're using the default sorting
IF _sort_column = 'sort_order' THEN
UPDATE tasks SET
sort_order = _update_record.sort_order,
status_id = COALESCE(_update_record.status_id, status_id),
priority_id = COALESCE(_update_record.priority_id, priority_id)
WHERE id = _update_record.task_id;
ELSE
-- Update only the grouping-specific sort column, not the main sort_order
_sql := 'UPDATE tasks SET ' || _sort_column || ' = $1, ' ||
'status_id = COALESCE($2, status_id), ' ||
'priority_id = COALESCE($3, priority_id) ' ||
'WHERE id = $4';
EXECUTE _sql USING
_update_record.sort_order,
_update_record.status_id,
_update_record.priority_id,
_update_record.task_id;
END IF;
-- Handle phase updates separately since it's in a different table
IF _update_record.phase_id IS NOT NULL THEN
INSERT INTO task_phase (task_id, phase_id)
VALUES (_update_record.task_id, _update_record.phase_id)
ON CONFLICT (task_id) DO UPDATE SET phase_id = _update_record.phase_id;
END IF;
END LOOP;
END;
$$;
-- Updated main sort order change handler
CREATE OR REPLACE FUNCTION handle_task_list_sort_order_change(_body json) RETURNS void
LANGUAGE plpgsql
AS
$$
DECLARE
_from_index INT;
_to_index INT;
_task_id UUID;
_project_id UUID;
_from_group UUID;
_to_group UUID;
_group_by TEXT;
_batch_size INT := 100;
_sort_column TEXT;
_sql TEXT;
BEGIN
_project_id = (_body ->> 'project_id')::UUID;
_task_id = (_body ->> 'task_id')::UUID;
_from_index = (_body ->> 'from_index')::INT;
_to_index = (_body ->> 'to_index')::INT;
_from_group = (_body ->> 'from_group')::UUID;
_to_group = (_body ->> 'to_group')::UUID;
_group_by = (_body ->> 'group_by')::TEXT;
-- Get the appropriate sort column
_sort_column := get_sort_column_name(_group_by);
-- Handle group changes
IF (_from_group <> _to_group OR (_from_group <> _to_group) IS NULL) THEN
IF (_group_by = 'status') THEN
UPDATE tasks
SET status_id = _to_group
WHERE id = _task_id
AND status_id = _from_group
AND project_id = _project_id;
END IF;
IF (_group_by = 'priority') THEN
UPDATE tasks
SET priority_id = _to_group
WHERE id = _task_id
AND priority_id = _from_group
AND project_id = _project_id;
END IF;
IF (_group_by = 'phase') THEN
IF (is_null_or_empty(_to_group) IS FALSE) THEN
INSERT INTO task_phase (task_id, phase_id)
VALUES (_task_id, _to_group)
ON CONFLICT (task_id) DO UPDATE SET phase_id = _to_group;
ELSE
DELETE FROM task_phase WHERE task_id = _task_id;
END IF;
END IF;
END IF;
-- Handle sort order changes using dynamic SQL
IF (_from_index <> _to_index) THEN
-- For the main sort_order column, we need to be careful about unique constraints
IF _sort_column = 'sort_order' THEN
-- Use a transaction-safe approach for the main sort_order column
IF (_to_index > _from_index) THEN
-- Moving down: decrease sort_order for items between old and new position
UPDATE tasks SET sort_order = sort_order - 1
WHERE project_id = _project_id
AND sort_order > _from_index
AND sort_order <= _to_index;
ELSE
-- Moving up: increase sort_order for items between new and old position
UPDATE tasks SET sort_order = sort_order + 1
WHERE project_id = _project_id
AND sort_order >= _to_index
AND sort_order < _from_index;
END IF;
-- Set the new sort_order for the moved task
UPDATE tasks SET sort_order = _to_index WHERE id = _task_id;
ELSE
-- For grouping-specific columns, use dynamic SQL since there's no unique constraint
IF (_to_index > _from_index) THEN
-- Moving down: decrease sort_order for items between old and new position
_sql := 'UPDATE tasks SET ' || _sort_column || ' = ' || _sort_column || ' - 1 ' ||
'WHERE project_id = $1 AND ' || _sort_column || ' > $2 AND ' || _sort_column || ' <= $3';
EXECUTE _sql USING _project_id, _from_index, _to_index;
ELSE
-- Moving up: increase sort_order for items between new and old position
_sql := 'UPDATE tasks SET ' || _sort_column || ' = ' || _sort_column || ' + 1 ' ||
'WHERE project_id = $1 AND ' || _sort_column || ' >= $2 AND ' || _sort_column || ' < $3';
EXECUTE _sql USING _project_id, _to_index, _from_index;
END IF;
-- Set the new sort_order for the moved task
_sql := 'UPDATE tasks SET ' || _sort_column || ' = $1 WHERE id = $2';
EXECUTE _sql USING _to_index, _task_id;
END IF;
END IF;
END;
$$;

View File

@@ -0,0 +1,179 @@
-- Migration: Fix sort order constraint violations
-- First, let's ensure all existing tasks have unique sort_order values within each project
-- This is a one-time fix to ensure data consistency
DO $$
DECLARE
_project RECORD;
_task RECORD;
_counter INTEGER;
BEGIN
-- For each project, reassign sort_order values to ensure uniqueness
FOR _project IN
SELECT DISTINCT project_id
FROM tasks
WHERE project_id IS NOT NULL
LOOP
_counter := 0;
-- Reassign sort_order values sequentially for this project
FOR _task IN
SELECT id
FROM tasks
WHERE project_id = _project.project_id
ORDER BY sort_order, created_at
LOOP
UPDATE tasks
SET sort_order = _counter
WHERE id = _task.id;
_counter := _counter + 1;
END LOOP;
END LOOP;
END
$$;
-- Now create a better version of our functions that properly handles the constraints
-- Updated bulk sort order function that avoids sort_order conflicts
CREATE OR REPLACE FUNCTION update_task_sort_orders_bulk(_updates json, _group_by text DEFAULT 'status') RETURNS void
LANGUAGE plpgsql
AS
$$
DECLARE
_update_record RECORD;
_sort_column TEXT;
_sql TEXT;
BEGIN
-- Get the appropriate sort column based on grouping
_sort_column := get_sort_column_name(_group_by);
-- Process each update record
FOR _update_record IN
SELECT
(item->>'task_id')::uuid as task_id,
(item->>'sort_order')::int as sort_order,
(item->>'status_id')::uuid as status_id,
(item->>'priority_id')::uuid as priority_id,
(item->>'phase_id')::uuid as phase_id
FROM json_array_elements(_updates) as item
LOOP
-- Update the grouping-specific sort column and other fields
_sql := 'UPDATE tasks SET ' || _sort_column || ' = $1, ' ||
'status_id = COALESCE($2, status_id), ' ||
'priority_id = COALESCE($3, priority_id), ' ||
'updated_at = CURRENT_TIMESTAMP ' ||
'WHERE id = $4';
EXECUTE _sql USING
_update_record.sort_order,
_update_record.status_id,
_update_record.priority_id,
_update_record.task_id;
-- Handle phase updates separately since it's in a different table
IF _update_record.phase_id IS NOT NULL THEN
INSERT INTO task_phase (task_id, phase_id)
VALUES (_update_record.task_id, _update_record.phase_id)
ON CONFLICT (task_id) DO UPDATE SET phase_id = _update_record.phase_id;
END IF;
END LOOP;
END;
$$;
-- Also update the helper function to be more explicit
CREATE OR REPLACE FUNCTION get_sort_column_name(_group_by TEXT) RETURNS TEXT
LANGUAGE plpgsql
AS
$$
BEGIN
CASE _group_by
WHEN 'status' THEN RETURN 'status_sort_order';
WHEN 'priority' THEN RETURN 'priority_sort_order';
WHEN 'phase' THEN RETURN 'phase_sort_order';
WHEN 'members' THEN RETURN 'member_sort_order';
-- For backward compatibility, still support general sort_order but be explicit
WHEN 'general' THEN RETURN 'sort_order';
ELSE RETURN 'status_sort_order'; -- Default to status sorting
END CASE;
END;
$$;
-- Updated main sort order change handler that avoids conflicts
CREATE OR REPLACE FUNCTION handle_task_list_sort_order_change(_body json) RETURNS void
LANGUAGE plpgsql
AS
$$
DECLARE
_from_index INT;
_to_index INT;
_task_id UUID;
_project_id UUID;
_from_group UUID;
_to_group UUID;
_group_by TEXT;
_sort_column TEXT;
_sql TEXT;
BEGIN
_project_id = (_body ->> 'project_id')::UUID;
_task_id = (_body ->> 'task_id')::UUID;
_from_index = (_body ->> 'from_index')::INT;
_to_index = (_body ->> 'to_index')::INT;
_from_group = (_body ->> 'from_group')::UUID;
_to_group = (_body ->> 'to_group')::UUID;
_group_by = (_body ->> 'group_by')::TEXT;
-- Get the appropriate sort column
_sort_column := get_sort_column_name(_group_by);
-- Handle group changes first
IF (_from_group <> _to_group OR (_from_group <> _to_group) IS NULL) THEN
IF (_group_by = 'status') THEN
UPDATE tasks
SET status_id = _to_group, updated_at = CURRENT_TIMESTAMP
WHERE id = _task_id
AND project_id = _project_id;
END IF;
IF (_group_by = 'priority') THEN
UPDATE tasks
SET priority_id = _to_group, updated_at = CURRENT_TIMESTAMP
WHERE id = _task_id
AND project_id = _project_id;
END IF;
IF (_group_by = 'phase') THEN
IF (is_null_or_empty(_to_group) IS FALSE) THEN
INSERT INTO task_phase (task_id, phase_id)
VALUES (_task_id, _to_group)
ON CONFLICT (task_id) DO UPDATE SET phase_id = _to_group;
ELSE
DELETE FROM task_phase WHERE task_id = _task_id;
END IF;
END IF;
END IF;
-- Handle sort order changes for the grouping-specific column only
IF (_from_index <> _to_index) THEN
-- Update the grouping-specific sort order (no unique constraint issues)
IF (_to_index > _from_index) THEN
-- Moving down: decrease sort order for items between old and new position
_sql := 'UPDATE tasks SET ' || _sort_column || ' = ' || _sort_column || ' - 1, ' ||
'updated_at = CURRENT_TIMESTAMP ' ||
'WHERE project_id = $1 AND ' || _sort_column || ' > $2 AND ' || _sort_column || ' <= $3';
EXECUTE _sql USING _project_id, _from_index, _to_index;
ELSE
-- Moving up: increase sort order for items between new and old position
_sql := 'UPDATE tasks SET ' || _sort_column || ' = ' || _sort_column || ' + 1, ' ||
'updated_at = CURRENT_TIMESTAMP ' ||
'WHERE project_id = $1 AND ' || _sort_column || ' >= $2 AND ' || _sort_column || ' < $3';
EXECUTE _sql USING _project_id, _to_index, _from_index;
END IF;
-- Set the new sort order for the moved task
_sql := 'UPDATE tasks SET ' || _sort_column || ' = $1, updated_at = CURRENT_TIMESTAMP WHERE id = $2';
EXECUTE _sql USING _to_index, _task_id;
END IF;
END;
$$;

View File

@@ -0,0 +1,93 @@
-- Migration: Add survey tables for account setup questionnaire
-- Date: 2025-07-24
-- Description: Creates tables to store survey questions and user responses for account setup flow
BEGIN;
-- Create surveys table to define different types of surveys
CREATE TABLE IF NOT EXISTS surveys (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR(255) NOT NULL,
description TEXT,
survey_type VARCHAR(50) DEFAULT 'account_setup' NOT NULL, -- 'account_setup', 'onboarding', 'feedback'
is_active BOOLEAN DEFAULT TRUE NOT NULL,
created_at TIMESTAMP DEFAULT now() NOT NULL,
updated_at TIMESTAMP DEFAULT now() NOT NULL
);
-- Create survey_questions table to store individual questions
CREATE TABLE IF NOT EXISTS survey_questions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
survey_id UUID REFERENCES surveys(id) ON DELETE CASCADE NOT NULL,
question_key VARCHAR(100) NOT NULL, -- Used for localization keys
question_type VARCHAR(50) NOT NULL, -- 'single_choice', 'multiple_choice', 'text'
is_required BOOLEAN DEFAULT FALSE NOT NULL,
sort_order INTEGER DEFAULT 0 NOT NULL,
options JSONB, -- For choice questions, store options as JSON array
created_at TIMESTAMP DEFAULT now() NOT NULL,
updated_at TIMESTAMP DEFAULT now() NOT NULL
);
-- Create survey_responses table to track user responses to surveys
CREATE TABLE IF NOT EXISTS survey_responses (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
survey_id UUID REFERENCES surveys(id) ON DELETE CASCADE NOT NULL,
user_id UUID REFERENCES users(id) ON DELETE CASCADE NOT NULL,
is_completed BOOLEAN DEFAULT FALSE NOT NULL,
started_at TIMESTAMP DEFAULT now() NOT NULL,
completed_at TIMESTAMP,
created_at TIMESTAMP DEFAULT now() NOT NULL,
updated_at TIMESTAMP DEFAULT now() NOT NULL
);
-- Create survey_answers table to store individual question answers
CREATE TABLE IF NOT EXISTS survey_answers (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
response_id UUID REFERENCES survey_responses(id) ON DELETE CASCADE NOT NULL,
question_id UUID REFERENCES survey_questions(id) ON DELETE CASCADE NOT NULL,
answer_text TEXT,
answer_json JSONB, -- For multiple choice answers stored as array
created_at TIMESTAMP DEFAULT now() NOT NULL,
updated_at TIMESTAMP DEFAULT now() NOT NULL
);
-- Add performance indexes
CREATE INDEX IF NOT EXISTS idx_surveys_type_active ON surveys(survey_type, is_active);
CREATE INDEX IF NOT EXISTS idx_survey_questions_survey_order ON survey_questions(survey_id, sort_order);
CREATE INDEX IF NOT EXISTS idx_survey_responses_user_survey ON survey_responses(user_id, survey_id);
CREATE INDEX IF NOT EXISTS idx_survey_responses_completed ON survey_responses(survey_id, is_completed);
CREATE INDEX IF NOT EXISTS idx_survey_answers_response ON survey_answers(response_id);
-- Add constraints
ALTER TABLE survey_questions ADD CONSTRAINT survey_questions_sort_order_check CHECK (sort_order >= 0);
ALTER TABLE survey_questions ADD CONSTRAINT survey_questions_type_check CHECK (question_type IN ('single_choice', 'multiple_choice', 'text'));
-- Add unique constraint to prevent duplicate responses per user per survey
ALTER TABLE survey_responses ADD CONSTRAINT unique_user_survey_response UNIQUE (user_id, survey_id);
-- Add unique constraint to prevent duplicate answers per question per response
ALTER TABLE survey_answers ADD CONSTRAINT unique_response_question_answer UNIQUE (response_id, question_id);
-- Insert the default account setup survey
INSERT INTO surveys (name, description, survey_type, is_active) VALUES
('Account Setup Survey', 'Initial questionnaire during account setup to understand user needs', 'account_setup', true)
ON CONFLICT DO NOTHING;
-- Get the survey ID for inserting questions
DO $$
DECLARE
survey_uuid UUID;
BEGIN
SELECT id INTO survey_uuid FROM surveys WHERE survey_type = 'account_setup' AND name = 'Account Setup Survey' LIMIT 1;
-- Insert survey questions
INSERT INTO survey_questions (survey_id, question_key, question_type, is_required, sort_order, options) VALUES
(survey_uuid, 'organization_type', 'single_choice', true, 1, '["freelancer", "startup", "small_medium_business", "agency", "enterprise", "other"]'),
(survey_uuid, 'user_role', 'single_choice', true, 2, '["founder_ceo", "project_manager", "software_developer", "designer", "operations", "other"]'),
(survey_uuid, 'main_use_cases', 'multiple_choice', true, 3, '["task_management", "team_collaboration", "resource_planning", "client_communication", "time_tracking", "other"]'),
(survey_uuid, 'previous_tools', 'text', false, 4, null),
(survey_uuid, 'how_heard_about', 'single_choice', false, 5, '["google_search", "twitter", "linkedin", "friend_colleague", "blog_article", "other"]')
ON CONFLICT DO NOTHING;
END $$;
COMMIT;

View File

@@ -0,0 +1,72 @@
# Node-pg-migrate Migrations
This directory contains database migrations managed by node-pg-migrate.
## Migration Commands
- `npm run migrate:create -- migration-name` - Create a new migration file
- `npm run migrate:up` - Run all pending migrations
- `npm run migrate:down` - Rollback the last migration
- `npm run migrate:redo` - Rollback and re-run the last migration
## Migration File Format
Migrations are JavaScript files with timestamp prefixes (e.g., `20250115000000_performance-indexes.js`).
Each migration file exports two functions:
- `exports.up` - Contains the forward migration logic
- `exports.down` - Contains the rollback logic
## Best Practices
1. **Always use IF EXISTS/IF NOT EXISTS checks** to make migrations idempotent
2. **Test migrations locally** before deploying to production
3. **Include rollback logic** in the `down` function for all changes
4. **Use descriptive names** for migration files
5. **Keep migrations focused** - one logical change per migration
## Example Migration
```javascript
exports.up = pgm => {
// Create table with IF NOT EXISTS
pgm.createTable('users', {
id: 'id',
name: { type: 'varchar(100)', notNull: true },
created_at: {
type: 'timestamp',
notNull: true,
default: pgm.func('current_timestamp')
}
}, { ifNotExists: true });
// Add index with IF NOT EXISTS
pgm.createIndex('users', 'name', {
name: 'idx_users_name',
ifNotExists: true
});
};
exports.down = pgm => {
// Drop in reverse order
pgm.dropIndex('users', 'name', {
name: 'idx_users_name',
ifExists: true
});
pgm.dropTable('users', { ifExists: true });
};
```
## Migration History
The `pgmigrations` table tracks which migrations have been run. Do not modify this table manually.
## Converting from SQL Migrations
When converting SQL migrations to node-pg-migrate format:
1. Wrap SQL statements in `pgm.sql()` calls
2. Use node-pg-migrate helper methods where possible (createTable, addColumns, etc.)
3. Always include `IF EXISTS/IF NOT EXISTS` checks
4. Ensure proper rollback logic in the `down` function

View File

@@ -12,7 +12,7 @@ CREATE TYPE DEPENDENCY_TYPE AS ENUM ('blocked_by');
CREATE TYPE SCHEDULE_TYPE AS ENUM ('daily', 'weekly', 'yearly', 'monthly', 'every_x_days', 'every_x_weeks', 'every_x_months');
CREATE TYPE LANGUAGE_TYPE AS ENUM ('en', 'es', 'pt');
CREATE TYPE LANGUAGE_TYPE AS ENUM ('en', 'es', 'pt', 'alb', 'de', 'zh_cn');
-- START: Users
CREATE SEQUENCE IF NOT EXISTS users_user_no_seq START 1;
@@ -1391,27 +1391,30 @@ ALTER TABLE task_work_log
CHECK (time_spent >= (0)::NUMERIC);
CREATE TABLE IF NOT EXISTS tasks (
id UUID DEFAULT uuid_generate_v4() NOT NULL,
name TEXT NOT NULL,
description TEXT,
done BOOLEAN DEFAULT FALSE NOT NULL,
total_minutes NUMERIC DEFAULT 0 NOT NULL,
archived BOOLEAN DEFAULT FALSE NOT NULL,
task_no BIGINT NOT NULL,
start_date TIMESTAMP WITH TIME ZONE,
end_date TIMESTAMP WITH TIME ZONE,
priority_id UUID NOT NULL,
project_id UUID NOT NULL,
reporter_id UUID NOT NULL,
parent_task_id UUID,
status_id UUID NOT NULL,
completed_at TIMESTAMP WITH TIME ZONE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL,
sort_order INTEGER DEFAULT 0 NOT NULL,
roadmap_sort_order INTEGER DEFAULT 0 NOT NULL,
billable BOOLEAN DEFAULT TRUE,
schedule_id UUID
id UUID DEFAULT uuid_generate_v4() NOT NULL,
name TEXT NOT NULL,
description TEXT,
done BOOLEAN DEFAULT FALSE NOT NULL,
total_minutes NUMERIC DEFAULT 0 NOT NULL,
archived BOOLEAN DEFAULT FALSE NOT NULL,
task_no BIGINT NOT NULL,
start_date TIMESTAMP WITH TIME ZONE,
end_date TIMESTAMP WITH TIME ZONE,
priority_id UUID NOT NULL,
project_id UUID NOT NULL,
reporter_id UUID NOT NULL,
parent_task_id UUID,
status_id UUID NOT NULL,
completed_at TIMESTAMP WITH TIME ZONE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL,
sort_order INTEGER DEFAULT 0 NOT NULL,
roadmap_sort_order INTEGER DEFAULT 0 NOT NULL,
status_sort_order INTEGER DEFAULT 0 NOT NULL,
priority_sort_order INTEGER DEFAULT 0 NOT NULL,
phase_sort_order INTEGER DEFAULT 0 NOT NULL,
billable BOOLEAN DEFAULT TRUE,
schedule_id UUID
);
ALTER TABLE tasks
@@ -1499,6 +1502,21 @@ ALTER TABLE tasks
ADD CONSTRAINT tasks_total_minutes_check
CHECK ((total_minutes >= (0)::NUMERIC) AND (total_minutes <= (999999)::NUMERIC));
-- Add constraints for new sort order columns
ALTER TABLE tasks ADD CONSTRAINT tasks_status_sort_order_check CHECK (status_sort_order >= 0);
ALTER TABLE tasks ADD CONSTRAINT tasks_priority_sort_order_check CHECK (priority_sort_order >= 0);
ALTER TABLE tasks ADD CONSTRAINT tasks_phase_sort_order_check CHECK (phase_sort_order >= 0);
-- Add indexes for performance on new sort order columns
CREATE INDEX IF NOT EXISTS idx_tasks_status_sort_order ON tasks(project_id, status_sort_order);
CREATE INDEX IF NOT EXISTS idx_tasks_priority_sort_order ON tasks(project_id, priority_sort_order);
CREATE INDEX IF NOT EXISTS idx_tasks_phase_sort_order ON tasks(project_id, phase_sort_order);
-- Add comments for documentation
COMMENT ON COLUMN tasks.status_sort_order IS 'Sort order when grouped by status';
COMMENT ON COLUMN tasks.priority_sort_order IS 'Sort order when grouped by priority';
COMMENT ON COLUMN tasks.phase_sort_order IS 'Sort order when grouped by phase';
CREATE TABLE IF NOT EXISTS tasks_assignees (
task_id UUID NOT NULL,
project_member_id UUID NOT NULL,
@@ -2279,3 +2297,60 @@ ALTER TABLE organization_working_days
ALTER TABLE organization_working_days
ADD CONSTRAINT org_organization_id_fk
FOREIGN KEY (organization_id) REFERENCES organizations;
-- Survey tables for account setup questionnaire
CREATE TABLE IF NOT EXISTS surveys (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR(255) NOT NULL,
description TEXT,
survey_type VARCHAR(50) DEFAULT 'account_setup' NOT NULL,
is_active BOOLEAN DEFAULT TRUE NOT NULL,
created_at TIMESTAMP DEFAULT now() NOT NULL,
updated_at TIMESTAMP DEFAULT now() NOT NULL
);
CREATE TABLE IF NOT EXISTS survey_questions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
survey_id UUID REFERENCES surveys(id) ON DELETE CASCADE NOT NULL,
question_key VARCHAR(100) NOT NULL,
question_type VARCHAR(50) NOT NULL,
is_required BOOLEAN DEFAULT FALSE NOT NULL,
sort_order INTEGER DEFAULT 0 NOT NULL,
options JSONB,
created_at TIMESTAMP DEFAULT now() NOT NULL,
updated_at TIMESTAMP DEFAULT now() NOT NULL
);
CREATE TABLE IF NOT EXISTS survey_responses (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
survey_id UUID REFERENCES surveys(id) ON DELETE CASCADE NOT NULL,
user_id UUID REFERENCES users(id) ON DELETE CASCADE NOT NULL,
is_completed BOOLEAN DEFAULT FALSE NOT NULL,
started_at TIMESTAMP DEFAULT now() NOT NULL,
completed_at TIMESTAMP,
created_at TIMESTAMP DEFAULT now() NOT NULL,
updated_at TIMESTAMP DEFAULT now() NOT NULL
);
CREATE TABLE IF NOT EXISTS survey_answers (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
response_id UUID REFERENCES survey_responses(id) ON DELETE CASCADE NOT NULL,
question_id UUID REFERENCES survey_questions(id) ON DELETE CASCADE NOT NULL,
answer_text TEXT,
answer_json JSONB,
created_at TIMESTAMP DEFAULT now() NOT NULL,
updated_at TIMESTAMP DEFAULT now() NOT NULL
);
-- Survey table indexes
CREATE INDEX IF NOT EXISTS idx_surveys_type_active ON surveys(survey_type, is_active);
CREATE INDEX IF NOT EXISTS idx_survey_questions_survey_order ON survey_questions(survey_id, sort_order);
CREATE INDEX IF NOT EXISTS idx_survey_responses_user_survey ON survey_responses(user_id, survey_id);
CREATE INDEX IF NOT EXISTS idx_survey_responses_completed ON survey_responses(survey_id, is_completed);
CREATE INDEX IF NOT EXISTS idx_survey_answers_response ON survey_answers(response_id);
-- Survey table constraints
ALTER TABLE survey_questions ADD CONSTRAINT survey_questions_sort_order_check CHECK (sort_order >= 0);
ALTER TABLE survey_questions ADD CONSTRAINT survey_questions_type_check CHECK (question_type IN ('single_choice', 'multiple_choice', 'text'));
ALTER TABLE survey_responses ADD CONSTRAINT unique_user_survey_response UNIQUE (user_id, survey_id);
ALTER TABLE survey_answers ADD CONSTRAINT unique_response_question_answer UNIQUE (response_id, question_id);

View File

@@ -142,3 +142,25 @@ DROP FUNCTION sys_insert_license_types();
INSERT INTO timezones (name, abbrev, utc_offset)
SELECT name, abbrev, utc_offset
FROM pg_timezone_names;
-- Insert default account setup survey
INSERT INTO surveys (name, description, survey_type, is_active) VALUES
('Account Setup Survey', 'Initial questionnaire during account setup to understand user needs', 'account_setup', true)
ON CONFLICT DO NOTHING;
-- Insert survey questions for account setup survey
DO $$
DECLARE
survey_uuid UUID;
BEGIN
SELECT id INTO survey_uuid FROM surveys WHERE survey_type = 'account_setup' AND name = 'Account Setup Survey' LIMIT 1;
-- Insert survey questions
INSERT INTO survey_questions (survey_id, question_key, question_type, is_required, sort_order, options) VALUES
(survey_uuid, 'organization_type', 'single_choice', true, 1, '["freelancer", "startup", "small_medium_business", "agency", "enterprise", "other"]'),
(survey_uuid, 'user_role', 'single_choice', true, 2, '["founder_ceo", "project_manager", "software_developer", "designer", "operations", "other"]'),
(survey_uuid, 'main_use_cases', 'multiple_choice', true, 3, '["task_management", "team_collaboration", "resource_planning", "client_communication", "time_tracking", "other"]'),
(survey_uuid, 'previous_tools', 'text', false, 4, null),
(survey_uuid, 'how_heard_about', 'single_choice', false, 5, '["google_search", "twitter", "linkedin", "friend_colleague", "blog_article", "other"]')
ON CONFLICT DO NOTHING;
END $$;

View File

@@ -32,3 +32,37 @@ SELECT u.avatar_url,
FROM team_members
LEFT JOIN users u ON team_members.user_id = u.id;
-- PERFORMANCE OPTIMIZATION: Create materialized view for team member info
-- This pre-calculates the expensive joins and subqueries from team_member_info_view
CREATE MATERIALIZED VIEW IF NOT EXISTS team_member_info_mv AS
SELECT
u.avatar_url,
COALESCE(u.email, ei.email) AS email,
COALESCE(u.name, ei.name) AS name,
u.id AS user_id,
tm.id AS team_member_id,
tm.team_id,
tm.active,
u.socket_id
FROM team_members tm
LEFT JOIN users u ON tm.user_id = u.id
LEFT JOIN email_invitations ei ON ei.team_member_id = tm.id
WHERE tm.active = TRUE;
-- Create unique index on the materialized view for fast lookups
CREATE UNIQUE INDEX IF NOT EXISTS idx_team_member_info_mv_team_member_id
ON team_member_info_mv(team_member_id);
CREATE INDEX IF NOT EXISTS idx_team_member_info_mv_team_user
ON team_member_info_mv(team_id, user_id);
-- Function to refresh the materialized view
CREATE OR REPLACE FUNCTION refresh_team_member_info_mv()
RETURNS void
LANGUAGE plpgsql
AS $$
BEGIN
REFRESH MATERIALIZED VIEW CONCURRENTLY team_member_info_mv;
END;
$$;

View File

@@ -3351,15 +3351,15 @@ BEGIN
SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(rec))), '[]'::JSON)
FROM (SELECT team_member_id,
project_member_id,
(SELECT name FROM team_member_info_view WHERE team_member_info_view.team_member_id = tm.id),
(SELECT email_notifications_enabled
COALESCE((SELECT name FROM team_member_info_view WHERE team_member_info_view.team_member_id = tm.id), '') as name,
COALESCE((SELECT email_notifications_enabled
FROM notification_settings
WHERE team_id = tm.team_id
AND notification_settings.user_id = u.id) AS email_notifications_enabled,
u.avatar_url,
AND notification_settings.user_id = u.id), false) AS email_notifications_enabled,
COALESCE(u.avatar_url, '') as avatar_url,
u.id AS user_id,
u.email,
u.socket_id AS socket_id,
COALESCE(u.email, '') as email,
COALESCE(u.socket_id, '') as socket_id,
tm.team_id AS team_id
FROM tasks_assignees
INNER JOIN team_members tm ON tm.id = tasks_assignees.team_member_id
@@ -4066,14 +4066,14 @@ DECLARE
_schedule_id JSON;
_task_completed_at TIMESTAMPTZ;
BEGIN
SELECT name FROM tasks WHERE id = _task_id INTO _task_name;
SELECT COALESCE(name, '') FROM tasks WHERE id = _task_id INTO _task_name;
SELECT name
SELECT COALESCE(name, '')
FROM task_statuses
WHERE id = (SELECT status_id FROM tasks WHERE id = _task_id)
INTO _previous_status_name;
SELECT name FROM task_statuses WHERE id = _status_id INTO _new_status_name;
SELECT COALESCE(name, '') FROM task_statuses WHERE id = _status_id INTO _new_status_name;
IF (_previous_status_name != _new_status_name)
THEN
@@ -4081,14 +4081,22 @@ BEGIN
SELECT get_task_complete_info(_task_id, _status_id) INTO _task_info;
SELECT name FROM users WHERE id = _user_id INTO _updater_name;
SELECT COALESCE(name, '') FROM users WHERE id = _user_id INTO _updater_name;
_message = CONCAT(_updater_name, ' transitioned "', _task_name, '" from ', _previous_status_name, '',
_new_status_name);
END IF;
SELECT completed_at FROM tasks WHERE id = _task_id INTO _task_completed_at;
SELECT schedule_id FROM tasks WHERE id = _task_id INTO _schedule_id;
-- Handle schedule_id properly for recurring tasks
SELECT CASE
WHEN schedule_id IS NULL THEN 'null'::json
ELSE json_build_object('id', schedule_id)
END
FROM tasks
WHERE id = _task_id
INTO _schedule_id;
SELECT COALESCE(ROW_TO_JSON(r), '{}'::JSON)
FROM (SELECT is_done, is_doing, is_todo
@@ -4097,7 +4105,7 @@ BEGIN
INTO _status_category;
RETURN JSON_BUILD_OBJECT(
'message', _message,
'message', COALESCE(_message, ''),
'project_id', (SELECT project_id FROM tasks WHERE id = _task_id),
'parent_done', (CASE
WHEN EXISTS(SELECT 1
@@ -4105,14 +4113,14 @@ BEGIN
WHERE tasks_with_status_view.task_id = _task_id
AND is_done IS TRUE) THEN 1
ELSE 0 END),
'color_code', (_task_info ->> 'color_code')::TEXT,
'color_code_dark', (_task_info ->> 'color_code_dark')::TEXT,
'total_tasks', (_task_info ->> 'total_tasks')::INT,
'total_completed', (_task_info ->> 'total_completed')::INT,
'members', (_task_info ->> 'members')::JSON,
'color_code', COALESCE((_task_info ->> 'color_code')::TEXT, ''),
'color_code_dark', COALESCE((_task_info ->> 'color_code_dark')::TEXT, ''),
'total_tasks', COALESCE((_task_info ->> 'total_tasks')::INT, 0),
'total_completed', COALESCE((_task_info ->> 'total_completed')::INT, 0),
'members', COALESCE((_task_info ->> 'members')::JSON, '[]'::JSON),
'completed_at', _task_completed_at,
'status_category', _status_category,
'schedule_id', _schedule_id
'status_category', COALESCE(_status_category, '{}'::JSON),
'schedule_id', COALESCE(_schedule_id, 'null'::JSON)
);
END
$$;
@@ -4305,6 +4313,24 @@ BEGIN
END
$$;
-- Helper function to get the appropriate sort column name based on grouping type
CREATE OR REPLACE FUNCTION get_sort_column_name(_group_by TEXT) RETURNS TEXT
LANGUAGE plpgsql
AS
$$
BEGIN
CASE _group_by
WHEN 'status' THEN RETURN 'status_sort_order';
WHEN 'priority' THEN RETURN 'priority_sort_order';
WHEN 'phase' THEN RETURN 'phase_sort_order';
WHEN 'members' THEN RETURN 'member_sort_order';
-- For backward compatibility, still support general sort_order but be explicit
WHEN 'general' THEN RETURN 'sort_order';
ELSE RETURN 'status_sort_order'; -- Default to status sorting
END CASE;
END;
$$;
CREATE OR REPLACE FUNCTION handle_task_list_sort_order_change(_body json) RETURNS void
LANGUAGE plpgsql
AS
@@ -4317,54 +4343,67 @@ DECLARE
_from_group UUID;
_to_group UUID;
_group_by TEXT;
_sort_column TEXT;
_sql TEXT;
BEGIN
_project_id = (_body ->> 'project_id')::UUID;
_task_id = (_body ->> 'task_id')::UUID;
_from_index = (_body ->> 'from_index')::INT; -- from sort_order
_to_index = (_body ->> 'to_index')::INT; -- to sort_order
_from_index = (_body ->> 'from_index')::INT;
_to_index = (_body ->> 'to_index')::INT;
_from_group = (_body ->> 'from_group')::UUID;
_to_group = (_body ->> 'to_group')::UUID;
_group_by = (_body ->> 'group_by')::TEXT;
IF (_from_group <> _to_group OR (_from_group <> _to_group) IS NULL)
THEN
IF (_group_by = 'status')
THEN
UPDATE tasks SET status_id = _to_group WHERE id = _task_id AND status_id = _from_group;
-- Get the appropriate sort column
_sort_column := get_sort_column_name(_group_by);
-- Handle group changes first
IF (_from_group <> _to_group OR (_from_group <> _to_group) IS NULL) THEN
IF (_group_by = 'status') THEN
UPDATE tasks
SET status_id = _to_group, updated_at = CURRENT_TIMESTAMP
WHERE id = _task_id
AND project_id = _project_id;
END IF;
IF (_group_by = 'priority')
THEN
UPDATE tasks SET priority_id = _to_group WHERE id = _task_id AND priority_id = _from_group;
IF (_group_by = 'priority') THEN
UPDATE tasks
SET priority_id = _to_group, updated_at = CURRENT_TIMESTAMP
WHERE id = _task_id
AND project_id = _project_id;
END IF;
IF (_group_by = 'phase')
THEN
IF (is_null_or_empty(_to_group) IS FALSE)
THEN
IF (_group_by = 'phase') THEN
IF (is_null_or_empty(_to_group) IS FALSE) THEN
INSERT INTO task_phase (task_id, phase_id)
VALUES (_task_id, _to_group)
ON CONFLICT (task_id) DO UPDATE SET phase_id = _to_group;
END IF;
IF (is_null_or_empty(_to_group) IS TRUE)
THEN
DELETE
FROM task_phase
WHERE task_id = _task_id;
ELSE
DELETE FROM task_phase WHERE task_id = _task_id;
END IF;
END IF;
END IF;
IF ((_body ->> 'to_last_index')::BOOLEAN IS TRUE AND _from_index < _to_index)
THEN
PERFORM handle_task_list_sort_inside_group(_from_index, _to_index, _task_id, _project_id);
-- Handle sort order changes for the grouping-specific column only
IF (_from_index <> _to_index) THEN
-- Update the grouping-specific sort order (no unique constraint issues)
IF (_to_index > _from_index) THEN
-- Moving down: decrease sort order for items between old and new position
_sql := 'UPDATE tasks SET ' || _sort_column || ' = ' || _sort_column || ' - 1, ' ||
'updated_at = CURRENT_TIMESTAMP ' ||
'WHERE project_id = $1 AND ' || _sort_column || ' > $2 AND ' || _sort_column || ' <= $3';
EXECUTE _sql USING _project_id, _from_index, _to_index;
ELSE
PERFORM handle_task_list_sort_between_groups(_from_index, _to_index, _task_id, _project_id);
-- Moving up: increase sort order for items between new and old position
_sql := 'UPDATE tasks SET ' || _sort_column || ' = ' || _sort_column || ' + 1, ' ||
'updated_at = CURRENT_TIMESTAMP ' ||
'WHERE project_id = $1 AND ' || _sort_column || ' >= $2 AND ' || _sort_column || ' < $3';
EXECUTE _sql USING _project_id, _to_index, _from_index;
END IF;
ELSE
PERFORM handle_task_list_sort_inside_group(_from_index, _to_index, _task_id, _project_id);
-- Set the new sort order for the moved task
_sql := 'UPDATE tasks SET ' || _sort_column || ' = $1, updated_at = CURRENT_TIMESTAMP WHERE id = $2';
EXECUTE _sql USING _to_index, _task_id;
END IF;
END
$$;
@@ -4569,31 +4608,31 @@ BEGIN
INSERT INTO project_task_list_cols (project_id, name, key, index, pinned)
VALUES (_project_id, 'Progress', 'PROGRESS', 3, TRUE);
INSERT INTO project_task_list_cols (project_id, name, key, index, pinned)
VALUES (_project_id, 'Members', 'ASSIGNEES', 4, TRUE);
VALUES (_project_id, 'Status', 'STATUS', 4, TRUE);
INSERT INTO project_task_list_cols (project_id, name, key, index, pinned)
VALUES (_project_id, 'Labels', 'LABELS', 5, TRUE);
VALUES (_project_id, 'Members', 'ASSIGNEES', 5, TRUE);
INSERT INTO project_task_list_cols (project_id, name, key, index, pinned)
VALUES (_project_id, 'Status', 'STATUS', 6, TRUE);
VALUES (_project_id, 'Labels', 'LABELS', 6, TRUE);
INSERT INTO project_task_list_cols (project_id, name, key, index, pinned)
VALUES (_project_id, 'Priority', 'PRIORITY', 7, TRUE);
VALUES (_project_id, 'Phase', 'PHASE', 7, TRUE);
INSERT INTO project_task_list_cols (project_id, name, key, index, pinned)
VALUES (_project_id, 'Time Tracking', 'TIME_TRACKING', 8, TRUE);
VALUES (_project_id, 'Priority', 'PRIORITY', 8, TRUE);
INSERT INTO project_task_list_cols (project_id, name, key, index, pinned)
VALUES (_project_id, 'Estimation', 'ESTIMATION', 9, FALSE);
VALUES (_project_id, 'Time Tracking', 'TIME_TRACKING', 9, TRUE);
INSERT INTO project_task_list_cols (project_id, name, key, index, pinned)
VALUES (_project_id, 'Start Date', 'START_DATE', 10, FALSE);
VALUES (_project_id, 'Estimation', 'ESTIMATION', 10, FALSE);
INSERT INTO project_task_list_cols (project_id, name, key, index, pinned)
VALUES (_project_id, 'Due Date', 'DUE_DATE', 11, TRUE);
VALUES (_project_id, 'Start Date', 'START_DATE', 11, FALSE);
INSERT INTO project_task_list_cols (project_id, name, key, index, pinned)
VALUES (_project_id, 'Completed Date', 'COMPLETED_DATE', 12, FALSE);
VALUES (_project_id, 'Due Date', 'DUE_DATE', 12, TRUE);
INSERT INTO project_task_list_cols (project_id, name, key, index, pinned)
VALUES (_project_id, 'Created Date', 'CREATED_DATE', 13, FALSE);
VALUES (_project_id, 'Completed Date', 'COMPLETED_DATE', 13, FALSE);
INSERT INTO project_task_list_cols (project_id, name, key, index, pinned)
VALUES (_project_id, 'Last Updated', 'LAST_UPDATED', 14, FALSE);
VALUES (_project_id, 'Created Date', 'CREATED_DATE', 14, FALSE);
INSERT INTO project_task_list_cols (project_id, name, key, index, pinned)
VALUES (_project_id, 'Reporter', 'REPORTER', 15, FALSE);
VALUES (_project_id, 'Last Updated', 'LAST_UPDATED', 15, FALSE);
INSERT INTO project_task_list_cols (project_id, name, key, index, pinned)
VALUES (_project_id, 'Phase', 'PHASE', 16, FALSE);
VALUES (_project_id, 'Reporter', 'REPORTER', 16, FALSE);
END
$$;
@@ -5477,8 +5516,15 @@ $$
DECLARE
_iterator NUMERIC := 0;
_status_id TEXT;
_project_id UUID;
_base_sort_order NUMERIC;
BEGIN
-- Get the project_id from the first status to ensure we update all statuses in the same project
SELECT project_id INTO _project_id
FROM task_statuses
WHERE id = (SELECT TRIM(BOTH '"' FROM JSON_ARRAY_ELEMENTS(_status_ids)::TEXT) LIMIT 1)::UUID;
-- Update the sort_order for statuses in the provided order
FOR _status_id IN SELECT * FROM JSON_ARRAY_ELEMENTS((_status_ids)::JSON)
LOOP
UPDATE task_statuses
@@ -5487,6 +5533,29 @@ BEGIN
_iterator := _iterator + 1;
END LOOP;
-- Get the base sort order for remaining statuses (simple count approach)
SELECT COUNT(*) INTO _base_sort_order
FROM task_statuses ts2
WHERE ts2.project_id = _project_id
AND ts2.id = ANY(SELECT (TRIM(BOTH '"' FROM JSON_ARRAY_ELEMENTS(_status_ids)::TEXT))::UUID);
-- Update remaining statuses with simple sequential numbering
-- Reset iterator to start from base_sort_order
_iterator := _base_sort_order;
-- Use a cursor approach to avoid window functions
FOR _status_id IN
SELECT id::TEXT FROM task_statuses
WHERE project_id = _project_id
AND id NOT IN (SELECT (TRIM(BOTH '"' FROM JSON_ARRAY_ELEMENTS(_status_ids)::TEXT))::UUID)
ORDER BY sort_order
LOOP
UPDATE task_statuses
SET sort_order = _iterator
WHERE id = _status_id::UUID;
_iterator := _iterator + 1;
END LOOP;
RETURN;
END
$$;
@@ -6148,3 +6217,434 @@ BEGIN
RETURN v_new_id;
END;
$$;
CREATE OR REPLACE FUNCTION transfer_team_ownership(_team_id UUID, _new_owner_id UUID) RETURNS json
LANGUAGE plpgsql
AS
$$
DECLARE
_old_owner_id UUID;
_owner_role_id UUID;
_admin_role_id UUID;
_old_org_id UUID;
_new_org_id UUID;
_has_license BOOLEAN;
_old_owner_role_id UUID;
_new_owner_role_id UUID;
_has_active_coupon BOOLEAN;
_other_teams_count INTEGER;
_new_owner_org_id UUID;
_license_type_id UUID;
_has_valid_license BOOLEAN;
BEGIN
-- Get the current owner's ID and organization
SELECT t.user_id, t.organization_id
INTO _old_owner_id, _old_org_id
FROM teams t
WHERE t.id = _team_id;
IF _old_owner_id IS NULL THEN
RAISE EXCEPTION 'Team not found';
END IF;
-- Get the new owner's organization
SELECT organization_id INTO _new_owner_org_id
FROM organizations
WHERE user_id = _new_owner_id;
-- Get the old organization
SELECT id INTO _old_org_id
FROM organizations
WHERE id = _old_org_id;
IF _old_org_id IS NULL THEN
RAISE EXCEPTION 'Organization not found';
END IF;
-- Check if new owner has any valid license type
SELECT EXISTS (
SELECT 1
FROM (
-- Check regular subscriptions
SELECT lus.user_id, lus.status, lus.active
FROM licensing_user_subscriptions lus
WHERE lus.user_id = _new_owner_id
AND lus.active = TRUE
AND lus.status IN ('active', 'trialing')
UNION ALL
-- Check custom subscriptions
SELECT lcs.user_id, lcs.subscription_status as status, TRUE as active
FROM licensing_custom_subs lcs
WHERE lcs.user_id = _new_owner_id
AND lcs.end_date > CURRENT_DATE
UNION ALL
-- Check trial status in organizations
SELECT o.user_id, o.subscription_status as status, TRUE as active
FROM organizations o
WHERE o.user_id = _new_owner_id
AND o.trial_in_progress = TRUE
AND o.trial_expire_date > CURRENT_DATE
) valid_licenses
) INTO _has_valid_license;
IF NOT _has_valid_license THEN
RAISE EXCEPTION 'New owner does not have a valid license (subscription, custom subscription, or trial)';
END IF;
-- Check if new owner has any active coupon codes
SELECT EXISTS (
SELECT 1
FROM licensing_coupon_codes lcc
WHERE lcc.redeemed_by = _new_owner_id
AND lcc.is_redeemed = TRUE
AND lcc.is_refunded = FALSE
) INTO _has_active_coupon;
IF _has_active_coupon THEN
RAISE EXCEPTION 'New owner has active coupon codes that need to be handled before transfer';
END IF;
-- Count other teams in the organization for information purposes
SELECT COUNT(*) INTO _other_teams_count
FROM teams
WHERE organization_id = _old_org_id
AND id != _team_id;
-- If new owner has their own organization, move the team to their organization
IF _new_owner_org_id IS NOT NULL THEN
-- Update the team to use the new owner's organization
UPDATE teams
SET user_id = _new_owner_id,
organization_id = _new_owner_org_id
WHERE id = _team_id;
-- Create notification about organization change
PERFORM create_notification(
_old_owner_id,
_team_id,
NULL,
NULL,
CONCAT('Team <b>', (SELECT name FROM teams WHERE id = _team_id), '</b> has been moved to a different organization')
);
PERFORM create_notification(
_new_owner_id,
_team_id,
NULL,
NULL,
CONCAT('Team <b>', (SELECT name FROM teams WHERE id = _team_id), '</b> has been moved to your organization')
);
ELSE
-- If new owner doesn't have an organization, transfer the old organization to them
UPDATE organizations
SET user_id = _new_owner_id
WHERE id = _old_org_id;
-- Update the team to use the same organization
UPDATE teams
SET user_id = _new_owner_id,
organization_id = _old_org_id
WHERE id = _team_id;
-- Notify both users about organization ownership transfer
PERFORM create_notification(
_old_owner_id,
NULL,
NULL,
NULL,
CONCAT('You are no longer the owner of organization <b>', (SELECT organization_name FROM organizations WHERE id = _old_org_id), '</b>')
);
PERFORM create_notification(
_new_owner_id,
NULL,
NULL,
NULL,
CONCAT('You are now the owner of organization <b>', (SELECT organization_name FROM organizations WHERE id = _old_org_id), '</b>')
);
END IF;
-- Get the owner and admin role IDs
SELECT id INTO _owner_role_id FROM roles WHERE team_id = _team_id AND owner = TRUE;
SELECT id INTO _admin_role_id FROM roles WHERE team_id = _team_id AND admin_role = TRUE;
-- Get current role IDs for both users
SELECT role_id INTO _old_owner_role_id
FROM team_members
WHERE team_id = _team_id AND user_id = _old_owner_id;
SELECT role_id INTO _new_owner_role_id
FROM team_members
WHERE team_id = _team_id AND user_id = _new_owner_id;
-- Update the old owner's role to admin if they want to stay in the team
IF _old_owner_role_id IS NOT NULL THEN
UPDATE team_members
SET role_id = _admin_role_id
WHERE team_id = _team_id AND user_id = _old_owner_id;
END IF;
-- Update the new owner's role to owner
IF _new_owner_role_id IS NOT NULL THEN
UPDATE team_members
SET role_id = _owner_role_id
WHERE team_id = _team_id AND user_id = _new_owner_id;
ELSE
-- If new owner is not a team member yet, add them
INSERT INTO team_members (user_id, team_id, role_id)
VALUES (_new_owner_id, _team_id, _owner_role_id);
END IF;
-- Create notification for both users about team ownership
PERFORM create_notification(
_old_owner_id,
_team_id,
NULL,
NULL,
CONCAT('You are no longer the owner of team <b>', (SELECT name FROM teams WHERE id = _team_id), '</b>')
);
PERFORM create_notification(
_new_owner_id,
_team_id,
NULL,
NULL,
CONCAT('You are now the owner of team <b>', (SELECT name FROM teams WHERE id = _team_id), '</b>')
);
RETURN json_build_object(
'success', TRUE,
'old_owner_id', _old_owner_id,
'new_owner_id', _new_owner_id,
'team_id', _team_id,
'old_org_id', _old_org_id,
'new_org_id', COALESCE(_new_owner_org_id, _old_org_id),
'old_role_id', _old_owner_role_id,
'new_role_id', _new_owner_role_id,
'has_valid_license', _has_valid_license,
'has_active_coupon', _has_active_coupon,
'other_teams_count', _other_teams_count,
'org_ownership_transferred', _new_owner_org_id IS NULL,
'team_moved_to_new_org', _new_owner_org_id IS NOT NULL
);
END;
$$;
-- PERFORMANCE OPTIMIZATION: Optimized version with batching for large datasets
CREATE OR REPLACE FUNCTION handle_task_list_sort_between_groups_optimized(_from_index integer, _to_index integer, _task_id uuid, _project_id uuid, _batch_size integer DEFAULT 100) RETURNS void
LANGUAGE plpgsql
AS
$$
DECLARE
_offset INT := 0;
_affected_rows INT;
BEGIN
-- PERFORMANCE OPTIMIZATION: Use direct updates without CTE in UPDATE
IF (_to_index = -1)
THEN
_to_index = COALESCE((SELECT MAX(sort_order) + 1 FROM tasks WHERE project_id = _project_id), 0);
END IF;
-- PERFORMANCE OPTIMIZATION: Batch updates for large datasets
IF _to_index > _from_index
THEN
LOOP
UPDATE tasks
SET sort_order = sort_order - 1
WHERE project_id = _project_id
AND sort_order > _from_index
AND sort_order < _to_index
AND sort_order > _offset
AND sort_order <= _offset + _batch_size;
GET DIAGNOSTICS _affected_rows = ROW_COUNT;
EXIT WHEN _affected_rows = 0;
_offset := _offset + _batch_size;
END LOOP;
UPDATE tasks SET sort_order = _to_index - 1 WHERE id = _task_id AND project_id = _project_id;
END IF;
IF _to_index < _from_index
THEN
_offset := 0;
LOOP
UPDATE tasks
SET sort_order = sort_order + 1
WHERE project_id = _project_id
AND sort_order > _to_index
AND sort_order < _from_index
AND sort_order > _offset
AND sort_order <= _offset + _batch_size;
GET DIAGNOSTICS _affected_rows = ROW_COUNT;
EXIT WHEN _affected_rows = 0;
_offset := _offset + _batch_size;
END LOOP;
UPDATE tasks SET sort_order = _to_index + 1 WHERE id = _task_id AND project_id = _project_id;
END IF;
END
$$;
-- PERFORMANCE OPTIMIZATION: Optimized version with batching for large datasets
CREATE OR REPLACE FUNCTION handle_task_list_sort_inside_group_optimized(_from_index integer, _to_index integer, _task_id uuid, _project_id uuid, _batch_size integer DEFAULT 100) RETURNS void
LANGUAGE plpgsql
AS
$$
DECLARE
_offset INT := 0;
_affected_rows INT;
BEGIN
-- PERFORMANCE OPTIMIZATION: Batch updates for large datasets without CTE in UPDATE
IF _to_index > _from_index
THEN
LOOP
UPDATE tasks
SET sort_order = sort_order - 1
WHERE project_id = _project_id
AND sort_order > _from_index
AND sort_order <= _to_index
AND sort_order > _offset
AND sort_order <= _offset + _batch_size;
GET DIAGNOSTICS _affected_rows = ROW_COUNT;
EXIT WHEN _affected_rows = 0;
_offset := _offset + _batch_size;
END LOOP;
END IF;
IF _to_index < _from_index
THEN
_offset := 0;
LOOP
UPDATE tasks
SET sort_order = sort_order + 1
WHERE project_id = _project_id
AND sort_order >= _to_index
AND sort_order < _from_index
AND sort_order > _offset
AND sort_order <= _offset + _batch_size;
GET DIAGNOSTICS _affected_rows = ROW_COUNT;
EXIT WHEN _affected_rows = 0;
_offset := _offset + _batch_size;
END LOOP;
END IF;
UPDATE tasks SET sort_order = _to_index WHERE id = _task_id AND project_id = _project_id;
END
$$;
-- Updated bulk sort order function that avoids sort_order conflicts
CREATE OR REPLACE FUNCTION update_task_sort_orders_bulk(_updates json, _group_by text DEFAULT 'status') RETURNS void
LANGUAGE plpgsql
AS
$$
DECLARE
_update_record RECORD;
_sort_column TEXT;
_sql TEXT;
BEGIN
-- Get the appropriate sort column based on grouping
_sort_column := get_sort_column_name(_group_by);
-- Process each update record
FOR _update_record IN
SELECT
(item->>'task_id')::uuid as task_id,
(item->>'sort_order')::int as sort_order,
(item->>'status_id')::uuid as status_id,
(item->>'priority_id')::uuid as priority_id,
(item->>'phase_id')::uuid as phase_id
FROM json_array_elements(_updates) as item
LOOP
-- Update the grouping-specific sort column and other fields
_sql := 'UPDATE tasks SET ' || _sort_column || ' = $1, ' ||
'status_id = COALESCE($2, status_id), ' ||
'priority_id = COALESCE($3, priority_id), ' ||
'updated_at = CURRENT_TIMESTAMP ' ||
'WHERE id = $4';
EXECUTE _sql USING
_update_record.sort_order,
_update_record.status_id,
_update_record.priority_id,
_update_record.task_id;
-- Handle phase updates separately since it's in a different table
IF _update_record.phase_id IS NOT NULL THEN
INSERT INTO task_phase (task_id, phase_id)
VALUES (_update_record.task_id, _update_record.phase_id)
ON CONFLICT (task_id) DO UPDATE SET phase_id = _update_record.phase_id;
END IF;
END LOOP;
END
$$;
-- Function to get the appropriate sort column name based on grouping type
CREATE OR REPLACE FUNCTION get_sort_column_name(_group_by TEXT) RETURNS TEXT
LANGUAGE plpgsql
AS
$$
BEGIN
CASE _group_by
WHEN 'status' THEN RETURN 'status_sort_order';
WHEN 'priority' THEN RETURN 'priority_sort_order';
WHEN 'phase' THEN RETURN 'phase_sort_order';
-- For backward compatibility, still support general sort_order but be explicit
WHEN 'general' THEN RETURN 'sort_order';
ELSE RETURN 'status_sort_order'; -- Default to status sorting
END CASE;
END;
$$;
-- Updated bulk sort order function to handle different sort columns
CREATE OR REPLACE FUNCTION update_task_sort_orders_bulk(_updates json, _group_by text DEFAULT 'status') RETURNS void
LANGUAGE plpgsql
AS
$$
DECLARE
_update_record RECORD;
_sort_column TEXT;
_sql TEXT;
BEGIN
-- Get the appropriate sort column based on grouping
_sort_column := get_sort_column_name(_group_by);
-- Process each update record
FOR _update_record IN
SELECT
(item->>'task_id')::uuid as task_id,
(item->>'sort_order')::int as sort_order,
(item->>'status_id')::uuid as status_id,
(item->>'priority_id')::uuid as priority_id,
(item->>'phase_id')::uuid as phase_id
FROM json_array_elements(_updates) as item
LOOP
-- Update the grouping-specific sort column and other fields
_sql := 'UPDATE tasks SET ' || _sort_column || ' = $1, ' ||
'status_id = COALESCE($2, status_id), ' ||
'priority_id = COALESCE($3, priority_id), ' ||
'updated_at = CURRENT_TIMESTAMP ' ||
'WHERE id = $4';
EXECUTE _sql USING
_update_record.sort_order,
_update_record.status_id,
_update_record.priority_id,
_update_record.task_id;
-- Handle phase updates separately since it's in a different table
IF _update_record.phase_id IS NOT NULL THEN
INSERT INTO task_phase (task_id, phase_id)
VALUES (_update_record.task_id, _update_record.phase_id)
ON CONFLICT (task_id) DO UPDATE SET phase_id = _update_record.phase_id;
END IF;
END LOOP;
END;
$$;

View File

@@ -132,3 +132,139 @@ CREATE INDEX IF NOT EXISTS projects_team_id_index
CREATE INDEX IF NOT EXISTS projects_team_id_name_index
ON projects (team_id, name);
-- Performance indexes for optimized tasks queries
-- From migration: 20250115000000-performance-indexes.sql
-- Composite index for main task filtering
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_tasks_project_archived_parent
ON tasks(project_id, archived, parent_task_id)
WHERE archived = FALSE;
-- Index for status joins
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_tasks_status_project
ON tasks(status_id, project_id)
WHERE archived = FALSE;
-- Index for assignees lookup
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_tasks_assignees_task_member
ON tasks_assignees(task_id, team_member_id);
-- Index for phase lookup
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_task_phase_task_phase
ON task_phase(task_id, phase_id);
-- Index for subtask counting
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_tasks_parent_archived
ON tasks(parent_task_id, archived)
WHERE parent_task_id IS NOT NULL AND archived = FALSE;
-- Index for labels
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_task_labels_task_label
ON task_labels(task_id, label_id);
-- Index for comments count
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_task_comments_task
ON task_comments(task_id);
-- Index for attachments count
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_task_attachments_task
ON task_attachments(task_id);
-- Index for work log aggregation
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_task_work_log_task
ON task_work_log(task_id);
-- Index for subscribers check
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_task_subscribers_task
ON task_subscribers(task_id);
-- Index for dependencies check
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_task_dependencies_task
ON task_dependencies(task_id);
-- Index for timers lookup
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_task_timers_task_user
ON task_timers(task_id, user_id);
-- Index for custom columns
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_cc_column_values_task
ON cc_column_values(task_id);
-- Index for team member info view optimization
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_team_members_team_user
ON team_members(team_id, user_id)
WHERE active = TRUE;
-- Index for notification settings
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_notification_settings_user_team
ON notification_settings(user_id, team_id);
-- Index for task status categories
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_task_statuses_category
ON task_statuses(category_id, project_id);
-- Index for project phases
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_project_phases_project_sort
ON project_phases(project_id, sort_index);
-- Index for task priorities
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_task_priorities_value
ON task_priorities(value);
-- Index for team labels
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_team_labels_team
ON team_labels(team_id);
-- Advanced performance indexes for task optimization
-- Composite index for task main query optimization (covers most WHERE conditions)
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_tasks_performance_main
ON tasks(project_id, archived, parent_task_id, status_id, priority_id)
WHERE archived = FALSE;
-- Index for sorting by sort_order with project filter
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_tasks_project_sort_order
ON tasks(project_id, sort_order)
WHERE archived = FALSE;
-- Index for email_invitations to optimize team_member_info_view
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_email_invitations_team_member
ON email_invitations(team_member_id);
-- Covering index for task status with category information
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_task_statuses_covering
ON task_statuses(id, category_id, project_id);
-- Index for task aggregation queries (parent task progress calculation)
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_tasks_parent_status_archived
ON tasks(parent_task_id, status_id, archived)
WHERE archived = FALSE;
-- Index for project team member filtering
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_team_members_project_lookup
ON team_members(team_id, active, user_id)
WHERE active = TRUE;
-- Covering index for tasks with frequently accessed columns
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_tasks_covering_main
ON tasks(id, project_id, archived, parent_task_id, status_id, priority_id, sort_order, name)
WHERE archived = FALSE;
-- Index for task search functionality
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_tasks_name_search
ON tasks USING gin(to_tsvector('english', name))
WHERE archived = FALSE;
-- Index for date-based filtering (if used)
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_tasks_dates
ON tasks(project_id, start_date, end_date)
WHERE archived = FALSE;
-- Index for task timers with user filtering
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_task_timers_user_task
ON task_timers(user_id, task_id);
-- Index for sys_task_status_categories lookups
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_sys_task_status_categories_covering
ON sys_task_status_categories(id, color_code, color_code_dark, is_done, is_doing, is_todo);

View File

@@ -0,0 +1,352 @@
# 🌍 Holiday Calendar System
The Worklenz Holiday Calendar System provides comprehensive holiday management for organizations operating globally.
## 📋 Features
- **200+ Countries Supported** - Comprehensive holiday data for countries worldwide
- **Multiple Holiday Types** - Public, Company, Personal, and Religious holidays
- **Import Country Holidays** - Bulk import official holidays from any supported country
- **Manual Holiday Management** - Add, edit, and delete custom holidays
- **Recurring Holidays** - Support for annual recurring holidays
- **Visual Calendar** - Interactive calendar with color-coded holiday display
- **Dark/Light Mode** - Full theme support
## 🚀 Quick Start
### 1. Database Setup
Run the migration to create the holiday tables:
```bash
# Run the migration
psql -d your_database -f database/migrations/20250130000000-add-holiday-calendar.sql
```
### 2. Populate Country Holidays
Use the npm package to populate holidays for 200+ countries:
```bash
# Run the holiday population script
./scripts/run-holiday-population.sh
```
This will populate holidays for years 2020-2030 for all supported countries.
### 3. Access the Holiday Calendar
Navigate to **Admin Center → Overview** to access the holiday calendar.
## 🌐 Supported Countries
The system includes **200+ countries** across all continents:
### North America
- United States 🇺🇸
- Canada 🇨🇦
- Mexico 🇲🇽
### Europe
- United Kingdom 🇬🇧
- Germany 🇩🇪
- France 🇫🇷
- Italy 🇮🇹
- Spain 🇪🇸
- Netherlands 🇳🇱
- Belgium 🇧🇪
- Switzerland 🇨🇭
- Austria 🇦🇹
- Sweden 🇸🇪
- Norway 🇳🇴
- Denmark 🇩🇰
- Finland 🇫🇮
- Poland 🇵🇱
- Czech Republic 🇨🇿
- Hungary 🇭🇺
- Romania 🇷🇴
- Bulgaria 🇧🇬
- Croatia 🇭🇷
- Slovenia 🇸🇮
- Slovakia 🇸🇰
- Lithuania 🇱🇹
- Latvia 🇱🇻
- Estonia 🇪🇪
- Ireland 🇮🇪
- Portugal 🇵🇹
- Greece 🇬🇷
- Cyprus 🇨🇾
- Malta 🇲🇹
- Luxembourg 🇱🇺
- Iceland 🇮🇸
### Asia
- China 🇨🇳
- Japan 🇯🇵
- South Korea 🇰🇷
- India 🇮🇳
- Pakistan 🇵🇰
- Bangladesh 🇧🇩
- Sri Lanka 🇱🇰
- Nepal 🇳🇵
- Thailand 🇹🇭
- Vietnam 🇻🇳
- Malaysia 🇲🇾
- Singapore 🇸🇬
- Indonesia 🇮🇩
- Philippines 🇵🇭
- Myanmar 🇲🇲
- Cambodia 🇰🇭
- Laos 🇱🇦
- Brunei 🇧🇳
- Timor-Leste 🇹🇱
- Mongolia 🇲🇳
- Kazakhstan 🇰🇿
- Uzbekistan 🇺🇿
- Kyrgyzstan 🇰🇬
- Tajikistan 🇹🇯
- Turkmenistan 🇹🇲
- Afghanistan 🇦🇫
- Iran 🇮🇷
- Iraq 🇮🇶
- Saudi Arabia 🇸🇦
- UAE 🇦🇪
- Qatar 🇶🇦
- Kuwait 🇰🇼
- Bahrain 🇧🇭
- Oman 🇴🇲
- Yemen 🇾🇪
- Jordan 🇯🇴
- Lebanon 🇱🇧
- Syria 🇸🇾
- Israel 🇮🇱
- Palestine 🇵🇸
- Turkey 🇹🇷
- Georgia 🇬🇪
- Armenia 🇦🇲
- Azerbaijan 🇦🇿
### Oceania
- Australia 🇦🇺
- New Zealand 🇳🇿
- Fiji 🇫🇯
- Papua New Guinea 🇵🇬
- Solomon Islands 🇸🇧
- Vanuatu 🇻🇺
- New Caledonia 🇳🇨
- French Polynesia 🇵🇫
- Tonga 🇹🇴
- Samoa 🇼🇸
- Kiribati 🇰🇮
- Tuvalu 🇹🇻
- Nauru 🇳🇷
- Palau 🇵🇼
- Marshall Islands 🇲🇭
- Micronesia 🇫🇲
### Africa
- South Africa 🇿🇦
- Egypt 🇪🇬
- Nigeria 🇳🇬
- Kenya 🇰🇪
- Ethiopia 🇪🇹
- Tanzania 🇹🇿
- Uganda 🇺🇬
- Ghana 🇬🇭
- Ivory Coast 🇨🇮
- Senegal 🇸🇳
- Mali 🇲🇱
- Burkina Faso 🇧🇫
- Niger 🇳🇪
- Chad 🇹🇩
- Cameroon 🇨🇲
- Central African Republic 🇨🇫
- Republic of the Congo 🇨🇬
- Democratic Republic of the Congo 🇨🇩
- Gabon 🇬🇦
- Equatorial Guinea 🇬🇶
- São Tomé and Príncipe 🇸🇹
- Angola 🇦🇴
- Zambia 🇿🇲
- Zimbabwe 🇿🇼
- Botswana 🇧🇼
- Namibia 🇳🇦
- Lesotho 🇱🇸
- Eswatini 🇸🇿
- Madagascar 🇲🇬
- Mauritius 🇲🇺
- Seychelles 🇸🇨
- Comoros 🇰🇲
- Djibouti 🇩🇯
- Somalia 🇸🇴
- Eritrea 🇪🇷
- Sudan 🇸🇩
- South Sudan 🇸🇸
- Libya 🇱🇾
- Tunisia 🇹🇳
- Algeria 🇩🇿
- Morocco 🇲🇦
- Western Sahara 🇪🇭
- Mauritania 🇲🇷
- Gambia 🇬🇲
- Guinea-Bissau 🇬🇼
- Guinea 🇬🇳
- Sierra Leone 🇸🇱
- Liberia 🇱🇷
- Togo 🇹🇬
- Benin 🇧🇯
### South America
- Brazil 🇧🇷
- Argentina 🇦🇷
- Chile 🇨🇱
- Colombia 🇨🇴
- Peru 🇵🇪
- Venezuela 🇻🇪
- Ecuador 🇪🇨
- Bolivia 🇧🇴
- Paraguay 🇵🇾
- Uruguay 🇺🇾
- Guyana 🇬🇾
- Suriname 🇸🇷
- Falkland Islands 🇫🇰
- French Guiana 🇬🇫
### Central America & Caribbean
- Mexico 🇲🇽
- Guatemala 🇬🇹
- Belize 🇧🇿
- El Salvador 🇸🇻
- Honduras 🇭🇳
- Nicaragua 🇳🇮
- Costa Rica 🇨🇷
- Panama 🇵🇦
- Cuba 🇨🇺
- Jamaica 🇯🇲
- Haiti 🇭🇹
- Dominican Republic 🇩🇴
- Puerto Rico 🇵🇷
- Trinidad and Tobago 🇹🇹
- Barbados 🇧🇧
- Grenada 🇬🇩
- Saint Lucia 🇱🇨
- Saint Vincent and the Grenadines 🇻🇨
- Antigua and Barbuda 🇦🇬
- Saint Kitts and Nevis 🇰🇳
- Dominica 🇩🇲
- Bahamas 🇧🇸
- Turks and Caicos Islands 🇹🇨
- Cayman Islands 🇰🇾
- Bermuda 🇧🇲
- Anguilla 🇦🇮
- British Virgin Islands 🇻🇬
- U.S. Virgin Islands 🇻🇮
- Aruba 🇦🇼
- Curaçao 🇨🇼
- Sint Maarten 🇸🇽
- Saint Martin 🇲🇫
- Saint Barthélemy 🇧🇱
- Guadeloupe 🇬🇵
- Martinique 🇲🇶
## 🔧 API Endpoints
### Holiday Types
```http
GET /api/holidays/types
```
### Organization Holidays
```http
GET /api/holidays/organization?year=2024
POST /api/holidays/organization
PUT /api/holidays/organization/:id
DELETE /api/holidays/organization/:id
```
### Country Holidays
```http
GET /api/holidays/countries
GET /api/holidays/countries/:country_code?year=2024
POST /api/holidays/import
```
### Calendar View
```http
GET /api/holidays/calendar?year=2024&month=1
```
## 📊 Holiday Types
The system supports four types of holidays:
1. **Public Holiday** - Official government holidays (Red)
2. **Company Holiday** - Organization-specific holidays (Blue)
3. **Personal Holiday** - Personal or optional holidays (Green)
4. **Religious Holiday** - Religious observances (Yellow)
## 🎯 Usage Examples
### Import US Holidays
```javascript
const result = await holidayApiService.importCountryHolidays({
country_code: 'US',
year: 2024
});
```
### Add Custom Holiday
```javascript
const holiday = await holidayApiService.createOrganizationHoliday({
name: 'Company Retreat',
description: 'Annual team building event',
date: '2024-06-15',
holiday_type_id: 'company-holiday-id',
is_recurring: true
});
```
### Get Calendar View
```javascript
const calendar = await holidayApiService.getHolidayCalendar(2024, 1);
```
## 🔄 Data Sources
The holiday data is sourced from the `date-holidays` npm package, which provides:
- **Official government holidays** for 200+ countries
- **Religious holidays** (Christian, Islamic, Jewish, Hindu, Buddhist)
- **Cultural and traditional holidays**
- **Historical and commemorative days**
## 🛠️ Maintenance
### Adding New Countries
1. Add the country to the `countries` table
2. Update the `populate-holidays.js` script
3. Run the population script
### Updating Holiday Data
```bash
# Re-run the holiday population script
./scripts/run-holiday-population.sh
```
## 📝 Notes
- Holidays are stored for years 2020-2030 by default
- The system prevents duplicate holidays on the same date
- Imported holidays are automatically classified as "Public Holiday" type
- All holidays support recurring annual patterns
- The calendar view combines organization and country holidays
## 🎉 Benefits
- **Global Compliance** - Ensure compliance with local holiday regulations
- **Resource Planning** - Better project scheduling and resource allocation
- **Team Coordination** - Improved team communication and planning
- **Cost Management** - Accurate billing and time tracking
- **Cultural Awareness** - Respect for diverse cultural and religious practices

View File

@@ -1,28 +0,0 @@
module.exports = {
brotli_js: {
options: {
mode: "brotli",
brotli: {
mode: 1
}
},
expand: true,
cwd: "build/public",
src: ["**/*.js"],
dest: "build/public",
extDot: "last",
ext: ".js.br"
},
gzip_js: {
options: {
mode: "gzip"
},
files: [{
expand: true,
cwd: "build/public",
src: ["**/*.js"],
dest: "build/public",
ext: ".js.gz"
}]
}
};

File diff suppressed because it is too large Load Diff

View File

@@ -4,23 +4,37 @@
"private": true,
"engines": {
"npm": ">=8.11.0",
"node": ">=16.13.0",
"node": ">=20.0.0",
"yarn": "WARNING: Please use npm package manager instead of yarn"
},
"main": "build/bin/www",
"repository": "GITHUB_REPO_HERE",
"author": "worklenz.com",
"scripts": {
"start": "node ./build/bin/www",
"tcs": "grunt build:tsc",
"build": "grunt build",
"watch": "grunt watch",
"dev": "grunt dev",
"es": "esbuild `find src -type f -name '*.ts'` --platform=node --minify=true --watch=true --target=esnext --format=cjs --tsconfig=tsconfig.prod.json --outdir=dist",
"copy": "grunt copy",
"test": "jest",
"start": "node build/bin/www.js",
"dev": "npm run build:dev && npm run watch",
"build": "npm run clean && npm run compile && npm run copy && npm run compress",
"build:dev": "npm run clean && npm run compile:dev && npm run copy",
"build:prod": "npm run clean && npm run compile:prod && npm run copy && npm run minify && npm run compress",
"clean": "rimraf build",
"compile": "tsc --build tsconfig.prod.json",
"compile:dev": "tsc --build tsconfig.json",
"compile:prod": "tsc --build tsconfig.prod.json",
"copy": "npm run copy:assets && npm run copy:views && npm run copy:config && npm run copy:shared",
"copy:assets": "npx cpx2 \"src/public/**\" build/public",
"copy:views": "npx cpx2 \"src/views/**\" build/views",
"copy:config": "npx cpx2 \".env\" build && npx cpx2 \"package.json\" build",
"copy:shared": "npx cpx2 \"src/shared/postgresql-error-codes.json\" build/shared && npx cpx2 \"src/shared/sample-data.json\" build/shared && npx cpx2 \"src/shared/templates/**\" build/shared/templates",
"watch": "concurrently \"npm run watch:ts\" \"npm run watch:assets\"",
"watch:ts": "tsc --build tsconfig.json --watch",
"watch:assets": "npx cpx2 \"src/{public,views}/**\" build --watch",
"minify": "terser build/**/*.js --compress --mangle --output-dir build",
"compress": "node scripts/compress.js",
"swagger": "node ./cli/swagger",
"inline-queries": "node ./cli/inline-queries",
"sonar": "sonar-scanner -Dproject.settings=sonar-project-dev.properties",
"tsc": "tsc",
"test": "jest --setupFiles dotenv/config",
"test:watch": "jest --watch --setupFiles dotenv/config"
},
"jestSonar": {
@@ -45,7 +59,9 @@
"cors": "^2.8.5",
"cron": "^2.4.0",
"crypto-js": "^4.1.1",
"csrf-sync": "^4.2.1",
"csurf": "^1.11.0",
"date-holidays": "^3.24.4",
"debug": "^4.3.4",
"dotenv": "^16.3.1",
"exceljs": "^4.3.0",
@@ -70,7 +86,6 @@
"passport-local": "^1.0.0",
"path": "^0.12.7",
"pg": "^8.14.1",
"pg-native": "^3.3.0",
"pug": "^3.0.2",
"redis": "^4.6.7",
"sanitize-html": "^2.11.0",
@@ -78,8 +93,10 @@
"sharp": "^0.32.6",
"slugify": "^1.6.6",
"socket.io": "^4.7.1",
"tinymce": "^7.8.0",
"uglify-js": "^3.17.4",
"winston": "^3.10.0",
"worklenz-backend": "file:",
"xss-filters": "^1.2.7"
},
"devDependencies": {
@@ -87,15 +104,17 @@
"@babel/preset-typescript": "^7.22.5",
"@types/bcrypt": "^5.0.0",
"@types/bluebird": "^3.5.38",
"@types/body-parser": "^1.19.2",
"@types/compression": "^1.7.2",
"@types/connect-flash": "^0.0.37",
"@types/cookie-parser": "^1.4.3",
"@types/cron": "^2.0.1",
"@types/crypto-js": "^4.2.2",
"@types/csurf": "^1.11.2",
"@types/express": "^4.17.17",
"@types/express": "^4.17.21",
"@types/express-brute": "^1.0.2",
"@types/express-brute-redis": "^0.0.4",
"@types/express-serve-static-core": "^4.17.34",
"@types/express-session": "^1.17.7",
"@types/fs-extra": "^9.0.13",
"@types/hpp": "^0.2.2",
@@ -120,26 +139,22 @@
"@typescript-eslint/eslint-plugin": "^5.62.0",
"@typescript-eslint/parser": "^5.62.0",
"chokidar": "^3.5.3",
"concurrently": "^9.1.2",
"cpx2": "^8.0.0",
"esbuild": "^0.17.19",
"esbuild-envfile-plugin": "^1.0.5",
"esbuild-node-externals": "^1.8.0",
"eslint": "^8.45.0",
"eslint-plugin-security": "^1.7.1",
"fs-extra": "^10.1.0",
"grunt": "^1.6.1",
"grunt-contrib-clean": "^2.0.1",
"grunt-contrib-compress": "^2.0.0",
"grunt-contrib-copy": "^1.0.0",
"grunt-contrib-uglify": "^5.2.2",
"grunt-contrib-watch": "^1.1.0",
"grunt-shell": "^4.0.0",
"grunt-sync": "^0.8.2",
"highcharts": "^11.1.0",
"jest": "^28.1.3",
"jest-sonar-reporter": "^2.0.0",
"ncp": "^2.0.0",
"nodeman": "^1.1.2",
"rimraf": "^6.0.1",
"swagger-jsdoc": "^6.2.8",
"terser": "^5.40.0",
"ts-jest": "^28.0.8",
"ts-node": "^10.9.1",
"tslint": "^6.1.3",

View File

@@ -0,0 +1,53 @@
const fs = require('fs');
const path = require('path');
const { createGzip } = require('zlib');
const { pipeline } = require('stream');
async function compressFile(inputPath, outputPath) {
return new Promise((resolve, reject) => {
const gzip = createGzip();
const source = fs.createReadStream(inputPath);
const destination = fs.createWriteStream(outputPath);
pipeline(source, gzip, destination, (err) => {
if (err) {
reject(err);
} else {
resolve();
}
});
});
}
async function compressDirectory(dir) {
const files = fs.readdirSync(dir, { withFileTypes: true });
for (const file of files) {
const fullPath = path.join(dir, file.name);
if (file.isDirectory()) {
await compressDirectory(fullPath);
} else if (file.name.endsWith('.js') || file.name.endsWith('.css')) {
const gzPath = fullPath + '.gz';
await compressFile(fullPath, gzPath);
console.log(`Compressed: ${fullPath} -> ${gzPath}`);
}
}
}
async function main() {
try {
const buildDir = path.join(__dirname, '../build');
if (fs.existsSync(buildDir)) {
await compressDirectory(buildDir);
console.log('Compression complete!');
} else {
console.log('Build directory not found. Run build first.');
}
} catch (error) {
console.error('Compression failed:', error);
process.exit(1);
}
}
main();

View File

@@ -0,0 +1,265 @@
const Holidays = require("date-holidays");
const { Pool } = require("pg");
const config = require("../build/config/db-config").default;
// Database connection
const pool = new Pool(config);
// Countries to populate with holidays
const countries = [
{ code: "US", name: "United States" },
{ code: "GB", name: "United Kingdom" },
{ code: "CA", name: "Canada" },
{ code: "AU", name: "Australia" },
{ code: "DE", name: "Germany" },
{ code: "FR", name: "France" },
{ code: "IT", name: "Italy" },
{ code: "ES", name: "Spain" },
{ code: "NL", name: "Netherlands" },
{ code: "BE", name: "Belgium" },
{ code: "CH", name: "Switzerland" },
{ code: "AT", name: "Austria" },
{ code: "SE", name: "Sweden" },
{ code: "NO", name: "Norway" },
{ code: "DK", name: "Denmark" },
{ code: "FI", name: "Finland" },
{ code: "PL", name: "Poland" },
{ code: "CZ", name: "Czech Republic" },
{ code: "HU", name: "Hungary" },
{ code: "RO", name: "Romania" },
{ code: "BG", name: "Bulgaria" },
{ code: "HR", name: "Croatia" },
{ code: "SI", name: "Slovenia" },
{ code: "SK", name: "Slovakia" },
{ code: "LT", name: "Lithuania" },
{ code: "LV", name: "Latvia" },
{ code: "EE", name: "Estonia" },
{ code: "IE", name: "Ireland" },
{ code: "PT", name: "Portugal" },
{ code: "GR", name: "Greece" },
{ code: "CY", name: "Cyprus" },
{ code: "MT", name: "Malta" },
{ code: "LU", name: "Luxembourg" },
{ code: "IS", name: "Iceland" },
{ code: "CN", name: "China" },
{ code: "JP", name: "Japan" },
{ code: "KR", name: "South Korea" },
{ code: "IN", name: "India" },
{ code: "PK", name: "Pakistan" },
{ code: "BD", name: "Bangladesh" },
{ code: "LK", name: "Sri Lanka" },
{ code: "NP", name: "Nepal" },
{ code: "TH", name: "Thailand" },
{ code: "VN", name: "Vietnam" },
{ code: "MY", name: "Malaysia" },
{ code: "SG", name: "Singapore" },
{ code: "ID", name: "Indonesia" },
{ code: "PH", name: "Philippines" },
{ code: "MM", name: "Myanmar" },
{ code: "KH", name: "Cambodia" },
{ code: "LA", name: "Laos" },
{ code: "BN", name: "Brunei" },
{ code: "TL", name: "Timor-Leste" },
{ code: "MN", name: "Mongolia" },
{ code: "KZ", name: "Kazakhstan" },
{ code: "UZ", name: "Uzbekistan" },
{ code: "KG", name: "Kyrgyzstan" },
{ code: "TJ", name: "Tajikistan" },
{ code: "TM", name: "Turkmenistan" },
{ code: "AF", name: "Afghanistan" },
{ code: "IR", name: "Iran" },
{ code: "IQ", name: "Iraq" },
{ code: "SA", name: "Saudi Arabia" },
{ code: "AE", name: "United Arab Emirates" },
{ code: "QA", name: "Qatar" },
{ code: "KW", name: "Kuwait" },
{ code: "BH", name: "Bahrain" },
{ code: "OM", name: "Oman" },
{ code: "YE", name: "Yemen" },
{ code: "JO", name: "Jordan" },
{ code: "LB", name: "Lebanon" },
{ code: "SY", name: "Syria" },
{ code: "IL", name: "Israel" },
{ code: "PS", name: "Palestine" },
{ code: "TR", name: "Turkey" },
{ code: "GE", name: "Georgia" },
{ code: "AM", name: "Armenia" },
{ code: "AZ", name: "Azerbaijan" },
{ code: "NZ", name: "New Zealand" },
{ code: "FJ", name: "Fiji" },
{ code: "PG", name: "Papua New Guinea" },
{ code: "SB", name: "Solomon Islands" },
{ code: "VU", name: "Vanuatu" },
{ code: "NC", name: "New Caledonia" },
{ code: "PF", name: "French Polynesia" },
{ code: "TO", name: "Tonga" },
{ code: "WS", name: "Samoa" },
{ code: "KI", name: "Kiribati" },
{ code: "TV", name: "Tuvalu" },
{ code: "NR", name: "Nauru" },
{ code: "PW", name: "Palau" },
{ code: "MH", name: "Marshall Islands" },
{ code: "FM", name: "Micronesia" },
{ code: "ZA", name: "South Africa" },
{ code: "EG", name: "Egypt" },
{ code: "NG", name: "Nigeria" },
{ code: "KE", name: "Kenya" },
{ code: "ET", name: "Ethiopia" },
{ code: "TZ", name: "Tanzania" },
{ code: "UG", name: "Uganda" },
{ code: "GH", name: "Ghana" },
{ code: "CI", name: "Ivory Coast" },
{ code: "SN", name: "Senegal" },
{ code: "ML", name: "Mali" },
{ code: "BF", name: "Burkina Faso" },
{ code: "NE", name: "Niger" },
{ code: "TD", name: "Chad" },
{ code: "CM", name: "Cameroon" },
{ code: "CF", name: "Central African Republic" },
{ code: "CG", name: "Republic of the Congo" },
{ code: "CD", name: "Democratic Republic of the Congo" },
{ code: "GA", name: "Gabon" },
{ code: "GQ", name: "Equatorial Guinea" },
{ code: "ST", name: "São Tomé and Príncipe" },
{ code: "AO", name: "Angola" },
{ code: "ZM", name: "Zambia" },
{ code: "ZW", name: "Zimbabwe" },
{ code: "BW", name: "Botswana" },
{ code: "NA", name: "Namibia" },
{ code: "LS", name: "Lesotho" },
{ code: "SZ", name: "Eswatini" },
{ code: "MG", name: "Madagascar" },
{ code: "MU", name: "Mauritius" },
{ code: "SC", name: "Seychelles" },
{ code: "KM", name: "Comoros" },
{ code: "DJ", name: "Djibouti" },
{ code: "SO", name: "Somalia" },
{ code: "ER", name: "Eritrea" },
{ code: "SD", name: "Sudan" },
{ code: "SS", name: "South Sudan" },
{ code: "LY", name: "Libya" },
{ code: "TN", name: "Tunisia" },
{ code: "DZ", name: "Algeria" },
{ code: "MA", name: "Morocco" },
{ code: "EH", name: "Western Sahara" },
{ code: "MR", name: "Mauritania" },
{ code: "GM", name: "Gambia" },
{ code: "GW", name: "Guinea-Bissau" },
{ code: "GN", name: "Guinea" },
{ code: "SL", name: "Sierra Leone" },
{ code: "LR", name: "Liberia" },
{ code: "TG", name: "Togo" },
{ code: "BJ", name: "Benin" },
{ code: "BR", name: "Brazil" },
{ code: "AR", name: "Argentina" },
{ code: "CL", name: "Chile" },
{ code: "CO", name: "Colombia" },
{ code: "PE", name: "Peru" },
{ code: "VE", name: "Venezuela" },
{ code: "EC", name: "Ecuador" },
{ code: "BO", name: "Bolivia" },
{ code: "PY", name: "Paraguay" },
{ code: "UY", name: "Uruguay" },
{ code: "GY", name: "Guyana" },
{ code: "SR", name: "Suriname" },
{ code: "FK", name: "Falkland Islands" },
{ code: "GF", name: "French Guiana" },
{ code: "MX", name: "Mexico" },
{ code: "GT", name: "Guatemala" },
{ code: "BZ", name: "Belize" },
{ code: "SV", name: "El Salvador" },
{ code: "HN", name: "Honduras" },
{ code: "NI", name: "Nicaragua" },
{ code: "CR", name: "Costa Rica" },
{ code: "PA", name: "Panama" },
{ code: "CU", name: "Cuba" },
{ code: "JM", name: "Jamaica" },
{ code: "HT", name: "Haiti" },
{ code: "DO", name: "Dominican Republic" },
{ code: "PR", name: "Puerto Rico" },
{ code: "TT", name: "Trinidad and Tobago" },
{ code: "BB", name: "Barbados" },
{ code: "GD", name: "Grenada" },
{ code: "LC", name: "Saint Lucia" },
{ code: "VC", name: "Saint Vincent and the Grenadines" },
{ code: "AG", name: "Antigua and Barbuda" },
{ code: "KN", name: "Saint Kitts and Nevis" },
{ code: "DM", name: "Dominica" },
{ code: "BS", name: "Bahamas" },
{ code: "TC", name: "Turks and Caicos Islands" },
{ code: "KY", name: "Cayman Islands" },
{ code: "BM", name: "Bermuda" },
{ code: "AI", name: "Anguilla" },
{ code: "VG", name: "British Virgin Islands" },
{ code: "VI", name: "U.S. Virgin Islands" },
{ code: "AW", name: "Aruba" },
{ code: "CW", name: "Curaçao" },
{ code: "SX", name: "Sint Maarten" },
{ code: "MF", name: "Saint Martin" },
{ code: "BL", name: "Saint Barthélemy" },
{ code: "GP", name: "Guadeloupe" },
{ code: "MQ", name: "Martinique" }
];
async function populateHolidays() {
const client = await pool.connect();
try {
console.log("Starting holiday population...");
for (const country of countries) {
console.log(`Processing ${country.name} (${country.code})...`);
try {
const hd = new Holidays(country.code);
// Get holidays for multiple years (2020-2030)
for (let year = 2020; year <= 2030; year++) {
const holidays = hd.getHolidays(year);
for (const holiday of holidays) {
// Skip if holiday is not a date object
if (!holiday.date || typeof holiday.date !== "object") {
continue;
}
const dateStr = holiday.date.toISOString().split("T")[0];
const name = holiday.name || "Unknown Holiday";
const description = holiday.type || "Public Holiday";
// Insert holiday into database
const query = `
INSERT INTO country_holidays (country_code, name, description, date, is_recurring)
VALUES ($1, $2, $3, $4, $5)
ON CONFLICT (country_code, name, date) DO NOTHING
`;
await client.query(query, [
country.code,
name,
description,
dateStr,
true // Most holidays are recurring
]);
}
}
console.log(`✓ Completed ${country.name}`);
} catch (error) {
console.log(`✗ Error processing ${country.name}: ${error.message}`);
}
}
console.log("Holiday population completed!");
} catch (error) {
console.error("Database error:", error);
} finally {
client.release();
await pool.end();
}
}
// Run the script
populateHolidays().catch(console.error);

View File

@@ -0,0 +1,25 @@
#!/bin/bash
echo "🌍 Starting Holiday Population Script..."
echo "This will populate the database with holidays for 200+ countries using the date-holidays npm package."
echo ""
# Check if Node.js is installed
if ! command -v node &> /dev/null; then
echo "❌ Node.js is not installed. Please install Node.js first."
exit 1
fi
# Check if the script exists
if [ ! -f "scripts/populate-holidays.js" ]; then
echo "❌ Holiday population script not found."
exit 1
fi
# Run the holiday population script
echo "🚀 Running holiday population script..."
node scripts/populate-holidays.js
echo ""
echo "✅ Holiday population completed!"
echo "You can now use the holiday import feature in the admin center."

View File

@@ -6,7 +6,7 @@ import logger from "morgan";
import helmet from "helmet";
import compression from "compression";
import passport from "passport";
import csurf from "csurf";
import { csrfSync } from "csrf-sync";
import rateLimit from "express-rate-limit";
import cors from "cors";
import flash from "connect-flash";
@@ -112,17 +112,13 @@ function isLoggedIn(req: Request, _res: Response, next: NextFunction) {
return req.user ? next() : next(createError(401));
}
// CSRF configuration
const csrfProtection = csurf({
cookie: {
key: "XSRF-TOKEN",
path: "/",
httpOnly: false,
secure: isProduction(), // Only secure in production
sameSite: isProduction() ? "none" : "lax", // Different settings for dev vs prod
domain: isProduction() ? ".worklenz.com" : undefined // Only set domain in production
},
ignoreMethods: ["HEAD", "OPTIONS"]
// CSRF configuration using csrf-sync for session-based authentication
const {
invalidCsrfTokenError,
generateToken,
csrfSynchronisedProtection,
} = csrfSync({
getTokenFromRequest: (req: Request) => req.headers["x-csrf-token"] as string || (req.body && req.body["_csrf"])
});
// Apply CSRF selectively (exclude webhooks and public routes)
@@ -135,38 +131,25 @@ app.use((req, res, next) => {
) {
next();
} else {
csrfProtection(req, res, next);
csrfSynchronisedProtection(req, res, next);
}
});
// Set CSRF token cookie
// Set CSRF token method on request object for compatibility
app.use((req: Request, res: Response, next: NextFunction) => {
if (req.csrfToken) {
const token = req.csrfToken();
res.cookie("XSRF-TOKEN", token, {
httpOnly: false,
secure: isProduction(),
sameSite: isProduction() ? "none" : "lax",
domain: isProduction() ? ".worklenz.com" : undefined,
path: "/"
});
// Add csrfToken method to request object for compatibility
if (!req.csrfToken && generateToken) {
req.csrfToken = (overwrite?: boolean) => generateToken(req, overwrite);
}
next();
});
// CSRF token refresh endpoint
app.get("/csrf-token", (req: Request, res: Response) => {
if (req.csrfToken) {
const token = req.csrfToken();
res.cookie("XSRF-TOKEN", token, {
httpOnly: false,
secure: isProduction(),
sameSite: isProduction() ? "none" : "lax",
domain: isProduction() ? ".worklenz.com" : undefined,
path: "/"
});
res.status(200).json({ done: true, message: "CSRF token refreshed" });
} else {
try {
const token = generateToken(req);
res.status(200).json({ done: true, message: "CSRF token refreshed", token });
} catch (error) {
res.status(500).json({ done: false, message: "Failed to generate CSRF token" });
}
});
@@ -219,7 +202,7 @@ if (isInternalServer()) {
// CSRF error handler
app.use((err: any, req: Request, res: Response, next: NextFunction) => {
if (err.code === "EBADCSRFTOKEN") {
if (err === invalidCsrfTokenError) {
return res.status(403).json({
done: false,
message: "Invalid CSRF token",

File diff suppressed because it is too large Load Diff

View File

@@ -35,8 +35,18 @@ export default class AuthController extends WorklenzControllerBase {
const auth_error = errors.length > 0 ? errors[0] : null;
const message = messages.length > 0 ? messages[0] : null;
const midTitle = req.query.strategy === "login" ? "Login Failed!" : "Signup Failed!";
const title = req.query.strategy ? midTitle : null;
// Determine title based on authentication status and strategy
let title = null;
if (req.query.strategy) {
if (auth_error) {
// Show failure title only when there's an actual error
title = req.query.strategy === "login" ? "Login Failed!" : "Signup Failed!";
} else if (req.isAuthenticated() && message) {
// Show success title when authenticated and there's a success message
title = req.query.strategy === "login" ? "Login Successful!" : "Signup Successful!";
}
// If no error and not authenticated, don't show any title (this might be a redirect without completion)
}
if (req.user)
req.user.build_v = FileConstants.getRelease();

View File

@@ -0,0 +1,416 @@
import { IWorkLenzRequest } from "../interfaces/worklenz-request";
import { IWorkLenzResponse } from "../interfaces/worklenz-response";
import db from "../config/db";
import { ServerResponse } from "../models/server-response";
import WorklenzControllerBase from "./worklenz-controller-base";
import HandleExceptions from "../decorators/handle-exceptions";
import {
ICreateHolidayRequest,
IUpdateHolidayRequest,
IImportCountryHolidaysRequest,
} from "../interfaces/holiday.interface";
export default class HolidayController extends WorklenzControllerBase {
@HandleExceptions()
public static async getHolidayTypes(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
const q = `SELECT id, name, description, color_code, created_at, updated_at
FROM holiday_types
ORDER BY name;`;
const result = await db.query(q);
return res.status(200).send(new ServerResponse(true, result.rows));
}
@HandleExceptions()
public static async getOrganizationHolidays(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
const { year } = req.query;
const yearFilter = year ? `AND EXTRACT(YEAR FROM date) = $2` : "";
const params = year ? [req.user?.owner_id, year] : [req.user?.owner_id];
const q = `SELECT oh.id, oh.organization_id, oh.holiday_type_id, oh.name, oh.description,
oh.date, oh.is_recurring, oh.created_at, oh.updated_at,
ht.name as holiday_type_name, ht.color_code
FROM organization_holidays oh
JOIN holiday_types ht ON oh.holiday_type_id = ht.id
WHERE oh.organization_id = (
SELECT id FROM organizations WHERE user_id = $1
) ${yearFilter}
ORDER BY oh.date;`;
const result = await db.query(q, params);
return res.status(200).send(new ServerResponse(true, result.rows));
}
@HandleExceptions()
public static async createOrganizationHoliday(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
const { name, description, date, holiday_type_id, is_recurring = false }: ICreateHolidayRequest = req.body;
const q = `INSERT INTO organization_holidays (organization_id, holiday_type_id, name, description, date, is_recurring)
VALUES (
(SELECT id FROM organizations WHERE user_id = $1),
$2, $3, $4, $5, $6
)
RETURNING id;`;
const result = await db.query(q, [req.user?.owner_id, holiday_type_id, name, description, date, is_recurring]);
return res.status(201).send(new ServerResponse(true, result.rows[0]));
}
@HandleExceptions()
public static async updateOrganizationHoliday(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
const { id } = req.params;
const { name, description, date, holiday_type_id, is_recurring }: IUpdateHolidayRequest = req.body;
const updateFields = [];
const values = [req.user?.owner_id, id];
let paramIndex = 3;
if (name !== undefined) {
updateFields.push(`name = $${paramIndex++}`);
values.push(name);
}
if (description !== undefined) {
updateFields.push(`description = $${paramIndex++}`);
values.push(description);
}
if (date !== undefined) {
updateFields.push(`date = $${paramIndex++}`);
values.push(date);
}
if (holiday_type_id !== undefined) {
updateFields.push(`holiday_type_id = $${paramIndex++}`);
values.push(holiday_type_id);
}
if (is_recurring !== undefined) {
updateFields.push(`is_recurring = $${paramIndex++}`);
values.push(is_recurring.toString());
}
if (updateFields.length === 0) {
return res.status(400).send(new ServerResponse(false, "No fields to update"));
}
const q = `UPDATE organization_holidays
SET ${updateFields.join(", ")}, updated_at = CURRENT_TIMESTAMP
WHERE id = $2 AND organization_id = (
SELECT id FROM organizations WHERE user_id = $1
)
RETURNING id;`;
const result = await db.query(q, values);
if (result.rows.length === 0) {
return res.status(404).send(new ServerResponse(false, "Holiday not found"));
}
return res.status(200).send(new ServerResponse(true, result.rows[0]));
}
@HandleExceptions()
public static async deleteOrganizationHoliday(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
const { id } = req.params;
const q = `DELETE FROM organization_holidays
WHERE id = $2 AND organization_id = (
SELECT id FROM organizations WHERE user_id = $1
)
RETURNING id;`;
const result = await db.query(q, [req.user?.owner_id, id]);
if (result.rows.length === 0) {
return res.status(404).send(new ServerResponse(false, "Holiday not found"));
}
return res.status(200).send(new ServerResponse(true, { message: "Holiday deleted successfully" }));
}
@HandleExceptions()
public static async getCountryHolidays(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
const { country_code, year } = req.query;
if (!country_code) {
return res.status(400).send(new ServerResponse(false, "Country code is required"));
}
const yearFilter = year ? `AND EXTRACT(YEAR FROM date) = $2` : "";
const params = year ? [country_code, year] : [country_code];
const q = `SELECT id, country_code, name, description, date, is_recurring, created_at, updated_at
FROM country_holidays
WHERE country_code = $1 ${yearFilter}
ORDER BY date;`;
const result = await db.query(q, params);
return res.status(200).send(new ServerResponse(true, result.rows));
}
@HandleExceptions()
public static async getAvailableCountries(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
const q = `SELECT DISTINCT c.code, c.name
FROM countries c
JOIN country_holidays ch ON c.code = ch.country_code
ORDER BY c.name;`;
const result = await db.query(q);
return res.status(200).send(new ServerResponse(true, result.rows));
}
@HandleExceptions()
public static async importCountryHolidays(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
const { country_code, year }: IImportCountryHolidaysRequest = req.body;
if (!country_code) {
return res.status(400).send(new ServerResponse(false, "Country code is required"));
}
// Get organization ID
const orgQ = `SELECT id FROM organizations WHERE user_id = $1`;
const orgResult = await db.query(orgQ, [req.user?.owner_id]);
const organizationId = orgResult.rows[0]?.id;
if (!organizationId) {
return res.status(404).send(new ServerResponse(false, "Organization not found"));
}
// Get default holiday type (Public Holiday)
const typeQ = `SELECT id FROM holiday_types WHERE name = 'Public Holiday' LIMIT 1`;
const typeResult = await db.query(typeQ);
const holidayTypeId = typeResult.rows[0]?.id;
if (!holidayTypeId) {
return res.status(404).send(new ServerResponse(false, "Default holiday type not found"));
}
// Get country holidays for the specified year
const yearFilter = year ? `AND EXTRACT(YEAR FROM date) = $2` : "";
const params = year ? [country_code, year] : [country_code];
const holidaysQ = `SELECT name, description, date, is_recurring
FROM country_holidays
WHERE country_code = $1 ${yearFilter}`;
const holidaysResult = await db.query(holidaysQ, params);
if (holidaysResult.rows.length === 0) {
return res.status(404).send(new ServerResponse(false, "No holidays found for this country and year"));
}
// Import holidays to organization
const importQ = `INSERT INTO organization_holidays (organization_id, holiday_type_id, name, description, date, is_recurring)
VALUES ($1, $2, $3, $4, $5, $6)
ON CONFLICT (organization_id, date) DO NOTHING`;
let importedCount = 0;
for (const holiday of holidaysResult.rows) {
try {
await db.query(importQ, [
organizationId,
holidayTypeId,
holiday.name,
holiday.description,
holiday.date,
holiday.is_recurring
]);
importedCount++;
} catch (error) {
// Skip duplicates
continue;
}
}
return res.status(200).send(new ServerResponse(true, {
message: `Successfully imported ${importedCount} holidays`,
imported_count: importedCount
}));
}
@HandleExceptions()
public static async getHolidayCalendar(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
const { year, month } = req.query;
if (!year || !month) {
return res.status(400).send(new ServerResponse(false, "Year and month are required"));
}
const q = `SELECT oh.id, oh.name, oh.description, oh.date, oh.is_recurring,
ht.name as holiday_type_name, ht.color_code,
'organization' as source
FROM organization_holidays oh
JOIN holiday_types ht ON oh.holiday_type_id = ht.id
WHERE oh.organization_id = (
SELECT id FROM organizations WHERE user_id = $1
)
AND EXTRACT(YEAR FROM oh.date) = $2
AND EXTRACT(MONTH FROM oh.date) = $3
UNION ALL
SELECT ch.id, ch.name, ch.description, ch.date, ch.is_recurring,
'Public Holiday' as holiday_type_name, '#f37070' as color_code,
'country' as source
FROM country_holidays ch
JOIN organizations o ON ch.country_code = (
SELECT c.code FROM countries c WHERE c.id = o.country
)
WHERE o.user_id = $1
AND EXTRACT(YEAR FROM ch.date) = $2
AND EXTRACT(MONTH FROM ch.date) = $3
ORDER BY date;`;
const result = await db.query(q, [req.user?.owner_id, year, month]);
return res.status(200).send(new ServerResponse(true, result.rows));
}
@HandleExceptions()
public static async populateCountryHolidays(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
// Check if this organization has recently populated holidays (within last hour)
const recentPopulationCheck = `
SELECT COUNT(*) as count
FROM organization_holidays
WHERE organization_id = (SELECT id FROM organizations WHERE user_id = $1)
AND created_at > NOW() - INTERVAL '1 hour'
`;
const recentResult = await db.query(recentPopulationCheck, [req.user?.owner_id]);
const recentCount = parseInt(recentResult.rows[0]?.count || '0');
// If there are recent holidays added, skip population
if (recentCount > 10) {
return res.status(200).send(new ServerResponse(true, {
success: true,
message: "Holidays were recently populated, skipping to avoid duplicates",
total_populated: 0,
recently_populated: true
}));
}
const Holidays = require("date-holidays");
const countries = [
{ code: "US", name: "United States" },
{ code: "GB", name: "United Kingdom" },
{ code: "CA", name: "Canada" },
{ code: "AU", name: "Australia" },
{ code: "DE", name: "Germany" },
{ code: "FR", name: "France" },
{ code: "IT", name: "Italy" },
{ code: "ES", name: "Spain" },
{ code: "NL", name: "Netherlands" },
{ code: "BE", name: "Belgium" },
{ code: "CH", name: "Switzerland" },
{ code: "AT", name: "Austria" },
{ code: "SE", name: "Sweden" },
{ code: "NO", name: "Norway" },
{ code: "DK", name: "Denmark" },
{ code: "FI", name: "Finland" },
{ code: "PL", name: "Poland" },
{ code: "CZ", name: "Czech Republic" },
{ code: "HU", name: "Hungary" },
{ code: "RO", name: "Romania" },
{ code: "BG", name: "Bulgaria" },
{ code: "HR", name: "Croatia" },
{ code: "SI", name: "Slovenia" },
{ code: "SK", name: "Slovakia" },
{ code: "LT", name: "Lithuania" },
{ code: "LV", name: "Latvia" },
{ code: "EE", name: "Estonia" },
{ code: "IE", name: "Ireland" },
{ code: "PT", name: "Portugal" },
{ code: "GR", name: "Greece" },
{ code: "CY", name: "Cyprus" },
{ code: "MT", name: "Malta" },
{ code: "LU", name: "Luxembourg" },
{ code: "IS", name: "Iceland" },
{ code: "CN", name: "China" },
{ code: "JP", name: "Japan" },
{ code: "KR", name: "South Korea" },
{ code: "IN", name: "India" },
{ code: "BR", name: "Brazil" },
{ code: "AR", name: "Argentina" },
{ code: "MX", name: "Mexico" },
{ code: "ZA", name: "South Africa" },
{ code: "NZ", name: "New Zealand" },
{ code: "LK", name: "Sri Lanka" }
];
let totalPopulated = 0;
const errors = [];
for (const country of countries) {
try {
// Special handling for Sri Lanka
if (country.code === 'LK') {
// Import the holiday data provider
const { HolidayDataProvider } = require("../services/holiday-data-provider");
for (let year = 2020; year <= 2050; year++) {
const sriLankanHolidays = await HolidayDataProvider.getSriLankanHolidays(year);
for (const holiday of sriLankanHolidays) {
const query = `
INSERT INTO country_holidays (country_code, name, description, date, is_recurring)
VALUES ($1, $2, $3, $4, $5)
ON CONFLICT (country_code, name, date) DO NOTHING
`;
await db.query(query, [
'LK',
holiday.name,
holiday.description,
holiday.date,
holiday.is_recurring
]);
totalPopulated++;
}
}
} else {
// Use date-holidays for other countries
const hd = new Holidays(country.code);
for (let year = 2020; year <= 2050; year++) {
const holidays = hd.getHolidays(year);
for (const holiday of holidays) {
if (!holiday.date || typeof holiday.date !== "object") {
continue;
}
const dateStr = holiday.date.toISOString().split("T")[0];
const name = holiday.name || "Unknown Holiday";
const description = holiday.type || "Public Holiday";
const query = `
INSERT INTO country_holidays (country_code, name, description, date, is_recurring)
VALUES ($1, $2, $3, $4, $5)
ON CONFLICT (country_code, name, date) DO NOTHING
`;
await db.query(query, [
country.code,
name,
description,
dateStr,
true
]);
totalPopulated++;
}
}
}
} catch (error: any) {
errors.push(`${country.name}: ${error?.message || "Unknown error"}`);
}
}
const response = {
success: true,
message: `Successfully populated ${totalPopulated} holidays`,
total_populated: totalPopulated,
errors: errors.length > 0 ? errors : undefined
};
return res.status(200).send(new ServerResponse(true, response));
}
}

View File

@@ -137,6 +137,10 @@ export default class HomePageController extends WorklenzControllerBase {
WHERE category_id NOT IN (SELECT id
FROM sys_task_status_categories
WHERE is_done IS FALSE))
AND NOT EXISTS(SELECT project_id
FROM archived_projects
WHERE project_id = p.id
AND user_id = $2)
${groupByClosure}
ORDER BY t.end_date ASC`;
@@ -158,9 +162,13 @@ export default class HomePageController extends WorklenzControllerBase {
WHERE category_id NOT IN (SELECT id
FROM sys_task_status_categories
WHERE is_done IS FALSE))
AND NOT EXISTS(SELECT project_id
FROM archived_projects
WHERE project_id = p.id
AND user_id = $3)
${groupByClosure}`;
const result = await db.query(q, [teamId, userId]);
const result = await db.query(q, [teamId, userId, userId]);
const [row] = result.rows;
return row;
}

File diff suppressed because it is too large Load Diff

View File

@@ -9,7 +9,7 @@ import {getColor} from "../shared/utils";
import TeamMembersController from "./team-members-controller";
import {checkTeamSubscriptionStatus} from "../shared/paddle-utils";
import {updateUsers} from "../shared/paddle-requests";
import {statusExclude} from "../shared/constants";
import {statusExclude, TRIAL_MEMBER_LIMIT} from "../shared/constants";
import {NotificationsService} from "../services/notifications/notifications.service";
export default class ProjectMembersController extends WorklenzControllerBase {
@@ -118,6 +118,17 @@ export default class ProjectMembersController extends WorklenzControllerBase {
return res.status(200).send(new ServerResponse(false, null, "Maximum number of life time users reached."));
}
/**
* Checks trial user team member limit
*/
if (subscriptionData.subscription_status === "trialing") {
const currentTrialMembers = parseInt(subscriptionData.current_count) || 0;
if (currentTrialMembers + 1 > TRIAL_MEMBER_LIMIT) {
return res.status(200).send(new ServerResponse(false, null, `Trial users cannot exceed ${TRIAL_MEMBER_LIMIT} team members. Please upgrade to add more members.`));
}
}
// if (subscriptionData.status === "trialing") break;
if (!userExists && !subscriptionData.is_credit && !subscriptionData.is_custom && subscriptionData.subscription_status !== "trialing") {
// if (subscriptionData.subscription_status === "active") {

View File

@@ -0,0 +1,292 @@
import db from "../config/db";
import { IWorkLenzRequest } from "../interfaces/worklenz-request";
import { IWorkLenzResponse } from "../interfaces/worklenz-response";
import { ServerResponse } from "../models/server-response";
import HandleExceptions from "../decorators/handle-exceptions";
import WorklenzControllerBase from "./worklenz-controller-base";
export default class ProjectRateCardController extends WorklenzControllerBase {
// Insert a single role for a project
@HandleExceptions()
public static async createOne(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
const { project_id, job_title_id, rate, man_day_rate } = req.body;
if (!project_id || !job_title_id || typeof rate !== "number") {
return res.status(400).send(new ServerResponse(false, null, "Invalid input"));
}
// Handle both rate and man_day_rate fields
const columns = ["project_id", "job_title_id", "rate"];
const values = [project_id, job_title_id, rate];
if (typeof man_day_rate !== "undefined") {
columns.push("man_day_rate");
values.push(man_day_rate);
}
const q = `
INSERT INTO finance_project_rate_card_roles (${columns.join(", ")})
VALUES (${values.map((_, i) => `$${i + 1}`).join(", ")})
ON CONFLICT (project_id, job_title_id) DO UPDATE SET
rate = EXCLUDED.rate${typeof man_day_rate !== "undefined" ? ", man_day_rate = EXCLUDED.man_day_rate" : ""}
RETURNING *,
(SELECT name FROM job_titles jt WHERE jt.id = finance_project_rate_card_roles.job_title_id) AS jobtitle;
`;
const result = await db.query(q, values);
return res.status(200).send(new ServerResponse(true, result.rows[0]));
}
// Insert multiple roles for a project
@HandleExceptions()
public static async createMany(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
const { project_id, roles } = req.body;
if (!Array.isArray(roles) || !project_id) {
return res.status(400).send(new ServerResponse(false, null, "Invalid input"));
}
// Handle both rate and man_day_rate fields for each role
const columns = ["project_id", "job_title_id", "rate", "man_day_rate"];
const values = roles.map((role: any) => [
project_id,
role.job_title_id,
typeof role.rate !== "undefined" ? role.rate : 0,
typeof role.man_day_rate !== "undefined" ? role.man_day_rate : 0
]);
const q = `
INSERT INTO finance_project_rate_card_roles (${columns.join(", ")})
VALUES ${values.map((_, i) => `($${i * 4 + 1}, $${i * 4 + 2}, $${i * 4 + 3}, $${i * 4 + 4})`).join(",")}
ON CONFLICT (project_id, job_title_id) DO UPDATE SET
rate = EXCLUDED.rate,
man_day_rate = EXCLUDED.man_day_rate
RETURNING *,
(SELECT name FROM job_titles jt WHERE jt.id = finance_project_rate_card_roles.job_title_id) AS jobtitle;
`;
const flatValues = values.flat();
const result = await db.query(q, flatValues);
return res.status(200).send(new ServerResponse(true, result.rows));
}
// Get all roles for a project
@HandleExceptions()
public static async getByProjectId(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
const { project_id } = req.params;
const q = `
SELECT
fprr.*,
jt.name as jobtitle,
(
SELECT COALESCE(json_agg(pm.id), '[]'::json)
FROM project_members pm
WHERE pm.project_rate_card_role_id = fprr.id
) AS members
FROM finance_project_rate_card_roles fprr
LEFT JOIN job_titles jt ON fprr.job_title_id = jt.id
WHERE fprr.project_id = $1
ORDER BY fprr.created_at;
`;
const result = await db.query(q, [project_id]);
return res.status(200).send(new ServerResponse(true, result.rows));
}
// Get a single role by id
@HandleExceptions()
public static async getById(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
const { id } = req.params;
const q = `
SELECT
fprr.*,
jt.name as jobtitle,
(
SELECT COALESCE(json_agg(pm.id), '[]'::json)
FROM project_members pm
WHERE pm.project_rate_card_role_id = fprr.id
) AS members
FROM finance_project_rate_card_roles fprr
LEFT JOIN job_titles jt ON fprr.job_title_id = jt.id
WHERE fprr.id = $1;
`;
const result = await db.query(q, [id]);
return res.status(200).send(new ServerResponse(true, result.rows[0]));
}
// Update a single role by id
@HandleExceptions()
public static async updateById(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
const { id } = req.params;
const { job_title_id, rate, man_day_rate } = req.body;
let setClause = "job_title_id = $1, updated_at = NOW()";
const values = [job_title_id];
if (typeof man_day_rate !== "undefined") {
setClause += ", man_day_rate = $2";
values.push(man_day_rate);
} else {
setClause += ", rate = $2";
values.push(rate);
}
values.push(id);
const q = `
WITH updated AS (
UPDATE finance_project_rate_card_roles
SET ${setClause}
WHERE id = $3
RETURNING *
),
jobtitles AS (
SELECT u.*, jt.name AS jobtitle
FROM updated u
JOIN job_titles jt ON jt.id = u.job_title_id
),
members AS (
SELECT json_agg(pm.id) AS members, pm.project_rate_card_role_id
FROM project_members pm
WHERE pm.project_rate_card_role_id IN (SELECT id FROM jobtitles)
GROUP BY pm.project_rate_card_role_id
)
SELECT jt.*, m.members
FROM jobtitles jt
LEFT JOIN members m ON m.project_rate_card_role_id = jt.id;
`;
const result = await db.query(q, values);
return res.status(200).send(new ServerResponse(true, result.rows[0]));
}
// update project member rate for a project with members
@HandleExceptions()
public static async updateProjectMemberByProjectIdAndMemberId(
req: IWorkLenzRequest,
res: IWorkLenzResponse
): Promise<IWorkLenzResponse> {
const { project_id, id } = req.params;
const { project_rate_card_role_id } = req.body;
if (!project_id || !id || !project_rate_card_role_id) {
return res.status(400).send(new ServerResponse(false, null, "Missing values"));
}
try {
// Step 1: Check current role assignment
const checkQuery = `
SELECT project_rate_card_role_id
FROM project_members
WHERE id = $1 AND project_id = $2;
`;
const { rows: checkRows } = await db.query(checkQuery, [id, project_id]);
const currentRoleId = checkRows[0]?.project_rate_card_role_id;
if (currentRoleId !== null && currentRoleId !== project_rate_card_role_id) {
// Step 2: Fetch members with the requested role
const membersQuery = `
SELECT COALESCE(json_agg(id), '[]'::json) AS members
FROM project_members
WHERE project_id = $1 AND project_rate_card_role_id = $2;
`;
const { rows: memberRows } = await db.query(membersQuery, [project_id, project_rate_card_role_id]);
return res.status(200).send(
new ServerResponse(false, memberRows[0], "Already Assigned !")
);
}
// Step 3: Perform the update
const updateQuery = `
UPDATE project_members
SET project_rate_card_role_id = CASE
WHEN project_rate_card_role_id = $1 THEN NULL
ELSE $1
END
WHERE id = $2
AND project_id = $3
AND EXISTS (
SELECT 1
FROM finance_project_rate_card_roles
WHERE id = $1 AND project_id = $3
)
RETURNING project_rate_card_role_id;
`;
const { rows: updateRows } = await db.query(updateQuery, [project_rate_card_role_id, id, project_id]);
if (updateRows.length === 0) {
return res.status(200).send(new ServerResponse(true, [], "Project member not found or invalid project_rate_card_role_id"));
}
const updatedRoleId = updateRows[0].project_rate_card_role_id || project_rate_card_role_id;
// Step 4: Fetch updated members list
const membersQuery = `
SELECT COALESCE(json_agg(id), '[]'::json) AS members
FROM project_members
WHERE project_id = $1 AND project_rate_card_role_id = $2;
`;
const { rows: finalMembers } = await db.query(membersQuery, [project_id, updatedRoleId]);
return res.status(200).send(new ServerResponse(true, finalMembers[0]));
} catch (error) {
return res.status(500).send(new ServerResponse(false, null, "Internal server error"));
}
}
// Update all roles for a project (delete then insert)
@HandleExceptions()
public static async updateByProjectId(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
const { project_id, roles } = req.body;
if (!Array.isArray(roles) || !project_id) {
return res.status(400).send(new ServerResponse(false, null, "Invalid input"));
}
if (roles.length === 0) {
// If no roles provided, do nothing and return empty array
return res.status(200).send(new ServerResponse(true, []));
}
// Build upsert query for all roles
const columns = ["project_id", "job_title_id", "rate", "man_day_rate"];
const values = roles.map((role: any) => [
project_id,
role.job_title_id,
typeof role.rate !== "undefined" ? role.rate : null,
typeof role.man_day_rate !== "undefined" ? role.man_day_rate : null
]);
const q = `
WITH upserted AS (
INSERT INTO finance_project_rate_card_roles (${columns.join(", ")})
VALUES ${values.map((_, i) => `($${i * 4 + 1}, $${i * 4 + 2}, $${i * 4 + 3}, $${i * 4 + 4})`).join(",")}
ON CONFLICT (project_id, job_title_id)
DO UPDATE SET rate = EXCLUDED.rate, man_day_rate = EXCLUDED.man_day_rate, updated_at = NOW()
RETURNING *
),
jobtitles AS (
SELECT upr.*, jt.name AS jobtitle
FROM upserted upr
JOIN job_titles jt ON jt.id = upr.job_title_id
),
members AS (
SELECT json_agg(pm.id) AS members, pm.project_rate_card_role_id
FROM project_members pm
WHERE pm.project_rate_card_role_id IN (SELECT id FROM jobtitles)
GROUP BY pm.project_rate_card_role_id
)
SELECT jt.*, m.members
FROM jobtitles jt
LEFT JOIN members m ON m.project_rate_card_role_id = jt.id;
`;
const flatValues = values.flat();
const result = await db.query(q, flatValues);
return res.status(200).send(new ServerResponse(true, result.rows));
}
// Delete a single role by id
@HandleExceptions()
public static async deleteById(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
const { id } = req.params;
const q = `DELETE FROM finance_project_rate_card_roles WHERE id = $1 RETURNING *;`;
const result = await db.query(q, [id]);
return res.status(200).send(new ServerResponse(true, result.rows[0]));
}
// Delete all roles for a project
@HandleExceptions()
public static async deleteByProjectId(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
const { project_id } = req.params;
const q = `DELETE FROM finance_project_rate_card_roles WHERE project_id = $1 RETURNING *;`;
const result = await db.query(q, [project_id]);
return res.status(200).send(new ServerResponse(true, result.rows));
}
}

View File

@@ -71,7 +71,7 @@ export default class ProjectsController extends WorklenzControllerBase {
return res.status(200).send(new ServerResponse(false, [], `Sorry, the free plan cannot have more than ${projectsLimit} projects.`));
}
}
const q = `SELECT create_project($1) AS project`;
req.body.team_id = req.user?.team_id || null;
@@ -317,65 +317,58 @@ export default class ProjectsController extends WorklenzControllerBase {
@HandleExceptions()
public static async getMembersByProjectId(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
const {sortField, sortOrder, size, offset} = this.toPaginationOptions(req.query, "name");
const search = (req.query.search || "").toString().trim();
let searchFilter = "";
const params = [req.params.id, req.user?.team_id ?? null, size, offset];
if (search) {
searchFilter = `
AND (
(SELECT name FROM team_member_info_view WHERE team_member_info_view.team_member_id = tm.id) ILIKE '%' || $5 || '%'
OR (SELECT email FROM team_member_info_view WHERE team_member_info_view.team_member_id = tm.id) ILIKE '%' || $5 || '%'
)
`;
params.push(search);
}
const q = `
SELECT ROW_TO_JSON(rec) AS members
FROM (SELECT COUNT(*) AS total,
(SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(t))), '[]'::JSON)
FROM (SELECT project_members.id,
team_member_id,
(SELECT name
FROM team_member_info_view
WHERE team_member_info_view.team_member_id = tm.id),
(SELECT email
FROM team_member_info_view
WHERE team_member_info_view.team_member_id = tm.id) AS email,
u.avatar_url,
(SELECT COUNT(*)
FROM tasks
WHERE archived IS FALSE
AND project_id = project_members.project_id
AND id IN (SELECT task_id
FROM tasks_assignees
WHERE tasks_assignees.project_member_id = project_members.id)) AS all_tasks_count,
(SELECT COUNT(*)
FROM tasks
WHERE archived IS FALSE
AND project_id = project_members.project_id
AND id IN (SELECT task_id
FROM tasks_assignees
WHERE tasks_assignees.project_member_id = project_members.id)
AND status_id IN (SELECT id
FROM task_statuses
WHERE category_id = (SELECT id
FROM sys_task_status_categories
WHERE is_done IS TRUE))) AS completed_tasks_count,
EXISTS(SELECT email
FROM email_invitations
WHERE team_member_id = project_members.team_member_id
AND email_invitations.team_id = $2) AS pending_invitation,
(SELECT project_access_levels.name
FROM project_access_levels
WHERE project_access_levels.id = project_members.project_access_level_id) AS access,
(SELECT name FROM job_titles WHERE id = tm.job_title_id) AS job_title
FROM project_members
INNER JOIN team_members tm ON project_members.team_member_id = tm.id
LEFT JOIN users u ON tm.user_id = u.id
WHERE project_id = $1
ORDER BY ${sortField} ${sortOrder}
LIMIT $3 OFFSET $4) t) AS data
FROM project_members
WHERE project_id = $1) rec;
WITH filtered_members AS (
SELECT project_members.id,
team_member_id,
(SELECT name FROM team_member_info_view WHERE team_member_info_view.team_member_id = tm.id) AS name,
(SELECT email FROM team_member_info_view WHERE team_member_info_view.team_member_id = tm.id) AS email,
u.avatar_url,
(SELECT COUNT(*) FROM tasks WHERE archived IS FALSE AND project_id = project_members.project_id AND id IN (SELECT task_id FROM tasks_assignees WHERE tasks_assignees.project_member_id = project_members.id)) AS all_tasks_count,
(SELECT COUNT(*) FROM tasks WHERE archived IS FALSE AND project_id = project_members.project_id AND id IN (SELECT task_id FROM tasks_assignees WHERE tasks_assignees.project_member_id = project_members.id) AND status_id IN (SELECT id FROM task_statuses WHERE category_id = (SELECT id FROM sys_task_status_categories WHERE is_done IS TRUE))) AS completed_tasks_count,
EXISTS(SELECT email FROM email_invitations WHERE team_member_id = project_members.team_member_id AND email_invitations.team_id = $2) AS pending_invitation,
(SELECT project_access_levels.name FROM project_access_levels WHERE project_access_levels.id = project_members.project_access_level_id) AS access,
(SELECT name FROM job_titles WHERE id = tm.job_title_id) AS job_title
FROM project_members
INNER JOIN team_members tm ON project_members.team_member_id = tm.id
LEFT JOIN users u ON tm.user_id = u.id
WHERE project_id = $1
${search ? searchFilter : ""}
)
SELECT
(SELECT COUNT(*) FROM filtered_members) AS total,
(SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(t))), '[]'::JSON)
FROM (
SELECT * FROM filtered_members
ORDER BY ${sortField} ${sortOrder}
LIMIT $3 OFFSET $4
) t
) AS data
`;
const result = await db.query(q, [req.params.id, req.user?.team_id ?? null, size, offset]);
const result = await db.query(q, params);
const [data] = result.rows;
for (const member of data?.members.data || []) {
for (const member of data?.data || []) {
member.progress = member.all_tasks_count > 0
? ((member.completed_tasks_count / member.all_tasks_count) * 100).toFixed(0) : 0;
}
return res.status(200).send(new ServerResponse(true, data?.members || this.paginatedDatasetDefaultStruct));
return res.status(200).send(new ServerResponse(true, data || this.paginatedDatasetDefaultStruct));
}
@HandleExceptions()
@@ -395,6 +388,8 @@ export default class ProjectsController extends WorklenzControllerBase {
projects.folder_id,
projects.phase_label,
projects.category_id,
projects.currency,
projects.budget,
(projects.estimated_man_days) AS man_days,
(projects.estimated_working_days) AS working_days,
(projects.hours_per_day) AS hours_per_day,
@@ -756,4 +751,186 @@ export default class ProjectsController extends WorklenzControllerBase {
}
@HandleExceptions()
public static async getGrouped(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
// Use qualified field name for projects to avoid ambiguity
const {searchQuery, sortField, sortOrder, size, offset} = this.toPaginationOptions(req.query, ["projects.name"]);
const groupBy = req.query.groupBy as string || "category";
const filterByMember = !req.user?.owner && !req.user?.is_admin ?
` AND is_member_of_project(projects.id, '${req.user?.id}', $1) ` : "";
const isFavorites = req.query.filter === "1" ? ` AND EXISTS(SELECT user_id FROM favorite_projects WHERE user_id = '${req.user?.id}' AND project_id = projects.id)` : "";
const isArchived = req.query.filter === "2"
? ` AND EXISTS(SELECT user_id FROM archived_projects WHERE user_id = '${req.user?.id}' AND project_id = projects.id)`
: ` AND NOT EXISTS(SELECT user_id FROM archived_projects WHERE user_id = '${req.user?.id}' AND project_id = projects.id)`;
const categories = this.getFilterByCategoryWhereClosure(req.query.categories as string);
const statuses = this.getFilterByStatusWhereClosure(req.query.statuses as string);
// Determine grouping field and join based on groupBy parameter
let groupField = "";
let groupName = "";
let groupColor = "";
let groupJoin = "";
let groupByFields = "";
let groupOrderBy = "";
switch (groupBy) {
case "client":
groupField = "COALESCE(projects.client_id::text, 'no-client')";
groupName = "COALESCE(clients.name, 'No Client')";
groupColor = "'#688'";
groupJoin = "LEFT JOIN clients ON projects.client_id = clients.id";
groupByFields = "projects.client_id, clients.name";
groupOrderBy = "COALESCE(clients.name, 'No Client')";
break;
case "status":
groupField = "COALESCE(projects.status_id::text, 'no-status')";
groupName = "COALESCE(sys_project_statuses.name, 'No Status')";
groupColor = "COALESCE(sys_project_statuses.color_code, '#888')";
groupJoin = "LEFT JOIN sys_project_statuses ON projects.status_id = sys_project_statuses.id";
groupByFields = "projects.status_id, sys_project_statuses.name, sys_project_statuses.color_code";
groupOrderBy = "COALESCE(sys_project_statuses.name, 'No Status')";
break;
case "category":
default:
groupField = "COALESCE(projects.category_id::text, 'uncategorized')";
groupName = "COALESCE(project_categories.name, 'Uncategorized')";
groupColor = "COALESCE(project_categories.color_code, '#888')";
groupJoin = "LEFT JOIN project_categories ON projects.category_id = project_categories.id";
groupByFields = "projects.category_id, project_categories.name, project_categories.color_code";
groupOrderBy = "COALESCE(project_categories.name, 'Uncategorized')";
}
// Ensure sortField is properly qualified for the inner project query
let qualifiedSortField = sortField;
if (Array.isArray(sortField)) {
qualifiedSortField = sortField[0]; // Take the first field if it's an array
}
// Replace "projects." with "p2." for the inner query
const innerSortField = qualifiedSortField.replace("projects.", "p2.");
const q = `
SELECT ROW_TO_JSON(rec) AS groups
FROM (
SELECT COUNT(DISTINCT ${groupField}) AS total_groups,
(SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(group_data))), '[]'::JSON)
FROM (
SELECT ${groupField} AS group_key,
${groupName} AS group_name,
${groupColor} AS group_color,
COUNT(*) AS project_count,
(SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(project_data))), '[]'::JSON)
FROM (
SELECT p2.id,
p2.name,
(SELECT sys_project_statuses.name FROM sys_project_statuses WHERE sys_project_statuses.id = p2.status_id) AS status,
(SELECT sys_project_statuses.color_code FROM sys_project_statuses WHERE sys_project_statuses.id = p2.status_id) AS status_color,
(SELECT sys_project_statuses.icon FROM sys_project_statuses WHERE sys_project_statuses.id = p2.status_id) AS status_icon,
EXISTS(SELECT user_id
FROM favorite_projects
WHERE user_id = '${req.user?.id}'
AND project_id = p2.id) AS favorite,
EXISTS(SELECT user_id
FROM archived_projects
WHERE user_id = '${req.user?.id}'
AND project_id = p2.id) AS archived,
p2.color_code,
p2.start_date,
p2.end_date,
p2.category_id,
(SELECT COUNT(*)
FROM tasks
WHERE archived IS FALSE
AND project_id = p2.id) AS all_tasks_count,
(SELECT COUNT(*)
FROM tasks
WHERE archived IS FALSE
AND project_id = p2.id
AND status_id IN (SELECT task_statuses.id
FROM task_statuses
WHERE task_statuses.project_id = p2.id
AND task_statuses.category_id IN
(SELECT sys_task_status_categories.id FROM sys_task_status_categories WHERE sys_task_status_categories.is_done IS TRUE))) AS completed_tasks_count,
(SELECT COUNT(*)
FROM project_members
WHERE project_members.project_id = p2.id) AS members_count,
(SELECT get_project_members(p2.id)) AS names,
(SELECT clients.name FROM clients WHERE clients.id = p2.client_id) AS client_name,
(SELECT users.name FROM users WHERE users.id = p2.owner_id) AS project_owner,
(SELECT project_categories.name FROM project_categories WHERE project_categories.id = p2.category_id) AS category_name,
(SELECT project_categories.color_code
FROM project_categories
WHERE project_categories.id = p2.category_id) AS category_color,
((SELECT project_members.team_member_id as team_member_id
FROM project_members
WHERE project_members.project_id = p2.id
AND project_members.project_access_level_id = (SELECT project_access_levels.id FROM project_access_levels WHERE project_access_levels.key = 'PROJECT_MANAGER'))) AS project_manager_team_member_id,
(SELECT project_members.default_view
FROM project_members
WHERE project_members.project_id = p2.id
AND project_members.team_member_id = '${req.user?.team_member_id}') AS team_member_default_view,
(SELECT CASE
WHEN ((SELECT MAX(tasks.updated_at)
FROM tasks
WHERE tasks.archived IS FALSE
AND tasks.project_id = p2.id) >
p2.updated_at)
THEN (SELECT MAX(tasks.updated_at)
FROM tasks
WHERE tasks.archived IS FALSE
AND tasks.project_id = p2.id)
ELSE p2.updated_at END) AS updated_at
FROM projects p2
${groupJoin.replace("projects.", "p2.")}
WHERE p2.team_id = $1
AND ${groupField.replace("projects.", "p2.")} = ${groupField}
${categories.replace("projects.", "p2.")}
${statuses.replace("projects.", "p2.")}
${isArchived.replace("projects.", "p2.")}
${isFavorites.replace("projects.", "p2.")}
${filterByMember.replace("projects.", "p2.")}
${searchQuery.replace("projects.", "p2.")}
ORDER BY ${innerSortField} ${sortOrder}
) project_data
) AS projects
FROM projects
${groupJoin}
WHERE projects.team_id = $1 ${categories} ${statuses} ${isArchived} ${isFavorites} ${filterByMember} ${searchQuery}
GROUP BY ${groupByFields}
ORDER BY ${groupOrderBy}
LIMIT $2 OFFSET $3
) group_data
) AS data
FROM projects
${groupJoin}
WHERE projects.team_id = $1 ${categories} ${statuses} ${isArchived} ${isFavorites} ${filterByMember} ${searchQuery}
) rec;
`;
const result = await db.query(q, [req.user?.team_id || null, size, offset]);
const [data] = result.rows;
// Process the grouped data
for (const group of data?.groups.data || []) {
for (const project of group.projects || []) {
project.progress = project.all_tasks_count > 0
? ((project.completed_tasks_count / project.all_tasks_count) * 100).toFixed(0) : 0;
project.updated_at_string = moment(project.updated_at).fromNow();
project.names = this.createTagList(project?.names);
project.names.map((a: any) => a.color_code = getColor(a.name));
if (project.project_manager_team_member_id) {
project.project_manager = {
id: project.project_manager_team_member_id
};
}
}
}
return res.status(200).send(new ServerResponse(true, data?.groups || { total_groups: 0, data: [] }));
}
}

View File

@@ -0,0 +1,198 @@
import { IWorkLenzRequest } from "../interfaces/worklenz-request";
import { IWorkLenzResponse } from "../interfaces/worklenz-response";
import db from "../config/db";
import { ServerResponse } from "../models/server-response";
import WorklenzControllerBase from "./worklenz-controller-base";
import HandleExceptions from "../decorators/handle-exceptions";
export default class RateCardController extends WorklenzControllerBase {
@HandleExceptions()
public static async create(
req: IWorkLenzRequest,
res: IWorkLenzResponse
): Promise<IWorkLenzResponse> {
const q = `
INSERT INTO finance_rate_cards (team_id, name)
VALUES ($1, $2)
RETURNING id, name, team_id, created_at, updated_at;
`;
const result = await db.query(q, [
req.user?.team_id || null,
req.body.name,
]);
const [data] = result.rows;
return res.status(200).send(new ServerResponse(true, data));
}
@HandleExceptions()
public static async get(
req: IWorkLenzRequest,
res: IWorkLenzResponse
): Promise<IWorkLenzResponse> {
const { searchQuery, sortField, sortOrder, size, offset } =
this.toPaginationOptions(req.query, "name");
const q = `
SELECT ROW_TO_JSON(rec) AS rate_cards
FROM (
SELECT COUNT(*) AS total,
(
SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(t))), '[]'::JSON)
FROM (
SELECT id, name, team_id, currency, created_at, updated_at
FROM finance_rate_cards
WHERE team_id = $1 ${searchQuery}
ORDER BY ${sortField} ${sortOrder}
LIMIT $2 OFFSET $3
) t
) AS data
FROM finance_rate_cards
WHERE team_id = $1 ${searchQuery}
) rec;
`;
const result = await db.query(q, [req.user?.team_id || null, size, offset]);
const [data] = result.rows;
return res
.status(200)
.send(
new ServerResponse(
true,
data.rate_cards || this.paginatedDatasetDefaultStruct
)
);
}
@HandleExceptions()
public static async getById(
req: IWorkLenzRequest,
res: IWorkLenzResponse
): Promise<IWorkLenzResponse> {
// 1. Fetch the rate card
const q = `
SELECT id, name, team_id, currency, created_at, updated_at
FROM finance_rate_cards
WHERE id = $1 AND team_id = $2;
`;
const result = await db.query(q, [
req.params.id,
req.user?.team_id || null,
]);
const [data] = result.rows;
if (!data) {
return res
.status(404)
.send(new ServerResponse(false, null, "Rate card not found"));
}
// 2. Fetch job roles with job title names
const jobRolesQ = `
SELECT
rcr.job_title_id,
jt.name AS jobTitle,
rcr.rate,
rcr.man_day_rate,
rcr.rate_card_id
FROM finance_rate_card_roles rcr
LEFT JOIN job_titles jt ON rcr.job_title_id = jt.id
WHERE rcr.rate_card_id = $1
`;
const jobRolesResult = await db.query(jobRolesQ, [req.params.id]);
const jobRolesList = jobRolesResult.rows;
// 3. Return the rate card with jobRolesList
return res.status(200).send(
new ServerResponse(true, {
...data,
jobRolesList,
})
);
}
@HandleExceptions()
public static async update(
req: IWorkLenzRequest,
res: IWorkLenzResponse
): Promise<IWorkLenzResponse> {
// 1. Update the rate card
const updateRateCardQ = `
UPDATE finance_rate_cards
SET name = $3, currency = $4, updated_at = NOW()
WHERE id = $1 AND team_id = $2
RETURNING id, name, team_id, currency, created_at, updated_at;
`;
const result = await db.query(updateRateCardQ, [
req.params.id,
req.user?.team_id || null,
req.body.name,
req.body.currency,
]);
const [rateCardData] = result.rows;
// 2. Update job roles (delete old, insert new)
if (Array.isArray(req.body.jobRolesList)) {
// Delete existing roles for this rate card
await db.query(
`DELETE FROM finance_rate_card_roles WHERE rate_card_id = $1;`,
[req.params.id]
);
// Insert new roles
for (const role of req.body.jobRolesList) {
if (role.job_title_id) {
await db.query(
`INSERT INTO finance_rate_card_roles (rate_card_id, job_title_id, rate, man_day_rate)
VALUES ($1, $2, $3, $4);`,
[
req.params.id,
role.job_title_id,
role.rate ?? 0,
role.man_day_rate ?? 0,
]
);
}
}
}
// 3. Get jobRolesList with job title names
const jobRolesQ = `
SELECT
rcr.job_title_id,
jt.name AS jobTitle,
rcr.rate
FROM finance_rate_card_roles rcr
LEFT JOIN job_titles jt ON rcr.job_title_id = jt.id
WHERE rcr.rate_card_id = $1
`;
const jobRolesResult = await db.query(jobRolesQ, [req.params.id]);
const jobRolesList = jobRolesResult.rows;
// 4. Return the updated rate card with jobRolesList
return res.status(200).send(
new ServerResponse(true, {
...rateCardData,
jobRolesList,
})
);
}
@HandleExceptions()
public static async deleteById(
req: IWorkLenzRequest,
res: IWorkLenzResponse
): Promise<IWorkLenzResponse> {
const q = `
DELETE FROM finance_rate_cards
WHERE id = $1 AND team_id = $2
RETURNING id;
`;
const result = await db.query(q, [
req.params.id,
req.user?.team_id || null,
]);
return res
.status(200)
.send(new ServerResponse(true, result.rows.length > 0));
}
}

View File

@@ -0,0 +1,179 @@
// Example of updated getMemberTimeSheets method with timezone support
// This shows the key changes needed to handle timezones properly
import moment from "moment-timezone";
import db from "../../config/db";
import { IWorkLenzRequest } from "../../interfaces/worklenz-request";
import { IWorkLenzResponse } from "../../interfaces/worklenz-response";
import { ServerResponse } from "../../models/server-response";
import { DATE_RANGES } from "../../shared/constants";
export async function getMemberTimeSheets(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
const archived = req.query.archived === "true";
const teams = (req.body.teams || []) as string[];
const teamIds = teams.map(id => `'${id}'`).join(",");
const projects = (req.body.projects || []) as string[];
const projectIds = projects.map(p => `'${p}'`).join(",");
const {billable} = req.body;
// Get user timezone from request or database
const userTimezone = req.body.timezone || await getUserTimezone(req.user?.id || "");
if (!teamIds || !projectIds.length)
return res.status(200).send(new ServerResponse(true, { users: [], projects: [] }));
const { duration, date_range } = req.body;
// Calculate date range with timezone support
let startDate: moment.Moment;
let endDate: moment.Moment;
if (date_range && date_range.length === 2) {
// Convert user's local dates to their timezone's start/end of day
startDate = moment.tz(date_range[0], userTimezone).startOf("day");
endDate = moment.tz(date_range[1], userTimezone).endOf("day");
} else if (duration === DATE_RANGES.ALL_TIME) {
const minDateQuery = `SELECT MIN(COALESCE(start_date, created_at)) as min_date FROM projects WHERE id IN (${projectIds})`;
const minDateResult = await db.query(minDateQuery, []);
const minDate = minDateResult.rows[0]?.min_date;
startDate = minDate ? moment.tz(minDate, userTimezone) : moment.tz("2000-01-01", userTimezone);
endDate = moment.tz(userTimezone);
} else {
// Calculate ranges based on user's timezone
const now = moment.tz(userTimezone);
switch (duration) {
case DATE_RANGES.YESTERDAY:
startDate = now.clone().subtract(1, "day").startOf("day");
endDate = now.clone().subtract(1, "day").endOf("day");
break;
case DATE_RANGES.LAST_WEEK:
startDate = now.clone().subtract(1, "week").startOf("isoWeek");
endDate = now.clone().subtract(1, "week").endOf("isoWeek");
break;
case DATE_RANGES.LAST_MONTH:
startDate = now.clone().subtract(1, "month").startOf("month");
endDate = now.clone().subtract(1, "month").endOf("month");
break;
case DATE_RANGES.LAST_QUARTER:
startDate = now.clone().subtract(3, "months").startOf("day");
endDate = now.clone().endOf("day");
break;
default:
startDate = now.clone().startOf("day");
endDate = now.clone().endOf("day");
}
}
// Convert to UTC for database queries
const startUtc = startDate.utc().format("YYYY-MM-DD HH:mm:ss");
const endUtc = endDate.utc().format("YYYY-MM-DD HH:mm:ss");
// Calculate working days in user's timezone
const totalDays = endDate.diff(startDate, "days") + 1;
let workingDays = 0;
const current = startDate.clone();
while (current.isSameOrBefore(endDate, "day")) {
if (current.isoWeekday() >= 1 && current.isoWeekday() <= 5) {
workingDays++;
}
current.add(1, "day");
}
// Updated SQL query with proper timezone handling
const billableQuery = buildBillableQuery(billable);
const archivedClause = archived ? "" : `AND projects.id NOT IN (SELECT project_id FROM archived_projects WHERE project_id = projects.id AND user_id = '${req.user?.id}')`;
const q = `
WITH project_hours AS (
SELECT
id,
COALESCE(hours_per_day, 8) as hours_per_day
FROM projects
WHERE id IN (${projectIds})
),
total_working_hours AS (
SELECT
SUM(hours_per_day) * ${workingDays} as total_hours
FROM project_hours
)
SELECT
u.id,
u.email,
tm.name,
tm.color_code,
COALESCE(SUM(twl.time_spent), 0) as logged_time,
COALESCE(SUM(twl.time_spent), 0) / 3600.0 as value,
(SELECT total_hours FROM total_working_hours) as total_working_hours,
CASE
WHEN (SELECT total_hours FROM total_working_hours) > 0
THEN ROUND((COALESCE(SUM(twl.time_spent), 0) / 3600.0) / (SELECT total_hours FROM total_working_hours) * 100, 2)
ELSE 0
END as utilization_percent,
ROUND(COALESCE(SUM(twl.time_spent), 0) / 3600.0, 2) as utilized_hours,
ROUND(COALESCE(SUM(twl.time_spent), 0) / 3600.0 - (SELECT total_hours FROM total_working_hours), 2) as over_under_utilized_hours,
'${userTimezone}' as user_timezone,
'${startDate.format("YYYY-MM-DD")}' as report_start_date,
'${endDate.format("YYYY-MM-DD")}' as report_end_date
FROM team_members tm
LEFT JOIN users u ON tm.user_id = u.id
LEFT JOIN task_work_log twl ON twl.user_id = u.id
LEFT JOIN tasks t ON twl.task_id = t.id ${billableQuery}
LEFT JOIN projects p ON t.project_id = p.id
WHERE tm.team_id IN (${teamIds})
AND (
twl.id IS NULL
OR (
p.id IN (${projectIds})
AND twl.created_at >= '${startUtc}'::TIMESTAMP
AND twl.created_at <= '${endUtc}'::TIMESTAMP
${archivedClause}
)
)
GROUP BY u.id, u.email, tm.name, tm.color_code
ORDER BY logged_time DESC`;
const result = await db.query(q, []);
// Add timezone context to response
const response = {
data: result.rows,
timezone_info: {
user_timezone: userTimezone,
report_period: {
start: startDate.format("YYYY-MM-DD HH:mm:ss z"),
end: endDate.format("YYYY-MM-DD HH:mm:ss z"),
working_days: workingDays,
total_days: totalDays
}
}
};
return res.status(200).send(new ServerResponse(true, response));
}
async function getUserTimezone(userId: string): Promise<string> {
const q = `SELECT tz.name as timezone
FROM users u
JOIN timezones tz ON u.timezone_id = tz.id
WHERE u.id = $1`;
const result = await db.query(q, [userId]);
return result.rows[0]?.timezone || "UTC";
}
function buildBillableQuery(billable: { billable: boolean; nonBillable: boolean }): string {
if (!billable) return "";
const { billable: isBillable, nonBillable } = billable;
if (isBillable && nonBillable) {
return "";
} else if (isBillable) {
return " AND tasks.billable IS TRUE";
} else if (nonBillable) {
return " AND tasks.billable IS FALSE";
}
return "";
}

View File

@@ -15,6 +15,25 @@ enum IToggleOptions {
}
export default class ReportingAllocationController extends ReportingControllerBase {
// Helper method to build billable query with custom table alias
private static buildBillableQueryWithAlias(selectedStatuses: { billable: boolean; nonBillable: boolean }, tableAlias: string = 'tasks'): string {
const { billable, nonBillable } = selectedStatuses;
if (billable && nonBillable) {
// Both are enabled, no need to filter
return "";
} else if (billable && !nonBillable) {
// Only billable is enabled - show only billable tasks
return ` AND ${tableAlias}.billable IS TRUE`;
} else if (!billable && nonBillable) {
// Only non-billable is enabled - show only non-billable tasks
return ` AND ${tableAlias}.billable IS FALSE`;
} else {
// Neither selected - this shouldn't happen in normal UI flow
return "";
}
}
private static async getTimeLoggedByProjects(projects: string[], users: string[], key: string, dateRange: string[], archived = false, user_id = "", billable: { billable: boolean; nonBillable: boolean }): Promise<any> {
try {
const projectIds = projects.map(p => `'${p}'`).join(",");
@@ -77,8 +96,8 @@ export default class ReportingAllocationController extends ReportingControllerBa
sps.icon AS status_icon,
(SELECT COUNT(*)
FROM tasks
WHERE CASE WHEN ($1 IS TRUE) THEN project_id IS NOT NULL ELSE archived = FALSE END ${billableQuery}
AND project_id = projects.id) AS all_tasks_count,
WHERE CASE WHEN ($1 IS TRUE) THEN project_id IS NOT NULL ELSE archived = FALSE END
AND project_id = projects.id ${billableQuery}) AS all_tasks_count,
(SELECT COUNT(*)
FROM tasks
WHERE CASE WHEN ($1 IS TRUE) THEN project_id IS NOT NULL ELSE archived = FALSE END
@@ -94,10 +113,11 @@ export default class ReportingAllocationController extends ReportingControllerBa
SELECT name,
(SELECT COALESCE(SUM(time_spent), 0)
FROM task_work_log
LEFT JOIN tasks ON task_work_log.task_id = tasks.id
WHERE user_id = users.id ${billableQuery}
LEFT JOIN tasks ON task_work_log.task_id = tasks.id
WHERE user_id = users.id
AND CASE WHEN ($1 IS TRUE) THEN tasks.project_id IS NOT NULL ELSE tasks.archived = FALSE END
AND tasks.project_id = projects.id
${billableQuery}
${duration}) AS time_logged
FROM users
WHERE id IN (${userIds})
@@ -121,10 +141,11 @@ export default class ReportingAllocationController extends ReportingControllerBa
const q = `(SELECT id,
(SELECT COALESCE(SUM(time_spent), 0)
FROM task_work_log
LEFT JOIN tasks ON task_work_log.task_id = tasks.id ${billableQuery}
LEFT JOIN tasks ON task_work_log.task_id = tasks.id
WHERE user_id = users.id
AND CASE WHEN ($1 IS TRUE) THEN tasks.project_id IS NOT NULL ELSE tasks.archived = FALSE END
AND tasks.project_id IN (${projectIds})
${billableQuery}
${duration}) AS time_logged
FROM users
WHERE id IN (${userIds})
@@ -346,6 +367,8 @@ export default class ReportingAllocationController extends ReportingControllerBa
const projects = (req.body.projects || []) as string[];
const projectIds = projects.map(p => `'${p}'`).join(",");
const categories = (req.body.categories || []) as string[];
const noCategory = req.body.noCategory || false;
const billable = req.body.billable;
if (!teamIds || !projectIds.length)
@@ -361,6 +384,33 @@ export default class ReportingAllocationController extends ReportingControllerBa
const billableQuery = this.buildBillableQuery(billable);
// Prepare projects filter
let projectsFilter = "";
if (projectIds.length > 0) {
projectsFilter = `AND p.id IN (${projectIds})`;
} else {
// If no projects are selected, don't show any data
projectsFilter = `AND 1=0`; // This will match no rows
}
// Prepare categories filter - updated logic
let categoriesFilter = "";
if (categories.length > 0 && noCategory) {
// Both specific categories and "No Category" are selected
const categoryIds = categories.map(id => `'${id}'`).join(",");
categoriesFilter = `AND (p.category_id IS NULL OR p.category_id IN (${categoryIds}))`;
} else if (categories.length === 0 && noCategory) {
// Only "No Category" is selected
categoriesFilter = `AND p.category_id IS NULL`;
} else if (categories.length > 0 && !noCategory) {
// Only specific categories are selected
const categoryIds = categories.map(id => `'${id}'`).join(",");
categoriesFilter = `AND p.category_id IN (${categoryIds})`;
} else {
// categories.length === 0 && !noCategory - no categories selected, show nothing
categoriesFilter = `AND 1=0`; // This will match no rows
}
const q = `
SELECT p.id,
p.name,
@@ -368,13 +418,15 @@ export default class ReportingAllocationController extends ReportingControllerBa
SUM(total_minutes) AS estimated,
color_code
FROM projects p
LEFT JOIN tasks ON tasks.project_id = p.id ${billableQuery}
LEFT JOIN tasks ON tasks.project_id = p.id
LEFT JOIN task_work_log ON task_work_log.task_id = tasks.id
WHERE p.id IN (${projectIds}) ${durationClause} ${archivedClause}
WHERE p.id IN (${projectIds}) ${durationClause} ${archivedClause} ${categoriesFilter} ${billableQuery}
GROUP BY p.id, p.name
ORDER BY logged_time DESC;`;
const result = await db.query(q, []);
const utilization = (req.body.utilization || []) as string[];
const data = [];
for (const project of result.rows) {
@@ -401,10 +453,12 @@ export default class ReportingAllocationController extends ReportingControllerBa
const projects = (req.body.projects || []) as string[];
const projectIds = projects.map(p => `'${p}'`).join(",");
const categories = (req.body.categories || []) as string[];
const noCategory = req.body.noCategory || false;
const billable = req.body.billable;
if (!teamIds || !projectIds.length)
return res.status(200).send(new ServerResponse(true, { users: [], projects: [] }));
if (!teamIds)
return res.status(200).send(new ServerResponse(true, { filteredRows: [], totals: { total_time_logs: "0", total_estimated_hours: "0", total_utilization: "0" } }));
const { duration, date_range } = req.body;
@@ -416,7 +470,9 @@ export default class ReportingAllocationController extends ReportingControllerBa
endDate = moment(date_range[1]);
} else if (duration === DATE_RANGES.ALL_TIME) {
// Fetch the earliest start_date (or created_at if null) from selected projects
const minDateQuery = `SELECT MIN(COALESCE(start_date, created_at)) as min_date FROM projects WHERE id IN (${projectIds})`;
const minDateQuery = projectIds.length > 0
? `SELECT MIN(COALESCE(start_date, created_at)) as min_date FROM projects WHERE id IN (${projectIds})`
: `SELECT MIN(COALESCE(start_date, created_at)) as min_date FROM projects WHERE team_id IN (${teamIds})`;
const minDateResult = await db.query(minDateQuery, []);
const minDate = minDateResult.rows[0]?.min_date;
startDate = minDate ? moment(minDate) : moment('2000-01-01');
@@ -445,59 +501,368 @@ export default class ReportingAllocationController extends ReportingControllerBa
}
}
// Count only weekdays (Mon-Fri) in the period
// Get organization working days
const orgWorkingDaysQuery = `
SELECT monday, tuesday, wednesday, thursday, friday, saturday, sunday
FROM organization_working_days
WHERE organization_id IN (
SELECT t.organization_id
FROM teams t
WHERE t.id IN (${teamIds})
LIMIT 1
);
`;
const orgWorkingDaysResult = await db.query(orgWorkingDaysQuery, []);
const workingDaysConfig = orgWorkingDaysResult.rows[0] || {
monday: true,
tuesday: true,
wednesday: true,
thursday: true,
friday: true,
saturday: false,
sunday: false
};
// Get organization ID for holiday queries
const orgIdQuery = `SELECT t.organization_id FROM teams t WHERE t.id IN (${teamIds}) LIMIT 1`;
const orgIdResult = await db.query(orgIdQuery, []);
const organizationId = orgIdResult.rows[0]?.organization_id;
// Fetch organization holidays within the date range
const orgHolidaysQuery = `
SELECT date
FROM organization_holidays
WHERE organization_id = $1
AND date >= $2::date
AND date <= $3::date
`;
const orgHolidaysResult = await db.query(orgHolidaysQuery, [
organizationId,
startDate.format('YYYY-MM-DD'),
endDate.format('YYYY-MM-DD')
]);
// Fetch country/state holidays if auto-sync is enabled
let countryStateHolidays: any[] = [];
const holidaySettingsQuery = `
SELECT country_code, state_code, auto_sync_holidays
FROM organization_holiday_settings
WHERE organization_id = $1
`;
const holidaySettingsResult = await db.query(holidaySettingsQuery, [organizationId]);
const holidaySettings = holidaySettingsResult.rows[0];
if (holidaySettings?.auto_sync_holidays && holidaySettings.country_code) {
// Fetch country holidays
const countryHolidaysQuery = `
SELECT date
FROM country_holidays
WHERE country_code = $1
AND (
(is_recurring = false AND date >= $2::date AND date <= $3::date) OR
(is_recurring = true AND
EXTRACT(MONTH FROM date) || '-' || EXTRACT(DAY FROM date) IN (
SELECT EXTRACT(MONTH FROM d::date) || '-' || EXTRACT(DAY FROM d::date)
FROM generate_series($2::date, $3::date, '1 day'::interval) d
)
)
)
`;
const countryHolidaysResult = await db.query(countryHolidaysQuery, [
holidaySettings.country_code,
startDate.format('YYYY-MM-DD'),
endDate.format('YYYY-MM-DD')
]);
countryStateHolidays = countryStateHolidays.concat(countryHolidaysResult.rows);
// Fetch state holidays if state_code is set
if (holidaySettings.state_code) {
const stateHolidaysQuery = `
SELECT date
FROM state_holidays
WHERE country_code = $1 AND state_code = $2
AND (
(is_recurring = false AND date >= $3::date AND date <= $4::date) OR
(is_recurring = true AND
EXTRACT(MONTH FROM date) || '-' || EXTRACT(DAY FROM date) IN (
SELECT EXTRACT(MONTH FROM d::date) || '-' || EXTRACT(DAY FROM d::date)
FROM generate_series($3::date, $4::date, '1 day'::interval) d
)
)
)
`;
const stateHolidaysResult = await db.query(stateHolidaysQuery, [
holidaySettings.country_code,
holidaySettings.state_code,
startDate.format('YYYY-MM-DD'),
endDate.format('YYYY-MM-DD')
]);
countryStateHolidays = countryStateHolidays.concat(stateHolidaysResult.rows);
}
}
// Create a Set of holiday dates for efficient lookup
const holidayDates = new Set<string>();
// Add organization holidays
orgHolidaysResult.rows.forEach(row => {
holidayDates.add(moment(row.date).format('YYYY-MM-DD'));
});
// Add country/state holidays (handling recurring holidays)
countryStateHolidays.forEach(row => {
const holidayDate = moment(row.date);
if (row.is_recurring) {
// For recurring holidays, check each year in the date range
let checkDate = startDate.clone().month(holidayDate.month()).date(holidayDate.date());
if (checkDate.isBefore(startDate)) {
checkDate.add(1, 'year');
}
while (checkDate.isSameOrBefore(endDate)) {
if (checkDate.isSameOrAfter(startDate)) {
holidayDates.add(checkDate.format('YYYY-MM-DD'));
}
checkDate.add(1, 'year');
}
} else {
holidayDates.add(holidayDate.format('YYYY-MM-DD'));
}
});
// Count working days based on organization settings, excluding holidays
let workingDays = 0;
let current = startDate.clone();
while (current.isSameOrBefore(endDate, 'day')) {
const day = current.isoWeekday();
if (day >= 1 && day <= 5) workingDays++;
const currentDateStr = current.format('YYYY-MM-DD');
// Check if it's a working day AND not a holiday
if (
!holidayDates.has(currentDateStr) && (
(day === 1 && workingDaysConfig.monday) ||
(day === 2 && workingDaysConfig.tuesday) ||
(day === 3 && workingDaysConfig.wednesday) ||
(day === 4 && workingDaysConfig.thursday) ||
(day === 5 && workingDaysConfig.friday) ||
(day === 6 && workingDaysConfig.saturday) ||
(day === 7 && workingDaysConfig.sunday)
)
) {
workingDays++;
}
current.add(1, 'day');
}
// Get hours_per_day for all selected projects
const projectHoursQuery = `SELECT id, hours_per_day FROM projects WHERE id IN (${projectIds})`;
const projectHoursResult = await db.query(projectHoursQuery, []);
const projectHoursMap: Record<string, number> = {};
for (const row of projectHoursResult.rows) {
projectHoursMap[row.id] = row.hours_per_day || 8;
}
// Sum total working hours for all selected projects
let totalWorkingHours = 0;
for (const pid of Object.keys(projectHoursMap)) {
totalWorkingHours += workingDays * projectHoursMap[pid];
// Get organization working hours
const orgWorkingHoursQuery = `SELECT hours_per_day FROM organizations WHERE id = (SELECT t.organization_id FROM teams t WHERE t.id IN (${teamIds}) LIMIT 1)`;
const orgWorkingHoursResult = await db.query(orgWorkingHoursQuery, []);
const orgWorkingHours = orgWorkingHoursResult.rows[0]?.hours_per_day || 8;
// Calculate total working hours with minimum baseline for non-working day scenarios
let totalWorkingHours = workingDays * orgWorkingHours;
let isNonWorkingPeriod = false;
// If no working days but there might be logged time, set minimum baseline
// This ensures that time logged on non-working days is treated as over-utilization
// Business Logic: If someone works on weekends/holidays when workingDays = 0,
// we use a minimal baseline (1 hour) so any logged time results in >100% utilization
if (totalWorkingHours === 0) {
totalWorkingHours = 1; // Minimal baseline to ensure over-utilization
isNonWorkingPeriod = true;
}
const durationClause = this.getDateRangeClause(duration || DATE_RANGES.LAST_WEEK, date_range);
const archivedClause = archived
? ""
: `AND p.id NOT IN (SELECT project_id FROM archived_projects WHERE project_id = p.id AND user_id = '${req.user?.id}') `;
const billableQuery = this.buildBillableQuery(billable);
const billableQuery = this.buildBillableQueryWithAlias(billable, 't');
const members = (req.body.members || []) as string[];
// Prepare members filter
let membersFilter = "";
if (members.length > 0) {
const memberIds = members.map(id => `'${id}'`).join(",");
membersFilter = `AND tmiv.team_member_id IN (${memberIds})`;
} else {
// If no members are selected, we should not show any data
// This is different from other filters where no selection means "show all"
// For members, no selection should mean "show none" to respect the UI filter state
membersFilter = `AND 1=0`; // This will match no rows
}
// Note: Members filter works differently - when no members are selected, show nothing
const q = `
SELECT tmiv.email, tmiv.name, SUM(time_spent) AS logged_time
FROM team_member_info_view tmiv
LEFT JOIN task_work_log ON task_work_log.user_id = tmiv.user_id
LEFT JOIN tasks ON tasks.id = task_work_log.task_id ${billableQuery}
LEFT JOIN projects p ON p.id = tasks.project_id AND p.team_id = tmiv.team_id
WHERE p.id IN (${projectIds})
${durationClause} ${archivedClause}
GROUP BY tmiv.email, tmiv.name
ORDER BY logged_time DESC;`;
const result = await db.query(q, []);
for (const member of result.rows) {
member.value = member.logged_time ? parseFloat(moment.duration(member.logged_time, "seconds").asHours().toFixed(2)) : 0;
member.color_code = getColor(member.name);
member.total_working_hours = totalWorkingHours;
member.utilization_percent = (totalWorkingHours > 0 && member.logged_time) ? ((parseFloat(member.logged_time) / (totalWorkingHours * 3600)) * 100).toFixed(2) : '0.00';
member.utilized_hours = member.logged_time ? (parseFloat(member.logged_time) / 3600).toFixed(2) : '0.00';
// Over/under utilized hours: utilized_hours - total_working_hours
const overUnder = member.utilized_hours && member.total_working_hours ? (parseFloat(member.utilized_hours) - member.total_working_hours) : 0;
member.over_under_utilized_hours = overUnder.toFixed(2);
// Create custom duration clause for twl table alias
let customDurationClause = "";
if (date_range && date_range.length === 2) {
const start = moment(date_range[0]).format("YYYY-MM-DD");
const end = moment(date_range[1]).format("YYYY-MM-DD");
if (start === end) {
customDurationClause = `AND twl.created_at::DATE = '${start}'::DATE`;
} else {
customDurationClause = `AND twl.created_at::DATE >= '${start}'::DATE AND twl.created_at < '${end}'::DATE + INTERVAL '1 day'`;
}
} else {
const key = duration || DATE_RANGES.LAST_WEEK;
if (key === DATE_RANGES.YESTERDAY)
customDurationClause = "AND twl.created_at >= (CURRENT_DATE - INTERVAL '1 day')::DATE AND twl.created_at < CURRENT_DATE::DATE";
else if (key === DATE_RANGES.LAST_WEEK)
customDurationClause = "AND twl.created_at >= (CURRENT_DATE - INTERVAL '1 week')::DATE AND twl.created_at < CURRENT_DATE::DATE + INTERVAL '1 day'";
else if (key === DATE_RANGES.LAST_MONTH)
customDurationClause = "AND twl.created_at >= (CURRENT_DATE - INTERVAL '1 month')::DATE AND twl.created_at < CURRENT_DATE::DATE + INTERVAL '1 day'";
else if (key === DATE_RANGES.LAST_QUARTER)
customDurationClause = "AND twl.created_at >= (CURRENT_DATE - INTERVAL '3 months')::DATE AND twl.created_at < CURRENT_DATE::DATE + INTERVAL '1 day'";
}
return res.status(200).send(new ServerResponse(true, result.rows));
// Prepare conditional filters for the subquery - only apply if selections are made
let conditionalProjectsFilter = "";
let conditionalCategoriesFilter = "";
// Only apply project filter if projects are actually selected
if (projectIds.length > 0) {
conditionalProjectsFilter = `AND p.id IN (${projectIds})`;
}
// Only apply category filter if categories are selected or noCategory is true
if (categories.length > 0 && noCategory) {
const categoryIds = categories.map(id => `'${id}'`).join(",");
conditionalCategoriesFilter = `AND (p.category_id IS NULL OR p.category_id IN (${categoryIds}))`;
} else if (categories.length === 0 && noCategory) {
conditionalCategoriesFilter = `AND p.category_id IS NULL`;
} else if (categories.length > 0 && !noCategory) {
const categoryIds = categories.map(id => `'${id}'`).join(",");
conditionalCategoriesFilter = `AND p.category_id IN (${categoryIds})`;
}
// If no categories and no noCategory, don't filter by category (show all)
// Check if all filters are unchecked (Clear All scenario) - return no data to avoid overwhelming UI
const hasProjectFilter = projectIds.length > 0;
const hasCategoryFilter = categories.length > 0 || noCategory;
const hasMemberFilter = members.length > 0;
// Note: We'll check utilization filter after the query since it's applied post-processing
if (!hasProjectFilter && !hasCategoryFilter && !hasMemberFilter) {
// Still need to check utilization filter, but we'll do a quick check
const utilization = (req.body.utilization || []) as string[];
const hasUtilizationFilter = utilization.length > 0;
if (!hasUtilizationFilter) {
return res.status(200).send(new ServerResponse(true, { filteredRows: [], totals: { total_time_logs: "0", total_estimated_hours: "0", total_utilization: "0" } }));
}
}
// Modified query to start from team members and calculate filtered time logs
// This query ensures ALL active team members are included, even if they have no logged time
const q = `
SELECT
tmiv.team_member_id,
tmiv.email,
tmiv.name,
COALESCE(
(SELECT SUM(twl.time_spent)
FROM task_work_log twl
LEFT JOIN tasks t ON t.id = twl.task_id
LEFT JOIN projects p ON p.id = t.project_id
WHERE twl.user_id = tmiv.user_id
${customDurationClause}
${conditionalProjectsFilter}
${conditionalCategoriesFilter}
${archivedClause}
${billableQuery}
AND p.team_id = tmiv.team_id
), 0
) AS logged_time
FROM team_member_info_view tmiv
WHERE tmiv.team_id IN (${teamIds})
AND tmiv.active = TRUE
${membersFilter}
GROUP BY tmiv.email, tmiv.name, tmiv.team_member_id, tmiv.user_id, tmiv.team_id
ORDER BY logged_time DESC;`;
const result = await db.query(q, []);
const utilization = (req.body.utilization || []) as string[];
// Precompute totalWorkingHours * 3600 for efficiency
const totalWorkingSeconds = totalWorkingHours * 3600;
// calculate utilization state
for (let i = 0, len = result.rows.length; i < len; i++) {
const member = result.rows[i];
const loggedSeconds = member.logged_time ? parseFloat(member.logged_time) : 0;
const utilizedHours = loggedSeconds / 3600;
// For individual members, use the same logic as total calculation
let memberWorkingHours;
let utilizationPercent;
if (isNonWorkingPeriod) {
// Non-working period: each member's expected working hours is 0
memberWorkingHours = 0;
// Any time logged during non-working period is overtime
utilizationPercent = loggedSeconds > 0 ? 100 : 0; // Show 100+ as numeric 100 for consistency
} else {
// Normal working period
memberWorkingHours = totalWorkingHours;
utilizationPercent = memberWorkingHours > 0 && loggedSeconds
? ((loggedSeconds / (memberWorkingHours * 3600)) * 100)
: 0;
}
const overUnder = utilizedHours - memberWorkingHours;
member.value = utilizedHours ? parseFloat(utilizedHours.toFixed(2)) : 0;
member.color_code = getColor(member.name);
member.total_working_hours = memberWorkingHours;
member.utilization_percent = utilizationPercent.toFixed(2);
member.utilized_hours = utilizedHours.toFixed(2);
member.over_under_utilized_hours = overUnder.toFixed(2);
if (utilizationPercent < 90) {
member.utilization_state = 'under';
} else if (utilizationPercent <= 110) {
member.utilization_state = 'optimal';
} else {
member.utilization_state = 'over';
}
}
// Apply utilization filter
let filteredRows;
if (utilization.length > 0) {
// Filter to only show selected utilization states
filteredRows = result.rows.filter(member => utilization.includes(member.utilization_state));
} else {
// No utilization states selected
// If we reached here, it means at least one other filter was applied
// so we show all members (don't filter by utilization)
filteredRows = result.rows;
}
// Calculate totals
const total_time_logs = filteredRows.reduce((sum, member) => sum + parseFloat(member.logged_time || '0'), 0);
let total_estimated_hours;
let total_utilization;
if (isNonWorkingPeriod) {
// Non-working period: expected capacity is 0
total_estimated_hours = 0;
// Special handling for utilization on non-working days
total_utilization = total_time_logs > 0 ? "100+" : "0";
} else {
// Normal working period calculation
total_estimated_hours = totalWorkingHours * filteredRows.length;
total_utilization = total_time_logs > 0 && total_estimated_hours > 0
? ((total_time_logs / (total_estimated_hours * 3600)) * 100).toFixed(1)
: '0';
}
return res.status(200).send(new ServerResponse(true, {
filteredRows,
totals: {
total_time_logs: ((total_time_logs / 3600).toFixed(2)).toString(),
total_estimated_hours: total_estimated_hours.toString(),
total_utilization: total_utilization.toString(),
},
}));
}
@HandleExceptions()
@@ -580,6 +945,9 @@ export default class ReportingAllocationController extends ReportingControllerBa
const projects = (req.body.projects || []) as string[];
const projectIds = projects.map(p => `'${p}'`).join(",");
const categories = (req.body.categories || []) as string[];
const noCategory = req.body.noCategory || false;
const { type, billable } = req.body;
if (!teamIds || !projectIds.length)
@@ -595,6 +963,33 @@ export default class ReportingAllocationController extends ReportingControllerBa
const billableQuery = this.buildBillableQuery(billable);
// Prepare projects filter
let projectsFilter = "";
if (projectIds.length > 0) {
projectsFilter = `AND p.id IN (${projectIds})`;
} else {
// If no projects are selected, don't show any data
projectsFilter = `AND 1=0`; // This will match no rows
}
// Prepare categories filter - updated logic
let categoriesFilter = "";
if (categories.length > 0 && noCategory) {
// Both specific categories and "No Category" are selected
const categoryIds = categories.map(id => `'${id}'`).join(",");
categoriesFilter = `AND (p.category_id IS NULL OR p.category_id IN (${categoryIds}))`;
} else if (categories.length === 0 && noCategory) {
// Only "No Category" is selected
categoriesFilter = `AND p.category_id IS NULL`;
} else if (categories.length > 0 && !noCategory) {
// Only specific categories are selected
const categoryIds = categories.map(id => `'${id}'`).join(",");
categoriesFilter = `AND p.category_id IN (${categoryIds})`;
} else {
// categories.length === 0 && !noCategory - no categories selected, show nothing
categoriesFilter = `AND 1=0`; // This will match no rows
}
const q = `
SELECT p.id,
p.name,
@@ -608,9 +1003,9 @@ export default class ReportingAllocationController extends ReportingControllerBa
WHERE project_id = p.id) AS estimated,
color_code
FROM projects p
LEFT JOIN tasks ON tasks.project_id = p.id ${billableQuery}
LEFT JOIN tasks ON tasks.project_id = p.id
LEFT JOIN task_work_log ON task_work_log.task_id = tasks.id
WHERE p.id IN (${projectIds}) ${durationClause} ${archivedClause}
WHERE p.id IN (${projectIds}) ${durationClause} ${archivedClause} ${categoriesFilter} ${billableQuery}
GROUP BY p.id, p.name
ORDER BY logged_time DESC;`;
const result = await db.query(q, []);
@@ -636,4 +1031,4 @@ export default class ReportingAllocationController extends ReportingControllerBa
return res.status(200).send(new ServerResponse(true, data));
}
}
}

View File

@@ -0,0 +1,140 @@
import WorklenzControllerBase from "../worklenz-controller-base";
import { IWorkLenzRequest } from "../../interfaces/worklenz-request";
import db from "../../config/db";
import moment from "moment-timezone";
import { DATE_RANGES } from "../../shared/constants";
export default abstract class ReportingControllerBaseWithTimezone extends WorklenzControllerBase {
/**
* Get the user's timezone from the database or request
* @param userId - The user ID
* @returns The user's timezone or 'UTC' as default
*/
protected static async getUserTimezone(userId: string): Promise<string> {
const q = `SELECT tz.name as timezone
FROM users u
JOIN timezones tz ON u.timezone_id = tz.id
WHERE u.id = $1`;
const result = await db.query(q, [userId]);
return result.rows[0]?.timezone || "UTC";
}
/**
* Generate date range clause with timezone support
* @param key - Date range key (e.g., YESTERDAY, LAST_WEEK)
* @param dateRange - Array of date strings
* @param userTimezone - User's timezone (e.g., 'America/New_York')
* @returns SQL clause for date filtering
*/
protected static getDateRangeClauseWithTimezone(key: string, dateRange: string[], userTimezone: string) {
// For custom date ranges
if (dateRange.length === 2) {
try {
// Handle different date formats that might come from frontend
let startDate, endDate;
// Try to parse the date - it might be a full JS Date string or ISO string
if (dateRange[0].includes("GMT") || dateRange[0].includes("(")) {
// Parse JavaScript Date toString() format
startDate = moment(new Date(dateRange[0]));
endDate = moment(new Date(dateRange[1]));
} else {
// Parse ISO format or other formats
startDate = moment(dateRange[0]);
endDate = moment(dateRange[1]);
}
// Convert to user's timezone and get start/end of day
const start = startDate.tz(userTimezone).startOf("day");
const end = endDate.tz(userTimezone).endOf("day");
// Convert to UTC for database comparison
const startUtc = start.utc().format("YYYY-MM-DD HH:mm:ss");
const endUtc = end.utc().format("YYYY-MM-DD HH:mm:ss");
if (start.isSame(end, "day")) {
// Single day selection
return `AND twl.created_at >= '${startUtc}'::TIMESTAMP AND twl.created_at <= '${endUtc}'::TIMESTAMP`;
}
return `AND twl.created_at >= '${startUtc}'::TIMESTAMP AND twl.created_at <= '${endUtc}'::TIMESTAMP`;
} catch (error) {
console.error("Error parsing date range:", error, { dateRange, userTimezone });
// Fallback to current date if parsing fails
const now = moment.tz(userTimezone);
const startUtc = now.clone().startOf("day").utc().format("YYYY-MM-DD HH:mm:ss");
const endUtc = now.clone().endOf("day").utc().format("YYYY-MM-DD HH:mm:ss");
return `AND twl.created_at >= '${startUtc}'::TIMESTAMP AND twl.created_at <= '${endUtc}'::TIMESTAMP`;
}
}
// For predefined ranges, calculate based on user's timezone
const now = moment.tz(userTimezone);
let startDate, endDate;
switch (key) {
case DATE_RANGES.YESTERDAY:
startDate = now.clone().subtract(1, "day").startOf("day");
endDate = now.clone().subtract(1, "day").endOf("day");
break;
case DATE_RANGES.LAST_WEEK:
startDate = now.clone().subtract(1, "week").startOf("week");
endDate = now.clone().subtract(1, "week").endOf("week");
break;
case DATE_RANGES.LAST_MONTH:
startDate = now.clone().subtract(1, "month").startOf("month");
endDate = now.clone().subtract(1, "month").endOf("month");
break;
case DATE_RANGES.LAST_QUARTER:
startDate = now.clone().subtract(3, "months").startOf("day");
endDate = now.clone().endOf("day");
break;
default:
return "";
}
if (startDate && endDate) {
const startUtc = startDate.utc().format("YYYY-MM-DD HH:mm:ss");
const endUtc = endDate.utc().format("YYYY-MM-DD HH:mm:ss");
return `AND twl.created_at >= '${startUtc}'::TIMESTAMP AND twl.created_at <= '${endUtc}'::TIMESTAMP`;
}
return "";
}
/**
* Format dates for display in user's timezone
* @param date - Date to format
* @param userTimezone - User's timezone
* @param format - Moment format string
* @returns Formatted date string
*/
protected static formatDateInTimezone(date: string | Date, userTimezone: string, format = "YYYY-MM-DD HH:mm:ss") {
return moment.tz(date, userTimezone).format(format);
}
/**
* Get working days count between two dates in user's timezone
* @param startDate - Start date
* @param endDate - End date
* @param userTimezone - User's timezone
* @returns Number of working days
*/
protected static getWorkingDaysInTimezone(startDate: string, endDate: string, userTimezone: string): number {
const start = moment.tz(startDate, userTimezone);
const end = moment.tz(endDate, userTimezone);
let workingDays = 0;
const current = start.clone();
while (current.isSameOrBefore(end, "day")) {
// Monday = 1, Friday = 5
if (current.isoWeekday() >= 1 && current.isoWeekday() <= 5) {
workingDays++;
}
current.add(1, "day");
}
return workingDays;
}
}

View File

@@ -6,10 +6,69 @@ import { IWorkLenzResponse } from "../../interfaces/worklenz-response";
import { ServerResponse } from "../../models/server-response";
import { DATE_RANGES, TASK_PRIORITY_COLOR_ALPHA } from "../../shared/constants";
import { formatDuration, getColor, int } from "../../shared/utils";
import ReportingControllerBase from "./reporting-controller-base";
import ReportingControllerBaseWithTimezone from "./reporting-controller-base-with-timezone";
import Excel from "exceljs";
export default class ReportingMembersController extends ReportingControllerBase {
export default class ReportingMembersController extends ReportingControllerBaseWithTimezone {
protected static getPercentage(n: number, total: number) {
return +(n ? (n / total) * 100 : 0).toFixed();
}
protected static getCurrentTeamId(req: IWorkLenzRequest): string | null {
return req.user?.team_id ?? null;
}
public static convertMinutesToHoursAndMinutes(totalMinutes: number) {
const hours = Math.floor(totalMinutes / 60);
const minutes = totalMinutes % 60;
return `${hours}h ${minutes}m`;
}
public static convertSecondsToHoursAndMinutes(seconds: number) {
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
return `${hours}h ${minutes}m`;
}
protected static formatEndDate(endDate: string) {
const end = moment(endDate).format("YYYY-MM-DD");
const fEndDate = moment(end);
return fEndDate;
}
protected static formatCurrentDate() {
const current = moment().format("YYYY-MM-DD");
const fCurrentDate = moment(current);
return fCurrentDate;
}
protected static getDaysLeft(endDate: string): number | null {
if (!endDate) return null;
const fCurrentDate = this.formatCurrentDate();
const fEndDate = this.formatEndDate(endDate);
return fEndDate.diff(fCurrentDate, "days");
}
protected static isOverdue(endDate: string): boolean {
if (!endDate) return false;
const fCurrentDate = this.formatCurrentDate();
const fEndDate = this.formatEndDate(endDate);
return fEndDate.isBefore(fCurrentDate);
}
protected static isToday(endDate: string): boolean {
if (!endDate) return false;
const fCurrentDate = this.formatCurrentDate();
const fEndDate = this.formatEndDate(endDate);
return fEndDate.isSame(fCurrentDate);
}
private static async getMembers(
teamId: string, searchQuery = "",
@@ -31,6 +90,7 @@ export default class ReportingMembersController extends ReportingControllerBase
const completedDurationClasue = this.completedDurationFilter(key, dateRange);
const overdueActivityLogsClause = this.getActivityLogsOverdue(key, dateRange);
const activityLogCreationFilter = this.getActivityLogsCreationClause(key, dateRange);
const timeLogDateRangeClause = this.getTimeLogDateRangeClause(key, dateRange);
const q = `SELECT COUNT(DISTINCT email) AS total,
(SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(t))), '[]'::JSON)
@@ -100,12 +160,27 @@ export default class ReportingMembersController extends ReportingControllerBase
FROM tasks t
LEFT JOIN tasks_assignees ta ON t.id = ta.task_id
WHERE team_member_id = tmiv.team_member_id
AND is_doing((SELECT new_value FROM task_activity_logs tl WHERE tl.task_id = t.id AND tl.attribute_type = 'status' ${activityLogCreationFilter} ORDER BY tl.created_at DESC LIMIT 1)::UUID, t.project_id) ${archivedClause}) AS ongoing_by_activity_logs
AND is_doing((SELECT new_value FROM task_activity_logs tl WHERE tl.task_id = t.id AND tl.attribute_type = 'status' ${activityLogCreationFilter} ORDER BY tl.created_at DESC LIMIT 1)::UUID, t.project_id) ${archivedClause}) AS ongoing_by_activity_logs,
(SELECT COALESCE(SUM(twl.time_spent), 0)
FROM task_work_log twl
LEFT JOIN tasks t ON twl.task_id = t.id
WHERE twl.user_id = (SELECT user_id FROM team_members WHERE id = tmiv.team_member_id)
AND t.billable IS TRUE
AND t.project_id IN (SELECT id FROM projects WHERE team_id = $1)
${timeLogDateRangeClause}
${includeArchived ? "" : `AND t.project_id NOT IN (SELECT project_id FROM archived_projects WHERE project_id = t.project_id AND archived_projects.user_id = '${userId}')`}) AS billable_time,
(SELECT COALESCE(SUM(twl.time_spent), 0)
FROM task_work_log twl
LEFT JOIN tasks t ON twl.task_id = t.id
WHERE twl.user_id = (SELECT user_id FROM team_members WHERE id = tmiv.team_member_id)
AND t.billable IS FALSE
AND t.project_id IN (SELECT id FROM projects WHERE team_id = $1)
${timeLogDateRangeClause}
${includeArchived ? "" : `AND t.project_id NOT IN (SELECT project_id FROM archived_projects WHERE project_id = t.project_id AND archived_projects.user_id = '${userId}')`}) AS non_billable_time
FROM team_member_info_view tmiv
WHERE tmiv.team_id = $1 ${teamsClause}
AND tmiv.team_member_id IN (SELECT team_member_id
FROM project_members
WHERE project_id IN (SELECT id FROM projects WHERE projects.team_id = tmiv.team_id))
${searchQuery}
GROUP BY email, name, avatar_url, team_member_id, tmiv.team_id
ORDER BY last_user_activity DESC NULLS LAST
@@ -113,9 +188,6 @@ export default class ReportingMembersController extends ReportingControllerBase
${pagingClause}) t) AS members
FROM team_member_info_view tmiv
WHERE tmiv.team_id = $1 ${teamsClause}
AND tmiv.team_member_id IN (SELECT team_member_id
FROM project_members
WHERE project_id IN (SELECT id FROM projects WHERE projects.team_id = tmiv.team_id))
${searchQuery}`;
const result = await db.query(q, [teamId]);
const [data] = result.rows;
@@ -311,6 +383,30 @@ export default class ReportingMembersController extends ReportingControllerBase
return "";
}
protected static getTimeLogDateRangeClause(key: string, dateRange: string[]) {
if (dateRange.length === 2) {
const start = moment(dateRange[0]).format("YYYY-MM-DD");
const end = moment(dateRange[1]).format("YYYY-MM-DD");
if (start === end) {
return `AND twl.created_at::DATE = '${start}'::DATE`;
}
return `AND twl.created_at::DATE >= '${start}'::DATE AND twl.created_at < '${end}'::DATE + INTERVAL '1 day'`;
}
if (key === DATE_RANGES.YESTERDAY)
return `AND twl.created_at >= (CURRENT_DATE - INTERVAL '1 day')::DATE AND twl.created_at < CURRENT_DATE::DATE`;
if (key === DATE_RANGES.LAST_WEEK)
return `AND twl.created_at >= (CURRENT_DATE - INTERVAL '1 week')::DATE AND twl.created_at < CURRENT_DATE::DATE + INTERVAL '1 day'`;
if (key === DATE_RANGES.LAST_MONTH)
return `AND twl.created_at >= (CURRENT_DATE - INTERVAL '1 month')::DATE AND twl.created_at < CURRENT_DATE::DATE + INTERVAL '1 day'`;
if (key === DATE_RANGES.LAST_QUARTER)
return `AND twl.created_at >= (CURRENT_DATE - INTERVAL '3 months')::DATE AND twl.created_at < CURRENT_DATE::DATE + INTERVAL '1 day'`;
return "";
}
private static formatDuration(duration: moment.Duration) {
const empty = "0h 0m";
let format = "";
@@ -423,6 +519,8 @@ export default class ReportingMembersController extends ReportingControllerBase
{ header: "Overdue Tasks", key: "overdue_tasks", width: 20 },
{ header: "Completed Tasks", key: "completed_tasks", width: 20 },
{ header: "Ongoing Tasks", key: "ongoing_tasks", width: 20 },
{ header: "Billable Time (seconds)", key: "billable_time", width: 25 },
{ header: "Non-Billable Time (seconds)", key: "non_billable_time", width: 25 },
{ header: "Done Tasks(%)", key: "done_tasks", width: 20 },
{ header: "Doing Tasks(%)", key: "doing_tasks", width: 20 },
{ header: "Todo Tasks(%)", key: "todo_tasks", width: 20 }
@@ -430,14 +528,14 @@ export default class ReportingMembersController extends ReportingControllerBase
// set title
sheet.getCell("A1").value = `Members from ${teamName}`;
sheet.mergeCells("A1:K1");
sheet.mergeCells("A1:M1");
sheet.getCell("A1").alignment = { horizontal: "center" };
sheet.getCell("A1").style.fill = { type: "pattern", pattern: "solid", fgColor: { argb: "D9D9D9" } };
sheet.getCell("A1").font = { size: 16 };
// set export date
sheet.getCell("A2").value = `Exported on : ${exportDate}`;
sheet.mergeCells("A2:K2");
sheet.mergeCells("A2:M2");
sheet.getCell("A2").alignment = { horizontal: "center" };
sheet.getCell("A2").style.fill = { type: "pattern", pattern: "solid", fgColor: { argb: "F2F2F2" } };
sheet.getCell("A2").font = { size: 12 };
@@ -447,7 +545,7 @@ export default class ReportingMembersController extends ReportingControllerBase
sheet.mergeCells("A3:D3");
// set table headers
sheet.getRow(5).values = ["Member", "Email", "Tasks Assigned", "Overdue Tasks", "Completed Tasks", "Ongoing Tasks", "Done Tasks(%)", "Doing Tasks(%)", "Todo Tasks(%)"];
sheet.getRow(5).values = ["Member", "Email", "Tasks Assigned", "Overdue Tasks", "Completed Tasks", "Ongoing Tasks", "Billable Time (seconds)", "Non-Billable Time (seconds)", "Done Tasks(%)", "Doing Tasks(%)", "Todo Tasks(%)"];
sheet.getRow(5).font = { bold: true };
for (const member of result.members) {
@@ -458,6 +556,8 @@ export default class ReportingMembersController extends ReportingControllerBase
overdue_tasks: member.overdue,
completed_tasks: member.completed,
ongoing_tasks: member.ongoing,
billable_time: member.billable_time || 0,
non_billable_time: member.non_billable_time || 0,
done_tasks: member.completed,
doing_tasks: member.ongoing_by_activity_logs,
todo_tasks: member.todo_by_activity_logs
@@ -487,7 +587,9 @@ export default class ReportingMembersController extends ReportingControllerBase
dateRange = date_range.split(",");
}
const durationClause = ReportingMembersController.getDateRangeClauseMembers(duration as string || DATE_RANGES.LAST_WEEK, dateRange, "twl");
// Get user timezone for proper date filtering
const userTimezone = await this.getUserTimezone(req.user?.id as string);
const durationClause = this.getDateRangeClauseWithTimezone(duration as string || DATE_RANGES.LAST_WEEK, dateRange, userTimezone);
const minMaxDateClause = this.getMinMaxDates(duration as string || DATE_RANGES.LAST_WEEK, dateRange, "task_work_log");
const memberName = (req.query.member_name as string)?.trim() || null;
@@ -1038,7 +1140,9 @@ export default class ReportingMembersController extends ReportingControllerBase
public static async getMemberTimelogs(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
const { team_member_id, team_id, duration, date_range, archived, billable } = req.body;
const durationClause = ReportingMembersController.getDateRangeClauseMembers(duration || DATE_RANGES.LAST_WEEK, date_range, "twl");
// Get user timezone for proper date filtering
const userTimezone = await this.getUserTimezone(req.user?.id as string);
const durationClause = this.getDateRangeClauseWithTimezone(duration || DATE_RANGES.LAST_WEEK, date_range, userTimezone);
const minMaxDateClause = this.getMinMaxDates(duration || DATE_RANGES.LAST_WEEK, date_range, "task_work_log");
const billableQuery = this.buildBillableQuery(billable);
@@ -1230,8 +1334,8 @@ public static async getSingleMemberProjects(req: IWorkLenzRequest, res: IWorkLen
row.actual_time = int(row.actual_time);
row.estimated_time_string = this.convertMinutesToHoursAndMinutes(int(row.estimated_time));
row.actual_time_string = this.convertSecondsToHoursAndMinutes(int(row.actual_time));
row.days_left = ReportingControllerBase.getDaysLeft(row.end_date);
row.is_overdue = ReportingControllerBase.isOverdue(row.end_date);
row.days_left = this.getDaysLeft(row.end_date);
row.is_overdue = this.isOverdue(row.end_date);
if (row.days_left && row.is_overdue) {
row.days_left = row.days_left.toString().replace(/-/g, "");
}
@@ -1329,4 +1433,4 @@ public static async getSingleMemberProjects(req: IWorkLenzRequest, res: IWorkLen
}
}
}

View File

@@ -53,13 +53,13 @@ export default class ScheduleControllerV2 extends WorklenzControllerBase {
const [workingDays] = workingDaysResults.rows;
// get organization working hours
const getDataHoursq = `SELECT working_hours FROM organizations WHERE user_id = $1 GROUP BY id LIMIT 1;`;
const getDataHoursq = `SELECT hours_per_day FROM organizations WHERE user_id = $1 GROUP BY id LIMIT 1;`;
const workingHoursResults = await db.query(getDataHoursq, [req.user?.owner_id]);
const [workingHours] = workingHoursResults.rows;
return res.status(200).send(new ServerResponse(true, { workingDays: workingDays?.working_days, workingHours: workingHours?.working_hours }));
return res.status(200).send(new ServerResponse(true, { workingDays: workingDays?.working_days, workingHours: workingHours?.hours_per_day }));
}
@HandleExceptions()
@@ -74,18 +74,13 @@ export default class ScheduleControllerV2 extends WorklenzControllerBase {
.map(day => `${day.toLowerCase()} = ${workingDays.includes(day)}`)
.join(", ");
const updateQuery = `
UPDATE public.organization_working_days
const updateQuery = `UPDATE public.organization_working_days
SET ${setClause}, updated_at = CURRENT_TIMESTAMP
WHERE organization_id IN (
SELECT organization_id FROM organizations
WHERE user_id = $1
);
`;
WHERE organization_id IN (SELECT id FROM organizations WHERE user_id = $1);`;
await db.query(updateQuery, [req.user?.owner_id]);
const getDataHoursq = `UPDATE organizations SET working_hours = $1 WHERE user_id = $2;`;
const getDataHoursq = `UPDATE organizations SET hours_per_day = $1 WHERE user_id = $2;`;
await db.query(getDataHoursq, [workingHours, req.user?.owner_id]);

View File

@@ -0,0 +1,201 @@
import { IWorkLenzRequest } from "../interfaces/worklenz-request";
import { IWorkLenzResponse } from "../interfaces/worklenz-response";
import { ServerResponse } from "../models/server-response";
import WorklenzControllerBase from "./worklenz-controller-base";
import HandleExceptions from "../decorators/handle-exceptions";
import { ISurveySubmissionRequest } from "../interfaces/survey";
import db from "../config/db";
export default class SurveyController extends WorklenzControllerBase {
@HandleExceptions()
public static async getAccountSetupSurvey(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
const q = `
SELECT
s.id,
s.name,
s.description,
s.survey_type,
s.is_active,
COALESCE(
json_agg(
json_build_object(
'id', sq.id,
'survey_id', sq.survey_id,
'question_key', sq.question_key,
'question_type', sq.question_type,
'is_required', sq.is_required,
'sort_order', sq.sort_order,
'options', sq.options
) ORDER BY sq.sort_order
) FILTER (WHERE sq.id IS NOT NULL),
'[]'
) AS questions
FROM surveys s
LEFT JOIN survey_questions sq ON s.id = sq.survey_id
WHERE s.survey_type = 'account_setup' AND s.is_active = true
GROUP BY s.id, s.name, s.description, s.survey_type, s.is_active
LIMIT 1;
`;
const result = await db.query(q);
const [survey] = result.rows;
if (!survey) {
return res.status(200).send(new ServerResponse(false, null, "Account setup survey not found"));
}
return res.status(200).send(new ServerResponse(true, survey));
}
@HandleExceptions()
public static async submitSurveyResponse(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
const userId = req.user?.id;
const body = req.body as ISurveySubmissionRequest;
if (!userId) {
return res.status(200).send(new ServerResponse(false, null, "User not authenticated"));
}
if (!body.survey_id || !body.answers || !Array.isArray(body.answers)) {
return res.status(200).send(new ServerResponse(false, null, "Invalid survey submission data"));
}
// Check if user has already submitted a response for this survey
const existingResponseQuery = `
SELECT id FROM survey_responses
WHERE user_id = $1 AND survey_id = $2;
`;
const existingResult = await db.query(existingResponseQuery, [userId, body.survey_id]);
let responseId: string;
if (existingResult.rows.length > 0) {
// Update existing response
responseId = existingResult.rows[0].id;
const updateResponseQuery = `
UPDATE survey_responses
SET is_completed = true, completed_at = NOW(), updated_at = NOW()
WHERE id = $1;
`;
await db.query(updateResponseQuery, [responseId]);
// Delete existing answers
const deleteAnswersQuery = `DELETE FROM survey_answers WHERE response_id = $1;`;
await db.query(deleteAnswersQuery, [responseId]);
} else {
// Create new response
const createResponseQuery = `
INSERT INTO survey_responses (survey_id, user_id, is_completed, completed_at)
VALUES ($1, $2, true, NOW())
RETURNING id;
`;
const responseResult = await db.query(createResponseQuery, [body.survey_id, userId]);
responseId = responseResult.rows[0].id;
}
// Insert new answers
if (body.answers.length > 0) {
const answerValues: string[] = [];
const params: any[] = [];
body.answers.forEach((answer, index) => {
const baseIndex = index * 4;
answerValues.push(`($${baseIndex + 1}, $${baseIndex + 2}, $${baseIndex + 3}, $${baseIndex + 4})`);
params.push(
responseId,
answer.question_id,
answer.answer_text || null,
answer.answer_json ? JSON.stringify(answer.answer_json) : null
);
});
const insertAnswersQuery = `
INSERT INTO survey_answers (response_id, question_id, answer_text, answer_json)
VALUES ${answerValues.join(', ')};
`;
await db.query(insertAnswersQuery, params);
}
return res.status(200).send(new ServerResponse(true, { response_id: responseId }));
}
@HandleExceptions()
public static async getUserSurveyResponse(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
const userId = req.user?.id;
const surveyId = req.params.survey_id;
if (!userId) {
return res.status(200).send(new ServerResponse(false, null, "User not authenticated"));
}
const q = `
SELECT
sr.id,
sr.survey_id,
sr.user_id,
sr.is_completed,
sr.started_at,
sr.completed_at,
COALESCE(
json_agg(
json_build_object(
'question_id', sa.question_id,
'answer_text', sa.answer_text,
'answer_json', sa.answer_json
)
) FILTER (WHERE sa.id IS NOT NULL),
'[]'
) AS answers
FROM survey_responses sr
LEFT JOIN survey_answers sa ON sr.id = sa.response_id
WHERE sr.user_id = $1 AND sr.survey_id = $2
GROUP BY sr.id, sr.survey_id, sr.user_id, sr.is_completed, sr.started_at, sr.completed_at;
`;
const result = await db.query(q, [userId, surveyId]);
const [response] = result.rows;
if (!response) {
return res.status(200).send(new ServerResponse(false, null, "Survey response not found"));
}
return res.status(200).send(new ServerResponse(true, response));
}
@HandleExceptions()
public static async checkAccountSetupSurveyStatus(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
const userId = req.user?.id;
if (!userId) {
return res.status(200).send(new ServerResponse(false, null, "User not authenticated"));
}
const q = `
SELECT EXISTS(
SELECT 1
FROM survey_responses sr
INNER JOIN surveys s ON sr.survey_id = s.id
WHERE sr.user_id = $1
AND s.survey_type = 'account_setup'
AND sr.is_completed = true
) as is_completed,
(
SELECT sr.completed_at
FROM survey_responses sr
INNER JOIN surveys s ON sr.survey_id = s.id
WHERE sr.user_id = $1
AND s.survey_type = 'account_setup'
AND sr.is_completed = true
LIMIT 1
) as completed_at;
`;
const result = await db.query(q, [userId]);
const status = result.rows[0] || { is_completed: false, completed_at: null };
return res.status(200).send(new ServerResponse(true, status));
}
}

View File

@@ -16,19 +16,23 @@ export default class TaskPhasesController extends WorklenzControllerBase {
if (!req.query.id)
return res.status(400).send(new ServerResponse(false, null, "Invalid request"));
// Use custom name if provided, otherwise use default naming pattern
const phaseName = req.body.name?.trim() ||
`Untitled Phase (${(await db.query("SELECT COUNT(*) FROM project_phases WHERE project_id = $1", [req.query.id])).rows[0].count + 1})`;
const q = `
INSERT INTO project_phases (name, color_code, project_id, sort_index)
VALUES (
CONCAT('Untitled Phase (', (SELECT COUNT(*) FROM project_phases WHERE project_id = $2) + 1, ')'),
$1,
$2,
(SELECT COUNT(*) FROM project_phases WHERE project_id = $2) + 1)
$3,
(SELECT COUNT(*) FROM project_phases WHERE project_id = $3) + 1)
RETURNING id, name, color_code, sort_index;
`;
req.body.color_code = this.DEFAULT_PHASE_COLOR;
const result = await db.query(q, [req.body.color_code, req.query.id]);
const result = await db.query(q, [phaseName, req.body.color_code, req.query.id]);
const [data] = result.rows;
data.color_code = getColor(data.name) + TASK_STATUS_COLOR_ALPHA;

View File

@@ -134,6 +134,25 @@ export default class TaskStatusesController extends WorklenzControllerBase {
return res.status(200).send(new ServerResponse(true, data));
}
@HandleExceptions()
public static async updateCategory(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
const hasMoreCategories = await TaskStatusesController.hasMoreCategories(req.params.id, req.query.current_project_id as string);
if (!hasMoreCategories)
return res.status(200).send(new ServerResponse(false, null, existsErrorMessage).withTitle("Status category update failed!"));
const q = `
UPDATE task_statuses
SET category_id = $2
WHERE id = $1
AND project_id = $3
RETURNING (SELECT color_code FROM sys_task_status_categories WHERE id = task_statuses.category_id), (SELECT color_code_dark FROM sys_task_status_categories WHERE id = task_statuses.category_id);
`;
const result = await db.query(q, [req.params.id, req.body.category_id, req.query.current_project_id]);
const [data] = result.rows;
return res.status(200).send(new ServerResponse(true, data));
}
@HandleExceptions()
public static async updateStatusOrder(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
const q = `SELECT update_status_order($1);`;

View File

@@ -1,6 +1,6 @@
import WorklenzControllerBase from "./worklenz-controller-base";
import {getColor} from "../shared/utils";
import {PriorityColorCodes, TASK_PRIORITY_COLOR_ALPHA, TASK_STATUS_COLOR_ALPHA} from "../shared/constants";
import { getColor } from "../shared/utils";
import { PriorityColorCodes, TASK_PRIORITY_COLOR_ALPHA, TASK_STATUS_COLOR_ALPHA } from "../shared/constants";
import moment from "moment/moment";
export const GroupBy = {
@@ -16,6 +16,7 @@ export interface ITaskGroup {
start_date?: string;
end_date?: string;
color_code: string;
color_code_dark: string;
category_id: string | null;
old_category_id?: string;
todo_progress?: number;
@@ -32,23 +33,14 @@ export default class TasksControllerBase extends WorklenzControllerBase {
}
public static updateTaskViewModel(task: any) {
console.log(`Processing task ${task.id} (${task.name})`);
console.log(` manual_progress: ${task.manual_progress}, progress_value: ${task.progress_value}`);
console.log(` project_use_manual_progress: ${task.project_use_manual_progress}, project_use_weighted_progress: ${task.project_use_weighted_progress}`);
console.log(` has subtasks: ${task.sub_tasks_count > 0}`);
// For parent tasks (with subtasks), always use calculated progress from subtasks
if (task.sub_tasks_count > 0) {
// For parent tasks without manual progress, calculate from subtasks (already done via db function)
console.log(` Parent task with subtasks: complete_ratio=${task.complete_ratio}`);
// Ensure progress matches complete_ratio for consistency
task.progress = task.complete_ratio || 0;
// Important: Parent tasks should not have manual progress
// If they somehow do, reset it
if (task.manual_progress) {
console.log(` WARNING: Parent task ${task.id} had manual_progress set to true, resetting`);
task.manual_progress = false;
task.progress_value = null;
}
@@ -58,28 +50,29 @@ export default class TasksControllerBase extends WorklenzControllerBase {
// For manually set progress, use that value directly
task.progress = parseInt(task.progress_value);
task.complete_ratio = parseInt(task.progress_value);
console.log(` Using manual progress: progress=${task.progress}, complete_ratio=${task.complete_ratio}`);
}
// For tasks with no subtasks and no manual progress, calculate based on time
}
// For tasks with no subtasks and no manual progress
else {
task.progress = task.total_minutes_spent && task.total_minutes
? ~~(task.total_minutes_spent / task.total_minutes * 100)
: 0;
// Only calculate progress based on time if time-based progress is enabled for the project
if (task.project_use_time_progress && task.total_minutes_spent && task.total_minutes) {
// Cap the progress at 100% to prevent showing more than 100% progress
task.progress = Math.min(~~(task.total_minutes_spent / task.total_minutes * 100), 100);
} else {
// Default to 0% progress when time-based calculation is not enabled
task.progress = 0;
}
// Set complete_ratio to match progress
task.complete_ratio = task.progress;
console.log(` Calculated time-based progress: progress=${task.progress}, complete_ratio=${task.complete_ratio}`);
}
// Ensure numeric values
task.progress = parseInt(task.progress) || 0;
task.complete_ratio = parseInt(task.complete_ratio) || 0;
task.overdue = task.total_minutes < task.total_minutes_spent;
task.time_spent = {hours: ~~(task.total_minutes_spent / 60), minutes: task.total_minutes_spent % 60};
task.time_spent = { hours: ~~(task.total_minutes_spent / 60), minutes: task.total_minutes_spent % 60 };
task.comments_count = Number(task.comments_count) ? +task.comments_count : 0;
task.attachments_count = Number(task.attachments_count) ? +task.attachments_count : 0;

File diff suppressed because it is too large Load Diff

View File

@@ -13,7 +13,7 @@ import { SocketEvents } from "../socket.io/events";
import WorklenzControllerBase from "./worklenz-controller-base";
import HandleExceptions from "../decorators/handle-exceptions";
import { formatDuration, getColor } from "../shared/utils";
import { statusExclude, TEAM_MEMBER_TREE_MAP_COLOR_ALPHA } from "../shared/constants";
import { statusExclude, TEAM_MEMBER_TREE_MAP_COLOR_ALPHA, TRIAL_MEMBER_LIMIT } from "../shared/constants";
import { checkTeamSubscriptionStatus } from "../shared/paddle-utils";
import { updateUsers } from "../shared/paddle-requests";
import { NotificationsService } from "../services/notifications/notifications.service";
@@ -141,6 +141,17 @@ export default class TeamMembersController extends WorklenzControllerBase {
return res.status(200).send(new ServerResponse(false, null, "Cannot exceed the maximum number of life time users."));
}
/**
* Checks trial user team member limit
*/
if (subscriptionData.subscription_status === "trialing") {
const currentTrialMembers = parseInt(subscriptionData.current_count) || 0;
if (currentTrialMembers + incrementBy > TRIAL_MEMBER_LIMIT) {
return res.status(200).send(new ServerResponse(false, null, `Trial users cannot exceed ${TRIAL_MEMBER_LIMIT} team members. Please upgrade to add more members.`));
}
}
/**
* Checks subscription details and updates the user count if applicable.
* Sends a response if there is an issue with the subscription.
@@ -1081,6 +1092,18 @@ export default class TeamMembersController extends WorklenzControllerBase {
return res.status(200).send(new ServerResponse(false, "Please check your subscription status."));
}
/**
* Checks trial user team member limit
*/
if (subscriptionData.subscription_status === "trialing") {
const currentTrialMembers = parseInt(subscriptionData.current_count) || 0;
const emailsToAdd = req.body.emails?.length || 1;
if (currentTrialMembers + emailsToAdd > TRIAL_MEMBER_LIMIT) {
return res.status(200).send(new ServerResponse(false, null, `Trial users cannot exceed ${TRIAL_MEMBER_LIMIT} team members. Please upgrade to add more members.`));
}
}
// if (subscriptionData.status === "trialing") break;
if (!subscriptionData.is_credit && !subscriptionData.is_custom) {
if (subscriptionData.subscription_status === "active") {

View File

@@ -0,0 +1,117 @@
import moment from "moment";
import { IWorkLenzRequest } from "../interfaces/worklenz-request";
import { IWorkLenzResponse } from "../interfaces/worklenz-response";
import db from "../config/db";
import { ServerResponse } from "../models/server-response";
import WorklenzControllerBase from "./worklenz-controller-base";
import HandleExceptions from "../decorators/handle-exceptions";
import { formatDuration, formatLogText, getColor } from "../shared/utils";
interface IUserRecentTask {
task_id: string;
task_name: string;
project_id: string;
project_name: string;
last_activity_at: string;
activity_count: number;
project_color?: string;
task_status?: string;
status_color?: string;
}
interface IUserTimeLoggedTask {
task_id: string;
task_name: string;
project_id: string;
project_name: string;
total_time_logged: number;
total_time_logged_string: string;
last_logged_at: string;
logged_by_timer: boolean;
project_color?: string;
task_status?: string;
status_color?: string;
log_entries_count?: number;
estimated_time?: number;
}
export default class UserActivityLogsController extends WorklenzControllerBase {
@HandleExceptions()
public static async getRecentTasks(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
if (!req.user) {
return res.status(401).send(new ServerResponse(false, null, "Unauthorized"));
}
const { id: userId, team_id: teamId } = req.user;
const { offset = 0, limit = 10 } = req.query;
// Optimized query with better performance and team filtering
const q = `
SELECT DISTINCT tal.task_id, t.name AS task_name, tal.project_id, p.name AS project_name,
MAX(tal.created_at) AS last_activity_at,
COUNT(DISTINCT tal.id) AS activity_count,
p.color_code AS project_color,
(SELECT name FROM task_statuses WHERE id = t.status_id) AS task_status,
(SELECT color_code
FROM sys_task_status_categories
WHERE id = (SELECT category_id FROM task_statuses WHERE id = t.status_id)) AS status_color
FROM task_activity_logs tal
INNER JOIN tasks t ON tal.task_id = t.id AND t.archived = FALSE
INNER JOIN projects p ON tal.project_id = p.id AND p.team_id = $1
WHERE tal.user_id = $2
AND tal.created_at >= NOW() - INTERVAL '30 days'
GROUP BY tal.task_id, t.name, tal.project_id, p.name, p.color_code, t.status_id
ORDER BY MAX(tal.created_at) DESC
LIMIT $3 OFFSET $4;
`;
const result = await db.query(q, [teamId, userId, limit, offset]);
const tasks: IUserRecentTask[] = result.rows;
return res.status(200).send(new ServerResponse(true, tasks));
}
@HandleExceptions()
public static async getTimeLoggedTasks(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
if (!req.user) {
return res.status(401).send(new ServerResponse(false, null, "Unauthorized"));
}
const { id: userId, team_id: teamId } = req.user;
const { offset = 0, limit = 10 } = req.query;
// Optimized query with better performance, team filtering, and useful additional data
const q = `
SELECT twl.task_id, t.name AS task_name, t.project_id, p.name AS project_name,
SUM(twl.time_spent) AS total_time_logged,
MAX(twl.created_at) AS last_logged_at,
MAX(twl.logged_by_timer::int)::boolean AS logged_by_timer,
p.color_code AS project_color,
(SELECT name FROM task_statuses WHERE id = t.status_id) AS task_status,
(SELECT color_code
FROM sys_task_status_categories
WHERE id = (SELECT category_id FROM task_statuses WHERE id = t.status_id)) AS status_color,
COUNT(DISTINCT twl.id) AS log_entries_count,
(t.total_minutes * 60) AS estimated_time
FROM task_work_log twl
INNER JOIN tasks t ON twl.task_id = t.id AND t.archived = FALSE
INNER JOIN projects p ON t.project_id = p.id AND p.team_id = $1
WHERE twl.user_id = $2
AND twl.created_at >= NOW() - INTERVAL '90 days'
GROUP BY twl.task_id, t.name, t.project_id, p.name, p.color_code, t.status_id, t.total_minutes
HAVING SUM(twl.time_spent) > 0
ORDER BY MAX(twl.created_at) DESC
LIMIT $3 OFFSET $4;
`;
const result = await db.query(q, [teamId, userId, limit, offset]);
const tasks: IUserTimeLoggedTask[] = result.rows.map(task => ({
...task,
total_time_logged_string: formatDuration(moment.duration(task.total_time_logged, "seconds")),
}));
return res.status(200).send(new ServerResponse(true, tasks));
}
}

View File

@@ -34,29 +34,24 @@ export default abstract class WorklenzControllerBase {
const offset = queryParams.search ? 0 : (index - 1) * size;
const paging = queryParams.paging || "true";
// let s = "";
// if (typeof searchField === "string") {
// s = `${searchField} || ' ' || id::TEXT`;
// } else if (Array.isArray(searchField)) {
// s = searchField.join(" || ' ' || ");
// }
// const search = (queryParams.search as string || "").trim();
// const searchQuery = search ? `AND TO_TSVECTOR(${s}) @@ TO_TSQUERY('${toTsQuery(search)}')` : "";
const search = (queryParams.search as string || "").trim();
let s = "";
if (typeof searchField === "string") {
s = ` ${searchField} ILIKE '%${search}%'`;
} else if (Array.isArray(searchField)) {
s = searchField.map(index => ` ${index} ILIKE '%${search}%'`).join(" OR ");
}
let searchQuery = "";
if (search) {
searchQuery = isMemberFilter ? ` (${s}) AND ` : ` AND (${s}) `;
// Properly escape single quotes to prevent SQL syntax errors
const escapedSearch = search.replace(/'/g, "''");
let s = "";
if (typeof searchField === "string") {
s = ` ${searchField} ILIKE '%${escapedSearch}%'`;
} else if (Array.isArray(searchField)) {
s = searchField.map(field => ` ${field} ILIKE '%${escapedSearch}%'`).join(" OR ");
}
if (s) {
searchQuery = isMemberFilter ? ` (${s}) AND ` : ` AND (${s}) `;
}
}
// Sort

View File

@@ -1,11 +1,11 @@
import {startDailyDigestJob} from "./daily-digest-job";
import {startNotificationsJob} from "./notifications-job";
import {startProjectDigestJob} from "./project-digest-job";
import { startRecurringTasksJob } from "./recurring-tasks";
import {startRecurringTasksJob} from "./recurring-tasks";
export function startCronJobs() {
startNotificationsJob();
startDailyDigestJob();
startProjectDigestJob();
// startRecurringTasksJob();
if (process.env.ENABLE_RECURRING_JOBS === "true") startRecurringTasksJob();
}

View File

@@ -7,12 +7,90 @@ import TasksController from "../controllers/tasks-controller";
// At 11:00+00 (4.30pm+530) on every day-of-month if it's on every day-of-week from Monday through Friday.
// const TIME = "0 11 */1 * 1-5";
const TIME = "*/2 * * * *";
const TIME = process.env.RECURRING_JOBS_INTERVAL || "0 11 */1 * 1-5";
const TIME_FORMAT = "YYYY-MM-DD";
// const TIME = "0 0 * * *"; // Runs at midnight every day
const log = (value: any) => console.log("recurring-task-cron-job:", value);
// Define future limits for different schedule types
// More conservative limits to prevent task list clutter
const FUTURE_LIMITS = {
daily: moment.duration(3, "days"),
weekly: moment.duration(1, "week"),
monthly: moment.duration(1, "month"),
every_x_days: (interval: number) => moment.duration(interval, "days"),
every_x_weeks: (interval: number) => moment.duration(interval, "weeks"),
every_x_months: (interval: number) => moment.duration(interval, "months")
};
// Helper function to get the future limit based on schedule type
function getFutureLimit(scheduleType: string, interval?: number): moment.Duration {
switch (scheduleType) {
case "daily":
return FUTURE_LIMITS.daily;
case "weekly":
return FUTURE_LIMITS.weekly;
case "monthly":
return FUTURE_LIMITS.monthly;
case "every_x_days":
return FUTURE_LIMITS.every_x_days(interval || 1);
case "every_x_weeks":
return FUTURE_LIMITS.every_x_weeks(interval || 1);
case "every_x_months":
return FUTURE_LIMITS.every_x_months(interval || 1);
default:
return moment.duration(3, "days"); // Default to 3 days
}
}
// Helper function to batch create tasks
async function createBatchTasks(template: ITaskTemplate & IRecurringSchedule, endDates: moment.Moment[]) {
const createdTasks = [];
for (const nextEndDate of endDates) {
const existingTaskQuery = `
SELECT id FROM tasks
WHERE schedule_id = $1 AND end_date::DATE = $2::DATE;
`;
const existingTaskResult = await db.query(existingTaskQuery, [template.schedule_id, nextEndDate.format(TIME_FORMAT)]);
if (existingTaskResult.rows.length === 0) {
const createTaskQuery = `SELECT create_quick_task($1::json) as task;`;
const taskData = {
name: template.name,
priority_id: template.priority_id,
project_id: template.project_id,
reporter_id: template.reporter_id,
status_id: template.status_id || null,
end_date: nextEndDate.format(TIME_FORMAT),
schedule_id: template.schedule_id
};
const createTaskResult = await db.query(createTaskQuery, [JSON.stringify(taskData)]);
const createdTask = createTaskResult.rows[0].task;
if (createdTask) {
createdTasks.push(createdTask);
for (const assignee of template.assignees) {
await TasksController.createTaskBulkAssignees(assignee.team_member_id, template.project_id, createdTask.id, assignee.assigned_by);
}
for (const label of template.labels) {
const q = `SELECT add_or_remove_task_label($1, $2) AS labels;`;
await db.query(q, [createdTask.id, label.label_id]);
}
console.log(`Created task for template ${template.name} with end date ${nextEndDate.format(TIME_FORMAT)}`);
}
} else {
console.log(`Skipped creating task for template ${template.name} with end date ${nextEndDate.format(TIME_FORMAT)} - task already exists`);
}
}
return createdTasks;
}
async function onRecurringTaskJobTick() {
try {
log("(cron) Recurring tasks job started.");
@@ -33,65 +111,44 @@ async function onRecurringTaskJobTick() {
? moment(template.last_task_end_date)
: moment(template.created_at);
const futureLimit = moment(template.last_checked_at || template.created_at).add(1, "week");
// Calculate future limit based on schedule type
const futureLimit = moment(template.last_checked_at || template.created_at)
.add(getFutureLimit(
template.schedule_type,
template.interval_days || template.interval_weeks || template.interval_months || 1
));
let nextEndDate = calculateNextEndDate(template, lastTaskEndDate);
const endDatesToCreate: moment.Moment[] = [];
// Find the next future occurrence
while (nextEndDate.isSameOrBefore(now)) {
// Find all future occurrences within the limit
while (nextEndDate.isSameOrBefore(futureLimit)) {
if (nextEndDate.isAfter(now)) {
endDatesToCreate.push(moment(nextEndDate));
}
nextEndDate = calculateNextEndDate(template, nextEndDate);
}
// Only create a task if it's within the future limit
if (nextEndDate.isSameOrBefore(futureLimit)) {
const existingTaskQuery = `
SELECT id FROM tasks
WHERE schedule_id = $1 AND end_date::DATE = $2::DATE;
// Batch create tasks for all future dates
if (endDatesToCreate.length > 0) {
const createdTasks = await createBatchTasks(template, endDatesToCreate);
createdTaskCount += createdTasks.length;
// Update the last_checked_at in the schedule
const updateScheduleQuery = `
UPDATE task_recurring_schedules
SET last_checked_at = $1::DATE,
last_created_task_end_date = $2
WHERE id = $3;
`;
const existingTaskResult = await db.query(existingTaskQuery, [template.schedule_id, nextEndDate.format(TIME_FORMAT)]);
if (existingTaskResult.rows.length === 0) {
const createTaskQuery = `SELECT create_quick_task($1::json) as task;`;
const taskData = {
name: template.name,
priority_id: template.priority_id,
project_id: template.project_id,
reporter_id: template.reporter_id,
status_id: template.status_id || null,
end_date: nextEndDate.format(TIME_FORMAT),
schedule_id: template.schedule_id
};
const createTaskResult = await db.query(createTaskQuery, [JSON.stringify(taskData)]);
const createdTask = createTaskResult.rows[0].task;
if (createdTask) {
createdTaskCount++;
for (const assignee of template.assignees) {
await TasksController.createTaskBulkAssignees(assignee.team_member_id, template.project_id, createdTask.id, assignee.assigned_by);
}
for (const label of template.labels) {
const q = `SELECT add_or_remove_task_label($1, $2) AS labels;`;
await db.query(q, [createdTask.id, label.label_id]);
}
console.log(`Created task for template ${template.name} with end date ${nextEndDate.format(TIME_FORMAT)}`);
}
} else {
console.log(`Skipped creating task for template ${template.name} with end date ${nextEndDate.format(TIME_FORMAT)} - task already exists`);
}
await db.query(updateScheduleQuery, [
moment().format(TIME_FORMAT),
endDatesToCreate[endDatesToCreate.length - 1].format(TIME_FORMAT),
template.schedule_id
]);
} else {
console.log(`No task created for template ${template.name} - next occurrence is beyond the future limit`);
console.log(`No tasks created for template ${template.name} - next occurrence is beyond the future limit`);
}
// Update the last_checked_at in the schedule
const updateScheduleQuery = `
UPDATE task_recurring_schedules
SET last_checked_at = $1::DATE, last_created_task_end_date = $2
WHERE id = $3;
`;
await db.query(updateScheduleQuery, [moment(template.last_checked_at || template.created_at).add(1, "day").format(TIME_FORMAT), nextEndDate.format(TIME_FORMAT), template.schedule_id]);
}
log(`(cron) Recurring tasks job ended with ${createdTaskCount} new tasks created.`);

View File

@@ -0,0 +1,219 @@
{
"_metadata": {
"description": "Sri Lankan Public Holidays Data",
"last_updated": "2025-01-31",
"sources": {
"2025": "Based on official government sources and existing verified data",
"note": "All dates should be verified against official sources before use"
},
"official_sources": [
"Central Bank of Sri Lanka - Holiday Circulars",
"Department of Meteorology - Astrological calculations",
"Ministry of Public Administration - Official gazette",
"Buddhist and Pali University - Poya day calculations",
"All Ceylon Jamiyyatul Ulama - Islamic calendar",
"Hindu Cultural Centre - Hindu calendar"
],
"verification_process": "Each year should be verified against current official publications before adding to production systems"
},
"2025": [
{
"name": "Duruthu Full Moon Poya Day",
"date": "2025-01-13",
"type": "Poya",
"description": "Commemorates the first visit of Buddha to Sri Lanka",
"is_recurring": false
},
{
"name": "Navam Full Moon Poya Day",
"date": "2025-02-12",
"type": "Poya",
"description": "Commemorates the appointment of Sariputta and Moggallana as Buddha's chief disciples",
"is_recurring": false
},
{
"name": "Independence Day",
"date": "2025-02-04",
"type": "Public",
"description": "Commemorates the independence of Sri Lanka from British rule in 1948",
"is_recurring": true
},
{
"name": "Medin Full Moon Poya Day",
"date": "2025-03-14",
"type": "Poya",
"description": "Commemorates Buddha's first visit to his father's palace after enlightenment",
"is_recurring": false
},
{
"name": "Eid al-Fitr",
"date": "2025-03-31",
"type": "Public",
"description": "Festival marking the end of Ramadan",
"is_recurring": false
},
{
"name": "Bak Full Moon Poya Day",
"date": "2025-04-12",
"type": "Poya",
"description": "Commemorates Buddha's second visit to Sri Lanka",
"is_recurring": false
},
{
"name": "Sinhala and Tamil New Year Day",
"date": "2025-04-13",
"type": "Public",
"description": "Traditional New Year celebrated by Sinhalese and Tamil communities",
"is_recurring": true
},
{
"name": "Day after Sinhala and Tamil New Year",
"date": "2025-04-14",
"type": "Public",
"description": "Second day of traditional New Year celebrations",
"is_recurring": true
},
{
"name": "Good Friday",
"date": "2025-04-18",
"type": "Public",
"description": "Christian commemoration of the crucifixion of Jesus Christ",
"is_recurring": false
},
{
"name": "May Day",
"date": "2025-05-01",
"type": "Public",
"description": "International Workers' Day",
"is_recurring": true
},
{
"name": "Vesak Full Moon Poya Day",
"date": "2025-05-12",
"type": "Poya",
"description": "Most sacred day for Buddhists - commemorates birth, enlightenment and passing of Buddha",
"is_recurring": false
},
{
"name": "Day after Vesak Full Moon Poya Day",
"date": "2025-05-13",
"type": "Public",
"description": "Additional day for Vesak celebrations",
"is_recurring": false
},
{
"name": "Eid al-Adha",
"date": "2025-06-07",
"type": "Public",
"description": "Islamic festival of sacrifice",
"is_recurring": false
},
{
"name": "Poson Full Moon Poya Day",
"date": "2025-06-11",
"type": "Poya",
"description": "Commemorates the introduction of Buddhism to Sri Lanka by Arahat Mahinda",
"is_recurring": false
},
{
"name": "Esala Full Moon Poya Day",
"date": "2025-07-10",
"type": "Poya",
"description": "Commemorates Buddha's first sermon and the arrival of the Sacred Tooth Relic",
"is_recurring": false
},
{
"name": "Nikini Full Moon Poya Day",
"date": "2025-08-09",
"type": "Poya",
"description": "Commemorates the first Buddhist council",
"is_recurring": false
},
{
"name": "Binara Full Moon Poya Day",
"date": "2025-09-07",
"type": "Poya",
"description": "Commemorates Buddha's visit to heaven to preach to his mother",
"is_recurring": false
},
{
"name": "Vap Full Moon Poya Day",
"date": "2025-10-07",
"type": "Poya",
"description": "Marks the end of Buddhist Lent and Buddha's return from heaven",
"is_recurring": false
},
{
"name": "Deepavali",
"date": "2025-10-20",
"type": "Public",
"description": "Hindu Festival of Lights",
"is_recurring": false
},
{
"name": "Il Full Moon Poya Day",
"date": "2025-11-05",
"type": "Poya",
"description": "Commemorates Buddha's ordination of sixty disciples",
"is_recurring": false
},
{
"name": "Unduvap Full Moon Poya Day",
"date": "2025-12-04",
"type": "Poya",
"description": "Commemorates the arrival of Sanghamitta Theri with the Sacred Bo sapling",
"is_recurring": false
},
{
"name": "Christmas Day",
"date": "2025-12-25",
"type": "Public",
"description": "Christian celebration of the birth of Jesus Christ",
"is_recurring": true
}
],
"fixed_holidays": [
{
"name": "Independence Day",
"month": 2,
"day": 4,
"type": "Public",
"description": "Commemorates the independence of Sri Lanka from British rule in 1948"
},
{
"name": "May Day",
"month": 5,
"day": 1,
"type": "Public",
"description": "International Workers' Day"
},
{
"name": "Christmas Day",
"month": 12,
"day": 25,
"type": "Public",
"description": "Christian celebration of the birth of Jesus Christ"
}
],
"variable_holidays_info": {
"sinhala_tamil_new_year": {
"description": "Sinhala and Tamil New Year dates vary based on astrological calculations. Common patterns:",
"common_dates": [
{ "pattern": "April 12-13", "years": "Some years" },
{ "pattern": "April 13-14", "years": "Most common" },
{ "pattern": "April 14-15", "years": "Occasional" }
],
"note": "These dates should be verified annually from official sources like the Department of Meteorology or astrological authorities"
},
"poya_days": {
"description": "Full moon Poya days follow the lunar calendar and change each year",
"note": "Dates should be obtained from Buddhist calendar or astronomical calculations"
},
"religious_holidays": {
"eid_fitr": "Based on Islamic lunar calendar - varies each year",
"eid_adha": "Based on Islamic lunar calendar - varies each year",
"good_friday": "Based on Easter calculation - varies each year",
"deepavali": "Based on Hindu lunar calendar - varies each year"
}
}
}

View File

@@ -0,0 +1,170 @@
# Sri Lankan Holiday Annual Update Process
## Overview
This document outlines the process for annually updating Sri Lankan holiday data to ensure accurate utilization calculations.
## Data Sources & Verification
### Official Government Sources
1. **Central Bank of Sri Lanka**
- Holiday circulars (usually published in December for the next year)
- Website: [cbsl.gov.lk](https://www.cbsl.gov.lk)
2. **Department of Meteorology**
- Astrological calculations for Sinhala & Tamil New Year
- Website: [meteo.gov.lk](http://www.meteo.gov.lk)
3. **Ministry of Public Administration**
- Official gazette notifications
- Public holiday declarations
### Religious Authorities
1. **Buddhist Calendar**
- Buddhist and Pali University of Sri Lanka
- Major temples (Malwatte, Asgiriya)
2. **Islamic Calendar**
- All Ceylon Jamiyyatul Ulama (ACJU)
- Colombo Grand Mosque
3. **Hindu Calendar**
- Hindu Cultural Centre
- Tamil cultural organizations
## Annual Update Workflow
### 1. Preparation (October - November)
```bash
# Check current data status
node update-sri-lankan-holidays.js --list
node update-sri-lankan-holidays.js --validate
```
### 2. Research Phase (November - December)
For the upcoming year (e.g., 2026):
1. **Fixed Holidays** ✅ Already handled
- Independence Day (Feb 4)
- May Day (May 1)
- Christmas Day (Dec 25)
2. **Variable Holidays** ⚠️ Require verification
- **Sinhala & Tamil New Year**: Check Department of Meteorology
- **Poya Days**: Check Buddhist calendar/temples
- **Good Friday**: Calculate from Easter
- **Eid al-Fitr & Eid al-Adha**: Check Islamic calendar
- **Deepavali**: Check Hindu calendar
### 3. Data Collection Template
```bash
# Generate template for the new year
node update-sri-lankan-holidays.js --poya-template 2026
```
This will output a template like:
```json
{
"name": "Duruthu Full Moon Poya Day",
"date": "2026-??-??",
"type": "Poya",
"description": "Commemorates the first visit of Buddha to Sri Lanka",
"is_recurring": false
}
```
### 4. Research Checklist
#### Sinhala & Tamil New Year
- [ ] Check Department of Meteorology announcements
- [ ] Verify with astrological authorities
- [ ] Confirm if dates are April 12-13, 13-14, or 14-15
#### Poya Days (12 per year)
- [ ] Get Buddhist calendar for the year
- [ ] Verify with temples or Buddhist authorities
- [ ] Double-check lunar calendar calculations
#### Religious Holidays
- [ ] **Good Friday**: Calculate based on Easter
- [ ] **Eid al-Fitr**: Check Islamic calendar/ACJU
- [ ] **Eid al-Adha**: Check Islamic calendar/ACJU
- [ ] **Deepavali**: Check Hindu calendar/cultural centers
### 5. Data Entry
1. Edit `src/data/sri-lankan-holidays.json`
2. Add new year section with verified dates
3. Update metadata with sources used
### 6. Validation & Testing
```bash
# Validate the new data
node update-sri-lankan-holidays.js --validate
# Generate SQL for database
node update-sri-lankan-holidays.js --generate-sql 2026
```
### 7. Database Update
1. Create new migration file with the generated SQL
2. Test in development environment
3. Deploy to production
### 8. Documentation
- Update metadata in JSON file
- Document sources used
- Note any special circumstances or date changes
## Emergency Updates
If holidays are announced late or changed:
1. **Quick JSON Update**:
```bash
# Edit the JSON file directly
# Add the new/changed holiday
```
2. **Database Hotfix**:
```sql
INSERT INTO country_holidays (country_code, name, description, date, is_recurring)
VALUES ('LK', 'Emergency Holiday', 'Description', 'YYYY-MM-DD', false)
ON CONFLICT (country_code, name, date) DO NOTHING;
```
3. **Notify Users**: Consider adding a notification system for holiday changes
## Quality Assurance
### Pre-Release Checklist
- [ ] All 12 Poya days included for the year
- [ ] Sinhala & Tamil New Year dates verified
- [ ] Religious holidays cross-checked with multiple sources
- [ ] No duplicate dates
- [ ] JSON format validation passes
- [ ] Database migration tested
### Post-Release Monitoring
- [ ] Monitor utilization calculations for anomalies
- [ ] Check user feedback for missed holidays
- [ ] Verify against actual government announcements
## Automation Opportunities
Future improvements could include:
1. **API Integration**: Connect to reliable holiday APIs
2. **Web Scraping**: Automated monitoring of official websites
3. **Notification System**: Alert when new holidays are announced
4. **Validation Service**: Cross-check against multiple sources
## Contact Information
For questions about the holiday update process:
- Technical issues: Development team
- Holiday verification: Sri Lankan team members
- Religious holidays: Local community contacts
## Version History
- **v1.0** (2025-01-31): Initial process documentation
- **2025 Data**: Verified and included
- **2026+ Data**: Pending official source verification

View File

@@ -0,0 +1,54 @@
export interface IHolidayType {
id: string;
name: string;
description?: string;
color_code: string;
created_at: string;
updated_at: string;
}
export interface IOrganizationHoliday {
id: string;
organization_id: string;
holiday_type_id: string;
name: string;
description?: string;
date: string;
is_recurring: boolean;
created_at: string;
updated_at: string;
holiday_type?: IHolidayType;
}
export interface ICountryHoliday {
id: string;
country_code: string;
name: string;
description?: string;
date: string;
is_recurring: boolean;
created_at: string;
updated_at: string;
}
export interface ICreateHolidayRequest {
name: string;
description?: string;
date: string;
holiday_type_id: string;
is_recurring?: boolean;
}
export interface IUpdateHolidayRequest {
id: string;
name?: string;
description?: string;
date?: string;
holiday_type_id?: string;
is_recurring?: boolean;
}
export interface IImportCountryHolidaysRequest {
country_code: string;
year?: number;
}

View File

@@ -0,0 +1,37 @@
export interface ISurveyQuestion {
id: string;
survey_id: string;
question_key: string;
question_type: 'single_choice' | 'multiple_choice' | 'text';
is_required: boolean;
sort_order: number;
options?: string[];
}
export interface ISurvey {
id: string;
name: string;
description?: string;
survey_type: 'account_setup' | 'onboarding' | 'feedback';
is_active: boolean;
questions?: ISurveyQuestion[];
}
export interface ISurveyAnswer {
question_id: string;
answer_text?: string;
answer_json?: string[];
}
export interface ISurveyResponse {
id?: string;
survey_id: string;
user_id?: string;
is_completed: boolean;
answers: ISurveyAnswer[];
}
export interface ISurveySubmissionRequest {
survey_id: string;
answers: ISurveyAnswer[];
}

View File

@@ -0,0 +1,53 @@
import { NextFunction } from "express";
import { IWorkLenzRequest } from "../../interfaces/worklenz-request";
import { IWorkLenzResponse } from "../../interfaces/worklenz-response";
import { ServerResponse } from "../../models/server-response";
import { ISurveySubmissionRequest } from "../../interfaces/survey";
export default function surveySubmissionValidator(req: IWorkLenzRequest, res: IWorkLenzResponse, next: NextFunction): IWorkLenzResponse | void {
const body = req.body as ISurveySubmissionRequest;
if (!body) {
return res.status(200).send(new ServerResponse(false, null, "Request body is required"));
}
if (!body.survey_id || typeof body.survey_id !== 'string') {
return res.status(200).send(new ServerResponse(false, null, "Survey ID is required and must be a string"));
}
if (!body.answers || !Array.isArray(body.answers)) {
return res.status(200).send(new ServerResponse(false, null, "Answers are required and must be an array"));
}
// Validate each answer
for (let i = 0; i < body.answers.length; i++) {
const answer = body.answers[i];
if (!answer.question_id || typeof answer.question_id !== 'string') {
return res.status(200).send(new ServerResponse(false, null, `Answer ${i + 1}: Question ID is required and must be a string`));
}
// answer_text and answer_json are both optional - users can submit empty answers
// Validate answer_text if provided
if (answer.answer_text && typeof answer.answer_text !== 'string') {
return res.status(200).send(new ServerResponse(false, null, `Answer ${i + 1}: answer_text must be a string`));
}
// Validate answer_json if provided
if (answer.answer_json && !Array.isArray(answer.answer_json)) {
return res.status(200).send(new ServerResponse(false, null, `Answer ${i + 1}: answer_json must be an array`));
}
// Validate answer_json items are strings
if (answer.answer_json) {
for (let j = 0; j < answer.answer_json.length; j++) {
if (typeof answer.answer_json[j] !== 'string') {
return res.status(200).send(new ServerResponse(false, null, `Answer ${i + 1}: answer_json items must be strings`));
}
}
}
}
return next();
}

View File

@@ -3,13 +3,16 @@ import { Strategy as LocalStrategy } from "passport-local";
import { log_error } from "../../shared/utils";
import db from "../../config/db";
import { Request } from "express";
import { ERROR_KEY, SUCCESS_KEY } from "./passport-constants";
async function handleLogin(req: Request, email: string, password: string, done: any) {
console.log("Login attempt for:", email);
// Clear any existing flash messages
(req.session as any).flash = {};
if (!email || !password) {
console.log("Missing credentials");
return done(null, false, { message: "Please enter both email and password" });
const errorMsg = "Please enter both email and password";
req.flash(ERROR_KEY, errorMsg);
return done(null, false);
}
try {
@@ -19,23 +22,27 @@ async function handleLogin(req: Request, email: string, password: string, done:
AND google_id IS NULL
AND is_deleted IS FALSE;`;
const result = await db.query(q, [email]);
console.log("User query result count:", result.rowCount);
const [data] = result.rows;
if (!data?.password) {
console.log("No account found");
return done(null, false, { message: "No account found with this email" });
const errorMsg = "No account found with this email";
req.flash(ERROR_KEY, errorMsg);
return done(null, false);
}
const passwordMatch = bcrypt.compareSync(password, data.password);
console.log("Password match:", passwordMatch);
if (passwordMatch && email === data.email) {
delete data.password;
return done(null, data, {message: "User successfully logged in"});
const successMsg = "User successfully logged in";
req.flash(SUCCESS_KEY, successMsg);
return done(null, data);
}
return done(null, false, { message: "Incorrect email or password" });
const errorMsg = "Incorrect email or password";
req.flash(ERROR_KEY, errorMsg);
return done(null, false);
} catch (error) {
console.error("Login error:", error);
log_error(error, req.body);

View File

@@ -0,0 +1,4 @@
{
"doesNotExistText": "Na vjen keq, faqja që kërkoni nuk ekziston.",
"backHomeButton": "Kthehu në Faqen Kryesore"
}

View File

@@ -0,0 +1,31 @@
{
"continue": "Vazhdo",
"setupYourAccount": "Konfiguro Llogarinë Tënde në Worklenz.",
"organizationStepTitle": "Emërtoni Organizatën Tuaj",
"organizationStepLabel": "Zgjidhni një emër për llogarinë tuaj në Worklenz.",
"projectStepTitle": "Krijoni projektin tuaj të parë",
"projectStepLabel": "Në cilin projekt po punoni aktualisht?",
"projectStepPlaceholder": "p.sh. Plani i Marketingut",
"tasksStepTitle": "Krijoni detyrat tuaja të para",
"tasksStepLabel": "Shkruani disa detyra që do të kryeni në",
"tasksStepAddAnother": "Shto një tjetër",
"emailPlaceholder": "Adresa email",
"invalidEmail": "Ju lutemi vendosni një adresë email të vlefshme",
"or": "ose",
"templateButton": "Importo nga shablloni",
"goBack": "Kthehu Mbrapa",
"cancel": "Anulo",
"create": "Krijo",
"templateDrawerTitle": "Zgjidh nga shabllonet",
"step3InputLabel": "Fto me email",
"addAnother": "Shto një tjetër",
"skipForNow": "Kalo tani për tani",
"formTitle": "Krijoni detyrën tuaj të parë.",
"step3Title": "Fto ekipin tënd të punojë me",
"maxMembers": " (Mund të ftoni deri në 5 anëtarë)",
"maxTasks": " (Mund të krijoni deri në 5 detyra)"
}

View File

@@ -0,0 +1,113 @@
{
"title": "Faturimet",
"currentBill": "Fatura Aktuale",
"configuration": "Konfigurimi",
"currentPlanDetails": "Detajet e Planit Aktual",
"upgradePlan": "Përmirëso Planin",
"cardBodyText01": "Provë falas",
"cardBodyText02": "(Plani juaj i provës skadon në 1 muaj 19 ditë)",
"redeemCode": "Kodi i Zbritjes",
"accountStorage": "Depozita e Llogarisë",
"used": "Përdorur:",
"remaining": "E mbetur:",
"charges": "Tarifat",
"tooltip": "Tarifat për ciklin aktual të faturimit",
"description": "Përshkrimi",
"billingPeriod": "Periudha e Faturimit",
"billStatus": "Statusi i Faturës",
"perUserValue": "Vlera për Përdorues",
"users": "Përdoruesit",
"amount": "Shuma",
"invoices": "Faturat",
"transactionId": "ID e Transaksionit",
"transactionDate": "Data e Transaksionit",
"paymentMethod": "Metoda e Pagesës",
"status": "Statusi",
"ltdUsers": "Mund të shtoni deri në {{ltd_users}} përdorues.",
"totalSeats": "Vende totale",
"availableSeats": "Vende të disponueshme",
"addMoreSeats": "Shto më shumë vende",
"drawerTitle": "Kodi i Zbritjes",
"label": "Kodi i Zbritjes",
"drawerPlaceholder": "Vendosni kodin tuaj të zbritjes",
"redeemSubmit": "Paraqit",
"modalTitle": "Zgjidhni planin më të mirë për ekipin tuaj",
"seatLabel": "Numri i vendeve",
"freePlan": "Plan Falas",
"startup": "Startup",
"business": "Biznes",
"tag": "Më i Popullarizuar",
"enterprise": "Ndërmarrje",
"freeSubtitle": "falas përgjithmonë",
"freeUsers": "Më e mira për përdorim personal",
"freeText01": "100MB depozitë",
"freeText02": "3 projekte",
"freeText03": "5 anëtarë të ekipit",
"startupSubtitle": "ÇMIM I RASTËSISHËM / muaj",
"startupUsers": "Deri në 15 përdorues",
"startupText01": "25GB depozitë",
"startupText02": "Projekte të pakufizuara aktive",
"startupText03": "Orar",
"startupText04": "Raportim",
"startupText05": "Abonohu në projekte",
"businessSubtitle": "përdorues / muaj",
"businessUsers": "16 - 200 përdorues",
"enterpriseUsers": "200 - 500+ përdorues",
"footerTitle": "Ju lutemi na jepni një numër kontakti që mund të përdorim për t'ju kontaktuar.",
"footerLabel": "Numri i Kontaktit",
"footerButton": "Na kontaktoni",
"redeemCodePlaceHolder": "Vendosni kodin tuaj të zbritjes",
"submit": "Paraqit",
"trialPlan": "Provë Falas",
"trialExpireDate": "E vlefshme deri më {{trial_expire_date}}",
"trialExpired": "Provat tuaja falas skaduan {{trial_expire_string}}",
"trialInProgress": "Provat tuaja falas skadojnë {{trial_expire_string}}",
"required": "Kjo fushë është e detyrueshme",
"invalidCode": "Kod i pavlefshëm",
"selectPlan": "Zgjidhni planin më të mirë për ekipin tuaj",
"changeSubscriptionPlan": "Ndryshoni planin tuaj të abonimit",
"noOfSeats": "Numri i vendeve",
"annualPlan": "Pro - Vjetor",
"monthlyPlan": "Pro - Mujor",
"freeForever": "Falas Përgjithmonë",
"bestForPersonalUse": "Më e mira për përdorim personal",
"storage": "Depozitë",
"projects": "Projekte",
"teamMembers": "Anëtarët e Ekipit",
"unlimitedTeamMembers": "Anëtarë të pakufizuar të ekipit",
"unlimitedActiveProjects": "Projekte të pakufizuara aktive",
"schedule": "Orar",
"reporting": "Raportim",
"subscribeToProjects": "Abonohu në projekte",
"billedAnnually": "Faturuar çdo vit",
"billedMonthly": "Faturuar çdo muaj",
"pausePlan": "Pauzë Planin",
"resumePlan": "Rifillo Planin",
"changePlan": "Ndrysho Planin",
"cancelPlan": "Anulo Planin",
"perMonthPerUser": "për përdorues/muaj",
"viewInvoice": "Shiko Faturën",
"switchToFreePlan": "Kalo në Planin Falas",
"expirestoday": "sot",
"expirestomorrow": "nesër",
"expiredDaysAgo": "{{days}} ditë më parë",
"continueWith": "Vazhdo me {{plan}}",
"changeToPlan": "Ndrysho në {{plan}}"
}

View File

@@ -0,0 +1,8 @@
{
"overview": "Përmbledhje",
"name": "Emri i Organizatës",
"owner": "Pronari i Organizatës",
"admins": "Administruesit e Organizatës",
"contactNumber": "Shto Numrin e Kontaktit",
"edit": "Redakto"
}

View File

@@ -0,0 +1,12 @@
{
"membersCount": "Numri i Anëtarëve",
"createdAt": "Krijuar më",
"projectName": "Emri i Projektit",
"teamName": "Emri i Ekipit",
"refreshProjects": "Rifresko Projektet",
"searchPlaceholder": "Kërkoni sipas emrit të projektit",
"deleteProject": "Jeni i sigurt që dëshironi të fshini këtë projekt?",
"confirm": "Konfirmo",
"cancel": "Anulo",
"delete": "Fshi Projektin"
}

View File

@@ -0,0 +1,8 @@
{
"overview": "Përmbledhje",
"users": "Përdoruesit",
"teams": "Ekipet",
"billing": "Faturimi",
"projects": "Projektet",
"adminCenter": "Qendra Administrative"
}

View File

@@ -0,0 +1,33 @@
{
"title": "Ekipet",
"subtitle": "ekipet",
"tooltip": "Rifresko ekipet",
"placeholder": "Kërko sipas emrit",
"addTeam": "Shto Ekip",
"team": "Ekipi",
"membersCount": "Numri i Anëtarëve",
"members": "Anëtarët",
"drawerTitle": "Krijo Ekip të Ri",
"label": "Emri i Ekipit",
"drawerPlaceholder": "Emri",
"create": "Krijo",
"delete": "Fshi",
"settings": "Cilësimet",
"popTitle": "Jeni i sigurt?",
"message": "Ju lutemi shkruani një Emër",
"teamSettings": "Cilësimet e Ekipit",
"teamName": "Emri i Ekipit",
"teamDescription": "Përshkrimi i Ekipit",
"teamMembers": "Anëtarët e Ekipit",
"teamMembersCount": "Numri i Anëtarëve të Ekipit",
"teamMembersPlaceholder": "Kërko sipas emrit",
"addMember": "Shto Anëtar",
"add": "Shto",
"update": "Përditëso",
"teamNamePlaceholder": "Emri i ekipit",
"user": "Përdoruesi",
"role": "Roli",
"owner": "Pronari",
"admin": "Administruesi",
"member": "Anëtari"
}

View File

@@ -0,0 +1,9 @@
{
"title": "Përdoruesit",
"subTitle": "përdoruesit",
"placeholder": "Kërko sipas emrit",
"user": "Përdoruesi",
"email": "Email",
"lastActivity": "Aktiviteti i Fundit",
"refresh": "Rifresko përdoruesit"
}

View File

@@ -0,0 +1,34 @@
{
"name": "Emri",
"client": "Klienti",
"category": "Kategoria",
"status": "Statusi",
"tasksProgress": "Përparimi i Detyrave",
"updated_at": "E Përditësuar së Fundi",
"members": "Anëtarët",
"setting": "Cilësimet",
"projects": "Projektet",
"refreshProjects": "Rifresko projektet",
"all": "Të gjitha",
"favorites": "Të preferuarit",
"archived": "E arkivuar",
"placeholder": "Kërko sipas emrit",
"archive": "Arkivo",
"unarchive": "Çarkivo",
"archiveConfirm": "Jeni i sigurt që dëshironi të arkivoni këtë projekt?",
"unarchiveConfirm": "Jeni i sigurt që dëshironi të çarkivoni këtë projekt?",
"yes": "Po",
"no": "Jo",
"clickToFilter": "Kliko për të filtruar sipas",
"noProjects": "Nuk u gjetën projekte",
"addToFavourites": "Shto te të preferuarit",
"list": "Lista",
"group": "Grupi",
"listView": "Pamja e Listës",
"groupView": "Pamja e Grupit",
"groupBy": {
"category": "Kategoria",
"client": "Klienti"
},
"noPermission": "Nuk keni leje për të kryer këtë veprim"
}

View File

@@ -0,0 +1,5 @@
{
"loggingOut": "Po dilni...",
"authenticating": "Po autentikoheni...",
"gettingThingsReady": "Po përgatiten gjërat për ju..."
}

View File

@@ -0,0 +1,12 @@
{
"headerDescription": "Rivendosni fjalëkalimin tuaj",
"emailLabel": "Email",
"emailPlaceholder": "Vendosni email-in tuaj",
"emailRequired": "Ju lutemi vendosni Email-in tuaj!",
"resetPasswordButton": "Rivendos Fjalëkalimin",
"returnToLoginButton": "Kthehu te Hyrja",
"passwordResetSuccessMessage": "Një lidhje për rivendosjen e fjalëkalimit është dërguar në email-in tuaj.",
"orText": "OSE",
"successTitle": "U dërguan udhëzimet për rivendosje!",
"successMessage": "Informacioni për rivendosje është dërguar në email-in tuaj. Ju lutemi kontrolloni email-in."
}

View File

@@ -0,0 +1,27 @@
{
"headerDescription": "Hyni në llogarinë tuaj",
"emailLabel": "Email",
"emailPlaceholder": "Vendosni email-in tuaj",
"emailRequired": "Ju lutemi vendosni Email-in tuaj!",
"passwordLabel": "Fjalëkalimi",
"passwordPlaceholder": "Vendosni fjalëkalimin",
"passwordRequired": "Ju lutemi vendosni Fjalëkalimin!",
"rememberMe": "Më mbaj mend",
"loginButton": "Hyr",
"signupButton": "Regjistrohu",
"forgotPasswordButton": "Keni harruar fjalëkalimin?",
"signInWithGoogleButton": "Hyr me Google",
"dontHaveAccountText": "Nuk keni llogari?",
"orText": "OSE",
"successMessage": "Jeni futur me sukses!",
"loginError": "Hyrja dështoi",
"googleLoginError": "Hyrja përmes Google dështoi",
"validationMessages": {
"email": "Ju lutemi vendosni një adresë email të vlefshme",
"password": "Fjalëkalimi duhet të jetë së paku 8 karaktere"
},
"errorMessages": {
"loginErrorTitle": "Hyrja dështoi",
"loginErrorMessage": "Ju lutemi kontrolloni email-in dhe fjalëkalimin dhe provoni përsëri"
}
}

View File

@@ -0,0 +1,29 @@
{
"headerDescription": "Regjistrohuni për të filluar",
"nameLabel": "Emri i Plotë",
"namePlaceholder": "Shkruani emrin tuaj të plotë",
"nameRequired": "Ju lutemi shkruani emrin tuaj të plotë!",
"nameMinCharacterRequired": "Emri duhet të jetë së paku 4 karaktere!",
"emailLabel": "Email",
"emailPlaceholder": "Shkruani email-in tuaj",
"emailRequired": "Ju lutemi shkruani Email-in tuaj!",
"passwordLabel": "Fjalëkalimi",
"passwordPlaceholder": "Krijoni një fjalëkalim",
"passwordRequired": "Ju lutemi krijoni një Fjalëkalim!",
"passwordMinCharacterRequired": "Fjalëkalimi duhet të jetë së paku 8 karaktere!",
"passwordPatternRequired": "Fjalëkalimi nuk plotëson kërkesat!",
"strongPasswordPlaceholder": "Vendosni një fjalëkalim më të fortë",
"passwordValidationAltText": "Fjalëkalimi duhet të përmbajë së paku 8 karaktere me shkronja të mëdha dhe të vogla, një numër dhe një simbol.",
"signupSuccessMessage": "Jeni regjistruar me sukses!",
"privacyPolicyLink": "Politika e Privatësisë",
"termsOfUseLink": "Kushtet e Përdorimit",
"bySigningUpText": "Duke u regjistruar, ju pranoni",
"andText": "dhe",
"signupButton": "Regjistrohu",
"signInWithGoogleButton": "Hyr me Google",
"alreadyHaveAccountText": "Keni tashmë një llogari?",
"loginButton": "Hyr",
"orText": "OSE",
"reCAPTCHAVerificationError": "Gabim në Verifikimin e reCAPTCHA",
"reCAPTCHAVerificationErrorMessage": "Nuk mundëm të verifikojmë reCAPTCHA-n tuaj. Ju lutemi provoni përsëri."
}

View File

@@ -0,0 +1,14 @@
{
"title": "Verifikoni Email-in për Rivendosje",
"description": "Vendosni fjalëkalimin tuaj të ri",
"placeholder": "Vendosni fjalëkalimin tuaj të ri",
"confirmPasswordPlaceholder": "Konfirmoni fjalëkalimin e ri",
"passwordHint": "Të paktën 8 karaktere, me shkronja të mëdha dhe të vogla, një numër dhe një simbol.",
"resetPasswordButton": "Rivendos fjalëkalimin",
"orText": "Ose",
"resendResetEmail": "Dërgo përsëri email-in e rivendosjes",
"passwordRequired": "Ju lutemi vendosni fjalëkalimin e ri",
"returnToLoginButton": "Kthehu te Hyrja",
"confirmPasswordRequired": "Ju lutemi konfirmoni fjalëkalimin e ri",
"passwordMismatch": "Fjalëkalimet nuk përputhen"
}

View File

@@ -0,0 +1,9 @@
{
"login-success": "Hyrja u krye me sukses!",
"login-failed": "Hyrja dështoi. Ju lutemi kontrolloni kredencialet dhe provoni përsëri.",
"signup-success": "Regjistrimi u krye me sukses! Mirë se erdhët.",
"signup-failed": "Regjistrimi dështoi. Ju lutemi sigurohuni që të gjitha fushat e nevojshme janë plotësuar dhe provoni përsëri.",
"reconnecting": "Jeni shkëputur nga serveri.",
"connection-lost": "Lidhja me serverin dështoi. Ju lutemi kontrolloni lidhjen tuaj me internet.",
"connection-restored": "U lidhët me serverin me sukses"
}

View File

@@ -0,0 +1,13 @@
{
"formTitle": "Krijoni projektin tuaj të parë",
"inputLabel": "Në cilin projekt po punoni aktualisht?",
"or": "ose",
"templateButton": "Importo nga shablloni",
"createFromTemplate": "Krijo nga shablloni",
"goBack": "Kthehu Mbrapa",
"continue": "Vazhdo",
"cancel": "Anulo",
"create": "Krijo",
"templateDrawerTitle": "Zgjidh nga shabllonet",
"createProject": "Krijo Projekt"
}

View File

@@ -0,0 +1,7 @@
{
"formTitle": "Krijo detyrën tënde të parë.",
"inputLabel": "Shkruaj disa detyra që do të kryesh në",
"addAnother": "Shto një tjetër",
"goBack": "Kthehu mbrapa",
"continue": "Vazhdo"
}

View File

@@ -0,0 +1,46 @@
{
"todoList": {
"title": "Lista e Detyrave",
"refreshTasks": "Rifresko detyrat",
"addTask": "+ Shto Detyrë",
"noTasks": "Asnjë detyrë",
"pressEnter": "Shtyp",
"toCreate": "për të krijuar.",
"markAsDone": "Shëno si të përfunduar"
},
"projects": {
"title": "Projektet",
"refreshProjects": "Rifresko projektet",
"noRecentProjects": "Aktualisht nuk jeni caktuar në asnjë projekt.",
"noFavouriteProjects": "Asnjë projekt i shënuar si i preferuar.",
"recent": "Të Fundit",
"favourites": "Të Preferuarat"
},
"tasks": {
"assignedToMe": "Më janë caktuar",
"assignedByMe": "I kam caktuar",
"all": "Të Gjitha",
"today": "Sot",
"upcoming": "Ardhj",
"overdue": "Të vonuara",
"noDueDate": "Pa afat",
"noTasks": "Asnjë detyrë për të shfaqur.",
"addTask": "+ Shto detyrë",
"name": "Emri",
"project": "Projekti",
"status": "Statusi",
"dueDate": "Afati",
"dueDatePlaceholder": "Cakto Afatin",
"tomorrow": "Nesër",
"nextWeek": "Javën e Ardhshme",
"nextMonth": "Muajin e Ardhshëm",
"projectRequired": "Ju lutemi zgjidhni një projekt",
"pressTabToSelectDueDateAndProject": "Shtyp Tab për të zgjedhur afatin dhe projektin",
"dueOn": "Detyrat me afat më",
"taskRequired": "Ju lutemi shtoni një detyrë",
"list": "Listë",
"calendar": "Kalendar",
"tasks": "Detyrat",
"refresh": "Rifresko"
}
}

View File

@@ -0,0 +1,8 @@
{
"formTitle": "Fto ekipin tënd të punojë me",
"inputLabel": "Fto me email",
"addAnother": "Shto një tjetër",
"goBack": "Kthehu mbrapa",
"continue": "Vazhdo",
"skipForNow": "Anashkalo tani për tani"
}

View File

@@ -0,0 +1,30 @@
{
"rename": "Riemërto",
"delete": "Fshi",
"addTask": "Shto Detyrë",
"addSectionButton": "Shto Seksion",
"changeCategory": "Ndrysho kategorinë",
"deleteTooltip": "Fshi",
"deleteConfirmationTitle": "Jeni i sigurt?",
"deleteConfirmationOk": "Po",
"deleteConfirmationCancel": "Anulo",
"dueDate": "Data e përfundimit",
"cancel": "Anulo",
"today": "Sot",
"tomorrow": "Nesër",
"assignToMe": "Cakto mua",
"archive": "Arkivo",
"newTaskNamePlaceholder": "Shkruaj emrin e detyrës",
"newSubtaskNamePlaceholder": "Shkruaj emrin e nëndetyrës",
"untitledSection": "Seksion pa titull",
"unmapped": "Pa hartë",
"clickToChangeDate": "Klikoni për të ndryshuar datën",
"noDueDate": "Pa datë përfundimi",
"save": "Ruaj",
"clear": "Pastro",
"nextWeek": "Javën e ardhshme"
}

View File

@@ -0,0 +1,6 @@
{
"title": "Prova juaj e Worklenz ka skaduar!",
"subtitle": "Ju lutemi përmirësoni tani.",
"button": "Përmirëso tani",
"checking": "Po kontrollohet statusi i abonimit..."
}

View File

@@ -0,0 +1,31 @@
{
"logoAlt": "Logoja e Worklenz",
"home": "Kryefaqja",
"projects": "Projektet",
"schedule": "Orari",
"reporting": "Raportimi",
"clients": "Klientët",
"teams": "Ekipet",
"labels": "Etiketa",
"jobTitles": "Tituj Pune",
"upgradePlan": "Përmirëso Abonimin",
"upgradePlanTooltip": "Përmirëso abonimin",
"invite": "Fto",
"inviteTooltip": "Fto anëtarë të ekipit të bashkohen",
"switchTeamTooltip": "Ndrysho ekipin",
"help": "Ndihmë",
"notificationTooltip": "Shiko njoftimet",
"profileTooltip": "Shiko profilin",
"adminCenter": "Qendra Administrative",
"settings": "Cilësimet",
"logOut": "Dil",
"notificationsDrawer": {
"read": "Lexuara e njoftimet ",
"unread": "Njoftimet e palexuara",
"markAsRead": "Shëno si të lexuara",
"readAndJoin": "Lexo & Bashkohu",
"accept": "Prano",
"acceptAndJoin": "Prano & Bashkohu",
"noNotifications": "Asnjë njoftim"
}
}

View File

@@ -0,0 +1,5 @@
{
"nameYourOrganization": "Emërtoni organizatën tuaj.",
"worklenzAccountTitle": "Zgjidhni një emër për llogarinë tuaj në Worklenz.",
"continue": "Vazhdo"
}

View File

@@ -0,0 +1,19 @@
{
"configurePhases": "Konfiguro Fazat",
"phaseLabel": "Etiketa e Fazës",
"enterPhaseName": "Vendosni një emër për etiketën e fazës",
"addOption": "Shto Opsion",
"phaseOptions": "Opsionet e Fazës:",
"dragToReorderPhases": "Zvarrit fazat për t'i rirenditur. Çdo fazë mund të ketë një ngjyrë të ndryshme.",
"enterNewPhaseName": "Shkruani emrin e fazës së re...",
"addPhase": "Shto Fazë",
"noPhasesFound": "Nuk u gjetën faza. Krijoni fazën tuaj të parë më sipër.",
"deletePhase": "Fshi Fazën",
"deletePhaseConfirm": "Jeni të sigurt që doni të fshini këtë fazë? Ky veprim nuk mund të zhbëhet.",
"rename": "Riemëro",
"delete": "Fshi",
"enterPhaseName": "Shkruani emrin e fazës",
"selectColor": "Zgjidh ngjyrën",
"managePhases": "Menaxho Fazat",
"close": "Mbyll"
}

View File

@@ -0,0 +1,42 @@
{
"createProject": "Krijo Projekt",
"editProject": "Modifiko Projektin",
"enterCategoryName": "Vendosni emër për kategorinë",
"hitEnterToCreate": "Shtyp Enter për të krijuar!",
"enterNotes": "Shënime",
"youCanManageClientsUnderSettings": "Mund të menaxhoni klientët nën Cilësimet",
"addCategory": "Shto kategori projektit",
"newCategory": "Kategori e Re",
"notes": "Shënime",
"startDate": "Data e Fillimit",
"endDate": "Data e Përfundimit",
"estimateWorkingDays": "Vlerëso ditët e punës",
"estimateManDays": "Vlerëso ditët e punëtorëve",
"hoursPerDay": "Orë në ditë",
"create": "Krijo",
"update": "Përditëso",
"delete": "Fshi",
"typeToSearchClients": "Shkruani për të kërkuar klientë",
"projectColor": "Ngjyra e Projektit",
"pleaseEnterAName": "Ju lutemi vendosni një emër",
"enterProjectName": "Vendosni emrin e projektit",
"name": "Emri",
"status": "Statusi",
"health": "Gjendja",
"category": "Kategoria",
"projectManager": "Menaxheri i Projektit",
"client": "Klienti",
"deleteConfirmation": "Jeni i sigurt që doni të fshini?",
"deleteConfirmationDescription": "Kjo do të fshijë të gjitha të dhënat e lidhura dhe nuk mund të zhbëhet.",
"yes": "Po",
"no": "Jo",
"createdAt": "Krijuar më",
"updatedAt": "Përditësuar më",
"by": "nga",
"add": "Shto",
"asClient": "si klient",
"createClient": "Krijo klient",
"searchInputPlaceholder": "Kërko sipas emrit ose emailit",
"hoursPerDayValidationMessage": "Orët në ditë duhet të jenë një numër midis 1 dhe 24",
"noPermission": "Nuk ka leje"
}

View File

@@ -0,0 +1,14 @@
{
"nameColumn": "Emri",
"attachedTaskColumn": "Detyra e Bashkangjitur",
"sizeColumn": "Madhësia",
"uploadedByColumn": "Ngarkuar Nga",
"uploadedAtColumn": "Ngarkuar Më",
"fileIconAlt": "Ikona e skedarit",
"titleDescriptionText": "Të gjitha bashkëngjitjet e detyrave në këtë projekt do të shfahen këtu.",
"deleteConfirmationTitle": "Jeni i sigurt?",
"deleteConfirmationOk": "Po",
"deleteConfirmationCancel": "Anulo",
"segmentedTooltip": "Së shpejti! Kaloni midis pamjes listë dhe pamjes miniaturash.",
"emptyText": "Nuk ka bashkëngjitje në projekt."
}

View File

@@ -0,0 +1,41 @@
{
"overview": {
"title": "Përmbledhje",
"statusOverview": "Përmbledhje Statusi",
"priorityOverview": "Përmbledhje Prioriteti",
"lastUpdatedTasks": "Detyrat e Përditësuara Së Fundi"
},
"members": {
"title": "Anëtarët",
"tooltip": "Anëtarët",
"tasksByMembers": "Detyrat sipas anëtarëve",
"tasksByMembersTooltip": "Detyrat sipas anëtarëve",
"name": "Emri",
"taskCount": "Numri i Detyrave",
"contribution": "Kontributi",
"completed": "Të Përfunduara",
"incomplete": "Të Papërfunduara",
"overdue": "Të Vonuara",
"progress": "Progresi"
},
"tasks": {
"overdueTasks": "Detyrat e Vonuara",
"overLoggedTasks": "Detyrat me regjistrim të tepërt",
"tasksCompletedEarly": "Detyrat e përfunduara para afatit",
"tasksCompletedLate": "Detyrat e përfunduara pas afatit",
"overLoggedTasksTooltip": "Detyrat me kohë të regjistruar mbi kohën e vlerësuar",
"overdueTasksTooltip": "Detyrat që kanë kaluar afatin e tyre"
},
"common": {
"seeAll": "Shiko të gjitha",
"totalLoggedHours": "Orët totale të regjistruara",
"totalEstimation": "Vlerësimi total",
"completedTasks": "Detyrat e përfunduara",
"incompleteTasks": "Detyrat e papërfunduara",
"overdueTasks": "Detyrat e vonuara",
"overdueTasksTooltip": "Detyrat që kanë kaluar afatin e tyre",
"totalLoggedHoursTooltip": "Vlerësimi dhe koha e regjistruar për detyrat.",
"includeArchivedTasks": "Përfshi Detyrat e Arkivuara",
"export": "Eksporto"
}
}

Some files were not shown because too many files have changed in this diff Show More