Compare commits

..

151 Commits

Author SHA1 Message Date
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
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
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
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
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
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
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
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
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
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
Dev-Tanaay
fbbd820512 Updation in Documentation 2025-07-01 17:29:02 +05:30
1068 changed files with 36942 additions and 7869 deletions

View File

@@ -0,0 +1,13 @@
{
"permissions": {
"allow": [
"Bash(find:*)",
"Bash(npm run build:*)",
"Bash(npm run type-check:*)",
"Bash(npm run:*)",
"Bash(grep:*)",
"Bash(rm:*)"
],
"deny": []
}
}

View File

@@ -0,0 +1,237 @@
---
alwaysApply: true
---
# Ant Design Import Rules for Worklenz
## 🚨 CRITICAL: Always Use Centralized Imports
**NEVER import Ant Design components directly from 'antd' or '@ant-design/icons'**
### ✅ Correct Import Pattern
```typescript
import { Button, Input, Select, EditOutlined, PlusOutlined } from '@antd-imports';
// or
import { Button, Input, Select, EditOutlined, PlusOutlined } from '@/shared/antd-imports';
```
### ❌ Forbidden Import Patterns
```typescript
// NEVER do this:
import { Button, Input, Select } from 'antd';
import { EditOutlined, PlusOutlined } from '@ant-design/icons';
```
## Why This Rule Exists
### Benefits of Centralized Imports:
- **Better Tree-Shaking**: Optimized bundle size through centralized management
- **Consistent React Context**: Proper context sharing across components
- **Type Safety**: Centralized TypeScript definitions
- **Maintainability**: Single source of truth for all Ant Design imports
- **Performance**: Reduced bundle size and improved loading times
## What's Available in `@antd-imports`
### Core Components
- **Layout**: Layout, Row, Col, Flex, Divider, Space
- **Navigation**: Menu, Tabs, Breadcrumb, Pagination
- **Data Entry**: Input, Select, DatePicker, TimePicker, Form, Checkbox, InputNumber
- **Data Display**: Table, List, Card, Tag, Avatar, Badge, Progress, Statistic
- **Feedback**: Modal, Drawer, Alert, Message, Notification, Spin, Skeleton, Result
- **Other**: Button, Typography, Tooltip, Popconfirm, Dropdown, ConfigProvider
### Icons
Common icons including: EditOutlined, DeleteOutlined, PlusOutlined, MoreOutlined, CheckOutlined, CloseOutlined, CalendarOutlined, UserOutlined, TeamOutlined, and many more.
### Utilities
- **appMessage**: Centralized message utility
- **appNotification**: Centralized notification utility
- **antdConfig**: Default Ant Design configuration
- **taskManagementAntdConfig**: Task-specific configuration
## Implementation Guidelines
### When Creating New Components:
1. **Always** import from `@/shared/antd-imports`
2. Use `appMessage` and `appNotification` for user feedback
3. Apply `antdConfig` for consistent styling
4. Use `taskManagementAntdConfig` for task-related components
### When Refactoring Existing Code:
1. Replace direct 'antd' imports with `@/shared/antd-imports`
2. Replace direct '@ant-design/icons' imports with `@/shared/antd-imports`
3. Update any custom message/notification calls to use the utilities
### File Location
The centralized import file is located at: `worklenz-frontend/src/shared/antd-imports.ts`
## Examples
### Component Creation
```typescript
import React from 'react';
import { Button, Input, Modal, EditOutlined, appMessage } from '@antd-imports';
const MyComponent = () => {
const handleClick = () => {
appMessage.success('Operation completed!');
};
return (
<Button icon={<EditOutlined />} onClick={handleClick}>
Edit Item
</Button>
);
};
```
### Form Implementation
```typescript
import { Form, Input, Select, Button, DatePicker } from '@antd-imports';
const MyForm = () => {
return (
<Form layout="vertical">
<Form.Item label="Name" name="name">
<Input />
</Form.Item>
<Form.Item label="Type" name="type">
<Select options={options} />
</Form.Item>
<Form.Item label="Date" name="date">
<DatePicker />
</Form.Item>
</Form>
);
};
```
## Enforcement
This rule is **MANDATORY** and applies to:
- All new component development
- All code refactoring
- All bug fixes
- All feature implementations
**Violations will result in code review rejection.**
### File Path:
The centralized file is located at: `worklenz-frontend/src/shared/antd-imports.ts`
# Ant Design Import Rules for Worklenz
## 🚨 CRITICAL: Always Use Centralized Imports
**NEVER import Ant Design components directly from 'antd' or '@ant-design/icons'**
### ✅ Correct Import Pattern
```typescript
import { Button, Input, Select, EditOutlined, PlusOutlined } from '@antd-imports';
// or
import { Button, Input, Select, EditOutlined, PlusOutlined } from '@/shared/antd-imports';
```
### ❌ Forbidden Import Patterns
```typescript
// NEVER do this:
import { Button, Input, Select } from 'antd';
import { EditOutlined, PlusOutlined } from '@ant-design/icons';
```
## Why This Rule Exists
### Benefits of Centralized Imports:
- **Better Tree-Shaking**: Optimized bundle size through centralized management
- **Consistent React Context**: Proper context sharing across components
- **Type Safety**: Centralized TypeScript definitions
- **Maintainability**: Single source of truth for all Ant Design imports
- **Performance**: Reduced bundle size and improved loading times
## What's Available in `@antd-imports`
### Core Components
- **Layout**: Layout, Row, Col, Flex, Divider, Space
- **Navigation**: Menu, Tabs, Breadcrumb, Pagination
- **Data Entry**: Input, Select, DatePicker, TimePicker, Form, Checkbox, InputNumber
- **Data Display**: Table, List, Card, Tag, Avatar, Badge, Progress, Statistic
- **Feedback**: Modal, Drawer, Alert, Message, Notification, Spin, Skeleton, Result
- **Other**: Button, Typography, Tooltip, Popconfirm, Dropdown, ConfigProvider
### Icons
Common icons including: EditOutlined, DeleteOutlined, PlusOutlined, MoreOutlined, CheckOutlined, CloseOutlined, CalendarOutlined, UserOutlined, TeamOutlined, and many more.
### Utilities
- **appMessage**: Centralized message utility
- **appNotification**: Centralized notification utility
- **antdConfig**: Default Ant Design configuration
- **taskManagementAntdConfig**: Task-specific configuration
## Implementation Guidelines
### When Creating New Components:
1. **Always** import from `@antd-imports` or `@/shared/antd-imports`
2. Use `appMessage` and `appNotification` for user feedback
3. Apply `antdConfig` for consistent styling
4. Use `taskManagementAntdConfig` for task-related components
### When Refactoring Existing Code:
1. Replace direct 'antd' imports with `@antd-imports`
2. Replace direct '@ant-design/icons' imports with `@antd-imports`
3. Update any custom message/notification calls to use the utilities
### File Location
The centralized import file is located at: `worklenz-frontend/src/shared/antd-imports.ts`
## Examples
### Component Creation
```typescript
import React from 'react';
import { Button, Input, Modal, EditOutlined, appMessage } from '@antd-imports';
const MyComponent = () => {
const handleClick = () => {
appMessage.success('Operation completed!');
};
return (
<Button icon={<EditOutlined />} onClick={handleClick}>
Edit Item
</Button>
);
};
```
### Form Implementation
```typescript
import { Form, Input, Select, Button, DatePicker } from '@antd-imports';
const MyForm = () => {
return (
<Form layout="vertical">
<Form.Item label="Name" name="name">
<Input />
</Form.Item>
<Form.Item label="Type" name="type">
<Select options={options} />
</Form.Item>
<Form.Item label="Date" name="date">
<DatePicker />
</Form.Item>
</Form>
);
};
```
## Enforcement
This rule is **MANDATORY** and applies to:
- All new component development
- All code refactoring
- All bug fixes
- All feature implementations
**Violations will result in code review rejection.**
### File Path:
The centralized file is located at: `worklenz-frontend/src/shared/antd-imports.ts`

412
README.md
View File

@@ -6,6 +6,24 @@
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,20 +280,12 @@ 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
This project is licensed under the [MIT License](LICENSE).
## Analytics
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.
@@ -261,215 +355,13 @@ If you've previously opted in and want to opt-out:
</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)
4. 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).
### 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
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).
### 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)

View File

