Merge pull request #249 from Worklenz/release/v2.1.0

Release/v2.1.0
This commit is contained in:
Chamika J
2025-07-09 06:37:37 +05:30
committed by GitHub
588 changed files with 50179 additions and 13006 deletions

137
.gitignore vendored
View File

@@ -1,82 +1,79 @@
# Dependencies
node_modules/
.pnp/
.pnp.js
# Dependencies
node_modules/
.pnp/
.pnp.js
# Build outputs
dist/
build/
out/
.next/
.nuxt/
.cache/
# Build outputs
dist/
build/
out/
.next/
.nuxt/
.cache/
# Environment variables
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
.env.development
.env.production
.env.*
!.env.example
!.env.template
# Environment variables
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
.env.development
.env.production
.env.*
!.env.example
!.env.template
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
#backups
pg_backups/
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea/
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
*.sublime-workspace
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea/
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
*.sublime-workspace
# Testing
coverage/
.nyc_output/
# Testing
coverage/
.nyc_output/
# Temp files
.temp/
.tmp/
temp/
tmp/
# Temp files
.temp/
.tmp/
temp/
tmp/
# Debug
.debug/
# Debug
.debug/
# Misc
.DS_Store
Thumbs.db
.thumbs.db
ehthumbs.db
Desktop.ini
$RECYCLE.BIN/
# Misc
.DS_Store
Thumbs.db
.thumbs.db
ehthumbs.db
Desktop.ini
$RECYCLE.BIN/
# Yarn
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/sdks
!.yarn/versions
# Yarn
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/sdks
!.yarn/versions
# TypeScript
*.tsbuildinfo
# TypeScript
*.tsbuildinfo

View File

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

View File

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

6
package-lock.json generated Normal file
View File

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

View File

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

View File

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

View File

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

View File

@@ -4325,6 +4325,7 @@ DECLARE
_from_group UUID;
_to_group UUID;
_group_by TEXT;
_batch_size INT := 100; -- PERFORMANCE OPTIMIZATION: Batch size for large updates
BEGIN
_project_id = (_body ->> 'project_id')::UUID;
_task_id = (_body ->> 'task_id')::UUID;
@@ -4337,16 +4338,26 @@ BEGIN
_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
UPDATE tasks SET status_id = _to_group WHERE id = _task_id AND status_id = _from_group;
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;
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')
@@ -4365,14 +4376,15 @@ BEGIN
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(_from_index, _to_index, _task_id, _project_id);
PERFORM handle_task_list_sort_inside_group_optimized(_from_index, _to_index, _task_id, _project_id, _batch_size);
ELSE
PERFORM handle_task_list_sort_between_groups(_from_index, _to_index, _task_id, _project_id);
PERFORM handle_task_list_sort_between_groups_optimized(_from_index, _to_index, _task_id, _project_id, _batch_size);
END IF;
ELSE
PERFORM handle_task_list_sort_inside_group(_from_index, _to_index, _task_id, _project_id);
PERFORM handle_task_list_sort_inside_group_optimized(_from_index, _to_index, _task_id, _project_id, _batch_size);
END IF;
END
$$;
@@ -6372,3 +6384,121 @@ BEGIN
);
END;
$$;
-- PERFORMANCE OPTIMIZATION: Optimized version with batching for large datasets
CREATE OR REPLACE FUNCTION handle_task_list_sort_between_groups_optimized(_from_index integer, _to_index integer, _task_id uuid, _project_id uuid, _batch_size integer DEFAULT 100) RETURNS void
LANGUAGE plpgsql
AS
$$
DECLARE
_offset INT := 0;
_affected_rows INT;
BEGIN
-- PERFORMANCE OPTIMIZATION: Use CTE for better query planning
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
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;
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
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;
EXIT WHEN _affected_rows = 0;
_offset := _offset + _batch_size;
END LOOP;
UPDATE tasks SET sort_order = _to_index + 1 WHERE id = _task_id AND project_id = _project_id;
END IF;
END
$$;
-- PERFORMANCE OPTIMIZATION: Optimized version with batching for large datasets
CREATE OR REPLACE FUNCTION handle_task_list_sort_inside_group_optimized(_from_index integer, _to_index integer, _task_id uuid, _project_id uuid, _batch_size integer DEFAULT 100) RETURNS void
LANGUAGE plpgsql
AS
$$
DECLARE
_offset INT := 0;
_affected_rows INT;
BEGIN
-- PERFORMANCE OPTIMIZATION: Batch updates for large datasets
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;
EXIT WHEN _affected_rows = 0;
_offset := _offset + _batch_size;
END LOOP;
END IF;
IF _to_index < _from_index
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;
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
$$;

File diff suppressed because it is too large Load Diff

View File

@@ -68,6 +68,7 @@
"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",
@@ -85,7 +86,6 @@
"passport-local": "^1.0.0",
"path": "^0.12.7",
"pg": "^8.14.1",
"pg-native": "^3.3.0",
"pug": "^3.0.2",
"redis": "^4.6.7",
"sanitize-html": "^2.11.0",
@@ -93,8 +93,10 @@
"sharp": "^0.32.6",
"slugify": "^1.6.6",
"socket.io": "^4.7.1",
"tinymce": "^7.8.0",
"uglify-js": "^3.17.4",
"winston": "^3.10.0",
"worklenz-backend": "file:",
"xss-filters": "^1.2.7"
},
"devDependencies": {
@@ -102,15 +104,17 @@
"@babel/preset-typescript": "^7.22.5",
"@types/bcrypt": "^5.0.0",
"@types/bluebird": "^3.5.38",
"@types/body-parser": "^1.19.2",
"@types/compression": "^1.7.2",
"@types/connect-flash": "^0.0.37",
"@types/cookie-parser": "^1.4.3",
"@types/cron": "^2.0.1",
"@types/crypto-js": "^4.2.2",
"@types/csurf": "^1.11.2",
"@types/express": "^4.17.17",
"@types/express": "^4.17.21",
"@types/express-brute": "^1.0.2",
"@types/express-brute-redis": "^0.0.4",
"@types/express-serve-static-core": "^4.17.34",
"@types/express-session": "^1.17.7",
"@types/fs-extra": "^9.0.13",
"@types/hpp": "^0.2.2",

View File

@@ -756,4 +756,186 @@ export default class ProjectsController extends WorklenzControllerBase {
}
@HandleExceptions()
public static async getGrouped(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
// Use qualified field name for projects to avoid ambiguity
const {searchQuery, sortField, sortOrder, size, offset} = this.toPaginationOptions(req.query, ["projects.name"]);
const groupBy = req.query.groupBy as string || "category";
const filterByMember = !req.user?.owner && !req.user?.is_admin ?
` AND is_member_of_project(projects.id, '${req.user?.id}', $1) ` : "";
const isFavorites = req.query.filter === "1" ? ` AND EXISTS(SELECT user_id FROM favorite_projects WHERE user_id = '${req.user?.id}' AND project_id = projects.id)` : "";
const isArchived = req.query.filter === "2"
? ` AND EXISTS(SELECT user_id FROM archived_projects WHERE user_id = '${req.user?.id}' AND project_id = projects.id)`
: ` AND NOT EXISTS(SELECT user_id FROM archived_projects WHERE user_id = '${req.user?.id}' AND project_id = projects.id)`;
const categories = this.getFilterByCategoryWhereClosure(req.query.categories as string);
const statuses = this.getFilterByStatusWhereClosure(req.query.statuses as string);
// Determine grouping field and join based on groupBy parameter
let groupField = "";
let groupName = "";
let groupColor = "";
let groupJoin = "";
let groupByFields = "";
let groupOrderBy = "";
switch (groupBy) {
case "client":
groupField = "COALESCE(projects.client_id::text, 'no-client')";
groupName = "COALESCE(clients.name, 'No Client')";
groupColor = "'#688'";
groupJoin = "LEFT JOIN clients ON projects.client_id = clients.id";
groupByFields = "projects.client_id, clients.name";
groupOrderBy = "COALESCE(clients.name, 'No Client')";
break;
case "status":
groupField = "COALESCE(projects.status_id::text, 'no-status')";
groupName = "COALESCE(sys_project_statuses.name, 'No Status')";
groupColor = "COALESCE(sys_project_statuses.color_code, '#888')";
groupJoin = "LEFT JOIN sys_project_statuses ON projects.status_id = sys_project_statuses.id";
groupByFields = "projects.status_id, sys_project_statuses.name, sys_project_statuses.color_code";
groupOrderBy = "COALESCE(sys_project_statuses.name, 'No Status')";
break;
case "category":
default:
groupField = "COALESCE(projects.category_id::text, 'uncategorized')";
groupName = "COALESCE(project_categories.name, 'Uncategorized')";
groupColor = "COALESCE(project_categories.color_code, '#888')";
groupJoin = "LEFT JOIN project_categories ON projects.category_id = project_categories.id";
groupByFields = "projects.category_id, project_categories.name, project_categories.color_code";
groupOrderBy = "COALESCE(project_categories.name, 'Uncategorized')";
}
// Ensure sortField is properly qualified for the inner project query
let qualifiedSortField = sortField;
if (Array.isArray(sortField)) {
qualifiedSortField = sortField[0]; // Take the first field if it's an array
}
// Replace "projects." with "p2." for the inner query
const innerSortField = qualifiedSortField.replace("projects.", "p2.");
const q = `
SELECT ROW_TO_JSON(rec) AS groups
FROM (
SELECT COUNT(DISTINCT ${groupField}) AS total_groups,
(SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(group_data))), '[]'::JSON)
FROM (
SELECT ${groupField} AS group_key,
${groupName} AS group_name,
${groupColor} AS group_color,
COUNT(*) AS project_count,
(SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(project_data))), '[]'::JSON)
FROM (
SELECT p2.id,
p2.name,
(SELECT sys_project_statuses.name FROM sys_project_statuses WHERE sys_project_statuses.id = p2.status_id) AS status,
(SELECT sys_project_statuses.color_code FROM sys_project_statuses WHERE sys_project_statuses.id = p2.status_id) AS status_color,
(SELECT sys_project_statuses.icon FROM sys_project_statuses WHERE sys_project_statuses.id = p2.status_id) AS status_icon,
EXISTS(SELECT user_id
FROM favorite_projects
WHERE user_id = '${req.user?.id}'
AND project_id = p2.id) AS favorite,
EXISTS(SELECT user_id
FROM archived_projects
WHERE user_id = '${req.user?.id}'
AND project_id = p2.id) AS archived,
p2.color_code,
p2.start_date,
p2.end_date,
p2.category_id,
(SELECT COUNT(*)
FROM tasks
WHERE archived IS FALSE
AND project_id = p2.id) AS all_tasks_count,
(SELECT COUNT(*)
FROM tasks
WHERE archived IS FALSE
AND project_id = p2.id
AND status_id IN (SELECT task_statuses.id
FROM task_statuses
WHERE task_statuses.project_id = p2.id
AND task_statuses.category_id IN
(SELECT sys_task_status_categories.id FROM sys_task_status_categories WHERE sys_task_status_categories.is_done IS TRUE))) AS completed_tasks_count,
(SELECT COUNT(*)
FROM project_members
WHERE project_members.project_id = p2.id) AS members_count,
(SELECT get_project_members(p2.id)) AS names,
(SELECT clients.name FROM clients WHERE clients.id = p2.client_id) AS client_name,
(SELECT users.name FROM users WHERE users.id = p2.owner_id) AS project_owner,
(SELECT project_categories.name FROM project_categories WHERE project_categories.id = p2.category_id) AS category_name,
(SELECT project_categories.color_code
FROM project_categories
WHERE project_categories.id = p2.category_id) AS category_color,
((SELECT project_members.team_member_id as team_member_id
FROM project_members
WHERE project_members.project_id = p2.id
AND project_members.project_access_level_id = (SELECT project_access_levels.id FROM project_access_levels WHERE project_access_levels.key = 'PROJECT_MANAGER'))) AS project_manager_team_member_id,
(SELECT project_members.default_view
FROM project_members
WHERE project_members.project_id = p2.id
AND project_members.team_member_id = '${req.user?.team_member_id}') AS team_member_default_view,
(SELECT CASE
WHEN ((SELECT MAX(tasks.updated_at)
FROM tasks
WHERE tasks.archived IS FALSE
AND tasks.project_id = p2.id) >
p2.updated_at)
THEN (SELECT MAX(tasks.updated_at)
FROM tasks
WHERE tasks.archived IS FALSE
AND tasks.project_id = p2.id)
ELSE p2.updated_at END) AS updated_at
FROM projects p2
${groupJoin.replace("projects.", "p2.")}
WHERE p2.team_id = $1
AND ${groupField.replace("projects.", "p2.")} = ${groupField}
${categories.replace("projects.", "p2.")}
${statuses.replace("projects.", "p2.")}
${isArchived.replace("projects.", "p2.")}
${isFavorites.replace("projects.", "p2.")}
${filterByMember.replace("projects.", "p2.")}
${searchQuery.replace("projects.", "p2.")}
ORDER BY ${innerSortField} ${sortOrder}
) project_data
) AS projects
FROM projects
${groupJoin}
WHERE projects.team_id = $1 ${categories} ${statuses} ${isArchived} ${isFavorites} ${filterByMember} ${searchQuery}
GROUP BY ${groupByFields}
ORDER BY ${groupOrderBy}
LIMIT $2 OFFSET $3
) group_data
) AS data
FROM projects
${groupJoin}
WHERE projects.team_id = $1 ${categories} ${statuses} ${isArchived} ${isFavorites} ${filterByMember} ${searchQuery}
) rec;
`;
const result = await db.query(q, [req.user?.team_id || null, size, offset]);
const [data] = result.rows;
// Process the grouped data
for (const group of data?.groups.data || []) {
for (const project of group.projects || []) {
project.progress = project.all_tasks_count > 0
? ((project.completed_tasks_count / project.all_tasks_count) * 100).toFixed(0) : 0;
project.updated_at_string = moment(project.updated_at).fromNow();
project.names = this.createTagList(project?.names);
project.names.map((a: any) => a.color_code = getColor(a.name));
if (project.project_manager_team_member_id) {
project.project_manager = {
id: project.project_manager_team_member_id
};
}
}
}
return res.status(200).send(new ServerResponse(true, data?.groups || { total_groups: 0, data: [] }));
}
}

View File

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

View File

