Compare commits
72 Commits
v2.1.0
...
fix/home-p
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e87f33dcc8 | ||
|
|
6286d4315d | ||
|
|
a1234b8af0 | ||
|
|
bc0a62002b | ||
|
|
52eca27619 | ||
|
|
e4c9e22972 | ||
|
|
20e7d3c51a | ||
|
|
6d5aa0ccab | ||
|
|
7618ae7c6a | ||
|
|
808731387b | ||
|
|
502726cd83 | ||
|
|
a26d8d0f90 | ||
|
|
747088e7cc | ||
|
|
affbbbffbf | ||
|
|
12b430a349 | ||
|
|
2f3e555b5a | ||
|
|
2498effce3 | ||
|
|
2ad3c2dcd4 | ||
|
|
6226ae35ff | ||
|
|
26de439fab | ||
|
|
295d7a92df | ||
|
|
e20ab86d6e | ||
|
|
5c938586b8 | ||
|
|
93b67fba07 | ||
|
|
e4dfae9f1d | ||
|
|
0efcbf448b | ||
|
|
f2f12a2dfa | ||
|
|
ea37b55078 | ||
|
|
cc0ff20ca1 | ||
|
|
6b58709848 | ||
|
|
f2b1262e3d | ||
|
|
7def564950 | ||
|
|
278e221c75 | ||
|
|
d9a5f76449 | ||
|
|
b9b707410d | ||
|
|
87675cc73c | ||
|
|
0e083868cb | ||
|
|
94977f7255 | ||
|
|
cf686ef8c5 | ||
|
|
857b48e225 | ||
|
|
f846230d59 | ||
|
|
bcfa18b1e8 | ||
|
|
bb8e6ee60f | ||
|
|
6ebdd78855 | ||
|
|
70cca5d4c0 | ||
|
|
6448d24e20 | ||
|
|
5fb2633bc5 | ||
|
|
75c55fff21 | ||
|
|
8f5de8f1a1 | ||
|
|
db9b481e8d | ||
|
|
cdd22e5f2f | ||
|
|
635b5ce8e1 | ||
|
|
1a476a0e3c | ||
|
|
80b1d6c292 | ||
|
|
deb0f3f602 | ||
|
|
71f168f8fa | ||
|
|
6f63041148 | ||
|
|
399a01904a | ||
|
|
9cc19460bd | ||
|
|
2920f131f8 | ||
|
|
04f622a7f0 | ||
|
|
fadc115412 | ||
|
|
10c53d954e | ||
|
|
29a09ec500 | ||
|
|
6dba080ade | ||
|
|
ab7ca33ac1 | ||
|
|
bc6a15de8f | ||
|
|
a47a9045e6 | ||
|
|
b6e92b4211 | ||
|
|
6c08f10e9d | ||
|
|
f80ec9797e | ||
|
|
fbbd820512 |
412
README.md
412
README.md
@@ -6,6 +6,24 @@
|
|||||||
Worklenz
|
Worklenz
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://github.com/Worklenz/worklenz/blob/main/LICENSE">
|
||||||
|
<img src="https://img.shields.io/badge/license-AGPL--3.0-blue.svg" alt="License">
|
||||||
|
</a>
|
||||||
|
<a href="https://github.com/Worklenz/worklenz/releases">
|
||||||
|
<img src="https://img.shields.io/github/v/release/Worklenz/worklenz" alt="Release">
|
||||||
|
</a>
|
||||||
|
<a href="https://github.com/Worklenz/worklenz/stargazers">
|
||||||
|
<img src="https://img.shields.io/github/stars/Worklenz/worklenz" alt="Stars">
|
||||||
|
</a>
|
||||||
|
<a href="https://github.com/Worklenz/worklenz/network/members">
|
||||||
|
<img src="https://img.shields.io/github/forks/Worklenz/worklenz" alt="Forks">
|
||||||
|
</a>
|
||||||
|
<a href="https://github.com/Worklenz/worklenz/issues">
|
||||||
|
<img src="https://img.shields.io/github/issues/Worklenz/worklenz" alt="Issues">
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://worklenz.com/task-management/">Task Management</a> |
|
<a href="https://worklenz.com/task-management/">Task Management</a> |
|
||||||
<a href="https://worklenz.com/time-tracking/">Time Tracking</a> |
|
<a href="https://worklenz.com/time-tracking/">Time Tracking</a> |
|
||||||
@@ -27,6 +45,24 @@
|
|||||||
Worklenz is a project management tool designed to help organizations improve their efficiency. It provides a
|
Worklenz is a project management tool designed to help organizations improve their efficiency. It provides a
|
||||||
comprehensive solution for managing projects, tasks, and collaboration within teams.
|
comprehensive solution for managing projects, tasks, and collaboration within teams.
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
|
||||||
|
- [Features](#features)
|
||||||
|
- [Tech Stack](#tech-stack)
|
||||||
|
- [Getting Started](#getting-started)
|
||||||
|
- [Quick Start (Docker)](#-quick-start-docker---recommended)
|
||||||
|
- [Manual Installation](#️-manual-installation-for-development)
|
||||||
|
- [Deployment](#deployment)
|
||||||
|
- [Local Development](#local-development-with-docker)
|
||||||
|
- [Remote Server Deployment](#remote-server-deployment)
|
||||||
|
- [Configuration](#configuration)
|
||||||
|
- [MinIO Integration](#minio-integration)
|
||||||
|
- [Security](#security)
|
||||||
|
- [Analytics](#analytics)
|
||||||
|
- [Screenshots](#screenshots)
|
||||||
|
- [Contributing](#contributing)
|
||||||
|
- [License](#license)
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- **Project Planning**: Create and organize projects, assign tasks to team members.
|
- **Project Planning**: Create and organize projects, assign tasks to team members.
|
||||||
@@ -50,42 +86,80 @@ This repository contains the frontend and backend code for Worklenz.
|
|||||||
|
|
||||||
## Getting Started
|
## Getting Started
|
||||||
|
|
||||||
These instructions will help you set up and run the Worklenz project on your local machine for development and testing purposes.
|
Choose your preferred setup method below. Docker is recommended for quick setup and testing.
|
||||||
|
|
||||||
### Prerequisites
|
### 🚀 Quick Start (Docker - Recommended)
|
||||||
|
|
||||||
- Node.js (version 18 or higher)
|
The fastest way to get Worklenz running locally with all dependencies included.
|
||||||
- PostgreSQL database
|
|
||||||
- An S3-compatible storage service (like MinIO) or Azure Blob Storage
|
|
||||||
|
|
||||||
### Option 1: Manual Installation
|
**Prerequisites:**
|
||||||
|
- Docker and Docker Compose installed on your system
|
||||||
|
- Git
|
||||||
|
|
||||||
1. Clone the repository
|
**Steps:**
|
||||||
|
|
||||||
|
1. Clone the repository:
|
||||||
```bash
|
```bash
|
||||||
git clone https://github.com/Worklenz/worklenz.git
|
git clone https://github.com/Worklenz/worklenz.git
|
||||||
cd worklenz
|
cd worklenz
|
||||||
```
|
```
|
||||||
|
|
||||||
2. Set up environment variables
|
2. Start the Docker containers:
|
||||||
- Copy the example environment files
|
|
||||||
```bash
|
|
||||||
cp .env.example .env
|
|
||||||
cp worklenz-backend/.env.example worklenz-backend/.env
|
|
||||||
```
|
|
||||||
- Update the environment variables with your configuration
|
|
||||||
|
|
||||||
3. Install dependencies
|
|
||||||
```bash
|
```bash
|
||||||
# Install backend dependencies
|
docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Access the application:
|
||||||
|
- **Frontend**: http://localhost:5000
|
||||||
|
- **Backend API**: http://localhost:3000
|
||||||
|
- **MinIO Console**: http://localhost:9001 (login: minioadmin/minioadmin)
|
||||||
|
|
||||||
|
4. To stop the services:
|
||||||
|
```bash
|
||||||
|
docker-compose down
|
||||||
|
```
|
||||||
|
|
||||||
|
**Alternative startup methods:**
|
||||||
|
- **Windows**: Run `start.bat`
|
||||||
|
- **Linux/macOS**: Run `./start.sh`
|
||||||
|
|
||||||
|
**Video Guide**: For a visual walkthrough of the local Docker deployment process, check out our [step-by-step video guide](https://www.youtube.com/watch?v=AfwAKxJbqLg).
|
||||||
|
|
||||||
|
### 🛠️ Manual Installation (For Development)
|
||||||
|
|
||||||
|
For developers who want to run the services individually or customize the setup.
|
||||||
|
|
||||||
|
**Prerequisites:**
|
||||||
|
- Node.js (version 18 or higher)
|
||||||
|
- PostgreSQL (version 15 or higher)
|
||||||
|
- An S3-compatible storage service (like MinIO) or Azure Blob Storage
|
||||||
|
|
||||||
|
**Steps:**
|
||||||
|
|
||||||
|
1. Clone the repository:
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/Worklenz/worklenz.git
|
||||||
|
cd worklenz
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Set up environment variables:
|
||||||
|
```bash
|
||||||
|
cp worklenz-backend/.env.template worklenz-backend/.env
|
||||||
|
# Update the environment variables with your configuration
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Install dependencies:
|
||||||
|
```bash
|
||||||
|
# Backend dependencies
|
||||||
cd worklenz-backend
|
cd worklenz-backend
|
||||||
npm install
|
npm install
|
||||||
|
|
||||||
# Install frontend dependencies
|
# Frontend dependencies
|
||||||
cd ../worklenz-frontend
|
cd ../worklenz-frontend
|
||||||
npm install
|
npm install
|
||||||
```
|
```
|
||||||
|
|
||||||
4. Set up the database
|
4. Set up the database:
|
||||||
```bash
|
```bash
|
||||||
# Create a PostgreSQL database named worklenz_db
|
# Create a PostgreSQL database named worklenz_db
|
||||||
cd worklenz-backend
|
cd worklenz-backend
|
||||||
@@ -101,49 +175,47 @@ psql -U your_username -d worklenz_db -f database/sql/2_dml.sql
|
|||||||
psql -U your_username -d worklenz_db -f database/sql/5_database_user.sql
|
psql -U your_username -d worklenz_db -f database/sql/5_database_user.sql
|
||||||
```
|
```
|
||||||
|
|
||||||
5. Start the development servers
|
5. Start the development servers:
|
||||||
```bash
|
```bash
|
||||||
# In one terminal, start the backend
|
# Terminal 1: Start the backend
|
||||||
cd worklenz-backend
|
cd worklenz-backend
|
||||||
npm run dev
|
npm run dev
|
||||||
|
|
||||||
# In another terminal, start the frontend
|
# Terminal 2: Start the frontend
|
||||||
cd worklenz-frontend
|
cd worklenz-frontend
|
||||||
npm run dev
|
npm run dev
|
||||||
```
|
```
|
||||||
|
|
||||||
6. Access the application at http://localhost:5000
|
6. Access the application at http://localhost:5000
|
||||||
|
|
||||||
### Option 2: Docker Setup
|
## Deployment
|
||||||
|
|
||||||
The project includes a fully configured Docker setup with:
|
For local development, follow the [Quick Start (Docker)](#-quick-start-docker---recommended) section above.
|
||||||
- Frontend React application
|
|
||||||
- Backend server
|
|
||||||
- PostgreSQL database
|
|
||||||
- MinIO for S3-compatible storage
|
|
||||||
|
|
||||||
1. Clone the repository:
|
### Remote Server Deployment
|
||||||
```bash
|
|
||||||
git clone https://github.com/Worklenz/worklenz.git
|
|
||||||
cd worklenz
|
|
||||||
```
|
|
||||||
|
|
||||||
2. Start the Docker containers (choose one option):
|
When deploying to a remote server:
|
||||||
|
|
||||||
**Using Docker Compose directly**
|
1. Set up the environment files with your server's hostname:
|
||||||
```bash
|
```bash
|
||||||
docker-compose up -d
|
# For HTTP/WS
|
||||||
```
|
./update-docker-env.sh your-server-hostname
|
||||||
|
|
||||||
3. The application will be available at:
|
# For HTTPS/WSS
|
||||||
- Frontend: http://localhost:5000
|
./update-docker-env.sh your-server-hostname true
|
||||||
- Backend API: http://localhost:3000
|
```
|
||||||
- MinIO Console: http://localhost:9001 (login with minioadmin/minioadmin)
|
|
||||||
|
|
||||||
4. To stop the services:
|
2. Pull and run the latest Docker images:
|
||||||
```bash
|
```bash
|
||||||
docker-compose down
|
docker-compose pull
|
||||||
```
|
docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Access the application through your server's hostname:
|
||||||
|
- Frontend: http://your-server-hostname:5000
|
||||||
|
- Backend API: http://your-server-hostname:3000
|
||||||
|
|
||||||
|
4. **Video Guide**: For a complete walkthrough of deploying Worklenz to a remote server, check out our [deployment video guide](https://www.youtube.com/watch?v=CAZGu2iOXQs&t=10s).
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
@@ -158,16 +230,46 @@ Worklenz requires several environment variables to be configured for proper oper
|
|||||||
|
|
||||||
Please refer to the `.env.example` files for a full list of required variables.
|
Please refer to the `.env.example` files for a full list of required variables.
|
||||||
|
|
||||||
### MinIO Integration
|
The Docker setup uses environment variables to configure the services:
|
||||||
|
|
||||||
|
- **Frontend:**
|
||||||
|
- `VITE_API_URL`: URL of the backend API (default: http://backend:3000 for container networking)
|
||||||
|
- `VITE_SOCKET_URL`: WebSocket URL for real-time communication (default: ws://backend:3000)
|
||||||
|
|
||||||
|
- **Backend:**
|
||||||
|
- Database connection parameters
|
||||||
|
- Storage configuration
|
||||||
|
- Other backend settings
|
||||||
|
|
||||||
|
For custom configuration, edit the `.env` file or the `update-docker-env.sh` script.
|
||||||
|
|
||||||
|
## MinIO Integration
|
||||||
|
|
||||||
The project uses MinIO as an S3-compatible object storage service, which provides an open-source alternative to AWS S3 for development and production.
|
The project uses MinIO as an S3-compatible object storage service, which provides an open-source alternative to AWS S3 for development and production.
|
||||||
|
|
||||||
|
### Working with MinIO
|
||||||
|
|
||||||
|
MinIO provides an S3-compatible API, so any code that works with S3 will work with MinIO by simply changing the endpoint URL. The backend has been configured to use MinIO by default, with no additional configuration required.
|
||||||
|
|
||||||
- **MinIO Console**: http://localhost:9001
|
- **MinIO Console**: http://localhost:9001
|
||||||
- Username: minioadmin
|
- Username: minioadmin
|
||||||
- Password: minioadmin
|
- Password: minioadmin
|
||||||
|
|
||||||
- **Default Bucket**: worklenz-bucket (created automatically when the containers start)
|
- **Default Bucket**: worklenz-bucket (created automatically when the containers start)
|
||||||
|
|
||||||
|
### Backend Storage Configuration
|
||||||
|
|
||||||
|
The backend is pre-configured to use MinIO with the following settings:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// S3 credentials with MinIO defaults
|
||||||
|
export const REGION = process.env.AWS_REGION || "us-east-1";
|
||||||
|
export const BUCKET = process.env.AWS_BUCKET || "worklenz-bucket";
|
||||||
|
export const S3_URL = process.env.S3_URL || "http://minio:9000/worklenz-bucket";
|
||||||
|
export const S3_ACCESS_KEY_ID = process.env.AWS_ACCESS_KEY_ID || "minioadmin";
|
||||||
|
export const S3_SECRET_ACCESS_KEY = process.env.AWS_SECRET_ACCESS_KEY || "minioadmin";
|
||||||
|
```
|
||||||
|
|
||||||
### Security Considerations
|
### Security Considerations
|
||||||
|
|
||||||
For production deployments:
|
For production deployments:
|
||||||
@@ -178,20 +280,12 @@ For production deployments:
|
|||||||
4. Enable HTTPS for all public endpoints
|
4. Enable HTTPS for all public endpoints
|
||||||
5. Review and update dependencies regularly
|
5. Review and update dependencies regularly
|
||||||
|
|
||||||
## Contributing
|
|
||||||
|
|
||||||
We welcome contributions from the community! If you'd like to contribute, please follow our [contributing guidelines](CONTRIBUTING.md).
|
|
||||||
|
|
||||||
## Security
|
## Security
|
||||||
|
|
||||||
If you believe you have found a security vulnerability in Worklenz, we encourage you to responsibly disclose this and not open a public issue. We will investigate all legitimate reports.
|
If you believe you have found a security vulnerability in Worklenz, we encourage you to responsibly disclose this and not open a public issue. We will investigate all legitimate reports.
|
||||||
|
|
||||||
Email [info@worklenz.com](mailto:info@worklenz.com) to disclose any security vulnerabilities.
|
Email [info@worklenz.com](mailto:info@worklenz.com) to disclose any security vulnerabilities.
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
This project is licensed under the [MIT License](LICENSE).
|
|
||||||
|
|
||||||
## Analytics
|
## Analytics
|
||||||
|
|
||||||
Worklenz uses Google Analytics to understand how the application is being used. This helps us improve the application and make better decisions about future development.
|
Worklenz uses Google Analytics to understand how the application is being used. This helps us improve the application and make better decisions about future development.
|
||||||
@@ -261,215 +355,13 @@ If you've previously opted in and want to opt-out:
|
|||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
### Contributing
|
## Contributing
|
||||||
|
|
||||||
We welcome contributions from the community! If you'd like to contribute, please follow
|
We welcome contributions from the community! If you'd like to contribute, please follow our [contributing guidelines](CONTRIBUTING.md).
|
||||||
our [contributing guidelines](CONTRIBUTING.md).
|
|
||||||
|
|
||||||
### License
|
## License
|
||||||
|
|
||||||
Worklenz is open source and released under the [GNU Affero General Public License Version 3 (AGPLv3)](LICENSE).
|
Worklenz is open source and released under the [GNU Affero General Public License Version 3 (AGPLv3)](LICENSE).
|
||||||
|
|
||||||
By contributing to Worklenz, you agree that your contributions will be licensed under its AGPL.
|
By contributing to Worklenz, you agree that your contributions will be licensed under its AGPL.
|
||||||
|
|
||||||
# Worklenz React
|
|
||||||
|
|
||||||
This repository contains the React version of Worklenz with a Docker setup for easy development and deployment.
|
|
||||||
|
|
||||||
## Getting Started with Docker
|
|
||||||
|
|
||||||
The project includes a fully configured Docker setup with:
|
|
||||||
- Frontend React application
|
|
||||||
- Backend server
|
|
||||||
- PostgreSQL database
|
|
||||||
- MinIO for S3-compatible storage
|
|
||||||
|
|
||||||
### Prerequisites
|
|
||||||
|
|
||||||
- Docker and Docker Compose installed on your system
|
|
||||||
- Git
|
|
||||||
|
|
||||||
### Quick Start
|
|
||||||
|
|
||||||
1. Clone the repository:
|
|
||||||
```bash
|
|
||||||
git clone https://github.com/Worklenz/worklenz.git
|
|
||||||
cd worklenz
|
|
||||||
```
|
|
||||||
|
|
||||||
2. Start the Docker containers (choose one option):
|
|
||||||
|
|
||||||
**Option 1: Using the provided scripts (easiest)**
|
|
||||||
- On Windows:
|
|
||||||
```
|
|
||||||
start.bat
|
|
||||||
```
|
|
||||||
- On Linux/macOS:
|
|
||||||
```bash
|
|
||||||
./start.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
**Option 2: Using Docker Compose directly**
|
|
||||||
```bash
|
|
||||||
docker-compose up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
3. The application will be available at:
|
|
||||||
- Frontend: http://localhost:5000
|
|
||||||
- Backend API: http://localhost:3000
|
|
||||||
- MinIO Console: http://localhost:9001 (login with minioadmin/minioadmin)
|
|
||||||
|
|
||||||
4. To stop the services (choose one option):
|
|
||||||
|
|
||||||
**Option 1: Using the provided scripts**
|
|
||||||
- On Windows:
|
|
||||||
```
|
|
||||||
stop.bat
|
|
||||||
```
|
|
||||||
- On Linux/macOS:
|
|
||||||
```bash
|
|
||||||
./stop.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
**Option 2: Using Docker Compose directly**
|
|
||||||
```bash
|
|
||||||
docker-compose down
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
## MinIO Integration
|
|
||||||
|
|
||||||
The project uses MinIO as an S3-compatible object storage service, which provides an open-source alternative to AWS S3 for development and production.
|
|
||||||
|
|
||||||
### Working with MinIO
|
|
||||||
|
|
||||||
MinIO provides an S3-compatible API, so any code that works with S3 will work with MinIO by simply changing the endpoint URL. The backend has been configured to use MinIO by default, with no additional configuration required.
|
|
||||||
|
|
||||||
- **MinIO Console**: http://localhost:9001
|
|
||||||
- Username: minioadmin
|
|
||||||
- Password: minioadmin
|
|
||||||
|
|
||||||
- **Default Bucket**: worklenz-bucket (created automatically when the containers start)
|
|
||||||
|
|
||||||
### Backend Storage Configuration
|
|
||||||
|
|
||||||
The backend is pre-configured to use MinIO with the following settings:
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
// S3 credentials with MinIO defaults
|
|
||||||
export const REGION = process.env.AWS_REGION || "us-east-1";
|
|
||||||
export const BUCKET = process.env.AWS_BUCKET || "worklenz-bucket";
|
|
||||||
export const S3_URL = process.env.S3_URL || "http://minio:9000/worklenz-bucket";
|
|
||||||
export const S3_ACCESS_KEY_ID = process.env.AWS_ACCESS_KEY_ID || "minioadmin";
|
|
||||||
export const S3_SECRET_ACCESS_KEY = process.env.AWS_SECRET_ACCESS_KEY || "minioadmin";
|
|
||||||
```
|
|
||||||
|
|
||||||
The S3 client is initialized with special MinIO configuration:
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
const s3Client = new S3Client({
|
|
||||||
region: REGION,
|
|
||||||
credentials: {
|
|
||||||
accessKeyId: S3_ACCESS_KEY_ID || "",
|
|
||||||
secretAccessKey: S3_SECRET_ACCESS_KEY || "",
|
|
||||||
},
|
|
||||||
endpoint: getEndpointFromUrl(), // Extracts endpoint from S3_URL
|
|
||||||
forcePathStyle: true, // Required for MinIO
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
### Environment Configuration
|
|
||||||
|
|
||||||
The project uses the following environment file structure:
|
|
||||||
|
|
||||||
- **Frontend**:
|
|
||||||
- `worklenz-frontend/.env.development` - Development environment variables
|
|
||||||
- `worklenz-frontend/.env.production` - Production build variables
|
|
||||||
|
|
||||||
- **Backend**:
|
|
||||||
- `worklenz-backend/.env` - Backend environment variables
|
|
||||||
|
|
||||||
### Setting Up Environment Files
|
|
||||||
|
|
||||||
The Docker environment script will create or overwrite all environment files:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# For HTTP/WS
|
|
||||||
./update-docker-env.sh your-hostname
|
|
||||||
|
|
||||||
# For HTTPS/WSS
|
|
||||||
./update-docker-env.sh your-hostname true
|
|
||||||
```
|
|
||||||
|
|
||||||
This script generates properly configured environment files for both development and production environments.
|
|
||||||
|
|
||||||
## Docker Deployment
|
|
||||||
|
|
||||||
### Local Development with Docker
|
|
||||||
|
|
||||||
1. Set up the environment files:
|
|
||||||
```bash
|
|
||||||
# For HTTP/WS
|
|
||||||
./update-docker-env.sh
|
|
||||||
|
|
||||||
# For HTTPS/WSS
|
|
||||||
./update-docker-env.sh localhost true
|
|
||||||
```
|
|
||||||
|
|
||||||
2. Run the application using Docker Compose:
|
|
||||||
```bash
|
|
||||||
docker-compose up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
3. Access the application:
|
|
||||||
- Frontend: http://localhost:5000
|
|
||||||
- Backend API: http://localhost:3000 (or https://localhost:3000 with SSL)
|
|
||||||
|
|
||||||
4. Video Guide
|
|
||||||
|
|
||||||
For a visual walkthrough of the local Docker deployment process, check out our [step-by-step video guide](https://www.youtube.com/watch?v=AfwAKxJbqLg).
|
|
||||||
|
|
||||||
### Remote Server Deployment
|
|
||||||
|
|
||||||
When deploying to a remote server:
|
|
||||||
|
|
||||||
1. Set up the environment files with your server's hostname:
|
|
||||||
```bash
|
|
||||||
# For HTTP/WS
|
|
||||||
./update-docker-env.sh your-server-hostname
|
|
||||||
|
|
||||||
# For HTTPS/WSS
|
|
||||||
./update-docker-env.sh your-server-hostname true
|
|
||||||
```
|
|
||||||
|
|
||||||
This ensures that the frontend correctly connects to the backend API.
|
|
||||||
|
|
||||||
2. Pull and run the latest Docker images:
|
|
||||||
```bash
|
|
||||||
docker-compose pull
|
|
||||||
docker-compose up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
3. Access the application through your server's hostname:
|
|
||||||
- Frontend: http://your-server-hostname:5000
|
|
||||||
- Backend API: http://your-server-hostname:3000
|
|
||||||
|
|
||||||
4. Video Guide
|
|
||||||
|
|
||||||
For a complete walkthrough of deploying Worklenz to a remote server, check out our [deployment video guide](https://www.youtube.com/watch?v=CAZGu2iOXQs&t=10s).
|
|
||||||
|
|
||||||
### Environment Configuration
|
|
||||||
|
|
||||||
The Docker setup uses environment variables to configure the services:
|
|
||||||
|
|
||||||
- Frontend:
|
|
||||||
- `VITE_API_URL`: URL of the backend API (default: http://backend:3000 for container networking)
|
|
||||||
- `VITE_SOCKET_URL`: WebSocket URL for real-time communication (default: ws://backend:3000)
|
|
||||||
|
|
||||||
- Backend:
|
|
||||||
- Database connection parameters
|
|
||||||
- Storage configuration
|
|
||||||
- Other backend settings
|
|
||||||
|
|
||||||
For custom configuration, edit the `.env` file or the `update-docker-env.sh` script.
|
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ Getting started with development is a breeze! Follow these steps and you'll be c
|
|||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
- Node.js version v16 or newer - [Node.js](https://nodejs.org/en/download/)
|
- Node.js version v20 or newer - [Node.js](https://nodejs.org/en/download/)
|
||||||
- PostgreSQL version v15 or newer - [PostgreSQL](https://www.postgresql.org/download/)
|
- PostgreSQL version v15 or newer - [PostgreSQL](https://www.postgresql.org/download/)
|
||||||
- S3-compatible storage (like MinIO) for file storage
|
- S3-compatible storage (like MinIO) for file storage
|
||||||
|
|
||||||
@@ -38,7 +38,7 @@ Getting started with development is a breeze! Follow these steps and you'll be c
|
|||||||
npm start
|
npm start
|
||||||
```
|
```
|
||||||
|
|
||||||
4. Navigate to [http://localhost:5173](http://localhost:5173)
|
4. Navigate to [http://localhost:5173](http://localhost:5173) (development server)
|
||||||
|
|
||||||
### Backend installation
|
### Backend installation
|
||||||
|
|
||||||
@@ -126,7 +126,7 @@ For an easier setup, you can use Docker and Docker Compose:
|
|||||||
```
|
```
|
||||||
|
|
||||||
3. Access the application:
|
3. Access the application:
|
||||||
- Frontend: http://localhost:5000
|
- Frontend: http://localhost:5000 (Docker production build)
|
||||||
- Backend API: http://localhost:3000
|
- Backend API: http://localhost:3000
|
||||||
- MinIO Console: http://localhost:9001 (login with minioadmin/minioadmin)
|
- MinIO Console: http://localhost:9001 (login with minioadmin/minioadmin)
|
||||||
|
|
||||||
|
|||||||
3
worklenz-backend/.gitignore
vendored
3
worklenz-backend/.gitignore
vendored
@@ -20,9 +20,6 @@ coverage
|
|||||||
# nyc test coverage
|
# nyc test coverage
|
||||||
.nyc_output
|
.nyc_output
|
||||||
|
|
||||||
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
|
|
||||||
.grunt
|
|
||||||
|
|
||||||
# Bower dependency directory (https://bower.io/)
|
# Bower dependency directory (https://bower.io/)
|
||||||
bower_components
|
bower_components
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,143 @@
|
|||||||
|
-- Fix window function error in task sort optimized functions
|
||||||
|
-- Error: window functions are not allowed in UPDATE
|
||||||
|
|
||||||
|
-- Replace the optimized sort functions to avoid CTE usage in UPDATE statements
|
||||||
|
CREATE OR REPLACE FUNCTION handle_task_list_sort_between_groups_optimized(_from_index integer, _to_index integer, _task_id uuid, _project_id uuid, _batch_size integer DEFAULT 100) RETURNS void
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS
|
||||||
|
$$
|
||||||
|
DECLARE
|
||||||
|
_offset INT := 0;
|
||||||
|
_affected_rows INT;
|
||||||
|
BEGIN
|
||||||
|
-- PERFORMANCE OPTIMIZATION: Use direct updates without CTE in UPDATE
|
||||||
|
IF (_to_index = -1)
|
||||||
|
THEN
|
||||||
|
_to_index = COALESCE((SELECT MAX(sort_order) + 1 FROM tasks WHERE project_id = _project_id), 0);
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- PERFORMANCE OPTIMIZATION: Batch updates for large datasets
|
||||||
|
IF _to_index > _from_index
|
||||||
|
THEN
|
||||||
|
LOOP
|
||||||
|
UPDATE tasks
|
||||||
|
SET sort_order = sort_order - 1
|
||||||
|
WHERE project_id = _project_id
|
||||||
|
AND sort_order > _from_index
|
||||||
|
AND sort_order < _to_index
|
||||||
|
AND sort_order > _offset
|
||||||
|
AND sort_order <= _offset + _batch_size;
|
||||||
|
|
||||||
|
GET DIAGNOSTICS _affected_rows = ROW_COUNT;
|
||||||
|
EXIT WHEN _affected_rows = 0;
|
||||||
|
_offset := _offset + _batch_size;
|
||||||
|
END LOOP;
|
||||||
|
|
||||||
|
UPDATE tasks SET sort_order = _to_index - 1 WHERE id = _task_id AND project_id = _project_id;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF _to_index < _from_index
|
||||||
|
THEN
|
||||||
|
_offset := 0;
|
||||||
|
LOOP
|
||||||
|
UPDATE tasks
|
||||||
|
SET sort_order = sort_order + 1
|
||||||
|
WHERE project_id = _project_id
|
||||||
|
AND sort_order > _to_index
|
||||||
|
AND sort_order < _from_index
|
||||||
|
AND sort_order > _offset
|
||||||
|
AND sort_order <= _offset + _batch_size;
|
||||||
|
|
||||||
|
GET DIAGNOSTICS _affected_rows = ROW_COUNT;
|
||||||
|
EXIT WHEN _affected_rows = 0;
|
||||||
|
_offset := _offset + _batch_size;
|
||||||
|
END LOOP;
|
||||||
|
|
||||||
|
UPDATE tasks SET sort_order = _to_index + 1 WHERE id = _task_id AND project_id = _project_id;
|
||||||
|
END IF;
|
||||||
|
END
|
||||||
|
$$;
|
||||||
|
|
||||||
|
-- Replace the second optimized sort function
|
||||||
|
CREATE OR REPLACE FUNCTION handle_task_list_sort_inside_group_optimized(_from_index integer, _to_index integer, _task_id uuid, _project_id uuid, _batch_size integer DEFAULT 100) RETURNS void
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS
|
||||||
|
$$
|
||||||
|
DECLARE
|
||||||
|
_offset INT := 0;
|
||||||
|
_affected_rows INT;
|
||||||
|
BEGIN
|
||||||
|
-- PERFORMANCE OPTIMIZATION: Batch updates for large datasets without CTE in UPDATE
|
||||||
|
IF _to_index > _from_index
|
||||||
|
THEN
|
||||||
|
LOOP
|
||||||
|
UPDATE tasks
|
||||||
|
SET sort_order = sort_order - 1
|
||||||
|
WHERE project_id = _project_id
|
||||||
|
AND sort_order > _from_index
|
||||||
|
AND sort_order <= _to_index
|
||||||
|
AND sort_order > _offset
|
||||||
|
AND sort_order <= _offset + _batch_size;
|
||||||
|
|
||||||
|
GET DIAGNOSTICS _affected_rows = ROW_COUNT;
|
||||||
|
EXIT WHEN _affected_rows = 0;
|
||||||
|
_offset := _offset + _batch_size;
|
||||||
|
END LOOP;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF _to_index < _from_index
|
||||||
|
THEN
|
||||||
|
_offset := 0;
|
||||||
|
LOOP
|
||||||
|
UPDATE tasks
|
||||||
|
SET sort_order = sort_order + 1
|
||||||
|
WHERE project_id = _project_id
|
||||||
|
AND sort_order >= _to_index
|
||||||
|
AND sort_order < _from_index
|
||||||
|
AND sort_order > _offset
|
||||||
|
AND sort_order <= _offset + _batch_size;
|
||||||
|
|
||||||
|
GET DIAGNOSTICS _affected_rows = ROW_COUNT;
|
||||||
|
EXIT WHEN _affected_rows = 0;
|
||||||
|
_offset := _offset + _batch_size;
|
||||||
|
END LOOP;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
UPDATE tasks SET sort_order = _to_index WHERE id = _task_id AND project_id = _project_id;
|
||||||
|
END
|
||||||
|
$$;
|
||||||
|
|
||||||
|
-- Add simple bulk update function as alternative
|
||||||
|
CREATE OR REPLACE FUNCTION update_task_sort_orders_bulk(_updates json) RETURNS void
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS
|
||||||
|
$$
|
||||||
|
DECLARE
|
||||||
|
_update_record RECORD;
|
||||||
|
BEGIN
|
||||||
|
-- Simple approach: update each task's sort_order from the provided array
|
||||||
|
FOR _update_record IN
|
||||||
|
SELECT
|
||||||
|
(item->>'task_id')::uuid as task_id,
|
||||||
|
(item->>'sort_order')::int as sort_order,
|
||||||
|
(item->>'status_id')::uuid as status_id,
|
||||||
|
(item->>'priority_id')::uuid as priority_id,
|
||||||
|
(item->>'phase_id')::uuid as phase_id
|
||||||
|
FROM json_array_elements(_updates) as item
|
||||||
|
LOOP
|
||||||
|
UPDATE tasks
|
||||||
|
SET
|
||||||
|
sort_order = _update_record.sort_order,
|
||||||
|
status_id = COALESCE(_update_record.status_id, status_id),
|
||||||
|
priority_id = COALESCE(_update_record.priority_id, priority_id)
|
||||||
|
WHERE id = _update_record.task_id;
|
||||||
|
|
||||||
|
-- Handle phase updates separately since it's in a different table
|
||||||
|
IF _update_record.phase_id IS NOT NULL THEN
|
||||||
|
INSERT INTO task_phase (task_id, phase_id)
|
||||||
|
VALUES (_update_record.task_id, _update_record.phase_id)
|
||||||
|
ON CONFLICT (task_id) DO UPDATE SET phase_id = _update_record.phase_id;
|
||||||
|
END IF;
|
||||||
|
END LOOP;
|
||||||
|
END
|
||||||
|
$$;
|
||||||
@@ -5497,8 +5497,15 @@ $$
|
|||||||
DECLARE
|
DECLARE
|
||||||
_iterator NUMERIC := 0;
|
_iterator NUMERIC := 0;
|
||||||
_status_id TEXT;
|
_status_id TEXT;
|
||||||
|
_project_id UUID;
|
||||||
|
_base_sort_order NUMERIC;
|
||||||
BEGIN
|
BEGIN
|
||||||
|
-- Get the project_id from the first status to ensure we update all statuses in the same project
|
||||||
|
SELECT project_id INTO _project_id
|
||||||
|
FROM task_statuses
|
||||||
|
WHERE id = (SELECT TRIM(BOTH '"' FROM JSON_ARRAY_ELEMENTS(_status_ids)::TEXT) LIMIT 1)::UUID;
|
||||||
|
|
||||||
|
-- Update the sort_order for statuses in the provided order
|
||||||
FOR _status_id IN SELECT * FROM JSON_ARRAY_ELEMENTS((_status_ids)::JSON)
|
FOR _status_id IN SELECT * FROM JSON_ARRAY_ELEMENTS((_status_ids)::JSON)
|
||||||
LOOP
|
LOOP
|
||||||
UPDATE task_statuses
|
UPDATE task_statuses
|
||||||
@@ -5507,6 +5514,29 @@ BEGIN
|
|||||||
_iterator := _iterator + 1;
|
_iterator := _iterator + 1;
|
||||||
END LOOP;
|
END LOOP;
|
||||||
|
|
||||||
|
-- Get the base sort order for remaining statuses (simple count approach)
|
||||||
|
SELECT COUNT(*) INTO _base_sort_order
|
||||||
|
FROM task_statuses ts2
|
||||||
|
WHERE ts2.project_id = _project_id
|
||||||
|
AND ts2.id = ANY(SELECT (TRIM(BOTH '"' FROM JSON_ARRAY_ELEMENTS(_status_ids)::TEXT))::UUID);
|
||||||
|
|
||||||
|
-- Update remaining statuses with simple sequential numbering
|
||||||
|
-- Reset iterator to start from base_sort_order
|
||||||
|
_iterator := _base_sort_order;
|
||||||
|
|
||||||
|
-- Use a cursor approach to avoid window functions
|
||||||
|
FOR _status_id IN
|
||||||
|
SELECT id::TEXT FROM task_statuses
|
||||||
|
WHERE project_id = _project_id
|
||||||
|
AND id NOT IN (SELECT (TRIM(BOTH '"' FROM JSON_ARRAY_ELEMENTS(_status_ids)::TEXT))::UUID)
|
||||||
|
ORDER BY sort_order
|
||||||
|
LOOP
|
||||||
|
UPDATE task_statuses
|
||||||
|
SET sort_order = _iterator
|
||||||
|
WHERE id = _status_id::UUID;
|
||||||
|
_iterator := _iterator + 1;
|
||||||
|
END LOOP;
|
||||||
|
|
||||||
RETURN;
|
RETURN;
|
||||||
END
|
END
|
||||||
$$;
|
$$;
|
||||||
@@ -6394,7 +6424,7 @@ DECLARE
|
|||||||
_offset INT := 0;
|
_offset INT := 0;
|
||||||
_affected_rows INT;
|
_affected_rows INT;
|
||||||
BEGIN
|
BEGIN
|
||||||
-- PERFORMANCE OPTIMIZATION: Use CTE for better query planning
|
-- PERFORMANCE OPTIMIZATION: Use direct updates without CTE in UPDATE
|
||||||
IF (_to_index = -1)
|
IF (_to_index = -1)
|
||||||
THEN
|
THEN
|
||||||
_to_index = COALESCE((SELECT MAX(sort_order) + 1 FROM tasks WHERE project_id = _project_id), 0);
|
_to_index = COALESCE((SELECT MAX(sort_order) + 1 FROM tasks WHERE project_id = _project_id), 0);
|
||||||
@@ -6404,18 +6434,15 @@ BEGIN
|
|||||||
IF _to_index > _from_index
|
IF _to_index > _from_index
|
||||||
THEN
|
THEN
|
||||||
LOOP
|
LOOP
|
||||||
WITH batch_update AS (
|
|
||||||
UPDATE tasks
|
UPDATE tasks
|
||||||
SET sort_order = sort_order - 1
|
SET sort_order = sort_order - 1
|
||||||
WHERE project_id = _project_id
|
WHERE project_id = _project_id
|
||||||
AND sort_order > _from_index
|
AND sort_order > _from_index
|
||||||
AND sort_order < _to_index
|
AND sort_order < _to_index
|
||||||
AND sort_order > _offset
|
AND sort_order > _offset
|
||||||
AND sort_order <= _offset + _batch_size
|
AND sort_order <= _offset + _batch_size;
|
||||||
RETURNING 1
|
|
||||||
)
|
|
||||||
SELECT COUNT(*) INTO _affected_rows FROM batch_update;
|
|
||||||
|
|
||||||
|
GET DIAGNOSTICS _affected_rows = ROW_COUNT;
|
||||||
EXIT WHEN _affected_rows = 0;
|
EXIT WHEN _affected_rows = 0;
|
||||||
_offset := _offset + _batch_size;
|
_offset := _offset + _batch_size;
|
||||||
END LOOP;
|
END LOOP;
|
||||||
@@ -6427,18 +6454,15 @@ BEGIN
|
|||||||
THEN
|
THEN
|
||||||
_offset := 0;
|
_offset := 0;
|
||||||
LOOP
|
LOOP
|
||||||
WITH batch_update AS (
|
|
||||||
UPDATE tasks
|
UPDATE tasks
|
||||||
SET sort_order = sort_order + 1
|
SET sort_order = sort_order + 1
|
||||||
WHERE project_id = _project_id
|
WHERE project_id = _project_id
|
||||||
AND sort_order > _to_index
|
AND sort_order > _to_index
|
||||||
AND sort_order < _from_index
|
AND sort_order < _from_index
|
||||||
AND sort_order > _offset
|
AND sort_order > _offset
|
||||||
AND sort_order <= _offset + _batch_size
|
AND sort_order <= _offset + _batch_size;
|
||||||
RETURNING 1
|
|
||||||
)
|
|
||||||
SELECT COUNT(*) INTO _affected_rows FROM batch_update;
|
|
||||||
|
|
||||||
|
GET DIAGNOSTICS _affected_rows = ROW_COUNT;
|
||||||
EXIT WHEN _affected_rows = 0;
|
EXIT WHEN _affected_rows = 0;
|
||||||
_offset := _offset + _batch_size;
|
_offset := _offset + _batch_size;
|
||||||
END LOOP;
|
END LOOP;
|
||||||
@@ -6457,22 +6481,19 @@ DECLARE
|
|||||||
_offset INT := 0;
|
_offset INT := 0;
|
||||||
_affected_rows INT;
|
_affected_rows INT;
|
||||||
BEGIN
|
BEGIN
|
||||||
-- PERFORMANCE OPTIMIZATION: Batch updates for large datasets
|
-- PERFORMANCE OPTIMIZATION: Batch updates for large datasets without CTE in UPDATE
|
||||||
IF _to_index > _from_index
|
IF _to_index > _from_index
|
||||||
THEN
|
THEN
|
||||||
LOOP
|
LOOP
|
||||||
WITH batch_update AS (
|
|
||||||
UPDATE tasks
|
UPDATE tasks
|
||||||
SET sort_order = sort_order - 1
|
SET sort_order = sort_order - 1
|
||||||
WHERE project_id = _project_id
|
WHERE project_id = _project_id
|
||||||
AND sort_order > _from_index
|
AND sort_order > _from_index
|
||||||
AND sort_order <= _to_index
|
AND sort_order <= _to_index
|
||||||
AND sort_order > _offset
|
AND sort_order > _offset
|
||||||
AND sort_order <= _offset + _batch_size
|
AND sort_order <= _offset + _batch_size;
|
||||||
RETURNING 1
|
|
||||||
)
|
|
||||||
SELECT COUNT(*) INTO _affected_rows FROM batch_update;
|
|
||||||
|
|
||||||
|
GET DIAGNOSTICS _affected_rows = ROW_COUNT;
|
||||||
EXIT WHEN _affected_rows = 0;
|
EXIT WHEN _affected_rows = 0;
|
||||||
_offset := _offset + _batch_size;
|
_offset := _offset + _batch_size;
|
||||||
END LOOP;
|
END LOOP;
|
||||||
@@ -6482,18 +6503,15 @@ BEGIN
|
|||||||
THEN
|
THEN
|
||||||
_offset := 0;
|
_offset := 0;
|
||||||
LOOP
|
LOOP
|
||||||
WITH batch_update AS (
|
|
||||||
UPDATE tasks
|
UPDATE tasks
|
||||||
SET sort_order = sort_order + 1
|
SET sort_order = sort_order + 1
|
||||||
WHERE project_id = _project_id
|
WHERE project_id = _project_id
|
||||||
AND sort_order >= _to_index
|
AND sort_order >= _to_index
|
||||||
AND sort_order < _from_index
|
AND sort_order < _from_index
|
||||||
AND sort_order > _offset
|
AND sort_order > _offset
|
||||||
AND sort_order <= _offset + _batch_size
|
AND sort_order <= _offset + _batch_size;
|
||||||
RETURNING 1
|
|
||||||
)
|
|
||||||
SELECT COUNT(*) INTO _affected_rows FROM batch_update;
|
|
||||||
|
|
||||||
|
GET DIAGNOSTICS _affected_rows = ROW_COUNT;
|
||||||
EXIT WHEN _affected_rows = 0;
|
EXIT WHEN _affected_rows = 0;
|
||||||
_offset := _offset + _batch_size;
|
_offset := _offset + _batch_size;
|
||||||
END LOOP;
|
END LOOP;
|
||||||
@@ -6502,3 +6520,38 @@ BEGIN
|
|||||||
UPDATE tasks SET sort_order = _to_index WHERE id = _task_id AND project_id = _project_id;
|
UPDATE tasks SET sort_order = _to_index WHERE id = _task_id AND project_id = _project_id;
|
||||||
END
|
END
|
||||||
$$;
|
$$;
|
||||||
|
|
||||||
|
-- Simple function to update task sort orders in bulk
|
||||||
|
CREATE OR REPLACE FUNCTION update_task_sort_orders_bulk(_updates json) RETURNS void
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS
|
||||||
|
$$
|
||||||
|
DECLARE
|
||||||
|
_update_record RECORD;
|
||||||
|
BEGIN
|
||||||
|
-- Simple approach: update each task's sort_order from the provided array
|
||||||
|
FOR _update_record IN
|
||||||
|
SELECT
|
||||||
|
(item->>'task_id')::uuid as task_id,
|
||||||
|
(item->>'sort_order')::int as sort_order,
|
||||||
|
(item->>'status_id')::uuid as status_id,
|
||||||
|
(item->>'priority_id')::uuid as priority_id,
|
||||||
|
(item->>'phase_id')::uuid as phase_id
|
||||||
|
FROM json_array_elements(_updates) as item
|
||||||
|
LOOP
|
||||||
|
UPDATE tasks
|
||||||
|
SET
|
||||||
|
sort_order = _update_record.sort_order,
|
||||||
|
status_id = COALESCE(_update_record.status_id, status_id),
|
||||||
|
priority_id = COALESCE(_update_record.priority_id, priority_id)
|
||||||
|
WHERE id = _update_record.task_id;
|
||||||
|
|
||||||
|
-- Handle phase updates separately since it's in a different table
|
||||||
|
IF _update_record.phase_id IS NOT NULL THEN
|
||||||
|
INSERT INTO task_phase (task_id, phase_id)
|
||||||
|
VALUES (_update_record.task_id, _update_record.phase_id)
|
||||||
|
ON CONFLICT (task_id) DO UPDATE SET phase_id = _update_record.phase_id;
|
||||||
|
END IF;
|
||||||
|
END LOOP;
|
||||||
|
END
|
||||||
|
$$;
|
||||||
|
|||||||
@@ -1,28 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
brotli_js: {
|
|
||||||
options: {
|
|
||||||
mode: "brotli",
|
|
||||||
brotli: {
|
|
||||||
mode: 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
expand: true,
|
|
||||||
cwd: "build/public",
|
|
||||||
src: ["**/*.js"],
|
|
||||||
dest: "build/public",
|
|
||||||
extDot: "last",
|
|
||||||
ext: ".js.br"
|
|
||||||
},
|
|
||||||
gzip_js: {
|
|
||||||
options: {
|
|
||||||
mode: "gzip"
|
|
||||||
},
|
|
||||||
files: [{
|
|
||||||
expand: true,
|
|
||||||
cwd: "build/public",
|
|
||||||
src: ["**/*.js"],
|
|
||||||
dest: "build/public",
|
|
||||||
ext: ".js.gz"
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"npm": ">=8.11.0",
|
"npm": ">=8.11.0",
|
||||||
"node": ">=16.13.0",
|
"node": ">=20.0.0",
|
||||||
"yarn": "WARNING: Please use npm package manager instead of yarn"
|
"yarn": "WARNING: Please use npm package manager instead of yarn"
|
||||||
},
|
},
|
||||||
"main": "build/bin/www",
|
"main": "build/bin/www",
|
||||||
@@ -68,7 +68,6 @@
|
|||||||
"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",
|
||||||
|
|||||||
@@ -137,6 +137,10 @@ export default class HomePageController extends WorklenzControllerBase {
|
|||||||
WHERE category_id NOT IN (SELECT id
|
WHERE category_id NOT IN (SELECT id
|
||||||
FROM sys_task_status_categories
|
FROM sys_task_status_categories
|
||||||
WHERE is_done IS FALSE))
|
WHERE is_done IS FALSE))
|
||||||
|
AND NOT EXISTS(SELECT project_id
|
||||||
|
FROM archived_projects
|
||||||
|
WHERE project_id = p.id
|
||||||
|
AND user_id = $2)
|
||||||
${groupByClosure}
|
${groupByClosure}
|
||||||
ORDER BY t.end_date ASC`;
|
ORDER BY t.end_date ASC`;
|
||||||
|
|
||||||
@@ -158,9 +162,13 @@ export default class HomePageController extends WorklenzControllerBase {
|
|||||||
WHERE category_id NOT IN (SELECT id
|
WHERE category_id NOT IN (SELECT id
|
||||||
FROM sys_task_status_categories
|
FROM sys_task_status_categories
|
||||||
WHERE is_done IS FALSE))
|
WHERE is_done IS FALSE))
|
||||||
|
AND NOT EXISTS(SELECT project_id
|
||||||
|
FROM archived_projects
|
||||||
|
WHERE project_id = p.id
|
||||||
|
AND user_id = $3)
|
||||||
${groupByClosure}`;
|
${groupByClosure}`;
|
||||||
|
|
||||||
const result = await db.query(q, [teamId, userId]);
|
const result = await db.query(q, [teamId, userId, userId]);
|
||||||
const [row] = result.rows;
|
const [row] = result.rows;
|
||||||
return row;
|
return row;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,19 +16,23 @@ export default class TaskPhasesController extends WorklenzControllerBase {
|
|||||||
if (!req.query.id)
|
if (!req.query.id)
|
||||||
return res.status(400).send(new ServerResponse(false, null, "Invalid request"));
|
return res.status(400).send(new ServerResponse(false, null, "Invalid request"));
|
||||||
|
|
||||||
|
// Use custom name if provided, otherwise use default naming pattern
|
||||||
|
const phaseName = req.body.name?.trim() ||
|
||||||
|
`Untitled Phase (${(await db.query("SELECT COUNT(*) FROM project_phases WHERE project_id = $1", [req.query.id])).rows[0].count + 1})`;
|
||||||
|
|
||||||
const q = `
|
const q = `
|
||||||
INSERT INTO project_phases (name, color_code, project_id, sort_index)
|
INSERT INTO project_phases (name, color_code, project_id, sort_index)
|
||||||
VALUES (
|
VALUES (
|
||||||
CONCAT('Untitled Phase (', (SELECT COUNT(*) FROM project_phases WHERE project_id = $2) + 1, ')'),
|
|
||||||
$1,
|
$1,
|
||||||
$2,
|
$2,
|
||||||
(SELECT COUNT(*) FROM project_phases WHERE project_id = $2) + 1)
|
$3,
|
||||||
|
(SELECT COUNT(*) FROM project_phases WHERE project_id = $3) + 1)
|
||||||
RETURNING id, name, color_code, sort_index;
|
RETURNING id, name, color_code, sort_index;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
req.body.color_code = this.DEFAULT_PHASE_COLOR;
|
req.body.color_code = this.DEFAULT_PHASE_COLOR;
|
||||||
|
|
||||||
const result = await db.query(q, [req.body.color_code, req.query.id]);
|
const result = await db.query(q, [phaseName, req.body.color_code, req.query.id]);
|
||||||
const [data] = result.rows;
|
const [data] = result.rows;
|
||||||
|
|
||||||
data.color_code = getColor(data.name) + TASK_STATUS_COLOR_ALPHA;
|
data.color_code = getColor(data.name) + TASK_STATUS_COLOR_ALPHA;
|
||||||
|
|||||||
@@ -69,13 +69,13 @@ export default class TasksControllerV2 extends TasksControllerBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static getFilterByProjectsWhereClosure(text: string) {
|
private static getFilterByProjectsWhereClosure(text: string) {
|
||||||
return text ? `t.project_id IN (${this.flatString(text)})` : "";
|
return text ? `project_id IN (${this.flatString(text)})` : "";
|
||||||
}
|
}
|
||||||
|
|
||||||
private static getFilterByAssignee(filterBy: string) {
|
private static getFilterByAssignee(filterBy: string) {
|
||||||
return filterBy === "member"
|
return filterBy === "member"
|
||||||
? `t.id IN (SELECT task_id FROM tasks_assignees WHERE team_member_id = $1)`
|
? `id IN (SELECT task_id FROM tasks_assignees WHERE team_member_id = $1)`
|
||||||
: "t.project_id = $1";
|
: "project_id = $1";
|
||||||
}
|
}
|
||||||
|
|
||||||
private static getStatusesQuery(filterBy: string) {
|
private static getStatusesQuery(filterBy: string) {
|
||||||
@@ -109,7 +109,7 @@ export default class TasksControllerV2 extends TasksControllerBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static getQuery(userId: string, options: ParsedQs) {
|
private static getQuery(userId: string, options: ParsedQs) {
|
||||||
const searchField = options.search ? "t.name" : "sort_order";
|
const searchField = options.search ? ["t.name", "CONCAT((SELECT key FROM projects WHERE id = t.project_id), '-', task_no)"] : "sort_order";
|
||||||
const { searchQuery, sortField } = TasksControllerV2.toPaginationOptions(options, searchField);
|
const { searchQuery, sortField } = TasksControllerV2.toPaginationOptions(options, searchField);
|
||||||
|
|
||||||
const isSubTasks = !!options.parent_task;
|
const isSubTasks = !!options.parent_task;
|
||||||
@@ -131,19 +131,41 @@ export default class TasksControllerV2 extends TasksControllerBase {
|
|||||||
// Returns statuses of each task as a json array if filterBy === "member"
|
// Returns statuses of each task as a json array if filterBy === "member"
|
||||||
const statusesQuery = TasksControllerV2.getStatusesQuery(options.filterBy as string);
|
const statusesQuery = TasksControllerV2.getStatusesQuery(options.filterBy as string);
|
||||||
|
|
||||||
// Custom columns data query - optimized with LEFT JOIN
|
// Custom columns data query
|
||||||
const customColumnsQuery = options.customColumns
|
const customColumnsQuery = options.customColumns
|
||||||
? `, COALESCE(cc_data.custom_column_values, '{}'::JSONB) AS custom_column_values`
|
? `, (SELECT COALESCE(
|
||||||
|
jsonb_object_agg(
|
||||||
|
custom_cols.key,
|
||||||
|
custom_cols.value
|
||||||
|
),
|
||||||
|
'{}'::JSONB
|
||||||
|
)
|
||||||
|
FROM (
|
||||||
|
SELECT
|
||||||
|
cc.key,
|
||||||
|
CASE
|
||||||
|
WHEN ccv.text_value IS NOT NULL THEN to_jsonb(ccv.text_value)
|
||||||
|
WHEN ccv.number_value IS NOT NULL THEN to_jsonb(ccv.number_value)
|
||||||
|
WHEN ccv.boolean_value IS NOT NULL THEN to_jsonb(ccv.boolean_value)
|
||||||
|
WHEN ccv.date_value IS NOT NULL THEN to_jsonb(ccv.date_value)
|
||||||
|
WHEN ccv.json_value IS NOT NULL THEN ccv.json_value
|
||||||
|
ELSE NULL::JSONB
|
||||||
|
END AS value
|
||||||
|
FROM cc_column_values ccv
|
||||||
|
JOIN cc_custom_columns cc ON ccv.column_id = cc.id
|
||||||
|
WHERE ccv.task_id = t.id
|
||||||
|
) AS custom_cols
|
||||||
|
WHERE custom_cols.value IS NOT NULL) AS custom_column_values`
|
||||||
: "";
|
: "";
|
||||||
|
|
||||||
const archivedFilter = options.archived === "true" ? "t.archived IS TRUE" : "t.archived IS FALSE";
|
const archivedFilter = options.archived === "true" ? "archived IS TRUE" : "archived IS FALSE";
|
||||||
|
|
||||||
let subTasksFilter;
|
let subTasksFilter;
|
||||||
|
|
||||||
if (options.isSubtasksInclude === "true") {
|
if (options.isSubtasksInclude === "true") {
|
||||||
subTasksFilter = "";
|
subTasksFilter = "";
|
||||||
} else {
|
} else {
|
||||||
subTasksFilter = isSubTasks ? "t.parent_task_id = $2" : "t.parent_task_id IS NULL";
|
subTasksFilter = isSubTasks ? "parent_task_id = $2" : "parent_task_id IS NULL";
|
||||||
}
|
}
|
||||||
|
|
||||||
const filters = [
|
const filters = [
|
||||||
@@ -157,100 +179,19 @@ export default class TasksControllerV2 extends TasksControllerBase {
|
|||||||
projectsFilter
|
projectsFilter
|
||||||
].filter(i => !!i).join(" AND ");
|
].filter(i => !!i).join(" AND ");
|
||||||
|
|
||||||
// PERFORMANCE OPTIMIZED QUERY - Using CTEs and JOINs instead of correlated subqueries
|
|
||||||
return `
|
return `
|
||||||
WITH task_aggregates AS (
|
SELECT id,
|
||||||
SELECT
|
name,
|
||||||
t.id,
|
CONCAT((SELECT key FROM projects WHERE id = t.project_id), '-', task_no) AS task_key,
|
||||||
COUNT(DISTINCT sub.id) AS sub_tasks_count,
|
(SELECT name FROM projects WHERE id = t.project_id) AS project_name,
|
||||||
COUNT(DISTINCT CASE WHEN sub_status.is_done THEN sub.id END) AS completed_sub_tasks,
|
t.project_id AS project_id,
|
||||||
COUNT(DISTINCT tc.id) AS comments_count,
|
|
||||||
COUNT(DISTINCT ta.id) AS attachments_count,
|
|
||||||
COUNT(DISTINCT twl.id) AS work_log_count,
|
|
||||||
COALESCE(SUM(twl.time_spent), 0) AS total_minutes_spent,
|
|
||||||
MAX(CASE WHEN ts.id IS NOT NULL THEN 1 ELSE 0 END) AS has_subscribers,
|
|
||||||
MAX(CASE WHEN td.id IS NOT NULL THEN 1 ELSE 0 END) AS has_dependencies
|
|
||||||
FROM tasks t
|
|
||||||
LEFT JOIN tasks sub ON t.id = sub.parent_task_id AND sub.archived = FALSE
|
|
||||||
LEFT JOIN task_statuses sub_ts ON sub.status_id = sub_ts.id
|
|
||||||
LEFT JOIN sys_task_status_categories sub_status ON sub_ts.category_id = sub_status.id
|
|
||||||
LEFT JOIN task_comments tc ON t.id = tc.task_id
|
|
||||||
LEFT JOIN task_attachments ta ON t.id = ta.task_id
|
|
||||||
LEFT JOIN task_work_log twl ON t.id = twl.task_id
|
|
||||||
LEFT JOIN task_subscribers ts ON t.id = ts.task_id
|
|
||||||
LEFT JOIN task_dependencies td ON t.id = td.task_id
|
|
||||||
WHERE t.project_id = $1 AND t.archived = FALSE
|
|
||||||
GROUP BY t.id
|
|
||||||
),
|
|
||||||
task_assignees AS (
|
|
||||||
SELECT
|
|
||||||
ta.task_id,
|
|
||||||
JSON_AGG(JSON_BUILD_OBJECT(
|
|
||||||
'team_member_id', ta.team_member_id,
|
|
||||||
'project_member_id', ta.project_member_id,
|
|
||||||
'name', COALESCE(tmiv.name, ''),
|
|
||||||
'avatar_url', COALESCE(tmiv.avatar_url, ''),
|
|
||||||
'email', COALESCE(tmiv.email, ''),
|
|
||||||
'user_id', tmiv.user_id,
|
|
||||||
'socket_id', COALESCE(u.socket_id, ''),
|
|
||||||
'team_id', tmiv.team_id,
|
|
||||||
'email_notifications_enabled', COALESCE(ns.email_notifications_enabled, false)
|
|
||||||
)) AS assignees,
|
|
||||||
STRING_AGG(COALESCE(tmiv.name, ''), ', ') AS assignee_names,
|
|
||||||
STRING_AGG(COALESCE(tmiv.name, ''), ', ') AS names
|
|
||||||
FROM tasks_assignees ta
|
|
||||||
LEFT JOIN team_member_info_view tmiv ON ta.team_member_id = tmiv.team_member_id
|
|
||||||
LEFT JOIN users u ON tmiv.user_id = u.id
|
|
||||||
LEFT JOIN notification_settings ns ON ns.user_id = u.id AND ns.team_id = tmiv.team_id
|
|
||||||
GROUP BY ta.task_id
|
|
||||||
),
|
|
||||||
task_labels AS (
|
|
||||||
SELECT
|
|
||||||
tl.task_id,
|
|
||||||
JSON_AGG(JSON_BUILD_OBJECT(
|
|
||||||
'id', tl.label_id,
|
|
||||||
'label_id', tl.label_id,
|
|
||||||
'name', team_l.name,
|
|
||||||
'color_code', team_l.color_code
|
|
||||||
)) AS labels,
|
|
||||||
JSON_AGG(JSON_BUILD_OBJECT(
|
|
||||||
'id', tl.label_id,
|
|
||||||
'label_id', tl.label_id,
|
|
||||||
'name', team_l.name,
|
|
||||||
'color_code', team_l.color_code
|
|
||||||
)) AS all_labels
|
|
||||||
FROM task_labels tl
|
|
||||||
JOIN team_labels team_l ON tl.label_id = team_l.id
|
|
||||||
GROUP BY tl.task_id
|
|
||||||
)
|
|
||||||
${options.customColumns ? `,
|
|
||||||
custom_columns_data AS (
|
|
||||||
SELECT
|
|
||||||
ccv.task_id,
|
|
||||||
JSONB_OBJECT_AGG(
|
|
||||||
cc.key,
|
|
||||||
CASE
|
|
||||||
WHEN ccv.text_value IS NOT NULL THEN to_jsonb(ccv.text_value)
|
|
||||||
WHEN ccv.number_value IS NOT NULL THEN to_jsonb(ccv.number_value)
|
|
||||||
WHEN ccv.boolean_value IS NOT NULL THEN to_jsonb(ccv.boolean_value)
|
|
||||||
WHEN ccv.date_value IS NOT NULL THEN to_jsonb(ccv.date_value)
|
|
||||||
WHEN ccv.json_value IS NOT NULL THEN ccv.json_value
|
|
||||||
ELSE NULL::JSONB
|
|
||||||
END
|
|
||||||
) AS custom_column_values
|
|
||||||
FROM cc_column_values ccv
|
|
||||||
JOIN cc_custom_columns cc ON ccv.column_id = cc.id
|
|
||||||
GROUP BY ccv.task_id
|
|
||||||
)` : ""}
|
|
||||||
SELECT
|
|
||||||
t.id,
|
|
||||||
t.name,
|
|
||||||
CONCAT(p.key, '-', t.task_no) AS task_key,
|
|
||||||
p.name AS project_name,
|
|
||||||
t.project_id,
|
|
||||||
t.parent_task_id,
|
t.parent_task_id,
|
||||||
t.parent_task_id IS NOT NULL AS is_sub_task,
|
t.parent_task_id IS NOT NULL AS is_sub_task,
|
||||||
parent_task.name AS parent_task_name,
|
(SELECT name FROM tasks WHERE id = t.parent_task_id) AS parent_task_name,
|
||||||
|
(SELECT COUNT(*)
|
||||||
|
FROM tasks
|
||||||
|
WHERE parent_task_id = t.id)::INT AS sub_tasks_count,
|
||||||
|
|
||||||
t.status_id AS status,
|
t.status_id AS status,
|
||||||
t.archived,
|
t.archived,
|
||||||
t.description,
|
t.description,
|
||||||
@@ -258,70 +199,74 @@ export default class TasksControllerV2 extends TasksControllerBase {
|
|||||||
t.progress_value,
|
t.progress_value,
|
||||||
t.manual_progress,
|
t.manual_progress,
|
||||||
t.weight,
|
t.weight,
|
||||||
p.use_manual_progress AS project_use_manual_progress,
|
(SELECT use_manual_progress FROM projects WHERE id = t.project_id) AS project_use_manual_progress,
|
||||||
p.use_weighted_progress AS project_use_weighted_progress,
|
(SELECT use_weighted_progress FROM projects WHERE id = t.project_id) AS project_use_weighted_progress,
|
||||||
p.use_time_progress AS project_use_time_progress,
|
(SELECT use_time_progress FROM projects WHERE id = t.project_id) AS project_use_time_progress,
|
||||||
-- Use stored progress value instead of expensive function call
|
(SELECT get_task_complete_ratio(t.id)->>'ratio') AS complete_ratio,
|
||||||
COALESCE(t.progress_value, 0) AS complete_ratio,
|
|
||||||
-- Phase information via JOINs
|
(SELECT phase_id FROM task_phase WHERE task_id = t.id) AS phase_id,
|
||||||
tp.phase_id,
|
(SELECT name
|
||||||
pp.name AS phase_name,
|
FROM project_phases
|
||||||
pp.color_code AS phase_color_code,
|
WHERE id = (SELECT phase_id FROM task_phase WHERE task_id = t.id)) AS phase_name,
|
||||||
-- Status information via JOINs
|
(SELECT color_code
|
||||||
stsc.color_code AS status_color,
|
FROM project_phases
|
||||||
stsc.color_code_dark AS status_color_dark,
|
WHERE id = (SELECT phase_id FROM task_phase WHERE task_id = t.id)) AS phase_color_code,
|
||||||
JSON_BUILD_OBJECT(
|
|
||||||
'is_done', stsc.is_done,
|
(EXISTS(SELECT 1 FROM task_subscribers WHERE task_id = t.id)) AS has_subscribers,
|
||||||
'is_doing', stsc.is_doing,
|
(EXISTS(SELECT 1 FROM task_dependencies td WHERE td.task_id = t.id)) AS has_dependencies,
|
||||||
'is_todo', stsc.is_todo
|
(SELECT start_time
|
||||||
) AS status_category,
|
FROM task_timers
|
||||||
-- Aggregated counts
|
WHERE task_id = t.id
|
||||||
COALESCE(agg.sub_tasks_count, 0) AS sub_tasks_count,
|
AND user_id = '${userId}') AS timer_start_time,
|
||||||
COALESCE(agg.completed_sub_tasks, 0) AS completed_sub_tasks,
|
|
||||||
COALESCE(agg.comments_count, 0) AS comments_count,
|
(SELECT color_code
|
||||||
COALESCE(agg.attachments_count, 0) AS attachments_count,
|
FROM sys_task_status_categories
|
||||||
COALESCE(agg.total_minutes_spent, 0) AS total_minutes_spent,
|
WHERE id = (SELECT category_id FROM task_statuses WHERE id = t.status_id)) AS status_color,
|
||||||
CASE WHEN agg.has_subscribers > 0 THEN true ELSE false END AS has_subscribers,
|
|
||||||
CASE WHEN agg.has_dependencies > 0 THEN true ELSE false END AS has_dependencies,
|
(SELECT color_code_dark
|
||||||
-- Task completion status
|
FROM sys_task_status_categories
|
||||||
CASE WHEN stsc.is_done THEN 1 ELSE 0 END AS parent_task_completed,
|
WHERE id = (SELECT category_id FROM task_statuses WHERE id = t.status_id)) AS status_color_dark,
|
||||||
-- Assignees and labels via JOINs
|
|
||||||
COALESCE(assignees.assignees, '[]'::JSON) AS assignees,
|
(SELECT COALESCE(ROW_TO_JSON(r), '{}'::JSON)
|
||||||
COALESCE(assignees.assignee_names, '') AS assignee_names,
|
FROM (SELECT is_done, is_doing, is_todo
|
||||||
COALESCE(assignees.names, '') AS names,
|
FROM sys_task_status_categories
|
||||||
COALESCE(labels.labels, '[]'::JSON) AS labels,
|
WHERE id = (SELECT category_id FROM task_statuses WHERE id = t.status_id)) r) AS status_category,
|
||||||
COALESCE(labels.all_labels, '[]'::JSON) AS all_labels,
|
|
||||||
-- Other fields
|
(SELECT COUNT(*) FROM task_comments WHERE task_id = t.id) AS comments_count,
|
||||||
stsc.is_done AS is_complete,
|
(SELECT COUNT(*) FROM task_attachments WHERE task_id = t.id) AS attachments_count,
|
||||||
reporter.name AS reporter,
|
(CASE
|
||||||
t.priority_id AS priority,
|
WHEN EXISTS(SELECT 1
|
||||||
tp_priority.value AS priority_value,
|
FROM tasks_with_status_view
|
||||||
t.total_minutes,
|
WHERE tasks_with_status_view.task_id = t.id
|
||||||
t.created_at,
|
AND is_done IS TRUE) THEN 1
|
||||||
t.updated_at,
|
ELSE 0 END) AS parent_task_completed,
|
||||||
t.completed_at,
|
(SELECT get_task_assignees(t.id)) AS assignees,
|
||||||
t.start_date,
|
(SELECT COUNT(*)
|
||||||
t.billable,
|
FROM tasks_with_status_view tt
|
||||||
t.schedule_id,
|
WHERE tt.parent_task_id = t.id
|
||||||
t.END_DATE,
|
AND tt.is_done IS TRUE)::INT
|
||||||
-- Timer information
|
AS completed_sub_tasks,
|
||||||
tt.start_time AS timer_start_time
|
|
||||||
${customColumnsQuery}
|
(SELECT COALESCE(JSON_AGG(r), '[]'::JSON)
|
||||||
${statusesQuery}
|
FROM (SELECT task_labels.label_id AS id,
|
||||||
|
(SELECT name FROM team_labels WHERE id = task_labels.label_id),
|
||||||
|
(SELECT color_code FROM team_labels WHERE id = task_labels.label_id)
|
||||||
|
FROM task_labels
|
||||||
|
WHERE task_id = t.id) r) AS labels,
|
||||||
|
(SELECT is_completed(status_id, project_id)) AS is_complete,
|
||||||
|
(SELECT name FROM users WHERE id = t.reporter_id) AS reporter,
|
||||||
|
(SELECT id FROM task_priorities WHERE id = t.priority_id) AS priority,
|
||||||
|
(SELECT value FROM task_priorities WHERE id = t.priority_id) AS priority_value,
|
||||||
|
total_minutes,
|
||||||
|
(SELECT SUM(time_spent) FROM task_work_log WHERE task_id = t.id) AS total_minutes_spent,
|
||||||
|
created_at,
|
||||||
|
updated_at,
|
||||||
|
completed_at,
|
||||||
|
start_date,
|
||||||
|
billable,
|
||||||
|
schedule_id,
|
||||||
|
END_DATE ${customColumnsQuery} ${statusesQuery}
|
||||||
FROM tasks t
|
FROM tasks t
|
||||||
JOIN projects p ON t.project_id = p.id
|
|
||||||
JOIN task_statuses ts ON t.status_id = ts.id
|
|
||||||
JOIN sys_task_status_categories stsc ON ts.category_id = stsc.id
|
|
||||||
LEFT JOIN tasks parent_task ON t.parent_task_id = parent_task.id
|
|
||||||
LEFT JOIN task_phase tp ON t.id = tp.task_id
|
|
||||||
LEFT JOIN project_phases pp ON tp.phase_id = pp.id
|
|
||||||
LEFT JOIN task_priorities tp_priority ON t.priority_id = tp_priority.id
|
|
||||||
LEFT JOIN users reporter ON t.reporter_id = reporter.id
|
|
||||||
LEFT JOIN task_timers tt ON t.id = tt.task_id AND tt.user_id = $${isSubTasks ? "3" : "2"}
|
|
||||||
LEFT JOIN task_aggregates agg ON t.id = agg.id
|
|
||||||
LEFT JOIN task_assignees assignees ON t.id = assignees.task_id
|
|
||||||
LEFT JOIN task_labels labels ON t.id = labels.task_id
|
|
||||||
${options.customColumns ? "LEFT JOIN custom_columns_data cc_data ON t.id = cc_data.task_id" : ""}
|
|
||||||
WHERE ${filters} ${searchQuery}
|
WHERE ${filters} ${searchQuery}
|
||||||
ORDER BY ${sortFields}
|
ORDER BY ${sortFields}
|
||||||
`;
|
`;
|
||||||
@@ -402,7 +347,7 @@ export default class TasksControllerV2 extends TasksControllerBase {
|
|||||||
req.query.customColumns = "true";
|
req.query.customColumns = "true";
|
||||||
|
|
||||||
const q = TasksControllerV2.getQuery(req.user?.id as string, req.query);
|
const q = TasksControllerV2.getQuery(req.user?.id as string, req.query);
|
||||||
const params = isSubTasks ? [req.params.id || null, req.query.parent_task, req.user?.id] : [req.params.id || null, req.user?.id];
|
const params = isSubTasks ? [req.params.id || null, req.query.parent_task] : [req.params.id || null];
|
||||||
|
|
||||||
const result = await db.query(q, params);
|
const result = await db.query(q, params);
|
||||||
const tasks = [...result.rows];
|
const tasks = [...result.rows];
|
||||||
@@ -510,7 +455,7 @@ export default class TasksControllerV2 extends TasksControllerBase {
|
|||||||
req.query.customColumns = "true";
|
req.query.customColumns = "true";
|
||||||
|
|
||||||
const q = TasksControllerV2.getQuery(req.user?.id as string, req.query);
|
const q = TasksControllerV2.getQuery(req.user?.id as string, req.query);
|
||||||
const params = isSubTasks ? [req.params.id || null, req.query.parent_task, req.user?.id] : [req.params.id || null, req.user?.id];
|
const params = isSubTasks ? [req.params.id || null, req.query.parent_task] : [req.params.id || null];
|
||||||
const result = await db.query(q, params);
|
const result = await db.query(q, params);
|
||||||
|
|
||||||
let data: any[] = [];
|
let data: any[] = [];
|
||||||
@@ -1041,62 +986,60 @@ export default class TasksControllerV2 extends TasksControllerBase {
|
|||||||
@HandleExceptions()
|
@HandleExceptions()
|
||||||
public static async getTasksV3(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
public static async getTasksV3(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||||
const startTime = performance.now();
|
const startTime = performance.now();
|
||||||
console.log(`[PERFORMANCE] getTasksV3 method called for project ${req.params.id}`);
|
const isSubTasks = !!req.query.parent_task;
|
||||||
|
const groupBy = (req.query.group || GroupBy.STATUS) as string;
|
||||||
|
const archived = req.query.archived === "true";
|
||||||
|
|
||||||
// PERFORMANCE OPTIMIZATION: Skip expensive progress calculation by default
|
// PERFORMANCE OPTIMIZATION: Skip expensive progress calculation by default
|
||||||
// Progress values are already calculated and stored in the database
|
// Progress values are already calculated and stored in the database
|
||||||
// Only refresh if explicitly requested via refresh_progress=true query parameter
|
// Only refresh if explicitly requested via refresh_progress=true query parameter
|
||||||
if (req.query.refresh_progress === "true" && req.params.id) {
|
// This dramatically improves initial load performance (from ~2-5s to ~200-500ms)
|
||||||
console.log(`[PERFORMANCE] Starting progress refresh for project ${req.params.id} (getTasksV3)`);
|
const shouldRefreshProgress = req.query.refresh_progress === "true";
|
||||||
|
|
||||||
|
if (shouldRefreshProgress && req.params.id) {
|
||||||
const progressStartTime = performance.now();
|
const progressStartTime = performance.now();
|
||||||
await this.refreshProjectTaskProgressValues(req.params.id);
|
await this.refreshProjectTaskProgressValues(req.params.id);
|
||||||
const progressEndTime = performance.now();
|
const progressEndTime = performance.now();
|
||||||
console.log(`[PERFORMANCE] Progress refresh completed in ${(progressEndTime - progressStartTime).toFixed(2)}ms`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const isSubTasks = !!req.query.parent_task;
|
const queryStartTime = performance.now();
|
||||||
const groupBy = (req.query.group || GroupBy.STATUS) as string;
|
|
||||||
|
|
||||||
// Add customColumns flag to query params (same as getList)
|
|
||||||
req.query.customColumns = "true";
|
|
||||||
|
|
||||||
// Use the exact same database query as getList method
|
|
||||||
const q = TasksControllerV2.getQuery(req.user?.id as string, req.query);
|
const q = TasksControllerV2.getQuery(req.user?.id as string, req.query);
|
||||||
const params = isSubTasks ? [req.params.id || null, req.query.parent_task, req.user?.id] : [req.params.id || null, req.user?.id];
|
const params = isSubTasks ? [req.params.id || null, req.query.parent_task] : [req.params.id || null];
|
||||||
|
|
||||||
const result = await db.query(q, params);
|
const result = await db.query(q, params);
|
||||||
const tasks = [...result.rows];
|
const tasks = [...result.rows];
|
||||||
|
const queryEndTime = performance.now();
|
||||||
|
|
||||||
// Use the same groups query as getList method
|
// Get groups metadata dynamically from database
|
||||||
|
const groupsStartTime = performance.now();
|
||||||
const groups = await this.getGroups(groupBy, req.params.id);
|
const groups = await this.getGroups(groupBy, req.params.id);
|
||||||
const map = groups.reduce((g: { [x: string]: ITaskGroup }, group) => {
|
const groupsEndTime = performance.now();
|
||||||
if (group.id)
|
|
||||||
g[group.id] = new TaskListGroup(group);
|
|
||||||
return g;
|
|
||||||
}, {});
|
|
||||||
|
|
||||||
// Use the same updateMapByGroup method as getList
|
// Create priority value to name mapping
|
||||||
await this.updateMapByGroup(tasks, groupBy, map);
|
|
||||||
|
|
||||||
// Calculate progress for groups (same as getList)
|
|
||||||
const updatedGroups = Object.keys(map).map(key => {
|
|
||||||
const group = map[key];
|
|
||||||
TasksControllerV2.updateTaskProgresses(group);
|
|
||||||
return {
|
|
||||||
id: key,
|
|
||||||
...group
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
// Transform to V3 response format while maintaining the same data processing
|
|
||||||
const priorityMap: Record<string, string> = {
|
const priorityMap: Record<string, string> = {
|
||||||
"0": "low",
|
"0": "low",
|
||||||
"1": "medium",
|
"1": "medium",
|
||||||
"2": "high"
|
"2": "high"
|
||||||
};
|
};
|
||||||
|
|
||||||
// Transform all tasks to V3 format
|
// Create status category mapping based on actual status names from database
|
||||||
|
const statusCategoryMap: Record<string, string> = {};
|
||||||
|
for (const group of groups) {
|
||||||
|
if (groupBy === GroupBy.STATUS && group.id) {
|
||||||
|
// Use the actual status name from database, convert to lowercase for consistency
|
||||||
|
statusCategoryMap[group.id] = group.name.toLowerCase().replace(/\s+/g, "_");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// Transform tasks with all necessary data preprocessing
|
||||||
|
const transformStartTime = performance.now();
|
||||||
const transformedTasks = tasks.map((task, index) => {
|
const transformedTasks = tasks.map((task, index) => {
|
||||||
|
// Update task with calculated values (lightweight version)
|
||||||
|
TasksControllerV2.updateTaskViewModel(task);
|
||||||
|
task.index = index;
|
||||||
|
|
||||||
// Convert time values
|
// Convert time values
|
||||||
const convertTimeValue = (value: any): number => {
|
const convertTimeValue = (value: any): number => {
|
||||||
if (typeof value === "number") return value;
|
if (typeof value === "number") return value;
|
||||||
@@ -1119,12 +1062,15 @@ export default class TasksControllerV2 extends TasksControllerBase {
|
|||||||
task_key: task.task_key || "",
|
task_key: task.task_key || "",
|
||||||
title: task.name || "",
|
title: task.name || "",
|
||||||
description: task.description || "",
|
description: task.description || "",
|
||||||
status: task.status || "todo",
|
// Use dynamic status mapping from database
|
||||||
|
status: statusCategoryMap[task.status] || task.status,
|
||||||
|
// Pre-processed priority using mapping
|
||||||
priority: priorityMap[task.priority_value?.toString()] || "medium",
|
priority: priorityMap[task.priority_value?.toString()] || "medium",
|
||||||
|
// Use actual phase name from database
|
||||||
phase: task.phase_name || "Development",
|
phase: task.phase_name || "Development",
|
||||||
progress: typeof task.complete_ratio === "number" ? task.complete_ratio : 0,
|
progress: typeof task.complete_ratio === "number" ? task.complete_ratio : 0,
|
||||||
assignees: task.assignees?.map((a: any) => a.team_member_id) || [],
|
assignees: task.assignees?.map((a: any) => a.team_member_id) || [],
|
||||||
assignee_names: task.assignees || [],
|
assignee_names: task.assignee_names || task.names || [],
|
||||||
labels: task.labels?.map((l: any) => ({
|
labels: task.labels?.map((l: any) => ({
|
||||||
id: l.id || l.label_id,
|
id: l.id || l.label_id,
|
||||||
name: l.name,
|
name: l.name,
|
||||||
@@ -1132,11 +1078,6 @@ export default class TasksControllerV2 extends TasksControllerBase {
|
|||||||
end: l.end,
|
end: l.end,
|
||||||
names: l.names
|
names: l.names
|
||||||
})) || [],
|
})) || [],
|
||||||
all_labels: task.all_labels?.map((l: any) => ({
|
|
||||||
id: l.id || l.label_id,
|
|
||||||
name: l.name,
|
|
||||||
color_code: l.color_code || "#1890ff"
|
|
||||||
})) || [],
|
|
||||||
dueDate: task.end_date || task.END_DATE,
|
dueDate: task.end_date || task.END_DATE,
|
||||||
startDate: task.start_date,
|
startDate: task.start_date,
|
||||||
timeTracking: {
|
timeTracking: {
|
||||||
@@ -1144,7 +1085,7 @@ export default class TasksControllerV2 extends TasksControllerBase {
|
|||||||
logged: convertTimeValue(task.time_spent),
|
logged: convertTimeValue(task.time_spent),
|
||||||
},
|
},
|
||||||
customFields: {},
|
customFields: {},
|
||||||
custom_column_values: task.custom_column_values || {},
|
custom_column_values: task.custom_column_values || {}, // Include custom column values
|
||||||
createdAt: task.created_at || new Date().toISOString(),
|
createdAt: task.created_at || new Date().toISOString(),
|
||||||
updatedAt: task.updated_at || new Date().toISOString(),
|
updatedAt: task.updated_at || new Date().toISOString(),
|
||||||
order: typeof task.sort_order === "number" ? task.sort_order : 0,
|
order: typeof task.sort_order === "number" ? task.sort_order : 0,
|
||||||
@@ -1161,55 +1102,186 @@ export default class TasksControllerV2 extends TasksControllerBase {
|
|||||||
attachments_count: task.attachments_count || 0,
|
attachments_count: task.attachments_count || 0,
|
||||||
has_dependencies: !!task.has_dependencies,
|
has_dependencies: !!task.has_dependencies,
|
||||||
schedule_id: task.schedule_id || null,
|
schedule_id: task.schedule_id || null,
|
||||||
|
reporter: task.reporter || null,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
const transformEndTime = performance.now();
|
||||||
|
|
||||||
// Transform groups to V3 format while preserving the getList logic
|
// Create groups based on dynamic data from database
|
||||||
const responseGroups = updatedGroups.map(group => {
|
const groupingStartTime = performance.now();
|
||||||
// Create status category mapping for consistent group naming
|
const groupedResponse: Record<string, any> = {};
|
||||||
let groupValue = group.name;
|
|
||||||
if (groupBy === GroupBy.STATUS) {
|
|
||||||
groupValue = group.name.toLowerCase().replace(/\s+/g, "_");
|
|
||||||
} else if (groupBy === GroupBy.PRIORITY) {
|
|
||||||
groupValue = group.name.toLowerCase();
|
|
||||||
} else if (groupBy === GroupBy.PHASE) {
|
|
||||||
groupValue = group.name.toLowerCase().replace(/\s+/g, "_");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Transform tasks in this group to V3 format
|
// Initialize groups from database data
|
||||||
const groupTasks = group.tasks.map(task => {
|
groups.forEach(group => {
|
||||||
const foundTask = transformedTasks.find(t => t.id === task.id);
|
const groupKey = groupBy === GroupBy.STATUS
|
||||||
return foundTask || task;
|
? group.name.toLowerCase().replace(/\s+/g, "_")
|
||||||
});
|
: groupBy === GroupBy.PRIORITY
|
||||||
|
? priorityMap[(group as any).value?.toString()] || group.name.toLowerCase()
|
||||||
|
: group.name.toLowerCase().replace(/\s+/g, "_");
|
||||||
|
|
||||||
return {
|
groupedResponse[groupKey] = {
|
||||||
id: group.id,
|
id: group.id,
|
||||||
title: group.name,
|
title: group.name,
|
||||||
groupType: groupBy,
|
groupType: groupBy,
|
||||||
groupValue,
|
groupValue: groupKey,
|
||||||
collapsed: false,
|
collapsed: false,
|
||||||
tasks: groupTasks,
|
tasks: [],
|
||||||
taskIds: groupTasks.map((task: any) => task.id),
|
taskIds: [],
|
||||||
color: group.color_code || this.getDefaultGroupColor(groupBy, groupValue),
|
color: group.color_code || this.getDefaultGroupColor(groupBy, groupKey),
|
||||||
// Include additional metadata from database
|
// Include additional metadata from database
|
||||||
category_id: group.category_id,
|
category_id: group.category_id,
|
||||||
start_date: group.start_date,
|
start_date: group.start_date,
|
||||||
end_date: group.end_date,
|
end_date: group.end_date,
|
||||||
sort_index: (group as any).sort_index,
|
sort_index: (group as any).sort_index,
|
||||||
// Include progress information from getList logic
|
|
||||||
todo_progress: group.todo_progress,
|
|
||||||
doing_progress: group.doing_progress,
|
|
||||||
done_progress: group.done_progress,
|
|
||||||
};
|
};
|
||||||
}).filter(group => group.tasks.length > 0 || req.query.include_empty === "true");
|
});
|
||||||
|
|
||||||
|
// Distribute tasks into groups
|
||||||
|
const unmappedTasks: any[] = [];
|
||||||
|
|
||||||
|
transformedTasks.forEach(task => {
|
||||||
|
let groupKey: string;
|
||||||
|
let taskAssigned = false;
|
||||||
|
|
||||||
|
if (groupBy === GroupBy.STATUS) {
|
||||||
|
groupKey = task.status;
|
||||||
|
if (groupedResponse[groupKey]) {
|
||||||
|
groupedResponse[groupKey].tasks.push(task);
|
||||||
|
groupedResponse[groupKey].taskIds.push(task.id);
|
||||||
|
taskAssigned = true;
|
||||||
|
}
|
||||||
|
} else if (groupBy === GroupBy.PRIORITY) {
|
||||||
|
groupKey = task.priority;
|
||||||
|
if (groupedResponse[groupKey]) {
|
||||||
|
groupedResponse[groupKey].tasks.push(task);
|
||||||
|
groupedResponse[groupKey].taskIds.push(task.id);
|
||||||
|
taskAssigned = true;
|
||||||
|
}
|
||||||
|
} else if (groupBy === GroupBy.PHASE) {
|
||||||
|
// For phase grouping, check if task has a valid phase
|
||||||
|
if (task.phase && task.phase.trim() !== "") {
|
||||||
|
groupKey = task.phase.toLowerCase().replace(/\s+/g, "_");
|
||||||
|
if (groupedResponse[groupKey]) {
|
||||||
|
groupedResponse[groupKey].tasks.push(task);
|
||||||
|
groupedResponse[groupKey].taskIds.push(task.id);
|
||||||
|
taskAssigned = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If task doesn't have a valid phase, add to unmapped
|
||||||
|
if (!taskAssigned) {
|
||||||
|
unmappedTasks.push(task);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Calculate progress stats for priority and phase grouping
|
||||||
|
if (groupBy === GroupBy.PRIORITY || groupBy === GroupBy.PHASE) {
|
||||||
|
Object.values(groupedResponse).forEach((group: any) => {
|
||||||
|
if (group.tasks && group.tasks.length > 0) {
|
||||||
|
const todoCount = group.tasks.filter((task: any) => {
|
||||||
|
// For tasks, we need to check their original status category
|
||||||
|
const originalTask = tasks.find(t => t.id === task.id);
|
||||||
|
return originalTask?.status_category?.is_todo;
|
||||||
|
}).length;
|
||||||
|
|
||||||
|
const doingCount = group.tasks.filter((task: any) => {
|
||||||
|
const originalTask = tasks.find(t => t.id === task.id);
|
||||||
|
return originalTask?.status_category?.is_doing;
|
||||||
|
}).length;
|
||||||
|
|
||||||
|
const doneCount = group.tasks.filter((task: any) => {
|
||||||
|
const originalTask = tasks.find(t => t.id === task.id);
|
||||||
|
return originalTask?.status_category?.is_done;
|
||||||
|
}).length;
|
||||||
|
|
||||||
|
const total = group.tasks.length;
|
||||||
|
|
||||||
|
// Calculate progress percentages
|
||||||
|
group.todo_progress = total > 0 ? +((todoCount / total) * 100).toFixed(0) : 0;
|
||||||
|
group.doing_progress = total > 0 ? +((doingCount / total) * 100).toFixed(0) : 0;
|
||||||
|
group.done_progress = total > 0 ? +((doneCount / total) * 100).toFixed(0) : 0;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create unmapped group if there are tasks without proper phase assignment
|
||||||
|
if (unmappedTasks.length > 0 && groupBy === GroupBy.PHASE) {
|
||||||
|
const unmappedGroup = {
|
||||||
|
id: UNMAPPED,
|
||||||
|
title: UNMAPPED,
|
||||||
|
groupType: groupBy,
|
||||||
|
groupValue: UNMAPPED.toLowerCase(),
|
||||||
|
collapsed: false,
|
||||||
|
tasks: unmappedTasks,
|
||||||
|
taskIds: unmappedTasks.map(task => task.id),
|
||||||
|
color: "#fbc84c69", // Orange color with transparency
|
||||||
|
category_id: null,
|
||||||
|
start_date: null,
|
||||||
|
end_date: null,
|
||||||
|
sort_index: 999, // Put unmapped group at the end
|
||||||
|
todo_progress: 0,
|
||||||
|
doing_progress: 0,
|
||||||
|
done_progress: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Calculate progress stats for unmapped group
|
||||||
|
if (unmappedTasks.length > 0) {
|
||||||
|
const todoCount = unmappedTasks.filter((task: any) => {
|
||||||
|
const originalTask = tasks.find(t => t.id === task.id);
|
||||||
|
return originalTask?.status_category?.is_todo;
|
||||||
|
}).length;
|
||||||
|
|
||||||
|
const doingCount = unmappedTasks.filter((task: any) => {
|
||||||
|
const originalTask = tasks.find(t => t.id === task.id);
|
||||||
|
return originalTask?.status_category?.is_doing;
|
||||||
|
}).length;
|
||||||
|
|
||||||
|
const doneCount = unmappedTasks.filter((task: any) => {
|
||||||
|
const originalTask = tasks.find(t => t.id === task.id);
|
||||||
|
return originalTask?.status_category?.is_done;
|
||||||
|
}).length;
|
||||||
|
|
||||||
|
const total = unmappedTasks.length;
|
||||||
|
|
||||||
|
unmappedGroup.todo_progress = total > 0 ? +((todoCount / total) * 100).toFixed(0) : 0;
|
||||||
|
unmappedGroup.doing_progress = total > 0 ? +((doingCount / total) * 100).toFixed(0) : 0;
|
||||||
|
unmappedGroup.done_progress = total > 0 ? +((doneCount / total) * 100).toFixed(0) : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
groupedResponse[UNMAPPED.toLowerCase()] = unmappedGroup;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort tasks within each group by order
|
||||||
|
Object.values(groupedResponse).forEach((group: any) => {
|
||||||
|
group.tasks.sort((a: any, b: any) => a.order - b.order);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Convert to array format expected by frontend, maintaining database order
|
||||||
|
const responseGroups = groups
|
||||||
|
.map(group => {
|
||||||
|
const groupKey = groupBy === GroupBy.STATUS
|
||||||
|
? group.name.toLowerCase().replace(/\s+/g, "_")
|
||||||
|
: groupBy === GroupBy.PRIORITY
|
||||||
|
? priorityMap[(group as any).value?.toString()] || group.name.toLowerCase()
|
||||||
|
: group.name.toLowerCase().replace(/\s+/g, "_");
|
||||||
|
|
||||||
|
return groupedResponse[groupKey];
|
||||||
|
})
|
||||||
|
.filter(group => group && (group.tasks.length > 0 || req.query.include_empty === "true"));
|
||||||
|
|
||||||
|
// Add unmapped group to the end if it exists
|
||||||
|
if (groupedResponse[UNMAPPED.toLowerCase()]) {
|
||||||
|
responseGroups.push(groupedResponse[UNMAPPED.toLowerCase()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const groupingEndTime = performance.now();
|
||||||
|
|
||||||
const endTime = performance.now();
|
const endTime = performance.now();
|
||||||
const totalTime = endTime - startTime;
|
const totalTime = endTime - startTime;
|
||||||
console.log(`[PERFORMANCE] getTasksV3 method completed in ${totalTime.toFixed(2)}ms for project ${req.params.id} with ${transformedTasks.length} tasks`);
|
|
||||||
|
|
||||||
// Log warning if this method is taking too long
|
// Log warning if request is taking too long
|
||||||
if (totalTime > 1000) {
|
if (totalTime > 1000) {
|
||||||
console.warn(`[PERFORMANCE WARNING] getTasksV3 method taking ${totalTime.toFixed(2)}ms - Consider optimizing the query or data processing!`);
|
console.warn(`[PERFORMANCE WARNING] Slow request detected: ${totalTime.toFixed(2)}ms for project ${req.params.id} with ${transformedTasks.length} tasks`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.status(200).send(new ServerResponse(true, {
|
return res.status(200).send(new ServerResponse(true, {
|
||||||
@@ -1220,315 +1292,6 @@ export default class TasksControllerV2 extends TasksControllerBase {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* NEW OPTIMIZED METHOD: Split complex query into focused segments for better performance
|
|
||||||
*/
|
|
||||||
@HandleExceptions()
|
|
||||||
public static async getTasksV4Optimized(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
|
||||||
const startTime = performance.now();
|
|
||||||
console.log(`[PERFORMANCE] getTasksV4Optimized method called for project ${req.params.id}`);
|
|
||||||
|
|
||||||
// Skip progress refresh by default for better performance
|
|
||||||
if (req.query.refresh_progress === "true" && req.params.id) {
|
|
||||||
const progressStartTime = performance.now();
|
|
||||||
await this.refreshProjectTaskProgressValues(req.params.id);
|
|
||||||
const progressEndTime = performance.now();
|
|
||||||
console.log(`[PERFORMANCE] Progress refresh completed in ${(progressEndTime - progressStartTime).toFixed(2)}ms`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const isSubTasks = !!req.query.parent_task;
|
|
||||||
const groupBy = (req.query.group || GroupBy.STATUS) as string;
|
|
||||||
const projectId = req.params.id;
|
|
||||||
const userId = req.user?.id;
|
|
||||||
|
|
||||||
// STEP 1: Get basic task data with optimized query
|
|
||||||
const baseTasksQuery = `
|
|
||||||
SELECT
|
|
||||||
t.id,
|
|
||||||
t.name,
|
|
||||||
CONCAT(p.key, '-', t.task_no) AS task_key,
|
|
||||||
p.name AS project_name,
|
|
||||||
t.project_id,
|
|
||||||
t.parent_task_id,
|
|
||||||
t.parent_task_id IS NOT NULL AS is_sub_task,
|
|
||||||
t.status_id AS status,
|
|
||||||
t.priority_id AS priority,
|
|
||||||
t.description,
|
|
||||||
t.sort_order,
|
|
||||||
t.progress_value AS complete_ratio,
|
|
||||||
t.manual_progress,
|
|
||||||
t.weight,
|
|
||||||
t.start_date,
|
|
||||||
t.end_date,
|
|
||||||
t.created_at,
|
|
||||||
t.updated_at,
|
|
||||||
t.completed_at,
|
|
||||||
t.billable,
|
|
||||||
t.schedule_id,
|
|
||||||
t.total_minutes,
|
|
||||||
-- Status information via JOINs
|
|
||||||
stsc.color_code AS status_color,
|
|
||||||
stsc.color_code_dark AS status_color_dark,
|
|
||||||
stsc.is_done,
|
|
||||||
stsc.is_doing,
|
|
||||||
stsc.is_todo,
|
|
||||||
-- Priority information
|
|
||||||
tp_priority.value AS priority_value,
|
|
||||||
-- Phase information
|
|
||||||
tp.phase_id,
|
|
||||||
pp.name AS phase_name,
|
|
||||||
pp.color_code AS phase_color_code,
|
|
||||||
-- Reporter information
|
|
||||||
reporter.name AS reporter,
|
|
||||||
-- Timer information
|
|
||||||
tt.start_time AS timer_start_time
|
|
||||||
FROM tasks t
|
|
||||||
JOIN projects p ON t.project_id = p.id
|
|
||||||
JOIN task_statuses ts ON t.status_id = ts.id
|
|
||||||
JOIN sys_task_status_categories stsc ON ts.category_id = stsc.id
|
|
||||||
LEFT JOIN task_phase tp ON t.id = tp.task_id
|
|
||||||
LEFT JOIN project_phases pp ON tp.phase_id = pp.id
|
|
||||||
LEFT JOIN task_priorities tp_priority ON t.priority_id = tp_priority.id
|
|
||||||
LEFT JOIN users reporter ON t.reporter_id = reporter.id
|
|
||||||
LEFT JOIN task_timers tt ON t.id = tt.task_id AND tt.user_id = $2
|
|
||||||
WHERE t.project_id = $1
|
|
||||||
AND t.archived = FALSE
|
|
||||||
${isSubTasks ? "AND t.parent_task_id = $3" : "AND t.parent_task_id IS NULL"}
|
|
||||||
ORDER BY t.sort_order
|
|
||||||
`;
|
|
||||||
|
|
||||||
const baseParams = isSubTasks ? [projectId, userId, req.query.parent_task] : [projectId, userId];
|
|
||||||
const baseResult = await db.query(baseTasksQuery, baseParams);
|
|
||||||
const baseTasks = baseResult.rows;
|
|
||||||
|
|
||||||
if (baseTasks.length === 0) {
|
|
||||||
return res.status(200).send(new ServerResponse(true, {
|
|
||||||
groups: [],
|
|
||||||
allTasks: [],
|
|
||||||
grouping: groupBy,
|
|
||||||
totalTasks: 0
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
const taskIds = baseTasks.map(t => t.id);
|
|
||||||
|
|
||||||
// STEP 2: Get aggregated data in parallel
|
|
||||||
const [assigneesResult, labelsResult, aggregatesResult] = await Promise.all([
|
|
||||||
// Get assignees
|
|
||||||
db.query(`
|
|
||||||
SELECT
|
|
||||||
ta.task_id,
|
|
||||||
JSON_AGG(JSON_BUILD_OBJECT(
|
|
||||||
'team_member_id', ta.team_member_id,
|
|
||||||
'project_member_id', ta.project_member_id,
|
|
||||||
'name', COALESCE(tm.name, ''),
|
|
||||||
'avatar_url', COALESCE(u.avatar_url, ''),
|
|
||||||
'email', COALESCE(u.email, ei.email, ''),
|
|
||||||
'user_id', tm.user_id,
|
|
||||||
'socket_id', COALESCE(u.socket_id, ''),
|
|
||||||
'team_id', tm.team_id
|
|
||||||
)) AS assignees
|
|
||||||
FROM tasks_assignees ta
|
|
||||||
LEFT JOIN team_members tm ON ta.team_member_id = tm.id
|
|
||||||
LEFT JOIN users u ON tm.user_id = u.id
|
|
||||||
LEFT JOIN email_invitations ei ON ta.team_member_id = ei.team_member_id
|
|
||||||
WHERE ta.task_id = ANY($1)
|
|
||||||
GROUP BY ta.task_id
|
|
||||||
`, [taskIds]),
|
|
||||||
|
|
||||||
// Get labels
|
|
||||||
db.query(`
|
|
||||||
SELECT
|
|
||||||
tl.task_id,
|
|
||||||
JSON_AGG(JSON_BUILD_OBJECT(
|
|
||||||
'id', tl.label_id,
|
|
||||||
'label_id', tl.label_id,
|
|
||||||
'name', team_l.name,
|
|
||||||
'color_code', team_l.color_code
|
|
||||||
)) AS labels
|
|
||||||
FROM task_labels tl
|
|
||||||
JOIN team_labels team_l ON tl.label_id = team_l.id
|
|
||||||
WHERE tl.task_id = ANY($1)
|
|
||||||
GROUP BY tl.task_id
|
|
||||||
`, [taskIds]),
|
|
||||||
|
|
||||||
// Get aggregated counts
|
|
||||||
db.query(`
|
|
||||||
SELECT
|
|
||||||
t.id,
|
|
||||||
COUNT(DISTINCT sub.id) AS sub_tasks_count,
|
|
||||||
COUNT(DISTINCT CASE WHEN sub_status.is_done THEN sub.id END) AS completed_sub_tasks,
|
|
||||||
COUNT(DISTINCT tc.id) AS comments_count,
|
|
||||||
COUNT(DISTINCT ta.id) AS attachments_count,
|
|
||||||
COALESCE(SUM(twl.time_spent), 0) AS total_minutes_spent,
|
|
||||||
CASE WHEN COUNT(ts.id) > 0 THEN true ELSE false END AS has_subscribers,
|
|
||||||
CASE WHEN COUNT(td.id) > 0 THEN true ELSE false END AS has_dependencies
|
|
||||||
FROM unnest($1::uuid[]) AS t(id)
|
|
||||||
LEFT JOIN tasks sub ON t.id = sub.parent_task_id AND sub.archived = FALSE
|
|
||||||
LEFT JOIN task_statuses sub_ts ON sub.status_id = sub_ts.id
|
|
||||||
LEFT JOIN sys_task_status_categories sub_status ON sub_ts.category_id = sub_status.id
|
|
||||||
LEFT JOIN task_comments tc ON t.id = tc.task_id
|
|
||||||
LEFT JOIN task_attachments ta ON t.id = ta.task_id
|
|
||||||
LEFT JOIN task_work_log twl ON t.id = twl.task_id
|
|
||||||
LEFT JOIN task_subscribers ts ON t.id = ts.task_id
|
|
||||||
LEFT JOIN task_dependencies td ON t.id = td.task_id
|
|
||||||
GROUP BY t.id
|
|
||||||
`, [taskIds])
|
|
||||||
]);
|
|
||||||
|
|
||||||
// STEP 3: Create lookup maps for efficient data merging
|
|
||||||
const assigneesMap = new Map();
|
|
||||||
assigneesResult.rows.forEach(row => assigneesMap.set(row.task_id, row.assignees || []));
|
|
||||||
|
|
||||||
const labelsMap = new Map();
|
|
||||||
labelsResult.rows.forEach(row => labelsMap.set(row.task_id, row.labels || []));
|
|
||||||
|
|
||||||
const aggregatesMap = new Map();
|
|
||||||
aggregatesResult.rows.forEach(row => aggregatesMap.set(row.id, row));
|
|
||||||
|
|
||||||
// STEP 4: Merge data efficiently
|
|
||||||
const enrichedTasks = baseTasks.map(task => {
|
|
||||||
const aggregates = aggregatesMap.get(task.id) || {};
|
|
||||||
const assignees = assigneesMap.get(task.id) || [];
|
|
||||||
const labels = labelsMap.get(task.id) || [];
|
|
||||||
|
|
||||||
return {
|
|
||||||
...task,
|
|
||||||
assignees,
|
|
||||||
assignee_names: assignees.map((a: any) => a.name).join(", "),
|
|
||||||
names: assignees.map((a: any) => a.name).join(", "),
|
|
||||||
labels,
|
|
||||||
all_labels: labels,
|
|
||||||
sub_tasks_count: parseInt(aggregates.sub_tasks_count || 0),
|
|
||||||
completed_sub_tasks: parseInt(aggregates.completed_sub_tasks || 0),
|
|
||||||
comments_count: parseInt(aggregates.comments_count || 0),
|
|
||||||
attachments_count: parseInt(aggregates.attachments_count || 0),
|
|
||||||
total_minutes_spent: parseFloat(aggregates.total_minutes_spent || 0),
|
|
||||||
has_subscribers: aggregates.has_subscribers || false,
|
|
||||||
has_dependencies: aggregates.has_dependencies || false,
|
|
||||||
status_category: {
|
|
||||||
is_done: task.is_done,
|
|
||||||
is_doing: task.is_doing,
|
|
||||||
is_todo: task.is_todo
|
|
||||||
}
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
// STEP 5: Group tasks (same logic as existing method)
|
|
||||||
const groups = await this.getGroups(groupBy, req.params.id);
|
|
||||||
const map = groups.reduce((g: { [x: string]: ITaskGroup }, group) => {
|
|
||||||
if (group.id)
|
|
||||||
g[group.id] = new TaskListGroup(group);
|
|
||||||
return g;
|
|
||||||
}, {});
|
|
||||||
|
|
||||||
await this.updateMapByGroup(enrichedTasks, groupBy, map);
|
|
||||||
|
|
||||||
const updatedGroups = Object.keys(map).map(key => {
|
|
||||||
const group = map[key];
|
|
||||||
TasksControllerV2.updateTaskProgresses(group);
|
|
||||||
return {
|
|
||||||
id: key,
|
|
||||||
...group
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
// STEP 6: Transform to V3 format (same as existing method)
|
|
||||||
const priorityMap: Record<string, string> = {
|
|
||||||
"0": "low",
|
|
||||||
"1": "medium",
|
|
||||||
"2": "high"
|
|
||||||
};
|
|
||||||
|
|
||||||
const transformedTasks = enrichedTasks.map(task => ({
|
|
||||||
id: task.id,
|
|
||||||
task_key: task.task_key || "",
|
|
||||||
title: task.name || "",
|
|
||||||
description: task.description || "",
|
|
||||||
status: task.status || "todo",
|
|
||||||
priority: priorityMap[task.priority_value?.toString()] || "medium",
|
|
||||||
phase: task.phase_name || "Development",
|
|
||||||
progress: typeof task.complete_ratio === "number" ? task.complete_ratio : 0,
|
|
||||||
assignees: task.assignees?.map((a: any) => a.team_member_id) || [],
|
|
||||||
assignee_names: task.assignees || [],
|
|
||||||
labels: task.labels?.map((l: any) => ({
|
|
||||||
id: l.id || l.label_id,
|
|
||||||
name: l.name,
|
|
||||||
color: l.color_code || "#1890ff"
|
|
||||||
})) || [],
|
|
||||||
dueDate: task.end_date,
|
|
||||||
startDate: task.start_date,
|
|
||||||
timeTracking: {
|
|
||||||
estimated: task.total_minutes || 0,
|
|
||||||
logged: task.total_minutes_spent || 0,
|
|
||||||
},
|
|
||||||
customFields: {},
|
|
||||||
createdAt: task.created_at || new Date().toISOString(),
|
|
||||||
updatedAt: task.updated_at || new Date().toISOString(),
|
|
||||||
order: typeof task.sort_order === "number" ? task.sort_order : 0,
|
|
||||||
originalStatusId: task.status,
|
|
||||||
originalPriorityId: task.priority,
|
|
||||||
statusColor: task.status_color,
|
|
||||||
priorityColor: task.priority_color,
|
|
||||||
sub_tasks_count: task.sub_tasks_count || 0,
|
|
||||||
comments_count: task.comments_count || 0,
|
|
||||||
has_subscribers: !!task.has_subscribers,
|
|
||||||
attachments_count: task.attachments_count || 0,
|
|
||||||
has_dependencies: !!task.has_dependencies,
|
|
||||||
schedule_id: task.schedule_id || null,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const responseGroups = updatedGroups.map(group => {
|
|
||||||
let groupValue = group.name;
|
|
||||||
if (groupBy === GroupBy.STATUS) {
|
|
||||||
groupValue = group.name.toLowerCase().replace(/\s+/g, "_");
|
|
||||||
} else if (groupBy === GroupBy.PRIORITY) {
|
|
||||||
groupValue = group.name.toLowerCase();
|
|
||||||
} else if (groupBy === GroupBy.PHASE) {
|
|
||||||
groupValue = group.name.toLowerCase().replace(/\s+/g, "_");
|
|
||||||
}
|
|
||||||
|
|
||||||
const groupTasks = group.tasks.map(task => {
|
|
||||||
const foundTask = transformedTasks.find(t => t.id === task.id);
|
|
||||||
return foundTask || task;
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: group.id,
|
|
||||||
title: group.name,
|
|
||||||
groupType: groupBy,
|
|
||||||
groupValue,
|
|
||||||
collapsed: false,
|
|
||||||
tasks: groupTasks,
|
|
||||||
taskIds: groupTasks.map((task: any) => task.id),
|
|
||||||
color: group.color_code || this.getDefaultGroupColor(groupBy, groupValue),
|
|
||||||
category_id: group.category_id,
|
|
||||||
start_date: group.start_date,
|
|
||||||
end_date: group.end_date,
|
|
||||||
sort_index: (group as any).sort_index,
|
|
||||||
todo_progress: group.todo_progress,
|
|
||||||
doing_progress: group.doing_progress,
|
|
||||||
done_progress: group.done_progress,
|
|
||||||
};
|
|
||||||
}).filter(group => group.tasks.length > 0 || req.query.include_empty === "true");
|
|
||||||
|
|
||||||
const endTime = performance.now();
|
|
||||||
const totalTime = endTime - startTime;
|
|
||||||
console.log(`[PERFORMANCE] getTasksV4Optimized method completed in ${totalTime.toFixed(2)}ms for project ${req.params.id} with ${transformedTasks.length} tasks - Improvement: ${2136 - totalTime > 0 ? "+" : ""}${(2136 - totalTime).toFixed(2)}ms`);
|
|
||||||
|
|
||||||
return res.status(200).send(new ServerResponse(true, {
|
|
||||||
groups: responseGroups,
|
|
||||||
allTasks: transformedTasks,
|
|
||||||
grouping: groupBy,
|
|
||||||
totalTasks: transformedTasks.length,
|
|
||||||
performanceMetrics: {
|
|
||||||
executionTime: Math.round(totalTime),
|
|
||||||
tasksCount: transformedTasks.length,
|
|
||||||
optimizationGain: Math.round(2136 - totalTime)
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static getDefaultGroupColor(groupBy: string, groupValue: string): string {
|
private static getDefaultGroupColor(groupBy: string, groupValue: string): string {
|
||||||
const colorMaps: Record<string, Record<string, string>> = {
|
const colorMaps: Record<string, Record<string, string>> = {
|
||||||
[GroupBy.STATUS]: {
|
[GroupBy.STATUS]: {
|
||||||
@@ -1624,6 +1387,4 @@ export default class TasksControllerV2 extends TasksControllerBase {
|
|||||||
return res.status(500).send(new ServerResponse(false, null, "Failed to get task progress status"));
|
return res.status(500).send(new ServerResponse(false, null, "Failed to get task progress status"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,30 +34,25 @@ export default abstract class WorklenzControllerBase {
|
|||||||
const offset = queryParams.search ? 0 : (index - 1) * size;
|
const offset = queryParams.search ? 0 : (index - 1) * size;
|
||||||
const paging = queryParams.paging || "true";
|
const paging = queryParams.paging || "true";
|
||||||
|
|
||||||
// let s = "";
|
|
||||||
// if (typeof searchField === "string") {
|
|
||||||
// s = `${searchField} || ' ' || id::TEXT`;
|
|
||||||
// } else if (Array.isArray(searchField)) {
|
|
||||||
// s = searchField.join(" || ' ' || ");
|
|
||||||
// }
|
|
||||||
|
|
||||||
// const search = (queryParams.search as string || "").trim();
|
|
||||||
// const searchQuery = search ? `AND TO_TSVECTOR(${s}) @@ TO_TSQUERY('${toTsQuery(search)}')` : "";
|
|
||||||
|
|
||||||
const search = (queryParams.search as string || "").trim();
|
const search = (queryParams.search as string || "").trim();
|
||||||
|
|
||||||
let s = "";
|
|
||||||
if (typeof searchField === "string") {
|
|
||||||
s = ` ${searchField} ILIKE '%${search}%'`;
|
|
||||||
} else if (Array.isArray(searchField)) {
|
|
||||||
s = searchField.map(index => ` ${index} ILIKE '%${search}%'`).join(" OR ");
|
|
||||||
}
|
|
||||||
|
|
||||||
let searchQuery = "";
|
let searchQuery = "";
|
||||||
|
|
||||||
if (search) {
|
if (search) {
|
||||||
|
// Properly escape single quotes to prevent SQL syntax errors
|
||||||
|
const escapedSearch = search.replace(/'/g, "''");
|
||||||
|
|
||||||
|
let s = "";
|
||||||
|
if (typeof searchField === "string") {
|
||||||
|
s = ` ${searchField} ILIKE '%${escapedSearch}%'`;
|
||||||
|
} else if (Array.isArray(searchField)) {
|
||||||
|
s = searchField.map(field => ` ${field} ILIKE '%${escapedSearch}%'`).join(" OR ");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (s) {
|
||||||
searchQuery = isMemberFilter ? ` (${s}) AND ` : ` AND (${s}) `;
|
searchQuery = isMemberFilter ? ` (${s}) AND ` : ` AND (${s}) `;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Sort
|
// Sort
|
||||||
const sortField = /null|undefined/.test(queryParams.field as string) ? searchField : queryParams.field;
|
const sortField = /null|undefined/.test(queryParams.field as string) ? searchField : queryParams.field;
|
||||||
|
|||||||
4
worklenz-backend/src/public/locales/alb/404-page.json
Normal file
4
worklenz-backend/src/public/locales/alb/404-page.json
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"doesNotExistText": "Na vjen keq, faqja që kërkoni nuk ekziston.",
|
||||||
|
"backHomeButton": "Kthehu në Faqen Kryesore"
|
||||||
|
}
|
||||||
31
worklenz-backend/src/public/locales/alb/account-setup.json
Normal file
31
worklenz-backend/src/public/locales/alb/account-setup.json
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
{
|
||||||
|
"continue": "Vazhdo",
|
||||||
|
|
||||||
|
"setupYourAccount": "Konfiguro Llogarinë Tënde në Worklenz.",
|
||||||
|
"organizationStepTitle": "Emërtoni Organizatën Tuaj",
|
||||||
|
"organizationStepLabel": "Zgjidhni një emër për llogarinë tuaj në Worklenz.",
|
||||||
|
|
||||||
|
"projectStepTitle": "Krijoni projektin tuaj të parë",
|
||||||
|
"projectStepLabel": "Në cilin projekt po punoni aktualisht?",
|
||||||
|
"projectStepPlaceholder": "p.sh. Plani i Marketingut",
|
||||||
|
|
||||||
|
"tasksStepTitle": "Krijoni detyrat tuaja të para",
|
||||||
|
"tasksStepLabel": "Shkruani disa detyra që do të kryeni në",
|
||||||
|
"tasksStepAddAnother": "Shto një tjetër",
|
||||||
|
|
||||||
|
"emailPlaceholder": "Adresa email",
|
||||||
|
"invalidEmail": "Ju lutemi vendosni një adresë email të vlefshme",
|
||||||
|
"or": "ose",
|
||||||
|
"templateButton": "Importo nga shablloni",
|
||||||
|
"goBack": "Kthehu Mbrapa",
|
||||||
|
"cancel": "Anulo",
|
||||||
|
"create": "Krijo",
|
||||||
|
"templateDrawerTitle": "Zgjidh nga shabllonet",
|
||||||
|
"step3InputLabel": "Fto me email",
|
||||||
|
"addAnother": "Shto një tjetër",
|
||||||
|
"skipForNow": "Kalo tani për tani",
|
||||||
|
"formTitle": "Krijoni detyrën tuaj të parë.",
|
||||||
|
"step3Title": "Fto ekipin tënd të punojë me",
|
||||||
|
"maxMembers": " (Mund të ftoni deri në 5 anëtarë)",
|
||||||
|
"maxTasks": " (Mund të krijoni deri në 5 detyra)"
|
||||||
|
}
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
{
|
||||||
|
"title": "Faturimet",
|
||||||
|
"currentBill": "Fatura Aktuale",
|
||||||
|
"configuration": "Konfigurimi",
|
||||||
|
"currentPlanDetails": "Detajet e Planit Aktual",
|
||||||
|
"upgradePlan": "Përmirëso Planin",
|
||||||
|
"cardBodyText01": "Provë falas",
|
||||||
|
"cardBodyText02": "(Plani juaj i provës skadon në 1 muaj 19 ditë)",
|
||||||
|
"redeemCode": "Kodi i Zbritjes",
|
||||||
|
"accountStorage": "Depozita e Llogarisë",
|
||||||
|
"used": "Përdorur:",
|
||||||
|
"remaining": "E mbetur:",
|
||||||
|
"charges": "Tarifat",
|
||||||
|
"tooltip": "Tarifat për ciklin aktual të faturimit",
|
||||||
|
"description": "Përshkrimi",
|
||||||
|
"billingPeriod": "Periudha e Faturimit",
|
||||||
|
"billStatus": "Statusi i Faturës",
|
||||||
|
"perUserValue": "Vlera për Përdorues",
|
||||||
|
"users": "Përdoruesit",
|
||||||
|
|
||||||
|
"amount": "Shuma",
|
||||||
|
"invoices": "Faturat",
|
||||||
|
"transactionId": "ID e Transaksionit",
|
||||||
|
"transactionDate": "Data e Transaksionit",
|
||||||
|
"paymentMethod": "Metoda e Pagesës",
|
||||||
|
"status": "Statusi",
|
||||||
|
"ltdUsers": "Mund të shtoni deri në {{ltd_users}} përdorues.",
|
||||||
|
|
||||||
|
"totalSeats": "Vende totale",
|
||||||
|
"availableSeats": "Vende të disponueshme",
|
||||||
|
"addMoreSeats": "Shto më shumë vende",
|
||||||
|
|
||||||
|
"drawerTitle": "Kodi i Zbritjes",
|
||||||
|
"label": "Kodi i Zbritjes",
|
||||||
|
"drawerPlaceholder": "Vendosni kodin tuaj të zbritjes",
|
||||||
|
"redeemSubmit": "Paraqit",
|
||||||
|
|
||||||
|
"modalTitle": "Zgjidhni planin më të mirë për ekipin tuaj",
|
||||||
|
"seatLabel": "Numri i vendeve",
|
||||||
|
"freePlan": "Plan Falas",
|
||||||
|
"startup": "Startup",
|
||||||
|
"business": "Biznes",
|
||||||
|
"tag": "Më i Popullarizuar",
|
||||||
|
"enterprise": "Ndërmarrje",
|
||||||
|
|
||||||
|
"freeSubtitle": "falas përgjithmonë",
|
||||||
|
"freeUsers": "Më e mira për përdorim personal",
|
||||||
|
"freeText01": "100MB depozitë",
|
||||||
|
"freeText02": "3 projekte",
|
||||||
|
"freeText03": "5 anëtarë të ekipit",
|
||||||
|
|
||||||
|
"startupSubtitle": "ÇMIM I RASTËSISHËM / muaj",
|
||||||
|
"startupUsers": "Deri në 15 përdorues",
|
||||||
|
"startupText01": "25GB depozitë",
|
||||||
|
"startupText02": "Projekte të pakufizuara aktive",
|
||||||
|
"startupText03": "Orar",
|
||||||
|
"startupText04": "Raportim",
|
||||||
|
"startupText05": "Abonohu në projekte",
|
||||||
|
|
||||||
|
"businessSubtitle": "përdorues / muaj",
|
||||||
|
"businessUsers": "16 - 200 përdorues",
|
||||||
|
|
||||||
|
"enterpriseUsers": "200 - 500+ përdorues",
|
||||||
|
|
||||||
|
"footerTitle": "Ju lutemi na jepni një numër kontakti që mund të përdorim për t'ju kontaktuar.",
|
||||||
|
"footerLabel": "Numri i Kontaktit",
|
||||||
|
"footerButton": "Na kontaktoni",
|
||||||
|
|
||||||
|
"redeemCodePlaceHolder": "Vendosni kodin tuaj të zbritjes",
|
||||||
|
"submit": "Paraqit",
|
||||||
|
|
||||||
|
"trialPlan": "Provë Falas",
|
||||||
|
"trialExpireDate": "E vlefshme deri më {{trial_expire_date}}",
|
||||||
|
"trialExpired": "Provat tuaja falas skaduan {{trial_expire_string}}",
|
||||||
|
"trialInProgress": "Provat tuaja falas skadojnë {{trial_expire_string}}",
|
||||||
|
|
||||||
|
"required": "Kjo fushë është e detyrueshme",
|
||||||
|
"invalidCode": "Kod i pavlefshëm",
|
||||||
|
|
||||||
|
"selectPlan": "Zgjidhni planin më të mirë për ekipin tuaj",
|
||||||
|
"changeSubscriptionPlan": "Ndryshoni planin tuaj të abonimit",
|
||||||
|
"noOfSeats": "Numri i vendeve",
|
||||||
|
"annualPlan": "Pro - Vjetor",
|
||||||
|
"monthlyPlan": "Pro - Mujor",
|
||||||
|
"freeForever": "Falas Përgjithmonë",
|
||||||
|
"bestForPersonalUse": "Më e mira për përdorim personal",
|
||||||
|
"storage": "Depozitë",
|
||||||
|
"projects": "Projekte",
|
||||||
|
"teamMembers": "Anëtarët e Ekipit",
|
||||||
|
"unlimitedTeamMembers": "Anëtarë të pakufizuar të ekipit",
|
||||||
|
"unlimitedActiveProjects": "Projekte të pakufizuara aktive",
|
||||||
|
"schedule": "Orar",
|
||||||
|
"reporting": "Raportim",
|
||||||
|
"subscribeToProjects": "Abonohu në projekte",
|
||||||
|
"billedAnnually": "Faturuar çdo vit",
|
||||||
|
"billedMonthly": "Faturuar çdo muaj",
|
||||||
|
|
||||||
|
"pausePlan": "Pauzë Planin",
|
||||||
|
"resumePlan": "Rifillo Planin",
|
||||||
|
"changePlan": "Ndrysho Planin",
|
||||||
|
"cancelPlan": "Anulo Planin",
|
||||||
|
|
||||||
|
"perMonthPerUser": "për përdorues/muaj",
|
||||||
|
"viewInvoice": "Shiko Faturën",
|
||||||
|
"switchToFreePlan": "Kalo në Planin Falas",
|
||||||
|
|
||||||
|
"expirestoday": "sot",
|
||||||
|
"expirestomorrow": "nesër",
|
||||||
|
"expiredDaysAgo": "{{days}} ditë më parë",
|
||||||
|
|
||||||
|
"continueWith": "Vazhdo me {{plan}}",
|
||||||
|
"changeToPlan": "Ndrysho në {{plan}}"
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"overview": "Përmbledhje",
|
||||||
|
"name": "Emri i Organizatës",
|
||||||
|
"owner": "Pronari i Organizatës",
|
||||||
|
"admins": "Administruesit e Organizatës",
|
||||||
|
"contactNumber": "Shto Numrin e Kontaktit",
|
||||||
|
"edit": "Redakto"
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"membersCount": "Numri i Anëtarëve",
|
||||||
|
"createdAt": "Krijuar më",
|
||||||
|
"projectName": "Emri i Projektit",
|
||||||
|
"teamName": "Emri i Ekipit",
|
||||||
|
"refreshProjects": "Rifresko Projektet",
|
||||||
|
"searchPlaceholder": "Kërkoni sipas emrit të projektit",
|
||||||
|
"deleteProject": "Jeni i sigurt që dëshironi të fshini këtë projekt?",
|
||||||
|
"confirm": "Konfirmo",
|
||||||
|
"cancel": "Anulo",
|
||||||
|
"delete": "Fshi Projektin"
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"overview": "Përmbledhje",
|
||||||
|
"users": "Përdoruesit",
|
||||||
|
"teams": "Ekipet",
|
||||||
|
"billing": "Faturimi",
|
||||||
|
"projects": "Projektet",
|
||||||
|
"adminCenter": "Qendra Administrative"
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"title": "Ekipet",
|
||||||
|
"subtitle": "ekipet",
|
||||||
|
"tooltip": "Rifresko ekipet",
|
||||||
|
"placeholder": "Kërko sipas emrit",
|
||||||
|
"addTeam": "Shto Ekip",
|
||||||
|
"team": "Ekipi",
|
||||||
|
"membersCount": "Numri i Anëtarëve",
|
||||||
|
"members": "Anëtarët",
|
||||||
|
"drawerTitle": "Krijo Ekip të Ri",
|
||||||
|
"label": "Emri i Ekipit",
|
||||||
|
"drawerPlaceholder": "Emri",
|
||||||
|
"create": "Krijo",
|
||||||
|
"delete": "Fshi",
|
||||||
|
"settings": "Cilësimet",
|
||||||
|
"popTitle": "Jeni i sigurt?",
|
||||||
|
"message": "Ju lutemi shkruani një Emër",
|
||||||
|
"teamSettings": "Cilësimet e Ekipit",
|
||||||
|
"teamName": "Emri i Ekipit",
|
||||||
|
"teamDescription": "Përshkrimi i Ekipit",
|
||||||
|
"teamMembers": "Anëtarët e Ekipit",
|
||||||
|
"teamMembersCount": "Numri i Anëtarëve të Ekipit",
|
||||||
|
"teamMembersPlaceholder": "Kërko sipas emrit",
|
||||||
|
"addMember": "Shto Anëtar",
|
||||||
|
"add": "Shto",
|
||||||
|
"update": "Përditëso",
|
||||||
|
"teamNamePlaceholder": "Emri i ekipit",
|
||||||
|
"user": "Përdoruesi",
|
||||||
|
"role": "Roli",
|
||||||
|
"owner": "Pronari",
|
||||||
|
"admin": "Administruesi",
|
||||||
|
"member": "Anëtari"
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"title": "Përdoruesit",
|
||||||
|
"subTitle": "përdoruesit",
|
||||||
|
"placeholder": "Kërko sipas emrit",
|
||||||
|
"user": "Përdoruesi",
|
||||||
|
"email": "Email",
|
||||||
|
"lastActivity": "Aktiviteti i Fundit",
|
||||||
|
"refresh": "Rifresko përdoruesit"
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
{
|
||||||
|
"name": "Emri",
|
||||||
|
"client": "Klienti",
|
||||||
|
"category": "Kategoria",
|
||||||
|
"status": "Statusi",
|
||||||
|
"tasksProgress": "Përparimi i Detyrave",
|
||||||
|
"updated_at": "E Përditësuar së Fundi",
|
||||||
|
"members": "Anëtarët",
|
||||||
|
"setting": "Cilësimet",
|
||||||
|
"projects": "Projektet",
|
||||||
|
"refreshProjects": "Rifresko projektet",
|
||||||
|
"all": "Të gjitha",
|
||||||
|
"favorites": "Të preferuarit",
|
||||||
|
"archived": "E arkivuar",
|
||||||
|
"placeholder": "Kërko sipas emrit",
|
||||||
|
"archive": "Arkivo",
|
||||||
|
"unarchive": "Çarkivo",
|
||||||
|
"archiveConfirm": "Jeni i sigurt që dëshironi të arkivoni këtë projekt?",
|
||||||
|
"unarchiveConfirm": "Jeni i sigurt që dëshironi të çarkivoni këtë projekt?",
|
||||||
|
"yes": "Po",
|
||||||
|
"no": "Jo",
|
||||||
|
"clickToFilter": "Kliko për të filtruar sipas",
|
||||||
|
"noProjects": "Nuk u gjetën projekte",
|
||||||
|
"addToFavourites": "Shto te të preferuarit",
|
||||||
|
"list": "Lista",
|
||||||
|
"group": "Grupi",
|
||||||
|
"listView": "Pamja e Listës",
|
||||||
|
"groupView": "Pamja e Grupit",
|
||||||
|
"groupBy": {
|
||||||
|
"category": "Kategoria",
|
||||||
|
"client": "Klienti"
|
||||||
|
},
|
||||||
|
"noPermission": "Nuk keni leje për të kryer këtë veprim"
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"loggingOut": "Po dilni...",
|
||||||
|
"authenticating": "Po autentikoheni...",
|
||||||
|
"gettingThingsReady": "Po përgatiten gjërat për ju..."
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"headerDescription": "Rivendosni fjalëkalimin tuaj",
|
||||||
|
"emailLabel": "Email",
|
||||||
|
"emailPlaceholder": "Vendosni email-in tuaj",
|
||||||
|
"emailRequired": "Ju lutemi vendosni Email-in tuaj!",
|
||||||
|
"resetPasswordButton": "Rivendos Fjalëkalimin",
|
||||||
|
"returnToLoginButton": "Kthehu te Hyrja",
|
||||||
|
"passwordResetSuccessMessage": "Një lidhje për rivendosjen e fjalëkalimit është dërguar në email-in tuaj.",
|
||||||
|
"orText": "OSE",
|
||||||
|
"successTitle": "U dërguan udhëzimet për rivendosje!",
|
||||||
|
"successMessage": "Informacioni për rivendosje është dërguar në email-in tuaj. Ju lutemi kontrolloni email-in."
|
||||||
|
}
|
||||||
27
worklenz-backend/src/public/locales/alb/auth/login.json
Normal file
27
worklenz-backend/src/public/locales/alb/auth/login.json
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"headerDescription": "Hyni në llogarinë tuaj",
|
||||||
|
"emailLabel": "Email",
|
||||||
|
"emailPlaceholder": "Vendosni email-in tuaj",
|
||||||
|
"emailRequired": "Ju lutemi vendosni Email-in tuaj!",
|
||||||
|
"passwordLabel": "Fjalëkalimi",
|
||||||
|
"passwordPlaceholder": "Vendosni fjalëkalimin",
|
||||||
|
"passwordRequired": "Ju lutemi vendosni Fjalëkalimin!",
|
||||||
|
"rememberMe": "Më mbaj mend",
|
||||||
|
"loginButton": "Hyr",
|
||||||
|
"signupButton": "Regjistrohu",
|
||||||
|
"forgotPasswordButton": "Keni harruar fjalëkalimin?",
|
||||||
|
"signInWithGoogleButton": "Hyr me Google",
|
||||||
|
"dontHaveAccountText": "Nuk keni llogari?",
|
||||||
|
"orText": "OSE",
|
||||||
|
"successMessage": "Jeni futur me sukses!",
|
||||||
|
"loginError": "Hyrja dështoi",
|
||||||
|
"googleLoginError": "Hyrja përmes Google dështoi",
|
||||||
|
"validationMessages": {
|
||||||
|
"email": "Ju lutemi vendosni një adresë email të vlefshme",
|
||||||
|
"password": "Fjalëkalimi duhet të jetë së paku 8 karaktere"
|
||||||
|
},
|
||||||
|
"errorMessages": {
|
||||||
|
"loginErrorTitle": "Hyrja dështoi",
|
||||||
|
"loginErrorMessage": "Ju lutemi kontrolloni email-in dhe fjalëkalimin dhe provoni përsëri"
|
||||||
|
}
|
||||||
|
}
|
||||||
29
worklenz-backend/src/public/locales/alb/auth/signup.json
Normal file
29
worklenz-backend/src/public/locales/alb/auth/signup.json
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"headerDescription": "Regjistrohuni për të filluar",
|
||||||
|
"nameLabel": "Emri i Plotë",
|
||||||
|
"namePlaceholder": "Shkruani emrin tuaj të plotë",
|
||||||
|
"nameRequired": "Ju lutemi shkruani emrin tuaj të plotë!",
|
||||||
|
"nameMinCharacterRequired": "Emri duhet të jetë së paku 4 karaktere!",
|
||||||
|
"emailLabel": "Email",
|
||||||
|
"emailPlaceholder": "Shkruani email-in tuaj",
|
||||||
|
"emailRequired": "Ju lutemi shkruani Email-in tuaj!",
|
||||||
|
"passwordLabel": "Fjalëkalimi",
|
||||||
|
"passwordPlaceholder": "Krijoni një fjalëkalim",
|
||||||
|
"passwordRequired": "Ju lutemi krijoni një Fjalëkalim!",
|
||||||
|
"passwordMinCharacterRequired": "Fjalëkalimi duhet të jetë së paku 8 karaktere!",
|
||||||
|
"passwordPatternRequired": "Fjalëkalimi nuk plotëson kërkesat!",
|
||||||
|
"strongPasswordPlaceholder": "Vendosni një fjalëkalim më të fortë",
|
||||||
|
"passwordValidationAltText": "Fjalëkalimi duhet të përmbajë së paku 8 karaktere me shkronja të mëdha dhe të vogla, një numër dhe një simbol.",
|
||||||
|
"signupSuccessMessage": "Jeni regjistruar me sukses!",
|
||||||
|
"privacyPolicyLink": "Politika e Privatësisë",
|
||||||
|
"termsOfUseLink": "Kushtet e Përdorimit",
|
||||||
|
"bySigningUpText": "Duke u regjistruar, ju pranoni",
|
||||||
|
"andText": "dhe",
|
||||||
|
"signupButton": "Regjistrohu",
|
||||||
|
"signInWithGoogleButton": "Hyr me Google",
|
||||||
|
"alreadyHaveAccountText": "Keni tashmë një llogari?",
|
||||||
|
"loginButton": "Hyr",
|
||||||
|
"orText": "OSE",
|
||||||
|
"reCAPTCHAVerificationError": "Gabim në Verifikimin e reCAPTCHA",
|
||||||
|
"reCAPTCHAVerificationErrorMessage": "Nuk mundëm të verifikojmë reCAPTCHA-n tuaj. Ju lutemi provoni përsëri."
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"title": "Verifikoni Email-in për Rivendosje",
|
||||||
|
"description": "Vendosni fjalëkalimin tuaj të ri",
|
||||||
|
"placeholder": "Vendosni fjalëkalimin tuaj të ri",
|
||||||
|
"confirmPasswordPlaceholder": "Konfirmoni fjalëkalimin e ri",
|
||||||
|
"passwordHint": "Të paktën 8 karaktere, me shkronja të mëdha dhe të vogla, një numër dhe një simbol.",
|
||||||
|
"resetPasswordButton": "Rivendos fjalëkalimin",
|
||||||
|
"orText": "Ose",
|
||||||
|
"resendResetEmail": "Dërgo përsëri email-in e rivendosjes",
|
||||||
|
"passwordRequired": "Ju lutemi vendosni fjalëkalimin e ri",
|
||||||
|
"returnToLoginButton": "Kthehu te Hyrja",
|
||||||
|
"confirmPasswordRequired": "Ju lutemi konfirmoni fjalëkalimin e ri",
|
||||||
|
"passwordMismatch": "Fjalëkalimet nuk përputhen"
|
||||||
|
}
|
||||||
9
worklenz-backend/src/public/locales/alb/common.json
Normal file
9
worklenz-backend/src/public/locales/alb/common.json
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"login-success": "Hyrja u krye me sukses!",
|
||||||
|
"login-failed": "Hyrja dështoi. Ju lutemi kontrolloni kredencialet dhe provoni përsëri.",
|
||||||
|
"signup-success": "Regjistrimi u krye me sukses! Mirë se erdhët.",
|
||||||
|
"signup-failed": "Regjistrimi dështoi. Ju lutemi sigurohuni që të gjitha fushat e nevojshme janë plotësuar dhe provoni përsëri.",
|
||||||
|
"reconnecting": "Jeni shkëputur nga serveri.",
|
||||||
|
"connection-lost": "Lidhja me serverin dështoi. Ju lutemi kontrolloni lidhjen tuaj me internet.",
|
||||||
|
"connection-restored": "U lidhët me serverin me sukses"
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"formTitle": "Krijoni projektin tuaj të parë",
|
||||||
|
"inputLabel": "Në cilin projekt po punoni aktualisht?",
|
||||||
|
"or": "ose",
|
||||||
|
"templateButton": "Importo nga shablloni",
|
||||||
|
"createFromTemplate": "Krijo nga shablloni",
|
||||||
|
"goBack": "Kthehu Mbrapa",
|
||||||
|
"continue": "Vazhdo",
|
||||||
|
"cancel": "Anulo",
|
||||||
|
"create": "Krijo",
|
||||||
|
"templateDrawerTitle": "Zgjidh nga shabllonet",
|
||||||
|
"createProject": "Krijo Projekt"
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"formTitle": "Krijo detyrën tënde të parë.",
|
||||||
|
"inputLabel": "Shkruaj disa detyra që do të kryesh në",
|
||||||
|
"addAnother": "Shto një tjetër",
|
||||||
|
"goBack": "Kthehu mbrapa",
|
||||||
|
"continue": "Vazhdo"
|
||||||
|
}
|
||||||
46
worklenz-backend/src/public/locales/alb/home.json
Normal file
46
worklenz-backend/src/public/locales/alb/home.json
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
{
|
||||||
|
"todoList": {
|
||||||
|
"title": "Lista e Detyrave",
|
||||||
|
"refreshTasks": "Rifresko detyrat",
|
||||||
|
"addTask": "+ Shto Detyrë",
|
||||||
|
"noTasks": "Asnjë detyrë",
|
||||||
|
"pressEnter": "Shtyp",
|
||||||
|
"toCreate": "për të krijuar.",
|
||||||
|
"markAsDone": "Shëno si të përfunduar"
|
||||||
|
},
|
||||||
|
"projects": {
|
||||||
|
"title": "Projektet",
|
||||||
|
"refreshProjects": "Rifresko projektet",
|
||||||
|
"noRecentProjects": "Aktualisht nuk jeni caktuar në asnjë projekt.",
|
||||||
|
"noFavouriteProjects": "Asnjë projekt i shënuar si i preferuar.",
|
||||||
|
"recent": "Të Fundit",
|
||||||
|
"favourites": "Të Preferuarat"
|
||||||
|
},
|
||||||
|
"tasks": {
|
||||||
|
"assignedToMe": "Më janë caktuar",
|
||||||
|
"assignedByMe": "I kam caktuar",
|
||||||
|
"all": "Të Gjitha",
|
||||||
|
"today": "Sot",
|
||||||
|
"upcoming": "Ardhj",
|
||||||
|
"overdue": "Të vonuara",
|
||||||
|
"noDueDate": "Pa afat",
|
||||||
|
"noTasks": "Asnjë detyrë për të shfaqur.",
|
||||||
|
"addTask": "+ Shto detyrë",
|
||||||
|
"name": "Emri",
|
||||||
|
"project": "Projekti",
|
||||||
|
"status": "Statusi",
|
||||||
|
"dueDate": "Afati",
|
||||||
|
"dueDatePlaceholder": "Cakto Afatin",
|
||||||
|
"tomorrow": "Nesër",
|
||||||
|
"nextWeek": "Javën e Ardhshme",
|
||||||
|
"nextMonth": "Muajin e Ardhshëm",
|
||||||
|
"projectRequired": "Ju lutemi zgjidhni një projekt",
|
||||||
|
"pressTabToSelectDueDateAndProject": "Shtyp Tab për të zgjedhur afatin dhe projektin",
|
||||||
|
"dueOn": "Detyrat me afat më",
|
||||||
|
"taskRequired": "Ju lutemi shtoni një detyrë",
|
||||||
|
"list": "Listë",
|
||||||
|
"calendar": "Kalendar",
|
||||||
|
"tasks": "Detyrat",
|
||||||
|
"refresh": "Rifresko"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"formTitle": "Fto ekipin tënd të punojë me",
|
||||||
|
"inputLabel": "Fto me email",
|
||||||
|
"addAnother": "Shto një tjetër",
|
||||||
|
"goBack": "Kthehu mbrapa",
|
||||||
|
"continue": "Vazhdo",
|
||||||
|
"skipForNow": "Anashkalo tani për tani"
|
||||||
|
}
|
||||||
30
worklenz-backend/src/public/locales/alb/kanban-board.json
Normal file
30
worklenz-backend/src/public/locales/alb/kanban-board.json
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"rename": "Riemërto",
|
||||||
|
"delete": "Fshi",
|
||||||
|
"addTask": "Shto Detyrë",
|
||||||
|
"addSectionButton": "Shto Seksion",
|
||||||
|
"changeCategory": "Ndrysho kategorinë",
|
||||||
|
|
||||||
|
"deleteTooltip": "Fshi",
|
||||||
|
"deleteConfirmationTitle": "Jeni i sigurt?",
|
||||||
|
"deleteConfirmationOk": "Po",
|
||||||
|
"deleteConfirmationCancel": "Anulo",
|
||||||
|
|
||||||
|
"dueDate": "Data e përfundimit",
|
||||||
|
"cancel": "Anulo",
|
||||||
|
|
||||||
|
"today": "Sot",
|
||||||
|
"tomorrow": "Nesër",
|
||||||
|
"assignToMe": "Cakto mua",
|
||||||
|
"archive": "Arkivo",
|
||||||
|
|
||||||
|
"newTaskNamePlaceholder": "Shkruaj emrin e detyrës",
|
||||||
|
"newSubtaskNamePlaceholder": "Shkruaj emrin e nëndetyrës",
|
||||||
|
"untitledSection": "Seksion pa titull",
|
||||||
|
"unmapped": "Pa hartë",
|
||||||
|
"clickToChangeDate": "Klikoni për të ndryshuar datën",
|
||||||
|
"noDueDate": "Pa datë përfundimi",
|
||||||
|
"save": "Ruaj",
|
||||||
|
"clear": "Pastro",
|
||||||
|
"nextWeek": "Javën e ardhshme"
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"title": "Prova juaj e Worklenz ka skaduar!",
|
||||||
|
"subtitle": "Ju lutemi përmirësoni tani.",
|
||||||
|
"button": "Përmirëso tani",
|
||||||
|
"checking": "Po kontrollohet statusi i abonimit..."
|
||||||
|
}
|
||||||
31
worklenz-backend/src/public/locales/alb/navbar.json
Normal file
31
worklenz-backend/src/public/locales/alb/navbar.json
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
{
|
||||||
|
"logoAlt": "Logoja e Worklenz",
|
||||||
|
"home": "Kryefaqja",
|
||||||
|
"projects": "Projektet",
|
||||||
|
"schedule": "Orari",
|
||||||
|
"reporting": "Raportimi",
|
||||||
|
"clients": "Klientët",
|
||||||
|
"teams": "Ekipet",
|
||||||
|
"labels": "Etiketa",
|
||||||
|
"jobTitles": "Tituj Pune",
|
||||||
|
"upgradePlan": "Përmirëso Abonimin",
|
||||||
|
"upgradePlanTooltip": "Përmirëso abonimin",
|
||||||
|
"invite": "Fto",
|
||||||
|
"inviteTooltip": "Fto anëtarë të ekipit të bashkohen",
|
||||||
|
"switchTeamTooltip": "Ndrysho ekipin",
|
||||||
|
"help": "Ndihmë",
|
||||||
|
"notificationTooltip": "Shiko njoftimet",
|
||||||
|
"profileTooltip": "Shiko profilin",
|
||||||
|
"adminCenter": "Qendra Administrative",
|
||||||
|
"settings": "Cilësimet",
|
||||||
|
"logOut": "Dil",
|
||||||
|
"notificationsDrawer": {
|
||||||
|
"read": "Lexuara e njoftimet ",
|
||||||
|
"unread": "Njoftimet e palexuara",
|
||||||
|
"markAsRead": "Shëno si të lexuara",
|
||||||
|
"readAndJoin": "Lexo & Bashkohu",
|
||||||
|
"accept": "Prano",
|
||||||
|
"acceptAndJoin": "Prano & Bashkohu",
|
||||||
|
"noNotifications": "Asnjë njoftim"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"nameYourOrganization": "Emërtoni organizatën tuaj.",
|
||||||
|
"worklenzAccountTitle": "Zgjidhni një emër për llogarinë tuaj në Worklenz.",
|
||||||
|
"continue": "Vazhdo"
|
||||||
|
}
|
||||||
19
worklenz-backend/src/public/locales/alb/phases-drawer.json
Normal file
19
worklenz-backend/src/public/locales/alb/phases-drawer.json
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"configurePhases": "Konfiguro Fazat",
|
||||||
|
"phaseLabel": "Etiketa e Fazës",
|
||||||
|
"enterPhaseName": "Vendosni një emër për etiketën e fazës",
|
||||||
|
"addOption": "Shto Opsion",
|
||||||
|
"phaseOptions": "Opsionet e Fazës:",
|
||||||
|
"dragToReorderPhases": "Zvarrit fazat për t'i rirenditur. Çdo fazë mund të ketë një ngjyrë të ndryshme.",
|
||||||
|
"enterNewPhaseName": "Shkruani emrin e fazës së re...",
|
||||||
|
"addPhase": "Shto Fazë",
|
||||||
|
"noPhasesFound": "Nuk u gjetën faza. Krijoni fazën tuaj të parë më sipër.",
|
||||||
|
"deletePhase": "Fshi Fazën",
|
||||||
|
"deletePhaseConfirm": "Jeni të sigurt që doni të fshini këtë fazë? Ky veprim nuk mund të zhbëhet.",
|
||||||
|
"rename": "Riemëro",
|
||||||
|
"delete": "Fshi",
|
||||||
|
"enterPhaseName": "Shkruani emrin e fazës",
|
||||||
|
"selectColor": "Zgjidh ngjyrën",
|
||||||
|
"managePhases": "Menaxho Fazat",
|
||||||
|
"close": "Mbyll"
|
||||||
|
}
|
||||||
42
worklenz-backend/src/public/locales/alb/project-drawer.json
Normal file
42
worklenz-backend/src/public/locales/alb/project-drawer.json
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
{
|
||||||
|
"createProject": "Krijo Projekt",
|
||||||
|
"editProject": "Modifiko Projektin",
|
||||||
|
"enterCategoryName": "Vendosni emër për kategorinë",
|
||||||
|
"hitEnterToCreate": "Shtyp Enter për të krijuar!",
|
||||||
|
"enterNotes": "Shënime",
|
||||||
|
"youCanManageClientsUnderSettings": "Mund të menaxhoni klientët nën Cilësimet",
|
||||||
|
"addCategory": "Shto kategori projektit",
|
||||||
|
"newCategory": "Kategori e Re",
|
||||||
|
"notes": "Shënime",
|
||||||
|
"startDate": "Data e Fillimit",
|
||||||
|
"endDate": "Data e Përfundimit",
|
||||||
|
"estimateWorkingDays": "Vlerëso ditët e punës",
|
||||||
|
"estimateManDays": "Vlerëso ditët e punëtorëve",
|
||||||
|
"hoursPerDay": "Orë në ditë",
|
||||||
|
"create": "Krijo",
|
||||||
|
"update": "Përditëso",
|
||||||
|
"delete": "Fshi",
|
||||||
|
"typeToSearchClients": "Shkruani për të kërkuar klientë",
|
||||||
|
"projectColor": "Ngjyra e Projektit",
|
||||||
|
"pleaseEnterAName": "Ju lutemi vendosni një emër",
|
||||||
|
"enterProjectName": "Vendosni emrin e projektit",
|
||||||
|
"name": "Emri",
|
||||||
|
"status": "Statusi",
|
||||||
|
"health": "Gjendja",
|
||||||
|
"category": "Kategoria",
|
||||||
|
"projectManager": "Menaxheri i Projektit",
|
||||||
|
"client": "Klienti",
|
||||||
|
"deleteConfirmation": "Jeni i sigurt që doni të fshini?",
|
||||||
|
"deleteConfirmationDescription": "Kjo do të fshijë të gjitha të dhënat e lidhura dhe nuk mund të zhbëhet.",
|
||||||
|
"yes": "Po",
|
||||||
|
"no": "Jo",
|
||||||
|
"createdAt": "Krijuar më",
|
||||||
|
"updatedAt": "Përditësuar më",
|
||||||
|
"by": "nga",
|
||||||
|
"add": "Shto",
|
||||||
|
"asClient": "si klient",
|
||||||
|
"createClient": "Krijo klient",
|
||||||
|
"searchInputPlaceholder": "Kërko sipas emrit ose emailit",
|
||||||
|
"hoursPerDayValidationMessage": "Orët në ditë duhet të jenë një numër midis 1 dhe 24",
|
||||||
|
"noPermission": "Nuk ka leje"
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"nameColumn": "Emri",
|
||||||
|
"attachedTaskColumn": "Detyra e Bashkangjitur",
|
||||||
|
"sizeColumn": "Madhësia",
|
||||||
|
"uploadedByColumn": "Ngarkuar Nga",
|
||||||
|
"uploadedAtColumn": "Ngarkuar Më",
|
||||||
|
"fileIconAlt": "Ikona e skedarit",
|
||||||
|
"titleDescriptionText": "Të gjitha bashkëngjitjet e detyrave në këtë projekt do të shfahen këtu.",
|
||||||
|
"deleteConfirmationTitle": "Jeni i sigurt?",
|
||||||
|
"deleteConfirmationOk": "Po",
|
||||||
|
"deleteConfirmationCancel": "Anulo",
|
||||||
|
"segmentedTooltip": "Së shpejti! Kaloni midis pamjes listë dhe pamjes miniaturash.",
|
||||||
|
"emptyText": "Nuk ka bashkëngjitje në projekt."
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
{
|
||||||
|
"overview": {
|
||||||
|
"title": "Përmbledhje",
|
||||||
|
"statusOverview": "Përmbledhje Statusi",
|
||||||
|
"priorityOverview": "Përmbledhje Prioriteti",
|
||||||
|
"lastUpdatedTasks": "Detyrat e Përditësuara Së Fundi"
|
||||||
|
},
|
||||||
|
"members": {
|
||||||
|
"title": "Anëtarët",
|
||||||
|
"tooltip": "Anëtarët",
|
||||||
|
"tasksByMembers": "Detyrat sipas anëtarëve",
|
||||||
|
"tasksByMembersTooltip": "Detyrat sipas anëtarëve",
|
||||||
|
"name": "Emri",
|
||||||
|
"taskCount": "Numri i Detyrave",
|
||||||
|
"contribution": "Kontributi",
|
||||||
|
"completed": "Të Përfunduara",
|
||||||
|
"incomplete": "Të Papërfunduara",
|
||||||
|
"overdue": "Të Vonuara",
|
||||||
|
"progress": "Progresi"
|
||||||
|
},
|
||||||
|
"tasks": {
|
||||||
|
"overdueTasks": "Detyrat e Vonuara",
|
||||||
|
"overLoggedTasks": "Detyrat me regjistrim të tepërt",
|
||||||
|
"tasksCompletedEarly": "Detyrat e përfunduara para afatit",
|
||||||
|
"tasksCompletedLate": "Detyrat e përfunduara pas afatit",
|
||||||
|
"overLoggedTasksTooltip": "Detyrat me kohë të regjistruar mbi kohën e vlerësuar",
|
||||||
|
"overdueTasksTooltip": "Detyrat që kanë kaluar afatin e tyre"
|
||||||
|
},
|
||||||
|
"common": {
|
||||||
|
"seeAll": "Shiko të gjitha",
|
||||||
|
"totalLoggedHours": "Orët totale të regjistruara",
|
||||||
|
"totalEstimation": "Vlerësimi total",
|
||||||
|
"completedTasks": "Detyrat e përfunduara",
|
||||||
|
"incompleteTasks": "Detyrat e papërfunduara",
|
||||||
|
"overdueTasks": "Detyrat e vonuara",
|
||||||
|
"overdueTasksTooltip": "Detyrat që kanë kaluar afatin e tyre",
|
||||||
|
"totalLoggedHoursTooltip": "Vlerësimi dhe koha e regjistruar për detyrat.",
|
||||||
|
"includeArchivedTasks": "Përfshi Detyrat e Arkivuara",
|
||||||
|
"export": "Eksporto"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"nameColumn": "Emri",
|
||||||
|
"jobTitleColumn": "Titulli i Punës",
|
||||||
|
"emailColumn": "Email",
|
||||||
|
"tasksColumn": "Detyrat",
|
||||||
|
"taskProgressColumn": "Progresi i Detyrave",
|
||||||
|
"accessColumn": "Qasja",
|
||||||
|
"fileIconAlt": "Ikona e skedarit",
|
||||||
|
"deleteConfirmationTitle": "Jeni i sigurt?",
|
||||||
|
"deleteConfirmationOk": "Po",
|
||||||
|
"deleteConfirmationCancel": "Anulo",
|
||||||
|
"refreshButtonTooltip": "Rifresko anëtarët",
|
||||||
|
"deleteButtonTooltip": "Hiq nga projekti",
|
||||||
|
"memberCount": "Anëtar",
|
||||||
|
"membersCountPlural": "Anëtarë",
|
||||||
|
"emptyText": "Nuk ka bashkëngjitje në projekt."
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"inputPlaceholder": "Shto një koment..",
|
||||||
|
"addButton": "Shto",
|
||||||
|
"cancelButton": "Anulo",
|
||||||
|
"deleteButton": "Fshi"
|
||||||
|
}
|
||||||
14
worklenz-backend/src/public/locales/alb/project-view.json
Normal file
14
worklenz-backend/src/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"
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"importTaskTemplate": "Importo Shabllon Detyrash",
|
||||||
|
"templateName": "Emri i Shabllonit",
|
||||||
|
"templateDescription": "Përshkrimi i Shabllonit",
|
||||||
|
"selectedTasks": "Detyrat e Përzgjedhura",
|
||||||
|
"tasks": "Detyrat",
|
||||||
|
"templates": "Shabllonet",
|
||||||
|
"remove": "Hiq",
|
||||||
|
"cancel": "Anulo",
|
||||||
|
"import": "Importo"
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"title": "Anëtarët e Projektit",
|
||||||
|
"searchLabel": "Shtoni anëtarë duke shkruar emrin ose email-in e tyre",
|
||||||
|
"searchPlaceholder": "Shkruani emrin ose email-in",
|
||||||
|
"inviteAsAMember": "Fto si anëtar",
|
||||||
|
"inviteNewMemberByEmail": "Fto anëtar të ri me email"
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"importTasks": "Importo detyra",
|
||||||
|
"importTask": "Importo detyrë",
|
||||||
|
"createTask": "Krijo detyrë",
|
||||||
|
"settings": "Cilësimet",
|
||||||
|
"subscribe": "Abonohu",
|
||||||
|
"unsubscribe": "Çabonohu",
|
||||||
|
"deleteProject": "Fshi projektin",
|
||||||
|
"startDate": "Data e fillimit",
|
||||||
|
"endDate": "Data e mbarimit",
|
||||||
|
"projectSettings": "Cilësimet e projektit",
|
||||||
|
"projectSummary": "Përmbledhja e projektit",
|
||||||
|
"receiveProjectSummary": "Merrni një përmbledhje të projektit çdo mbrëmje.",
|
||||||
|
"refreshProject": "Rifresko projektin",
|
||||||
|
"saveAsTemplate": "Ruaj si model",
|
||||||
|
"invite": "Fto",
|
||||||
|
"share": "Ndaj",
|
||||||
|
"subscribeTooltip": "Abonohu tek njoftimet e projektit",
|
||||||
|
"unsubscribeTooltip": "Çabonohu nga njoftimet e projektit",
|
||||||
|
"refreshTooltip": "Rifresko të dhënat e projektit",
|
||||||
|
"settingsTooltip": "Hap cilësimet e projektit",
|
||||||
|
"saveAsTemplateTooltip": "Ruaj këtë projekt si model",
|
||||||
|
"inviteTooltip": "Fto anëtarë të ekipit në këtë projekt",
|
||||||
|
"createTaskTooltip": "Krijo një detyrë të re",
|
||||||
|
"importTaskTooltip": "Importo detyrë nga modeli",
|
||||||
|
"navigateBackTooltip": "Kthehu tek lista e projekteve",
|
||||||
|
"projectStatusTooltip": "Statusi i projektit",
|
||||||
|
"projectDatesInfo": "Informacion për kohëzgjatjen e projektit",
|
||||||
|
"projectCategoryTooltip": "Kategoria e projektit"
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"title": "Ruaj si Shabllon",
|
||||||
|
"templateName": "Emri i Shabllonit",
|
||||||
|
"includes": "Çfarë duhet të përfshihet në shabllon nga projekti?",
|
||||||
|
"includesOptions": {
|
||||||
|
"statuses": "Statuset",
|
||||||
|
"phases": "Fazat",
|
||||||
|
"labels": "Etiketat"
|
||||||
|
},
|
||||||
|
"taskIncludes": "Çfarë duhet të përfshihet në shabllon nga detyrat?",
|
||||||
|
"taskIncludesOptions": {
|
||||||
|
"statuses": "Statuset",
|
||||||
|
"phases": "Fazat",
|
||||||
|
"labels": "Etiketat",
|
||||||
|
"name": "Emri",
|
||||||
|
"priority": "Prioriteti",
|
||||||
|
"status": "Statusi",
|
||||||
|
"phase": "Faza",
|
||||||
|
"label": "Etiketa",
|
||||||
|
"timeEstimate": "Vlerësimi i Kohës",
|
||||||
|
"description": "Përshkrimi",
|
||||||
|
"subTasks": "Nëndetyrat"
|
||||||
|
},
|
||||||
|
"cancel": "Anulo",
|
||||||
|
"save": "Ruaj",
|
||||||
|
"templateNamePlaceholder": "Shkruani emrin e shabllonit"
|
||||||
|
}
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
{
|
||||||
|
"exportButton": "Eksporto",
|
||||||
|
"timeLogsButton": "Regjistrimet e Kohës",
|
||||||
|
"activityLogsButton": "Regjistrimet e Aktivitetit",
|
||||||
|
"tasksButton": "Detyrat",
|
||||||
|
"searchByNameInputPlaceholder": "Kërko sipas emrit",
|
||||||
|
|
||||||
|
"overviewTab": "Përmbledhje",
|
||||||
|
"timeLogsTab": "Regjistrimet e Kohës",
|
||||||
|
"activityLogsTab": "Regjistrimet e Aktivitetit",
|
||||||
|
"tasksTab": "Detyrat",
|
||||||
|
|
||||||
|
"projectsText": "Projektet",
|
||||||
|
"totalTasksText": "Detyrat Gjithsej",
|
||||||
|
"assignedTasksText": "Detyrat e Caktuara",
|
||||||
|
"completedTasksText": "Detyrat e Përfunduara",
|
||||||
|
"ongoingTasksText": "Detyrat në Vazhdim",
|
||||||
|
"overdueTasksText": "Detyrat e Vonuara",
|
||||||
|
"loggedHoursText": "Orët e Regjistruara",
|
||||||
|
|
||||||
|
"tasksText": "Detyrat",
|
||||||
|
"allText": "Të Gjitha",
|
||||||
|
|
||||||
|
"tasksByProjectsText": "Detyrat Sipas Projekteve",
|
||||||
|
"tasksByStatusText": "Detyrat Sipas Statusit",
|
||||||
|
"tasksByPriorityText": "Detyrat Sipas Prioritetit",
|
||||||
|
|
||||||
|
"todoText": "Për Të Bërë",
|
||||||
|
"doingText": "Duke bërë",
|
||||||
|
"doneText": "E Përfunduar",
|
||||||
|
"lowText": "I Ulët",
|
||||||
|
"mediumText": "I Mesëm",
|
||||||
|
"highText": "I Lartë",
|
||||||
|
|
||||||
|
"billableButton": "Fakturueshme",
|
||||||
|
"billableText": "Fakturueshme",
|
||||||
|
"nonBillableText": "Jo Fakturueshme",
|
||||||
|
|
||||||
|
"timeLogsEmptyPlaceholder": "Asnjë regjistrim kohe për të shfaqur",
|
||||||
|
"loggedText": "Regjistruar",
|
||||||
|
"forText": "për",
|
||||||
|
"inText": "në",
|
||||||
|
"updatedText": "Përditësuar",
|
||||||
|
"fromText": "Nga",
|
||||||
|
"toText": "në",
|
||||||
|
"withinText": "brenda",
|
||||||
|
|
||||||
|
"activityLogsEmptyPlaceholder": "Asnjë regjistrim aktiviteti për të shfaqur",
|
||||||
|
|
||||||
|
"filterByText": "Filtro sipas:",
|
||||||
|
"selectProjectPlaceholder": "Zgjidh Projektin",
|
||||||
|
|
||||||
|
"taskColumn": "Detyra",
|
||||||
|
"nameColumn": "Emri",
|
||||||
|
"projectColumn": "Projekti",
|
||||||
|
"statusColumn": "Statusi",
|
||||||
|
"priorityColumn": "Prioriteti",
|
||||||
|
"dueDateColumn": "Afati",
|
||||||
|
"completedDateColumn": "Data e Përfundimit",
|
||||||
|
"estimatedTimeColumn": "Koha e Vlerësuar",
|
||||||
|
"loggedTimeColumn": "Koha e Regjistruar",
|
||||||
|
"overloggedTimeColumn": "Koha e Tepërt",
|
||||||
|
"daysLeftColumn": "Ditë të Mbetura/Vonuar",
|
||||||
|
"startDateColumn": "Data e Fillimit",
|
||||||
|
"endDateColumn": "Data e Përfundimit",
|
||||||
|
"actualTimeColumn": "Koha Aktuale",
|
||||||
|
"projectHealthColumn": "Gjendja e Projektit",
|
||||||
|
"categoryColumn": "Kategoria",
|
||||||
|
"projectManagerColumn": "Menaxheri i Projektit",
|
||||||
|
|
||||||
|
"tasksStatsOverviewDrawerTitle": "Detyrat e ",
|
||||||
|
"projectsStatsOverviewDrawerTitle": "Projektet e ",
|
||||||
|
|
||||||
|
"cancelledText": "Anuluar",
|
||||||
|
"blockedText": "E Bllokuar",
|
||||||
|
"onHoldText": "Në Pritje",
|
||||||
|
"proposedText": "E Propozuar",
|
||||||
|
"inPlanningText": "Në Planifikim",
|
||||||
|
"inProgressText": "Në Progres",
|
||||||
|
"completedText": "E Përfunduar",
|
||||||
|
"continuousText": "E Vazhdueshme",
|
||||||
|
|
||||||
|
"daysLeftText": "ditë të mbetura",
|
||||||
|
"daysOverdueText": "ditë vonuar",
|
||||||
|
|
||||||
|
"notSetText": "Pa Caktuar",
|
||||||
|
"needsAttentionText": "Kërkon Vëmendje",
|
||||||
|
"atRiskText": "Në Rrezik",
|
||||||
|
"goodText": "Në Rregull"
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
{
|
||||||
|
"yesterdayText": "Dje",
|
||||||
|
"lastSevenDaysText": "7 Ditët e Fundit",
|
||||||
|
"lastWeekText": "Javën e Kaluar",
|
||||||
|
"lastThirtyDaysText": "30 Ditët e Fundit",
|
||||||
|
"lastMonthText": "Muajin e Kaluar",
|
||||||
|
"lastThreeMonthsText": "3 Muajt e Fundit",
|
||||||
|
"allTimeText": "Të Gjitha",
|
||||||
|
"customRangeText": "Interval i Përshtatur",
|
||||||
|
"startDateInputPlaceholder": "Data e fillimit",
|
||||||
|
"EndDateInputPlaceholder": "Data e përfundimit",
|
||||||
|
"filterButton": "Filtro",
|
||||||
|
|
||||||
|
"membersTitle": "Anëtarët",
|
||||||
|
"includeArchivedButton": "Përfshij Projektet e Arkivuara",
|
||||||
|
"exportButton": "Eksporto",
|
||||||
|
"excelButton": "Excel",
|
||||||
|
"searchByNameInputPlaceholder": "Kërko sipas emrit",
|
||||||
|
|
||||||
|
"memberColumn": "Anëtari",
|
||||||
|
"tasksProgressColumn": "Progresi i Detyrave",
|
||||||
|
"tasksAssignedColumn": "Detyrat e Caktuara",
|
||||||
|
"completedTasksColumn": "Detyrat e Përfunduara",
|
||||||
|
"overdueTasksColumn": "Detyrat e Vonuara",
|
||||||
|
"ongoingTasksColumn": "Detyrat në Vazhdim",
|
||||||
|
|
||||||
|
"tasksAssignedColumnTooltip": "Detyrat e caktuara në intervalin e zgjedhur",
|
||||||
|
"overdueTasksColumnTooltip": "Detyrat e vonuara deri në fund të intervalit të zgjedhur",
|
||||||
|
"completedTasksColumnTooltip": "Detyrat e përfunduara në intervalin e zgjedhur",
|
||||||
|
"ongoingTasksColumnTooltip": "Detyrat e filluara por jo të përfunduara ende",
|
||||||
|
|
||||||
|
"todoText": "Për Të Bërë",
|
||||||
|
"doingText": "Duke bërë",
|
||||||
|
"doneText": "E Përfunduar"
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
{
|
||||||
|
"exportButton": "Eksporto",
|
||||||
|
"projectsButton": "Projektet",
|
||||||
|
"membersButton": "Anëtarët",
|
||||||
|
"searchByNameInputPlaceholder": "Kërko sipas emrit",
|
||||||
|
|
||||||
|
"overviewTab": "Përmbledhje",
|
||||||
|
"projectsTab": "Projektet",
|
||||||
|
"membersTab": "Anëtarët",
|
||||||
|
|
||||||
|
"projectsByStatusText": "Projektet Sipas Statusit",
|
||||||
|
"projectsByCategoryText": "Projektet Sipas Kategorisë",
|
||||||
|
"projectsByHealthText": "Projektet Sipas Gjendjes",
|
||||||
|
|
||||||
|
"projectsText": "Projektet",
|
||||||
|
"allText": "Të Gjitha",
|
||||||
|
|
||||||
|
"cancelledText": "Anuluar",
|
||||||
|
"blockedText": "E Bllokuar",
|
||||||
|
"onHoldText": "Në Pritje",
|
||||||
|
"proposedText": "E Propozuar",
|
||||||
|
"inPlanningText": "Në Planifikim",
|
||||||
|
"inProgressText": "Në Progres",
|
||||||
|
"completedText": "E Përfunduar",
|
||||||
|
"continuousText": "E Vazhdueshme",
|
||||||
|
|
||||||
|
"notSetText": "Pa Caktuar",
|
||||||
|
"needsAttentionText": "Kërkon Vëmendje",
|
||||||
|
"atRiskText": "Në Rrezik",
|
||||||
|
"goodText": "Në Rregull",
|
||||||
|
|
||||||
|
"nameColumn": "Emri",
|
||||||
|
"emailColumn": "Email",
|
||||||
|
"projectsColumn": "Projektet",
|
||||||
|
"tasksColumn": "Detyrat",
|
||||||
|
"overdueTasksColumn": "Detyrat e Vonuara",
|
||||||
|
"completedTasksColumn": "Detyrat e Përfunduara",
|
||||||
|
"ongoingTasksColumn": "Detyrat në Vazhdim"
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"overviewTitle": "Përmbledhje",
|
||||||
|
"includeArchivedButton": "Përfshij Projektet e Arkivuara",
|
||||||
|
|
||||||
|
"teamCount": "Ekip",
|
||||||
|
"teamCountPlural": "Ekipe",
|
||||||
|
"projectCount": "Projekt",
|
||||||
|
"projectCountPlural": "Projekte",
|
||||||
|
"memberCount": "Anëtar",
|
||||||
|
"memberCountPlural": "Anëtarë",
|
||||||
|
"activeProjectCount": "Projekt Aktiv",
|
||||||
|
"activeProjectCountPlural": "Projekte Aktive",
|
||||||
|
"overdueProjectCount": "Projekt i Vonuar",
|
||||||
|
"overdueProjectCountPlural": "Projekte të Vonuara",
|
||||||
|
"unassignedMemberCount": "Anëtar i Pacaktuar",
|
||||||
|
"unassignedMemberCountPlural": "Anëtarë të Pacaktuar",
|
||||||
|
"memberWithOverdueTaskCount": "Anëtar me Detyrë të Vonuar",
|
||||||
|
"memberWithOverdueTaskCountPlural": "Anëtarë me Detyra të Vonuara",
|
||||||
|
|
||||||
|
"teamsText": "Ekipet",
|
||||||
|
|
||||||
|
"nameColumn": "Emri",
|
||||||
|
"projectsColumn": "Projektet",
|
||||||
|
"membersColumn": "Anëtarët"
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
{
|
||||||
|
"exportButton": "Eksporto",
|
||||||
|
"membersButton": "Anëtarët",
|
||||||
|
"tasksButton": "Detyrat",
|
||||||
|
"searchByNameInputPlaceholder": "Kërko sipas emrit",
|
||||||
|
|
||||||
|
"overviewTab": "Përmbledhje",
|
||||||
|
"membersTab": "Anëtarët",
|
||||||
|
"tasksTab": "Detyrat",
|
||||||
|
|
||||||
|
"completedTasksText": "Detyrat e Përfunduara",
|
||||||
|
"incompleteTasksText": "Detyrat e Papërfunduara",
|
||||||
|
"overdueTasksText": "Detyrat e Vonuara",
|
||||||
|
"allocatedHoursText": "Orët e Alokuara",
|
||||||
|
"loggedHoursText": "Orët e Regjistruara",
|
||||||
|
|
||||||
|
"tasksText": "Detyrat",
|
||||||
|
"allText": "Të Gjitha",
|
||||||
|
|
||||||
|
"tasksByStatusText": "Detyrat Sipas Statusit",
|
||||||
|
"tasksByPriorityText": "Detyrat Sipas Prioritetit",
|
||||||
|
"tasksByDueDateText": "Detyrat Sipas Afatit",
|
||||||
|
|
||||||
|
"todoText": "Për Të Bërë",
|
||||||
|
"doingText": "Duke bërë",
|
||||||
|
"doneText": "E Përfunduar",
|
||||||
|
"lowText": "I Ulët",
|
||||||
|
"mediumText": "I Mesëm",
|
||||||
|
"highText": "I Lartë",
|
||||||
|
"completedText": "E Përfunduar",
|
||||||
|
"upcomingText": "Në Ardhje",
|
||||||
|
"overdueText": "E Vonuar",
|
||||||
|
"noDueDateText": "Pa Afat",
|
||||||
|
|
||||||
|
"nameColumn": "Emri",
|
||||||
|
"tasksCountColumn": "Numri i Detyrave",
|
||||||
|
"completedTasksColumn": "Detyrat e Përfunduara",
|
||||||
|
"incompleteTasksColumn": "Detyrat e Papërfunduara",
|
||||||
|
"overdueTasksColumn": "Detyrat e Vonuara",
|
||||||
|
"contributionColumn": "Kontributi",
|
||||||
|
"progressColumn": "Progresi",
|
||||||
|
"loggedTimeColumn": "Koha e Regjistruar",
|
||||||
|
"taskColumn": "Detyra",
|
||||||
|
"projectColumn": "Projekti",
|
||||||
|
"statusColumn": "Statusi",
|
||||||
|
"priorityColumn": "Prioriteti",
|
||||||
|
"phaseColumn": "Faza",
|
||||||
|
"dueDateColumn": "Afati",
|
||||||
|
"completedDateColumn": "Data e Përfundimit",
|
||||||
|
"estimatedTimeColumn": "Koha e Vlerësuar",
|
||||||
|
"overloggedTimeColumn": "Koha e Tepërt",
|
||||||
|
"completedOnColumn": "Përfunduar Më",
|
||||||
|
"daysOverdueColumn": "Ditë vonim",
|
||||||
|
|
||||||
|
"groupByText": "Grupo Sipas:",
|
||||||
|
"statusText": "Statusi",
|
||||||
|
"priorityText": "Prioriteti",
|
||||||
|
"phaseText": "Faza"
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
{
|
||||||
|
"searchByNamePlaceholder": "Kërko sipas emrit",
|
||||||
|
"searchByCategoryPlaceholder": "Kërko sipas kategorisë",
|
||||||
|
|
||||||
|
"statusText": "Statusi",
|
||||||
|
"healthText": "Gjendja",
|
||||||
|
"categoryText": "Kategoria",
|
||||||
|
"projectManagerText": "Menaxheri i Projektit",
|
||||||
|
"showFieldsText": "Shfaq fushat",
|
||||||
|
|
||||||
|
"cancelledText": "Anuluar",
|
||||||
|
"blockedText": "E bllokuar",
|
||||||
|
"onHoldText": "Në pritje",
|
||||||
|
"proposedText": "E propozuar",
|
||||||
|
"inPlanningText": "Në planifikim",
|
||||||
|
"inProgressText": "Në progres",
|
||||||
|
"completedText": "E përfunduar",
|
||||||
|
"continuousText": "E vazhdueshme",
|
||||||
|
|
||||||
|
"notSetText": "Pa caktuar",
|
||||||
|
"needsAttentionText": "Kërkon vëmendje",
|
||||||
|
"atRiskText": "Në rrezik",
|
||||||
|
"goodText": "Në rregull",
|
||||||
|
|
||||||
|
"nameText": "Projekti",
|
||||||
|
"estimatedVsActualText": "Vlerësuar vs Aktual",
|
||||||
|
"tasksProgressText": "Progresi i detyrave",
|
||||||
|
"lastActivityText": "Aktiviteti i fundit",
|
||||||
|
"datesText": "Datat e Fillimit/Përfundimit",
|
||||||
|
"daysLeftText": "Ditë të mbetura/vonuar",
|
||||||
|
"projectHealthText": "Gjendja e projektit",
|
||||||
|
"projectUpdateText": "Përditësimi i projektit",
|
||||||
|
"clientText": "Klienti",
|
||||||
|
"teamText": "Ekipi"
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
{
|
||||||
|
"projectCount": "Projekt",
|
||||||
|
"projectCountPlural": "Projekte",
|
||||||
|
"includeArchivedButton": "Përfshij Projektet e Arkivuara",
|
||||||
|
"exportButton": "Eksporto",
|
||||||
|
"excelButton": "Excel",
|
||||||
|
|
||||||
|
"projectColumn": "Projekti",
|
||||||
|
"estimatedVsActualColumn": "Vlerësuar vs Aktual",
|
||||||
|
"tasksProgressColumn": "Progresi i Detyrave",
|
||||||
|
"lastActivityColumn": "Aktiviteti i Fundit",
|
||||||
|
"statusColumn": "Statusi",
|
||||||
|
"datesColumn": "Data e Fillimit/Përfundimit",
|
||||||
|
"daysLeftColumn": "Ditë të Mbetura/Vonuar",
|
||||||
|
"projectHealthColumn": "Gjendja e Projektit",
|
||||||
|
"categoryColumn": "Kategoria",
|
||||||
|
"projectUpdateColumn": "Përditësimi i Projektit",
|
||||||
|
"clientColumn": "Klienti",
|
||||||
|
"teamColumn": "Ekipi",
|
||||||
|
"projectManagerColumn": "Menaxheri i Projektit",
|
||||||
|
|
||||||
|
"openButton": "Hap",
|
||||||
|
|
||||||
|
"estimatedText": "Vlerësuar",
|
||||||
|
"actualText": "Aktual",
|
||||||
|
|
||||||
|
"todoText": "Për të Bërë",
|
||||||
|
"doingText": "duke bërë",
|
||||||
|
"doneText": "E Përfunduar",
|
||||||
|
|
||||||
|
"cancelledText": "Anuluar",
|
||||||
|
"blockedText": "E Bllokuar",
|
||||||
|
"onHoldText": "Në Pritje",
|
||||||
|
"proposedText": "E Propozuar",
|
||||||
|
"inPlanningText": "Në Planifikim",
|
||||||
|
"inProgressText": "Në Progres",
|
||||||
|
"completedText": "E Përfunduar",
|
||||||
|
"continuousText": "E Vazhdueshme",
|
||||||
|
|
||||||
|
"daysLeftText": "ditë të mbetura",
|
||||||
|
"dayLeftText": "ditë e mbetur",
|
||||||
|
"daysOverdueText": "ditë vonuar",
|
||||||
|
|
||||||
|
"notSetText": "Pa Caktuar",
|
||||||
|
"needsAttentionText": "Kërkon Vëmendje",
|
||||||
|
"atRiskText": "Në Rrezik",
|
||||||
|
"goodText": "Në Rregull",
|
||||||
|
|
||||||
|
"setCategoryText": "Cakto Kategorinë",
|
||||||
|
"searchByNameInputPlaceholder": "Kërko sipas emrit",
|
||||||
|
"todayText": "Sot"
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"overview": "Përmbledhje",
|
||||||
|
"projects": "Projektet",
|
||||||
|
"members": "Anëtarët",
|
||||||
|
"timeReports": "Raportet e Kohës",
|
||||||
|
"estimateVsActual": "Vlerësimi vs Aktual",
|
||||||
|
"currentOrganizationTooltip": "Organizata aktuale"
|
||||||
|
}
|
||||||
39
worklenz-backend/src/public/locales/alb/schedule.json
Normal file
39
worklenz-backend/src/public/locales/alb/schedule.json
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
{
|
||||||
|
"today": "Sot",
|
||||||
|
"week": "Javë",
|
||||||
|
"month": "Muaj",
|
||||||
|
|
||||||
|
"settings": "Cilësimet",
|
||||||
|
"workingDays": "Ditët e punës",
|
||||||
|
"monday": "E hënë",
|
||||||
|
"tuesday": "E martë",
|
||||||
|
"wednesday": "E mërkurë",
|
||||||
|
"thursday": "E enjte",
|
||||||
|
"friday": "E premte",
|
||||||
|
"saturday": "E shtunë",
|
||||||
|
"sunday": "E diel",
|
||||||
|
"workingHours": "Orët e punës",
|
||||||
|
"hours": "Orë",
|
||||||
|
"saveButton": "Ruaj",
|
||||||
|
|
||||||
|
"totalAllocation": "Alokimi Total",
|
||||||
|
"timeLogged": "Koha e Regjistruar",
|
||||||
|
"remainingTime": "Koha e Mbetur",
|
||||||
|
"total": "Total",
|
||||||
|
"perDay": "Në Ditë",
|
||||||
|
"tasks": "detyra",
|
||||||
|
"startDate": "Data e Fillimit",
|
||||||
|
"endDate": "Data e Përfundimit",
|
||||||
|
|
||||||
|
"hoursPerDay": "Orë Në Ditë",
|
||||||
|
"totalHours": "Orë Totale",
|
||||||
|
"deleteButton": "Fshi",
|
||||||
|
"cancelButton": "Anulo",
|
||||||
|
|
||||||
|
"tabTitle": "Detyra pa Data Fillimi & Përfundimi",
|
||||||
|
|
||||||
|
"allocatedTime": "Koha e alokuar",
|
||||||
|
"totalLogged": "Total i Regjistruar",
|
||||||
|
"loggedBillable": "Regjistruar Fakturueshme",
|
||||||
|
"loggedNonBillable": "Regjistruar Jo Fakturueshme"
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"categoryColumn": "Kategoria",
|
||||||
|
"deleteConfirmationTitle": "Jeni të sigurt?",
|
||||||
|
"deleteConfirmationOk": "Po",
|
||||||
|
"deleteConfirmationCancel": "Anulo",
|
||||||
|
"associatedTaskColumn": "Projektet e Lidhura",
|
||||||
|
"searchPlaceholder": "Kërko sipas emrit",
|
||||||
|
"emptyText": "Kategoritë mund të krijohen gjatë përditësimit ose krijimit të projekteve.",
|
||||||
|
"colorChangeTooltip": "Klikoni për të ndryshuar ngjyrën"
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"title": "Ndrysho Fjalëkalimin",
|
||||||
|
"currentPassword": "Fjalëkalimi Aktual",
|
||||||
|
"newPassword": "Fjalëkalimi i Ri",
|
||||||
|
"confirmPassword": "Konfirmo Fjalëkalimin",
|
||||||
|
"currentPasswordPlaceholder": "Vendosni fjalëkalimin aktual",
|
||||||
|
"newPasswordPlaceholder": "Fjalëkalimi i Ri",
|
||||||
|
"confirmPasswordPlaceholder": "Konfirmo Fjalëkalimin",
|
||||||
|
"currentPasswordRequired": "Ju lutemi vendosni fjalëkalimin aktual!",
|
||||||
|
"newPasswordRequired": "Ju lutemi vendosni fjalëkalimin e ri!",
|
||||||
|
"passwordValidationError": "Fjalëkalimi duhet të përmbajë të paktën 8 karaktere, me një shkronjë të madhe, një numër dhe një simbol.",
|
||||||
|
"passwordMismatch": "Fjalëkalimet nuk përputhen!",
|
||||||
|
"passwordRequirements": "Fjalëkalimi i ri duhet të jetë së paku 8 karaktere, me një shkronjë të madhe, një numër dhe një simbol.",
|
||||||
|
"updateButton": "Përditëso Fjalëkalimin"
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"nameColumn": "Emri",
|
||||||
|
"projectColumn": "Projekti",
|
||||||
|
"noProjectsAvailable": "Nuk ka projekte të disponueshme",
|
||||||
|
"deleteConfirmationTitle": "Jeni i sigurt?",
|
||||||
|
"deleteConfirmationOk": "Po",
|
||||||
|
"deleteConfirmationCancel": "Anulo",
|
||||||
|
"searchPlaceholder": "Kërko sipas emrit",
|
||||||
|
"createClient": "Krijo Klient",
|
||||||
|
"pinTooltip": "Klikoni për ta fiksuar në menynë kryesore",
|
||||||
|
"createClientDrawerTitle": "Krijo Klient",
|
||||||
|
"updateClientDrawerTitle": "Përditëso Klientin",
|
||||||
|
"nameLabel": "Emri",
|
||||||
|
"namePlaceholder": "Emri",
|
||||||
|
"nameRequiredError": "Ju lutemi shkruani një Emër",
|
||||||
|
"createButton": "Krijo",
|
||||||
|
"updateButton": "Përditëso",
|
||||||
|
"createClientSuccessMessage": "Klienti u krijua me sukses!",
|
||||||
|
"createClientErrorMessage": "Krijimi i klientit dështoi!",
|
||||||
|
"updateClientSuccessMessage": "Klienti u përditësua me sukses!",
|
||||||
|
"updateClientErrorMessage": "Përditësimi i klientit dështoi!"
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"nameColumn": "Emri",
|
||||||
|
"deleteConfirmationTitle": "Jeni i sigurt?",
|
||||||
|
"deleteConfirmationOk": "Po",
|
||||||
|
"deleteConfirmationCancel": "Anulo",
|
||||||
|
"searchPlaceholder": "Kërko sipas emrit",
|
||||||
|
"createJobTitleButton": "Krijo Titull Pune",
|
||||||
|
"pinTooltip": "Klikoni për ta fiksuar në menynë kryesore",
|
||||||
|
"createJobTitleDrawerTitle": "Krijo Titull Pune",
|
||||||
|
"updateJobTitleDrawerTitle": "Përditëso Titullin e Punës",
|
||||||
|
"nameLabel": "Emri",
|
||||||
|
"namePlaceholder": "Emri",
|
||||||
|
"nameRequiredError": "Ju lutemi shkruani një Emër",
|
||||||
|
"createButton": "Krijo",
|
||||||
|
"updateButton": "Përditëso",
|
||||||
|
"createJobTitleSuccessMessage": "Titulli i punës u krijua me sukses!",
|
||||||
|
"createJobTitleErrorMessage": "Krijimi i titullit të punës dështoi!",
|
||||||
|
"updateJobTitleSuccessMessage": "Titulli i punës u përditësua me sukses!",
|
||||||
|
"updateJobTitleErrorMessage": "Përditësimi i titullit të punës dështoi!"
|
||||||
|
}
|
||||||
11
worklenz-backend/src/public/locales/alb/settings/labels.json
Normal file
11
worklenz-backend/src/public/locales/alb/settings/labels.json
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"labelColumn": "Etiketa",
|
||||||
|
"deleteConfirmationTitle": "Jeni i sigurt?",
|
||||||
|
"deleteConfirmationOk": "Po",
|
||||||
|
"deleteConfirmationCancel": "Anulo",
|
||||||
|
"associatedTaskColumn": "Numri i Detyrave të Lidhura",
|
||||||
|
"searchPlaceholder": "Kërko sipas emrit",
|
||||||
|
"emptyText": "Etiketat mund të krijohen gjatë përditësimit ose krijimit të detyrave.",
|
||||||
|
"pinTooltip": "Klikoni për ta fiksuar në menynë kryesore",
|
||||||
|
"colorChangeTooltip": "Klikoni për të ndryshuar ngjyrën"
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"language": "Gjuha",
|
||||||
|
"language_required": "Gjuha është e detyrueshme",
|
||||||
|
"time_zone": "Zona kohore",
|
||||||
|
"time_zone_required": "Zona kohore është e detyrueshme",
|
||||||
|
"save_changes": "Ruaj Ndryshimet"
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"title": "Cilësimet e Njoftimeve",
|
||||||
|
"emailTitle": "Më dërgo njoftime me email",
|
||||||
|
"emailDescription": "Kjo përfshin caktimet e reja të detyrave",
|
||||||
|
"dailyDigestTitle": "Më dërgo një përmbledhje ditore",
|
||||||
|
"dailyDigestDescription": "Çdo mbrëmje, do të merrni një përmbledhje të aktivitetit të fundit në detyra.",
|
||||||
|
"popupTitle": "Shfaq njoftimet në kompjuterin tim kur Worklenz është i hapur",
|
||||||
|
"popupDescription": "Njoftimet e shfaqura mund të çaktivizohen nga shfletuesi juaj. Ndryshoni cilësimet e shfletuesit për t'i lejuar ato.",
|
||||||
|
"unreadItemsTitle": "Shfaq numrin e artikujve të palexuar",
|
||||||
|
"unreadItemsDescription": "Do të shihni numërimin për çdo njoftim."
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"uploadError": "Mund të ngarkoni vetëm skedarë JPG/PNG!",
|
||||||
|
"uploadSizeError": "Imazhi duhet të jetë më i vogël se 2MB!",
|
||||||
|
"upload": "Ngarko",
|
||||||
|
"nameLabel": "Emri",
|
||||||
|
"nameRequiredError": "Emri është i detyrueshëm",
|
||||||
|
"emailLabel": "Email",
|
||||||
|
"emailRequiredError": "Email-i është i detyrueshëm",
|
||||||
|
"saveChanges": "Ruaj Ndryshimet",
|
||||||
|
"profileJoinedText": "U bashkua një muaj më parë",
|
||||||
|
"profileLastUpdatedText": "Përditësuar një muaj më parë",
|
||||||
|
"avatarTooltip": "Klikoni për të ngarkuar një avatar",
|
||||||
|
"title": "Cilësimet e Profilit"
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"nameColumn": "Emri",
|
||||||
|
"editToolTip": "Modifiko",
|
||||||
|
"deleteToolTip": "Fshi",
|
||||||
|
"confirmText": "Jeni i sigurt?",
|
||||||
|
"okText": "Po",
|
||||||
|
"cancelText": "Anulo"
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"profile": "Profili",
|
||||||
|
"notifications": "Njoftimet",
|
||||||
|
"clients": "Klientët",
|
||||||
|
"job-titles": "Tituj Pune",
|
||||||
|
"labels": "Etiketa",
|
||||||
|
"categories": "Kategoritë",
|
||||||
|
"project-templates": "Shabllonet e Projekteve",
|
||||||
|
"task-templates": "Shabllonet e Detyrave",
|
||||||
|
"team-members": "Anëtarët e Ekipit",
|
||||||
|
"teams": "Ekipet",
|
||||||
|
"change-password": "Ndrysho Fjalëkalimin",
|
||||||
|
"language-and-region": "Gjuha dhe Rajoni"
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"nameColumn": "Emri",
|
||||||
|
"createdColumn": "Krijuar",
|
||||||
|
"editToolTip": "Redakto",
|
||||||
|
"deleteToolTip": "Fshi",
|
||||||
|
"confirmText": "Jeni i sigurt?",
|
||||||
|
"okText": "Po",
|
||||||
|
"cancelText": "Anulo"
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
{
|
||||||
|
"title": "Anëtarët e Ekipit",
|
||||||
|
"nameColumn": "Emri",
|
||||||
|
"projectsColumn": "Projektet",
|
||||||
|
"emailColumn": "Email",
|
||||||
|
"teamAccessColumn": "Qasja në Ekip",
|
||||||
|
"memberCount": "Anëtar",
|
||||||
|
"membersCountPlural": "Anëtarë",
|
||||||
|
"searchPlaceholder": "Kërko anëtarë sipas emrit",
|
||||||
|
"pinTooltip": "Rifresko listën e anëtarëve",
|
||||||
|
"addMemberButton": "Shto Anëtar të Ri",
|
||||||
|
"editTooltip": "Modifiko anëtarin",
|
||||||
|
"deactivateTooltip": "Çaktivizo anëtarin",
|
||||||
|
"activateTooltip": "Aktivizo anëtarin",
|
||||||
|
"deleteTooltip": "Fshi anëtarin",
|
||||||
|
"confirmDeleteTitle": "Jeni i sigurt që doni të fshini këtë anëtar?",
|
||||||
|
"confirmActivateTitle": "Jeni i sigurt që doni të ndryshoni statusin e këtij anëtari?",
|
||||||
|
"okText": "Po, vazhdo",
|
||||||
|
"cancelText": "Jo, anulo",
|
||||||
|
"deactivatedText": "(Aktualisht i çaktivizuar)",
|
||||||
|
"pendingInvitationText": "(Ftesë në pritje)",
|
||||||
|
"addMemberDrawerTitle": "Shto Anëtar të Ri në Ekip",
|
||||||
|
"updateMemberDrawerTitle": "Përditëso Anëtarin e Ekipit",
|
||||||
|
"addMemberEmailHint": "Anëtarët do të shtohen në ekip pavarësisht nga statusi i pranimit të ftesës",
|
||||||
|
"memberEmailLabel": "Email(o)",
|
||||||
|
"memberEmailPlaceholder": "Vendos adresën email të anëtarit të ekipit",
|
||||||
|
"memberEmailRequiredError": "Ju lutemi vendosni një adresë email të vlefshme",
|
||||||
|
"jobTitleLabel": "Titulli i Punës",
|
||||||
|
"jobTitlePlaceholder": "Zgjidh ose kërko titull pune (Opsionale)",
|
||||||
|
"memberAccessLabel": "Niveli i Qasjes",
|
||||||
|
"addToTeamButton": "Shto Anëtar në Ekip",
|
||||||
|
"updateButton": "Ruaj Ndryshimet",
|
||||||
|
"resendInvitationButton": "Dërgo Përsëri Email-in e Ftesës",
|
||||||
|
"invitationSentSuccessMessage": "Ftesa për ekip u dërgua me sukses!",
|
||||||
|
"createMemberSuccessMessage": "Anëtari i ri i ekipit u shtua me sukses!",
|
||||||
|
"createMemberErrorMessage": "Dështoi shtimi i anëtarit të ri. Ju lutemi provoni përsëri.",
|
||||||
|
"updateMemberSuccessMessage": "Anëtari i ekipit u përditësua me sukses!",
|
||||||
|
"updateMemberErrorMessage": "Dështoi përditësimi i anëtarit. Ju lutemi provoni përsëri.",
|
||||||
|
"memberText": "Anëtar",
|
||||||
|
"adminText": "Administrues",
|
||||||
|
"ownerText": "Pronar i Ekipit",
|
||||||
|
"addedText": "Shtuar",
|
||||||
|
"updatedText": "Përditësuar",
|
||||||
|
"noResultFound": "Shkruani një adresë email dhe shtypni Enter...",
|
||||||
|
"jobTitlesFetchError": "Dështoi marrja e titujve të punës",
|
||||||
|
"invitationResent": "Ftesa u dërgua sërish me sukses!"
|
||||||
|
}
|
||||||
16
worklenz-backend/src/public/locales/alb/settings/teams.json
Normal file
16
worklenz-backend/src/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!"
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"details": {
|
||||||
|
"task-key": "Çelësi i Detyrës",
|
||||||
|
"phase": "Faza",
|
||||||
|
"assignees": "Përgjegjësit",
|
||||||
|
"due-date": "Data e Përfundimit",
|
||||||
|
"time-estimation": "Vlerësimi i Kohës",
|
||||||
|
"priority": "Prioriteti",
|
||||||
|
"labels": "Etiketa",
|
||||||
|
"billable": "Fakturueshme",
|
||||||
|
"notify": "Njofto",
|
||||||
|
"when-done-notify": "Kur të përfundojë, njofto",
|
||||||
|
"start-date": "Data e Fillimit",
|
||||||
|
"end-date": "Data e Përfundimit",
|
||||||
|
"hide-start-date": "Fshih Datën e Fillimit",
|
||||||
|
"show-start-date": "Shfaq Datën e Fillimit",
|
||||||
|
"hours": "Orë",
|
||||||
|
"minutes": "Minuta"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"title": "Përshkrimi",
|
||||||
|
"placeholder": "Shtoni një përshkrim më të detajuar..."
|
||||||
|
},
|
||||||
|
"subTasks": {
|
||||||
|
"title": "Nën-Detyrat",
|
||||||
|
"add-sub-task": "+ Shto Nën-Detyrë",
|
||||||
|
"refresh-sub-tasks": "Rifresko Nën-Detyrat"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
{
|
||||||
|
"taskHeader": {
|
||||||
|
"taskNamePlaceholder": "Shkruani Detyrën tuaj",
|
||||||
|
"deleteTask": "Fshi Detyrën"
|
||||||
|
},
|
||||||
|
"taskInfoTab": {
|
||||||
|
"title": "Informacioni",
|
||||||
|
"details": {
|
||||||
|
"title": "Detajet",
|
||||||
|
"task-key": "Çelësi i Detyrës",
|
||||||
|
"phase": "Faza",
|
||||||
|
"assignees": "Të Caktuar",
|
||||||
|
"due-date": "Data e Përfundimit",
|
||||||
|
"time-estimation": "Vlerësimi i Kohës",
|
||||||
|
"priority": "Prioriteti",
|
||||||
|
"labels": "Etiketat",
|
||||||
|
"billable": "E Faturueshme",
|
||||||
|
"notify": "Njofto",
|
||||||
|
"when-done-notify": "Kur përfundon, njofto",
|
||||||
|
"start-date": "Data e Fillimit",
|
||||||
|
"end-date": "Data e Përfundimit",
|
||||||
|
"hide-start-date": "Fshih Datën e Fillimit",
|
||||||
|
"show-start-date": "Shfaq Datën e Fillimit",
|
||||||
|
"hours": "Orë",
|
||||||
|
"minutes": "Minuta",
|
||||||
|
"progressValue": "Vlera e Progresit",
|
||||||
|
"progressValueTooltip": "Vendosni përqindjen e progresit (0-100%)",
|
||||||
|
"progressValueRequired": "Ju lutemi vendosni një vlerë progresi",
|
||||||
|
"progressValueRange": "Progresi duhet të jetë midis 0 dhe 100",
|
||||||
|
"taskWeight": "Pesha e Detyrës",
|
||||||
|
"taskWeightTooltip": "Vendosni peshën e kësaj nëndetyre (përqindje)",
|
||||||
|
"taskWeightRequired": "Ju lutemi vendosni një peshë detyre",
|
||||||
|
"taskWeightRange": "Pesha duhet të jetë midis 0 dhe 100",
|
||||||
|
"recurring": "E Përsëritur"
|
||||||
|
},
|
||||||
|
"labels": {
|
||||||
|
"labelInputPlaceholder": "Kërko ose krijo",
|
||||||
|
"labelsSelectorInputTip": "Shtyp Enter për të krijuar"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"title": "Përshkrimi",
|
||||||
|
"placeholder": "Shto një përshkrim më të detajuar..."
|
||||||
|
},
|
||||||
|
"subTasks": {
|
||||||
|
"title": "Nëndetyrat",
|
||||||
|
"addSubTask": "Shto Nëndetyrë",
|
||||||
|
"addSubTaskInputPlaceholder": "Shkruani detyrën tuaj dhe shtypni enter",
|
||||||
|
"refreshSubTasks": "Rifresko Nëndetyrat",
|
||||||
|
"edit": "Modifiko",
|
||||||
|
"delete": "Fshi",
|
||||||
|
"confirmDeleteSubTask": "Jeni i sigurt që doni të fshini këtë nëndetyrë?",
|
||||||
|
"deleteSubTask": "Fshi Nëndetyrën"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"title": "Varësitë",
|
||||||
|
"addDependency": "+ Shto varësi të re",
|
||||||
|
"blockedBy": "Bllokuar nga",
|
||||||
|
"searchTask": "Shkruani për të kërkuar detyrë",
|
||||||
|
"noTasksFound": "Nuk u gjetën detyra",
|
||||||
|
"confirmDeleteDependency": "Jeni i sigurt që doni të fshini?"
|
||||||
|
},
|
||||||
|
"attachments": {
|
||||||
|
"title": "Bashkëngjitjet",
|
||||||
|
"chooseOrDropFileToUpload": "Zgjidhni ose hidhni skedar për të ngarkuar",
|
||||||
|
"uploading": "Duke ngarkuar..."
|
||||||
|
},
|
||||||
|
"comments": {
|
||||||
|
"title": "Komentet",
|
||||||
|
"addComment": "+ Shto koment të ri",
|
||||||
|
"noComments": "Ende pa komente. Bëhu i pari që komenton!",
|
||||||
|
"delete": "Fshi",
|
||||||
|
"confirmDeleteComment": "Jeni i sigurt që doni të fshini këtë koment?",
|
||||||
|
"addCommentPlaceholder": "Shto një koment...",
|
||||||
|
"cancel": "Anulo",
|
||||||
|
"commentButton": "Komento",
|
||||||
|
"attachFiles": "Bashkëngjit skedarë",
|
||||||
|
"addMoreFiles": "Shto më shumë skedarë",
|
||||||
|
"selectedFiles": "Skedarët e Zgjedhur (Deri në 25MB, Maksimumi {count})",
|
||||||
|
"maxFilesError": "Mund të ngarkoni maksimum {count} skedarë",
|
||||||
|
"processFilesError": "Dështoi përpunimi i skedarëve",
|
||||||
|
"addCommentError": "Ju lutemi shtoni një koment ose bashkëngjitni skedarë",
|
||||||
|
"createdBy": "Krijuar {{time}} nga {{user}}",
|
||||||
|
"updatedTime": "Përditësuar {{time}}"
|
||||||
|
},
|
||||||
|
"searchInputPlaceholder": "Kërko sipas emrit",
|
||||||
|
"pendingInvitation": "Ftesë në Pritje"
|
||||||
|
},
|
||||||
|
"taskTimeLogTab": {
|
||||||
|
"title": "Regjistri i Kohës",
|
||||||
|
"addTimeLog": "Shto regjistrim të ri kohe",
|
||||||
|
"totalLogged": "Totali i Regjistruar",
|
||||||
|
"exportToExcel": "Eksporto në Excel",
|
||||||
|
"noTimeLogsFound": "Nuk u gjetën regjistra kohe",
|
||||||
|
"timeLogForm": {
|
||||||
|
"date": "Data",
|
||||||
|
"startTime": "Koha e Fillimit",
|
||||||
|
"endTime": "Koha e Përfundimit",
|
||||||
|
"workDescription": "Përshkrimi i Punës",
|
||||||
|
"descriptionPlaceholder": "Shto një përshkrim",
|
||||||
|
"logTime": "Regjistro kohën",
|
||||||
|
"updateTime": "Përditëso kohën",
|
||||||
|
"cancel": "Anulo",
|
||||||
|
"selectDateError": "Ju lutemi zgjidhni një datë",
|
||||||
|
"selectStartTimeError": "Ju lutemi zgjidhni kohën e fillimit",
|
||||||
|
"selectEndTimeError": "Ju lutemi zgjidhni kohën e përfundimit",
|
||||||
|
"endTimeAfterStartError": "Koha e përfundimit duhet të jetë pas kohës së fillimit"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"taskActivityLogTab": {
|
||||||
|
"title": "Regjistri i Aktivitetit",
|
||||||
|
"add": "SHTO",
|
||||||
|
"remove": "HIQE",
|
||||||
|
"none": "Asnjë",
|
||||||
|
"weight": "Pesha",
|
||||||
|
"createdTask": "krijoi detyrën."
|
||||||
|
},
|
||||||
|
"taskProgress": {
|
||||||
|
"markAsDoneTitle": "Shëno Detyrën si të Kryer?",
|
||||||
|
"confirmMarkAsDone": "Po, shëno si të kryer",
|
||||||
|
"cancelMarkAsDone": "Jo, mbaj statusin aktual",
|
||||||
|
"markAsDoneDescription": "Keni vendosur progresin në 100%. Doni të përditësoni statusin e detyrës në \"Kryer\"?"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
{
|
||||||
|
"searchButton": "Kërko",
|
||||||
|
"resetButton": "Rivendos",
|
||||||
|
"searchInputPlaceholder": "Kërko sipas emrit",
|
||||||
|
|
||||||
|
"sortText": "Rendit",
|
||||||
|
"statusText": "Statusi",
|
||||||
|
"phaseText": "Faza",
|
||||||
|
"memberText": "Anëtarët",
|
||||||
|
"assigneesText": "Përgjegjësit",
|
||||||
|
"priorityText": "Prioriteti",
|
||||||
|
"labelsText": "Etiketa",
|
||||||
|
"membersText": "Anëtarët",
|
||||||
|
"groupByText": "Grupo sipas",
|
||||||
|
"showArchivedText": "Shfaq të arkivuara",
|
||||||
|
"showFieldsText": "Shfaq fushat",
|
||||||
|
"keyText": "Çelësi",
|
||||||
|
"taskText": "Detyra",
|
||||||
|
"descriptionText": "Përshkrimi",
|
||||||
|
"phasesText": "Fazat",
|
||||||
|
"listText": "Listë",
|
||||||
|
"progressText": "Progresi",
|
||||||
|
"timeTrackingText": "Gjurmimi i Kohës",
|
||||||
|
"timetrackingText": "Gjurmimi i Kohës",
|
||||||
|
"estimationText": "Vlerësimi",
|
||||||
|
"startDateText": "Data e Fillimit",
|
||||||
|
"startdateText": "Data e Fillimit",
|
||||||
|
"endDateText": "Data e Përfundimit",
|
||||||
|
"dueDateText": "Afati",
|
||||||
|
"duedateText": "Afati",
|
||||||
|
"completedDateText": "Data e Përfundimit",
|
||||||
|
"completeddateText": "Data e Përfundimit",
|
||||||
|
"createdDateText": "Data e Krijimit",
|
||||||
|
"createddateText": "Data e Krijimit",
|
||||||
|
"lastUpdatedText": "Përditësuar Së Fundi",
|
||||||
|
"lastupdatedText": "Përditësuar Së Fundi",
|
||||||
|
"reporterText": "Raportuesi",
|
||||||
|
"dueTimeText": "Koha e Afatit",
|
||||||
|
"duetimeText": "Koha e Afatit",
|
||||||
|
|
||||||
|
"lowText": "I ulët",
|
||||||
|
"mediumText": "I mesëm",
|
||||||
|
"highText": "I lartë",
|
||||||
|
|
||||||
|
"createStatusButtonTooltip": "Cilësimet e statusit",
|
||||||
|
"configPhaseButtonTooltip": "Cilësimet e fazës",
|
||||||
|
"noLabelsFound": "Nuk u gjetën etiketa",
|
||||||
|
|
||||||
|
"addStatusButton": "Shto Status",
|
||||||
|
"addPhaseButton": "Shto Fazë",
|
||||||
|
|
||||||
|
"createStatus": "Krijo Status",
|
||||||
|
"name": "Emri",
|
||||||
|
"category": "Kategoria",
|
||||||
|
"selectCategory": "Zgjidh një kategori",
|
||||||
|
"pleaseEnterAName": "Ju lutemi vendosni një emër",
|
||||||
|
"pleaseSelectACategory": "Ju lutemi zgjidhni një kategori",
|
||||||
|
"create": "Krijo",
|
||||||
|
|
||||||
|
"searchTasks": "Kërko detyrat...",
|
||||||
|
"searchPlaceholder": "Kërko...",
|
||||||
|
"fieldsText": "Fushat",
|
||||||
|
"loadingFilters": "Duke ngarkuar filtrat...",
|
||||||
|
"noOptionsFound": "Nuk u gjetën opsione",
|
||||||
|
"filtersActive": "filtra aktiv",
|
||||||
|
"filterActive": "filtër aktiv",
|
||||||
|
"clearAll": "Pastro të gjitha",
|
||||||
|
"clearing": "Duke pastruar...",
|
||||||
|
"cancel": "Anulo",
|
||||||
|
"search": "Kërko",
|
||||||
|
"groupedBy": "Grupuar sipas",
|
||||||
|
"manageStatuses": "Menaxho Statuset",
|
||||||
|
"managePhases": "Menaxho Fazat",
|
||||||
|
"dragToReorderStatuses": "Zvarrit statuset për t'i rirenditur. Çdo status mund të ketë një kategori të ndryshme.",
|
||||||
|
"enterNewStatusName": "Shkruani emrin e statusit të ri...",
|
||||||
|
"addStatus": "Shto Status",
|
||||||
|
"noStatusesFound": "Nuk u gjetën statuse. Krijoni statusin tuaj të parë më sipër.",
|
||||||
|
"deleteStatus": "Fshi Statusin",
|
||||||
|
"deleteStatusConfirm": "Jeni të sigurt që doni të fshini këtë status? Ky veprim nuk mund të zhbëhet.",
|
||||||
|
"rename": "Riemëro",
|
||||||
|
"delete": "Fshi",
|
||||||
|
"enterStatusName": "Shkruani emrin e statusit",
|
||||||
|
"selectCategory": "Zgjidh kategorinë",
|
||||||
|
"close": "Mbyll"
|
||||||
|
}
|
||||||
136
worklenz-backend/src/public/locales/alb/task-list-table.json
Normal file
136
worklenz-backend/src/public/locales/alb/task-list-table.json
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
{
|
||||||
|
"keyColumn": "Çelësi",
|
||||||
|
"taskColumn": "Detyra",
|
||||||
|
"descriptionColumn": "Përshkrimi",
|
||||||
|
"progressColumn": "Progresi",
|
||||||
|
"membersColumn": "Anëtarët",
|
||||||
|
"assigneesColumn": "Përgjegjësit",
|
||||||
|
"labelsColumn": "Etiketa",
|
||||||
|
"phasesColumn": "Fazat",
|
||||||
|
"phaseColumn": "Faza",
|
||||||
|
"statusColumn": "Statusi",
|
||||||
|
"priorityColumn": "Prioriteti",
|
||||||
|
"timeTrackingColumn": "Gjurmimi i Kohës",
|
||||||
|
"timetrackingColumn": "Gjurmimi i Kohës",
|
||||||
|
"estimationColumn": "Vlerësimi",
|
||||||
|
"startDateColumn": "Data e Fillimit",
|
||||||
|
"startdateColumn": "Data e Fillimit",
|
||||||
|
"dueDateColumn": "Data e Afatit",
|
||||||
|
"duedateColumn": "Data e Afatit",
|
||||||
|
"completedDateColumn": "Data e Përfundimit",
|
||||||
|
"completeddateColumn": "Data e Përfundimit",
|
||||||
|
"createdDateColumn": "Data e Krijimit",
|
||||||
|
"createddateColumn": "Data e Krijimit",
|
||||||
|
"lastUpdatedColumn": "Përditësuar Së Fundi",
|
||||||
|
"lastupdatedColumn": "Përditësuar Së Fundi",
|
||||||
|
"reporterColumn": "Raportuesi",
|
||||||
|
"dueTimeColumn": "Koha e Afatit",
|
||||||
|
"todoSelectorText": "Për të Bërë",
|
||||||
|
"doingSelectorText": "Duke bërë",
|
||||||
|
"doneSelectorText": "E Përfunduar",
|
||||||
|
|
||||||
|
"lowSelectorText": "I ulët",
|
||||||
|
"mediumSelectorText": "I mesëm",
|
||||||
|
"highSelectorText": "I lartë",
|
||||||
|
|
||||||
|
"selectText": "Zgjidh",
|
||||||
|
"labelsSelectorInputTip": "Shtyp Enter për të krijuar!",
|
||||||
|
|
||||||
|
"addTaskText": "Shto Detyrë",
|
||||||
|
"addSubTaskText": "+ Shto Nën-Detyrë",
|
||||||
|
"noTasksInGroup": "Nuk ka detyra në këtë grup",
|
||||||
|
"addTaskInputPlaceholder": "Shkruaj detyrën dhe shtyp Enter",
|
||||||
|
|
||||||
|
"openButton": "Hap",
|
||||||
|
"okButton": "Në rregull",
|
||||||
|
|
||||||
|
"noLabelsFound": "Nuk u gjetën etiketa",
|
||||||
|
"searchInputPlaceholder": "Kërko ose krijo",
|
||||||
|
"assigneeSelectorInviteButton": "Fto një anëtar të ri me email",
|
||||||
|
"labelInputPlaceholder": "Kërko ose krijo",
|
||||||
|
"searchLabelsPlaceholder": "Kërko etiketa...",
|
||||||
|
"createLabelButton": "Krijo \"{{name}}\"",
|
||||||
|
"manageLabelsPath": "Cilësimet → Etiketat",
|
||||||
|
|
||||||
|
"pendingInvitation": "Ftesë në Pritje",
|
||||||
|
|
||||||
|
"contextMenu": {
|
||||||
|
"assignToMe": "Cakto mua",
|
||||||
|
"moveTo": "Zhvendos në",
|
||||||
|
"unarchive": "Ç'arkivizo",
|
||||||
|
"archive": "Arkivizo",
|
||||||
|
"convertToSubTask": "Shndërro në Nën-Detyrë",
|
||||||
|
"convertToTask": "Shndërro në Detyrë",
|
||||||
|
"delete": "Fshi",
|
||||||
|
"searchByNameInputPlaceholder": "Kërko sipas emrit"
|
||||||
|
},
|
||||||
|
"setDueDate": "Cakto datën e afatit",
|
||||||
|
"setStartDate": "Cakto datën e fillimit",
|
||||||
|
"clearDueDate": "Pastro datën e afatit",
|
||||||
|
"clearStartDate": "Pastro datën e fillimit",
|
||||||
|
"dueDatePlaceholder": "Data e afatit",
|
||||||
|
"startDatePlaceholder": "Data e fillimit",
|
||||||
|
|
||||||
|
"emptyStates": {
|
||||||
|
"noTaskGroups": "Nuk u gjetën grupe detyrash",
|
||||||
|
"noTaskGroupsDescription": "Detyrat do të shfaqen këtu kur krijohen ose kur aplikohen filtra.",
|
||||||
|
"errorPrefix": "Gabim:",
|
||||||
|
"dragTaskFallback": "Detyrë"
|
||||||
|
},
|
||||||
|
|
||||||
|
"customColumns": {
|
||||||
|
"addCustomColumn": "Shto një kolonë të personalizuar",
|
||||||
|
"customColumnHeader": "Kolona e Personalizuar",
|
||||||
|
"customColumnSettings": "Cilësimet e kolonës së personalizuar",
|
||||||
|
"noCustomValue": "Asnjë vlerë",
|
||||||
|
"peopleField": "Fusha e njerëzve",
|
||||||
|
"noDate": "Asnjë datë",
|
||||||
|
"unsupportedField": "Lloj fushe i pambështetur",
|
||||||
|
|
||||||
|
"modal": {
|
||||||
|
"addFieldTitle": "Shto fushë",
|
||||||
|
"editFieldTitle": "Redakto fushën",
|
||||||
|
"fieldTitle": "Titulli i fushës",
|
||||||
|
"fieldTitleRequired": "Titulli i fushës është i kërkuar",
|
||||||
|
"columnTitlePlaceholder": "Titulli i kolonës",
|
||||||
|
"type": "Lloji",
|
||||||
|
"deleteConfirmTitle": "Jeni i sigurt që doni të fshini këtë kolonë të personalizuar?",
|
||||||
|
"deleteConfirmDescription": "Kjo veprim nuk mund të zhbëhet. Të gjitha të dhënat e lidhura me këtë kolonë do të fshihen përgjithmonë.",
|
||||||
|
"deleteButton": "Fshi",
|
||||||
|
"cancelButton": "Anulo",
|
||||||
|
"createButton": "Krijo",
|
||||||
|
"updateButton": "Përditëso",
|
||||||
|
"createSuccessMessage": "Kolona e personalizuar u krijua me sukses",
|
||||||
|
"updateSuccessMessage": "Kolona e personalizuar u përditësua me sukses",
|
||||||
|
"deleteSuccessMessage": "Kolona e personalizuar u fshi me sukses",
|
||||||
|
"deleteErrorMessage": "Dështoi në fshirjen e kolonës së personalizuar",
|
||||||
|
"createErrorMessage": "Dështoi në krijimin e kolonës së personalizuar",
|
||||||
|
"updateErrorMessage": "Dështoi në përditësimin e kolonës së personalizuar"
|
||||||
|
},
|
||||||
|
|
||||||
|
"fieldTypes": {
|
||||||
|
"people": "Njerëz",
|
||||||
|
"number": "Numër",
|
||||||
|
"date": "Data",
|
||||||
|
"selection": "Zgjedhje",
|
||||||
|
"checkbox": "Kutia e kontrollit",
|
||||||
|
"labels": "Etiketat",
|
||||||
|
"key": "Çelësi",
|
||||||
|
"formula": "Formula"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"indicators": {
|
||||||
|
"tooltips": {
|
||||||
|
"subtasks": "{{count}} nën-detyrë",
|
||||||
|
"subtasks_plural": "{{count}} nën-detyra",
|
||||||
|
"comments": "{{count}} koment",
|
||||||
|
"comments_plural": "{{count}} komente",
|
||||||
|
"attachments": "{{count}} bashkëngjitje",
|
||||||
|
"attachments_plural": "{{count}} bashkëngjitje",
|
||||||
|
"subscribers": "Detyra ka pajtues",
|
||||||
|
"dependencies": "Detyra ka varësi",
|
||||||
|
"recurring": "Detyrë përsëritëse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
21
worklenz-backend/src/public/locales/alb/task-management.json
Normal file
21
worklenz-backend/src/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"
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"createTaskTemplate": "Krijo Shabllon Detyre",
|
||||||
|
"editTaskTemplate": "Modifiko Shabllon Detyre",
|
||||||
|
"cancelText": "Anulo",
|
||||||
|
"saveText": "Ruaj",
|
||||||
|
"templateNameText": "Emri i Shabllonit",
|
||||||
|
"selectedTasks": "Detyrat e Përzgjedhura",
|
||||||
|
"removeTask": "Hiq",
|
||||||
|
"cancelButton": "Anulo",
|
||||||
|
"saveButton": "Ruaj"
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"taskSelected": "detyrë e zgjedhur",
|
||||||
|
"tasksSelected": "detyra të zgjedhura",
|
||||||
|
"changeStatus": "Ndrysho Statusin/ Prioritetin/ Fazat",
|
||||||
|
"changeLabel": "Ndrysho Etiketën",
|
||||||
|
"assignToMe": "Cakto mua",
|
||||||
|
"changeAssignees": "Ndrysho Përgjegjësit",
|
||||||
|
"archive": "Arkivo",
|
||||||
|
"unarchive": "Ç'arkivo",
|
||||||
|
"delete": "Fshi",
|
||||||
|
"moreOptions": "Më shumë opsione",
|
||||||
|
"deselectAll": "Zgjidhja të gjitha",
|
||||||
|
"status": "Statusi",
|
||||||
|
"priority": "Prioriteti",
|
||||||
|
"phase": "Faza",
|
||||||
|
"member": "Anëtar",
|
||||||
|
"createTaskTemplate": "Krijo Shabllon Detyre",
|
||||||
|
"apply": "Apliko",
|
||||||
|
"createLabel": "+ Krijo Etiketë",
|
||||||
|
"searchOrCreateLabel": "Kërko ose krijo etiketë...",
|
||||||
|
"hitEnterToCreate": "Shtyp Enter për të krijuar",
|
||||||
|
"labelExists": "Etiketa ekziston tashmë",
|
||||||
|
"pendingInvitation": "Ftesë në Pritje",
|
||||||
|
"noMatchingLabels": "Asnjë etiketë që përputhet",
|
||||||
|
"noLabels": "Asnjë etiketë"
|
||||||
|
}
|
||||||
19
worklenz-backend/src/public/locales/alb/template-drawer.json
Normal file
19
worklenz-backend/src/public/locales/alb/template-drawer.json
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"title": "Modifiko Shabllon Detyre",
|
||||||
|
"cancelText": "Anulo",
|
||||||
|
"saveText": "Ruaj",
|
||||||
|
"templateNameText": "Emri i Shabllonit",
|
||||||
|
"selectedTasks": "Detyrat e Përzgjedhura",
|
||||||
|
"removeTask": "Hiq",
|
||||||
|
"description": "Përshkrimi",
|
||||||
|
"phase": "Faza",
|
||||||
|
"statuses": "Statuset",
|
||||||
|
"priorities": "Prioritetet",
|
||||||
|
"labels": "Etiketa",
|
||||||
|
"tasks": "Detyrat",
|
||||||
|
"noTemplateSelected": "Asnjë shabllon i përzgjedhur",
|
||||||
|
"noDescription": "Pa përshkrim",
|
||||||
|
"worklenzTemplates": "Shabllonet Worklenz",
|
||||||
|
"yourTemplatesLibrary": "Biblioteka Juaj",
|
||||||
|
"searchTemplates": "Kërko Shabllone"
|
||||||
|
}
|
||||||
23
worklenz-backend/src/public/locales/alb/templateDrawer.json
Normal file
23
worklenz-backend/src/public/locales/alb/templateDrawer.json
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"bugTracking": "Gjurmimi i Gabimeve",
|
||||||
|
"construction": "Ndërtim",
|
||||||
|
"designCreative": "Dizajn & Kreativ",
|
||||||
|
"education": "Arsim",
|
||||||
|
"finance": "Financë",
|
||||||
|
"hrRecruiting": "Burime Njerëzore & Rekrutim",
|
||||||
|
"informationTechnology": "Teknologji Informacioni",
|
||||||
|
"legal": "Juridik",
|
||||||
|
"manufacturing": "Prodhim",
|
||||||
|
"marketing": "Marketing",
|
||||||
|
"nonprofit": "Jo-fitimprurës",
|
||||||
|
"personalUse": "Përdorim Personal",
|
||||||
|
"salesCRM": "Shitje & CRM",
|
||||||
|
"serviceConsulting": "Shërbime & Këshillim",
|
||||||
|
"softwareDevelopment": "Zhvillim Softueri",
|
||||||
|
"description": "Përshkrimi",
|
||||||
|
"phase": "Faza",
|
||||||
|
"statuses": "Statuset",
|
||||||
|
"priorities": "Prioritetet",
|
||||||
|
"labels": "Etiketa",
|
||||||
|
"tasks": "Detyrat"
|
||||||
|
}
|
||||||
44
worklenz-backend/src/public/locales/alb/time-report.json
Normal file
44
worklenz-backend/src/public/locales/alb/time-report.json
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
{
|
||||||
|
"includeArchivedProjects": "Përfshij Projektet e Arkivuara",
|
||||||
|
"export": "Eksporto",
|
||||||
|
"timeSheet": "Fletë Kohore",
|
||||||
|
|
||||||
|
"searchByName": "Kërko sipas emrit",
|
||||||
|
"selectAll": "Zgjidh të Gjitha",
|
||||||
|
"teams": "Ekipet",
|
||||||
|
|
||||||
|
"searchByProject": "Kërko sipas emrit të projektit",
|
||||||
|
"projects": "Projektet",
|
||||||
|
|
||||||
|
"searchByCategory": "Kërko sipas emrit të kategorisë",
|
||||||
|
"categories": "Kategoritë",
|
||||||
|
|
||||||
|
"billable": "Fakturueshme",
|
||||||
|
"nonBillable": "Jo Fakturueshme",
|
||||||
|
|
||||||
|
"total": "Total",
|
||||||
|
|
||||||
|
"projectsTimeSheet": "Fletë Kohore e Projekteve",
|
||||||
|
|
||||||
|
"loggedTime": "Koha e Regjistruar(orë)",
|
||||||
|
|
||||||
|
"exportToExcel": "Eksporto në Excel",
|
||||||
|
"logged": "regjistruar",
|
||||||
|
"for": "për",
|
||||||
|
|
||||||
|
"membersTimeSheet": "Fletë Kohore e Anëtarëve",
|
||||||
|
"member": "Anëtar",
|
||||||
|
|
||||||
|
"estimatedVsActual": "Vlerësuar vs Aktual",
|
||||||
|
"workingDays": "Ditë Pune",
|
||||||
|
"manDays": "Ditë Njeri",
|
||||||
|
"days": "Ditë",
|
||||||
|
"estimatedDays": "Ditë të Vlerësuara",
|
||||||
|
"actualDays": "Ditë Aktuale",
|
||||||
|
|
||||||
|
"noCategories": "Nuk u gjetën kategori",
|
||||||
|
"noCategory": "Pa Kategori",
|
||||||
|
"noProjects": "Nuk u gjetën projekte",
|
||||||
|
"noTeams": "Nuk u gjetën ekipe",
|
||||||
|
"noData": "Nuk u gjetën të dhëna"
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"title": "E paautorizuar!",
|
||||||
|
"subtitle": "Nuk jeni të autorizuar të hyni në këtë faqe",
|
||||||
|
"button": "Kthehu në Faqen Kryesore"
|
||||||
|
}
|
||||||
4
worklenz-backend/src/public/locales/de/404-page.json
Normal file
4
worklenz-backend/src/public/locales/de/404-page.json
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"doesNotExistText": "Entschuldigung, die von Ihnen besuchte Seite existiert nicht.",
|
||||||
|
"backHomeButton": "Zurück zur Startseite"
|
||||||
|
}
|
||||||
31
worklenz-backend/src/public/locales/de/account-setup.json
Normal file
31
worklenz-backend/src/public/locales/de/account-setup.json
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
{
|
||||||
|
"continue": "Weiter",
|
||||||
|
|
||||||
|
"setupYourAccount": "Richten Sie Ihr Worklenz-Konto ein.",
|
||||||
|
"organizationStepTitle": "Organisation benennen",
|
||||||
|
"organizationStepLabel": "Wählen Sie einen Namen für Ihr Worklenz-Konto.",
|
||||||
|
|
||||||
|
"projectStepTitle": "Erstellen Sie Ihr erstes Projekt",
|
||||||
|
"projectStepLabel": "An welchem Projekt arbeiten Sie gerade?",
|
||||||
|
"projectStepPlaceholder": "z.B. Marketingplan",
|
||||||
|
|
||||||
|
"tasksStepTitle": "Erstellen Sie Ihre ersten Aufgaben",
|
||||||
|
"tasksStepLabel": "Geben Sie einige Aufgaben ein, die Sie in",
|
||||||
|
"tasksStepAddAnother": "Weitere hinzufügen",
|
||||||
|
|
||||||
|
"emailPlaceholder": "E-Mail-Adresse",
|
||||||
|
"invalidEmail": "Bitte geben Sie eine gültige E-Mail-Adresse ein",
|
||||||
|
"or": "oder",
|
||||||
|
"templateButton": "Aus Vorlage importieren",
|
||||||
|
"goBack": "Zurück",
|
||||||
|
"cancel": "Abbrechen",
|
||||||
|
"create": "Erstellen",
|
||||||
|
"templateDrawerTitle": "Aus Vorlagen auswählen",
|
||||||
|
"step3InputLabel": "Per E-Mail einladen",
|
||||||
|
"addAnother": "Weitere hinzufügen",
|
||||||
|
"skipForNow": "Jetzt überspringen",
|
||||||
|
"formTitle": "Erstellen Sie Ihre erste Aufgabe.",
|
||||||
|
"step3Title": "Laden Sie Ihr Team zur Zusammenarbeit ein",
|
||||||
|
"maxMembers": " (Sie können bis zu 5 Mitglieder einladen)",
|
||||||
|
"maxTasks": " (Sie können bis zu 5 Aufgaben erstellen)"
|
||||||
|
}
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
{
|
||||||
|
"title": "Abrechnungen",
|
||||||
|
"currentBill": "Aktuelle Rechnung",
|
||||||
|
"configuration": "Konfiguration",
|
||||||
|
"currentPlanDetails": "Aktuelle Plan Details",
|
||||||
|
"upgradePlan": "Plan upgraden",
|
||||||
|
"cardBodyText01": "Kostenlose Testversion",
|
||||||
|
"cardBodyText02": "(Ihr Testplan läuft in 1 Monat 19 Tagen ab)",
|
||||||
|
"redeemCode": "Gutscheincode einlösen",
|
||||||
|
"accountStorage": "Kontospeicher",
|
||||||
|
"used": "Verwendet:",
|
||||||
|
"remaining": "Verbleibend:",
|
||||||
|
"charges": "Gebühren",
|
||||||
|
"tooltip": "Gebühren für den aktuellen Abrechnungszeitraum",
|
||||||
|
"description": "Beschreibung",
|
||||||
|
"billingPeriod": "Abrechnungszeitraum",
|
||||||
|
"billStatus": "Rechnungsstatus",
|
||||||
|
"perUserValue": "Pro Benutzer Wert",
|
||||||
|
"users": "Benutzer",
|
||||||
|
|
||||||
|
"amount": "Betrag",
|
||||||
|
"invoices": "Rechnungen",
|
||||||
|
"transactionId": "Transaktions-ID",
|
||||||
|
"transactionDate": "Transaktionsdatum",
|
||||||
|
"paymentMethod": "Zahlungsmethode",
|
||||||
|
"status": "Status",
|
||||||
|
"ltdUsers": "Sie können bis zu {{ltd_users}} Benutzer hinzufügen.",
|
||||||
|
|
||||||
|
"totalSeats": "Gesamte Plätze",
|
||||||
|
"availableSeats": "Verfügbare Plätze",
|
||||||
|
"addMoreSeats": "Weitere Plätze hinzufügen",
|
||||||
|
|
||||||
|
"drawerTitle": "Gutscheincode einlösen",
|
||||||
|
"label": "Gutscheincode",
|
||||||
|
"drawerPlaceholder": "Geben Sie Ihren Gutscheincode ein",
|
||||||
|
"redeemSubmit": "Einreichen",
|
||||||
|
|
||||||
|
"modalTitle": "Wählen Sie den besten Plan für Ihr Team",
|
||||||
|
"seatLabel": "Anzahl der Plätze",
|
||||||
|
"freePlan": "Kostenloser Plan",
|
||||||
|
"startup": "Startup",
|
||||||
|
"business": "Business",
|
||||||
|
"tag": "Am beliebtesten",
|
||||||
|
"enterprise": "Enterprise",
|
||||||
|
|
||||||
|
"freeSubtitle": "kostenlos für immer",
|
||||||
|
"freeUsers": "Ideal für die persönliche Nutzung",
|
||||||
|
"freeText01": "100MB Speicher",
|
||||||
|
"freeText02": "3 Projekte",
|
||||||
|
"freeText03": "5 Teammitglieder",
|
||||||
|
|
||||||
|
"startupSubtitle": "PAUSCHALPREIS / Monat",
|
||||||
|
"startupUsers": "Bis zu 15 Benutzer",
|
||||||
|
"startupText01": "25GB Speicher",
|
||||||
|
"startupText02": "Unbegrenzte aktive Projekte",
|
||||||
|
"startupText03": "Zeitplan",
|
||||||
|
"startupText04": "Berichterstattung",
|
||||||
|
"startupText05": "Projekte abonnieren",
|
||||||
|
|
||||||
|
"businessSubtitle": "Benutzer / Monat",
|
||||||
|
"businessUsers": "16 - 200 Benutzer",
|
||||||
|
|
||||||
|
"enterpriseUsers": "200 - 500+ Benutzer",
|
||||||
|
|
||||||
|
"footerTitle": "Bitte geben Sie uns eine Kontaktnummer, unter der wir Sie erreichen können.",
|
||||||
|
"footerLabel": "Kontaktnummer",
|
||||||
|
"footerButton": "Kontaktieren Sie uns",
|
||||||
|
|
||||||
|
"redeemCodePlaceHolder": "Geben Sie Ihren Gutscheincode ein",
|
||||||
|
"submit": "Einreichen",
|
||||||
|
|
||||||
|
"trialPlan": "Kostenlose Testversion",
|
||||||
|
"trialExpireDate": "Gültig bis {{trial_expire_date}}",
|
||||||
|
"trialExpired": "Ihre kostenlose Testversion ist {{trial_expire_string}} abgelaufen",
|
||||||
|
"trialInProgress": "Ihre kostenlose Testversion läuft {{trial_expire_string}} ab",
|
||||||
|
|
||||||
|
"required": "Dieses Feld ist erforderlich",
|
||||||
|
"invalidCode": "Ungültiger Code",
|
||||||
|
|
||||||
|
"selectPlan": "Wählen Sie den besten Plan für Ihr Team",
|
||||||
|
"changeSubscriptionPlan": "Ändern Sie Ihren Abonnementplan",
|
||||||
|
"noOfSeats": "Anzahl der Plätze",
|
||||||
|
"annualPlan": "Pro - Jährlich",
|
||||||
|
"monthlyPlan": "Pro - Monatlich",
|
||||||
|
"freeForever": "Kostenlos für immer",
|
||||||
|
"bestForPersonalUse": "Ideal für die persönliche Nutzung",
|
||||||
|
"storage": "Speicher",
|
||||||
|
"projects": "Projekte",
|
||||||
|
"teamMembers": "Teammitglieder",
|
||||||
|
"unlimitedTeamMembers": "Unbegrenzte Teammitglieder",
|
||||||
|
"unlimitedActiveProjects": "Unbegrenzte aktive Projekte",
|
||||||
|
"schedule": "Zeitplan",
|
||||||
|
"reporting": "Berichterstattung",
|
||||||
|
"subscribeToProjects": "Projekte abonnieren",
|
||||||
|
"billedAnnually": "Jährlich abgerechnet",
|
||||||
|
"billedMonthly": "Monatlich abgerechnet",
|
||||||
|
|
||||||
|
"pausePlan": "Plan pausieren",
|
||||||
|
"resumePlan": "Plan fortsetzen",
|
||||||
|
"changePlan": "Plan ändern",
|
||||||
|
"cancelPlan": "Plan kündigen",
|
||||||
|
|
||||||
|
"perMonthPerUser": "pro Benutzer/Monat",
|
||||||
|
"viewInvoice": "Rechnung anzeigen",
|
||||||
|
"switchToFreePlan": "Wechsel zum kostenlosen Plan",
|
||||||
|
|
||||||
|
"expirestoday": "heute",
|
||||||
|
"expirestomorrow": "morgen",
|
||||||
|
"expiredDaysAgo": "vor {{days}} Tagen",
|
||||||
|
|
||||||
|
"continueWith": "Fortfahren mit {{plan}}",
|
||||||
|
"changeToPlan": "Wechseln zu {{plan}}"
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"overview": "Übersicht",
|
||||||
|
"name": "Organisationsname",
|
||||||
|
"owner": "Organisationsinhaber",
|
||||||
|
"admins": "Organisationsadministratoren",
|
||||||
|
"contactNumber": "Kontaktnummer hinzufügen",
|
||||||
|
"edit": "Bearbeiten"
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"membersCount": "Mitgliederanzahl",
|
||||||
|
"createdAt": "Erstellt am",
|
||||||
|
"projectName": "Projektname",
|
||||||
|
"teamName": "Teamname",
|
||||||
|
"refreshProjects": "Projekte aktualisieren",
|
||||||
|
"searchPlaceholder": "Nach Projektname suchen",
|
||||||
|
"deleteProject": "Sind Sie sicher, dass Sie dieses Projekt löschen möchten?",
|
||||||
|
"confirm": "Bestätigen",
|
||||||
|
"cancel": "Abbrechen",
|
||||||
|
"delete": "Projekt löschen"
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"overview": "Übersicht",
|
||||||
|
"users": "Benutzer",
|
||||||
|
"teams": "Teams",
|
||||||
|
"billing": "Abrechnung",
|
||||||
|
"projects": "Projekte",
|
||||||
|
"adminCenter": "Admin-Center"
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"title": "Teams",
|
||||||
|
"subtitle": "Teams",
|
||||||
|
"tooltip": "Teams aktualisieren",
|
||||||
|
"placeholder": "Nach Namen suchen",
|
||||||
|
"addTeam": "Team hinzufügen",
|
||||||
|
"team": "Team",
|
||||||
|
"membersCount": "Mitgliederanzahl",
|
||||||
|
"members": "Mitglieder",
|
||||||
|
"drawerTitle": "Neues Team erstellen",
|
||||||
|
"label": "Teamname",
|
||||||
|
"drawerPlaceholder": "Name",
|
||||||
|
"create": "Erstellen",
|
||||||
|
"delete": "Löschen",
|
||||||
|
"settings": "Einstellungen",
|
||||||
|
"popTitle": "Sind Sie sicher?",
|
||||||
|
"message": "Bitte geben Sie einen Namen ein",
|
||||||
|
"teamSettings": "Team-Einstellungen",
|
||||||
|
"teamName": "Teamname",
|
||||||
|
"teamDescription": "Teambeschreibung",
|
||||||
|
"teamMembers": "Teammitglieder",
|
||||||
|
"teamMembersCount": "Anzahl der Teammitglieder",
|
||||||
|
"teamMembersPlaceholder": "Nach Namen suchen",
|
||||||
|
"addMember": "Mitglied hinzufügen",
|
||||||
|
"add": "Hinzufügen",
|
||||||
|
"update": "Aktualisieren",
|
||||||
|
"teamNamePlaceholder": "Name des Teams",
|
||||||
|
"user": "Benutzer",
|
||||||
|
"role": "Rolle",
|
||||||
|
"owner": "Besitzer",
|
||||||
|
"admin": "Administrator",
|
||||||
|
"member": "Mitglied"
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"title": "Benutzer",
|
||||||
|
"subTitle": "Benutzer",
|
||||||
|
"placeholder": "Nach Namen suchen",
|
||||||
|
"user": "Benutzer",
|
||||||
|
"email": "E-Mail",
|
||||||
|
"lastActivity": "Letzte Aktivität",
|
||||||
|
"refresh": "Benutzer aktualisieren"
|
||||||
|
}
|
||||||
34
worklenz-backend/src/public/locales/de/all-project-list.json
Normal file
34
worklenz-backend/src/public/locales/de/all-project-list.json
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
{
|
||||||
|
"name": "Name",
|
||||||
|
"client": "Kunde",
|
||||||
|
"category": "Kategorie",
|
||||||
|
"status": "Status",
|
||||||
|
"tasksProgress": "Aufgabenfortschritt",
|
||||||
|
"updated_at": "Zuletzt aktualisiert",
|
||||||
|
"members": "Mitglieder",
|
||||||
|
"setting": "Einstellungen",
|
||||||
|
"projects": "Projekte",
|
||||||
|
"refreshProjects": "Projekte aktualisieren",
|
||||||
|
"all": "Alle",
|
||||||
|
"favorites": "Favoriten",
|
||||||
|
"archived": "Archiviert",
|
||||||
|
"placeholder": "Nach Namen suchen",
|
||||||
|
"archive": "Archivieren",
|
||||||
|
"unarchive": "Dearchivieren",
|
||||||
|
"archiveConfirm": "Sind Sie sicher, dass Sie dieses Projekt archivieren möchten?",
|
||||||
|
"unarchiveConfirm": "Sind Sie sicher, dass Sie dieses Projekt dearchivieren möchten?",
|
||||||
|
"yes": "Ja",
|
||||||
|
"no": "Nein",
|
||||||
|
"clickToFilter": "Zum Filtern klicken nach",
|
||||||
|
"noProjects": "Keine Projekte gefunden",
|
||||||
|
"addToFavourites": "Zu Favoriten hinzufügen",
|
||||||
|
"list": "Liste",
|
||||||
|
"group": "Gruppe",
|
||||||
|
"listView": "Listenansicht",
|
||||||
|
"groupView": "Gruppenansicht",
|
||||||
|
"groupBy": {
|
||||||
|
"category": "Kategorie",
|
||||||
|
"client": "Kunde"
|
||||||
|
},
|
||||||
|
"noPermission": "Sie haben keine Berechtigung, diese Aktion durchzuführen"
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"loggingOut": "Abmelden...",
|
||||||
|
"authenticating": "Authentifizierung läuft...",
|
||||||
|
"gettingThingsReady": "Bereite alles für Sie vor..."
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"headerDescription": "Passwort zurücksetzen",
|
||||||
|
"emailLabel": "E-Mail",
|
||||||
|
"emailPlaceholder": "Ihre E-Mail eingeben",
|
||||||
|
"emailRequired": "Bitte geben Sie Ihre E-Mail-Adresse ein!",
|
||||||
|
"resetPasswordButton": "Passwort zurücksetzen",
|
||||||
|
"returnToLoginButton": "Zurück zum Login",
|
||||||
|
"passwordResetSuccessMessage": "Ein Link zum Zurücksetzen des Passworts wurde an Ihre E-Mail gesendet.",
|
||||||
|
"orText": "ODER",
|
||||||
|
"successTitle": "Anweisung zum Zurücksetzen gesendet!",
|
||||||
|
"successMessage": "Die Informationen zum Zurücksetzen wurden an Ihre E-Mail gesendet. Bitte überprüfen Sie Ihr E-Mail-Postfach."
|
||||||
|
}
|
||||||
27
worklenz-backend/src/public/locales/de/auth/login.json
Normal file
27
worklenz-backend/src/public/locales/de/auth/login.json
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"headerDescription": "Melden Sie sich an",
|
||||||
|
"emailLabel": "E-Mail",
|
||||||
|
"emailPlaceholder": "Ihre E-Mail-Adresse eingeben",
|
||||||
|
"emailRequired": "Bitte geben Sie Ihre E-Mail-Adresse ein!",
|
||||||
|
"passwordLabel": "Passwort",
|
||||||
|
"passwordPlaceholder": "Ihr Passwort eingeben",
|
||||||
|
"passwordRequired": "Bitte geben Sie Ihr Passwort ein!",
|
||||||
|
"rememberMe": "Erinnere dich an mich",
|
||||||
|
"loginButton": "Anmelden",
|
||||||
|
"signupButton": "Registrieren",
|
||||||
|
"forgotPasswordButton": "Passwort vergessen?",
|
||||||
|
"signInWithGoogleButton": "Mit Google anmelden",
|
||||||
|
"dontHaveAccountText": "Noch kein Konto?",
|
||||||
|
"orText": "ODER",
|
||||||
|
"successMessage": "Sie haben sich erfolgreich angemeldet!",
|
||||||
|
"loginError": "Anmeldung fehlgeschlagen",
|
||||||
|
"googleLoginError": "Google-Anmeldung fehlgeschlagen",
|
||||||
|
"validationMessages": {
|
||||||
|
"email": "Bitte geben Sie eine gültige E-Mail-Adresse ein",
|
||||||
|
"password": "Das Passwort muss mindestens 8 Zeichen lang sein"
|
||||||
|
},
|
||||||
|
"errorMessages": {
|
||||||
|
"loginErrorTitle": "Anmeldung fehlgeschlagen",
|
||||||
|
"loginErrorMessage": "Bitte überprüfen Sie Ihre E-Mail-Adresse und Ihr Passwort und versuchen Sie es erneut"
|
||||||
|
}
|
||||||
|
}
|
||||||
29
worklenz-backend/src/public/locales/de/auth/signup.json
Normal file
29
worklenz-backend/src/public/locales/de/auth/signup.json
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"headerDescription": "Registrieren Sie sich, um loszulegen",
|
||||||
|
"nameLabel": "Vollständiger Name",
|
||||||
|
"namePlaceholder": "Ihren vollständigen Namen eingeben",
|
||||||
|
"nameRequired": "Bitte geben Sie Ihren vollständigen Namen ein!",
|
||||||
|
"nameMinCharacterRequired": "Der Name muss mindestens 4 Zeichen lang sein!",
|
||||||
|
"emailLabel": "E-Mail",
|
||||||
|
"emailPlaceholder": "Ihre E-Mail-Adresse eingeben",
|
||||||
|
"emailRequired": "Bitte geben Sie Ihre E-Mail-Adresse ein!",
|
||||||
|
"passwordLabel": "Passwort",
|
||||||
|
"passwordPlaceholder": "Ihr Passwort eingeben",
|
||||||
|
"passwordRequired": "Bitte geben Sie Ihr Passwort ein!",
|
||||||
|
"passwordMinCharacterRequired": "Das Passwort muss mindestens 8 Zeichen lang sein!",
|
||||||
|
"passwordPatternRequired": "Das Passwort erfüllt nicht die Anforderungen!",
|
||||||
|
"strongPasswordPlaceholder": "Ein stärkeres Passwort eingeben",
|
||||||
|
"passwordValidationAltText": "Das Passwort muss mindestens 8 Zeichen enthalten, mit Groß- und Kleinbuchstaben, einer Zahl und einem Sonderzeichen.",
|
||||||
|
"signupSuccessMessage": "Sie haben sich erfolgreich registriert!",
|
||||||
|
"privacyPolicyLink": "Datenschutzrichtlinie",
|
||||||
|
"termsOfUseLink": "Nutzungsbedingungen",
|
||||||
|
"bySigningUpText": "Mit der Registrierung stimmen Sie unseren",
|
||||||
|
"andText": "und",
|
||||||
|
"signupButton": "Registrieren",
|
||||||
|
"signInWithGoogleButton": "Mit Google anmelden",
|
||||||
|
"alreadyHaveAccountText": "Sie haben bereits ein Konto?",
|
||||||
|
"loginButton": "Anmelden",
|
||||||
|
"orText": "ODER",
|
||||||
|
"reCAPTCHAVerificationError": "reCAPTCHA-Verifizierungsfehler",
|
||||||
|
"reCAPTCHAVerificationErrorMessage": "Wir konnten Ihre reCAPTCHA nicht verifizieren. Bitte versuchen Sie es erneut."
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"title": "E-Mail zurücksetzen bestätigen",
|
||||||
|
"description": "Geben Sie Ihr neues Passwort ein",
|
||||||
|
"placeholder": "Neues Passwort eingeben",
|
||||||
|
"confirmPasswordPlaceholder": "Neues Passwort bestätigen",
|
||||||
|
"passwordHint": "Mindestens 8 Zeichen, mit Groß- und Kleinbuchstaben, einer Zahl und einem Sonderzeichen.",
|
||||||
|
"resetPasswordButton": "Passwort zurücksetzen",
|
||||||
|
"orText": "Oder",
|
||||||
|
"resendResetEmail": "Zurücksetz-E-Mail erneut senden",
|
||||||
|
"passwordRequired": "Bitte geben Sie Ihr neues Passwort ein",
|
||||||
|
"returnToLoginButton": "Zurück zur Anmeldung",
|
||||||
|
"confirmPasswordRequired": "Bitte bestätigen Sie Ihr neues Passwort",
|
||||||
|
"passwordMismatch": "Die beiden Passwörter stimmen nicht überein"
|
||||||
|
}
|
||||||
9
worklenz-backend/src/public/locales/de/common.json
Normal file
9
worklenz-backend/src/public/locales/de/common.json
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"login-success": "Anmeldung erfolgreich!",
|
||||||
|
"login-failed": "Anmeldung fehlgeschlagen. Bitte überprüfen Sie Ihre Anmeldedaten und versuchen Sie es erneut.",
|
||||||
|
"signup-success": "Registrierung erfolgreich! Willkommen an Bord.",
|
||||||
|
"signup-failed": "Registrierung fehlgeschlagen. Bitte füllen Sie alle erforderlichen Felder aus und versuchen Sie es erneut.",
|
||||||
|
"reconnecting": "Vom Server getrennt.",
|
||||||
|
"connection-lost": "Verbindung zum Server fehlgeschlagen. Bitte überprüfen Sie Ihre Internetverbindung.",
|
||||||
|
"connection-restored": "Erfolgreich mit dem Server verbunden"
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"formTitle": "Erstellen Sie Ihr erstes Projekt",
|
||||||
|
"inputLabel": "An welchem Projekt arbeiten Sie gerade?",
|
||||||
|
"or": "oder",
|
||||||
|
"templateButton": "Aus Vorlage importieren",
|
||||||
|
"createFromTemplate": "Aus Vorlage erstellen",
|
||||||
|
"goBack": "Zurück",
|
||||||
|
"continue": "Weitermachen",
|
||||||
|
"cancel": "Abbrechen",
|
||||||
|
"create": "Erstellen",
|
||||||
|
"templateDrawerTitle": "Aus Vorlagen auswählen",
|
||||||
|
"createProject": "Projekt erstellen"
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"formTitle": "Erstellen Sie Ihre erste Aufgabe.",
|
||||||
|
"inputLabel": "Geben Sie einige Aufgaben ein, die Sie erledigen werden in",
|
||||||
|
"addAnother": "Einen weiteren hinzufügen",
|
||||||
|
"goBack": "Zurück",
|
||||||
|
"continue": "Weiter"
|
||||||
|
}
|
||||||
46
worklenz-backend/src/public/locales/de/home.json
Normal file
46
worklenz-backend/src/public/locales/de/home.json
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
{
|
||||||
|
"todoList": {
|
||||||
|
"title": "Aufgabenliste",
|
||||||
|
"refreshTasks": "Aufgaben aktualisieren",
|
||||||
|
"addTask": "+ Aufgabe hinzufügen",
|
||||||
|
"noTasks": "Keine Aufgaben",
|
||||||
|
"pressEnter": "Drücken Sie",
|
||||||
|
"toCreate": "zum Erstellen.",
|
||||||
|
"markAsDone": "Als erledigt markieren"
|
||||||
|
},
|
||||||
|
"projects": {
|
||||||
|
"title": "Projekte",
|
||||||
|
"refreshProjects": "Projekte aktualisieren",
|
||||||
|
"noRecentProjects": "Sie sind aktuell keinem Projekt zugewiesen.",
|
||||||
|
"noFavouriteProjects": "Keine Projekte als Favoriten markiert.",
|
||||||
|
"recent": "Kürzlich",
|
||||||
|
"favourites": "Favoriten"
|
||||||
|
},
|
||||||
|
"tasks": {
|
||||||
|
"assignedToMe": "Mir zugewiesen",
|
||||||
|
"assignedByMe": "Von mir zugewiesen",
|
||||||
|
"all": "Alle",
|
||||||
|
"today": "Heute",
|
||||||
|
"upcoming": "Bevorstehend",
|
||||||
|
"overdue": "Überfällig",
|
||||||
|
"noDueDate": "Kein Fälligkeitsdatum",
|
||||||
|
"noTasks": "Keine Aufgaben zum Anzeigen.",
|
||||||
|
"addTask": "+ Aufgabe hinzufügen",
|
||||||
|
"name": "Name",
|
||||||
|
"project": "Projekt",
|
||||||
|
"status": "Status",
|
||||||
|
"dueDate": "Fälligkeitsdatum",
|
||||||
|
"dueDatePlaceholder": "Fälligkeitsdatum festlegen",
|
||||||
|
"tomorrow": "Morgen",
|
||||||
|
"nextWeek": "Nächste Woche",
|
||||||
|
"nextMonth": "Nächster Monat",
|
||||||
|
"projectRequired": "Bitte wählen Sie ein Projekt aus",
|
||||||
|
"pressTabToSelectDueDateAndProject": "Drücken Sie Tab, um ein Fälligkeitsdatum und ein Projekt auszuwählen",
|
||||||
|
"dueOn": "Fällige Aufgaben am",
|
||||||
|
"taskRequired": "Bitte fügen Sie eine Aufgabe hinzu",
|
||||||
|
"list": "Liste",
|
||||||
|
"calendar": "Kalender",
|
||||||
|
"tasks": "Aufgaben",
|
||||||
|
"refresh": "Aktualisieren"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"formTitle": "Laden Sie Ihr Team zur Zusammenarbeit ein",
|
||||||
|
"inputLabel": "Per E-Mail einladen",
|
||||||
|
"addAnother": "Weitere hinzufügen",
|
||||||
|
"goBack": "Zurück",
|
||||||
|
"continue": "Weitermachen",
|
||||||
|
"skipForNow": "Vorerst überspringen"
|
||||||
|
}
|
||||||
30
worklenz-backend/src/public/locales/de/kanban-board.json
Normal file
30
worklenz-backend/src/public/locales/de/kanban-board.json
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"rename": "Umbenennen",
|
||||||
|
"delete": "Löschen",
|
||||||
|
"addTask": "Aufgabe hinzufügen",
|
||||||
|
"addSectionButton": "Abschnitt hinzufügen",
|
||||||
|
"changeCategory": "Kategorie ändern",
|
||||||
|
|
||||||
|
"deleteTooltip": "Löschen",
|
||||||
|
"deleteConfirmationTitle": "Sind Sie sicher?",
|
||||||
|
"deleteConfirmationOk": "Ja",
|
||||||
|
"deleteConfirmationCancel": "Abbrechen",
|
||||||
|
|
||||||
|
"dueDate": "Fälligkeitsdatum",
|
||||||
|
"cancel": "Abbrechen",
|
||||||
|
|
||||||
|
"today": "Heute",
|
||||||
|
"tomorrow": "Morgen",
|
||||||
|
"assignToMe": "Mir zuweisen",
|
||||||
|
"archive": "Archivieren",
|
||||||
|
|
||||||
|
"newTaskNamePlaceholder": "Aufgabenname 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"
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"title": "Ihre Worklenz-Testversion ist abgelaufen!",
|
||||||
|
"subtitle": "Bitte führen Sie jetzt ein Upgrade durch.",
|
||||||
|
"button": "Jetzt upgraden",
|
||||||
|
"checking": "Überprüfen des Abonnementstatus..."
|
||||||
|
}
|
||||||
31
worklenz-backend/src/public/locales/de/navbar.json
Normal file
31
worklenz-backend/src/public/locales/de/navbar.json
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
{
|
||||||
|
"logoAlt": "Worklenz-Logo",
|
||||||
|
"home": "Startseite",
|
||||||
|
"projects": "Projekte",
|
||||||
|
"schedule": "Zeitplan",
|
||||||
|
"reporting": "Berichterstattung",
|
||||||
|
"clients": "Kunden",
|
||||||
|
"teams": "Teams",
|
||||||
|
"labels": "Labels",
|
||||||
|
"jobTitles": "Jobtitel",
|
||||||
|
"upgradePlan": "Plan upgraden",
|
||||||
|
"upgradePlanTooltip": "Plan upgraden",
|
||||||
|
"invite": "Einladen",
|
||||||
|
"inviteTooltip": "Teammitglieder zur Teilnahme einladen",
|
||||||
|
"switchTeamTooltip": "Team wechseln",
|
||||||
|
"help": "Hilfe",
|
||||||
|
"notificationTooltip": "Benachrichtigungen anzeigen",
|
||||||
|
"profileTooltip": "Profil anzeigen",
|
||||||
|
"adminCenter": "Admin-Center",
|
||||||
|
"settings": "Einstellungen",
|
||||||
|
"logOut": "Abmelden",
|
||||||
|
"notificationsDrawer": {
|
||||||
|
"read": "Gelesene Benachrichtigungen",
|
||||||
|
"unread": "Ungelesene Benachrichtigungen",
|
||||||
|
"markAsRead": "Als gelesen markieren",
|
||||||
|
"readAndJoin": "Lesen & Beitreten",
|
||||||
|
"accept": "Annehmen",
|
||||||
|
"acceptAndJoin": "Annehmen & Beitreten",
|
||||||
|
"noNotifications": "Keine Benachrichtigungen"
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user