137
.gitignore
vendored
137
.gitignore
vendored
@@ -1,82 +1,79 @@
|
|||||||
# Dependencies
|
# Dependencies
|
||||||
node_modules/
|
node_modules/
|
||||||
.pnp/
|
.pnp/
|
||||||
.pnp.js
|
.pnp.js
|
||||||
|
|
||||||
# Build outputs
|
# Build outputs
|
||||||
dist/
|
dist/
|
||||||
build/
|
build/
|
||||||
out/
|
out/
|
||||||
.next/
|
.next/
|
||||||
.nuxt/
|
.nuxt/
|
||||||
.cache/
|
.cache/
|
||||||
|
|
||||||
# Environment variables
|
# Environment variables
|
||||||
.env
|
.env
|
||||||
.env.local
|
.env.local
|
||||||
.env.development.local
|
.env.development.local
|
||||||
.env.test.local
|
.env.test.local
|
||||||
.env.production.local
|
.env.production.local
|
||||||
.env.development
|
.env.development
|
||||||
.env.production
|
.env.production
|
||||||
.env.*
|
.env.*
|
||||||
!.env.example
|
!.env.example
|
||||||
!.env.template
|
!.env.template
|
||||||
|
|
||||||
# Logs
|
# Logs
|
||||||
logs
|
logs
|
||||||
*.log
|
*.log
|
||||||
npm-debug.log*
|
npm-debug.log*
|
||||||
yarn-debug.log*
|
yarn-debug.log*
|
||||||
yarn-error.log*
|
yarn-error.log*
|
||||||
pnpm-debug.log*
|
pnpm-debug.log*
|
||||||
lerna-debug.log*
|
lerna-debug.log*
|
||||||
|
|
||||||
#backups
|
# Editor directories and files
|
||||||
pg_backups/
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea/
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
|
*.sublime-workspace
|
||||||
|
|
||||||
# Editor directories and files
|
# Testing
|
||||||
.vscode/*
|
coverage/
|
||||||
!.vscode/extensions.json
|
.nyc_output/
|
||||||
.idea/
|
|
||||||
.DS_Store
|
|
||||||
*.suo
|
|
||||||
*.ntvs*
|
|
||||||
*.njsproj
|
|
||||||
*.sln
|
|
||||||
*.sw?
|
|
||||||
*.sublime-workspace
|
|
||||||
|
|
||||||
# Testing
|
# Temp files
|
||||||
coverage/
|
.temp/
|
||||||
.nyc_output/
|
.tmp/
|
||||||
|
temp/
|
||||||
|
tmp/
|
||||||
|
|
||||||
# Temp files
|
# Debug
|
||||||
.temp/
|
.debug/
|
||||||
.tmp/
|
|
||||||
temp/
|
|
||||||
tmp/
|
|
||||||
|
|
||||||
# Debug
|
# Misc
|
||||||
.debug/
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
.thumbs.db
|
||||||
|
ehthumbs.db
|
||||||
|
Desktop.ini
|
||||||
|
$RECYCLE.BIN/
|
||||||
|
|
||||||
# Misc
|
# Yarn
|
||||||
.DS_Store
|
.yarn/*
|
||||||
Thumbs.db
|
!.yarn/patches
|
||||||
.thumbs.db
|
!.yarn/plugins
|
||||||
ehthumbs.db
|
!.yarn/releases
|
||||||
Desktop.ini
|
!.yarn/sdks
|
||||||
$RECYCLE.BIN/
|
!.yarn/versions
|
||||||
|
|
||||||
# Yarn
|
# TypeScript
|
||||||
.yarn/*
|
*.tsbuildinfo
|
||||||
!.yarn/patches
|
|
||||||
!.yarn/plugins
|
|
||||||
!.yarn/releases
|
|
||||||
!.yarn/sdks
|
|
||||||
!.yarn/versions
|
|
||||||
|
|
||||||
# TypeScript
|
|
||||||
*.tsbuildinfo
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
429
docs/enhanced-task-management-technical-guide.md
Normal file
429
docs/enhanced-task-management-technical-guide.md
Normal 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
|
||||||
275
docs/enhanced-task-management-user-guide.md
Normal file
275
docs/enhanced-task-management-user-guide.md
Normal 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
6
package-lock.json
generated
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"name": "worklenz",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
@@ -145,7 +145,7 @@ BEGIN
|
|||||||
SET progress_value = NULL,
|
SET progress_value = NULL,
|
||||||
progress_mode = NULL
|
progress_mode = NULL
|
||||||
WHERE project_id = _project_id
|
WHERE project_id = _project_id
|
||||||
AND progress_mode = _old_mode;
|
AND progress_mode::text::progress_mode_type = _old_mode;
|
||||||
END IF;
|
END IF;
|
||||||
|
|
||||||
RETURN NEW;
|
RETURN NEW;
|
||||||
|
|||||||
@@ -32,3 +32,37 @@ SELECT u.avatar_url,
|
|||||||
FROM team_members
|
FROM team_members
|
||||||
LEFT JOIN users u ON team_members.user_id = u.id;
|
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;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
|||||||
@@ -4325,6 +4325,7 @@ DECLARE
|
|||||||
_from_group UUID;
|
_from_group UUID;
|
||||||
_to_group UUID;
|
_to_group UUID;
|
||||||
_group_by TEXT;
|
_group_by TEXT;
|
||||||
|
_batch_size INT := 100; -- PERFORMANCE OPTIMIZATION: Batch size for large updates
|
||||||
BEGIN
|
BEGIN
|
||||||
_project_id = (_body ->> 'project_id')::UUID;
|
_project_id = (_body ->> 'project_id')::UUID;
|
||||||
_task_id = (_body ->> 'task_id')::UUID;
|
_task_id = (_body ->> 'task_id')::UUID;
|
||||||
@@ -4337,16 +4338,26 @@ BEGIN
|
|||||||
|
|
||||||
_group_by = (_body ->> 'group_by')::TEXT;
|
_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)
|
IF (_from_group <> _to_group OR (_from_group <> _to_group) IS NULL)
|
||||||
THEN
|
THEN
|
||||||
|
-- PERFORMANCE OPTIMIZATION: Batch update group changes
|
||||||
IF (_group_by = 'status')
|
IF (_group_by = 'status')
|
||||||
THEN
|
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;
|
END IF;
|
||||||
|
|
||||||
IF (_group_by = 'priority')
|
IF (_group_by = 'priority')
|
||||||
THEN
|
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;
|
END IF;
|
||||||
|
|
||||||
IF (_group_by = 'phase')
|
IF (_group_by = 'phase')
|
||||||
@@ -4365,14 +4376,15 @@ BEGIN
|
|||||||
END IF;
|
END IF;
|
||||||
END IF;
|
END IF;
|
||||||
|
|
||||||
|
-- PERFORMANCE OPTIMIZATION: Optimized sort order handling
|
||||||
IF ((_body ->> 'to_last_index')::BOOLEAN IS TRUE AND _from_index < _to_index)
|
IF ((_body ->> 'to_last_index')::BOOLEAN IS TRUE AND _from_index < _to_index)
|
||||||
THEN
|
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
|
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;
|
END IF;
|
||||||
ELSE
|
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 IF;
|
||||||
END
|
END
|
||||||
$$;
|
$$;
|
||||||
@@ -6372,3 +6384,121 @@ BEGIN
|
|||||||
);
|
);
|
||||||
END;
|
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
|
||||||
|
$$;
|
||||||
|
|||||||
10499
worklenz-backend/package-lock.json
generated
10499
worklenz-backend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -68,6 +68,7 @@
|
|||||||
"express-rate-limit": "^6.8.0",
|
"express-rate-limit": "^6.8.0",
|
||||||
"express-session": "^1.17.3",
|
"express-session": "^1.17.3",
|
||||||
"express-validator": "^6.15.0",
|
"express-validator": "^6.15.0",
|
||||||
|
"grunt-cli": "^1.5.0",
|
||||||
"helmet": "^6.2.0",
|
"helmet": "^6.2.0",
|
||||||
"hpp": "^0.2.3",
|
"hpp": "^0.2.3",
|
||||||
"http-errors": "^2.0.0",
|
"http-errors": "^2.0.0",
|
||||||
@@ -85,7 +86,6 @@
|
|||||||
"passport-local": "^1.0.0",
|
"passport-local": "^1.0.0",
|
||||||
"path": "^0.12.7",
|
"path": "^0.12.7",
|
||||||
"pg": "^8.14.1",
|
"pg": "^8.14.1",
|
||||||
"pg-native": "^3.3.0",
|
|
||||||
"pug": "^3.0.2",
|
"pug": "^3.0.2",
|
||||||
"redis": "^4.6.7",
|
"redis": "^4.6.7",
|
||||||
"sanitize-html": "^2.11.0",
|
"sanitize-html": "^2.11.0",
|
||||||
@@ -93,8 +93,10 @@
|
|||||||
"sharp": "^0.32.6",
|
"sharp": "^0.32.6",
|
||||||
"slugify": "^1.6.6",
|
"slugify": "^1.6.6",
|
||||||
"socket.io": "^4.7.1",
|
"socket.io": "^4.7.1",
|
||||||
|
"tinymce": "^7.8.0",
|
||||||
"uglify-js": "^3.17.4",
|
"uglify-js": "^3.17.4",
|
||||||
"winston": "^3.10.0",
|
"winston": "^3.10.0",
|
||||||
|
"worklenz-backend": "file:",
|
||||||
"xss-filters": "^1.2.7"
|
"xss-filters": "^1.2.7"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -102,15 +104,17 @@
|
|||||||
"@babel/preset-typescript": "^7.22.5",
|
"@babel/preset-typescript": "^7.22.5",
|
||||||
"@types/bcrypt": "^5.0.0",
|
"@types/bcrypt": "^5.0.0",
|
||||||
"@types/bluebird": "^3.5.38",
|
"@types/bluebird": "^3.5.38",
|
||||||
|
"@types/body-parser": "^1.19.2",
|
||||||
"@types/compression": "^1.7.2",
|
"@types/compression": "^1.7.2",
|
||||||
"@types/connect-flash": "^0.0.37",
|
"@types/connect-flash": "^0.0.37",
|
||||||
"@types/cookie-parser": "^1.4.3",
|
"@types/cookie-parser": "^1.4.3",
|
||||||
"@types/cron": "^2.0.1",
|
"@types/cron": "^2.0.1",
|
||||||
"@types/crypto-js": "^4.2.2",
|
"@types/crypto-js": "^4.2.2",
|
||||||
"@types/csurf": "^1.11.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": "^1.0.2",
|
||||||
"@types/express-brute-redis": "^0.0.4",
|
"@types/express-brute-redis": "^0.0.4",
|
||||||
|
"@types/express-serve-static-core": "^4.17.34",
|
||||||
"@types/express-session": "^1.17.7",
|
"@types/express-session": "^1.17.7",
|
||||||
"@types/fs-extra": "^9.0.13",
|
"@types/fs-extra": "^9.0.13",
|
||||||
"@types/hpp": "^0.2.2",
|
"@types/hpp": "^0.2.2",
|
||||||
|
|||||||
@@ -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: [] }));
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -134,6 +134,25 @@ export default class TaskStatusesController extends WorklenzControllerBase {
|
|||||||
return res.status(200).send(new ServerResponse(true, data));
|
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()
|
@HandleExceptions()
|
||||||
public static async updateStatusOrder(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
public static async updateStatusOrder(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||||
const q = `SELECT update_status_order($1);`;
|
const q = `SELECT update_status_order($1);`;
|
||||||
|
|||||||
@@ -50,11 +50,16 @@ export default class TasksControllerBase extends WorklenzControllerBase {
|
|||||||
task.progress = parseInt(task.progress_value);
|
task.progress = parseInt(task.progress_value);
|
||||||
task.complete_ratio = 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 {
|
else {
|
||||||
task.progress = task.total_minutes_spent && task.total_minutes
|
// Only calculate progress based on time if time-based progress is enabled for the project
|
||||||
? ~~(task.total_minutes_spent / task.total_minutes * 100)
|
if (task.project_use_time_progress && task.total_minutes_spent && task.total_minutes) {
|
||||||
: 0;
|
// 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
|
// Set complete_ratio to match progress
|
||||||
task.complete_ratio = task.progress;
|
task.complete_ratio = task.progress;
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -6,11 +6,11 @@ import { isProduction } from "../shared/utils";
|
|||||||
const pgSession = require("connect-pg-simple")(session);
|
const pgSession = require("connect-pg-simple")(session);
|
||||||
|
|
||||||
export default session({
|
export default session({
|
||||||
name: process.env.SESSION_NAME || "worklenz.sid",
|
name: process.env.SESSION_NAME,
|
||||||
secret: process.env.SESSION_SECRET || "development-secret-key",
|
secret: process.env.SESSION_SECRET || "development-secret-key",
|
||||||
proxy: true,
|
proxy: false,
|
||||||
resave: false,
|
resave: false,
|
||||||
saveUninitialized: false,
|
saveUninitialized: true,
|
||||||
rolling: true,
|
rolling: true,
|
||||||
store: new pgSession({
|
store: new pgSession({
|
||||||
pool: db.pool,
|
pool: db.pool,
|
||||||
@@ -18,9 +18,10 @@ export default session({
|
|||||||
}),
|
}),
|
||||||
cookie: {
|
cookie: {
|
||||||
path: "/",
|
path: "/",
|
||||||
secure: isProduction(), // Use secure cookies in production
|
// secure: isProduction(),
|
||||||
httpOnly: true,
|
// httpOnly: isProduction(),
|
||||||
sameSite: "lax", // Standard setting for same-origin requests
|
// sameSite: "none",
|
||||||
|
// domain: isProduction() ? ".worklenz.com" : undefined,
|
||||||
maxAge: 30 * 24 * 60 * 60 * 1000 // 30 days
|
maxAge: 30 * 24 * 60 * 60 * 1000 // 30 days
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
20
worklenz-backend/src/public/tinymce/package-lock.json
generated
Normal file
20
worklenz-backend/src/public/tinymce/package-lock.json
generated
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -28,5 +28,8 @@
|
|||||||
"homepage": "https://www.tiny.cloud/",
|
"homepage": "https://www.tiny.cloud/",
|
||||||
"bugs": {
|
"bugs": {
|
||||||
"url": "https://github.com/tinymce/tinymce/issues"
|
"url": "https://github.com/tinymce/tinymce/issues"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"tinymce": "file:"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ projectsApiRouter.get("/update-exist-sort-order", safeControllerFunction(Project
|
|||||||
|
|
||||||
projectsApiRouter.post("/", teamOwnerOrAdminValidator, projectsBodyValidator, safeControllerFunction(ProjectsController.create));
|
projectsApiRouter.post("/", teamOwnerOrAdminValidator, projectsBodyValidator, safeControllerFunction(ProjectsController.create));
|
||||||
projectsApiRouter.get("/", safeControllerFunction(ProjectsController.get));
|
projectsApiRouter.get("/", safeControllerFunction(ProjectsController.get));
|
||||||
|
projectsApiRouter.get("/grouped", safeControllerFunction(ProjectsController.getGrouped));
|
||||||
projectsApiRouter.get("/my-task-projects", safeControllerFunction(ProjectsController.getMyProjectsToTasks));
|
projectsApiRouter.get("/my-task-projects", safeControllerFunction(ProjectsController.getMyProjectsToTasks));
|
||||||
projectsApiRouter.get("/my-projects", safeControllerFunction(ProjectsController.getMyProjects));
|
projectsApiRouter.get("/my-projects", safeControllerFunction(ProjectsController.getMyProjects));
|
||||||
projectsApiRouter.get("/all", safeControllerFunction(ProjectsController.getAllProjects));
|
projectsApiRouter.get("/all", safeControllerFunction(ProjectsController.getAllProjects));
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ statusesApiRouter.put("/order", statusOrderValidator, safeControllerFunction(Tas
|
|||||||
statusesApiRouter.get("/categories", safeControllerFunction(TaskStatusesController.getCategories));
|
statusesApiRouter.get("/categories", safeControllerFunction(TaskStatusesController.getCategories));
|
||||||
statusesApiRouter.get("/:id", idParamValidator, safeControllerFunction(TaskStatusesController.getById));
|
statusesApiRouter.get("/:id", idParamValidator, safeControllerFunction(TaskStatusesController.getById));
|
||||||
statusesApiRouter.put("/name/:id", projectManagerValidator, idParamValidator, taskStatusBodyValidator, safeControllerFunction(TaskStatusesController.updateName));
|
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.put("/:id", projectManagerValidator, idParamValidator, taskStatusBodyValidator, safeControllerFunction(TaskStatusesController.update));
|
||||||
statusesApiRouter.delete("/:id", projectManagerValidator, idParamValidator, statusDeleteValidator, safeControllerFunction(TaskStatusesController.deleteById));
|
statusesApiRouter.delete("/:id", projectManagerValidator, idParamValidator, statusDeleteValidator, safeControllerFunction(TaskStatusesController.deleteById));
|
||||||
|
|
||||||
|
|||||||
@@ -42,6 +42,9 @@ tasksApiRouter.get("/list/columns/:id", idParamValidator, safeControllerFunction
|
|||||||
tasksApiRouter.put("/list/columns/:id", idParamValidator, safeControllerFunction(TaskListColumnsController.toggleColumn));
|
tasksApiRouter.put("/list/columns/:id", idParamValidator, safeControllerFunction(TaskListColumnsController.toggleColumn));
|
||||||
|
|
||||||
tasksApiRouter.get("/list/v2/:id", idParamValidator, safeControllerFunction(getList));
|
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.get("/assignees/:id", idParamValidator, safeControllerFunction(TasksController.getProjectTaskAssignees));
|
||||||
|
|
||||||
tasksApiRouter.put("/bulk/status", mapTasksToBulkUpdate, bulkTasksStatusValidator, safeControllerFunction(TasksController.bulkChangeStatus));
|
tasksApiRouter.put("/bulk/status", mapTasksToBulkUpdate, bulkTasksStatusValidator, safeControllerFunction(TasksController.bulkChangeStatus));
|
||||||
|
|||||||
@@ -19,7 +19,8 @@ export async function on_project_subscriber_change(_io: Server, socket: Socket,
|
|||||||
const isSubscribe = data.mode == 0;
|
const isSubscribe = data.mode == 0;
|
||||||
const q = isSubscribe
|
const q = isSubscribe
|
||||||
? `INSERT INTO project_subscribers (user_id, project_id, team_member_id)
|
? `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
|
: `DELETE
|
||||||
FROM project_subscribers
|
FROM project_subscribers
|
||||||
WHERE user_id = $1
|
WHERE user_id = $1
|
||||||
@@ -27,7 +28,7 @@ export async function on_project_subscriber_change(_io: Server, socket: Socket,
|
|||||||
AND team_member_id = $3;`;
|
AND team_member_id = $3;`;
|
||||||
await db.query(q, [data.user_id, data.project_id, data.team_member_id]);
|
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);
|
socket.emit(SocketEvents.PROJECT_SUBSCRIBERS_CHANGE.toString(), subscribers);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -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 q = `SELECT create_quick_task($1) AS task;`;
|
||||||
const body = JSON.parse(data as string);
|
const body = JSON.parse(data as string);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
body.name = (body.name || "").trim();
|
body.name = (body.name || "").trim();
|
||||||
body.priority_id = body.priority_id?.trim() || null;
|
body.priority_id = body.priority_id?.trim() || null;
|
||||||
body.status_id = body.status_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);
|
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) {
|
} catch (error) {
|
||||||
log_error(error);
|
log_error(error);
|
||||||
|
socket.emit(SocketEvents.QUICK_TASK.toString(), null);
|
||||||
}
|
}
|
||||||
|
|
||||||
socket.emit(SocketEvents.QUICK_TASK.toString(), null);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(), []);
|
socket.emit(SocketEvents.TASK_SORT_ORDER_CHANGE.toString(), []);
|
||||||
}
|
}
|
||||||
@@ -58,10 +58,10 @@ export async function on_task_status_change(_io: Server, socket: Socket, data?:
|
|||||||
FROM tasks
|
FROM tasks
|
||||||
WHERE id = $1
|
WHERE id = $1
|
||||||
`, [body.task_id]);
|
`, [body.task_id]);
|
||||||
|
|
||||||
const currentProgress = progressResult.rows[0]?.progress_value;
|
const currentProgress = progressResult.rows[0]?.progress_value;
|
||||||
const isManualProgress = progressResult.rows[0]?.manual_progress;
|
const isManualProgress = progressResult.rows[0]?.manual_progress;
|
||||||
|
|
||||||
// Only update if not already 100%
|
// Only update if not already 100%
|
||||||
if (currentProgress !== 100) {
|
if (currentProgress !== 100) {
|
||||||
// Update progress to 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
|
SET progress_value = 100, manual_progress = TRUE
|
||||||
WHERE id = $1
|
WHERE id = $1
|
||||||
`, [body.task_id]);
|
`, [body.task_id]);
|
||||||
|
|
||||||
log(`Task ${body.task_id} moved to done status - progress automatically set to 100%`, null);
|
log(`Task ${body.task_id} moved to done status - progress automatically set to 100%`, null);
|
||||||
|
|
||||||
// Log the progress change to activity logs
|
// Log the progress change to activity logs
|
||||||
await logProgressChange({
|
await logProgressChange({
|
||||||
task_id: body.task_id,
|
task_id: body.task_id,
|
||||||
@@ -80,7 +80,7 @@ export async function on_task_status_change(_io: Server, socket: Socket, data?:
|
|||||||
new_value: "100",
|
new_value: "100",
|
||||||
socket
|
socket
|
||||||
});
|
});
|
||||||
|
|
||||||
// If this is a subtask, update parent task progress
|
// If this is a subtask, update parent task progress
|
||||||
if (body.parent_task) {
|
if (body.parent_task) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@@ -88,6 +88,23 @@ export async function on_task_status_change(_io: Server, socket: Socket, data?:
|
|||||||
}, 100);
|
}, 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);
|
const info = await TasksControllerV2.getTaskCompleteRatio(body.parent_task || body.task_id);
|
||||||
|
|||||||
@@ -2,31 +2,35 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
|
|
||||||
<head>
|
<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="text/html; charset=utf-8" http-equiv="Content-Type">
|
||||||
<meta content="width=device-width,initial-scale=1" name="viewport">
|
<meta content="width=device-width,initial-scale=1" name="viewport">
|
||||||
<style>
|
<style>
|
||||||
* {
|
* {
|
||||||
box-sizing: border-box
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0
|
padding: 0;
|
||||||
|
background: #f6f8fa;
|
||||||
|
font-family: 'Mada', 'Segoe UI', Arial, sans-serif;
|
||||||
|
color: #222;
|
||||||
}
|
}
|
||||||
|
|
||||||
a[x-apple-data-detectors] {
|
a[x-apple-data-detectors] {
|
||||||
color: inherit !important;
|
color: inherit !important;
|
||||||
text-decoration: inherit !important
|
text-decoration: inherit !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
#MessageViewBody a {
|
#MessageViewBody a {
|
||||||
color: inherit;
|
color: inherit;
|
||||||
text-decoration: none
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
p {
|
p {
|
||||||
line-height: inherit
|
line-height: 1.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
.padding-30 {
|
.padding-30 {
|
||||||
@@ -37,272 +41,201 @@
|
|||||||
padding: 0px 20px;
|
padding: 0px 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.desktop_hide,
|
.card {
|
||||||
.desktop_hide table {
|
background: #fff;
|
||||||
mso-hide: all;
|
border-radius: 16px;
|
||||||
display: none;
|
box-shadow: 0 2px 12px rgba(24, 144, 255, 0.08);
|
||||||
max-height: 0;
|
margin-bottom: 32px;
|
||||||
overflow: hidden
|
padding: 32px 32px 24px 32px;
|
||||||
|
transition: box-shadow 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 525px) {
|
.card h3 {
|
||||||
.desktop_hide table.icons-inner {
|
color: #1890ff;
|
||||||
display: inline-block !important
|
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 {
|
.main-btn {
|
||||||
text-align: center
|
width: 90%;
|
||||||
|
font-size: 16px;
|
||||||
|
padding: 12px 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
body {
|
||||||
|
background: #181a1b;
|
||||||
|
color: #e6e6e6;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icons-inner td {
|
.card {
|
||||||
margin: 0 auto
|
background: #23272a;
|
||||||
|
box-shadow: 0 2px 12px rgba(24, 144, 255, 0.13);
|
||||||
}
|
}
|
||||||
|
|
||||||
.row-content {
|
.main-btn {
|
||||||
width: 95% !important
|
background: #1890ff;
|
||||||
|
color: #fff;
|
||||||
|
border: 2px solid #1890ff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mobile_hide {
|
.main-btn:hover {
|
||||||
display: none
|
background: #40a9ff;
|
||||||
|
color: #fff;
|
||||||
|
border-color: #40a9ff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stack .column {
|
.logo-light {
|
||||||
width: 100%;
|
display: none !important;
|
||||||
display: block
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.mobile_hide {
|
.logo-dark {
|
||||||
min-height: 0;
|
display: block !important;
|
||||||
max-height: 0;
|
|
||||||
max-width: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
font-size: 0
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.desktop_hide,
|
.logo-light {
|
||||||
.desktop_hide table {
|
display: block;
|
||||||
display: table !important;
|
}
|
||||||
max-height: none !important
|
|
||||||
}
|
.logo-dark {
|
||||||
|
display: none;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body style="background-color:#fff;margin:0;padding:0;-webkit-text-size-adjust:none;text-size-adjust:none">
|
<body>
|
||||||
<table border="0" cellpadding="0" cellspacing="0" class="nl-container" role="presentation"
|
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="background: #f6f8fa;">
|
||||||
style="mso-table-lspace:0;mso-table-rspace:0;background-image:none;background-position:top left;background-size:auto;background-repeat:no-repeat"
|
<tr>
|
||||||
width="100%">
|
<td align="center">
|
||||||
<tbody>
|
<table border="0" cellpadding="0" cellspacing="0" width="720" style="max-width: 98vw;">
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td align="center" style="padding: 32px 0 18px 0;">
|
||||||
<table align="center" border="0" cellpadding="0" cellspacing="0" class="row row-1" role="presentation"
|
<a href="https://worklenz.com" target="_blank" style="display: inline-block;">
|
||||||
style="mso-table-lspace:0;mso-table-rspace:0" width="100%">
|
<img class="logo-light"
|
||||||
<tbody>
|
src="https://s3.us-west-2.amazonaws.com/worklenz.com/assets/worklenz-light-mode.png"
|
||||||
<tr>
|
alt="Worklenz Light Logo" style="width: 170px; margin-bottom: 0; display: block;" />
|
||||||
<td>
|
<img class="logo-dark"
|
||||||
|
src="https://s3.us-west-2.amazonaws.com/worklenz.com/assets/worklenz-dark-mode.png"
|
||||||
<table border="0" cellpadding="0" cellspacing="0" class="image_block block-1 padding-20"
|
alt="Worklenz Dark Logo" style="width: 170px; margin-bottom: 0; display: none;" />
|
||||||
role="presentation" style="mso-table-lspace:0;mso-table-rspace:0;margin-left: auto;margin-right: auto;padding-top: 20px;
|
</a>
|
||||||
padding-bottom: 20px;" width="300">
|
</td>
|
||||||
<tr>
|
</tr>
|
||||||
<td class="pad" style="width:100%;padding-right:0;padding-left:0">
|
<tr>
|
||||||
<div align="left" class="alignment" style="line-height:10px">
|
<td>
|
||||||
<a href="https://worklenz.com" style="outline:none;width: 170px;" tabindex="-1"
|
<div class="card">
|
||||||
target="_blank"><img
|
<h3>🚀 New Tasks List & Kanban Board</h3>
|
||||||
src="https://worklenz.s3.amazonaws.com/email-templates-assets/email-logo.png"
|
<ul class="feature-list">
|
||||||
style="display:block;max-width: 300px;height:auto;border:0;max-width:100%;margin-top: 10px;margin-bottom: 0px;"></a>
|
<li>Performance optimized for faster loading</li>
|
||||||
</div>
|
<li>Redesigned UI for clarity and speed</li>
|
||||||
</td>
|
<li>Advanced filters for easier task management</li>
|
||||||
</tr>
|
</ul>
|
||||||
</table>
|
<img src="https://s3.us-west-2.amazonaws.com/worklenz.com/gifs/WL20250708/task-list-v2.gif"
|
||||||
<table align="center" border="0" cellpadding="0" cellspacing="0" class="row-content stack"
|
alt="New Task List">
|
||||||
role="presentation" style="mso-table-lspace:0;mso-table-rspace:0;color:#000;width:720px;"
|
<img src="https://s3.us-west-2.amazonaws.com/worklenz.com/gifs/WL20250708/kanban-v2.gif"
|
||||||
width="475">
|
alt="New Kanban Board">
|
||||||
<tbody>
|
</div>
|
||||||
<tr>
|
<div class="card">
|
||||||
<td class="column column-1"
|
<h3>📁 Group View in Projects List</h3>
|
||||||
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"
|
<ul class="feature-list">
|
||||||
width="100%">
|
<li>Toggle between list and group view</li>
|
||||||
|
<li>Group projects by client or category</li>
|
||||||
<table border="0" cellpadding="0" cellspacing="0" class="image_block block-3"
|
<li>Improved navigation and organization</li>
|
||||||
role="presentation" style="mso-table-lspace:0;mso-table-rspace:0" width="100%">
|
</ul>
|
||||||
|
<img src="https://s3.us-west-2.amazonaws.com/worklenz.com/gifs/WL20250708/project-list-group-view.gif"
|
||||||
<tr>
|
alt="Project List Group View">
|
||||||
<td class="pad" style="width:100%;padding-right:0;padding-left:0">
|
</div>
|
||||||
<div align="center" class="alignment" style="line-height:10px"><img
|
<div class="card">
|
||||||
src="https://worklenz.s3.amazonaws.com/email-templates-assets/under-maintenance.png"
|
<h3>🌐 New Language Support</h3>
|
||||||
style="display:block;height:auto;border:0;width:180px;max-width:100%;/* margin-top: 30px; */margin-bottom: 10px;"
|
<span class="lang-badge">Deutsch (DE)</span>
|
||||||
width="180">
|
<span class="lang-badge">Shqip (ALB)</span>
|
||||||
</div>
|
<p style="margin-top: 10px;">Worklenz is now available in German and Albanian!</p>
|
||||||
</td>
|
</div>
|
||||||
</tr>
|
<div class="card">
|
||||||
</table>
|
<h3>🛠️ Bug Fixes & UI Improvements</h3>
|
||||||
|
<ul class="feature-list">
|
||||||
<table border="0" cellpadding="8" cellspacing="0" class="paragraph_block block-5 padding-30"
|
<li>General bug fixes</li>
|
||||||
role="presentation" style="mso-table-lspace:0;mso-table-rspace:0;word-break:break-word"
|
<li>UI/UX enhancements for a smoother experience</li>
|
||||||
width="100%">
|
<li>Performance improvements across the platform</li>
|
||||||
<tr>
|
</ul>
|
||||||
<td class="pad">
|
</div>
|
||||||
<div
|
<div style="text-align: center;">
|
||||||
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;">
|
<a href="https://app.worklenz.com/auth" target="_blank" class="main-btn">See what's new</a>
|
||||||
<h3 style="margin-bottom: 0;">Project Roadmap Redesign</h3>
|
</div>
|
||||||
<p>
|
</td>
|
||||||
|
</tr>
|
||||||
Experience a comprehensive visual representation of task progression within your projects.
|
<tr>
|
||||||
The sequential arrangement unfolds seamlessly in a user-friendly timeline format, allowing
|
<td style="padding: 32px 0 0 0;">
|
||||||
for effortless understanding and efficient project management.
|
<hr style="border: none; border-top: 1px solid #e6e6e6; margin: 32px 0 16px 0;">
|
||||||
</p>
|
<p style="font-family:sans-serif;text-decoration:none; text-align: center; color: #888; font-size: 15px;">
|
||||||
<img src="https://worklenz.s3.amazonaws.com/gifs/WL20231114/roadmap.gif"
|
Click <a href="{{unsubscribe}}" target="_blank" style="color: #1890ff;">here</a> to unsubscribe and
|
||||||
style="width: 100%;margin: auto;" alt="Revamped Reporting">
|
manage your email preferences.
|
||||||
</div>
|
</p>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
</td>
|
||||||
<table border="0" cellpadding="8" cellspacing="0" class="paragraph_block block-5 padding-30"
|
</tr>
|
||||||
role="presentation" style="mso-table-lspace:0;mso-table-rspace:0;word-break:break-word"
|
</table>
|
||||||
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>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
@@ -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/).
|
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
|
## Table of Contents
|
||||||
|
|
||||||
- [Getting Started](#getting-started)
|
- [Getting Started](#getting-started)
|
||||||
- [Available Scripts](#available-scripts)
|
- [Available Scripts](#available-scripts)
|
||||||
- [Project Structure](#project-structure)
|
- [Project Structure](#project-structure)
|
||||||
|
|||||||
@@ -5,42 +5,72 @@
|
|||||||
<link rel="icon" href="./favicon.ico" />
|
<link rel="icon" href="./favicon.ico" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<meta name="theme-color" content="#2b2b2b" />
|
<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.googleapis.com" />
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
<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
|
<link
|
||||||
href="https://fonts.googleapis.com/css2?family=Inter:wght@100;200;300;400;500;600;700;800;900&display=swap"
|
href="https://fonts.googleapis.com/css2?family=Inter:wght@100;200;300;400;500;600;700;800;900&display=swap"
|
||||||
rel="stylesheet"
|
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>
|
<title>Worklenz</title>
|
||||||
|
|
||||||
<!-- Environment configuration -->
|
<!-- Environment configuration -->
|
||||||
<script src="/env-config.js"></script>
|
<script src="/env-config.js"></script>
|
||||||
<!-- Google Analytics -->
|
|
||||||
|
<!-- Optimized Google Analytics with reduced blocking -->
|
||||||
<script>
|
<script>
|
||||||
// Function to initialize Google Analytics
|
// Function to initialize Google Analytics asynchronously
|
||||||
function initGoogleAnalytics() {
|
function initGoogleAnalytics() {
|
||||||
// Load the Google Analytics script
|
// Use requestIdleCallback to defer analytics loading
|
||||||
const script = document.createElement('script');
|
const loadAnalytics = () => {
|
||||||
script.async = true;
|
// Determine which tracking ID to use based on the environment
|
||||||
|
const isProduction = window.location.hostname === 'app.worklenz.com';
|
||||||
// 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);
|
|
||||||
|
|
||||||
// Initialize Google Analytics
|
const trackingId = isProduction ? 'G-7KSRKQ1397' : 'G-3LM2HGWEXG'; // Open source tracking ID
|
||||||
window.dataLayer = window.dataLayer || [];
|
|
||||||
function gtag(){dataLayer.push(arguments);}
|
// Load the Google Analytics script
|
||||||
gtag('js', new Date());
|
const script = document.createElement('script');
|
||||||
gtag('config', trackingId);
|
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();
|
initGoogleAnalytics();
|
||||||
|
|
||||||
// Function to show privacy notice
|
// Function to show privacy notice
|
||||||
@@ -69,7 +99,7 @@
|
|||||||
document.body.appendChild(notice);
|
document.body.appendChild(notice);
|
||||||
// Add event listener to button
|
// Add event listener to button
|
||||||
const btn = notice.querySelector('#analytics-notice-btn');
|
const btn = notice.querySelector('#analytics-notice-btn');
|
||||||
btn.addEventListener('click', function(e) {
|
btn.addEventListener('click', function (e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
localStorage.setItem('privacyNoticeShown', 'true');
|
localStorage.setItem('privacyNoticeShown', 'true');
|
||||||
notice.remove();
|
notice.remove();
|
||||||
@@ -77,12 +107,13 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Wait for DOM to be ready
|
// Wait for DOM to be ready
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
// Check if we should show the notice
|
// Check if we should show the notice
|
||||||
const isProduction = window.location.hostname === 'worklenz.com' ||
|
const isProduction =
|
||||||
window.location.hostname === 'app.worklenz.com';
|
window.location.hostname === 'worklenz.com' ||
|
||||||
|
window.location.hostname === 'app.worklenz.com';
|
||||||
const noticeShown = localStorage.getItem('privacyNoticeShown') === 'true';
|
const noticeShown = localStorage.getItem('privacyNoticeShown') === 'true';
|
||||||
|
|
||||||
// Show notice if not in production and not shown before
|
// Show notice if not in production and not shown before
|
||||||
if (!isProduction && !noticeShown) {
|
if (!isProduction && !noticeShown) {
|
||||||
showPrivacyNotice();
|
showPrivacyNotice();
|
||||||
@@ -91,64 +122,30 @@
|
|||||||
</script>
|
</script>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<head>
|
<body>
|
||||||
<meta charset="utf-8" />
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||||
<link rel="icon" href="./favicon.ico" />
|
<div id="root"></div>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<script type="module" src="./src/index.tsx"></script>
|
||||||
<meta name="theme-color" content="#2b2b2b" />
|
<script type="text/javascript">
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
// Load HubSpot script asynchronously and only for production
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
if (window.location.hostname === 'app.worklenz.com') {
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@100;200;300;400;500;600;700;800;900&display=swap"
|
// Use requestIdleCallback to defer HubSpot loading
|
||||||
rel="stylesheet" />
|
const loadHubSpot = () => {
|
||||||
<title>Worklenz</title>
|
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 -->
|
if ('requestIdleCallback' in window) {
|
||||||
<script src="/env-config.js"></script>
|
requestIdleCallback(loadHubSpot, { timeout: 3000 });
|
||||||
<!-- Unregister service worker -->
|
} else {
|
||||||
<script src="/unregister-sw.js"></script>
|
setTimeout(loadHubSpot, 2000);
|
||||||
<!-- Microsoft Clarity -->
|
}
|
||||||
<script type="text/javascript">
|
}
|
||||||
if (window.location.hostname === 'app.worklenz.com') {
|
</script>
|
||||||
(function (c, l, a, r, i, t, y) {
|
</body>
|
||||||
c[a] = c[a] || function () { (c[a].q = c[a].q || []).push(arguments) };
|
</html>
|
||||||
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>
|
|
||||||
|
|||||||
2119
worklenz-frontend/package-lock.json
generated
2119
worklenz-frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -3,7 +3,8 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "vite",
|
"start": "vite dev",
|
||||||
|
"dev": "vite dev",
|
||||||
"prebuild": "node scripts/copy-tinymce.js",
|
"prebuild": "node scripts/copy-tinymce.js",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"dev-build": "vite build",
|
"dev-build": "vite build",
|
||||||
@@ -13,22 +14,25 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ant-design/colors": "^7.1.0",
|
"@ant-design/colors": "^7.1.0",
|
||||||
"@ant-design/compatible": "^5.1.4",
|
"@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",
|
"@ant-design/pro-components": "^2.7.19",
|
||||||
"@dnd-kit/core": "^6.3.1",
|
"@dnd-kit/core": "^6.3.1",
|
||||||
"@dnd-kit/modifiers": "^9.0.0",
|
"@dnd-kit/modifiers": "^9.0.0",
|
||||||
"@dnd-kit/sortable": "^10.0.0",
|
"@dnd-kit/sortable": "^10.0.0",
|
||||||
"@dnd-kit/utilities": "^3.2.2",
|
"@dnd-kit/utilities": "^3.2.2",
|
||||||
"@emotion/react": "^11.14.0",
|
"@emotion/react": "^11.14.0",
|
||||||
|
"@heroicons/react": "^2.2.0",
|
||||||
"@paddle/paddle-js": "^1.3.3",
|
"@paddle/paddle-js": "^1.3.3",
|
||||||
"@reduxjs/toolkit": "^2.2.7",
|
"@reduxjs/toolkit": "^2.2.7",
|
||||||
|
"@tailwindcss/forms": "^0.5.10",
|
||||||
"@tanstack/react-table": "^8.20.6",
|
"@tanstack/react-table": "^8.20.6",
|
||||||
"@tanstack/react-virtual": "^3.11.2",
|
"@tanstack/react-virtual": "^3.11.2",
|
||||||
"@tinymce/tinymce-react": "^5.1.1",
|
"@tinymce/tinymce-react": "^5.1.1",
|
||||||
"antd": "^5.24.9",
|
"antd": "^5.26.2",
|
||||||
"axios": "^1.9.0",
|
"axios": "^1.9.0",
|
||||||
"chart.js": "^4.4.7",
|
"chart.js": "^4.4.7",
|
||||||
"chartjs-plugin-datalabels": "^2.2.0",
|
"chartjs-plugin-datalabels": "^2.2.0",
|
||||||
|
"cors": "^2.8.5",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"dompurify": "^3.2.5",
|
"dompurify": "^3.2.5",
|
||||||
"gantt-task-react": "^0.3.9",
|
"gantt-task-react": "^0.3.9",
|
||||||
@@ -38,6 +42,7 @@
|
|||||||
"i18next-http-backend": "^2.7.3",
|
"i18next-http-backend": "^2.7.3",
|
||||||
"jspdf": "^3.0.0",
|
"jspdf": "^3.0.0",
|
||||||
"mixpanel-browser": "^2.56.0",
|
"mixpanel-browser": "^2.56.0",
|
||||||
|
"nanoid": "^5.1.5",
|
||||||
"primereact": "^10.8.4",
|
"primereact": "^10.8.4",
|
||||||
"re-resizable": "^6.10.3",
|
"re-resizable": "^6.10.3",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
@@ -49,10 +54,13 @@
|
|||||||
"react-responsive": "^10.0.0",
|
"react-responsive": "^10.0.0",
|
||||||
"react-router-dom": "^6.28.1",
|
"react-router-dom": "^6.28.1",
|
||||||
"react-timer-hook": "^3.0.8",
|
"react-timer-hook": "^3.0.8",
|
||||||
|
"react-virtuoso": "^4.13.0",
|
||||||
"react-window": "^1.8.11",
|
"react-window": "^1.8.11",
|
||||||
|
"react-window-infinite-loader": "^1.0.10",
|
||||||
"socket.io-client": "^4.8.1",
|
"socket.io-client": "^4.8.1",
|
||||||
"tinymce": "^7.7.2",
|
"tinymce": "^7.7.2",
|
||||||
"web-vitals": "^4.2.4"
|
"web-vitals": "^4.2.4",
|
||||||
|
"worklenz": "file:"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@testing-library/jest-dom": "^6.6.3",
|
"@testing-library/jest-dom": "^6.6.3",
|
||||||
@@ -66,10 +74,12 @@
|
|||||||
"@types/node": "^20.8.4",
|
"@types/node": "^20.8.4",
|
||||||
"@types/react": "19.0.0",
|
"@types/react": "19.0.0",
|
||||||
"@types/react-dom": "19.0.0",
|
"@types/react-dom": "19.0.0",
|
||||||
|
"@types/react-window": "^1.8.8",
|
||||||
"@vitejs/plugin-react": "^4.3.4",
|
"@vitejs/plugin-react": "^4.3.4",
|
||||||
"autoprefixer": "^10.4.20",
|
"autoprefixer": "^10.4.21",
|
||||||
"postcss": "^8.5.2",
|
"postcss": "^8.5.2",
|
||||||
"prettier-plugin-tailwindcss": "^0.6.8",
|
"prettier-plugin-tailwindcss": "^0.6.13",
|
||||||
|
"rollup": "^4.40.2",
|
||||||
"tailwindcss": "^3.4.17",
|
"tailwindcss": "^3.4.17",
|
||||||
"terser": "^5.39.0",
|
"terser": "^5.39.0",
|
||||||
"typescript": "^5.7.3",
|
"typescript": "^5.7.3",
|
||||||
|
|||||||
@@ -14,4 +14,4 @@
|
|||||||
/* Maintain hover state */
|
/* Maintain hover state */
|
||||||
.table-body-row:hover .sticky-column {
|
.table-body-row:hover .sticky-column {
|
||||||
background-color: var(--background-hover);
|
background-color: var(--background-hover);
|
||||||
}
|
}
|
||||||
|
|||||||
7
worklenz-frontend/public/env-config.js
Normal file
7
worklenz-frontend/public/env-config.js
Normal 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;
|
||||||
@@ -19,5 +19,12 @@
|
|||||||
"archive": "Arkivo",
|
"archive": "Arkivo",
|
||||||
|
|
||||||
"newTaskNamePlaceholder": "Shkruaj emrin e detyrës",
|
"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"
|
||||||
}
|
}
|
||||||
|
|||||||
14
worklenz-frontend/public/locales/alb/project-view.json
Normal file
14
worklenz-frontend/public/locales/alb/project-view.json
Normal 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"
|
||||||
|
}
|
||||||
@@ -1,13 +1,29 @@
|
|||||||
{
|
{
|
||||||
"importTasks": "Importo detyra",
|
"importTasks": "Importo detyra",
|
||||||
|
"importTask": "Importo detyrë",
|
||||||
"createTask": "Krijo detyrë",
|
"createTask": "Krijo detyrë",
|
||||||
"settings": "Cilësimet",
|
"settings": "Cilësimet",
|
||||||
"subscribe": "Abonohu",
|
"subscribe": "Abonohu",
|
||||||
"unsubscribe": "Ç'abonohu",
|
"unsubscribe": "Çabonohu",
|
||||||
"deleteProject": "Fshi projektin",
|
"deleteProject": "Fshi projektin",
|
||||||
"startDate": "Data e fillimit",
|
"startDate": "Data e fillimit",
|
||||||
"endDate": "Data e përfundimit",
|
"endDate": "Data e mbarimit",
|
||||||
"projectSettings": "Cilësimet e projektit",
|
"projectSettings": "Cilësimet e projektit",
|
||||||
"projectSummary": "Përmbledhja 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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,5 +9,6 @@
|
|||||||
"saveChanges": "Ruaj Ndryshimet",
|
"saveChanges": "Ruaj Ndryshimet",
|
||||||
"profileJoinedText": "U bashkua një muaj më parë",
|
"profileJoinedText": "U bashkua një muaj më parë",
|
||||||
"profileLastUpdatedText": "Përditësuar 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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
{
|
{
|
||||||
|
"title": "Anëtarët e Ekipit",
|
||||||
"nameColumn": "Emri",
|
"nameColumn": "Emri",
|
||||||
"projectsColumn": "Projektet",
|
"projectsColumn": "Projektet",
|
||||||
"emailColumn": "Email",
|
"emailColumn": "Email",
|
||||||
@@ -40,5 +41,7 @@
|
|||||||
"ownerText": "Pronar i Ekipit",
|
"ownerText": "Pronar i Ekipit",
|
||||||
"addedText": "Shtuar",
|
"addedText": "Shtuar",
|
||||||
"updatedText": "Përditësuar",
|
"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!"
|
||||||
}
|
}
|
||||||
|
|||||||
16
worklenz-frontend/public/locales/alb/settings/teams.json
Normal file
16
worklenz-frontend/public/locales/alb/settings/teams.json
Normal 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!"
|
||||||
|
}
|
||||||
@@ -1,28 +1,37 @@
|
|||||||
{
|
{
|
||||||
"taskHeader": {
|
"taskHeader": {
|
||||||
"taskNamePlaceholder": "Shkruani detyrën tuaj",
|
"taskNamePlaceholder": "Shkruani Detyrën tuaj",
|
||||||
"deleteTask": "Fshi Detyrën"
|
"deleteTask": "Fshi Detyrën"
|
||||||
},
|
},
|
||||||
"taskInfoTab": {
|
"taskInfoTab": {
|
||||||
"title": "Info",
|
"title": "Informacioni",
|
||||||
"details": {
|
"details": {
|
||||||
"title": "Detajet",
|
"title": "Detajet",
|
||||||
"task-key": "Çelësi i Detyrës",
|
"task-key": "Çelësi i Detyrës",
|
||||||
"phase": "Faza",
|
"phase": "Faza",
|
||||||
"assignees": "Përgjegjësit",
|
"assignees": "Të Caktuar",
|
||||||
"due-date": "Afati i Përfundimit",
|
"due-date": "Data e Përfundimit",
|
||||||
"time-estimation": "Vlerësimi i Kohës",
|
"time-estimation": "Vlerësimi i Kohës",
|
||||||
"priority": "Prioriteti",
|
"priority": "Prioriteti",
|
||||||
"labels": "Etiketa",
|
"labels": "Etiketat",
|
||||||
"billable": "Fakturueshme",
|
"billable": "E Faturueshme",
|
||||||
"notify": "Njofto",
|
"notify": "Njofto",
|
||||||
"when-done-notify": "Kur të përfundojë, njofto",
|
"when-done-notify": "Kur përfundon, njofto",
|
||||||
"start-date": "Data e Fillimit",
|
"start-date": "Data e Fillimit",
|
||||||
"end-date": "Data e Përfundimit",
|
"end-date": "Data e Përfundimit",
|
||||||
"hide-start-date": "Fshih Datën e Fillimit",
|
"hide-start-date": "Fshih Datën e Fillimit",
|
||||||
"show-start-date": "Shfaq Datën e Fillimit",
|
"show-start-date": "Shfaq Datën e Fillimit",
|
||||||
"hours": "Orë",
|
"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": {
|
"labels": {
|
||||||
"labelInputPlaceholder": "Kërko ose krijo",
|
"labelInputPlaceholder": "Kërko ose krijo",
|
||||||
@@ -30,37 +39,48 @@
|
|||||||
},
|
},
|
||||||
"description": {
|
"description": {
|
||||||
"title": "Përshkrimi",
|
"title": "Përshkrimi",
|
||||||
"placeholder": "Shtoni një përshkrim më të detajuar..."
|
"placeholder": "Shto një përshkrim më të detajuar..."
|
||||||
},
|
},
|
||||||
"subTasks": {
|
"subTasks": {
|
||||||
"title": "Nën-Detyrat",
|
"title": "Nëndetyrat",
|
||||||
"addSubTask": "+ Shto Nën-Detyrë",
|
"addSubTask": "Shto Nëndetyrë",
|
||||||
"addSubTaskInputPlaceholder": "Shkruani detyrën dhe shtypni Enter",
|
"addSubTaskInputPlaceholder": "Shkruani detyrën tuaj dhe shtypni enter",
|
||||||
"refreshSubTasks": "Rifresko Nën-Detyrat",
|
"refreshSubTasks": "Rifresko Nëndetyrat",
|
||||||
"edit": "Modifiko",
|
"edit": "Modifiko",
|
||||||
"delete": "Fshi",
|
"delete": "Fshi",
|
||||||
"confirmDeleteSubTask": "Jeni i sigurt që doni të fshini këtë nën-detyrë?",
|
"confirmDeleteSubTask": "Jeni i sigurt që doni të fshini këtë nëndetyrë?",
|
||||||
"deleteSubTask": "Fshi Nën-Detyrën"
|
"deleteSubTask": "Fshi Nëndetyrën"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"title": "Varësitë",
|
"title": "Varësitë",
|
||||||
"addDependency": "+ Shto varësi të re",
|
"addDependency": "+ Shto varësi të re",
|
||||||
"blockedBy": "I bllokuar nga",
|
"blockedBy": "Bllokuar nga",
|
||||||
"searchTask": "Shkruani për të kërkuar detyra",
|
"searchTask": "Shkruani për të kërkuar detyrë",
|
||||||
"noTasksFound": "Asnjë detyrë nuk u gjet",
|
"noTasksFound": "Nuk u gjetën detyra",
|
||||||
"confirmDeleteDependency": "Jeni i sigurt që doni të fshini?"
|
"confirmDeleteDependency": "Jeni i sigurt që doni të fshini?"
|
||||||
},
|
},
|
||||||
"attachments": {
|
"attachments": {
|
||||||
"title": "Bashkëngjitjet",
|
"title": "Bashkëngjitjet",
|
||||||
"chooseOrDropFileToUpload": "Zgjidhni ose lëshoni skedar për ngarkim",
|
"chooseOrDropFileToUpload": "Zgjidhni ose hidhni skedar për të ngarkuar",
|
||||||
"uploading": "Po ngarkohet..."
|
"uploading": "Duke ngarkuar..."
|
||||||
},
|
},
|
||||||
"comments": {
|
"comments": {
|
||||||
"title": "Komentet",
|
"title": "Komentet",
|
||||||
"addComment": "+ Shto koment të ri",
|
"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",
|
"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",
|
"searchInputPlaceholder": "Kërko sipas emrit",
|
||||||
"pendingInvitation": "Ftesë në Pritje"
|
"pendingInvitation": "Ftesë në Pritje"
|
||||||
@@ -68,11 +88,36 @@
|
|||||||
"taskTimeLogTab": {
|
"taskTimeLogTab": {
|
||||||
"title": "Regjistri i Kohës",
|
"title": "Regjistri i Kohës",
|
||||||
"addTimeLog": "Shto regjistrim të ri kohe",
|
"addTimeLog": "Shto regjistrim të ri kohe",
|
||||||
"totalLogged": "Koha totale e regjistruar",
|
"totalLogged": "Totali i Regjistruar",
|
||||||
"exportToExcel": "Eksporto në Excel",
|
"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": {
|
"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\"?"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,5 +55,18 @@
|
|||||||
"selectCategory": "Zgjidh një kategori",
|
"selectCategory": "Zgjidh një kategori",
|
||||||
"pleaseEnterAName": "Ju lutemi vendosni një emër",
|
"pleaseEnterAName": "Ju lutemi vendosni një emër",
|
||||||
"pleaseSelectACategory": "Ju lutemi zgjidhni një kategori",
|
"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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,8 +36,9 @@
|
|||||||
"selectText": "Zgjidh",
|
"selectText": "Zgjidh",
|
||||||
"labelsSelectorInputTip": "Shtyp Enter për të krijuar!",
|
"labelsSelectorInputTip": "Shtyp Enter për të krijuar!",
|
||||||
|
|
||||||
"addTaskText": "+ Shto Detyrë",
|
"addTaskText": "Shto Detyrë",
|
||||||
"addSubTaskText": "+ Shto Nën-Detyrë",
|
"addSubTaskText": "+ Shto Nën-Detyrë",
|
||||||
|
"noTasksInGroup": "Nuk ka detyra në këtë grup",
|
||||||
"addTaskInputPlaceholder": "Shkruaj detyrën dhe shtyp Enter",
|
"addTaskInputPlaceholder": "Shkruaj detyrën dhe shtyp Enter",
|
||||||
|
|
||||||
"openButton": "Hap",
|
"openButton": "Hap",
|
||||||
@@ -59,5 +60,74 @@
|
|||||||
"convertToTask": "Shndërro në Detyrë",
|
"convertToTask": "Shndërro në Detyrë",
|
||||||
"delete": "Fshi",
|
"delete": "Fshi",
|
||||||
"searchByNameInputPlaceholder": "Kërko sipas emrit"
|
"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"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
21
worklenz-frontend/public/locales/alb/task-management.json
Normal file
21
worklenz-frontend/public/locales/alb/task-management.json
Normal 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"
|
||||||
|
}
|
||||||
@@ -17,7 +17,9 @@
|
|||||||
"createTaskTemplate": "Krijo Shabllon Detyre",
|
"createTaskTemplate": "Krijo Shabllon Detyre",
|
||||||
"apply": "Apliko",
|
"apply": "Apliko",
|
||||||
"createLabel": "+ Krijo Etiketë",
|
"createLabel": "+ Krijo Etiketë",
|
||||||
|
"searchOrCreateLabel": "Kërko ose krijo etiketë...",
|
||||||
"hitEnterToCreate": "Shtyp Enter për të krijuar",
|
"hitEnterToCreate": "Shtyp Enter për të krijuar",
|
||||||
|
"labelExists": "Etiketa ekziston tashmë",
|
||||||
"pendingInvitation": "Ftesë në Pritje",
|
"pendingInvitation": "Ftesë në Pritje",
|
||||||
"noMatchingLabels": "Asnjë etiketë që përputhet",
|
"noMatchingLabels": "Asnjë etiketë që përputhet",
|
||||||
"noLabels": "Asnjë etiketë"
|
"noLabels": "Asnjë etiketë"
|
||||||
|
|||||||
@@ -19,5 +19,12 @@
|
|||||||
"archive": "Archivieren",
|
"archive": "Archivieren",
|
||||||
|
|
||||||
"newTaskNamePlaceholder": "Aufgabenname eingeben",
|
"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"
|
||||||
}
|
}
|
||||||
|
|||||||
14
worklenz-frontend/public/locales/de/project-view.json
Normal file
14
worklenz-frontend/public/locales/de/project-view.json
Normal 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"
|
||||||
|
}
|
||||||
@@ -1,13 +1,29 @@
|
|||||||
{
|
{
|
||||||
"importTasks": "Aufgaben importieren",
|
"importTasks": "Aufgaben importieren",
|
||||||
|
"importTask": "Aufgabe importieren",
|
||||||
"createTask": "Aufgabe erstellen",
|
"createTask": "Aufgabe erstellen",
|
||||||
"settings": "Einstellungen",
|
"settings": "Einstellungen",
|
||||||
"subscribe": "Abonnieren",
|
"subscribe": "Abonnieren",
|
||||||
"unsubscribe": "Abbestellen",
|
"unsubscribe": "Abonnement beenden",
|
||||||
"deleteProject": "Projekt löschen",
|
"deleteProject": "Projekt löschen",
|
||||||
"startDate": "Startdatum",
|
"startDate": "Startdatum",
|
||||||
"endDate": "Enddatum",
|
"endDate": "Enddatum",
|
||||||
"projectSettings": "Projekteinstellungen",
|
"projectSettings": "Projekteinstellungen",
|
||||||
"projectSummary": "Projektzusammenfassung",
|
"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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,5 +9,6 @@
|
|||||||
"saveChanges": "Änderungen speichern",
|
"saveChanges": "Änderungen speichern",
|
||||||
"profileJoinedText": "Vor einem Monat beigetreten",
|
"profileJoinedText": "Vor einem Monat beigetreten",
|
||||||
"profileLastUpdatedText": "Vor einem Monat aktualisiert",
|
"profileLastUpdatedText": "Vor einem Monat aktualisiert",
|
||||||
"avatarTooltip": "Klicken Sie zum Hochladen eines Avatars"
|
"avatarTooltip": "Klicken Sie zum Hochladen eines Avatars",
|
||||||
|
"title": "Profil-Einstellungen"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
{
|
{
|
||||||
|
"title": "Teammitglieder",
|
||||||
"nameColumn": "Name",
|
"nameColumn": "Name",
|
||||||
"projectsColumn": "Projekte",
|
"projectsColumn": "Projekte",
|
||||||
"emailColumn": "E-Mail",
|
"emailColumn": "E-Mail",
|
||||||
@@ -40,5 +41,7 @@
|
|||||||
"ownerText": "Team-Besitzer",
|
"ownerText": "Team-Besitzer",
|
||||||
"addedText": "Hinzugefügt",
|
"addedText": "Hinzugefügt",
|
||||||
"updatedText": "Aktualisiert",
|
"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!"
|
||||||
}
|
}
|
||||||
|
|||||||
16
worklenz-frontend/public/locales/de/settings/teams.json
Normal file
16
worklenz-frontend/public/locales/de/settings/teams.json
Normal 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!"
|
||||||
|
}
|
||||||
@@ -26,4 +26,4 @@
|
|||||||
"add-sub-task": "+ Unteraufgabe hinzufügen",
|
"add-sub-task": "+ Unteraufgabe hinzufügen",
|
||||||
"refresh-sub-tasks": "Unteraufgaben aktualisieren"
|
"refresh-sub-tasks": "Unteraufgaben aktualisieren"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"taskHeader": {
|
"taskHeader": {
|
||||||
"taskNamePlaceholder": "Aufgabe eingeben",
|
"taskNamePlaceholder": "Geben Sie Ihre Aufgabe ein",
|
||||||
"deleteTask": "Aufgabe löschen"
|
"deleteTask": "Aufgabe löschen"
|
||||||
},
|
},
|
||||||
"taskInfoTab": {
|
"taskInfoTab": {
|
||||||
@@ -9,20 +9,29 @@
|
|||||||
"title": "Details",
|
"title": "Details",
|
||||||
"task-key": "Aufgaben-Schlüssel",
|
"task-key": "Aufgaben-Schlüssel",
|
||||||
"phase": "Phase",
|
"phase": "Phase",
|
||||||
"assignees": "Zugewiesene",
|
"assignees": "Beauftragte",
|
||||||
"due-date": "Fälligkeitsdatum",
|
"due-date": "Fälligkeitsdatum",
|
||||||
"time-estimation": "Zeitschätzung",
|
"time-estimation": "Zeitschätzung",
|
||||||
"priority": "Priorität",
|
"priority": "Priorität",
|
||||||
"labels": "Labels",
|
"labels": "Labels",
|
||||||
"billable": "Abrechenbar",
|
"billable": "Abrechenbar",
|
||||||
"notify": "Benachrichtigen",
|
"notify": "Benachrichtigen",
|
||||||
"when-done-notify": "Bei Fertigstellung benachrichtigen",
|
"when-done-notify": "Bei Abschluss benachrichtigen",
|
||||||
"start-date": "Startdatum",
|
"start-date": "Startdatum",
|
||||||
"end-date": "Enddatum",
|
"end-date": "Enddatum",
|
||||||
"hide-start-date": "Startdatum ausblenden",
|
"hide-start-date": "Startdatum ausblenden",
|
||||||
"show-start-date": "Startdatum anzeigen",
|
"show-start-date": "Startdatum anzeigen",
|
||||||
"hours": "Stunden",
|
"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": {
|
"labels": {
|
||||||
"labelInputPlaceholder": "Suchen oder erstellen",
|
"labelInputPlaceholder": "Suchen oder erstellen",
|
||||||
@@ -30,29 +39,29 @@
|
|||||||
},
|
},
|
||||||
"description": {
|
"description": {
|
||||||
"title": "Beschreibung",
|
"title": "Beschreibung",
|
||||||
"placeholder": "Detaillierte Beschreibung hinzufügen..."
|
"placeholder": "Detailliertere Beschreibung hinzufügen..."
|
||||||
},
|
},
|
||||||
"subTasks": {
|
"subTasks": {
|
||||||
"title": "Unteraufgaben",
|
"title": "Teilaufgaben",
|
||||||
"addSubTask": "+ Unteraufgabe hinzufügen",
|
"addSubTask": "Teilaufgabe hinzufügen",
|
||||||
"addSubTaskInputPlaceholder": "Aufgabe eingeben und Enter drücken",
|
"addSubTaskInputPlaceholder": "Geben Sie Ihre Aufgabe ein und drücken Sie Enter",
|
||||||
"refreshSubTasks": "Unteraufgaben aktualisieren",
|
"refreshSubTasks": "Teilaufgaben aktualisieren",
|
||||||
"edit": "Bearbeiten",
|
"edit": "Bearbeiten",
|
||||||
"delete": "Löschen",
|
"delete": "Löschen",
|
||||||
"confirmDeleteSubTask": "Sind Sie sicher, dass Sie diese Unteraufgabe löschen möchten?",
|
"confirmDeleteSubTask": "Sind Sie sicher, dass Sie diese Teilaufgabe löschen möchten?",
|
||||||
"deleteSubTask": "Unteraufgabe löschen"
|
"deleteSubTask": "Teilaufgabe löschen"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"title": "Abhängigkeiten",
|
"title": "Abhängigkeiten",
|
||||||
"addDependency": "+ Neue Abhängigkeit hinzufügen",
|
"addDependency": "+ Neue Abhängigkeit hinzufügen",
|
||||||
"blockedBy": "Blockiert durch",
|
"blockedBy": "Blockiert von",
|
||||||
"searchTask": "Aufgabe suchen",
|
"searchTask": "Aufgabe suchen",
|
||||||
"noTasksFound": "Keine Aufgaben gefunden",
|
"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": {
|
"attachments": {
|
||||||
"title": "Anhänge",
|
"title": "Anhänge",
|
||||||
"chooseOrDropFileToUpload": "Datei auswählen oder zum Hochladen ablegen",
|
"chooseOrDropFileToUpload": "Datei zum Hochladen wählen oder ablegen",
|
||||||
"uploading": "Wird hochgeladen..."
|
"uploading": "Wird hochgeladen..."
|
||||||
},
|
},
|
||||||
"comments": {
|
"comments": {
|
||||||
@@ -60,19 +69,55 @@
|
|||||||
"addComment": "+ Neuen Kommentar hinzufügen",
|
"addComment": "+ Neuen Kommentar hinzufügen",
|
||||||
"noComments": "Noch keine Kommentare. Seien Sie der Erste!",
|
"noComments": "Noch keine Kommentare. Seien Sie der Erste!",
|
||||||
"delete": "Löschen",
|
"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",
|
"searchInputPlaceholder": "Nach Name suchen",
|
||||||
"pendingInvitation": "Einladung ausstehend"
|
"pendingInvitation": "Ausstehende Einladung"
|
||||||
},
|
},
|
||||||
"taskTimeLogTab": {
|
"taskTimeLogTab": {
|
||||||
"title": "Zeiterfassung",
|
"title": "Zeiterfassung",
|
||||||
"addTimeLog": "Neuen Zeiteintrag hinzufügen",
|
"addTimeLog": "Neuen Zeiteintrag hinzufügen",
|
||||||
"totalLogged": "Gesamt erfasst",
|
"totalLogged": "Gesamt erfasst",
|
||||||
"exportToExcel": "Nach Excel exportieren",
|
"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": {
|
"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?"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,5 +55,18 @@
|
|||||||
"selectCategory": "Kategorie auswählen",
|
"selectCategory": "Kategorie auswählen",
|
||||||
"pleaseEnterAName": "Bitte geben Sie einen Namen ein",
|
"pleaseEnterAName": "Bitte geben Sie einen Namen ein",
|
||||||
"pleaseSelectACategory": "Bitte wählen Sie eine Kategorie aus",
|
"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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,9 +36,10 @@
|
|||||||
"selectText": "Auswählen",
|
"selectText": "Auswählen",
|
||||||
"labelsSelectorInputTip": "Enter drücken zum Erstellen!",
|
"labelsSelectorInputTip": "Enter drücken zum Erstellen!",
|
||||||
|
|
||||||
"addTaskText": "+ Aufgabe hinzufügen",
|
"addTaskText": "Aufgabe hinzufügen",
|
||||||
"addSubTaskText": "+ Unteraufgabe hinzufügen",
|
"addSubTaskText": "+ Unteraufgabe hinzufügen",
|
||||||
"addTaskInputPlaceholder": "Aufgabe eingeben und Enter drücken",
|
"addTaskInputPlaceholder": "Aufgabe eingeben und Enter drücken",
|
||||||
|
"noTasksInGroup": "Keine Aufgaben in dieser Gruppe",
|
||||||
|
|
||||||
"openButton": "Öffnen",
|
"openButton": "Öffnen",
|
||||||
"okButton": "OK",
|
"okButton": "OK",
|
||||||
@@ -59,5 +60,74 @@
|
|||||||
"convertToTask": "In Aufgabe umwandeln",
|
"convertToTask": "In Aufgabe umwandeln",
|
||||||
"delete": "Löschen",
|
"delete": "Löschen",
|
||||||
"searchByNameInputPlaceholder": "Nach Namen suchen"
|
"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"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
21
worklenz-frontend/public/locales/de/task-management.json
Normal file
21
worklenz-frontend/public/locales/de/task-management.json
Normal 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"
|
||||||
|
}
|
||||||
@@ -17,8 +17,25 @@
|
|||||||
"createTaskTemplate": "Aufgabenvorlage erstellen",
|
"createTaskTemplate": "Aufgabenvorlage erstellen",
|
||||||
"apply": "Anwenden",
|
"apply": "Anwenden",
|
||||||
"createLabel": "+ Label erstellen",
|
"createLabel": "+ Label erstellen",
|
||||||
|
"searchOrCreateLabel": "Label suchen oder erstellen...",
|
||||||
"hitEnterToCreate": "Enter drücken zum Erstellen",
|
"hitEnterToCreate": "Enter drücken zum Erstellen",
|
||||||
|
"labelExists": "Label existiert bereits",
|
||||||
"pendingInvitation": "Einladung ausstehend",
|
"pendingInvitation": "Einladung ausstehend",
|
||||||
"noMatchingLabels": "Keine passenden Labels",
|
"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."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,7 +25,7 @@
|
|||||||
"paymentMethod": "Payment Method",
|
"paymentMethod": "Payment Method",
|
||||||
"status": "Status",
|
"status": "Status",
|
||||||
"ltdUsers": "You can add up to {{ltd_users}} users.",
|
"ltdUsers": "You can add up to {{ltd_users}} users.",
|
||||||
|
|
||||||
"totalSeats": "Total seats",
|
"totalSeats": "Total seats",
|
||||||
"availableSeats": "Available seats",
|
"availableSeats": "Available seats",
|
||||||
"addMoreSeats": "Add more seats",
|
"addMoreSeats": "Add more seats",
|
||||||
@@ -103,11 +103,11 @@
|
|||||||
"perMonthPerUser": "per user/month",
|
"perMonthPerUser": "per user/month",
|
||||||
"viewInvoice": "View Invoice",
|
"viewInvoice": "View Invoice",
|
||||||
"switchToFreePlan": "Switch to Free Plan",
|
"switchToFreePlan": "Switch to Free Plan",
|
||||||
|
|
||||||
"expirestoday": "today",
|
"expirestoday": "today",
|
||||||
"expirestomorrow": "tomorrow",
|
"expirestomorrow": "tomorrow",
|
||||||
"expiredDaysAgo": "{{days}} days ago",
|
"expiredDaysAgo": "{{days}} days ago",
|
||||||
|
|
||||||
"continueWith": "Continue with {{plan}}",
|
"continueWith": "Continue with {{plan}}",
|
||||||
"changeToPlan": "Change to {{plan}}",
|
"changeToPlan": "Change to {{plan}}",
|
||||||
"creditPlan": "Credit Plan",
|
"creditPlan": "Credit Plan",
|
||||||
|
|||||||
@@ -19,5 +19,13 @@
|
|||||||
"unarchiveConfirm": "Are you sure you want to unarchive this project?",
|
"unarchiveConfirm": "Are you sure you want to unarchive this project?",
|
||||||
"clickToFilter": "Click to filter by",
|
"clickToFilter": "Click to filter by",
|
||||||
"noProjects": "No projects found",
|
"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"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,5 +19,12 @@
|
|||||||
"archive": "Archive",
|
"archive": "Archive",
|
||||||
|
|
||||||
"newTaskNamePlaceholder": "Write a task Name",
|
"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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"configurePhases": "Configure Phases",
|
"configurePhases": "Configure Phases",
|
||||||
"phaseLabel": "Phase Label",
|
"phaseLabel": "Phase Label",
|
||||||
"enterPhaseName": "Enter a name for phase label",
|
"enterPhaseName": "Enter a name for phase label",
|
||||||
"addOption": "Add Option",
|
"addOption": "Add Option",
|
||||||
"phaseOptions": "Phase Options:"
|
"phaseOptions": "Phase Options:"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,5 +47,6 @@
|
|||||||
"weightedProgress": "Weighted Progress",
|
"weightedProgress": "Weighted Progress",
|
||||||
"weightedProgressTooltip": "Calculate progress based on subtask weights",
|
"weightedProgressTooltip": "Calculate progress based on subtask weights",
|
||||||
"timeProgress": "Time-based Progress",
|
"timeProgress": "Time-based Progress",
|
||||||
"timeProgressTooltip": "Calculate progress based on estimated time"
|
"timeProgressTooltip": "Calculate progress based on estimated time",
|
||||||
|
"enterProjectKey": "Enter project key"
|
||||||
}
|
}
|
||||||
|
|||||||
14
worklenz-frontend/public/locales/en/project-view.json
Normal file
14
worklenz-frontend/public/locales/en/project-view.json
Normal 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"
|
||||||
|
}
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
{
|
{
|
||||||
"importTaskTemplate": "Import Task Template",
|
"importTaskTemplate": "Import Task Template",
|
||||||
"templateName": "Template Name",
|
"templateName": "Template Name",
|
||||||
"templateDescription": "Template Description",
|
"templateDescription": "Template Description",
|
||||||
"selectedTasks": "Selected Tasks",
|
"selectedTasks": "Selected Tasks",
|
||||||
"tasks": "Tasks",
|
"tasks": "Tasks",
|
||||||
"templates": "Templates",
|
"templates": "Templates",
|
||||||
"remove": "Remove",
|
"remove": "Remove",
|
||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
"import": "Import"
|
"import": "Import"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
{
|
{
|
||||||
"title": "Project Members",
|
"title": "Project Members",
|
||||||
"searchLabel": "Add members by adding their name or email",
|
"searchLabel": "Add members by adding their name or email",
|
||||||
"searchPlaceholder": "Type name or email",
|
"searchPlaceholder": "Type name or email",
|
||||||
"inviteAsAMember": "Invite as a member",
|
"inviteAsAMember": "Invite as a member",
|
||||||
"inviteNewMemberByEmail": "Invite new member by email"
|
"inviteNewMemberByEmail": "Invite new member by email"
|
||||||
|
}
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,13 +1,29 @@
|
|||||||
{
|
{
|
||||||
"importTasks": "Import tasks",
|
"importTasks": "Import tasks",
|
||||||
"createTask": "Create task",
|
"importTask": "Import task",
|
||||||
"settings": "Settings",
|
"createTask": "Create task",
|
||||||
"subscribe": "Subscribe",
|
"settings": "Settings",
|
||||||
"unsubscribe": "Unsubscribe",
|
"subscribe": "Subscribe",
|
||||||
"deleteProject": "Delete project",
|
"unsubscribe": "Unsubscribe",
|
||||||
"startDate": "Start date",
|
"deleteProject": "Delete project",
|
||||||
"endDate": "End date",
|
"startDate": "Start date",
|
||||||
"projectSettings": "Project settings",
|
"endDate": "End date",
|
||||||
"projectSummary": "Project summary",
|
"projectSettings": "Project settings",
|
||||||
"receiveProjectSummary": "Receive a project summary every evening."
|
"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"
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,4 +2,4 @@
|
|||||||
"title": "Appearance",
|
"title": "Appearance",
|
||||||
"darkMode": "Dark Mode",
|
"darkMode": "Dark Mode",
|
||||||
"darkModeDescription": "Switch between light and dark mode to customize your viewing experience."
|
"darkModeDescription": "Switch between light and dark mode to customize your viewing experience."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,5 +9,6 @@
|
|||||||
"saveChanges": "Save Changes",
|
"saveChanges": "Save Changes",
|
||||||
"profileJoinedText": "Joined a month ago",
|
"profileJoinedText": "Joined a month ago",
|
||||||
"profileLastUpdatedText": "Last updated a month ago",
|
"profileLastUpdatedText": "Last updated a month ago",
|
||||||
"avatarTooltip": "Click to upload an avatar"
|
"avatarTooltip": "Click to upload an avatar",
|
||||||
|
"title": "Profile Settings"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
{
|
{
|
||||||
|
"title": "Team Members",
|
||||||
"nameColumn": "Name",
|
"nameColumn": "Name",
|
||||||
"projectsColumn": "Projects",
|
"projectsColumn": "Projects",
|
||||||
"emailColumn": "Email",
|
"emailColumn": "Email",
|
||||||
@@ -40,5 +41,7 @@
|
|||||||
"ownerText": "Team Owner",
|
"ownerText": "Team Owner",
|
||||||
"addedText": "Added",
|
"addedText": "Added",
|
||||||
"updatedText": "Updated",
|
"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!"
|
||||||
}
|
}
|
||||||
|
|||||||
16
worklenz-frontend/public/locales/en/settings/teams.json
Normal file
16
worklenz-frontend/public/locales/en/settings/teams.json
Normal 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!"
|
||||||
|
}
|
||||||
@@ -24,7 +24,7 @@
|
|||||||
},
|
},
|
||||||
"subTasks": {
|
"subTasks": {
|
||||||
"title": "Sub Tasks",
|
"title": "Sub Tasks",
|
||||||
"add-sub-task": "+ Add Sub Task",
|
"add-sub-task": "Add Sub Task",
|
||||||
"refresh-sub-tasks": "Refresh Sub Tasks"
|
"refresh-sub-tasks": "Refresh Sub Tasks"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,4 +31,4 @@
|
|||||||
"intervalWeeks": "Interval (weeks)",
|
"intervalWeeks": "Interval (weeks)",
|
||||||
"intervalMonths": "Interval (months)",
|
"intervalMonths": "Interval (months)",
|
||||||
"saveChanges": "Save Changes"
|
"saveChanges": "Save Changes"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,7 +43,7 @@
|
|||||||
},
|
},
|
||||||
"subTasks": {
|
"subTasks": {
|
||||||
"title": "Sub Tasks",
|
"title": "Sub Tasks",
|
||||||
"addSubTask": "+ Add Sub Task",
|
"addSubTask": "Add Sub Task",
|
||||||
"addSubTaskInputPlaceholder": "Type your task and hit enter",
|
"addSubTaskInputPlaceholder": "Type your task and hit enter",
|
||||||
"refreshSubTasks": "Refresh Sub Tasks",
|
"refreshSubTasks": "Refresh Sub Tasks",
|
||||||
"edit": "Edit",
|
"edit": "Edit",
|
||||||
@@ -69,7 +69,18 @@
|
|||||||
"addComment": "+ Add new comment",
|
"addComment": "+ Add new comment",
|
||||||
"noComments": "No comments yet. Be the first to comment!",
|
"noComments": "No comments yet. Be the first to comment!",
|
||||||
"delete": "Delete",
|
"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",
|
"searchInputPlaceholder": "Search by name",
|
||||||
"pendingInvitation": "Pending Invitation"
|
"pendingInvitation": "Pending Invitation"
|
||||||
@@ -79,10 +90,29 @@
|
|||||||
"addTimeLog": "Add new time log",
|
"addTimeLog": "Add new time log",
|
||||||
"totalLogged": "Total Logged",
|
"totalLogged": "Total Logged",
|
||||||
"exportToExcel": "Export to Excel",
|
"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": {
|
"taskActivityLogTab": {
|
||||||
"title": "Activity Log"
|
"title": "Activity Log",
|
||||||
|
"add": "ADD",
|
||||||
|
"remove": "REMOVE",
|
||||||
|
"none": "None",
|
||||||
|
"weight": "Weight",
|
||||||
|
"createdTask": "created the task."
|
||||||
},
|
},
|
||||||
"taskProgress": {
|
"taskProgress": {
|
||||||
"markAsDoneTitle": "Mark Task as Done?",
|
"markAsDoneTitle": "Mark Task as Done?",
|
||||||
|
|||||||
@@ -55,5 +55,18 @@
|
|||||||
"selectCategory": "Select a category",
|
"selectCategory": "Select a category",
|
||||||
"pleaseEnterAName": "Please enter a name",
|
"pleaseEnterAName": "Please enter a name",
|
||||||
"pleaseSelectACategory": "Please select a category",
|
"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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,9 +36,10 @@
|
|||||||
"selectText": "Select",
|
"selectText": "Select",
|
||||||
"labelsSelectorInputTip": "Hit enter to create!",
|
"labelsSelectorInputTip": "Hit enter to create!",
|
||||||
|
|
||||||
"addTaskText": "+ Add Task",
|
"addTaskText": "Add Task",
|
||||||
"addSubTaskText": "+ Add Sub Task",
|
"addSubTaskText": "Add Sub Task",
|
||||||
"addTaskInputPlaceholder": "Type your task and hit enter",
|
"addTaskInputPlaceholder": "Type your task and hit enter",
|
||||||
|
"noTasksInGroup": "No tasks in this group",
|
||||||
|
|
||||||
"openButton": "Open",
|
"openButton": "Open",
|
||||||
"okButton": "Ok",
|
"okButton": "Ok",
|
||||||
@@ -47,7 +48,7 @@
|
|||||||
"searchInputPlaceholder": "Search or create",
|
"searchInputPlaceholder": "Search or create",
|
||||||
"assigneeSelectorInviteButton": "Invite a new member by email",
|
"assigneeSelectorInviteButton": "Invite a new member by email",
|
||||||
"labelInputPlaceholder": "Search or create",
|
"labelInputPlaceholder": "Search or create",
|
||||||
|
|
||||||
"pendingInvitation": "Pending Invitation",
|
"pendingInvitation": "Pending Invitation",
|
||||||
|
|
||||||
"contextMenu": {
|
"contextMenu": {
|
||||||
@@ -59,5 +60,74 @@
|
|||||||
"convertToTask": "Convert to Task",
|
"convertToTask": "Convert to Task",
|
||||||
"delete": "Delete",
|
"delete": "Delete",
|
||||||
"searchByNameInputPlaceholder": "Search by name"
|
"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"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
35
worklenz-frontend/public/locales/en/task-management.json
Normal file
35
worklenz-frontend/public/locales/en/task-management.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@
|
|||||||
"cancelText": "Cancel",
|
"cancelText": "Cancel",
|
||||||
"saveText": "Save",
|
"saveText": "Save",
|
||||||
"templateNameText": "Template Name",
|
"templateNameText": "Template Name",
|
||||||
|
"templateNameRequired": "Template name is required",
|
||||||
"selectedTasks": "Selected Tasks",
|
"selectedTasks": "Selected Tasks",
|
||||||
"removeTask": "Remove",
|
"removeTask": "Remove",
|
||||||
"cancelButton": "Cancel",
|
"cancelButton": "Cancel",
|
||||||
|
|||||||
@@ -1,24 +1,41 @@
|
|||||||
{
|
{
|
||||||
"taskSelected": "task selected",
|
"taskSelected": "task selected",
|
||||||
"tasksSelected": "tasks selected",
|
"tasksSelected": "tasks selected",
|
||||||
"changeStatus": "Change Status/ Prioriy/ Phases",
|
"changeStatus": "Change Status/ Prioriy/ Phases",
|
||||||
"changeLabel": "Change Label",
|
"changeLabel": "Change Label",
|
||||||
"assignToMe": "Assign to me",
|
"assignToMe": "Assign to me",
|
||||||
"changeAssignees": "Change Assignees",
|
"changeAssignees": "Change Assignees",
|
||||||
"archive": "Archive",
|
"archive": "Archive",
|
||||||
"unarchive": "Unarchive",
|
"unarchive": "Unarchive",
|
||||||
"delete": "Delete",
|
"delete": "Delete",
|
||||||
"moreOptions": "More options",
|
"moreOptions": "More options",
|
||||||
"deselectAll": "Deselect all",
|
"deselectAll": "Deselect all",
|
||||||
"status": "Status",
|
"status": "Status",
|
||||||
"priority": "Priority",
|
"priority": "Priority",
|
||||||
"phase": "Phase",
|
"phase": "Phase",
|
||||||
"member": "Member",
|
"member": "Member",
|
||||||
"createTaskTemplate": "Create Task Template",
|
"createTaskTemplate": "Create Task Template",
|
||||||
"apply": "Apply",
|
"apply": "Apply",
|
||||||
"createLabel": "+ Create Label",
|
"createLabel": "+ Create Label",
|
||||||
"hitEnterToCreate": "Press Enter to create",
|
"searchOrCreateLabel": "Search or create label...",
|
||||||
"pendingInvitation": "Pending Invitation",
|
"hitEnterToCreate": "Press Enter to create",
|
||||||
"noMatchingLabels": "No matching labels",
|
"labelExists": "Label already exists",
|
||||||
"noLabels": "No labels"
|
"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."
|
||||||
|
}
|
||||||
|
|||||||
@@ -40,5 +40,18 @@
|
|||||||
"noCategory": "No Category",
|
"noCategory": "No Category",
|
||||||
"noProjects": "No projects found",
|
"noProjects": "No projects found",
|
||||||
"noTeams": "No teams 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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"title": "Unauthorized!",
|
"title": "Unauthorized!",
|
||||||
"subtitle": "You are not authorized to access this page",
|
"subtitle": "You are not authorized to access this page",
|
||||||
"button": "Go to Home"
|
"button": "Go to Home"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
"paymentMethod": "Método de Pago",
|
"paymentMethod": "Método de Pago",
|
||||||
"status": "Estado",
|
"status": "Estado",
|
||||||
"ltdUsers": "Puedes agregar hasta {{ltd_users}} usuarios.",
|
"ltdUsers": "Puedes agregar hasta {{ltd_users}} usuarios.",
|
||||||
|
|
||||||
"drawerTitle": "Canjear Código",
|
"drawerTitle": "Canjear Código",
|
||||||
"label": "Canjear Código",
|
"label": "Canjear Código",
|
||||||
"drawerPlaceholder": "Ingrese su código de canje",
|
"drawerPlaceholder": "Ingrese su código de canje",
|
||||||
@@ -98,7 +98,7 @@
|
|||||||
"perMonthPerUser": "por usuario / mes",
|
"perMonthPerUser": "por usuario / mes",
|
||||||
"viewInvoice": "Ver Factura",
|
"viewInvoice": "Ver Factura",
|
||||||
"switchToFreePlan": "Cambiar a Plan Gratuito",
|
"switchToFreePlan": "Cambiar a Plan Gratuito",
|
||||||
|
|
||||||
"expirestoday": "hoy",
|
"expirestoday": "hoy",
|
||||||
"expirestomorrow": "mañana",
|
"expirestomorrow": "mañana",
|
||||||
"expiredDaysAgo": "hace {{days}} días",
|
"expiredDaysAgo": "hace {{days}} días",
|
||||||
|
|||||||
@@ -19,5 +19,13 @@
|
|||||||
"unarchiveConfirm": "¿Estás seguro de que deseas desarchivar este proyecto?",
|
"unarchiveConfirm": "¿Estás seguro de que deseas desarchivar este proyecto?",
|
||||||
"clickToFilter": "Clique para filtrar por",
|
"clickToFilter": "Clique para filtrar por",
|
||||||
"noProjects": "No se encontraron proyectos",
|
"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"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,5 +19,12 @@
|
|||||||
"archive": "Archivar",
|
"archive": "Archivar",
|
||||||
|
|
||||||
"newTaskNamePlaceholder": "Escribe un nombre de tarea",
|
"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"
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"configurePhases": "Configurar fases",
|
"configurePhases": "Configurar fases",
|
||||||
"phaseLabel": "Etiqueta de fase",
|
"phaseLabel": "Etiqueta de fase",
|
||||||
"enterPhaseName": "Ingrese un nombre para la etiqueta de fase",
|
"enterPhaseName": "Ingrese un nombre para la etiqueta de fase",
|
||||||
"addOption": "Agregar opción",
|
"addOption": "Agregar opción",
|
||||||
"phaseOptions": "Opciones de fase:"
|
"phaseOptions": "Opciones de fase:"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,5 +47,6 @@
|
|||||||
"weightedProgress": "Progreso Ponderado",
|
"weightedProgress": "Progreso Ponderado",
|
||||||
"weightedProgressTooltip": "Calcular el progreso basado en los pesos de las subtareas",
|
"weightedProgressTooltip": "Calcular el progreso basado en los pesos de las subtareas",
|
||||||
"timeProgress": "Progreso Basado en Tiempo",
|
"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"
|
||||||
}
|
}
|
||||||
|
|||||||
14
worklenz-frontend/public/locales/es/project-view.json
Normal file
14
worklenz-frontend/public/locales/es/project-view.json
Normal 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"
|
||||||
|
}
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
{
|
{
|
||||||
"importTaskTemplate": "Importar plantilla de tarea",
|
"importTaskTemplate": "Importar plantilla de tarea",
|
||||||
"templateName": "Nombre de la plantilla",
|
"templateName": "Nombre de la plantilla",
|
||||||
"templateDescription": "Descripción de la plantilla",
|
"templateDescription": "Descripción de la plantilla",
|
||||||
"selectedTasks": "Tareas seleccionadas",
|
"selectedTasks": "Tareas seleccionadas",
|
||||||
"tasks": "Tareas",
|
"tasks": "Tareas",
|
||||||
"templates": "Plantillas",
|
"templates": "Plantillas",
|
||||||
"remove": "Eliminar",
|
"remove": "Eliminar",
|
||||||
"cancel": "Cancelar",
|
"cancel": "Cancelar",
|
||||||
"import": "Importar"
|
"import": "Importar"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
{
|
{
|
||||||
"title": "Miembros del Proyecto",
|
"title": "Miembros del Proyecto",
|
||||||
"searchLabel": "Agregar miembros ingresando su nombre o correo electrónico",
|
"searchLabel": "Agregar miembros ingresando su nombre o correo electrónico",
|
||||||
"searchPlaceholder": "Escriba nombre o correo electrónico",
|
"searchPlaceholder": "Escriba nombre o correo electrónico",
|
||||||
"inviteAsAMember": "Invitar como miembro",
|
"inviteAsAMember": "Invitar como miembro",
|
||||||
"inviteNewMemberByEmail": "Invitar nuevo miembro por correo electrónico"
|
"inviteNewMemberByEmail": "Invitar nuevo miembro por correo electrónico"
|
||||||
|
}
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,13 +1,29 @@
|
|||||||
{
|
{
|
||||||
"importTasks": "Importar tareas",
|
"importTasks": "Importar tareas",
|
||||||
"createTask": "Crear tarea",
|
"importTask": "Importar tarea",
|
||||||
"settings": "Ajustes",
|
"createTask": "Crear tarea",
|
||||||
"subscribe": "Suscribirse",
|
"settings": "Configuración",
|
||||||
"unsubscribe": "Cancelar suscripción",
|
"subscribe": "Suscribirse",
|
||||||
"deleteProject": "Eliminar proyecto",
|
"unsubscribe": "Cancelar suscripción",
|
||||||
"startDate": "Fecha de inicio",
|
"deleteProject": "Eliminar proyecto",
|
||||||
"endDate": "Fecha de finalización",
|
"startDate": "Fecha de inicio",
|
||||||
"projectSettings": "Ajustes del proyecto",
|
"endDate": "Fecha de finalización",
|
||||||
"projectSummary": "Resumen del proyecto",
|
"projectSettings": "Configuración del proyecto",
|
||||||
"receiveProjectSummary": "Recibir un resumen del proyecto todas las noches."
|
"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"
|
||||||
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
"taskIncludes": "¿Qué se debe incluir en la plantilla de las tareas?",
|
"taskIncludes": "¿Qué se debe incluir en la plantilla de las tareas?",
|
||||||
"taskIncludesOptions": {
|
"taskIncludesOptions": {
|
||||||
"statuses": "Estados",
|
"statuses": "Estados",
|
||||||
"phases": "Fases",
|
"phases": "Fases",
|
||||||
"labels": "Etiquetas",
|
"labels": "Etiquetas",
|
||||||
"name": "Nombre",
|
"name": "Nombre",
|
||||||
"priority": "Prioridad",
|
"priority": "Prioridad",
|
||||||
|
|||||||
@@ -2,4 +2,4 @@
|
|||||||
"title": "Apariencia",
|
"title": "Apariencia",
|
||||||
"darkMode": "Modo Oscuro",
|
"darkMode": "Modo Oscuro",
|
||||||
"darkModeDescription": "Cambia entre el modo claro y oscuro para personalizar tu experiencia visual."
|
"darkModeDescription": "Cambia entre el modo claro y oscuro para personalizar tu experiencia visual."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,5 +9,6 @@
|
|||||||
"saveChanges": "Guardar cambios",
|
"saveChanges": "Guardar cambios",
|
||||||
"profileJoinedText": "Se unió hace un mes",
|
"profileJoinedText": "Se unió hace un mes",
|
||||||
"profileLastUpdatedText": "Última actualización 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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
{
|
{
|
||||||
|
"title": "Miembros del Equipo",
|
||||||
"nameColumn": "Nombre",
|
"nameColumn": "Nombre",
|
||||||
"projectsColumn": "Proyectos",
|
"projectsColumn": "Proyectos",
|
||||||
"emailColumn": "Correo electrónico",
|
"emailColumn": "Correo electrónico",
|
||||||
@@ -40,5 +41,7 @@
|
|||||||
"ownerText": "Propietario del equipo",
|
"ownerText": "Propietario del equipo",
|
||||||
"addedText": "Agregado",
|
"addedText": "Agregado",
|
||||||
"updatedText": "Actualizado",
|
"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!"
|
||||||
}
|
}
|
||||||
|
|||||||
16
worklenz-frontend/public/locales/es/settings/teams.json
Normal file
16
worklenz-frontend/public/locales/es/settings/teams.json
Normal 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!"
|
||||||
|
}
|
||||||
@@ -27,4 +27,4 @@
|
|||||||
"add-sub-task": "+ Añadir subtarea",
|
"add-sub-task": "+ Añadir subtarea",
|
||||||
"refresh-sub-tasks": "Actualizar subtareas"
|
"refresh-sub-tasks": "Actualizar subtareas"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,4 +31,4 @@
|
|||||||
"intervalWeeks": "Intervalo (semanas)",
|
"intervalWeeks": "Intervalo (semanas)",
|
||||||
"intervalMonths": "Intervalo (meses)",
|
"intervalMonths": "Intervalo (meses)",
|
||||||
"saveChanges": "Guardar cambios"
|
"saveChanges": "Guardar cambios"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,93 +1,123 @@
|
|||||||
{
|
{
|
||||||
"taskHeader": {
|
"taskHeader": {
|
||||||
"taskNamePlaceholder": "Escribe tu tarea",
|
"taskNamePlaceholder": "Escriba su Tarea",
|
||||||
"deleteTask": "Eliminar tarea"
|
"deleteTask": "Eliminar Tarea"
|
||||||
},
|
},
|
||||||
"taskInfoTab": {
|
"taskInfoTab": {
|
||||||
"title": "Información",
|
"title": "Información",
|
||||||
"details": {
|
"details": {
|
||||||
"title": "Detalles",
|
"title": "Detalles",
|
||||||
"task-key": "Clave de tarea",
|
"task-key": "Clave de Tarea",
|
||||||
"phase": "Fase",
|
"phase": "Fase",
|
||||||
"assignees": "Asignados",
|
"assignees": "Asignados",
|
||||||
"due-date": "Fecha de vencimiento",
|
"due-date": "Fecha de Vencimiento",
|
||||||
"time-estimation": "Estimación de tiempo",
|
"time-estimation": "Estimación de Tiempo",
|
||||||
"priority": "Prioridad",
|
"priority": "Prioridad",
|
||||||
"labels": "Etiquetas",
|
"labels": "Etiquetas",
|
||||||
"billable": "Facturable",
|
"billable": "Facturable",
|
||||||
"notify": "Notificar",
|
"notify": "Notificar",
|
||||||
"when-done-notify": "Al terminar, notificar",
|
"when-done-notify": "Al terminar, notificar",
|
||||||
"start-date": "Fecha de inicio",
|
"start-date": "Fecha de Inicio",
|
||||||
"end-date": "Fecha de finalización",
|
"end-date": "Fecha de Fin",
|
||||||
"hide-start-date": "Ocultar fecha de inicio",
|
"hide-start-date": "Ocultar Fecha de Inicio",
|
||||||
"show-start-date": "Mostrar fecha de inicio",
|
"show-start-date": "Mostrar Fecha de Inicio",
|
||||||
"hours": "Horas",
|
"hours": "Horas",
|
||||||
"minutes": "Minutos",
|
"minutes": "Minutos",
|
||||||
"progressValue": "Valor de Progreso",
|
"progressValue": "Valor de Progreso",
|
||||||
"progressValueTooltip": "Establecer el porcentaje de progreso (0-100%)",
|
"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",
|
"progressValueRange": "El progreso debe estar entre 0 y 100",
|
||||||
"taskWeight": "Peso de la Tarea",
|
"taskWeight": "Peso de la Tarea",
|
||||||
"taskWeightTooltip": "Establecer el peso de esta subtarea (porcentaje)",
|
"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",
|
"taskWeightRange": "El peso debe estar entre 0 y 100",
|
||||||
"recurring": "Recurrente"
|
"recurring": "Recurrente"
|
||||||
},
|
},
|
||||||
"labels": {
|
"labels": {
|
||||||
"labelInputPlaceholder": "Buscar o crear",
|
"labelInputPlaceholder": "Buscar o crear",
|
||||||
"labelsSelectorInputTip": "Pulse Enter para crear"
|
"labelsSelectorInputTip": "Presiona Enter para crear"
|
||||||
},
|
},
|
||||||
"description": {
|
"description": {
|
||||||
"title": "Descripción",
|
"title": "Descripción",
|
||||||
"placeholder": "Añadir una descripción más detallada..."
|
"placeholder": "Añadir una descripción más detallada..."
|
||||||
},
|
},
|
||||||
"subTasks": {
|
"subTasks": {
|
||||||
"title": "Subtareas",
|
"title": "Sub Tareas",
|
||||||
"addSubTask": "+ Añadir subtarea",
|
"addSubTask": "Agregar Sub Tarea",
|
||||||
"addSubTaskInputPlaceholder": "Escribe tu tarea y pulsa enter",
|
"addSubTaskInputPlaceholder": "Escriba su tarea y presione enter",
|
||||||
"refreshSubTasks": "Actualizar subtareas",
|
"refreshSubTasks": "Actualizar Sub Tareas",
|
||||||
"edit": "Editar",
|
"edit": "Editar",
|
||||||
"delete": "Eliminar",
|
"delete": "Eliminar",
|
||||||
"confirmDeleteSubTask": "¿Estás seguro de que quieres eliminar esta subtarea?",
|
"confirmDeleteSubTask": "¿Está seguro de que desea eliminar esta subtarea?",
|
||||||
"deleteSubTask": "Eliminar subtarea"
|
"deleteSubTask": "Eliminar Sub Tarea"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"title": "Dependencias",
|
"title": "Dependencias",
|
||||||
"addDependency": "+ Añadir nueva dependencia",
|
"addDependency": "+ Agregar nueva dependencia",
|
||||||
"blockedBy": "Bloqueado por",
|
"blockedBy": "Bloqueado por",
|
||||||
"searchTask": "Escribe para buscar tarea",
|
"searchTask": "Escribir para buscar tarea",
|
||||||
"noTasksFound": "No se encontraron tareas",
|
"noTasksFound": "No se encontraron tareas",
|
||||||
"confirmDeleteDependency": "¿Estás seguro de que quieres eliminar?"
|
"confirmDeleteDependency": "¿Está seguro de que desea eliminar?"
|
||||||
},
|
},
|
||||||
"attachments": {
|
"attachments": {
|
||||||
"title": "Adjuntos",
|
"title": "Adjuntos",
|
||||||
"chooseOrDropFileToUpload": "Elige o arrastra un archivo para subir",
|
"chooseOrDropFileToUpload": "Elija o arrastre un archivo para subir",
|
||||||
"uploading": "Subiendo..."
|
"uploading": "Subiendo..."
|
||||||
},
|
},
|
||||||
"comments": {
|
"comments": {
|
||||||
"title": "Comentarios",
|
"title": "Comentarios",
|
||||||
"addComment": "+ Añadir nuevo comentario",
|
"addComment": "+ Agregar nuevo comentario",
|
||||||
"noComments": "No hay comentarios todavía. ¡Sé el primero en comentar!",
|
"noComments": "Aún no hay comentarios. ¡Sé el primero en comentar!",
|
||||||
"delete": "Eliminar",
|
"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",
|
"searchInputPlaceholder": "Buscar por nombre",
|
||||||
"pendingInvitation": "Invitación pendiente"
|
"pendingInvitation": "Invitación Pendiente"
|
||||||
},
|
},
|
||||||
"taskTimeLogTab": {
|
"taskTimeLogTab": {
|
||||||
"title": "Registro de tiempo",
|
"title": "Registro de Tiempo",
|
||||||
"addTimeLog": "Añadir nuevo registro de tiempo",
|
"addTimeLog": "Añadir nuevo registro de tiempo",
|
||||||
"totalLogged": "Total registrado",
|
"totalLogged": "Total Registrado",
|
||||||
"exportToExcel": "Exportar a Excel",
|
"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": {
|
"taskActivityLogTab": {
|
||||||
"title": "Registro de actividad"
|
"title": "Registro de Actividad",
|
||||||
|
"add": "AGREGAR",
|
||||||
|
"remove": "QUITAR",
|
||||||
|
"none": "Ninguno",
|
||||||
|
"weight": "Peso",
|
||||||
|
"createdTask": "creó la tarea."
|
||||||
},
|
},
|
||||||
"taskProgress": {
|
"taskProgress": {
|
||||||
"markAsDoneTitle": "¿Marcar Tarea como Completada?",
|
"markAsDoneTitle": "¿Marcar Tarea como Completada?",
|
||||||
"confirmMarkAsDone": "Sí, marcar como completada",
|
"confirmMarkAsDone": "Sí, marcar como completada",
|
||||||
"cancelMarkAsDone": "No, mantener estado actual",
|
"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\"?"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,5 +51,18 @@
|
|||||||
"selectCategory": "Seleccionar una categoría",
|
"selectCategory": "Seleccionar una categoría",
|
||||||
"pleaseEnterAName": "Por favor, ingrese un nombre",
|
"pleaseEnterAName": "Por favor, ingrese un nombre",
|
||||||
"pleaseSelectACategory": "Por favor, seleccione una categoría",
|
"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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,8 +36,9 @@
|
|||||||
"selectText": "Seleccionar",
|
"selectText": "Seleccionar",
|
||||||
"labelsSelectorInputTip": "¡Presiona enter para crear!",
|
"labelsSelectorInputTip": "¡Presiona enter para crear!",
|
||||||
|
|
||||||
"addTaskText": "+ Agregar tarea",
|
"addTaskText": "Agregar tarea",
|
||||||
"addSubTaskText": "+ Agregar subtarea",
|
"addSubTaskText": "Agregar subtarea",
|
||||||
|
"noTasksInGroup": "No hay tareas en este grupo",
|
||||||
"addTaskInputPlaceholder": "Escribe tu tarea y presiona enter",
|
"addTaskInputPlaceholder": "Escribe tu tarea y presiona enter",
|
||||||
|
|
||||||
"openButton": "Abrir",
|
"openButton": "Abrir",
|
||||||
@@ -47,7 +48,7 @@
|
|||||||
"searchInputPlaceholder": "Buscar o crear",
|
"searchInputPlaceholder": "Buscar o crear",
|
||||||
"assigneeSelectorInviteButton": "Invitar a un nuevo miembro por correo",
|
"assigneeSelectorInviteButton": "Invitar a un nuevo miembro por correo",
|
||||||
"labelInputPlaceholder": "Buscar o crear",
|
"labelInputPlaceholder": "Buscar o crear",
|
||||||
|
|
||||||
"pendingInvitation": "Invitación pendiente",
|
"pendingInvitation": "Invitación pendiente",
|
||||||
|
|
||||||
"contextMenu": {
|
"contextMenu": {
|
||||||
@@ -59,5 +60,74 @@
|
|||||||
"convertToTask": "Convertir en tarea",
|
"convertToTask": "Convertir en tarea",
|
||||||
"delete": "Eliminar",
|
"delete": "Eliminar",
|
||||||
"searchByNameInputPlaceholder": "Buscar por nombre"
|
"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"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
21
worklenz-frontend/public/locales/es/task-management.json
Normal file
21
worklenz-frontend/public/locales/es/task-management.json
Normal 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"
|
||||||
|
}
|
||||||
@@ -1,24 +1,41 @@
|
|||||||
{
|
{
|
||||||
"taskSelected": "Tarea seleccionada",
|
"taskSelected": "Tarea seleccionada",
|
||||||
"tasksSelected": "Tareas seleccionadas",
|
"tasksSelected": "Tareas seleccionadas",
|
||||||
"changeStatus": "Cambiar estado/ prioridad/ fases",
|
"changeStatus": "Cambiar estado/ prioridad/ fases",
|
||||||
"changeLabel": "Cambiar etiqueta",
|
"changeLabel": "Cambiar etiqueta",
|
||||||
"assignToMe": "Asignar a mí",
|
"assignToMe": "Asignar a mí",
|
||||||
"changeAssignees": "Cambiar asignados",
|
"changeAssignees": "Cambiar asignados",
|
||||||
"archive": "Archivar",
|
"archive": "Archivar",
|
||||||
"unarchive": "Desarchivar",
|
"unarchive": "Desarchivar",
|
||||||
"delete": "Eliminar",
|
"delete": "Eliminar",
|
||||||
"moreOptions": "Más opciones",
|
"moreOptions": "Más opciones",
|
||||||
"deselectAll": "Deseleccionar todo",
|
"deselectAll": "Deseleccionar todo",
|
||||||
"status": "Estado",
|
"status": "Estado",
|
||||||
"priority": "Prioridad",
|
"priority": "Prioridad",
|
||||||
"phase": "Fase",
|
"phase": "Fase",
|
||||||
"member": "Miembro",
|
"member": "Miembro",
|
||||||
"createTaskTemplate": "Crear plantilla de tarea",
|
"createTaskTemplate": "Crear plantilla de tarea",
|
||||||
"apply": "Aplicar",
|
"apply": "Aplicar",
|
||||||
"createLabel": "+ Crear etiqueta",
|
"createLabel": "+ Crear etiqueta",
|
||||||
"hitEnterToCreate": "Presione Enter para crear",
|
"searchOrCreateLabel": "Buscar o crear etiqueta...",
|
||||||
"pendingInvitation": "Invitación Pendiente",
|
"hitEnterToCreate": "Presione Enter para crear",
|
||||||
"noMatchingLabels": "No hay etiquetas coincidentes",
|
"labelExists": "La etiqueta ya existe",
|
||||||
"noLabels": "Sin etiquetas"
|
"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."
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
"selectAll": "Seleccionar Todo",
|
"selectAll": "Seleccionar Todo",
|
||||||
"teams": "Equipos",
|
"teams": "Equipos",
|
||||||
|
|
||||||
"searchByProject": "Buscar por nombre de proyecto",
|
"searchByProject": "Buscar por nombre del proyecto",
|
||||||
"projects": "Proyectos",
|
"projects": "Proyectos",
|
||||||
|
|
||||||
"searchByCategory": "Buscar por nombre de categoría",
|
"searchByCategory": "Buscar por nombre de categoría",
|
||||||
@@ -37,8 +37,21 @@
|
|||||||
"actualDays": "Días Reales",
|
"actualDays": "Días Reales",
|
||||||
|
|
||||||
"noCategories": "No se encontraron categorías",
|
"noCategories": "No se encontraron categorías",
|
||||||
"noCategory": "No Categoría",
|
"noCategory": "Sin Categoría",
|
||||||
"noProjects": "No se encontraron proyectos",
|
"noProjects": "No se encontraron proyectos",
|
||||||
"noTeams": "No se encontraron equipos",
|
"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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"title": "¡No autorizado!",
|
"title": "¡No autorizado!",
|
||||||
"subtitle": "No tienes permisos para acceder a esta página",
|
"subtitle": "No tienes permisos para acceder a esta página",
|
||||||
"button": "Ir a Inicio"
|
"button": "Ir a Inicio"
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user