@@ -50,11 +50,16 @@ export default class TasksControllerBase extends WorklenzControllerBase {
task.progress = parseInt(task.progress_value);
task.complete_ratio = parseInt(task.progress_value);
}
// For tasks with no subtasks and no manual progress, calculate based on time
// For tasks with no subtasks and no manual progress
else {
task.progress = task.total_minutes_spent && task.total_minutes
? ~~(task.total_minutes_spent / task.total_minutes * 100)
: 0;
// Only calculate progress based on time if time-based progress is enabled for the project
if (task.project_use_time_progress && task.total_minutes_spent && task.total_minutes) {
// Cap the progress at 100% to prevent showing more than 100% progress
task.progress = Math.min(~~(task.total_minutes_spent / task.total_minutes * 100), 100);
} else {
// Default to 0% progress when time-based calculation is not enabled
task.progress = 0;
}
// Set complete_ratio to match progress
task.complete_ratio = task.progress;

File diff suppressed because it is too large Load Diff

View File

@@ -6,11 +6,11 @@ import { isProduction } from "../shared/utils";
const pgSession = require("connect-pg-simple")(session);
export default session({
name: process.env.SESSION_NAME || "worklenz.sid",
name: process.env.SESSION_NAME,
secret: process.env.SESSION_SECRET || "development-secret-key",
proxy: true,
proxy: false,
resave: false,
saveUninitialized: false,
saveUninitialized: true,
rolling: true,
store: new pgSession({
pool: db.pool,
@@ -18,9 +18,10 @@ export default session({
}),
cookie: {
path: "/",
secure: isProduction(), // Use secure cookies in production
httpOnly: true,
sameSite: "lax", // Standard setting for same-origin requests
// secure: isProduction(),
// httpOnly: isProduction(),
// sameSite: "none",
// domain: isProduction() ? ".worklenz.com" : undefined,
maxAge: 30 * 24 * 60 * 60 * 1000 // 30 days
}
});

View File

@@ -0,0 +1,20 @@
{
"name": "tinymce",
"version": "6.8.4",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "tinymce",
"version": "6.8.4",
"license": "MIT",
"dependencies": {
"tinymce": "file:"
}
},
"node_modules/tinymce": {
"resolved": "",
"link": true
}
}
}

View File

@@ -28,5 +28,8 @@
"homepage": "https://www.tiny.cloud/",
"bugs": {
"url": "https://github.com/tinymce/tinymce/issues"
},
"dependencies": {
"tinymce": "file:"
}
}
}

View File