@@ -1,561 +0,0 @@
# Invited User Signup Flow - Technical Documentation
## Overview
This document outlines the comprehensive improvements made to the invited user signup flow in Worklenz, focusing on optimizing the experience for users who join through team invitations. The enhancements include database optimizations, frontend flow improvements, performance optimizations, and UI/UX enhancements.
## Table of Contents
1. [Files Modified](#files-modified)
2. [Database Optimizations](#database-optimizations)
3. [Frontend Flow Improvements](#frontend-flow-improvements)
4. [Performance Optimizations](#performance-optimizations)
5. [UI/UX Enhancements](#ui-ux-enhancements)
6. [Internationalization](#internationalization)
7. [Technical Implementation Details](#technical-implementation-details)
8. [Testing Considerations](#testing-considerations)
9. [Migration Guide](#migration-guide)
## Files Modified
### Backend Changes
- `worklenz-backend/database/migrations/20250116000000-invitation-signup-optimization.sql`
- `worklenz-backend/database/migrations/20250115000000-performance-indexes.sql`
### Frontend Changes
- `worklenz-frontend/src/pages/auth/signup-page.tsx`
- `worklenz-frontend/src/pages/auth/authenticating.tsx`
- `worklenz-frontend/src/pages/account-setup/account-setup.tsx`
- `worklenz-frontend/src/features/navbar/switchTeam/SwitchTeamButton.tsx`
- `worklenz-frontend/src/features/navbar/switchTeam/switchTeam.css`
- `worklenz-frontend/src/types/auth/local-session.types.ts`
- `worklenz-frontend/src/types/auth/signup.types.ts`
- `worklenz-frontend/public/locales/en/navbar.json` (+ 5 other locales)
## Database Optimizations
### 1. Invitation Signup Optimization Migration
The core database optimization focuses on streamlining the signup process for invited users by eliminating unnecessary organization/team creation steps.
#### Key Changes:
**Modified `register_user` Function:**
```sql
-- Before: All users go through organization/team creation
-- After: Invited users skip organization creation and join existing teams
-- Check if this is an invitation signup
IF _team_member_id IS NOT NULL THEN
-- Verify the invitation exists and get the team_id
SELECT team_id INTO _invited_team_id
FROM email_invitations
WHERE email = _trimmed_email
AND team_member_id = _team_member_id;
IF _invited_team_id IS NOT NULL THEN
_is_invitation = TRUE;
END IF;
END IF;
```
**Benefits:**
- 60% faster signup process for invited users
- Reduced database transactions from 8 to 3 operations
- Eliminates duplicate organization creation
- Automatic team assignment for invited users
### 2. Performance Indexes
Added comprehensive database indexes to optimize query performance:
```sql
-- Main task filtering optimization
CREATE INDEX CONCURRENTLY idx_tasks_project_archived_parent
ON tasks(project_id, archived, parent_task_id)
WHERE archived = FALSE;
-- Email invitations optimization
CREATE INDEX CONCURRENTLY idx_email_invitations_team_member
ON email_invitations(team_member_id);
-- Team member lookup optimization
CREATE INDEX CONCURRENTLY idx_team_members_team_user
ON team_members(team_id, user_id)
WHERE active = TRUE;
```
**Performance Impact:**
- 40% faster invitation verification
- 30% faster team member queries
- Improved overall application responsiveness
## Frontend Flow Improvements
### 1. Signup Page Enhancements
**File:** `worklenz-frontend/src/pages/auth/signup-page.tsx`
#### Pre-population Logic:
```typescript
// Extract invitation parameters from URL
const [urlParams, setUrlParams] = useState({
email: '',
name: '',
teamId: '',
teamMemberId: '',
projectId: '',
});
// Pre-populate form with invitation data
form.setFieldsValue({
email: searchParams.get('email') || '',
name: searchParams.get('name') || '',
});
```
#### Invitation Context Handling:
```typescript
// Pass invitation context to signup API
if (urlParams.teamId) {
body.team_id = urlParams.teamId;
}
if (urlParams.teamMemberId) {
body.team_member_id = urlParams.teamMemberId;
}
if (urlParams.projectId) {
body.project_id = urlParams.projectId;
}
```
### 2. Authentication Flow Optimization
**File:** `worklenz-frontend/src/pages/auth/authenticating.tsx`
#### Invitation-Aware Routing:
```typescript
// Check if user joined via invitation
if (session.user.invitation_accepted) {
// For invited users, redirect directly to their team
// They don't need to go through setup as they're joining an existing team
setTimeout(() => {
handleSuccessRedirect();
}, REDIRECT_DELAY);
return;
}
// For regular users (team owners), check if setup is needed
if (!session.user.setup_completed) {
return navigate('/worklenz/setup');
}
```
**Benefits:**
- Invited users skip account setup flow
- Direct navigation to assigned team/project
- Reduced onboarding friction
### 3. Account Setup Prevention
**File:** `worklenz-frontend/src/pages/account-setup/account-setup.tsx`
#### Invitation Check:
```typescript
// Prevent invited users from accessing account setup
if (response.user.invitation_accepted) {
navigate('/worklenz/home');
return;
}
```
**Rationale:**
- Invited users don't need to create organizations
- They join existing team structures
- Prevents confusion and duplicate setup
## Performance Optimizations
### 1. SwitchTeamButton Component Optimization
**File:** `worklenz-frontend/src/features/navbar/switchTeam/SwitchTeamButton.tsx`
#### React Performance Improvements:
**Memoization Strategy:**
```typescript
// Component memoization
const TeamCard = memo<TeamCardProps>(({ team, index, teamsList, isActive, onSelect }) => {
// Component implementation
});
const CreateOrgCard = memo<CreateOrgCardProps>(({ isCreating, themeMode, onCreateOrg, t }) => {
// Component implementation
});
```
**Hook Optimization:**
```typescript
// Memoized selectors
const session = useMemo(() => getCurrentSession(), [getCurrentSession]);
const userOwnsOrganization = useMemo(() => {
return teamsList.some(team => team.owner === true);
}, [teamsList]);
// Memoized event handlers
const handleTeamSelect = useCallback(async (id: string) => {
if (!id || isCreatingTeam) return;
// Implementation
}, [dispatch, handleVerifyAuth, isCreatingTeam, t]);
```
**Style Memoization:**
```typescript
// Memoized inline styles
const buttonStyle = useMemo(() => ({
color: themeMode === 'dark' ? '#e6f7ff' : colors.skyBlue,
backgroundColor: themeMode === 'dark' ? '#153450' : colors.paleBlue,
// ... other styles
}), [themeMode, isCreatingTeam]);
```
#### Performance Metrics:
- **Re-renders reduced by 60-70%**
- **API calls optimized** (only fetch when needed)
- **Memory usage reduced** through proper cleanup
- **Faster dropdown interactions**
### 2. CSS Performance Improvements
**File:** `worklenz-frontend/src/features/navbar/switchTeam/switchTeam.css`
#### GPU Acceleration:
```css
.switch-team-dropdown {
will-change: transform, opacity;
transform: translateZ(0);
backface-visibility: hidden;
}
.switch-team-card {
transition: all 0.15s cubic-bezier(0.4, 0, 0.2, 1);
will-change: transform;
}
```
#### Optimized Scrolling:
```css
.ant-dropdown-menu {
-webkit-overflow-scrolling: touch;
scrollbar-width: thin;
}
```
## UI/UX Enhancements
### 1. Business Logic Improvements
#### Organization Creation Restriction:
```typescript
// Check if user already owns an organization
const userOwnsOrganization = useMemo(() => {
return teamsList.some(team => team.owner === true);
}, [teamsList]);
// Only show create organization option if user doesn't already own one
if (!userOwnsOrganization) {
const createOrgItem = {
key: 'create-new-org',
label: <CreateOrgCard ... />,
type: 'item' as const,
};
return [...teamItems, createOrgItem];
}
```
### 2. Dark Mode Support
#### Enhanced Dark Mode Styling:
```css
/* Dark mode scrollbar */
.switch-team-dropdown .ant-dropdown-menu::-webkit-scrollbar-thumb {
background-color: rgba(255, 255, 255, 0.3);
}
/* Dark mode hover effects */
.switch-team-card:hover {
background-color: var(--dark-hover-bg, #f5f5f5);
}
```
### 3. Accessibility Improvements
#### High Contrast Mode:
```css
@media (prefers-contrast: high) {
.switch-team-card {
border: 2px solid currentColor;
}
}
```
#### Reduced Motion Support:
```css
@media (prefers-reduced-motion: reduce) {
.switch-team-card {
transition: none;
}
}
```
## Internationalization
### Translation Keys Added
Added comprehensive translation support across 6 languages:
| Key | English | German | Spanish | Portuguese | Chinese | Albanian |
|-----|---------|---------|---------|------------|---------|----------|
| `createNewOrganization` | "New Organization" | "Neue Organisation" | "Nueva Organización" | "Nova Organização" | "新建组织" | "Organizatë e Re" |
| `createNewOrganizationSubtitle` | "Create new" | "Neue erstellen" | "Crear nueva" | "Criar nova" | "创建新的" | "Krijo të re" |
| `creatingOrganization` | "Creating..." | "Erstelle..." | "Creando..." | "Criando..." | "创建中..." | "Duke krijuar..." |
| `organizationCreatedSuccess` | "Organization created successfully!" | "Organisation erfolgreich erstellt!" | "¡Organización creada exitosamente!" | "Organização criada com sucesso!" | "组织创建成功!" | "Organizata u krijua me sukses!" |
| `organizationCreatedError` | "Failed to create organization" | "Fehler beim Erstellen der Organisation" | "Error al crear la organización" | "Falha ao criar organização" | "创建组织失败" | "Dështoi krijimi i organizatës" |
| `teamSwitchError` | "Failed to switch team" | "Fehler beim Wechseln des Teams" | "Error al cambiar de equipo" | "Falha ao trocar de equipe" | "切换团队失败" | "Dështoi ndryshimi i ekipit" |
### Locale Files Updated:
- `worklenz-frontend/public/locales/en/navbar.json`
- `worklenz-frontend/public/locales/de/navbar.json`
- `worklenz-frontend/public/locales/es/navbar.json`
- `worklenz-frontend/public/locales/pt/navbar.json`
- `worklenz-frontend/public/locales/zh/navbar.json`
- `worklenz-frontend/public/locales/alb/navbar.json`
## Technical Implementation Details
### 1. Type Safety Improvements
#### Session Types:
```typescript
// Added invitation_accepted flag to session
export interface ILocalSession extends IUserType {
// ... existing fields
invitation_accepted?: boolean;
}
```
#### Signup Types:
```typescript
// Enhanced signup request interface
export interface IUserSignUpRequest {
name: string;
email: string;
password: string;
team_name?: string;
team_id?: string; // if from invitation
team_member_id?: string;
timezone?: string;
project_id?: string;
}
// Enhanced signup response interface
export interface IUserSignUpResponse {
id: string;
name?: string;
email: string;
team_id: string;
invitation_accepted: boolean;
google_id?: string;
}
```
### 2. Database Schema Changes
#### User Registration Function:
```sql
-- Returns invitation_accepted flag
RETURN JSON_BUILD_OBJECT(
'id', _user_id,
'name', _trimmed_name,
'email', _trimmed_email,
'team_id', _invited_team_id,
'invitation_accepted', TRUE
);
```
#### User Deserialization:
```sql
-- invitation_accepted is true if user is not the owner of their active team
(NOT is_owner(users.id, users.active_team)) AS invitation_accepted,
```
### 3. Error Handling
#### Robust Error Management:
```typescript
// Signup error handling
try {
const result = await dispatch(signUp(body)).unwrap();
if (result?.authenticated) {
message.success('Successfully signed up!');
navigate('/auth/authenticating');
}
} catch (error: any) {
message.error(error?.response?.data?.message || 'Failed to sign up');
}
// Team switching error handling
try {
await dispatch(setActiveTeam(id));
await handleVerifyAuth();
window.location.reload();
} catch (error) {
console.error('Team selection failed:', error);
message.error(t('teamSwitchError') || 'Failed to switch team');
}
```
## Testing Considerations
### 1. Unit Tests
**Components to Test:**
- `SwitchTeamButton` component memoization
- Team selection logic
- Organization creation flow
- Error handling scenarios
**Test Cases:**
```typescript
// Example test structure
describe('SwitchTeamButton', () => {
it('should only show create organization option for non-owners', () => {
// Test implementation
});
it('should handle team switching correctly', () => {
// Test implementation
});
it('should display loading state during organization creation', () => {
// Test implementation
});
});
```
### 2. Integration Tests
**Signup Flow Tests:**
- Invited user signup with valid invitation
- Regular user signup without invitation
- Error handling for invalid invitations
- Redirect logic after successful signup
**Database Tests:**
- Invitation verification queries
- Team member assignment
- Organization creation logic
- Index performance validation
### 3. Performance Tests
**Metrics to Monitor:**
- Component re-render frequency
- API call optimization
- Database query performance
- Memory usage patterns
## Migration Guide
### 1. Database Migration
**Steps:**
1. Run the invitation optimization migration:
```bash
psql -d worklenz_db -f 20250116000000-invitation-signup-optimization.sql
```
2. Run the performance indexes migration:
```bash
psql -d worklenz_db -f 20250115000000-performance-indexes.sql
```
3. Verify migration success:
```sql
-- Check if new indexes exist
SELECT indexname FROM pg_indexes WHERE tablename = 'email_invitations';
-- Verify function updates
SELECT proname FROM pg_proc WHERE proname = 'register_user';
```
### 2. Frontend Deployment
**Steps:**
1. Update environment variables if needed
2. Build and deploy frontend changes
3. Verify translation files are properly loaded
4. Test invitation flow end-to-end
### 3. Rollback Plan
**Database Rollback:**
```sql
-- Drop new indexes if needed
DROP INDEX IF EXISTS idx_email_invitations_team_member;
DROP INDEX IF EXISTS idx_team_members_team_user;
-- Restore previous function versions
-- (Keep backup of previous function definitions)
```
**Frontend Rollback:**
- Revert to previous component versions
- Remove new translation keys
- Restore original routing logic
## Performance Metrics
### Before Optimization:
- **Signup time for invited users:** 3.2 seconds
- **Component re-renders:** 15-20 per interaction
- **Database queries:** 8 operations per signup
- **Memory usage:** 45MB baseline
### After Optimization:
- **Signup time for invited users:** 1.3 seconds (60% improvement)
- **Component re-renders:** 5-7 per interaction (65% reduction)
- **Database queries:** 3 operations per signup (62% reduction)
- **Memory usage:** 38MB baseline (16% reduction)
## Future Enhancements
### 1. Potential Improvements
- **Batch invitation processing** for multiple users
- **Real-time invitation status updates** via WebSocket
- **Enhanced invitation analytics** and tracking
- **Mobile-optimized invitation flow**
### 2. Monitoring Recommendations
- **Performance monitoring** for signup flow
- **Error tracking** for invitation failures
- **User analytics** for signup conversion rates
- **Database performance** monitoring
## Related Documentation
- [Database Schema Documentation](./database-schema.md)
- [Authentication Flow Guide](./authentication-flow.md)
- [Component Performance Guide](./component-performance.md)
- [Internationalization Guide](./i18n-guide.md)
## Conclusion
The invited user signup flow improvements represent a comprehensive optimization of the user onboarding experience. By combining database optimizations, frontend performance enhancements, and improved UI/UX, the changes result in:
- **60% faster signup process** for invited users
- **65% reduction in component re-renders**
- **Improved user experience** with streamlined flows
- **Better performance** across all supported languages
- **Enhanced accessibility** and dark mode support
These improvements ensure that invited users can join teams quickly and efficiently, while maintaining high performance and user experience standards across the entire application.

View File

@@ -1,332 +0,0 @@
# SwitchTeamButton Component Improvements
## Overview
This document outlines the comprehensive improvements made to the `SwitchTeamButton` component, focusing on performance optimization, business logic enhancement, accessibility, and internationalization support.
## 📁 Files Modified
### Core Component Files
- `worklenz-frontend/src/features/navbar/switchTeam/SwitchTeamButton.tsx`
- `worklenz-frontend/src/features/navbar/switchTeam/switchTeam.css`
### Internationalization Files
- `worklenz-frontend/public/locales/en/navbar.json`
- `worklenz-frontend/public/locales/de/navbar.json`
- `worklenz-frontend/public/locales/es/navbar.json`
- `worklenz-frontend/public/locales/pt/navbar.json`
- `worklenz-frontend/public/locales/zh/navbar.json`
- `worklenz-frontend/public/locales/alb/navbar.json`
## 🚀 Performance Optimizations
### 1. Component Memoization
```typescript
// Before: No memoization
const SwitchTeamButton = () => { ... }
// After: Memoized component with sub-components
const SwitchTeamButton = memo(() => { ... })
const TeamCard = memo<TeamCardProps>(({ ... }) => { ... })
const CreateOrgCard = memo<CreateOrgCardProps>(({ ... }) => { ... })
```
### 2. Hook Optimizations
```typescript
// Memoized session data
const session = useMemo(() => getCurrentSession(), [getCurrentSession]);
// Memoized auth service
const authService = useMemo(() => createAuthService(navigate), [navigate]);
// Optimized team fetching
useEffect(() => {
if (!teamsLoading && teamsList.length === 0) {
dispatch(fetchTeams());
}
}, [dispatch, teamsLoading, teamsList.length]);
```
### 3. Event Handler Optimization
```typescript
// All event handlers are memoized with useCallback
const handleTeamSelect = useCallback(async (id: string) => {
// Implementation with proper error handling
}, [dispatch, handleVerifyAuth, isCreatingTeam, t]);
const handleCreateNewOrganization = useCallback(async () => {
// Implementation with loading states
}, [isCreatingTeam, session?.name, t, handleTeamSelect, navigate]);
```
### 4. Style Memoization
```typescript
// Memoized inline styles to prevent recreation
const buttonStyle = useMemo(() => ({
color: themeMode === 'dark' ? '#e6f7ff' : colors.skyBlue,
backgroundColor: themeMode === 'dark' ? '#153450' : colors.paleBlue,
// ... other styles
}), [themeMode, isCreatingTeam]);
```
## 🏢 Business Logic Changes
### 1. Organization Ownership Restriction
```typescript
// New logic: Only show "Create New Organization" if user doesn't own one
const userOwnsOrganization = useMemo(() => {
return teamsList.some(team => team.owner === true);
}, [teamsList]);
// Conditional rendering in dropdown items
if (!userOwnsOrganization) {
const createOrgItem = { /* ... */ };
return [...teamItems, createOrgItem];
}
return teamItems;
```
### 2. Enhanced Error Handling
```typescript
// Improved error handling with try-catch blocks
try {
await dispatch(setActiveTeam(id));
await handleVerifyAuth();
window.location.reload();
} catch (error) {
console.error('Team selection failed:', error);
message.error(t('teamSwitchError') || 'Failed to switch team');
}
```
### 3. Type Safety Improvements
```typescript
// Before: Generic 'any' types
team: any;
teamsList: any[];
// After: Proper TypeScript interfaces
team: ITeamGetResponse;
teamsList: ITeamGetResponse[];
```
## 🎨 CSS & Styling Improvements
### 1. Performance Optimizations
```css
/* GPU acceleration for smooth animations */
.switch-team-card {
transition: all 0.15s ease;
will-change: transform, background-color;
}
/* Optimized scrolling */
.switch-team-dropdown .ant-dropdown-menu {
will-change: transform;
transform: translateZ(0);
-webkit-overflow-scrolling: touch;
}
```
### 2. Enhanced Dark Mode Support
```css
/* Dark mode scrollbar */
.ant-theme-dark .switch-team-dropdown .ant-dropdown-menu::-webkit-scrollbar-thumb {
background-color: rgba(255, 255, 255, 0.2);
}
/* Dark mode text contrast */
.ant-theme-dark .switch-team-card .ant-typography {
color: rgba(255, 255, 255, 0.85);
}
```
### 3. Accessibility Improvements
```css
/* High contrast mode support */
@media (prefers-contrast: high) {
.switch-team-card {
border: 1px solid currentColor;
}
}
/* Reduced motion support */
@media (prefers-reduced-motion: reduce) {
.switch-team-card {
transition: none;
}
}
```
### 4. Responsive Design
```css
/* Mobile optimization */
@media (max-width: 768px) {
.switch-team-dropdown .ant-dropdown-menu {
max-height: 200px;
}
.switch-team-card {
width: 200px !important;
}
}
```
## 🌍 Internationalization Updates
### New Translation Keys Added
All locale files now include these new keys:
```json
{
"createNewOrganization": "New Organization",
"createNewOrganizationSubtitle": "Create new",
"creatingOrganization": "Creating...",
"organizationCreatedSuccess": "Organization created successfully!",
"organizationCreatedError": "Failed to create organization",
"teamSwitchError": "Failed to switch team"
}
```
### Language-Specific Translations
| Language | createNewOrganization | organizationCreatedSuccess |
|----------|----------------------|---------------------------|
| English | New Organization | Organization created successfully! |
| German | Neue Organisation | Organisation erfolgreich erstellt! |
| Spanish | Nueva Organización | ¡Organización creada exitosamente! |
| Portuguese | Nova Organização | Organização criada com sucesso! |
| Chinese | 新建组织 | 组织创建成功! |
| Albanian | Organizatë e Re | Organizata u krijua me sukses! |
## 🔧 Technical Implementation Details
### 1. Component Architecture
```
SwitchTeamButton (Main Component)
├── TeamCard (Memoized Sub-component)
├── CreateOrgCard (Memoized Sub-component)
└── Dropdown with conditional items
```
### 2. State Management
```typescript
// Local state
const [isCreatingTeam, setIsCreatingTeam] = useState(false);
// Redux selectors
const teamsList = useAppSelector(state => state.teamReducer.teamsList);
const themeMode = useAppSelector(state => state.themeReducer.mode);
const teamsLoading = useAppSelector(state => state.teamReducer.loading);
```
### 3. API Integration
```typescript
// Optimized team creation
const response = await teamsApiService.createTeam(teamData);
if (response.done && response.body?.id) {
message.success(t('organizationCreatedSuccess'));
await handleTeamSelect(response.body.id);
navigate('/account-setup');
}
```
## 📊 Performance Metrics
### Expected Improvements
- **Render Performance**: 60-70% reduction in unnecessary re-renders
- **Memory Usage**: 30-40% reduction through proper memoization
- **Animation Smoothness**: 90% improvement with GPU acceleration
- **Bundle Size**: No increase (optimized imports)
### Monitoring
```typescript
// Development performance tracking (removed in production)
useEffect(() => {
if (process.env.NODE_ENV === 'development') {
trackRender('SwitchTeamButton');
}
}, []);
```
## 🧪 Testing Considerations
### Unit Tests Required
1. **Organization ownership logic**
- Test when user owns organization (no create option)
- Test when user doesn't own organization (create option visible)
2. **Error handling**
- Test team switch failures
- Test organization creation failures
3. **Internationalization**
- Test all translation keys in different locales
- Test fallback behavior for missing translations
### Integration Tests
1. **API interactions**
- Team fetching optimization
- Organization creation flow
- Team switching flow
2. **Theme switching**
- Dark mode transitions
- Style consistency across themes
## 🚨 Breaking Changes
### None
All changes are backward compatible. The component maintains the same external API while improving internal implementation.
## 📝 Migration Notes
### For Developers
1. **Import Changes**: No changes required
2. **Props**: No changes to component props
3. **Styling**: Existing custom styles will continue to work
4. **Translations**: New keys added, existing keys unchanged
### For Translators
New translation keys need to be added to any custom locale files:
- `createNewOrganization`
- `createNewOrganizationSubtitle`
- `creatingOrganization`
- `organizationCreatedSuccess`
- `organizationCreatedError`
- `teamSwitchError`
## 🔮 Future Enhancements
### Potential Improvements
1. **Virtual scrolling** for large team lists
2. **Keyboard navigation** improvements
3. **Team search/filter** functionality
4. **Drag-and-drop** team reordering
5. **Team avatars** from organization logos
### Performance Monitoring
Consider adding performance monitoring in production:
```typescript
// Example: Performance monitoring hook
const { trackRender, createDebouncedCallback } = usePerformanceOptimization();
```
## 📚 Related Documentation
- [React Performance Best Practices](https://react.dev/learn/render-and-commit)
- [Ant Design Theme Customization](https://ant.design/docs/react/customize-theme)
- [i18next React Integration](https://react.i18next.com/)
- [TypeScript Best Practices](https://www.typescriptlang.org/docs/handbook/declaration-files/do-s-and-don-ts.html)
## 👥 Contributors
- **Performance Optimization**: Component memoization, CSS optimizations
- **Business Logic**: Organization ownership restrictions
- **Internationalization**: Multi-language support
- **Accessibility**: WCAG compliance improvements
- **Testing**: Unit and integration test guidelines
---
*Last updated: [Current Date]*
*Version: 2.0.0*

41
test_sort_fix.sql Normal file
View File

@@ -0,0 +1,41 @@
-- Test script to verify the sort order constraint fix
-- Test the helper function
SELECT get_sort_column_name('status'); -- Should return 'status_sort_order'
SELECT get_sort_column_name('priority'); -- Should return 'priority_sort_order'
SELECT get_sort_column_name('phase'); -- Should return 'phase_sort_order'
SELECT get_sort_column_name('members'); -- Should return 'member_sort_order'
SELECT get_sort_column_name('unknown'); -- Should return 'status_sort_order' (default)
-- Test bulk update function (example - would need real project_id and task_ids)
/*
SELECT update_task_sort_orders_bulk(
'[
{"task_id": "example-uuid", "sort_order": 1, "status_id": "status-uuid"},
{"task_id": "example-uuid-2", "sort_order": 2, "status_id": "status-uuid"}
]'::json,
'status'
);
*/
-- Verify that sort_order constraint still exists and works
SELECT
tc.constraint_name,
tc.table_name,
kcu.column_name
FROM information_schema.table_constraints tc
JOIN information_schema.key_column_usage kcu
ON tc.constraint_name = kcu.constraint_name
WHERE tc.constraint_name = 'tasks_sort_order_unique';
-- Check that new sort order columns don't have unique constraints (which is correct)
SELECT
tc.constraint_name,
tc.table_name,
kcu.column_name
FROM information_schema.table_constraints tc
JOIN information_schema.key_column_usage kcu
ON tc.constraint_name = kcu.constraint_name
WHERE kcu.table_name = 'tasks'
AND kcu.column_name IN ('status_sort_order', 'priority_sort_order', 'phase_sort_order', 'member_sort_order')
AND tc.constraint_type = 'UNIQUE';

30
test_sort_orders.sql Normal file
View File

@@ -0,0 +1,30 @@
-- Test script to validate the separate sort order implementation
-- Check if new columns exist
SELECT column_name, data_type, is_nullable, column_default
FROM information_schema.columns
WHERE table_name = 'tasks'
AND column_name IN ('status_sort_order', 'priority_sort_order', 'phase_sort_order', 'member_sort_order')
ORDER BY column_name;
-- Check if helper function exists
SELECT routine_name, routine_type
FROM information_schema.routines
WHERE routine_name IN ('get_sort_column_name', 'update_task_sort_orders_bulk', 'handle_task_list_sort_order_change');
-- Sample test data to verify different sort orders work
-- (This would be run after the migrations)
/*
-- Test: Tasks should have different orders for different groupings
SELECT
id,
name,
sort_order,
status_sort_order,
priority_sort_order,
phase_sort_order,
member_sort_order
FROM tasks
WHERE project_id = '<test-project-id>'
ORDER BY status_sort_order;
*/

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,292 +0,0 @@
-- Migration: Optimize invitation signup process to skip organization/team creation for invited users
-- Release: v2.1.1
-- Date: 2025-01-16
-- Drop and recreate register_user function with invitation optimization
DROP FUNCTION IF EXISTS register_user(_body json);
CREATE OR REPLACE FUNCTION register_user(_body json) RETURNS json
LANGUAGE plpgsql
AS
$$
DECLARE
_user_id UUID;
_organization_id UUID;
_team_id UUID;
_role_id UUID;
_trimmed_email TEXT;
_trimmed_name TEXT;
_trimmed_team_name TEXT;
_invited_team_id UUID;
_team_member_id UUID;
_is_invitation BOOLEAN DEFAULT FALSE;
BEGIN
_trimmed_email = LOWER(TRIM((_body ->> 'email')));
_trimmed_name = TRIM((_body ->> 'name'));
_trimmed_team_name = TRIM((_body ->> 'team_name'));
_team_member_id = (_body ->> 'team_member_id')::UUID;
-- check user exists
IF EXISTS(SELECT email FROM users WHERE email = _trimmed_email)
THEN
RAISE 'EMAIL_EXISTS_ERROR:%', (_body ->> 'email');
END IF;
-- insert user
INSERT INTO users (name, email, password, timezone_id)
VALUES (_trimmed_name, _trimmed_email, (_body ->> 'password'),
COALESCE((SELECT id FROM timezones WHERE name = (_body ->> 'timezone')),
(SELECT id FROM timezones WHERE name = 'UTC')))
RETURNING id INTO _user_id;
-- Check if this is an invitation signup
IF _team_member_id IS NOT NULL THEN
-- Verify the invitation exists and get the team_id
SELECT team_id INTO _invited_team_id
FROM email_invitations
WHERE email = _trimmed_email
AND team_member_id = _team_member_id;
IF _invited_team_id IS NOT NULL THEN
_is_invitation = TRUE;
END IF;
END IF;
-- Handle invitation signup (skip organization/team creation)
IF _is_invitation THEN
-- Set user's active team to the invited team
UPDATE users SET active_team = _invited_team_id WHERE id = _user_id;
-- Update the existing team_members record with the new user_id
UPDATE team_members
SET user_id = _user_id
WHERE id = _team_member_id
AND team_id = _invited_team_id;
-- Delete the email invitation record
DELETE FROM email_invitations
WHERE email = _trimmed_email
AND team_member_id = _team_member_id;
RETURN JSON_BUILD_OBJECT(
'id', _user_id,
'name', _trimmed_name,
'email', _trimmed_email,
'team_id', _invited_team_id,
'invitation_accepted', TRUE
);
END IF;
-- Handle regular signup (create organization/team)
--insert organization data
INSERT INTO organizations (user_id, organization_name, contact_number, contact_number_secondary, trial_in_progress,
trial_expire_date, subscription_status, license_type_id)
VALUES (_user_id, _trimmed_team_name, NULL, NULL, TRUE, CURRENT_DATE + INTERVAL '9999 days',
'active', (SELECT id FROM sys_license_types WHERE key = 'SELF_HOSTED'))
RETURNING id INTO _organization_id;
-- insert team
INSERT INTO teams (name, user_id, organization_id)
VALUES (_trimmed_team_name, _user_id, _organization_id)
RETURNING id INTO _team_id;
-- Set user's active team to their new team
UPDATE users SET active_team = _team_id WHERE id = _user_id;
-- insert default roles
INSERT INTO roles (name, team_id, default_role) VALUES ('Member', _team_id, TRUE);
INSERT INTO roles (name, team_id, admin_role) VALUES ('Admin', _team_id, TRUE);
INSERT INTO roles (name, team_id, owner) VALUES ('Owner', _team_id, TRUE) RETURNING id INTO _role_id;
-- insert team member
INSERT INTO team_members (user_id, team_id, role_id)
VALUES (_user_id, _team_id, _role_id);
RETURN JSON_BUILD_OBJECT(
'id', _user_id,
'name', _trimmed_name,
'email', _trimmed_email,
'team_id', _team_id,
'invitation_accepted', FALSE
);
END
$$;
-- Drop and recreate register_google_user function with invitation optimization
DROP FUNCTION IF EXISTS register_google_user(_body json);
CREATE OR REPLACE FUNCTION register_google_user(_body json) RETURNS json
LANGUAGE plpgsql
AS
$$
DECLARE
_user_id UUID;
_organization_id UUID;
_team_id UUID;
_role_id UUID;
_name TEXT;
_email TEXT;
_google_id TEXT;
_team_name TEXT;
_team_member_id UUID;
_invited_team_id UUID;
_is_invitation BOOLEAN DEFAULT FALSE;
BEGIN
_name = (_body ->> 'displayName')::TEXT;
_email = (_body ->> 'email')::TEXT;
_google_id = (_body ->> 'id');
_team_name = (_body ->> 'team_name')::TEXT;
_team_member_id = (_body ->> 'member_id')::UUID;
_invited_team_id = (_body ->> 'team')::UUID;
INSERT INTO users (name, email, google_id, timezone_id)
VALUES (_name, _email, _google_id, COALESCE((SELECT id FROM timezones WHERE name = (_body ->> 'timezone')),
(SELECT id FROM timezones WHERE name = 'UTC')))
RETURNING id INTO _user_id;
-- Check if this is an invitation signup
IF _team_member_id IS NOT NULL AND _invited_team_id IS NOT NULL THEN
-- Verify the team member exists in the invited team
IF EXISTS(SELECT id
FROM team_members
WHERE id = _team_member_id
AND team_id = _invited_team_id) THEN
_is_invitation = TRUE;
END IF;
END IF;
-- Handle invitation signup (skip organization/team creation)
IF _is_invitation THEN
-- Set user's active team to the invited team
UPDATE users SET active_team = _invited_team_id WHERE id = _user_id;
-- Update the existing team_members record with the new user_id
UPDATE team_members
SET user_id = _user_id
WHERE id = _team_member_id
AND team_id = _invited_team_id;
-- Delete the email invitation record
DELETE FROM email_invitations
WHERE team_id = _invited_team_id
AND team_member_id = _team_member_id;
RETURN JSON_BUILD_OBJECT(
'id', _user_id,
'email', _email,
'google_id', _google_id,
'team_id', _invited_team_id,
'invitation_accepted', TRUE
);
END IF;
-- Handle regular signup (create organization/team)
--insert organization data
INSERT INTO organizations (user_id, organization_name, contact_number, contact_number_secondary, trial_in_progress,
trial_expire_date, subscription_status, license_type_id)
VALUES (_user_id, COALESCE(_team_name, _name), NULL, NULL, TRUE, CURRENT_DATE + INTERVAL '9999 days',
'active', (SELECT id FROM sys_license_types WHERE key = 'SELF_HOSTED'))
RETURNING id INTO _organization_id;
INSERT INTO teams (name, user_id, organization_id)
VALUES (COALESCE(_team_name, _name), _user_id, _organization_id)
RETURNING id INTO _team_id;
-- Set user's active team to their new team
UPDATE users SET active_team = _team_id WHERE id = _user_id;
-- insert default roles
INSERT INTO roles (name, team_id, default_role) VALUES ('Member', _team_id, TRUE);
INSERT INTO roles (name, team_id, admin_role) VALUES ('Admin', _team_id, TRUE);
INSERT INTO roles (name, team_id, owner) VALUES ('Owner', _team_id, TRUE) RETURNING id INTO _role_id;
INSERT INTO team_members (user_id, team_id, role_id)
VALUES (_user_id, _team_id, _role_id);
RETURN JSON_BUILD_OBJECT(
'id', _user_id,
'email', _email,
'google_id', _google_id,
'team_id', _team_id,
'invitation_accepted', FALSE
);
END
$$;
-- Update deserialize_user function to include invitation_accepted flag
DROP FUNCTION IF EXISTS deserialize_user(_id uuid);
CREATE OR REPLACE FUNCTION deserialize_user(_id uuid) RETURNS json
LANGUAGE plpgsql
AS
$$
DECLARE
_result JSON;
_team_id UUID;
BEGIN
SELECT active_team FROM users WHERE id = _id INTO _team_id;
IF NOT EXISTS(SELECT 1 FROM notification_settings WHERE team_id = _team_id AND user_id = _id)
THEN
INSERT INTO notification_settings (popup_notifications_enabled, show_unread_items_count, user_id, team_id)
VALUES (TRUE, TRUE, _id, _team_id);
END IF;
SELECT ROW_TO_JSON(rec)
INTO _result
FROM (SELECT users.id,
users.name,
users.email,
users.timezone_id AS timezone,
(SELECT name FROM timezones WHERE id = users.timezone_id) AS timezone_name,
users.avatar_url,
users.user_no,
users.socket_id,
users.created_at AS joined_date,
users.updated_at AS last_updated,
(SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(rec))), '[]'::JSON)
FROM (SELECT description, type FROM worklenz_alerts WHERE active is TRUE) rec) AS alerts,
(SELECT email_notifications_enabled
FROM notification_settings
WHERE user_id = users.id
AND team_id = t.id) AS email_notifications_enabled,
(CASE
WHEN is_owner(users.id, users.active_team) THEN users.setup_completed
ELSE TRUE END) AS setup_completed,
users.setup_completed AS my_setup_completed,
(is_null_or_empty(users.google_id) IS FALSE) AS is_google,
t.name AS team_name,
t.id AS team_id,
(SELECT id
FROM team_members
WHERE team_members.user_id = _id
AND team_id = users.active_team
AND active IS TRUE) AS team_member_id,
is_owner(users.id, users.active_team) AS owner,
is_admin(users.id, users.active_team) AS is_admin,
t.user_id AS owner_id,
-- invitation_accepted is true if user is not the owner of their active team
(NOT is_owner(users.id, users.active_team)) AS invitation_accepted,
ud.subscription_status,
(SELECT CASE
WHEN (ud.subscription_status) = 'trialing'
THEN (trial_expire_date)::DATE
WHEN (EXISTS(SELECT id FROM licensing_custom_subs WHERE user_id = t.user_id))
THEN (SELECT end_date FROM licensing_custom_subs lcs WHERE lcs.user_id = t.user_id)::DATE
WHEN EXISTS (SELECT 1
FROM licensing_user_subscriptions
WHERE user_id = t.user_id AND active IS TRUE)
THEN (SELECT (next_bill_date)::DATE - INTERVAL '1 day'
FROM licensing_user_subscriptions
WHERE user_id = t.user_id)::DATE
END) AS valid_till_date
FROM users
INNER JOIN teams t
ON t.id = COALESCE(users.active_team,
(SELECT id FROM teams WHERE teams.user_id = users.id LIMIT 1))
LEFT JOIN organizations ud ON ud.user_id = t.user_id
WHERE users.id = _id) rec;
RETURN _result;
END
$$;

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,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,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

@@ -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,

View File

@@ -4313,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
@@ -4325,66 +4343,67 @@ DECLARE
_from_group UUID;
_to_group UUID;
_group_by TEXT;
_batch_size INT := 100; -- PERFORMANCE OPTIMIZATION: Batch size for large updates
_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;
-- PERFORMANCE OPTIMIZATION: Use CTE for better query planning
IF (_from_group <> _to_group OR (_from_group <> _to_group) IS NULL)
THEN
-- PERFORMANCE OPTIMIZATION: Batch update group changes
IF (_group_by = 'status')
THEN
-- 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
SET status_id = _to_group, updated_at = CURRENT_TIMESTAMP
WHERE id = _task_id
AND status_id = _from_group
AND project_id = _project_id;
END IF;
IF (_group_by = 'priority')
THEN
IF (_group_by = 'priority') THEN
UPDATE tasks
SET priority_id = _to_group
SET priority_id = _to_group, updated_at = CURRENT_TIMESTAMP
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
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;
-- PERFORMANCE OPTIMIZATION: Optimized sort order handling
IF ((_body ->> 'to_last_index')::BOOLEAN IS TRUE AND _from_index < _to_index)
THEN
PERFORM handle_task_list_sort_inside_group_optimized(_from_index, _to_index, _task_id, _project_id, _batch_size);
-- 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_optimized(_from_index, _to_index, _task_id, _project_id, _batch_size);
-- 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_optimized(_from_index, _to_index, _task_id, _project_id, _batch_size);
-- 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
$$;
@@ -4589,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
$$;
@@ -5497,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
@@ -5507,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
$$;
@@ -6394,7 +6443,7 @@ DECLARE
_offset INT := 0;
_affected_rows INT;
BEGIN
-- PERFORMANCE OPTIMIZATION: Use CTE for better query planning
-- 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);
@@ -6404,18 +6453,15 @@ BEGIN
IF _to_index > _from_index
THEN
LOOP
WITH batch_update AS (
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
RETURNING 1
)
SELECT COUNT(*) INTO _affected_rows FROM batch_update;
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;
@@ -6427,18 +6473,15 @@ BEGIN
THEN
_offset := 0;
LOOP
WITH batch_update AS (
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
RETURNING 1
)
SELECT COUNT(*) INTO _affected_rows FROM batch_update;
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;
@@ -6457,22 +6500,19 @@ DECLARE
_offset INT := 0;
_affected_rows INT;
BEGIN
-- PERFORMANCE OPTIMIZATION: Batch updates for large datasets
-- PERFORMANCE OPTIMIZATION: Batch updates for large datasets without CTE in UPDATE
IF _to_index > _from_index
THEN
LOOP
WITH batch_update AS (
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
RETURNING 1
)
SELECT COUNT(*) INTO _affected_rows FROM batch_update;
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;
@@ -6482,18 +6522,15 @@ BEGIN
THEN
_offset := 0;
LOOP
WITH batch_update AS (
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
RETURNING 1
)
SELECT COUNT(*) INTO _affected_rows FROM batch_update;
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;
@@ -6502,3 +6539,112 @@ BEGIN
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

@@ -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"
}]
}
};

View File

@@ -4,7 +4,7 @@
"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",
@@ -68,7 +68,6 @@
"express-rate-limit": "^6.8.0",
"express-session": "^1.17.3",
"express-validator": "^6.15.0",
"grunt-cli": "^1.5.0",
"helmet": "^6.2.0",
"hpp": "^0.2.3",
"http-errors": "^2.0.0",

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;
}

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()
@@ -779,7 +772,7 @@ export default class ProjectsController extends WorklenzControllerBase {
let groupJoin = "";
let groupByFields = "";
let groupOrderBy = "";
switch (groupBy) {
case "client":
groupField = "COALESCE(projects.client_id::text, 'no-client')";
@@ -888,13 +881,13 @@ export default class ProjectsController extends WorklenzControllerBase {
ELSE p2.updated_at END) AS updated_at
FROM projects p2
${groupJoin.replace("projects.", "p2.")}
WHERE p2.team_id = $1
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.")}
${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

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

@@ -0,0 +1,117 @@
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) {
// Convert dates to user's timezone start/end of day
const start = moment.tz(dateRange[0], userTimezone).startOf('day');
const end = moment.tz(dateRange[1], 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 task_work_log.created_at >= '${startUtc}'::TIMESTAMP AND task_work_log.created_at <= '${endUtc}'::TIMESTAMP`;
}
return `AND task_work_log.created_at >= '${startUtc}'::TIMESTAMP AND task_work_log.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 task_work_log.created_at >= '${startUtc}'::TIMESTAMP AND task_work_log.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: string = "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 = "",
@@ -487,7 +546,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 +1099,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 +1293,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, "");
}

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

@@ -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;

File diff suppressed because it is too large Load Diff

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

@@ -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"
}
}

View File

@@ -0,0 +1,17 @@
{
"nameColumn": "Emri",
"jobTitleColumn": "Titulli i Punës",
"emailColumn": "Email",
"tasksColumn": "Detyrat",
"taskProgressColumn": "Progresi i Detyrave",
"accessColumn": "Qasja",
"fileIconAlt": "Ikona e skedarit",
"deleteConfirmationTitle": "Jeni i sigurt?",
"deleteConfirmationOk": "Po",
"deleteConfirmationCancel": "Anulo",
"refreshButtonTooltip": "Rifresko anëtarët",
"deleteButtonTooltip": "Hiq nga projekti",
"memberCount": "Anëtar",
"membersCountPlural": "Anëtarë",
"emptyText": "Nuk ka bashkëngjitje në projekt."
}

View File

@@ -0,0 +1,6 @@
{
"inputPlaceholder": "Shto një koment..",
"addButton": "Shto",
"cancelButton": "Anulo",
"deleteButton": "Fshi"
}

View File

@@ -0,0 +1,14 @@
{
"taskList": "Lista e Detyrave",
"board": "Tabela Kanban",
"insights": "Analiza",
"files": "Skedarë",
"members": "Anëtarë",
"updates": "Përditësime",
"projectView": "Pamja e Projektit",
"loading": "Duke ngarkuar projektin...",
"error": "Gabim në ngarkimin e projektit",
"pinnedTab": "E fiksuar si tab i parazgjedhur",
"pinTab": "Fikso si tab i parazgjedhur",
"unpinTab": "Hiqe fiksimin e tab-it të parazgjedhur"
}

View File

@@ -0,0 +1,11 @@
{
"importTaskTemplate": "Importo Shabllon Detyrash",
"templateName": "Emri i Shabllonit",
"templateDescription": "Përshkrimi i Shabllonit",
"selectedTasks": "Detyrat e Përzgjedhura",
"tasks": "Detyrat",
"templates": "Shabllonet",
"remove": "Hiq",
"cancel": "Anulo",
"import": "Importo"
}

View File

@@ -0,0 +1,7 @@
{
"title": "Anëtarët e Projektit",
"searchLabel": "Shtoni anëtarë duke shkruar emrin ose email-in e tyre",
"searchPlaceholder": "Shkruani emrin ose email-in",
"inviteAsAMember": "Fto si anëtar",
"inviteNewMemberByEmail": "Fto anëtar të ri me email"
}

View File

@@ -0,0 +1,30 @@
{
"importTasks": "Importo detyra",
"importTask": "Importo detyrë",
"createTask": "Krijo detyrë",
"settings": "Cilësimet",
"subscribe": "Abonohu",
"unsubscribe": "Çabonohu",
"deleteProject": "Fshi projektin",
"startDate": "Data e fillimit",
"endDate": "Data e mbarimit",
"projectSettings": "Cilësimet e projektit",
"projectSummary": "Përmbledhja e projektit",
"receiveProjectSummary": "Merrni një përmbledhje të projektit çdo mbrëmje.",
"refreshProject": "Rifresko projektin",
"saveAsTemplate": "Ruaj si model",
"invite": "Fto",
"share": "Ndaj",
"subscribeTooltip": "Abonohu tek njoftimet e projektit",
"unsubscribeTooltip": "Çabonohu nga njoftimet e projektit",
"refreshTooltip": "Rifresko të dhënat e projektit",
"settingsTooltip": "Hap cilësimet e projektit",
"saveAsTemplateTooltip": "Ruaj këtë projekt si model",
"inviteTooltip": "Fto anëtarë të ekipit në këtë projekt",
"createTaskTooltip": "Krijo një detyrë të re",
"importTaskTooltip": "Importo detyrë nga modeli",
"navigateBackTooltip": "Kthehu tek lista e projekteve",
"projectStatusTooltip": "Statusi i projektit",
"projectDatesInfo": "Informacion për kohëzgjatjen e projektit",
"projectCategoryTooltip": "Kategoria e projektit"
}

View File

@@ -0,0 +1,27 @@
{
"title": "Ruaj si Shabllon",
"templateName": "Emri i Shabllonit",
"includes": "Çfarë duhet të përfshihet në shabllon nga projekti?",
"includesOptions": {
"statuses": "Statuset",
"phases": "Fazat",
"labels": "Etiketat"
},
"taskIncludes": "Çfarë duhet të përfshihet në shabllon nga detyrat?",
"taskIncludesOptions": {
"statuses": "Statuset",
"phases": "Fazat",
"labels": "Etiketat",
"name": "Emri",
"priority": "Prioriteti",
"status": "Statusi",
"phase": "Faza",
"label": "Etiketa",
"timeEstimate": "Vlerësimi i Kohës",
"description": "Përshkrimi",
"subTasks": "Nëndetyrat"
},
"cancel": "Anulo",
"save": "Ruaj",
"templateNamePlaceholder": "Shkruani emrin e shabllonit"
}

View File

@@ -0,0 +1,90 @@
{
"exportButton": "Eksporto",
"timeLogsButton": "Regjistrimet e Kohës",
"activityLogsButton": "Regjistrimet e Aktivitetit",
"tasksButton": "Detyrat",
"searchByNameInputPlaceholder": "Kërko sipas emrit",
"overviewTab": "Përmbledhje",
"timeLogsTab": "Regjistrimet e Kohës",
"activityLogsTab": "Regjistrimet e Aktivitetit",
"tasksTab": "Detyrat",
"projectsText": "Projektet",
"totalTasksText": "Detyrat Gjithsej",
"assignedTasksText": "Detyrat e Caktuara",
"completedTasksText": "Detyrat e Përfunduara",
"ongoingTasksText": "Detyrat në Vazhdim",
"overdueTasksText": "Detyrat e Vonuara",
"loggedHoursText": "Orët e Regjistruara",
"tasksText": "Detyrat",
"allText": "Të Gjitha",
"tasksByProjectsText": "Detyrat Sipas Projekteve",
"tasksByStatusText": "Detyrat Sipas Statusit",
"tasksByPriorityText": "Detyrat Sipas Prioritetit",
"todoText": "Për Të Bërë",
"doingText": "Duke bërë",
"doneText": "E Përfunduar",
"lowText": "I Ulët",
"mediumText": "I Mesëm",
"highText": "I Lartë",
"billableButton": "Fakturueshme",
"billableText": "Fakturueshme",
"nonBillableText": "Jo Fakturueshme",
"timeLogsEmptyPlaceholder": "Asnjë regjistrim kohe për të shfaqur",
"loggedText": "Regjistruar",
"forText": "për",
"inText": "në",
"updatedText": "Përditësuar",
"fromText": "Nga",
"toText": "në",
"withinText": "brenda",
"activityLogsEmptyPlaceholder": "Asnjë regjistrim aktiviteti për të shfaqur",
"filterByText": "Filtro sipas:",
"selectProjectPlaceholder": "Zgjidh Projektin",
"taskColumn": "Detyra",
"nameColumn": "Emri",
"projectColumn": "Projekti",
"statusColumn": "Statusi",
"priorityColumn": "Prioriteti",
"dueDateColumn": "Afati",
"completedDateColumn": "Data e Përfundimit",
"estimatedTimeColumn": "Koha e Vlerësuar",
"loggedTimeColumn": "Koha e Regjistruar",
"overloggedTimeColumn": "Koha e Tepërt",
"daysLeftColumn": "Ditë të Mbetura/Vonuar",
"startDateColumn": "Data e Fillimit",
"endDateColumn": "Data e Përfundimit",
"actualTimeColumn": "Koha Aktuale",
"projectHealthColumn": "Gjendja e Projektit",
"categoryColumn": "Kategoria",
"projectManagerColumn": "Menaxheri i Projektit",
"tasksStatsOverviewDrawerTitle": "Detyrat e ",
"projectsStatsOverviewDrawerTitle": "Projektet e ",
"cancelledText": "Anuluar",
"blockedText": "E Bllokuar",
"onHoldText": "Në Pritje",
"proposedText": "E Propozuar",
"inPlanningText": "Në Planifikim",
"inProgressText": "Në Progres",
"completedText": "E Përfunduar",
"continuousText": "E Vazhdueshme",
"daysLeftText": "ditë të mbetura",
"daysOverdueText": "ditë vonuar",
"notSetText": "Pa Caktuar",
"needsAttentionText": "Kërkon Vëmendje",
"atRiskText": "Në Rrezik",
"goodText": "Në Rregull"
}

View File

@@ -0,0 +1,35 @@
{
"yesterdayText": "Dje",
"lastSevenDaysText": "7 Ditët e Fundit",
"lastWeekText": "Javën e Kaluar",
"lastThirtyDaysText": "30 Ditët e Fundit",
"lastMonthText": "Muajin e Kaluar",
"lastThreeMonthsText": "3 Muajt e Fundit",
"allTimeText": "Të Gjitha",
"customRangeText": "Interval i Përshtatur",
"startDateInputPlaceholder": "Data e fillimit",
"EndDateInputPlaceholder": "Data e përfundimit",
"filterButton": "Filtro",
"membersTitle": "Anëtarët",
"includeArchivedButton": "Përfshij Projektet e Arkivuara",
"exportButton": "Eksporto",
"excelButton": "Excel",
"searchByNameInputPlaceholder": "Kërko sipas emrit",
"memberColumn": "Anëtari",
"tasksProgressColumn": "Progresi i Detyrave",
"tasksAssignedColumn": "Detyrat e Caktuara",
"completedTasksColumn": "Detyrat e Përfunduara",
"overdueTasksColumn": "Detyrat e Vonuara",
"ongoingTasksColumn": "Detyrat në Vazhdim",
"tasksAssignedColumnTooltip": "Detyrat e caktuara në intervalin e zgjedhur",
"overdueTasksColumnTooltip": "Detyrat e vonuara deri në fund të intervalit të zgjedhur",
"completedTasksColumnTooltip": "Detyrat e përfunduara në intervalin e zgjedhur",
"ongoingTasksColumnTooltip": "Detyrat e filluara por jo të përfunduara ende",
"todoText": "Për Të Bërë",
"doingText": "Duke bërë",
"doneText": "E Përfunduar"
}

View File

@@ -0,0 +1,39 @@
{
"exportButton": "Eksporto",
"projectsButton": "Projektet",
"membersButton": "Anëtarët",
"searchByNameInputPlaceholder": "Kërko sipas emrit",
"overviewTab": "Përmbledhje",
"projectsTab": "Projektet",
"membersTab": "Anëtarët",
"projectsByStatusText": "Projektet Sipas Statusit",
"projectsByCategoryText": "Projektet Sipas Kategorisë",
"projectsByHealthText": "Projektet Sipas Gjendjes",
"projectsText": "Projektet",
"allText": "Të Gjitha",
"cancelledText": "Anuluar",
"blockedText": "E Bllokuar",
"onHoldText": "Në Pritje",
"proposedText": "E Propozuar",
"inPlanningText": "Në Planifikim",
"inProgressText": "Në Progres",
"completedText": "E Përfunduar",
"continuousText": "E Vazhdueshme",
"notSetText": "Pa Caktuar",
"needsAttentionText": "Kërkon Vëmendje",
"atRiskText": "Në Rrezik",
"goodText": "Në Rregull",
"nameColumn": "Emri",
"emailColumn": "Email",
"projectsColumn": "Projektet",
"tasksColumn": "Detyrat",
"overdueTasksColumn": "Detyrat e Vonuara",
"completedTasksColumn": "Detyrat e Përfunduara",
"ongoingTasksColumn": "Detyrat në Vazhdim"
}

View File

@@ -0,0 +1,25 @@
{
"overviewTitle": "Përmbledhje",
"includeArchivedButton": "Përfshij Projektet e Arkivuara",
"teamCount": "Ekip",
"teamCountPlural": "Ekipe",
"projectCount": "Projekt",
"projectCountPlural": "Projekte",
"memberCount": "Anëtar",
"memberCountPlural": "Anëtarë",
"activeProjectCount": "Projekt Aktiv",
"activeProjectCountPlural": "Projekte Aktive",
"overdueProjectCount": "Projekt i Vonuar",
"overdueProjectCountPlural": "Projekte të Vonuara",
"unassignedMemberCount": "Anëtar i Pacaktuar",
"unassignedMemberCountPlural": "Anëtarë të Pacaktuar",
"memberWithOverdueTaskCount": "Anëtar me Detyrë të Vonuar",
"memberWithOverdueTaskCountPlural": "Anëtarë me Detyra të Vonuara",
"teamsText": "Ekipet",
"nameColumn": "Emri",
"projectsColumn": "Projektet",
"membersColumn": "Anëtarët"
}

View File

@@ -0,0 +1,59 @@
{
"exportButton": "Eksporto",
"membersButton": "Anëtarët",
"tasksButton": "Detyrat",
"searchByNameInputPlaceholder": "Kërko sipas emrit",
"overviewTab": "Përmbledhje",
"membersTab": "Anëtarët",
"tasksTab": "Detyrat",
"completedTasksText": "Detyrat e Përfunduara",
"incompleteTasksText": "Detyrat e Papërfunduara",
"overdueTasksText": "Detyrat e Vonuara",
"allocatedHoursText": "Orët e Alokuara",
"loggedHoursText": "Orët e Regjistruara",
"tasksText": "Detyrat",
"allText": "Të Gjitha",
"tasksByStatusText": "Detyrat Sipas Statusit",
"tasksByPriorityText": "Detyrat Sipas Prioritetit",
"tasksByDueDateText": "Detyrat Sipas Afatit",
"todoText": "Për Të Bërë",
"doingText": "Duke bërë",
"doneText": "E Përfunduar",
"lowText": "I Ulët",
"mediumText": "I Mesëm",
"highText": "I Lartë",
"completedText": "E Përfunduar",
"upcomingText": "Në Ardhje",
"overdueText": "E Vonuar",
"noDueDateText": "Pa Afat",
"nameColumn": "Emri",
"tasksCountColumn": "Numri i Detyrave",
"completedTasksColumn": "Detyrat e Përfunduara",
"incompleteTasksColumn": "Detyrat e Papërfunduara",
"overdueTasksColumn": "Detyrat e Vonuara",
"contributionColumn": "Kontributi",
"progressColumn": "Progresi",
"loggedTimeColumn": "Koha e Regjistruar",
"taskColumn": "Detyra",
"projectColumn": "Projekti",
"statusColumn": "Statusi",
"priorityColumn": "Prioriteti",
"phaseColumn": "Faza",
"dueDateColumn": "Afati",
"completedDateColumn": "Data e Përfundimit",
"estimatedTimeColumn": "Koha e Vlerësuar",
"overloggedTimeColumn": "Koha e Tepërt",
"completedOnColumn": "Përfunduar Më",
"daysOverdueColumn": "Ditë vonim",
"groupByText": "Grupo Sipas:",
"statusText": "Statusi",
"priorityText": "Prioriteti",
"phaseText": "Faza"
}

View File

@@ -0,0 +1,35 @@
{
"searchByNamePlaceholder": "Kërko sipas emrit",
"searchByCategoryPlaceholder": "Kërko sipas kategorisë",
"statusText": "Statusi",
"healthText": "Gjendja",
"categoryText": "Kategoria",
"projectManagerText": "Menaxheri i Projektit",
"showFieldsText": "Shfaq fushat",
"cancelledText": "Anuluar",
"blockedText": "E bllokuar",
"onHoldText": "Në pritje",
"proposedText": "E propozuar",
"inPlanningText": "Në planifikim",
"inProgressText": "Në progres",
"completedText": "E përfunduar",
"continuousText": "E vazhdueshme",
"notSetText": "Pa caktuar",
"needsAttentionText": "Kërkon vëmendje",
"atRiskText": "Në rrezik",
"goodText": "Në rregull",
"nameText": "Projekti",
"estimatedVsActualText": "Vlerësuar vs Aktual",
"tasksProgressText": "Progresi i detyrave",
"lastActivityText": "Aktiviteti i fundit",
"datesText": "Datat e Fillimit/Përfundimit",
"daysLeftText": "Ditë të mbetura/vonuar",
"projectHealthText": "Gjendja e projektit",
"projectUpdateText": "Përditësimi i projektit",
"clientText": "Klienti",
"teamText": "Ekipi"
}

View File

@@ -0,0 +1,52 @@
{
"projectCount": "Projekt",
"projectCountPlural": "Projekte",
"includeArchivedButton": "Përfshij Projektet e Arkivuara",
"exportButton": "Eksporto",
"excelButton": "Excel",
"projectColumn": "Projekti",
"estimatedVsActualColumn": "Vlerësuar vs Aktual",
"tasksProgressColumn": "Progresi i Detyrave",
"lastActivityColumn": "Aktiviteti i Fundit",
"statusColumn": "Statusi",
"datesColumn": "Data e Fillimit/Përfundimit",
"daysLeftColumn": "Ditë të Mbetura/Vonuar",
"projectHealthColumn": "Gjendja e Projektit",
"categoryColumn": "Kategoria",
"projectUpdateColumn": "Përditësimi i Projektit",
"clientColumn": "Klienti",
"teamColumn": "Ekipi",
"projectManagerColumn": "Menaxheri i Projektit",
"openButton": "Hap",
"estimatedText": "Vlerësuar",
"actualText": "Aktual",
"todoText": "Për të Bërë",
"doingText": "duke bërë",
"doneText": "E Përfunduar",
"cancelledText": "Anuluar",
"blockedText": "E Bllokuar",
"onHoldText": "Në Pritje",
"proposedText": "E Propozuar",
"inPlanningText": "Në Planifikim",
"inProgressText": "Në Progres",
"completedText": "E Përfunduar",
"continuousText": "E Vazhdueshme",
"daysLeftText": "ditë të mbetura",
"dayLeftText": "ditë e mbetur",
"daysOverdueText": "ditë vonuar",
"notSetText": "Pa Caktuar",
"needsAttentionText": "Kërkon Vëmendje",
"atRiskText": "Në Rrezik",
"goodText": "Në Rregull",
"setCategoryText": "Cakto Kategorinë",
"searchByNameInputPlaceholder": "Kërko sipas emrit",
"todayText": "Sot"
}

View File

@@ -0,0 +1,8 @@
{
"overview": "Përmbledhje",
"projects": "Projektet",
"members": "Anëtarët",
"timeReports": "Raportet e Kohës",
"estimateVsActual": "Vlerësimi vs Aktual",
"currentOrganizationTooltip": "Organizata aktuale"
}

View File

@@ -0,0 +1,39 @@
{
"today": "Sot",
"week": "Javë",
"month": "Muaj",
"settings": "Cilësimet",
"workingDays": "Ditët e punës",
"monday": "E hënë",
"tuesday": "E martë",
"wednesday": "E mërkurë",
"thursday": "E enjte",
"friday": "E premte",
"saturday": "E shtunë",
"sunday": "E diel",
"workingHours": "Orët e punës",
"hours": "Orë",
"saveButton": "Ruaj",
"totalAllocation": "Alokimi Total",
"timeLogged": "Koha e Regjistruar",
"remainingTime": "Koha e Mbetur",
"total": "Total",
"perDay": "Në Ditë",
"tasks": "detyra",
"startDate": "Data e Fillimit",
"endDate": "Data e Përfundimit",
"hoursPerDay": "Orë Në Ditë",
"totalHours": "Orë Totale",
"deleteButton": "Fshi",
"cancelButton": "Anulo",
"tabTitle": "Detyra pa Data Fillimi & Përfundimi",
"allocatedTime": "Koha e alokuar",
"totalLogged": "Total i Regjistruar",
"loggedBillable": "Regjistruar Fakturueshme",
"loggedNonBillable": "Regjistruar Jo Fakturueshme"
}

View File

@@ -0,0 +1,10 @@
{
"categoryColumn": "Kategoria",
"deleteConfirmationTitle": "Jeni të sigurt?",
"deleteConfirmationOk": "Po",
"deleteConfirmationCancel": "Anulo",
"associatedTaskColumn": "Projektet e Lidhura",
"searchPlaceholder": "Kërko sipas emrit",
"emptyText": "Kategoritë mund të krijohen gjatë përditësimit ose krijimit të projekteve.",
"colorChangeTooltip": "Klikoni për të ndryshuar ngjyrën"
}

View File

@@ -0,0 +1,15 @@
{
"title": "Ndrysho Fjalëkalimin",
"currentPassword": "Fjalëkalimi Aktual",
"newPassword": "Fjalëkalimi i Ri",
"confirmPassword": "Konfirmo Fjalëkalimin",
"currentPasswordPlaceholder": "Vendosni fjalëkalimin aktual",
"newPasswordPlaceholder": "Fjalëkalimi i Ri",
"confirmPasswordPlaceholder": "Konfirmo Fjalëkalimin",
"currentPasswordRequired": "Ju lutemi vendosni fjalëkalimin aktual!",
"newPasswordRequired": "Ju lutemi vendosni fjalëkalimin e ri!",
"passwordValidationError": "Fjalëkalimi duhet të përmbajë të paktën 8 karaktere, me një shkronjë të madhe, një numër dhe një simbol.",
"passwordMismatch": "Fjalëkalimet nuk përputhen!",
"passwordRequirements": "Fjalëkalimi i ri duhet të jetë së paku 8 karaktere, me një shkronjë të madhe, një numër dhe një simbol.",
"updateButton": "Përditëso Fjalëkalimin"
}

View File

@@ -0,0 +1,22 @@
{
"nameColumn": "Emri",
"projectColumn": "Projekti",
"noProjectsAvailable": "Nuk ka projekte të disponueshme",
"deleteConfirmationTitle": "Jeni i sigurt?",
"deleteConfirmationOk": "Po",
"deleteConfirmationCancel": "Anulo",
"searchPlaceholder": "Kërko sipas emrit",
"createClient": "Krijo Klient",
"pinTooltip": "Klikoni për ta fiksuar në menynë kryesore",
"createClientDrawerTitle": "Krijo Klient",
"updateClientDrawerTitle": "Përditëso Klientin",
"nameLabel": "Emri",
"namePlaceholder": "Emri",
"nameRequiredError": "Ju lutemi shkruani një Emër",
"createButton": "Krijo",
"updateButton": "Përditëso",
"createClientSuccessMessage": "Klienti u krijua me sukses!",
"createClientErrorMessage": "Krijimi i klientit dështoi!",
"updateClientSuccessMessage": "Klienti u përditësua me sukses!",
"updateClientErrorMessage": "Përditësimi i klientit dështoi!"
}

View File

@@ -0,0 +1,20 @@
{
"nameColumn": "Emri",
"deleteConfirmationTitle": "Jeni i sigurt?",
"deleteConfirmationOk": "Po",
"deleteConfirmationCancel": "Anulo",
"searchPlaceholder": "Kërko sipas emrit",
"createJobTitleButton": "Krijo Titull Pune",
"pinTooltip": "Klikoni për ta fiksuar në menynë kryesore",
"createJobTitleDrawerTitle": "Krijo Titull Pune",
"updateJobTitleDrawerTitle": "Përditëso Titullin e Punës",
"nameLabel": "Emri",
"namePlaceholder": "Emri",
"nameRequiredError": "Ju lutemi shkruani një Emër",
"createButton": "Krijo",
"updateButton": "Përditëso",
"createJobTitleSuccessMessage": "Titulli i punës u krijua me sukses!",
"createJobTitleErrorMessage": "Krijimi i titullit të punës dështoi!",
"updateJobTitleSuccessMessage": "Titulli i punës u përditësua me sukses!",
"updateJobTitleErrorMessage": "Përditësimi i titullit të punës dështoi!"
}

View File

@@ -0,0 +1,11 @@
{
"labelColumn": "Etiketa",
"deleteConfirmationTitle": "Jeni i sigurt?",
"deleteConfirmationOk": "Po",
"deleteConfirmationCancel": "Anulo",
"associatedTaskColumn": "Numri i Detyrave të Lidhura",
"searchPlaceholder": "Kërko sipas emrit",
"emptyText": "Etiketat mund të krijohen gjatë përditësimit ose krijimit të detyrave.",
"pinTooltip": "Klikoni për ta fiksuar në menynë kryesore",
"colorChangeTooltip": "Klikoni për të ndryshuar ngjyrën"
}

View File

@@ -0,0 +1,7 @@
{
"language": "Gjuha",
"language_required": "Gjuha është e detyrueshme",
"time_zone": "Zona kohore",
"time_zone_required": "Zona kohore është e detyrueshme",
"save_changes": "Ruaj Ndryshimet"
}

View File

@@ -0,0 +1,11 @@
{
"title": "Cilësimet e Njoftimeve",
"emailTitle": "Më dërgo njoftime me email",
"emailDescription": "Kjo përfshin caktimet e reja të detyrave",
"dailyDigestTitle": "Më dërgo një përmbledhje ditore",
"dailyDigestDescription": "Çdo mbrëmje, do të merrni një përmbledhje të aktivitetit të fundit në detyra.",
"popupTitle": "Shfaq njoftimet në kompjuterin tim kur Worklenz është i hapur",
"popupDescription": "Njoftimet e shfaqura mund të çaktivizohen nga shfletuesi juaj. Ndryshoni cilësimet e shfletuesit për t'i lejuar ato.",
"unreadItemsTitle": "Shfaq numrin e artikujve të palexuar",
"unreadItemsDescription": "Do të shihni numërimin për çdo njoftim."
}

View File

@@ -0,0 +1,14 @@
{
"uploadError": "Mund të ngarkoni vetëm skedarë JPG/PNG!",
"uploadSizeError": "Imazhi duhet të jetë më i vogël se 2MB!",
"upload": "Ngarko",
"nameLabel": "Emri",
"nameRequiredError": "Emri është i detyrueshëm",
"emailLabel": "Email",
"emailRequiredError": "Email-i është i detyrueshëm",
"saveChanges": "Ruaj Ndryshimet",
"profileJoinedText": "U bashkua një muaj më parë",
"profileLastUpdatedText": "Përditësuar një muaj më parë",
"avatarTooltip": "Klikoni për të ngarkuar një avatar",
"title": "Cilësimet e Profilit"
}

View File

@@ -0,0 +1,8 @@
{
"nameColumn": "Emri",
"editToolTip": "Modifiko",
"deleteToolTip": "Fshi",
"confirmText": "Jeni i sigurt?",
"okText": "Po",
"cancelText": "Anulo"
}

View File

@@ -0,0 +1,14 @@
{
"profile": "Profili",
"notifications": "Njoftimet",
"clients": "Klientët",
"job-titles": "Tituj Pune",
"labels": "Etiketa",
"categories": "Kategoritë",
"project-templates": "Shabllonet e Projekteve",
"task-templates": "Shabllonet e Detyrave",
"team-members": "Anëtarët e Ekipit",
"teams": "Ekipet",
"change-password": "Ndrysho Fjalëkalimin",
"language-and-region": "Gjuha dhe Rajoni"
}

View File

@@ -0,0 +1,9 @@
{
"nameColumn": "Emri",
"createdColumn": "Krijuar",
"editToolTip": "Redakto",
"deleteToolTip": "Fshi",
"confirmText": "Jeni i sigurt?",
"okText": "Po",
"cancelText": "Anulo"
}

View File

@@ -0,0 +1,47 @@
{
"title": "Anëtarët e Ekipit",
"nameColumn": "Emri",
"projectsColumn": "Projektet",
"emailColumn": "Email",
"teamAccessColumn": "Qasja në Ekip",
"memberCount": "Anëtar",
"membersCountPlural": "Anëtarë",
"searchPlaceholder": "Kërko anëtarë sipas emrit",
"pinTooltip": "Rifresko listën e anëtarëve",
"addMemberButton": "Shto Anëtar të Ri",
"editTooltip": "Modifiko anëtarin",
"deactivateTooltip": "Çaktivizo anëtarin",
"activateTooltip": "Aktivizo anëtarin",
"deleteTooltip": "Fshi anëtarin",
"confirmDeleteTitle": "Jeni i sigurt që doni të fshini këtë anëtar?",
"confirmActivateTitle": "Jeni i sigurt që doni të ndryshoni statusin e këtij anëtari?",
"okText": "Po, vazhdo",
"cancelText": "Jo, anulo",
"deactivatedText": "(Aktualisht i çaktivizuar)",
"pendingInvitationText": "(Ftesë në pritje)",
"addMemberDrawerTitle": "Shto Anëtar të Ri në Ekip",
"updateMemberDrawerTitle": "Përditëso Anëtarin e Ekipit",
"addMemberEmailHint": "Anëtarët do të shtohen në ekip pavarësisht nga statusi i pranimit të ftesës",
"memberEmailLabel": "Email(o)",
"memberEmailPlaceholder": "Vendos adresën email të anëtarit të ekipit",
"memberEmailRequiredError": "Ju lutemi vendosni një adresë email të vlefshme",
"jobTitleLabel": "Titulli i Punës",
"jobTitlePlaceholder": "Zgjidh ose kërko titull pune (Opsionale)",
"memberAccessLabel": "Niveli i Qasjes",
"addToTeamButton": "Shto Anëtar në Ekip",
"updateButton": "Ruaj Ndryshimet",
"resendInvitationButton": "Dërgo Përsëri Email-in e Ftesës",
"invitationSentSuccessMessage": "Ftesa për ekip u dërgua me sukses!",
"createMemberSuccessMessage": "Anëtari i ri i ekipit u shtua me sukses!",
"createMemberErrorMessage": "Dështoi shtimi i anëtarit të ri. Ju lutemi provoni përsëri.",
"updateMemberSuccessMessage": "Anëtari i ekipit u përditësua me sukses!",
"updateMemberErrorMessage": "Dështoi përditësimi i anëtarit. Ju lutemi provoni përsëri.",
"memberText": "Anëtar",
"adminText": "Administrues",
"ownerText": "Pronar i Ekipit",
"addedText": "Shtuar",
"updatedText": "Përditësuar",
"noResultFound": "Shkruani një adresë email dhe shtypni Enter...",
"jobTitlesFetchError": "Dështoi marrja e titujve të punës",
"invitationResent": "Ftesa u dërgua sërish me sukses!"
}

View File

@@ -0,0 +1,16 @@
{
"title": "Ekipet",
"team": "Ekip",
"teams": "Ekipet",
"name": "Emri",
"created": "Krijuar",
"ownsBy": "I përket",
"edit": "Ndrysho",
"editTeam": "Ndrysho Ekipin",
"pinTooltip": "Kliko për ta fiksuar në menunë kryesore",
"editTeamName": "Ndrysho Emrin e Ekipit",
"updateName": "Përditëso Emrin",
"namePlaceholder": "Emri",
"nameRequired": "Ju lutem shkruani një Emër",
"updateFailed": "Ndryshimi i emrit të ekipit dështoi!"
}

View File

@@ -0,0 +1,29 @@
{
"details": {
"task-key": "Çelësi i Detyrës",
"phase": "Faza",
"assignees": "Përgjegjësit",
"due-date": "Data e Përfundimit",
"time-estimation": "Vlerësimi i Kohës",
"priority": "Prioriteti",
"labels": "Etiketa",
"billable": "Fakturueshme",
"notify": "Njofto",
"when-done-notify": "Kur të përfundojë, njofto",
"start-date": "Data e Fillimit",
"end-date": "Data e Përfundimit",
"hide-start-date": "Fshih Datën e Fillimit",
"show-start-date": "Shfaq Datën e Fillimit",
"hours": "Orë",
"minutes": "Minuta"
},
"description": {
"title": "Përshkrimi",
"placeholder": "Shtoni një përshkrim më të detajuar..."
},
"subTasks": {
"title": "Nën-Detyrat",
"add-sub-task": "+ Shto Nën-Detyrë",
"refresh-sub-tasks": "Rifresko Nën-Detyrat"
}
}

View File

@@ -0,0 +1,123 @@
{
"taskHeader": {
"taskNamePlaceholder": "Shkruani Detyrën tuaj",
"deleteTask": "Fshi Detyrën"
},
"taskInfoTab": {
"title": "Informacioni",
"details": {
"title": "Detajet",
"task-key": "Çelësi i Detyrës",
"phase": "Faza",
"assignees": "Të Caktuar",
"due-date": "Data e Përfundimit",
"time-estimation": "Vlerësimi i Kohës",
"priority": "Prioriteti",
"labels": "Etiketat",
"billable": "E Faturueshme",
"notify": "Njofto",
"when-done-notify": "Kur përfundon, njofto",
"start-date": "Data e Fillimit",
"end-date": "Data e Përfundimit",
"hide-start-date": "Fshih Datën e Fillimit",
"show-start-date": "Shfaq Datën e Fillimit",
"hours": "Orë",
"minutes": "Minuta",
"progressValue": "Vlera e Progresit",
"progressValueTooltip": "Vendosni përqindjen e progresit (0-100%)",
"progressValueRequired": "Ju lutemi vendosni një vlerë progresi",
"progressValueRange": "Progresi duhet të jetë midis 0 dhe 100",
"taskWeight": "Pesha e Detyrës",
"taskWeightTooltip": "Vendosni peshën e kësaj nëndetyre (përqindje)",
"taskWeightRequired": "Ju lutemi vendosni një peshë detyre",
"taskWeightRange": "Pesha duhet të jetë midis 0 dhe 100",
"recurring": "E Përsëritur"
},
"labels": {
"labelInputPlaceholder": "Kërko ose krijo",
"labelsSelectorInputTip": "Shtyp Enter për të krijuar"
},
"description": {
"title": "Përshkrimi",
"placeholder": "Shto një përshkrim më të detajuar..."
},
"subTasks": {
"title": "Nëndetyrat",
"addSubTask": "Shto Nëndetyrë",
"addSubTaskInputPlaceholder": "Shkruani detyrën tuaj dhe shtypni enter",
"refreshSubTasks": "Rifresko Nëndetyrat",
"edit": "Modifiko",
"delete": "Fshi",
"confirmDeleteSubTask": "Jeni i sigurt që doni të fshini këtë nëndetyrë?",
"deleteSubTask": "Fshi Nëndetyrën"
},
"dependencies": {
"title": "Varësitë",
"addDependency": "+ Shto varësi të re",
"blockedBy": "Bllokuar nga",
"searchTask": "Shkruani për të kërkuar detyrë",
"noTasksFound": "Nuk u gjetën detyra",
"confirmDeleteDependency": "Jeni i sigurt që doni të fshini?"
},
"attachments": {
"title": "Bashkëngjitjet",
"chooseOrDropFileToUpload": "Zgjidhni ose hidhni skedar për të ngarkuar",
"uploading": "Duke ngarkuar..."
},
"comments": {
"title": "Komentet",
"addComment": "+ Shto koment të ri",
"noComments": "Ende pa komente. Bëhu i pari që komenton!",
"delete": "Fshi",
"confirmDeleteComment": "Jeni i sigurt që doni të fshini këtë koment?",
"addCommentPlaceholder": "Shto një koment...",
"cancel": "Anulo",
"commentButton": "Komento",
"attachFiles": "Bashkëngjit skedarë",
"addMoreFiles": "Shto më shumë skedarë",
"selectedFiles": "Skedarët e Zgjedhur (Deri në 25MB, Maksimumi {count})",
"maxFilesError": "Mund të ngarkoni maksimum {count} skedarë",
"processFilesError": "Dështoi përpunimi i skedarëve",
"addCommentError": "Ju lutemi shtoni një koment ose bashkëngjitni skedarë",
"createdBy": "Krijuar {{time}} nga {{user}}",
"updatedTime": "Përditësuar {{time}}"
},
"searchInputPlaceholder": "Kërko sipas emrit",
"pendingInvitation": "Ftesë në Pritje"
},
"taskTimeLogTab": {
"title": "Regjistri i Kohës",
"addTimeLog": "Shto regjistrim të ri kohe",
"totalLogged": "Totali i Regjistruar",
"exportToExcel": "Eksporto në Excel",
"noTimeLogsFound": "Nuk u gjetën regjistra kohe",
"timeLogForm": {
"date": "Data",
"startTime": "Koha e Fillimit",
"endTime": "Koha e Përfundimit",
"workDescription": "Përshkrimi i Punës",
"descriptionPlaceholder": "Shto një përshkrim",
"logTime": "Regjistro kohën",
"updateTime": "Përditëso kohën",
"cancel": "Anulo",
"selectDateError": "Ju lutemi zgjidhni një datë",
"selectStartTimeError": "Ju lutemi zgjidhni kohën e fillimit",
"selectEndTimeError": "Ju lutemi zgjidhni kohën e përfundimit",
"endTimeAfterStartError": "Koha e përfundimit duhet të jetë pas kohës së fillimit"
}
},
"taskActivityLogTab": {
"title": "Regjistri i Aktivitetit",
"add": "SHTO",
"remove": "HIQE",
"none": "Asnjë",
"weight": "Pesha",
"createdTask": "krijoi detyrën."
},
"taskProgress": {
"markAsDoneTitle": "Shëno Detyrën si të Kryer?",
"confirmMarkAsDone": "Po, shëno si të kryer",
"cancelMarkAsDone": "Jo, mbaj statusin aktual",
"markAsDoneDescription": "Keni vendosur progresin në 100%. Doni të përditësoni statusin e detyrës në \"Kryer\"?"
}
}

View File

@@ -0,0 +1,85 @@
{
"searchButton": "Kërko",
"resetButton": "Rivendos",
"searchInputPlaceholder": "Kërko sipas emrit",
"sortText": "Rendit",
"statusText": "Statusi",
"phaseText": "Faza",
"memberText": "Anëtarët",
"assigneesText": "Përgjegjësit",
"priorityText": "Prioriteti",
"labelsText": "Etiketa",
"membersText": "Anëtarët",
"groupByText": "Grupo sipas",
"showArchivedText": "Shfaq të arkivuara",
"showFieldsText": "Shfaq fushat",
"keyText": "Çelësi",
"taskText": "Detyra",
"descriptionText": "Përshkrimi",
"phasesText": "Fazat",
"listText": "Listë",
"progressText": "Progresi",
"timeTrackingText": "Gjurmimi i Kohës",
"timetrackingText": "Gjurmimi i Kohës",
"estimationText": "Vlerësimi",
"startDateText": "Data e Fillimit",
"startdateText": "Data e Fillimit",
"endDateText": "Data e Përfundimit",
"dueDateText": "Afati",
"duedateText": "Afati",
"completedDateText": "Data e Përfundimit",
"completeddateText": "Data e Përfundimit",
"createdDateText": "Data e Krijimit",
"createddateText": "Data e Krijimit",
"lastUpdatedText": "Përditësuar Së Fundi",
"lastupdatedText": "Përditësuar Së Fundi",
"reporterText": "Raportuesi",
"dueTimeText": "Koha e Afatit",
"duetimeText": "Koha e Afatit",
"lowText": "I ulët",
"mediumText": "I mesëm",
"highText": "I lartë",
"createStatusButtonTooltip": "Cilësimet e statusit",
"configPhaseButtonTooltip": "Cilësimet e fazës",
"noLabelsFound": "Nuk u gjetën etiketa",
"addStatusButton": "Shto Status",
"addPhaseButton": "Shto Fazë",
"createStatus": "Krijo Status",
"name": "Emri",
"category": "Kategoria",
"selectCategory": "Zgjidh një kategori",
"pleaseEnterAName": "Ju lutemi vendosni një emër",
"pleaseSelectACategory": "Ju lutemi zgjidhni një kategori",
"create": "Krijo",
"searchTasks": "Kërko detyrat...",
"searchPlaceholder": "Kërko...",
"fieldsText": "Fushat",
"loadingFilters": "Duke ngarkuar filtrat...",
"noOptionsFound": "Nuk u gjetën opsione",
"filtersActive": "filtra aktiv",
"filterActive": "filtër aktiv",
"clearAll": "Pastro të gjitha",
"clearing": "Duke pastruar...",
"cancel": "Anulo",
"search": "Kërko",
"groupedBy": "Grupuar sipas",
"manageStatuses": "Menaxho Statuset",
"managePhases": "Menaxho Fazat",
"dragToReorderStatuses": "Zvarrit statuset për t'i rirenditur. Çdo status mund të ketë një kategori të ndryshme.",
"enterNewStatusName": "Shkruani emrin e statusit të ri...",
"addStatus": "Shto Status",
"noStatusesFound": "Nuk u gjetën statuse. Krijoni statusin tuaj të parë më sipër.",
"deleteStatus": "Fshi Statusin",
"deleteStatusConfirm": "Jeni të sigurt që doni të fshini këtë status? Ky veprim nuk mund të zhbëhet.",
"rename": "Riemëro",
"delete": "Fshi",
"enterStatusName": "Shkruani emrin e statusit",
"selectCategory": "Zgjidh kategorinë",
"close": "Mbyll"
}

View File

@@ -0,0 +1,136 @@
{
"keyColumn": "Çelësi",
"taskColumn": "Detyra",
"descriptionColumn": "Përshkrimi",
"progressColumn": "Progresi",
"membersColumn": "Anëtarët",
"assigneesColumn": "Përgjegjësit",
"labelsColumn": "Etiketa",
"phasesColumn": "Fazat",
"phaseColumn": "Faza",
"statusColumn": "Statusi",
"priorityColumn": "Prioriteti",
"timeTrackingColumn": "Gjurmimi i Kohës",
"timetrackingColumn": "Gjurmimi i Kohës",
"estimationColumn": "Vlerësimi",
"startDateColumn": "Data e Fillimit",
"startdateColumn": "Data e Fillimit",
"dueDateColumn": "Data e Afatit",
"duedateColumn": "Data e Afatit",
"completedDateColumn": "Data e Përfundimit",
"completeddateColumn": "Data e Përfundimit",
"createdDateColumn": "Data e Krijimit",
"createddateColumn": "Data e Krijimit",
"lastUpdatedColumn": "Përditësuar Së Fundi",
"lastupdatedColumn": "Përditësuar Së Fundi",
"reporterColumn": "Raportuesi",
"dueTimeColumn": "Koha e Afatit",
"todoSelectorText": "Për të Bërë",
"doingSelectorText": "Duke bërë",
"doneSelectorText": "E Përfunduar",
"lowSelectorText": "I ulët",
"mediumSelectorText": "I mesëm",
"highSelectorText": "I lartë",
"selectText": "Zgjidh",
"labelsSelectorInputTip": "Shtyp Enter për të krijuar!",
"addTaskText": "Shto Detyrë",
"addSubTaskText": "+ Shto Nën-Detyrë",
"noTasksInGroup": "Nuk ka detyra në këtë grup",
"addTaskInputPlaceholder": "Shkruaj detyrën dhe shtyp Enter",
"openButton": "Hap",
"okButton": "Në rregull",
"noLabelsFound": "Nuk u gjetën etiketa",
"searchInputPlaceholder": "Kërko ose krijo",
"assigneeSelectorInviteButton": "Fto një anëtar të ri me email",
"labelInputPlaceholder": "Kërko ose krijo",
"searchLabelsPlaceholder": "Kërko etiketa...",
"createLabelButton": "Krijo \"{{name}}\"",
"manageLabelsPath": "Cilësimet → Etiketat",
"pendingInvitation": "Ftesë në Pritje",
"contextMenu": {
"assignToMe": "Cakto mua",
"moveTo": "Zhvendos në",
"unarchive": "Ç'arkivizo",
"archive": "Arkivizo",
"convertToSubTask": "Shndërro në Nën-Detyrë",
"convertToTask": "Shndërro në Detyrë",
"delete": "Fshi",
"searchByNameInputPlaceholder": "Kërko sipas emrit"
},
"setDueDate": "Cakto datën e afatit",
"setStartDate": "Cakto datën e fillimit",
"clearDueDate": "Pastro datën e afatit",
"clearStartDate": "Pastro datën e fillimit",
"dueDatePlaceholder": "Data e afatit",
"startDatePlaceholder": "Data e fillimit",
"emptyStates": {
"noTaskGroups": "Nuk u gjetën grupe detyrash",
"noTaskGroupsDescription": "Detyrat do të shfaqen këtu kur krijohen ose kur aplikohen filtra.",
"errorPrefix": "Gabim:",
"dragTaskFallback": "Detyrë"
},
"customColumns": {
"addCustomColumn": "Shto një kolonë të personalizuar",
"customColumnHeader": "Kolona e Personalizuar",
"customColumnSettings": "Cilësimet e kolonës së personalizuar",
"noCustomValue": "Asnjë vlerë",
"peopleField": "Fusha e njerëzve",
"noDate": "Asnjë datë",
"unsupportedField": "Lloj fushe i pambështetur",
"modal": {
"addFieldTitle": "Shto fushë",
"editFieldTitle": "Redakto fushën",
"fieldTitle": "Titulli i fushës",
"fieldTitleRequired": "Titulli i fushës është i kërkuar",
"columnTitlePlaceholder": "Titulli i kolonës",
"type": "Lloji",
"deleteConfirmTitle": "Jeni i sigurt që doni të fshini këtë kolonë të personalizuar?",
"deleteConfirmDescription": "Kjo veprim nuk mund të zhbëhet. Të gjitha të dhënat e lidhura me këtë kolonë do të fshihen përgjithmonë.",
"deleteButton": "Fshi",
"cancelButton": "Anulo",
"createButton": "Krijo",
"updateButton": "Përditëso",
"createSuccessMessage": "Kolona e personalizuar u krijua me sukses",
"updateSuccessMessage": "Kolona e personalizuar u përditësua me sukses",
"deleteSuccessMessage": "Kolona e personalizuar u fshi me sukses",
"deleteErrorMessage": "Dështoi në fshirjen e kolonës së personalizuar",
"createErrorMessage": "Dështoi në krijimin e kolonës së personalizuar",
"updateErrorMessage": "Dështoi në përditësimin e kolonës së personalizuar"
},
"fieldTypes": {
"people": "Njerëz",
"number": "Numër",
"date": "Data",
"selection": "Zgjedhje",
"checkbox": "Kutia e kontrollit",
"labels": "Etiketat",
"key": "Çelësi",
"formula": "Formula"
}
},
"indicators": {
"tooltips": {
"subtasks": "{{count}} nën-detyrë",
"subtasks_plural": "{{count}} nën-detyra",
"comments": "{{count}} koment",
"comments_plural": "{{count}} komente",
"attachments": "{{count}} bashkëngjitje",
"attachments_plural": "{{count}} bashkëngjitje",
"subscribers": "Detyra ka pajtues",
"dependencies": "Detyra ka varësi",
"recurring": "Detyrë përsëritëse"
}
}
}

View File

@@ -0,0 +1,21 @@
{
"noTasksInGroup": "Nuk ka detyra në këtë grup",
"noTasksInGroupDescription": "Shtoni një detyrë për të filluar",
"addFirstTask": "Shtoni detyrën tuaj të parë",
"openTask": "Hap",
"subtask": "nën-detyrë",
"subtasks": "nën-detyra",
"comment": "koment",
"comments": "komente",
"attachment": "bashkëngjitje",
"attachments": "bashkëngjitje",
"enterSubtaskName": "Shkruani emrin e nën-detyrës...",
"add": "Shto",
"cancel": "Anulo",
"renameGroup": "Riemërto Grupin",
"renameStatus": "Riemërto Statusin",
"renamePhase": "Riemërto Fazën",
"changeCategory": "Ndrysho Kategorinë",
"clickToEditGroupName": "Kliko për të ndryshuar emrin e grupit",
"enterGroupName": "Shkruani emrin e grupit"
}

View File

@@ -0,0 +1,11 @@
{
"createTaskTemplate": "Krijo Shabllon Detyre",
"editTaskTemplate": "Modifiko Shabllon Detyre",
"cancelText": "Anulo",
"saveText": "Ruaj",
"templateNameText": "Emri i Shabllonit",
"selectedTasks": "Detyrat e Përzgjedhura",
"removeTask": "Hiq",
"cancelButton": "Anulo",
"saveButton": "Ruaj"
}

View File

@@ -0,0 +1,26 @@
{
"taskSelected": "detyrë e zgjedhur",
"tasksSelected": "detyra të zgjedhura",
"changeStatus": "Ndrysho Statusin/ Prioritetin/ Fazat",
"changeLabel": "Ndrysho Etiketën",
"assignToMe": "Cakto mua",
"changeAssignees": "Ndrysho Përgjegjësit",
"archive": "Arkivo",
"unarchive": "Ç'arkivo",
"delete": "Fshi",
"moreOptions": "Më shumë opsione",
"deselectAll": "Zgjidhja të gjitha",
"status": "Statusi",
"priority": "Prioriteti",
"phase": "Faza",
"member": "Anëtar",
"createTaskTemplate": "Krijo Shabllon Detyre",
"apply": "Apliko",
"createLabel": "+ Krijo Etiketë",
"searchOrCreateLabel": "Kërko ose krijo etiketë...",
"hitEnterToCreate": "Shtyp Enter për të krijuar",
"labelExists": "Etiketa ekziston tashmë",
"pendingInvitation": "Ftesë në Pritje",
"noMatchingLabels": "Asnjë etiketë që përputhet",
"noLabels": "Asnjë etiketë"
}

View File

@@ -0,0 +1,19 @@
{
"title": "Modifiko Shabllon Detyre",
"cancelText": "Anulo",
"saveText": "Ruaj",
"templateNameText": "Emri i Shabllonit",
"selectedTasks": "Detyrat e Përzgjedhura",
"removeTask": "Hiq",
"description": "Përshkrimi",
"phase": "Faza",
"statuses": "Statuset",
"priorities": "Prioritetet",
"labels": "Etiketa",
"tasks": "Detyrat",
"noTemplateSelected": "Asnjë shabllon i përzgjedhur",
"noDescription": "Pa përshkrim",
"worklenzTemplates": "Shabllonet Worklenz",
"yourTemplatesLibrary": "Biblioteka Juaj",
"searchTemplates": "Kërko Shabllone"
}

View File

@@ -0,0 +1,23 @@
{
"bugTracking": "Gjurmimi i Gabimeve",
"construction": "Ndërtim",
"designCreative": "Dizajn & Kreativ",
"education": "Arsim",
"finance": "Financë",
"hrRecruiting": "Burime Njerëzore & Rekrutim",
"informationTechnology": "Teknologji Informacioni",
"legal": "Juridik",
"manufacturing": "Prodhim",
"marketing": "Marketing",
"nonprofit": "Jo-fitimprurës",
"personalUse": "Përdorim Personal",
"salesCRM": "Shitje & CRM",
"serviceConsulting": "Shërbime & Këshillim",
"softwareDevelopment": "Zhvillim Softueri",
"description": "Përshkrimi",
"phase": "Faza",
"statuses": "Statuset",
"priorities": "Prioritetet",
"labels": "Etiketa",
"tasks": "Detyrat"
}

View File

@@ -0,0 +1,44 @@
{
"includeArchivedProjects": "Përfshij Projektet e Arkivuara",
"export": "Eksporto",
"timeSheet": "Fletë Kohore",
"searchByName": "Kërko sipas emrit",
"selectAll": "Zgjidh të Gjitha",
"teams": "Ekipet",
"searchByProject": "Kërko sipas emrit të projektit",
"projects": "Projektet",
"searchByCategory": "Kërko sipas emrit të kategorisë",
"categories": "Kategoritë",
"billable": "Fakturueshme",
"nonBillable": "Jo Fakturueshme",
"total": "Total",
"projectsTimeSheet": "Fletë Kohore e Projekteve",
"loggedTime": "Koha e Regjistruar(orë)",
"exportToExcel": "Eksporto në Excel",
"logged": "regjistruar",
"for": "për",
"membersTimeSheet": "Fletë Kohore e Anëtarëve",
"member": "Anëtar",
"estimatedVsActual": "Vlerësuar vs Aktual",
"workingDays": "Ditë Pune",
"manDays": "Ditë Njeri",
"days": "Ditë",
"estimatedDays": "Ditë të Vlerësuara",
"actualDays": "Ditë Aktuale",
"noCategories": "Nuk u gjetën kategori",
"noCategory": "Pa Kategori",
"noProjects": "Nuk u gjetën projekte",
"noTeams": "Nuk u gjetën ekipe",
"noData": "Nuk u gjetën të dhëna"
}

View File

@@ -0,0 +1,5 @@
{
"title": "E paautorizuar!",
"subtitle": "Nuk jeni të autorizuar të hyni në këtë faqe",
"button": "Kthehu në Faqen Kryesore"
}

View File

@@ -0,0 +1,4 @@
{
"doesNotExistText": "Entschuldigung, die von Ihnen besuchte Seite existiert nicht.",
"backHomeButton": "Zurück zur Startseite"
}

View File

@@ -0,0 +1,31 @@
{
"continue": "Weiter",
"setupYourAccount": "Richten Sie Ihr Worklenz-Konto ein.",
"organizationStepTitle": "Organisation benennen",
"organizationStepLabel": "Wählen Sie einen Namen für Ihr Worklenz-Konto.",
"projectStepTitle": "Erstellen Sie Ihr erstes Projekt",
"projectStepLabel": "An welchem Projekt arbeiten Sie gerade?",
"projectStepPlaceholder": "z.B. Marketingplan",
"tasksStepTitle": "Erstellen Sie Ihre ersten Aufgaben",
"tasksStepLabel": "Geben Sie einige Aufgaben ein, die Sie in",
"tasksStepAddAnother": "Weitere hinzufügen",
"emailPlaceholder": "E-Mail-Adresse",
"invalidEmail": "Bitte geben Sie eine gültige E-Mail-Adresse ein",
"or": "oder",
"templateButton": "Aus Vorlage importieren",
"goBack": "Zurück",
"cancel": "Abbrechen",
"create": "Erstellen",
"templateDrawerTitle": "Aus Vorlagen auswählen",
"step3InputLabel": "Per E-Mail einladen",
"addAnother": "Weitere hinzufügen",
"skipForNow": "Jetzt überspringen",
"formTitle": "Erstellen Sie Ihre erste Aufgabe.",
"step3Title": "Laden Sie Ihr Team zur Zusammenarbeit ein",
"maxMembers": " (Sie können bis zu 5 Mitglieder einladen)",
"maxTasks": " (Sie können bis zu 5 Aufgaben erstellen)"
}

View File

@@ -0,0 +1,113 @@
{
"title": "Abrechnungen",
"currentBill": "Aktuelle Rechnung",
"configuration": "Konfiguration",
"currentPlanDetails": "Aktuelle Plan Details",
"upgradePlan": "Plan upgraden",
"cardBodyText01": "Kostenlose Testversion",
"cardBodyText02": "(Ihr Testplan läuft in 1 Monat 19 Tagen ab)",
"redeemCode": "Gutscheincode einlösen",
"accountStorage": "Kontospeicher",
"used": "Verwendet:",
"remaining": "Verbleibend:",
"charges": "Gebühren",
"tooltip": "Gebühren für den aktuellen Abrechnungszeitraum",
"description": "Beschreibung",
"billingPeriod": "Abrechnungszeitraum",
"billStatus": "Rechnungsstatus",
"perUserValue": "Pro Benutzer Wert",
"users": "Benutzer",
"amount": "Betrag",
"invoices": "Rechnungen",
"transactionId": "Transaktions-ID",
"transactionDate": "Transaktionsdatum",
"paymentMethod": "Zahlungsmethode",
"status": "Status",
"ltdUsers": "Sie können bis zu {{ltd_users}} Benutzer hinzufügen.",
"totalSeats": "Gesamte Plätze",
"availableSeats": "Verfügbare Plätze",
"addMoreSeats": "Weitere Plätze hinzufügen",
"drawerTitle": "Gutscheincode einlösen",
"label": "Gutscheincode",
"drawerPlaceholder": "Geben Sie Ihren Gutscheincode ein",
"redeemSubmit": "Einreichen",
"modalTitle": "Wählen Sie den besten Plan für Ihr Team",
"seatLabel": "Anzahl der Plätze",
"freePlan": "Kostenloser Plan",
"startup": "Startup",
"business": "Business",
"tag": "Am beliebtesten",
"enterprise": "Enterprise",
"freeSubtitle": "kostenlos für immer",
"freeUsers": "Ideal für die persönliche Nutzung",
"freeText01": "100MB Speicher",
"freeText02": "3 Projekte",
"freeText03": "5 Teammitglieder",
"startupSubtitle": "PAUSCHALPREIS / Monat",
"startupUsers": "Bis zu 15 Benutzer",
"startupText01": "25GB Speicher",
"startupText02": "Unbegrenzte aktive Projekte",
"startupText03": "Zeitplan",
"startupText04": "Berichterstattung",
"startupText05": "Projekte abonnieren",
"businessSubtitle": "Benutzer / Monat",
"businessUsers": "16 - 200 Benutzer",
"enterpriseUsers": "200 - 500+ Benutzer",
"footerTitle": "Bitte geben Sie uns eine Kontaktnummer, unter der wir Sie erreichen können.",
"footerLabel": "Kontaktnummer",
"footerButton": "Kontaktieren Sie uns",
"redeemCodePlaceHolder": "Geben Sie Ihren Gutscheincode ein",
"submit": "Einreichen",
"trialPlan": "Kostenlose Testversion",
"trialExpireDate": "Gültig bis {{trial_expire_date}}",
"trialExpired": "Ihre kostenlose Testversion ist {{trial_expire_string}} abgelaufen",
"trialInProgress": "Ihre kostenlose Testversion läuft {{trial_expire_string}} ab",
"required": "Dieses Feld ist erforderlich",
"invalidCode": "Ungültiger Code",
"selectPlan": "Wählen Sie den besten Plan für Ihr Team",
"changeSubscriptionPlan": "Ändern Sie Ihren Abonnementplan",
"noOfSeats": "Anzahl der Plätze",
"annualPlan": "Pro - Jährlich",
"monthlyPlan": "Pro - Monatlich",
"freeForever": "Kostenlos für immer",
"bestForPersonalUse": "Ideal für die persönliche Nutzung",
"storage": "Speicher",
"projects": "Projekte",
"teamMembers": "Teammitglieder",
"unlimitedTeamMembers": "Unbegrenzte Teammitglieder",
"unlimitedActiveProjects": "Unbegrenzte aktive Projekte",
"schedule": "Zeitplan",
"reporting": "Berichterstattung",
"subscribeToProjects": "Projekte abonnieren",
"billedAnnually": "Jährlich abgerechnet",
"billedMonthly": "Monatlich abgerechnet",
"pausePlan": "Plan pausieren",
"resumePlan": "Plan fortsetzen",
"changePlan": "Plan ändern",
"cancelPlan": "Plan kündigen",
"perMonthPerUser": "pro Benutzer/Monat",
"viewInvoice": "Rechnung anzeigen",
"switchToFreePlan": "Wechsel zum kostenlosen Plan",
"expirestoday": "heute",
"expirestomorrow": "morgen",
"expiredDaysAgo": "vor {{days}} Tagen",
"continueWith": "Fortfahren mit {{plan}}",
"changeToPlan": "Wechseln zu {{plan}}"
}

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