@@ -18,6 +18,7 @@ projectsApiRouter.get("/update-exist-sort-order", safeControllerFunction(Project
projectsApiRouter.post("/", teamOwnerOrAdminValidator, projectsBodyValidator, safeControllerFunction(ProjectsController.create));
projectsApiRouter.get("/", safeControllerFunction(ProjectsController.get));
projectsApiRouter.get("/grouped", safeControllerFunction(ProjectsController.getGrouped));
projectsApiRouter.get("/my-task-projects", safeControllerFunction(ProjectsController.getMyProjectsToTasks));
projectsApiRouter.get("/my-projects", safeControllerFunction(ProjectsController.getMyProjects));
projectsApiRouter.get("/all", safeControllerFunction(ProjectsController.getAllProjects));

View File

@@ -18,6 +18,7 @@ statusesApiRouter.put("/order", statusOrderValidator, safeControllerFunction(Tas
statusesApiRouter.get("/categories", safeControllerFunction(TaskStatusesController.getCategories));
statusesApiRouter.get("/:id", idParamValidator, safeControllerFunction(TaskStatusesController.getById));
statusesApiRouter.put("/name/:id", projectManagerValidator, idParamValidator, taskStatusBodyValidator, safeControllerFunction(TaskStatusesController.updateName));
statusesApiRouter.put("/category/:id", projectManagerValidator, idParamValidator, safeControllerFunction(TaskStatusesController.updateCategory));
statusesApiRouter.put("/:id", projectManagerValidator, idParamValidator, taskStatusBodyValidator, safeControllerFunction(TaskStatusesController.update));
statusesApiRouter.delete("/:id", projectManagerValidator, idParamValidator, statusDeleteValidator, safeControllerFunction(TaskStatusesController.deleteById));

View File

@@ -42,6 +42,9 @@ tasksApiRouter.get("/list/columns/:id", idParamValidator, safeControllerFunction
tasksApiRouter.put("/list/columns/:id", idParamValidator, safeControllerFunction(TaskListColumnsController.toggleColumn));
tasksApiRouter.get("/list/v2/:id", idParamValidator, safeControllerFunction(getList));
tasksApiRouter.get("/list/v3/:id", idParamValidator, safeControllerFunction(TasksControllerV2.getTasksV3));
tasksApiRouter.post("/refresh-progress/:id", idParamValidator, safeControllerFunction(TasksControllerV2.refreshTaskProgress));
tasksApiRouter.get("/progress-status/:id", idParamValidator, safeControllerFunction(TasksControllerV2.getTaskProgressStatus));
tasksApiRouter.get("/assignees/:id", idParamValidator, safeControllerFunction(TasksController.getProjectTaskAssignees));
tasksApiRouter.put("/bulk/status", mapTasksToBulkUpdate, bulkTasksStatusValidator, safeControllerFunction(TasksController.bulkChangeStatus));

View File

@@ -19,7 +19,8 @@ export async function on_project_subscriber_change(_io: Server, socket: Socket,
const isSubscribe = data.mode == 0;
const q = isSubscribe
? `INSERT INTO project_subscribers (user_id, project_id, team_member_id)
VALUES ($1, $2, $3);`
VALUES ($1, $2, $3)
ON CONFLICT (user_id, project_id, team_member_id) DO NOTHING;`
: `DELETE
FROM project_subscribers
WHERE user_id = $1
@@ -27,7 +28,7 @@ export async function on_project_subscriber_change(_io: Server, socket: Socket,
AND team_member_id = $3;`;
await db.query(q, [data.user_id, data.project_id, data.team_member_id]);
const subscribers = await TasksControllerV2.getTaskSubscribers(data.project_id);
const subscribers = await TasksControllerV2.getProjectSubscribers(data.project_id);
socket.emit(SocketEvents.PROJECT_SUBSCRIBERS_CHANGE.toString(), subscribers);
return;

View File

@@ -56,6 +56,8 @@ export async function on_quick_task(_io: Server, socket: Socket, data?: string)
const q = `SELECT create_quick_task($1) AS task;`;
const body = JSON.parse(data as string);
body.name = (body.name || "").trim();
body.priority_id = body.priority_id?.trim() || null;
body.status_id = body.status_id?.trim() || null;
@@ -111,10 +113,12 @@ export async function on_quick_task(_io: Server, socket: Socket, data?: string)
notifyProjectUpdates(socket, d.task.id);
}
} else {
// Empty task name, emit null to indicate no task was created
socket.emit(SocketEvents.QUICK_TASK.toString(), null);
}
} catch (error) {
log_error(error);
socket.emit(SocketEvents.QUICK_TASK.toString(), null);
}
socket.emit(SocketEvents.QUICK_TASK.toString(), null);
}

View File

@@ -138,4 +138,4 @@ export async function on_task_sort_order_change(_io: Server, socket: Socket, dat
}
socket.emit(SocketEvents.TASK_SORT_ORDER_CHANGE.toString(), []);
}
}

View File

@@ -58,10 +58,10 @@ export async function on_task_status_change(_io: Server, socket: Socket, data?:
FROM tasks
WHERE id = $1
`, [body.task_id]);
const currentProgress = progressResult.rows[0]?.progress_value;
const isManualProgress = progressResult.rows[0]?.manual_progress;
// Only update if not already 100%
if (currentProgress !== 100) {
// Update progress to 100%
@@ -70,9 +70,9 @@ export async function on_task_status_change(_io: Server, socket: Socket, data?:
SET progress_value = 100, manual_progress = TRUE
WHERE id = $1
`, [body.task_id]);
log(`Task ${body.task_id} moved to done status - progress automatically set to 100%`, null);
// Log the progress change to activity logs
await logProgressChange({
task_id: body.task_id,
@@ -80,7 +80,7 @@ export async function on_task_status_change(_io: Server, socket: Socket, data?:
new_value: "100",
socket
});
// If this is a subtask, update parent task progress
if (body.parent_task) {
setTimeout(() => {
@@ -88,6 +88,23 @@ export async function on_task_status_change(_io: Server, socket: Socket, data?:
}, 100);
}
}
} else {
// Task is moving from "done" to "todo" or "doing" - reset manual_progress to FALSE
// so progress can be recalculated based on subtasks
await db.query(`
UPDATE tasks
SET manual_progress = FALSE
WHERE id = $1
`, [body.task_id]);
log(`Task ${body.task_id} moved from done status - manual_progress reset to FALSE`, null);
// If this is a subtask, update parent task progress
if (body.parent_task) {
setTimeout(() => {
socket.emit(SocketEvents.GET_TASK_PROGRESS.toString(), body.parent_task);
}, 100);
}
}
const info = await TasksControllerV2.getTaskCompleteRatio(body.parent_task || body.task_id);

View File

@@ -2,31 +2,35 @@
<html lang="en">
<head>
<title></title>
<title>Worklenz 2.1.0 Release</title>
<meta name="subject" content="Worklenz 2.1.0 Release" />
<meta content="text/html; charset=utf-8" http-equiv="Content-Type">
<meta content="width=device-width,initial-scale=1" name="viewport">
<style>
* {
box-sizing: border-box
box-sizing: border-box;
}
body {
margin: 0;
padding: 0
padding: 0;
background: #f6f8fa;
font-family: 'Mada', 'Segoe UI', Arial, sans-serif;
color: #222;
}
a[x-apple-data-detectors] {
color: inherit !important;
text-decoration: inherit !important
text-decoration: inherit !important;
}
#MessageViewBody a {
color: inherit;
text-decoration: none
text-decoration: none;
}
p {
line-height: inherit
line-height: 1.6;
}
.padding-30 {
@@ -37,272 +41,201 @@
padding: 0px 20px;
}
.desktop_hide,
.desktop_hide table {
mso-hide: all;
display: none;
max-height: 0;
overflow: hidden
.card {
background: #fff;
border-radius: 16px;
box-shadow: 0 2px 12px rgba(24, 144, 255, 0.08);
margin-bottom: 32px;
padding: 32px 32px 24px 32px;
transition: box-shadow 0.2s;
}
@media (max-width: 525px) {
.desktop_hide table.icons-inner {
display: inline-block !important
.card h3 {
color: #1890ff;
margin-top: 0;
margin-bottom: 12px;
font-size: 22px;
}
.card img {
border-radius: 10px;
margin: 18px 0 0 0;
box-shadow: 0 1px 8px rgba(24, 144, 255, 0.07);
max-width: 100%;
display: block;
}
.feature-list {
padding-left: 18px;
margin: 0 0 12px 0;
}
.feature-list li {
margin-bottom: 6px;
font-size: 16px;
}
.lang-badge {
display: inline-block;
background: #e6f7ff;
color: #1890ff;
border-radius: 8px;
padding: 3px 10px;
font-size: 14px;
margin-right: 8px;
margin-bottom: 4px;
}
.main-btn {
background: #1890ff;
border: none;
outline: none;
padding: 14px 28px;
font-size: 18px;
text-decoration: none;
color: white;
border-radius: 23px;
margin: 32px auto 0 auto;
font-family: 'Mada', sans-serif;
display: inline-block;
box-shadow: 0 2px 8px rgba(24, 144, 255, 0.13);
transition: background 0.2s, color 0.2s, border 0.2s;
border: 2px solid #1890ff;
}
.main-btn:hover {
background: #40a9ff;
color: #fff;
border-color: #40a9ff;
}
@media (max-width: 600px) {
.card {
padding: 18px 8px 16px 8px;
}
.icons-inner {
text-align: center
.main-btn {
width: 90%;
font-size: 16px;
padding: 12px 0;
}
}
@media (prefers-color-scheme: dark) {
body {
background: #181a1b;
color: #e6e6e6;
}
.icons-inner td {
margin: 0 auto
.card {
background: #23272a;
box-shadow: 0 2px 12px rgba(24, 144, 255, 0.13);
}
.row-content {
width: 95% !important
.main-btn {
background: #1890ff;
color: #fff;
border: 2px solid #1890ff;
}
.mobile_hide {
display: none
.main-btn:hover {
background: #40a9ff;
color: #fff;
border-color: #40a9ff;
}
.stack .column {
width: 100%;
display: block
.logo-light {
display: none !important;
}
.mobile_hide {
min-height: 0;
max-height: 0;
max-width: 0;
overflow: hidden;
font-size: 0
.logo-dark {
display: block !important;
}
}
.desktop_hide,
.desktop_hide table {
display: table !important;
max-height: none !important
}
.logo-light {
display: block;
}
.logo-dark {
display: none;
}
</style>
</head>
<body style="background-color:#fff;margin:0;padding:0;-webkit-text-size-adjust:none;text-size-adjust:none">
<table border="0" cellpadding="0" cellspacing="0" class="nl-container" role="presentation"
style="mso-table-lspace:0;mso-table-rspace:0;background-image:none;background-position:top left;background-size:auto;background-repeat:no-repeat"
width="100%">
<tbody>
<tr>
<td>
<table align="center" border="0" cellpadding="0" cellspacing="0" class="row row-1" role="presentation"
style="mso-table-lspace:0;mso-table-rspace:0" width="100%">
<tbody>
<tr>
<td>
<table border="0" cellpadding="0" cellspacing="0" class="image_block block-1 padding-20"
role="presentation" style="mso-table-lspace:0;mso-table-rspace:0;margin-left: auto;margin-right: auto;padding-top: 20px;
padding-bottom: 20px;" width="300">
<tr>
<td class="pad" style="width:100%;padding-right:0;padding-left:0">
<div align="left" class="alignment" style="line-height:10px">
<a href="https://worklenz.com" style="outline:none;width: 170px;" tabindex="-1"
target="_blank"><img
src="https://worklenz.s3.amazonaws.com/email-templates-assets/email-logo.png"
style="display:block;max-width: 300px;height:auto;border:0;max-width:100%;margin-top: 10px;margin-bottom: 0px;"></a>
</div>
</td>
</tr>
</table>
<table align="center" border="0" cellpadding="0" cellspacing="0" class="row-content stack"
role="presentation" style="mso-table-lspace:0;mso-table-rspace:0;color:#000;width:720px;"
width="475">
<tbody>
<tr>
<td class="column column-1"
style="mso-table-lspace:0;mso-table-rspace:0;font-weight:400;text-align:left;vertical-align:top;padding-top:5px;padding-bottom:5px;border-top:0;border-right:0;border-bottom:0;border-left:0"
width="100%">
<table border="0" cellpadding="0" cellspacing="0" class="image_block block-3"
role="presentation" style="mso-table-lspace:0;mso-table-rspace:0" width="100%">
<tr>
<td class="pad" style="width:100%;padding-right:0;padding-left:0">
<div align="center" class="alignment" style="line-height:10px"><img
src="https://worklenz.s3.amazonaws.com/email-templates-assets/under-maintenance.png"
style="display:block;height:auto;border:0;width:180px;max-width:100%;/* margin-top: 30px; */margin-bottom: 10px;"
width="180">
</div>
</td>
</tr>
</table>
<table border="0" cellpadding="8" cellspacing="0" class="paragraph_block block-5 padding-30"
role="presentation" style="mso-table-lspace:0;mso-table-rspace:0;word-break:break-word"
width="100%">
<tr>
<td class="pad">
<div
style="color:#505771;font-size:16px;font-family: 'Mada', sans-serif;font-weight:400;line-height:135%;direction:ltr;letter-spacing:0;border-radius: 12px;margin-bottom: 12px;">
<h3 style="margin-bottom: 0;">Project Roadmap Redesign</h3>
<p>
Experience a comprehensive visual representation of task progression within your projects.
The sequential arrangement unfolds seamlessly in a user-friendly timeline format, allowing
for effortless understanding and efficient project management.
</p>
<img src="https://worklenz.s3.amazonaws.com/gifs/WL20231114/roadmap.gif"
style="width: 100%;margin: auto;" alt="Revamped Reporting">
</div>
</td>
</tr>
</table>
<table border="0" cellpadding="8" cellspacing="0" class="paragraph_block block-5 padding-30"
role="presentation" style="mso-table-lspace:0;mso-table-rspace:0;word-break:break-word"
width="100%">
<tr>
<td class="pad">
<div
style="color:#505771;font-size:16px;font-family: 'Mada', sans-serif;font-weight:400;line-height:135%;direction:ltr;letter-spacing:0;border-radius: 12px;margin-bottom: 12px;">
<h3 style="margin-bottom: 0;">Project Workload Redesign</h3>
<p>
Gain insights into the optimized allocation and utilization of resources within your project.
</p>
<img src="https://worklenz.s3.amazonaws.com/gifs/WL20231114/workload-1.gif"
style="width: 100%;margin: auto;" alt="Revamped Reporting">
</div>
</td>
</tr>
</table>
<table border="0" cellpadding="8" cellspacing="0" class="paragraph_block block-5 padding-30"
role="presentation" style="mso-table-lspace:0;mso-table-rspace:0;word-break:break-word"
width="100%">
<tr>
<td class="pad">
<div
style="color:#505771;font-size:16px;font-family: 'Mada', sans-serif;font-weight:400;line-height:135%;direction:ltr;letter-spacing:0;border-radius: 12px;margin-bottom: 12px;">
<h3 style="margin-bottom: 0;">Create new tasks from the roadmap itself</h3>
<p>
Effortlessly generate and modify tasks directly from the roadmap interface with a simple
click-and-drag functionality.
<br>Seamlessly adjust the task's date range according to your
preferences, providing a user-friendly and intuitive experience for efficient task management.
</p>
<img src="https://worklenz.s3.amazonaws.com/gifs/WL20231114/roadmap-2.gif"
style="width: 100%;margin: auto;" alt="Revamped Reporting">
</div>
</td>
</tr>
</table>
<table border="0" cellpadding="8" cellspacing="0" class="paragraph_block block-5 padding-30"
role="presentation" style="mso-table-lspace:0;mso-table-rspace:0;word-break:break-word"
width="100%">
<tr>
<td class="pad">
<div
style="color:#505771;font-size:16px;font-family: 'Mada', sans-serif;font-weight:400;line-height:135%;direction:ltr;letter-spacing:0;border-radius: 12px;margin-bottom: 12px;">
<h3 style="margin-bottom: 0;">Deactivate Team Members</h3>
<p>
Effortlessly manage your team by deactivating members without losing their valuable work.
<br>
<br>
Navigate to the "Settings" section and access "Team Members" to conveniently deactivate
team members while preserving the work they have contributed.
</p>
<img src="https://worklenz.s3.amazonaws.com/gifs/WL20231114/workload-1.gif"
style="width: 100%;margin: auto;" alt="Revamped Reporting">
</div>
</td>
</tr>
</table>
<table border="0" cellpadding="8" cellspacing="0" class="paragraph_block block-5 padding-30"
role="presentation" style="mso-table-lspace:0;mso-table-rspace:0;word-break:break-word"
width="100%">
<tr>
<td class="pad">
<div
style="color:#505771;font-size:16px;font-family: 'Mada', sans-serif;font-weight:400;line-height:135%;direction:ltr;letter-spacing:0;border-radius: 12px;margin-bottom: 12px;">
<h3 style="margin-bottom: 0;">Reporting Enhancements</h3>
<p>
This release also includes several other miscellaneous bug fixes and performance
enhancements to further improve your experience.
</p>
</div>
</td>
</tr>
</table>
<div style="text-align: center;">
<a href="https://worklenz.com/worklenz" target="_blank"
style="background: #1890ff;border: none;outline: none;padding: 12px 16px;font-size: 18px;text-decoration: none;color: white;border-radius: 23px;margin: auto;font-family: 'Mada', sans-serif;">See
what's new</a>
</div>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
<table align="center" border="0" cellpadding="0" cellspacing="0" class="row row-2" role="presentation"
style="mso-table-lspace:0;mso-table-rspace:0" width="100%">
<tbody>
<tr>
<td>
<table align="center" border="0" cellpadding="0" cellspacing="0" class="row-content stack"
role="presentation" style="mso-table-lspace:0;mso-table-rspace:0;color:#000;width:475px"
width="505">
<tbody>
<tr>
<td class="column column-1"
style="mso-table-lspace:0;mso-table-rspace:0;font-weight:400;text-align:left;vertical-align:top;padding-top:5px;padding-bottom:5px;border-top:0;border-right:0;border-bottom:0;border-left:0"
width="100%">
<table border="0" cellpadding="0" cellspacing="0" class="icons_block block-1"
role="presentation" style="mso-table-lspace:0;mso-table-rspace:0" width="100%">
<tr>
<td class="pad"
style="vertical-align:middle;color:#9d9d9d;font-family:inherit;font-size:15px;padding-bottom:5px;padding-top:5px;text-align:center">
<table cellpadding="0" cellspacing="0" role="presentation"
style="mso-table-lspace:0;mso-table-rspace:0" width="100%">
<tr>
<td class="alignment" style="vertical-align:middle;text-align:center">
<!--[if vml]>
<table align="left" cellpadding="0" cellspacing="0" role="presentation"
style="display:inline-block;padding-left:0px;padding-right:0px;mso-table-lspace: 0pt;mso-table-rspace: 0pt;">
<![endif]-->
<!--[if !vml]><!-->
<table cellpadding="0" cellspacing="0" class="icons-inner" role="presentation"
style="mso-table-lspace:0;mso-table-rspace:0;display:inline-block;margin-right:-4px;padding-left:0;padding-right:0">
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table><!-- End -->
<hr>
<p style="font-family:sans-serif;text-decoration:none; text-align: center;">
Click <a href="{{{unsubscribe}}}" target="_blank">here</a> to unsubscribe and manage your email preferences.
</p>
<body>
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="background: #f6f8fa;">
<tr>
<td align="center">
<table border="0" cellpadding="0" cellspacing="0" width="720" style="max-width: 98vw;">
<tr>
<td align="center" style="padding: 32px 0 18px 0;">
<a href="https://worklenz.com" target="_blank" style="display: inline-block;">
<img class="logo-light"
src="https://s3.us-west-2.amazonaws.com/worklenz.com/assets/worklenz-light-mode.png"
alt="Worklenz Light Logo" style="width: 170px; margin-bottom: 0; display: block;" />
<img class="logo-dark"
src="https://s3.us-west-2.amazonaws.com/worklenz.com/assets/worklenz-dark-mode.png"
alt="Worklenz Dark Logo" style="width: 170px; margin-bottom: 0; display: none;" />
</a>
</td>
</tr>
<tr>
<td>
<div class="card">
<h3>🚀 New Tasks List & Kanban Board</h3>
<ul class="feature-list">
<li>Performance optimized for faster loading</li>
<li>Redesigned UI for clarity and speed</li>
<li>Advanced filters for easier task management</li>
</ul>
<img src="https://s3.us-west-2.amazonaws.com/worklenz.com/gifs/WL20250708/task-list-v2.gif"
alt="New Task List">
<img src="https://s3.us-west-2.amazonaws.com/worklenz.com/gifs/WL20250708/kanban-v2.gif"
alt="New Kanban Board">
</div>
<div class="card">
<h3>📁 Group View in Projects List</h3>
<ul class="feature-list">
<li>Toggle between list and group view</li>
<li>Group projects by client or category</li>
<li>Improved navigation and organization</li>
</ul>
<img src="https://s3.us-west-2.amazonaws.com/worklenz.com/gifs/WL20250708/project-list-group-view.gif"
alt="Project List Group View">
</div>
<div class="card">
<h3>🌐 New Language Support</h3>
<span class="lang-badge">Deutsch (DE)</span>
<span class="lang-badge">Shqip (ALB)</span>
<p style="margin-top: 10px;">Worklenz is now available in German and Albanian!</p>
</div>
<div class="card">
<h3>🛠️ Bug Fixes & UI Improvements</h3>
<ul class="feature-list">
<li>General bug fixes</li>
<li>UI/UX enhancements for a smoother experience</li>
<li>Performance improvements across the platform</li>
</ul>
</div>
<div style="text-align: center;">
<a href="https://app.worklenz.com/auth" target="_blank" class="main-btn">See what's new</a>
</div>
</td>
</tr>
<tr>
<td style="padding: 32px 0 0 0;">
<hr style="border: none; border-top: 1px solid #e6e6e6; margin: 32px 0 16px 0;">
<p style="font-family:sans-serif;text-decoration:none; text-align: center; color: #888; font-size: 15px;">
Click <a href="{{unsubscribe}}" target="_blank" style="color: #1890ff;">here</a> to unsubscribe and
manage your email preferences.
</p>
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>
</html>

View File

@@ -3,6 +3,7 @@
Worklenz is a project management application built with React, TypeScript, and Ant Design. The project is bundled using [Vite](https://vitejs.dev/).
## Table of Contents
- [Getting Started](#getting-started)
- [Available Scripts](#available-scripts)
- [Project Structure](#project-structure)

View File

@@ -5,42 +5,72 @@
<link rel="icon" href="./favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#2b2b2b" />
<!-- Resource hints for better loading performance -->
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link rel="dns-prefetch" href="https://www.googletagmanager.com" />
<link rel="dns-prefetch" href="https://js.hs-scripts.com" />
<!-- Preload critical resources -->
<link rel="preload" href="/locales/en/common.json" as="fetch" type="application/json" crossorigin />
<link rel="preload" href="/locales/en/auth/login.json" as="fetch" type="application/json" crossorigin />
<link rel="preload" href="/locales/en/navbar.json" as="fetch" type="application/json" crossorigin />
<!-- Optimized font loading with font-display: swap -->
<link
href="https://fonts.googleapis.com/css2?family=Inter:wght@100;200;300;400;500;600;700;800;900&display=swap"
rel="stylesheet"
media="print"
onload="this.media='all'"
/>
<noscript>
<link
href="https://fonts.googleapis.com/css2?family=Inter:wght@100;200;300;400;500;600;700;800;900&display=swap"
rel="stylesheet"
/>
</noscript>
<title>Worklenz</title>
<!-- Environment configuration -->
<script src="/env-config.js"></script>
<!-- Google Analytics -->
<!-- Optimized Google Analytics with reduced blocking -->
<script>
// Function to initialize Google Analytics
// Function to initialize Google Analytics asynchronously
function initGoogleAnalytics() {
// Load the Google Analytics script
const script = document.createElement('script');
script.async = true;
// Determine which tracking ID to use based on the environment
const isProduction = window.location.hostname === 'worklenz.com' ||
window.location.hostname === 'app.worklenz.com';
const trackingId = isProduction
? 'G-XXXXXXXXXX'
: 'G-3LM2HGWEXG'; // Open source tracking ID
script.src = `https://www.googletagmanager.com/gtag/js?id=${trackingId}`;
document.head.appendChild(script);
// Use requestIdleCallback to defer analytics loading
const loadAnalytics = () => {
// Determine which tracking ID to use based on the environment
const isProduction = window.location.hostname === 'app.worklenz.com';
// Initialize Google Analytics
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', trackingId);
const trackingId = isProduction ? 'G-7KSRKQ1397' : 'G-3LM2HGWEXG'; // Open source tracking ID
// Load the Google Analytics script
const script = document.createElement('script');
script.async = true;
script.src = `https://www.googletagmanager.com/gtag/js?id=${trackingId}`;
document.head.appendChild(script);
// Initialize Google Analytics
window.dataLayer = window.dataLayer || [];
function gtag() {
dataLayer.push(arguments);
}
gtag('js', new Date());
gtag('config', trackingId);
};
// Use requestIdleCallback if available, otherwise setTimeout
if ('requestIdleCallback' in window) {
requestIdleCallback(loadAnalytics, { timeout: 2000 });
} else {
setTimeout(loadAnalytics, 1000);
}
}
// Initialize analytics
// Initialize analytics after a delay to not block initial render
initGoogleAnalytics();
// Function to show privacy notice
@@ -69,7 +99,7 @@
document.body.appendChild(notice);
// Add event listener to button
const btn = notice.querySelector('#analytics-notice-btn');
btn.addEventListener('click', function(e) {
btn.addEventListener('click', function (e) {
e.preventDefault();
localStorage.setItem('privacyNoticeShown', 'true');
notice.remove();
@@ -77,12 +107,13 @@
}
// Wait for DOM to be ready
document.addEventListener('DOMContentLoaded', function() {
document.addEventListener('DOMContentLoaded', function () {
// Check if we should show the notice
const isProduction = window.location.hostname === 'worklenz.com' ||
window.location.hostname === 'app.worklenz.com';
const isProduction =
window.location.hostname === 'worklenz.com' ||
window.location.hostname === 'app.worklenz.com';
const noticeShown = localStorage.getItem('privacyNoticeShown') === 'true';
// Show notice if not in production and not shown before
if (!isProduction && !noticeShown) {
showPrivacyNotice();
@@ -91,64 +122,30 @@
</script>
</head>
<head>
<meta charset="utf-8" />
<link rel="icon" href="./favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#2b2b2b" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@100;200;300;400;500;600;700;800;900&display=swap"
rel="stylesheet" />
<title>Worklenz</title>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<script type="module" src="./src/index.tsx"></script>
<script type="text/javascript">
// Load HubSpot script asynchronously and only for production
if (window.location.hostname === 'app.worklenz.com') {
// Use requestIdleCallback to defer HubSpot loading
const loadHubSpot = () => {
var hs = document.createElement('script');
hs.type = 'text/javascript';
hs.id = 'hs-script-loader';
hs.async = true;
hs.defer = true;
hs.src = '//js.hs-scripts.com/22348300.js';
document.body.appendChild(hs);
};
<!-- Environment configuration -->
<script src="/env-config.js"></script>
<!-- Unregister service worker -->
<script src="/unregister-sw.js"></script>
<!-- Microsoft Clarity -->
<script type="text/javascript">
if (window.location.hostname === 'app.worklenz.com') {
(function (c, l, a, r, i, t, y) {
c[a] = c[a] || function () { (c[a].q = c[a].q || []).push(arguments) };
t = l.createElement(r); t.async = 1; t.src = "https://www.clarity.ms/tag/dx77073klh";
y = l.getElementsByTagName(r)[0]; y.parentNode.insertBefore(t, y);
})(window, document, "clarity", "script", "dx77073klh");
}
</script>
<!-- Google Analytics (only on production) -->
<script type="text/javascript">
if (window.location.hostname === 'app.worklenz.com') {
var gaScript = document.createElement('script');
gaScript.async = true;
gaScript.src = 'https://www.googletagmanager.com/gtag/js?id=G-7KSRKQ1397';
document.head.appendChild(gaScript);
gaScript.onload = function() {
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'G-7KSRKQ1397');
};
}
</script>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<script type="module" src="./src/index.tsx"></script>
<script type="text/javascript">
if (window.location.hostname === 'app.worklenz.com') {
var hs = document.createElement('script');
hs.type = 'text/javascript';
hs.id = 'hs-script-loader';
hs.async = true;
hs.defer = true;
hs.src = '//js.hs-scripts.com/22348300.js';
document.body.appendChild(hs);
}
</script>
</body>
</html>
if ('requestIdleCallback' in window) {
requestIdleCallback(loadHubSpot, { timeout: 3000 });
} else {
setTimeout(loadHubSpot, 2000);
}
}
</script>
</body>
</html>

File diff suppressed because it is too large Load Diff

View File

@@ -3,7 +3,8 @@
"version": "1.0.0",
"private": true,
"scripts": {
"start": "vite",
"start": "vite dev",
"dev": "vite dev",
"prebuild": "node scripts/copy-tinymce.js",
"build": "vite build",
"dev-build": "vite build",
@@ -13,22 +14,25 @@
"dependencies": {
"@ant-design/colors": "^7.1.0",
"@ant-design/compatible": "^5.1.4",
"@ant-design/icons": "^5.4.0",
"@ant-design/icons": "^4.7.0",
"@ant-design/pro-components": "^2.7.19",
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/modifiers": "^9.0.0",
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@emotion/react": "^11.14.0",
"@heroicons/react": "^2.2.0",
"@paddle/paddle-js": "^1.3.3",
"@reduxjs/toolkit": "^2.2.7",
"@tailwindcss/forms": "^0.5.10",
"@tanstack/react-table": "^8.20.6",
"@tanstack/react-virtual": "^3.11.2",
"@tinymce/tinymce-react": "^5.1.1",
"antd": "^5.24.9",
"antd": "^5.26.2",
"axios": "^1.9.0",
"chart.js": "^4.4.7",
"chartjs-plugin-datalabels": "^2.2.0",
"cors": "^2.8.5",
"date-fns": "^4.1.0",
"dompurify": "^3.2.5",
"gantt-task-react": "^0.3.9",
@@ -38,6 +42,7 @@
"i18next-http-backend": "^2.7.3",
"jspdf": "^3.0.0",
"mixpanel-browser": "^2.56.0",
"nanoid": "^5.1.5",
"primereact": "^10.8.4",
"re-resizable": "^6.10.3",
"react": "^18.3.1",
@@ -49,10 +54,13 @@
"react-responsive": "^10.0.0",
"react-router-dom": "^6.28.1",
"react-timer-hook": "^3.0.8",
"react-virtuoso": "^4.13.0",
"react-window": "^1.8.11",
"react-window-infinite-loader": "^1.0.10",
"socket.io-client": "^4.8.1",
"tinymce": "^7.7.2",
"web-vitals": "^4.2.4"
"web-vitals": "^4.2.4",
"worklenz": "file:"
},
"devDependencies": {
"@testing-library/jest-dom": "^6.6.3",
@@ -66,10 +74,12 @@
"@types/node": "^20.8.4",
"@types/react": "19.0.0",
"@types/react-dom": "19.0.0",
"@types/react-window": "^1.8.8",
"@vitejs/plugin-react": "^4.3.4",
"autoprefixer": "^10.4.20",
"autoprefixer": "^10.4.21",
"postcss": "^8.5.2",
"prettier-plugin-tailwindcss": "^0.6.8",
"prettier-plugin-tailwindcss": "^0.6.13",
"rollup": "^4.40.2",
"tailwindcss": "^3.4.17",
"terser": "^5.39.0",
"typescript": "^5.7.3",

View File

@@ -14,4 +14,4 @@
/* Maintain hover state */
.table-body-row:hover .sticky-column {
background-color: var(--background-hover);
}
}

View File

@@ -0,0 +1,7 @@
// Development placeholder for env-config.js
// In production, this file is dynamically generated with actual environment values
// For development, we let the application fall back to import.meta.env variables
// Set undefined values so the application falls back to build-time env vars
window.VITE_API_URL = undefined;
window.VITE_SOCKET_URL = undefined;

View File

@@ -19,5 +19,12 @@
"archive": "Arkivo",
"newTaskNamePlaceholder": "Shkruaj emrin e detyrës",
"newSubtaskNamePlaceholder": "Shkruaj emrin e nëndetyrë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,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

@@ -1,13 +1,29 @@
{
"importTasks": "Importo detyra",
"importTask": "Importo detyrë",
"createTask": "Krijo detyrë",
"settings": "Cilësimet",
"subscribe": "Abonohu",
"unsubscribe": 'abonohu",
"unsubscribe": "Çabonohu",
"deleteProject": "Fshi projektin",
"startDate": "Data e fillimit",
"endDate": "Data e përfundimit",
"endDate": "Data e mbarimit",
"projectSettings": "Cilësimet e projektit",
"projectSummary": "Përmbledhja e projektit",
"receiveProjectSummary": "Merrni një përmbledhje të projektit çdo mbrëmje."
"receiveProjectSummary": "Merrni një përmbledhje të projektit çdo mbrëmje.",
"refreshProject": "Rifresko projektin",
"saveAsTemplate": "Ruaj si model",
"invite": "Fto",
"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

@@ -9,5 +9,6 @@
"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"
"avatarTooltip": "Klikoni për të ngarkuar një avatar",
"title": "Cilësimet e Profilit"
}

View File

@@ -1,4 +1,5 @@
{
"title": "Anëtarët e Ekipit",
"nameColumn": "Emri",
"projectsColumn": "Projektet",
"emailColumn": "Email",
@@ -40,5 +41,7 @@
"ownerText": "Pronar i Ekipit",
"addedText": "Shtuar",
"updatedText": "Përditësuar",
"noResultFound": "Shkruani një adresë email dhe shtypni Enter..."
"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

@@ -1,28 +1,37 @@
{
"taskHeader": {
"taskNamePlaceholder": "Shkruani detyrën tuaj",
"taskNamePlaceholder": "Shkruani Detyrën tuaj",
"deleteTask": "Fshi Detyrën"
},
"taskInfoTab": {
"title": "Info",
"title": "Informacioni",
"details": {
"title": "Detajet",
"task-key": "Çelësi i Detyrës",
"phase": "Faza",
"assignees": "Përgjegjësit",
"due-date": "Afati i Përfundimit",
"assignees": "Të Caktuar",
"due-date": "Data e Përfundimit",
"time-estimation": "Vlerësimi i Kohës",
"priority": "Prioriteti",
"labels": "Etiketa",
"billable": "Fakturueshme",
"labels": "Etiketat",
"billable": "E Faturueshme",
"notify": "Njofto",
"when-done-notify": "Kur përfundo, 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"
"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",
@@ -30,37 +39,48 @@
},
"description": {
"title": "Përshkrimi",
"placeholder": "Shtoni një përshkrim më të detajuar..."
"placeholder": "Shto një përshkrim më të detajuar..."
},
"subTasks": {
"title": "Nën-Detyrat",
"addSubTask": "+ Shto Nën-Detyrë",
"addSubTaskInputPlaceholder": "Shkruani detyrën dhe shtypni Enter",
"refreshSubTasks": "Rifresko Nën-Detyrat",
"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ën-detyrë?",
"deleteSubTask": "Fshi Nën-Detyrën"
"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": "I bllokuar nga",
"searchTask": "Shkruani për të kërkuar detyra",
"noTasksFound": "Asnjë detyrë nuk u gjet",
"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 lëshoni skedar për ngarkim",
"uploading": "Po ngarkohet..."
"chooseOrDropFileToUpload": "Zgjidhni ose hidhni skedar për ngarkuar",
"uploading": "Duke ngarkuar..."
},
"comments": {
"title": "Komentet",
"addComment": "+ Shto koment të ri",
"noComments": "Asnjë koment ende. Bëhu i pari që komenton!",
"noComments": "Ende pa komente. Bëhu i pari që komenton!",
"delete": "Fshi",
"confirmDeleteComment": "Jeni i sigurt që doni të fshini këtë koment?"
"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"
@@ -68,11 +88,36 @@
"taskTimeLogTab": {
"title": "Regjistri i Kohës",
"addTimeLog": "Shto regjistrim të ri kohe",
"totalLogged": "Koha totale e regjistruar",
"totalLogged": "Totali i Regjistruar",
"exportToExcel": "Eksporto në Excel",
"noTimeLogsFound": "Asnjë regjistrim kohe nuk u gjet"
"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"
"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

@@ -55,5 +55,18 @@
"selectCategory": "Zgjidh një kategori",
"pleaseEnterAName": "Ju lutemi vendosni një emër",
"pleaseSelectACategory": "Ju lutemi zgjidhni një kategori",
"create": "Krijo"
"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"
}

View File

@@ -36,8 +36,9 @@
"selectText": "Zgjidh",
"labelsSelectorInputTip": "Shtyp Enter për të krijuar!",
"addTaskText": "+ Shto Detyrë",
"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",
@@ -59,5 +60,74 @@
"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

@@ -17,7 +17,9 @@
"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

@@ -19,5 +19,12 @@
"archive": "Archivieren",
"newTaskNamePlaceholder": "Aufgabenname eingeben",
"newSubtaskNamePlaceholder": "Unteraufgabenname eingeben"
"newSubtaskNamePlaceholder": "Unteraufgabenname eingeben",
"untitledSection": "Unbenannter Abschnitt",
"unmapped": "Nicht zugeordnet",
"clickToChangeDate": "Klicken Sie, um das Datum zu ändern",
"noDueDate": "Kein Fälligkeitsdatum",
"save": "Speichern",
"clear": "Löschen",
"nextWeek": "Nächste Woche"
}

View File

@@ -0,0 +1,14 @@
{
"taskList": "Aufgabenliste",
"board": "Kanban-Board",
"insights": "Insights",
"files": "Dateien",
"members": "Mitglieder",
"updates": "Aktualisierungen",
"projectView": "Projektansicht",
"loading": "Projekt wird geladen...",
"error": "Fehler beim Laden des Projekts",
"pinnedTab": "Als Standard-Registerkarte festgesetzt",
"pinTab": "Als Standard-Registerkarte festsetzen",
"unpinTab": "Standard-Registerkarte lösen"
}

View File

@@ -1,13 +1,29 @@
{
"importTasks": "Aufgaben importieren",
"importTask": "Aufgabe importieren",
"createTask": "Aufgabe erstellen",
"settings": "Einstellungen",
"subscribe": "Abonnieren",
"unsubscribe": "Abbestellen",
"unsubscribe": "Abonnement beenden",
"deleteProject": "Projekt löschen",
"startDate": "Startdatum",
"endDate": "Enddatum",
"projectSettings": "Projekteinstellungen",
"projectSummary": "Projektzusammenfassung",
"receiveProjectSummary": "Erhalten Sie jeden Abend eine Projektzusammenfassung."
"receiveProjectSummary": "Erhalten Sie jeden Abend eine Projektzusammenfassung.",
"refreshProject": "Projekt aktualisieren",
"saveAsTemplate": "Als Vorlage speichern",
"invite": "Einladen",
"subscribeTooltip": "Projektbenachrichtigungen abonnieren",
"unsubscribeTooltip": "Projektbenachrichtigungen beenden",
"refreshTooltip": "Projektdaten aktualisieren",
"settingsTooltip": "Projekteinstellungen öffnen",
"saveAsTemplateTooltip": "Dieses Projekt als Vorlage speichern",
"inviteTooltip": "Teammitglieder zu diesem Projekt einladen",
"createTaskTooltip": "Neue Aufgabe erstellen",
"importTaskTooltip": "Aufgabe aus Vorlage importieren",
"navigateBackTooltip": "Zurück zur Projektliste",
"projectStatusTooltip": "Projektstatus",
"projectDatesInfo": "Informationen zum Projektzeitraum",
"projectCategoryTooltip": "Projektkategorie"
}

View File

@@ -9,5 +9,6 @@
"saveChanges": "Änderungen speichern",
"profileJoinedText": "Vor einem Monat beigetreten",
"profileLastUpdatedText": "Vor einem Monat aktualisiert",
"avatarTooltip": "Klicken Sie zum Hochladen eines Avatars"
"avatarTooltip": "Klicken Sie zum Hochladen eines Avatars",
"title": "Profil-Einstellungen"
}

View File

@@ -1,4 +1,5 @@
{
"title": "Teammitglieder",
"nameColumn": "Name",
"projectsColumn": "Projekte",
"emailColumn": "E-Mail",
@@ -40,5 +41,7 @@
"ownerText": "Team-Besitzer",
"addedText": "Hinzugefügt",
"updatedText": "Aktualisiert",
"noResultFound": "Geben Sie eine E-Mail-Adresse ein und drücken Sie Enter..."
"noResultFound": "Geben Sie eine E-Mail-Adresse ein und drücken Sie Enter...",
"jobTitlesFetchError": "Fehler beim Abrufen der Jobtitel",
"invitationResent": "Einladung erfolgreich erneut gesendet!"
}

View File

@@ -0,0 +1,16 @@
{
"title": "Teams",
"team": "Team",
"teams": "Teams",
"name": "Name",
"created": "Erstellt",
"ownsBy": "Gehört zu",
"edit": "Bearbeiten",
"editTeam": "Team bearbeiten",
"pinTooltip": "Klicken Sie hier, um dies im Hauptmenü zu fixieren",
"editTeamName": "Team-Name bearbeiten",
"updateName": "Name aktualisieren",
"namePlaceholder": "Name",
"nameRequired": "Bitte geben Sie einen Namen ein",
"updateFailed": "Änderung des Team-Namens fehlgeschlagen!"
}

View File

@@ -26,4 +26,4 @@
"add-sub-task": "+ Unteraufgabe hinzufügen",
"refresh-sub-tasks": "Unteraufgaben aktualisieren"
}
}
}

View File

@@ -1,6 +1,6 @@
{
"taskHeader": {
"taskNamePlaceholder": "Aufgabe eingeben",
"taskNamePlaceholder": "Geben Sie Ihre Aufgabe ein",
"deleteTask": "Aufgabe löschen"
},
"taskInfoTab": {
@@ -9,20 +9,29 @@
"title": "Details",
"task-key": "Aufgaben-Schlüssel",
"phase": "Phase",
"assignees": "Zugewiesene",
"assignees": "Beauftragte",
"due-date": "Fälligkeitsdatum",
"time-estimation": "Zeitschätzung",
"priority": "Priorität",
"labels": "Labels",
"billable": "Abrechenbar",
"notify": "Benachrichtigen",
"when-done-notify": "Bei Fertigstellung benachrichtigen",
"when-done-notify": "Bei Abschluss benachrichtigen",
"start-date": "Startdatum",
"end-date": "Enddatum",
"hide-start-date": "Startdatum ausblenden",
"show-start-date": "Startdatum anzeigen",
"hours": "Stunden",
"minutes": "Minuten"
"minutes": "Minuten",
"progressValue": "Fortschrittswert",
"progressValueTooltip": "Fortschritt in Prozent einstellen (0-100%)",
"progressValueRequired": "Bitte geben Sie einen Fortschrittswert ein",
"progressValueRange": "Fortschritt muss zwischen 0 und 100 liegen",
"taskWeight": "Aufgabengewicht",
"taskWeightTooltip": "Gewicht dieser Teilaufgabe festlegen (Prozent)",
"taskWeightRequired": "Bitte geben Sie ein Aufgabengewicht ein",
"taskWeightRange": "Gewicht muss zwischen 0 und 100 liegen",
"recurring": "Wiederkehrend"
},
"labels": {
"labelInputPlaceholder": "Suchen oder erstellen",
@@ -30,29 +39,29 @@
},
"description": {
"title": "Beschreibung",
"placeholder": "Detaillierte Beschreibung hinzufügen..."
"placeholder": "Detailliertere Beschreibung hinzufügen..."
},
"subTasks": {
"title": "Unteraufgaben",
"addSubTask": "+ Unteraufgabe hinzufügen",
"addSubTaskInputPlaceholder": "Aufgabe eingeben und Enter drücken",
"refreshSubTasks": "Unteraufgaben aktualisieren",
"title": "Teilaufgaben",
"addSubTask": "Teilaufgabe hinzufügen",
"addSubTaskInputPlaceholder": "Geben Sie Ihre Aufgabe ein und drücken Sie Enter",
"refreshSubTasks": "Teilaufgaben aktualisieren",
"edit": "Bearbeiten",
"delete": "Löschen",
"confirmDeleteSubTask": "Sind Sie sicher, dass Sie diese Unteraufgabe löschen möchten?",
"deleteSubTask": "Unteraufgabe löschen"
"confirmDeleteSubTask": "Sind Sie sicher, dass Sie diese Teilaufgabe löschen möchten?",
"deleteSubTask": "Teilaufgabe löschen"
},
"dependencies": {
"title": "Abhängigkeiten",
"addDependency": "+ Neue Abhängigkeit hinzufügen",
"blockedBy": "Blockiert durch",
"blockedBy": "Blockiert von",
"searchTask": "Aufgabe suchen",
"noTasksFound": "Keine Aufgaben gefunden",
"confirmDeleteDependency": "Sind Sie sicher, dass Sie dies löschen möchten?"
"confirmDeleteDependency": "Sind Sie sicher, dass Sie löschen möchten?"
},
"attachments": {
"title": "Anhänge",
"chooseOrDropFileToUpload": "Datei auswählen oder zum Hochladen ablegen",
"chooseOrDropFileToUpload": "Datei zum Hochladen wählen oder ablegen",
"uploading": "Wird hochgeladen..."
},
"comments": {
@@ -60,19 +69,55 @@
"addComment": "+ Neuen Kommentar hinzufügen",
"noComments": "Noch keine Kommentare. Seien Sie der Erste!",
"delete": "Löschen",
"confirmDeleteComment": "Sind Sie sicher, dass Sie diesen Kommentar löschen möchten?"
"confirmDeleteComment": "Sind Sie sicher, dass Sie diesen Kommentar löschen möchten?",
"addCommentPlaceholder": "Kommentar hinzufügen...",
"cancel": "Abbrechen",
"commentButton": "Kommentieren",
"attachFiles": "Dateien anhängen",
"addMoreFiles": "Weitere Dateien hinzufügen",
"selectedFiles": "Ausgewählte Dateien (Bis zu 25MB, Maximum {count})",
"maxFilesError": "Sie können maximal {count} Dateien hochladen",
"processFilesError": "Fehler beim Verarbeiten der Dateien",
"addCommentError": "Bitte fügen Sie einen Kommentar hinzu oder hängen Sie Dateien an",
"createdBy": "Erstellt {time} von {user}",
"updatedTime": "Aktualisiert {time}"
},
"searchInputPlaceholder": "Nach Namen suchen",
"pendingInvitation": "Einladung ausstehend"
"searchInputPlaceholder": "Nach Name suchen",
"pendingInvitation": "Ausstehende Einladung"
},
"taskTimeLogTab": {
"title": "Zeiterfassung",
"addTimeLog": "Neuen Zeiteintrag hinzufügen",
"totalLogged": "Gesamt erfasst",
"exportToExcel": "Nach Excel exportieren",
"noTimeLogsFound": "Keine Zeiterfassungen gefunden"
"noTimeLogsFound": "Keine Zeiteinträge gefunden",
"timeLogForm": {
"date": "Datum",
"startTime": "Startzeit",
"endTime": "Endzeit",
"workDescription": "Arbeitsbeschreibung",
"descriptionPlaceholder": "Beschreibung hinzufügen",
"logTime": "Zeit erfassen",
"updateTime": "Zeit aktualisieren",
"cancel": "Abbrechen",
"selectDateError": "Bitte wählen Sie ein Datum",
"selectStartTimeError": "Bitte wählen Sie eine Startzeit",
"selectEndTimeError": "Bitte wählen Sie eine Endzeit",
"endTimeAfterStartError": "Endzeit muss nach der Startzeit liegen"
}
},
"taskActivityLogTab": {
"title": "Aktivitätsprotokoll"
"title": "Aktivitätsprotokoll",
"add": "HINZUFÜGEN",
"remove": "ENTFERNEN",
"none": "Keine",
"weight": "Gewicht",
"createdTask": "hat die Aufgabe erstellt."
},
"taskProgress": {
"markAsDoneTitle": "Aufgabe als erledigt markieren?",
"confirmMarkAsDone": "Ja, als erledigt markieren",
"cancelMarkAsDone": "Nein, aktuellen Status beibehalten",
"markAsDoneDescription": "Sie haben den Fortschritt auf 100% gesetzt. Möchten Sie den Aufgabenstatus auf \"Erledigt\" aktualisieren?"
}
}

View File

@@ -55,5 +55,18 @@
"selectCategory": "Kategorie auswählen",
"pleaseEnterAName": "Bitte geben Sie einen Namen ein",
"pleaseSelectACategory": "Bitte wählen Sie eine Kategorie aus",
"create": "Erstellen"
"create": "Erstellen",
"searchTasks": "Aufgaben suchen...",
"searchPlaceholder": "Suchen...",
"fieldsText": "Felder",
"loadingFilters": "Filter werden geladen...",
"noOptionsFound": "Keine Optionen gefunden",
"filtersActive": "Filter aktiv",
"filterActive": "Filter aktiv",
"clearAll": "Alle löschen",
"clearing": "Löschen...",
"cancel": "Abbrechen",
"search": "Suchen",
"groupedBy": "Gruppiert nach"
}

View File

@@ -36,9 +36,10 @@
"selectText": "Auswählen",
"labelsSelectorInputTip": "Enter drücken zum Erstellen!",
"addTaskText": "+ Aufgabe hinzufügen",
"addTaskText": "Aufgabe hinzufügen",
"addSubTaskText": "+ Unteraufgabe hinzufügen",
"addTaskInputPlaceholder": "Aufgabe eingeben und Enter drücken",
"noTasksInGroup": "Keine Aufgaben in dieser Gruppe",
"openButton": "Öffnen",
"okButton": "OK",
@@ -59,5 +60,74 @@
"convertToTask": "In Aufgabe umwandeln",
"delete": "Löschen",
"searchByNameInputPlaceholder": "Nach Namen suchen"
},
"setDueDate": "Fälligkeitsdatum festlegen",
"setStartDate": "Startdatum festlegen",
"clearDueDate": "Fälligkeitsdatum löschen",
"clearStartDate": "Startdatum löschen",
"dueDatePlaceholder": "Fälligkeitsdatum",
"startDatePlaceholder": "Startdatum",
"emptyStates": {
"noTaskGroups": "Keine Aufgabengruppen gefunden",
"noTaskGroupsDescription": "Aufgaben werden hier angezeigt, wenn sie erstellt oder Filter angewendet werden.",
"errorPrefix": "Fehler:",
"dragTaskFallback": "Aufgabe"
},
"customColumns": {
"addCustomColumn": "Benutzerdefinierte Spalte hinzufügen",
"customColumnHeader": "Benutzerdefinierte Spalte",
"customColumnSettings": "Einstellungen für benutzerdefinierte Spalte",
"noCustomValue": "Kein Wert",
"peopleField": "Personenfeld",
"noDate": "Kein Datum",
"unsupportedField": "Nicht unterstützter Feldtyp",
"modal": {
"addFieldTitle": "Feld hinzufügen",
"editFieldTitle": "Feld bearbeiten",
"fieldTitle": "Feldtitel",
"fieldTitleRequired": "Feldtitel ist erforderlich",
"columnTitlePlaceholder": "Spaltentitel",
"type": "Typ",
"deleteConfirmTitle": "Sind Sie sicher, dass Sie diese benutzerdefinierte Spalte löschen möchten?",
"deleteConfirmDescription": "Diese Aktion kann nicht rückgängig gemacht werden. Alle mit dieser Spalte verbundenen Daten werden dauerhaft gelöscht.",
"deleteButton": "Löschen",
"cancelButton": "Abbrechen",
"createButton": "Erstellen",
"updateButton": "Aktualisieren",
"createSuccessMessage": "Benutzerdefinierte Spalte erfolgreich erstellt",
"updateSuccessMessage": "Benutzerdefinierte Spalte erfolgreich aktualisiert",
"deleteSuccessMessage": "Benutzerdefinierte Spalte erfolgreich gelöscht",
"deleteErrorMessage": "Fehler beim Löschen der benutzerdefinierten Spalte",
"createErrorMessage": "Fehler beim Erstellen der benutzerdefinierten Spalte",
"updateErrorMessage": "Fehler beim Aktualisieren der benutzerdefinierten Spalte"
},
"fieldTypes": {
"people": "Personen",
"number": "Zahl",
"date": "Datum",
"selection": "Auswahl",
"checkbox": "Kontrollkästchen",
"labels": "Etiketten",
"key": "Schlüssel",
"formula": "Formel"
}
},
"indicators": {
"tooltips": {
"subtasks": "{{count}} Unteraufgabe",
"subtasks_plural": "{{count}} Unteraufgaben",
"comments": "{{count}} Kommentar",
"comments_plural": "{{count}} Kommentare",
"attachments": "{{count}} Anhang",
"attachments_plural": "{{count}} Anhänge",
"subscribers": "Aufgabe hat Abonnenten",
"dependencies": "Aufgabe hat Abhängigkeiten",
"recurring": "Wiederkehrende Aufgabe"
}
}
}

View File

@@ -0,0 +1,21 @@
{
"noTasksInGroup": "Keine Aufgaben in dieser Gruppe",
"noTasksInGroupDescription": "Fügen Sie eine Aufgabe hinzu, um zu beginnen",
"addFirstTask": "Fügen Sie Ihre erste Aufgabe hinzu",
"openTask": "Öffnen",
"subtask": "Unteraufgabe",
"subtasks": "Unteraufgaben",
"comment": "Kommentar",
"comments": "Kommentare",
"attachment": "Anhang",
"attachments": "Anhänge",
"enterSubtaskName": "Unteraufgabenname eingeben...",
"add": "Hinzufügen",
"cancel": "Abbrechen",
"renameGroup": "Gruppe umbenennen",
"renameStatus": "Status umbenennen",
"renamePhase": "Phase umbenennen",
"changeCategory": "Kategorie ändern",
"clickToEditGroupName": "Klicken Sie, um den Gruppennamen zu bearbeiten",
"enterGroupName": "Gruppennamen eingeben"
}

View File

@@ -17,8 +17,25 @@
"createTaskTemplate": "Aufgabenvorlage erstellen",
"apply": "Anwenden",
"createLabel": "+ Label erstellen",
"searchOrCreateLabel": "Label suchen oder erstellen...",
"hitEnterToCreate": "Enter drücken zum Erstellen",
"labelExists": "Label existiert bereits",
"pendingInvitation": "Einladung ausstehend",
"noMatchingLabels": "Keine passenden Labels",
"noLabels": "Keine Labels"
"noLabels": "Keine Labels",
"CHANGE_STATUS": "Status ändern",
"CHANGE_PRIORITY": "Priorität ändern",
"CHANGE_PHASE": "Phase ändern",
"ADD_LABELS": "Labels hinzufügen",
"ASSIGN_TO_ME": "Mir zuweisen",
"ASSIGN_MEMBERS": "Mitglieder zuweisen",
"ARCHIVE": "Archivieren",
"DELETE": "Löschen",
"CANCEL": "Abbrechen",
"CLEAR_SELECTION": "Auswahl löschen",
"TASKS_SELECTED": "{{count}} Aufgabe ausgewählt",
"TASKS_SELECTED_plural": "{{count}} Aufgaben ausgewählt",
"DELETE_TASKS_CONFIRM": "{{count}} Aufgabe löschen?",
"DELETE_TASKS_CONFIRM_plural": "{{count}} Aufgaben löschen?",
"DELETE_TASKS_WARNING": "Diese Aktion kann nicht rückgängig gemacht werden."
}

View File

@@ -25,7 +25,7 @@
"paymentMethod": "Payment Method",
"status": "Status",
"ltdUsers": "You can add up to {{ltd_users}} users.",
"totalSeats": "Total seats",
"availableSeats": "Available seats",
"addMoreSeats": "Add more seats",
@@ -103,11 +103,11 @@
"perMonthPerUser": "per user/month",
"viewInvoice": "View Invoice",
"switchToFreePlan": "Switch to Free Plan",
"expirestoday": "today",
"expirestomorrow": "tomorrow",
"expiredDaysAgo": "{{days}} days ago",
"continueWith": "Continue with {{plan}}",
"changeToPlan": "Change to {{plan}}",
"creditPlan": "Credit Plan",

View File

@@ -19,5 +19,13 @@
"unarchiveConfirm": "Are you sure you want to unarchive this project?",
"clickToFilter": "Click to filter by",
"noProjects": "No projects found",
"addToFavourites": "Add to favourites"
"addToFavourites": "Add to favourites",
"list": "List",
"group": "Group",
"listView": "List View",
"groupView": "Group View",
"groupBy": {
"category": "Category",
"client": "Client"
}
}

View File

@@ -19,5 +19,12 @@
"archive": "Archive",
"newTaskNamePlaceholder": "Write a task Name",
"newSubtaskNamePlaceholder": "Write a subtask Name"
"newSubtaskNamePlaceholder": "Write a subtask Name",
"untitledSection": "Untitled section",
"unmapped": "Unmapped",
"clickToChangeDate": "Click to change date",
"noDueDate": "No due date",
"save": "Save",
"clear": "Clear",
"nextWeek": "Next week"
}

View File

@@ -1,7 +1,7 @@
{
"configurePhases": "Configure Phases",
"phaseLabel": "Phase Label",
"enterPhaseName": "Enter a name for phase label",
"addOption": "Add Option",
"phaseOptions": "Phase Options:"
}
"configurePhases": "Configure Phases",
"phaseLabel": "Phase Label",
"enterPhaseName": "Enter a name for phase label",
"addOption": "Add Option",
"phaseOptions": "Phase Options:"
}

View File

@@ -47,5 +47,6 @@
"weightedProgress": "Weighted Progress",
"weightedProgressTooltip": "Calculate progress based on subtask weights",
"timeProgress": "Time-based Progress",
"timeProgressTooltip": "Calculate progress based on estimated time"
"timeProgressTooltip": "Calculate progress based on estimated time",
"enterProjectKey": "Enter project key"
}

View File

@@ -0,0 +1,14 @@
{
"taskList": "Task List",
"board": "Kanban Board",
"insights": "Insights",
"files": "Files",
"members": "Members",
"updates": "Updates",
"projectView": "Project View",
"loading": "Loading project...",
"error": "Error loading project",
"pinnedTab": "Pinned as default tab",
"pinTab": "Pin as default tab",
"unpinTab": "Unpin default tab"
}

View File

@@ -1,11 +1,11 @@
{
"importTaskTemplate": "Import Task Template",
"templateName": "Template Name",
"templateDescription": "Template Description",
"selectedTasks": "Selected Tasks",
"tasks": "Tasks",
"templates": "Templates",
"remove": "Remove",
"cancel": "Cancel",
"import": "Import"
"importTaskTemplate": "Import Task Template",
"templateName": "Template Name",
"templateDescription": "Template Description",
"selectedTasks": "Selected Tasks",
"tasks": "Tasks",
"templates": "Templates",
"remove": "Remove",
"cancel": "Cancel",
"import": "Import"
}

View File

@@ -1,8 +1,7 @@
{
"title": "Project Members",
"searchLabel": "Add members by adding their name or email",
"searchPlaceholder": "Type name or email",
"inviteAsAMember": "Invite as a member",
"inviteNewMemberByEmail": "Invite new member by email"
}
"title": "Project Members",
"searchLabel": "Add members by adding their name or email",
"searchPlaceholder": "Type name or email",
"inviteAsAMember": "Invite as a member",
"inviteNewMemberByEmail": "Invite new member by email"
}

View File

@@ -1,13 +1,29 @@
{
"importTasks": "Import tasks",
"createTask": "Create task",
"settings": "Settings",
"subscribe": "Subscribe",
"unsubscribe": "Unsubscribe",
"deleteProject": "Delete project",
"startDate": "Start date",
"endDate": "End date",
"projectSettings": "Project settings",
"projectSummary": "Project summary",
"receiveProjectSummary": "Receive a project summary every evening."
}
"importTasks": "Import tasks",
"importTask": "Import task",
"createTask": "Create task",
"settings": "Settings",
"subscribe": "Subscribe",
"unsubscribe": "Unsubscribe",
"deleteProject": "Delete project",
"startDate": "Start date",
"endDate": "End date",
"projectSettings": "Project settings",
"projectSummary": "Project summary",
"receiveProjectSummary": "Receive a project summary every evening.",
"refreshProject": "Refresh project",
"saveAsTemplate": "Save as template",
"invite": "Invite",
"subscribeTooltip": "Subscribe to project notifications",
"unsubscribeTooltip": "Unsubscribe from project notifications",
"refreshTooltip": "Refresh project data",
"settingsTooltip": "Open project settings",
"saveAsTemplateTooltip": "Save this project as a template",
"inviteTooltip": "Invite team members to this project",
"createTaskTooltip": "Create a new task",
"importTaskTooltip": "Import task from template",
"navigateBackTooltip": "Go back to projects list",
"projectStatusTooltip": "Project status",
"projectDatesInfo": "Project timeline information",
"projectCategoryTooltip": "Project category"
}

View File

@@ -2,4 +2,4 @@
"title": "Appearance",
"darkMode": "Dark Mode",
"darkModeDescription": "Switch between light and dark mode to customize your viewing experience."
}
}

View File

@@ -9,5 +9,6 @@
"saveChanges": "Save Changes",
"profileJoinedText": "Joined a month ago",
"profileLastUpdatedText": "Last updated a month ago",
"avatarTooltip": "Click to upload an avatar"
"avatarTooltip": "Click to upload an avatar",
"title": "Profile Settings"
}

View File

@@ -1,4 +1,5 @@
{
"title": "Team Members",
"nameColumn": "Name",
"projectsColumn": "Projects",
"emailColumn": "Email",
@@ -40,5 +41,7 @@
"ownerText": "Team Owner",
"addedText": "Added",
"updatedText": "Updated",
"noResultFound": "Type an email address and hit enter..."
"noResultFound": "Type an email address and hit enter...",
"jobTitlesFetchError": "Failed to fetch job titles",
"invitationResent": "Invitation resent successfully!"
}

View File

@@ -0,0 +1,16 @@
{
"title": "Teams",
"team": "Team",
"teams": "Teams",
"name": "Name",
"created": "Created",
"ownsBy": "Owns By",
"edit": "Edit",
"editTeam": "Edit Team",
"pinTooltip": "Click to pin this into the main menu",
"editTeamName": "Edit Team Name",
"updateName": "Update Name",
"namePlaceholder": "Name",
"nameRequired": "Please enter a Name",
"updateFailed": "Team name change failed!"
}

View File

@@ -24,7 +24,7 @@
},
"subTasks": {
"title": "Sub Tasks",
"add-sub-task": "+ Add Sub Task",
"add-sub-task": "Add Sub Task",
"refresh-sub-tasks": "Refresh Sub Tasks"
}
}
}

View File

@@ -31,4 +31,4 @@
"intervalWeeks": "Interval (weeks)",
"intervalMonths": "Interval (months)",
"saveChanges": "Save Changes"
}
}

View File

@@ -43,7 +43,7 @@
},
"subTasks": {
"title": "Sub Tasks",
"addSubTask": "+ Add Sub Task",
"addSubTask": "Add Sub Task",
"addSubTaskInputPlaceholder": "Type your task and hit enter",
"refreshSubTasks": "Refresh Sub Tasks",
"edit": "Edit",
@@ -69,7 +69,18 @@
"addComment": "+ Add new comment",
"noComments": "No comments yet. Be the first to comment!",
"delete": "Delete",
"confirmDeleteComment": "Are you sure you want to delete this comment?"
"confirmDeleteComment": "Are you sure you want to delete this comment?",
"addCommentPlaceholder": "Add a comment...",
"cancel": "Cancel",
"commentButton": "Comment",
"attachFiles": "Attach files",
"addMoreFiles": "Add more files",
"selectedFiles": "Selected Files (Up to 25MB, Maximum of {count})",
"maxFilesError": "You can only upload a maximum of {count} files",
"processFilesError": "Failed to process files",
"addCommentError": "Please add a comment or attach files",
"createdBy": "Created {time} by {user}",
"updatedTime": "Updated {time}"
},
"searchInputPlaceholder": "Search by name",
"pendingInvitation": "Pending Invitation"
@@ -79,10 +90,29 @@
"addTimeLog": "Add new time log",
"totalLogged": "Total Logged",
"exportToExcel": "Export to Excel",
"noTimeLogsFound": "No time logs found"
"noTimeLogsFound": "No time logs found",
"timeLogForm": {
"date": "Date",
"startTime": "Start Time",
"endTime": "End Time",
"workDescription": "Work Description",
"descriptionPlaceholder": "Add a description",
"logTime": "Log time",
"updateTime": "Update time",
"cancel": "Cancel",
"selectDateError": "Please select a date",
"selectStartTimeError": "Please select start time",
"selectEndTimeError": "Please select end time",
"endTimeAfterStartError": "End time must be after start time"
}
},
"taskActivityLogTab": {
"title": "Activity Log"
"title": "Activity Log",
"add": "ADD",
"remove": "REMOVE",
"none": "None",
"weight": "Weight",
"createdTask": "created the task."
},
"taskProgress": {
"markAsDoneTitle": "Mark Task as Done?",

View File

@@ -55,5 +55,18 @@
"selectCategory": "Select a category",
"pleaseEnterAName": "Please enter a name",
"pleaseSelectACategory": "Please select a category",
"create": "Create"
"create": "Create",
"searchTasks": "Search tasks...",
"searchPlaceholder": "Search...",
"fieldsText": "Fields",
"loadingFilters": "Loading filters...",
"noOptionsFound": "No options found",
"filtersActive": "filters active",
"filterActive": "filter active",
"clearAll": "Clear all",
"clearing": "Clearing...",
"cancel": "Cancel",
"search": "Search",
"groupedBy": "Grouped by"
}

View File

@@ -36,9 +36,10 @@
"selectText": "Select",
"labelsSelectorInputTip": "Hit enter to create!",
"addTaskText": "+ Add Task",
"addSubTaskText": "+ Add Sub Task",
"addTaskText": "Add Task",
"addSubTaskText": "Add Sub Task",
"addTaskInputPlaceholder": "Type your task and hit enter",
"noTasksInGroup": "No tasks in this group",
"openButton": "Open",
"okButton": "Ok",
@@ -47,7 +48,7 @@
"searchInputPlaceholder": "Search or create",
"assigneeSelectorInviteButton": "Invite a new member by email",
"labelInputPlaceholder": "Search or create",
"pendingInvitation": "Pending Invitation",
"contextMenu": {
@@ -59,5 +60,74 @@
"convertToTask": "Convert to Task",
"delete": "Delete",
"searchByNameInputPlaceholder": "Search by name"
},
"setDueDate": "Set due date",
"setStartDate": "Set start date",
"clearDueDate": "Clear due date",
"clearStartDate": "Clear start date",
"dueDatePlaceholder": "Due Date",
"startDatePlaceholder": "Start Date",
"emptyStates": {
"noTaskGroups": "No task groups found",
"noTaskGroupsDescription": "Tasks will appear here when they are created or when filters are applied.",
"errorPrefix": "Error:",
"dragTaskFallback": "Task"
},
"customColumns": {
"addCustomColumn": "Add a custom column",
"customColumnHeader": "Custom Column",
"customColumnSettings": "Custom column settings",
"noCustomValue": "No value",
"peopleField": "People field",
"noDate": "No date",
"unsupportedField": "Unsupported field type",
"modal": {
"addFieldTitle": "Add field",
"editFieldTitle": "Edit field",
"fieldTitle": "Field title",
"fieldTitleRequired": "Field title is required",
"columnTitlePlaceholder": "Column title",
"type": "Type",
"deleteConfirmTitle": "Are you sure you want to delete this custom column?",
"deleteConfirmDescription": "This action cannot be undone. All data associated with this column will be permanently deleted.",
"deleteButton": "Delete",
"cancelButton": "Cancel",
"createButton": "Create",
"updateButton": "Update",
"createSuccessMessage": "Custom column created successfully",
"updateSuccessMessage": "Custom column updated successfully",
"deleteSuccessMessage": "Custom column deleted successfully",
"deleteErrorMessage": "Failed to delete custom column",
"createErrorMessage": "Failed to create custom column",
"updateErrorMessage": "Failed to update custom column"
},
"fieldTypes": {
"people": "People",
"number": "Number",
"date": "Date",
"selection": "Selection",
"checkbox": "Checkbox",
"labels": "Labels",
"key": "Key",
"formula": "Formula"
}
},
"indicators": {
"tooltips": {
"subtasks": "{{count}} subtask",
"subtasks_plural": "{{count}} subtasks",
"comments": "{{count}} comment",
"comments_plural": "{{count}} comments",
"attachments": "{{count}} attachment",
"attachments_plural": "{{count}} attachments",
"subscribers": "Task has subscribers",
"dependencies": "Task has dependencies",
"recurring": "Recurring task"
}
}
}

View File

@@ -0,0 +1,35 @@
{
"noTasksInGroup": "No tasks in this group",
"noTasksInGroupDescription": "Add a task to get started",
"addFirstTask": "Add your first task",
"openTask": "Open",
"subtask": "subtask",
"subtasks": "subtasks",
"comment": "comment",
"comments": "comments",
"attachment": "attachment",
"attachments": "attachments",
"enterSubtaskName": "Enter subtask name...",
"add": "Add",
"cancel": "Cancel",
"renameGroup": "Rename Group",
"renameStatus": "Rename Status",
"renamePhase": "Rename Phase",
"changeCategory": "Change Category",
"clickToEditGroupName": "Click to edit group name",
"enterGroupName": "Enter group name",
"indicators": {
"tooltips": {
"subtasks": "{{count}} subtask",
"subtasks_plural": "{{count}} subtasks",
"comments": "{{count}} comment",
"comments_plural": "{{count}} comments",
"attachments": "{{count}} attachment",
"attachments_plural": "{{count}} attachments",
"subscribers": "Task has subscribers",
"dependencies": "Task has dependencies",
"recurring": "Recurring task"
}
}
}

View File

@@ -4,6 +4,7 @@
"cancelText": "Cancel",
"saveText": "Save",
"templateNameText": "Template Name",
"templateNameRequired": "Template name is required",
"selectedTasks": "Selected Tasks",
"removeTask": "Remove",
"cancelButton": "Cancel",

View File

@@ -1,24 +1,41 @@
{
"taskSelected": "task selected",
"tasksSelected": "tasks selected",
"changeStatus": "Change Status/ Prioriy/ Phases",
"changeLabel": "Change Label",
"assignToMe": "Assign to me",
"changeAssignees": "Change Assignees",
"archive": "Archive",
"unarchive": "Unarchive",
"delete": "Delete",
"moreOptions": "More options",
"deselectAll": "Deselect all",
"status": "Status",
"priority": "Priority",
"phase": "Phase",
"member": "Member",
"createTaskTemplate": "Create Task Template",
"apply": "Apply",
"createLabel": "+ Create Label",
"hitEnterToCreate": "Press Enter to create",
"pendingInvitation": "Pending Invitation",
"noMatchingLabels": "No matching labels",
"noLabels": "No labels"
}
"taskSelected": "task selected",
"tasksSelected": "tasks selected",
"changeStatus": "Change Status/ Prioriy/ Phases",
"changeLabel": "Change Label",
"assignToMe": "Assign to me",
"changeAssignees": "Change Assignees",
"archive": "Archive",
"unarchive": "Unarchive",
"delete": "Delete",
"moreOptions": "More options",
"deselectAll": "Deselect all",
"status": "Status",
"priority": "Priority",
"phase": "Phase",
"member": "Member",
"createTaskTemplate": "Create Task Template",
"apply": "Apply",
"createLabel": "+ Create Label",
"searchOrCreateLabel": "Search or create label...",
"hitEnterToCreate": "Press Enter to create",
"labelExists": "Label already exists",
"pendingInvitation": "Pending Invitation",
"noMatchingLabels": "No matching labels",
"noLabels": "No labels",
"CHANGE_STATUS": "Change Status",
"CHANGE_PRIORITY": "Change Priority",
"CHANGE_PHASE": "Change Phase",
"ADD_LABELS": "Add Labels",
"ASSIGN_TO_ME": "Assign to Me",
"ASSIGN_MEMBERS": "Assign Members",
"ARCHIVE": "Archive",
"DELETE": "Delete",
"CANCEL": "Cancel",
"CLEAR_SELECTION": "Clear Selection",
"TASKS_SELECTED": "{{count}} task selected",
"TASKS_SELECTED_plural": "{{count}} tasks selected",
"DELETE_TASKS_CONFIRM": "Delete {{count}} task?",
"DELETE_TASKS_CONFIRM_plural": "Delete {{count}} tasks?",
"DELETE_TASKS_WARNING": "This action cannot be undone."
}

View File

@@ -40,5 +40,18 @@
"noCategory": "No Category",
"noProjects": "No projects found",
"noTeams": "No teams found",
"noData": "No data found"
"noData": "No data found",
"groupBy": "Group by",
"groupByCategory": "Category",
"groupByTeam": "Team",
"groupByStatus": "Status",
"groupByNone": "None",
"clearSearch": "Clear search",
"selectedProjects": "Selected Projects",
"projectsSelected": "projects selected",
"showSelected": "Show Selected Only",
"expandAll": "Expand All",
"collapseAll": "Collapse All",
"ungrouped": "Ungrouped"
}

View File

@@ -1,5 +1,5 @@
{
"title": "Unauthorized!",
"subtitle": "You are not authorized to access this page",
"button": "Go to Home"
}
"title": "Unauthorized!",
"subtitle": "You are not authorized to access this page",
"button": "Go to Home"
}

View File

@@ -24,7 +24,7 @@
"paymentMethod": "Método de Pago",
"status": "Estado",
"ltdUsers": "Puedes agregar hasta {{ltd_users}} usuarios.",
"drawerTitle": "Canjear Código",
"label": "Canjear Código",
"drawerPlaceholder": "Ingrese su código de canje",
@@ -98,7 +98,7 @@
"perMonthPerUser": "por usuario / mes",
"viewInvoice": "Ver Factura",
"switchToFreePlan": "Cambiar a Plan Gratuito",
"expirestoday": "hoy",
"expirestomorrow": "mañana",
"expiredDaysAgo": "hace {{days}} días",

View File

@@ -19,5 +19,13 @@
"unarchiveConfirm": "¿Estás seguro de que deseas desarchivar este proyecto?",
"clickToFilter": "Clique para filtrar por",
"noProjects": "No se encontraron proyectos",
"addToFavourites": "Añadir a favoritos"
"addToFavourites": "Añadir a favoritos",
"list": "Lista",
"group": "Grupo",
"listView": "Vista de Lista",
"groupView": "Vista de Grupo",
"groupBy": {
"category": "Categoría",
"client": "Cliente"
}
}

View File

@@ -19,5 +19,12 @@
"archive": "Archivar",
"newTaskNamePlaceholder": "Escribe un nombre de tarea",
"newSubtaskNamePlaceholder": "Escribe un nombre de subtarea"
}
"newSubtaskNamePlaceholder": "Escribe un nombre de subtarea",
"untitledSection": "Sección sin título",
"unmapped": "Sin asignar",
"clickToChangeDate": "Haz clic para cambiar la fecha",
"noDueDate": "Sin fecha de vencimiento",
"save": "Guardar",
"clear": "Limpiar",
"nextWeek": "Próxima semana"
}

View File

@@ -1,7 +1,7 @@
{
"configurePhases": "Configurar fases",
"phaseLabel": "Etiqueta de fase",
"enterPhaseName": "Ingrese un nombre para la etiqueta de fase",
"addOption": "Agregar opción",
"phaseOptions": "Opciones de fase:"
}
"configurePhases": "Configurar fases",
"phaseLabel": "Etiqueta de fase",
"enterPhaseName": "Ingrese un nombre para la etiqueta de fase",
"addOption": "Agregar opción",
"phaseOptions": "Opciones de fase:"
}

View File

@@ -47,5 +47,6 @@
"weightedProgress": "Progreso Ponderado",
"weightedProgressTooltip": "Calcular el progreso basado en los pesos de las subtareas",
"timeProgress": "Progreso Basado en Tiempo",
"timeProgressTooltip": "Calcular el progreso basado en el tiempo estimado"
"timeProgressTooltip": "Calcular el progreso basado en el tiempo estimado",
"enterProjectKey": "Ingresa la clave del proyecto"
}

View File

@@ -0,0 +1,14 @@
{
"taskList": "Lista de Tareas",
"board": "Tablero Kanban",
"insights": "Análisis",
"files": "Archivos",
"members": "Miembros",
"updates": "Actualizaciones",
"projectView": "Vista del Proyecto",
"loading": "Cargando proyecto...",
"error": "Error al cargar el proyecto",
"pinnedTab": "Fijado como pestaña predeterminada",
"pinTab": "Fijar como pestaña predeterminada",
"unpinTab": "Desfijar pestaña predeterminada"
}

View File

@@ -1,11 +1,11 @@
{
"importTaskTemplate": "Importar plantilla de tarea",
"templateName": "Nombre de la plantilla",
"templateDescription": "Descripción de la plantilla",
"selectedTasks": "Tareas seleccionadas",
"tasks": "Tareas",
"templates": "Plantillas",
"remove": "Eliminar",
"cancel": "Cancelar",
"import": "Importar"
}
"importTaskTemplate": "Importar plantilla de tarea",
"templateName": "Nombre de la plantilla",
"templateDescription": "Descripción de la plantilla",
"selectedTasks": "Tareas seleccionadas",
"tasks": "Tareas",
"templates": "Plantillas",
"remove": "Eliminar",
"cancel": "Cancelar",
"import": "Importar"
}

View File

@@ -1,8 +1,7 @@
{
"title": "Miembros del Proyecto",
"searchLabel": "Agregar miembros ingresando su nombre o correo electrónico",
"searchPlaceholder": "Escriba nombre o correo electrónico",
"inviteAsAMember": "Invitar como miembro",
"inviteNewMemberByEmail": "Invitar nuevo miembro por correo electrónico"
}
"title": "Miembros del Proyecto",
"searchLabel": "Agregar miembros ingresando su nombre o correo electrónico",
"searchPlaceholder": "Escriba nombre o correo electrónico",
"inviteAsAMember": "Invitar como miembro",
"inviteNewMemberByEmail": "Invitar nuevo miembro por correo electrónico"
}

View File

@@ -1,13 +1,29 @@
{
"importTasks": "Importar tareas",
"createTask": "Crear tarea",
"settings": "Ajustes",
"subscribe": "Suscribirse",
"unsubscribe": "Cancelar suscripción",
"deleteProject": "Eliminar proyecto",
"startDate": "Fecha de inicio",
"endDate": "Fecha de finalización",
"projectSettings": "Ajustes del proyecto",
"projectSummary": "Resumen del proyecto",
"receiveProjectSummary": "Recibir un resumen del proyecto todas las noches."
}
"importTasks": "Importar tareas",
"importTask": "Importar tarea",
"createTask": "Crear tarea",
"settings": "Configuración",
"subscribe": "Suscribirse",
"unsubscribe": "Cancelar suscripción",
"deleteProject": "Eliminar proyecto",
"startDate": "Fecha de inicio",
"endDate": "Fecha de finalización",
"projectSettings": "Configuración del proyecto",
"projectSummary": "Resumen del proyecto",
"receiveProjectSummary": "Recibe un resumen del proyecto cada noche.",
"refreshProject": "Actualizar proyecto",
"saveAsTemplate": "Guardar como plantilla",
"invite": "Invitar",
"subscribeTooltip": "Suscribirse a notificaciones del proyecto",
"unsubscribeTooltip": "Cancelar suscripción a notificaciones del proyecto",
"refreshTooltip": "Actualizar datos del proyecto",
"settingsTooltip": "Abrir configuración del proyecto",
"saveAsTemplateTooltip": "Guardar este proyecto como plantilla",
"inviteTooltip": "Invitar miembros del equipo a este proyecto",
"createTaskTooltip": "Crear una nueva tarea",
"importTaskTooltip": "Importar tarea desde plantilla",
"navigateBackTooltip": "Volver a la lista de proyectos",
"projectStatusTooltip": "Estado del proyecto",
"projectDatesInfo": "Información de cronograma del proyecto",
"projectCategoryTooltip": "Categoría del proyecto"
}

View File

@@ -10,7 +10,7 @@
"taskIncludes": "¿Qué se debe incluir en la plantilla de las tareas?",
"taskIncludesOptions": {
"statuses": "Estados",
"phases": "Fases",
"phases": "Fases",
"labels": "Etiquetas",
"name": "Nombre",
"priority": "Prioridad",

View File

@@ -2,4 +2,4 @@
"title": "Apariencia",
"darkMode": "Modo Oscuro",
"darkModeDescription": "Cambia entre el modo claro y oscuro para personalizar tu experiencia visual."
}
}

View File

@@ -9,5 +9,6 @@
"saveChanges": "Guardar cambios",
"profileJoinedText": "Se unió hace un mes",
"profileLastUpdatedText": "Última actualización hace un mes",
"avatarTooltip": "Haz clic para subir un avatar"
"avatarTooltip": "Haz clic para subir un avatar",
"title": "Configuración del Perfil"
}

View File

@@ -1,4 +1,5 @@
{
"title": "Miembros del Equipo",
"nameColumn": "Nombre",
"projectsColumn": "Proyectos",
"emailColumn": "Correo electrónico",
@@ -40,5 +41,7 @@
"ownerText": "Propietario del equipo",
"addedText": "Agregado",
"updatedText": "Actualizado",
"noResultFound": "Escriba una dirección de correo electrónico y presione enter..."
"noResultFound": "Escriba una dirección de correo electrónico y presione enter...",
"jobTitlesFetchError": "Error al obtener los cargos",
"invitationResent": "¡Invitación reenviada exitosamente!"
}

View File

@@ -0,0 +1,16 @@
{
"title": "Equipos",
"team": "Equipo",
"teams": "Equipos",
"name": "Nombre",
"created": "Creado",
"ownsBy": "Pertenece a",
"edit": "Editar",
"editTeam": "Editar Equipo",
"pinTooltip": "Haz clic para fijar esto en el menú principal",
"editTeamName": "Editar Nombre del Equipo",
"updateName": "Actualizar Nombre",
"namePlaceholder": "Nombre",
"nameRequired": "Por favor ingresa un Nombre",
"updateFailed": "¡Falló el cambio de nombre del equipo!"
}

View File

@@ -27,4 +27,4 @@
"add-sub-task": "+ Añadir subtarea",
"refresh-sub-tasks": "Actualizar subtareas"
}
}
}

View File

@@ -31,4 +31,4 @@
"intervalWeeks": "Intervalo (semanas)",
"intervalMonths": "Intervalo (meses)",
"saveChanges": "Guardar cambios"
}
}

View File

@@ -1,93 +1,123 @@
{
"taskHeader": {
"taskNamePlaceholder": "Escribe tu tarea",
"deleteTask": "Eliminar tarea"
"taskNamePlaceholder": "Escriba su Tarea",
"deleteTask": "Eliminar Tarea"
},
"taskInfoTab": {
"title": "Información",
"details": {
"title": "Detalles",
"task-key": "Clave de tarea",
"task-key": "Clave de Tarea",
"phase": "Fase",
"assignees": "Asignados",
"due-date": "Fecha de vencimiento",
"time-estimation": "Estimación de tiempo",
"due-date": "Fecha de Vencimiento",
"time-estimation": "Estimación de Tiempo",
"priority": "Prioridad",
"labels": "Etiquetas",
"billable": "Facturable",
"notify": "Notificar",
"when-done-notify": "Al terminar, notificar",
"start-date": "Fecha de inicio",
"end-date": "Fecha de finalización",
"hide-start-date": "Ocultar fecha de inicio",
"show-start-date": "Mostrar fecha de inicio",
"start-date": "Fecha de Inicio",
"end-date": "Fecha de Fin",
"hide-start-date": "Ocultar Fecha de Inicio",
"show-start-date": "Mostrar Fecha de Inicio",
"hours": "Horas",
"minutes": "Minutos",
"progressValue": "Valor de Progreso",
"progressValueTooltip": "Establecer el porcentaje de progreso (0-100%)",
"progressValueRequired": "Por favor, introduce un valor de progreso",
"progressValueRequired": "Por favor, introduzca un valor de progreso",
"progressValueRange": "El progreso debe estar entre 0 y 100",
"taskWeight": "Peso de la Tarea",
"taskWeightTooltip": "Establecer el peso de esta subtarea (porcentaje)",
"taskWeightRequired": "Por favor, introduce un peso para la tarea",
"taskWeightRequired": "Por favor, introduzca un peso de tarea",
"taskWeightRange": "El peso debe estar entre 0 y 100",
"recurring": "Recurrente"
},
"labels": {
"labelInputPlaceholder": "Buscar o crear",
"labelsSelectorInputTip": "Pulse Enter para crear"
"labelsSelectorInputTip": "Presiona Enter para crear"
},
"description": {
"title": "Descripción",
"placeholder": "Añadir una descripción más detallada..."
},
"subTasks": {
"title": "Subtareas",
"addSubTask": "+ Añadir subtarea",
"addSubTaskInputPlaceholder": "Escribe tu tarea y pulsa enter",
"refreshSubTasks": "Actualizar subtareas",
"title": "Sub Tareas",
"addSubTask": "Agregar Sub Tarea",
"addSubTaskInputPlaceholder": "Escriba su tarea y presione enter",
"refreshSubTasks": "Actualizar Sub Tareas",
"edit": "Editar",
"delete": "Eliminar",
"confirmDeleteSubTask": "¿Estás seguro de que quieres eliminar esta subtarea?",
"deleteSubTask": "Eliminar subtarea"
"confirmDeleteSubTask": "¿Está seguro de que desea eliminar esta subtarea?",
"deleteSubTask": "Eliminar Sub Tarea"
},
"dependencies": {
"title": "Dependencias",
"addDependency": "+ Añadir nueva dependencia",
"addDependency": "+ Agregar nueva dependencia",
"blockedBy": "Bloqueado por",
"searchTask": "Escribe para buscar tarea",
"searchTask": "Escribir para buscar tarea",
"noTasksFound": "No se encontraron tareas",
"confirmDeleteDependency": "¿Estás seguro de que quieres eliminar?"
"confirmDeleteDependency": "¿Está seguro de que desea eliminar?"
},
"attachments": {
"title": "Adjuntos",
"chooseOrDropFileToUpload": "Elige o arrastra un archivo para subir",
"chooseOrDropFileToUpload": "Elija o arrastre un archivo para subir",
"uploading": "Subiendo..."
},
"comments": {
"title": "Comentarios",
"addComment": "+ Añadir nuevo comentario",
"noComments": "No hay comentarios todavía. ¡Sé el primero en comentar!",
"addComment": "+ Agregar nuevo comentario",
"noComments": "Aún no hay comentarios. ¡Sé el primero en comentar!",
"delete": "Eliminar",
"confirmDeleteComment": "¿Estás seguro de que quieres eliminar este comentario?"
"confirmDeleteComment": "¿Está seguro de que desea eliminar este comentario?",
"addCommentPlaceholder": "Agregar un comentario...",
"cancel": "Cancelar",
"commentButton": "Comentar",
"attachFiles": "Adjuntar archivos",
"addMoreFiles": "Agregar más archivos",
"selectedFiles": "Archivos Seleccionados (Hasta 25MB, Máximo {count})",
"maxFilesError": "Solo puede subir un máximo de {count} archivos",
"processFilesError": "Error al procesar archivos",
"addCommentError": "Por favor agregue un comentario o adjunte archivos",
"createdBy": "Creado {time} por {user}",
"updatedTime": "Actualizado {time}"
},
"searchInputPlaceholder": "Buscar por nombre",
"pendingInvitation": "Invitación pendiente"
"pendingInvitation": "Invitación Pendiente"
},
"taskTimeLogTab": {
"title": "Registro de tiempo",
"title": "Registro de Tiempo",
"addTimeLog": "Añadir nuevo registro de tiempo",
"totalLogged": "Total registrado",
"totalLogged": "Total Registrado",
"exportToExcel": "Exportar a Excel",
"noTimeLogsFound": "No se encontraron registros de tiempo"
"noTimeLogsFound": "No se encontraron registros de tiempo",
"timeLogForm": {
"date": "Fecha",
"startTime": "Hora de Inicio",
"endTime": "Hora de Fin",
"workDescription": "Descripción del Trabajo",
"descriptionPlaceholder": "Agregar una descripción",
"logTime": "Registrar tiempo",
"updateTime": "Actualizar tiempo",
"cancel": "Cancelar",
"selectDateError": "Por favor seleccione una fecha",
"selectStartTimeError": "Por favor seleccione la hora de inicio",
"selectEndTimeError": "Por favor seleccione la hora de fin",
"endTimeAfterStartError": "La hora de fin debe ser posterior a la hora de inicio"
}
},
"taskActivityLogTab": {
"title": "Registro de actividad"
"title": "Registro de Actividad",
"add": "AGREGAR",
"remove": "QUITAR",
"none": "Ninguno",
"weight": "Peso",
"createdTask": "creó la tarea."
},
"taskProgress": {
"markAsDoneTitle": "¿Marcar Tarea como Completada?",
"confirmMarkAsDone": "Sí, marcar como completada",
"cancelMarkAsDone": "No, mantener estado actual",
"markAsDoneDescription": "Has establecido el progreso al 100%. ¿Quieres actualizar el estado de la tarea a \"Completada\"?"
"markAsDoneDescription": "Ha establecido el progreso al 100%. ¿Le gustaría actualizar el estado de la tarea a \"Completada\"?"
}
}
}

View File

@@ -51,5 +51,18 @@
"selectCategory": "Seleccionar una categoría",
"pleaseEnterAName": "Por favor, ingrese un nombre",
"pleaseSelectACategory": "Por favor, seleccione una categoría",
"create": "Crear"
"create": "Crear",
"searchTasks": "Buscar tareas...",
"searchPlaceholder": "Buscar...",
"fieldsText": "Campos",
"loadingFilters": "Cargando filtros...",
"noOptionsFound": "No se encontraron opciones",
"filtersActive": "filtros activos",
"filterActive": "filtro activo",
"clearAll": "Limpiar todo",
"clearing": "Limpiando...",
"cancel": "Cancelar",
"search": "Buscar",
"groupedBy": "Agrupado por"
}

View File

@@ -36,8 +36,9 @@
"selectText": "Seleccionar",
"labelsSelectorInputTip": "¡Presiona enter para crear!",
"addTaskText": "+ Agregar tarea",
"addSubTaskText": "+ Agregar subtarea",
"addTaskText": "Agregar tarea",
"addSubTaskText": "Agregar subtarea",
"noTasksInGroup": "No hay tareas en este grupo",
"addTaskInputPlaceholder": "Escribe tu tarea y presiona enter",
"openButton": "Abrir",
@@ -47,7 +48,7 @@
"searchInputPlaceholder": "Buscar o crear",
"assigneeSelectorInviteButton": "Invitar a un nuevo miembro por correo",
"labelInputPlaceholder": "Buscar o crear",
"pendingInvitation": "Invitación pendiente",
"contextMenu": {
@@ -59,5 +60,74 @@
"convertToTask": "Convertir en tarea",
"delete": "Eliminar",
"searchByNameInputPlaceholder": "Buscar por nombre"
},
"setDueDate": "Establecer fecha de vencimiento",
"setStartDate": "Establecer fecha de inicio",
"clearDueDate": "Limpiar fecha de vencimiento",
"clearStartDate": "Limpiar fecha de inicio",
"dueDatePlaceholder": "Fecha de vencimiento",
"startDatePlaceholder": "Fecha de inicio",
"emptyStates": {
"noTaskGroups": "No se encontraron grupos de tareas",
"noTaskGroupsDescription": "Las tareas aparecerán aquí cuando se creen o cuando se apliquen filtros.",
"errorPrefix": "Error:",
"dragTaskFallback": "Tarea"
},
"customColumns": {
"addCustomColumn": "Agregar una columna personalizada",
"customColumnHeader": "Columna Personalizada",
"customColumnSettings": "Configuración de columna personalizada",
"noCustomValue": "Sin valor",
"peopleField": "Campo de personas",
"noDate": "Sin fecha",
"unsupportedField": "Tipo de campo no compatible",
"modal": {
"addFieldTitle": "Agregar campo",
"editFieldTitle": "Editar campo",
"fieldTitle": "Título del campo",
"fieldTitleRequired": "El título del campo es obligatorio",
"columnTitlePlaceholder": "Título de la columna",
"type": "Tipo",
"deleteConfirmTitle": "¿Está seguro de que desea eliminar esta columna personalizada?",
"deleteConfirmDescription": "Esta acción no se puede deshacer. Todos los datos asociados con esta columna se eliminarán permanentemente.",
"deleteButton": "Eliminar",
"cancelButton": "Cancelar",
"createButton": "Crear",
"updateButton": "Actualizar",
"createSuccessMessage": "Columna personalizada creada exitosamente",
"updateSuccessMessage": "Columna personalizada actualizada exitosamente",
"deleteSuccessMessage": "Columna personalizada eliminada exitosamente",
"deleteErrorMessage": "Error al eliminar la columna personalizada",
"createErrorMessage": "Error al crear la columna personalizada",
"updateErrorMessage": "Error al actualizar la columna personalizada"
},
"fieldTypes": {
"people": "Personas",
"number": "Número",
"date": "Fecha",
"selection": "Selección",
"checkbox": "Casilla de verificación",
"labels": "Etiquetas",
"key": "Clave",
"formula": "Fórmula"
}
},
"indicators": {
"tooltips": {
"subtasks": "{{count}} subtarea",
"subtasks_plural": "{{count}} subtareas",
"comments": "{{count}} comentario",
"comments_plural": "{{count}} comentarios",
"attachments": "{{count}} archivo adjunto",
"attachments_plural": "{{count}} archivos adjuntos",
"subscribers": "La tarea tiene suscriptores",
"dependencies": "La tarea tiene dependencias",
"recurring": "Tarea recurrente"
}
}
}

View File

@@ -0,0 +1,21 @@
{
"noTasksInGroup": "No hay tareas en este grupo",
"noTasksInGroupDescription": "Añade una tarea para comenzar",
"addFirstTask": "Añade tu primera tarea",
"openTask": "Abrir",
"subtask": "subtarea",
"subtasks": "subtareas",
"comment": "comentario",
"comments": "comentarios",
"attachment": "adjunto",
"attachments": "adjuntos",
"enterSubtaskName": "Ingresa el nombre de la subtarea...",
"add": "Añadir",
"cancel": "Cancelar",
"renameGroup": "Renombrar Grupo",
"renameStatus": "Renombrar Estado",
"renamePhase": "Renombrar Fase",
"changeCategory": "Cambiar Categoría",
"clickToEditGroupName": "Haz clic para editar el nombre del grupo",
"enterGroupName": "Ingresa el nombre del grupo"
}

View File

@@ -1,24 +1,41 @@
{
"taskSelected": "Tarea seleccionada",
"tasksSelected": "Tareas seleccionadas",
"changeStatus": "Cambiar estado/ prioridad/ fases",
"changeLabel": "Cambiar etiqueta",
"assignToMe": "Asignar a mí",
"changeAssignees": "Cambiar asignados",
"archive": "Archivar",
"unarchive": "Desarchivar",
"delete": "Eliminar",
"moreOptions": "Más opciones",
"deselectAll": "Deseleccionar todo",
"status": "Estado",
"priority": "Prioridad",
"phase": "Fase",
"member": "Miembro",
"createTaskTemplate": "Crear plantilla de tarea",
"apply": "Aplicar",
"createLabel": "+ Crear etiqueta",
"hitEnterToCreate": "Presione Enter para crear",
"pendingInvitation": "Invitación Pendiente",
"noMatchingLabels": "No hay etiquetas coincidentes",
"noLabels": "Sin etiquetas"
}
"taskSelected": "Tarea seleccionada",
"tasksSelected": "Tareas seleccionadas",
"changeStatus": "Cambiar estado/ prioridad/ fases",
"changeLabel": "Cambiar etiqueta",
"assignToMe": "Asignar a mí",
"changeAssignees": "Cambiar asignados",
"archive": "Archivar",
"unarchive": "Desarchivar",
"delete": "Eliminar",
"moreOptions": "Más opciones",
"deselectAll": "Deseleccionar todo",
"status": "Estado",
"priority": "Prioridad",
"phase": "Fase",
"member": "Miembro",
"createTaskTemplate": "Crear plantilla de tarea",
"apply": "Aplicar",
"createLabel": "+ Crear etiqueta",
"searchOrCreateLabel": "Buscar o crear etiqueta...",
"hitEnterToCreate": "Presione Enter para crear",
"labelExists": "La etiqueta ya existe",
"pendingInvitation": "Invitación Pendiente",
"noMatchingLabels": "No hay etiquetas coincidentes",
"noLabels": "Sin etiquetas",
"CHANGE_STATUS": "Cambiar Estado",
"CHANGE_PRIORITY": "Cambiar Prioridad",
"CHANGE_PHASE": "Cambiar Fase",
"ADD_LABELS": "Agregar Etiquetas",
"ASSIGN_TO_ME": "Asignar a Mí",
"ASSIGN_MEMBERS": "Asignar Miembros",
"ARCHIVE": "Archivar",
"DELETE": "Eliminar",
"CANCEL": "Cancelar",
"CLEAR_SELECTION": "Limpiar Selección",
"TASKS_SELECTED": "{{count}} tarea seleccionada",
"TASKS_SELECTED_plural": "{{count}} tareas seleccionadas",
"DELETE_TASKS_CONFIRM": "¿Eliminar {{count}} tarea?",
"DELETE_TASKS_CONFIRM_plural": "¿Eliminar {{count}} tareas?",
"DELETE_TASKS_WARNING": "Esta acción no se puede deshacer."
}

View File

@@ -7,7 +7,7 @@
"selectAll": "Seleccionar Todo",
"teams": "Equipos",
"searchByProject": "Buscar por nombre de proyecto",
"searchByProject": "Buscar por nombre del proyecto",
"projects": "Proyectos",
"searchByCategory": "Buscar por nombre de categoría",
@@ -37,8 +37,21 @@
"actualDays": "Días Reales",
"noCategories": "No se encontraron categorías",
"noCategory": "No Categoría",
"noCategory": "Sin Categoría",
"noProjects": "No se encontraron proyectos",
"noTeams": "No se encontraron equipos",
"noData": "No se encontraron datos"
"noData": "No se encontraron datos",
"groupBy": "Agrupar por",
"groupByCategory": "Categoría",
"groupByTeam": "Equipo",
"groupByStatus": "Estado",
"groupByNone": "Ninguno",
"clearSearch": "Limpiar búsqueda",
"selectedProjects": "Proyectos Seleccionados",
"projectsSelected": "proyectos seleccionados",
"showSelected": "Mostrar Solo Seleccionados",
"expandAll": "Expandir Todo",
"collapseAll": "Contraer Todo",
"ungrouped": "Sin Agrupar"
}

View File

@@ -1,5 +1,5 @@
{
"title": "¡No autorizado!",
"subtitle": "No tienes permisos para acceder a esta página",
"button": "Ir a Inicio"
}
"title": "¡No autorizado!",
"subtitle": "No tienes permisos para acceder a esta página",
"button": "Ir a Inicio"
}

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