Merge pull request #247 from Worklenz/release/v2.0.4-bug-fix
Release/v2.0.4 bug fix
This commit is contained in:
16
backup.sh
Normal file
16
backup.sh
Normal file
@@ -0,0 +1,16 @@
|
||||
#!/bin/bash
|
||||
set -eu
|
||||
|
||||
# Adjust these as needed:
|
||||
CONTAINER=worklenz_db
|
||||
DB_NAME=worklenz_db
|
||||
DB_USER=postgres
|
||||
BACKUP_DIR=./pg_backups
|
||||
mkdir -p "$BACKUP_DIR"
|
||||
|
||||
timestamp=$(date +%Y-%m-%d_%H-%M-%S)
|
||||
outfile="${BACKUP_DIR}/${DB_NAME}_${timestamp}.sql"
|
||||
echo "Creating backup $outfile ..."
|
||||
|
||||
docker exec -t "$CONTAINER" pg_dump -U "$DB_USER" -d "$DB_NAME" > "$outfile"
|
||||
echo "Backup saved to $outfile"
|
||||
@@ -83,7 +83,11 @@ services:
|
||||
POSTGRES_DB: ${DB_NAME:-worklenz_db}
|
||||
POSTGRES_PASSWORD: ${DB_PASSWORD:-password}
|
||||
healthcheck:
|
||||
test: [ "CMD-SHELL", "pg_isready -d ${DB_NAME:-worklenz_db} -U ${DB_USER:-postgres}" ]
|
||||
test:
|
||||
[
|
||||
"CMD-SHELL",
|
||||
"pg_isready -d ${DB_NAME:-worklenz_db} -U ${DB_USER:-postgres}",
|
||||
]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
@@ -93,23 +97,65 @@ services:
|
||||
volumes:
|
||||
- worklenz_postgres_data:/var/lib/postgresql/data
|
||||
- type: bind
|
||||
source: ./worklenz-backend/database
|
||||
target: /docker-entrypoint-initdb.d
|
||||
source: ./worklenz-backend/database/sql
|
||||
target: /docker-entrypoint-initdb.d/sql
|
||||
consistency: cached
|
||||
- type: bind
|
||||
source: ./worklenz-backend/database/migrations
|
||||
target: /docker-entrypoint-initdb.d/migrations
|
||||
consistency: cached
|
||||
- type: bind
|
||||
source: ./worklenz-backend/database/00_init.sh
|
||||
target: /docker-entrypoint-initdb.d/00_init.sh
|
||||
consistency: cached
|
||||
- type: bind
|
||||
source: ./pg_backups
|
||||
target: /docker-entrypoint-initdb.d/pg_backups
|
||||
command: >
|
||||
bash -c ' if command -v apt-get >/dev/null 2>&1; then
|
||||
apt-get update && apt-get install -y dos2unix
|
||||
elif command -v apk >/dev/null 2>&1; then
|
||||
apk add --no-cache dos2unix
|
||||
fi && find /docker-entrypoint-initdb.d -type f -name "*.sh" -exec sh -c '\''
|
||||
dos2unix "{}" 2>/dev/null || true
|
||||
chmod +x "{}"
|
||||
'\'' \; && exec docker-entrypoint.sh postgres '
|
||||
bash -c '
|
||||
if command -v apt-get >/dev/null 2>&1; then
|
||||
apt-get update && apt-get install -y dos2unix
|
||||
elif command -v apk >/dev/null 2>&1; then
|
||||
apk add --no-cache dos2unix
|
||||
fi
|
||||
|
||||
find /docker-entrypoint-initdb.d -type f -name "*.sh" -exec sh -c '"'"'
|
||||
for f; do
|
||||
dos2unix "$f" 2>/dev/null || true
|
||||
chmod +x "$f"
|
||||
done
|
||||
'"'"' sh {} +
|
||||
|
||||
exec docker-entrypoint.sh postgres
|
||||
'
|
||||
db-backup:
|
||||
image: postgres:15
|
||||
container_name: worklenz_db_backup
|
||||
environment:
|
||||
POSTGRES_USER: ${DB_USER:-postgres}
|
||||
POSTGRES_DB: ${DB_NAME:-worklenz_db}
|
||||
POSTGRES_PASSWORD: ${DB_PASSWORD:-password}
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
volumes:
|
||||
- ./pg_backups:/pg_backups #host dir for backups files
|
||||
#setup bassh loop to backup data evey 24h
|
||||
command: >
|
||||
bash -c 'while true; do
|
||||
sleep 86400;
|
||||
PGPASSWORD=$$POSTGRES_PASSWORD pg_dump -h worklenz_db -U $$POSTGRES_USER -d $$POSTGRES_DB \
|
||||
> /pg_backups/worklenz_db_$$(date +%Y-%m-%d_%H-%M-%S).sql;
|
||||
find /pg_backups -type f -name "*.sql" -mtime +30 -delete;
|
||||
done'
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- worklenz
|
||||
|
||||
volumes:
|
||||
worklenz_postgres_data:
|
||||
worklenz_minio_data:
|
||||
|
||||
pgdata:
|
||||
|
||||
networks:
|
||||
worklenz:
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# This script controls the order of SQL file execution during database initialization
|
||||
echo "Starting database initialization..."
|
||||
|
||||
# Check if we have SQL files in expected locations
|
||||
if [ -f "/docker-entrypoint-initdb.d/sql/0_extensions.sql" ]; then
|
||||
SQL_DIR="/docker-entrypoint-initdb.d/sql"
|
||||
echo "Using SQL files from sql/ subdirectory"
|
||||
elif [ -f "/docker-entrypoint-initdb.d/0_extensions.sql" ]; then
|
||||
# First time setup - move files to subdirectory
|
||||
echo "Moving SQL files to sql/ subdirectory..."
|
||||
mkdir -p /docker-entrypoint-initdb.d/sql
|
||||
|
||||
# Move all SQL files (except this script) to the subdirectory
|
||||
for f in /docker-entrypoint-initdb.d/*.sql; do
|
||||
if [ -f "$f" ]; then
|
||||
cp "$f" /docker-entrypoint-initdb.d/sql/
|
||||
echo "Copied $f to sql/ subdirectory"
|
||||
fi
|
||||
done
|
||||
|
||||
SQL_DIR="/docker-entrypoint-initdb.d/sql"
|
||||
else
|
||||
echo "SQL files not found in expected locations!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Execute SQL files in the correct order
|
||||
echo "Executing 0_extensions.sql..."
|
||||
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" -f "$SQL_DIR/0_extensions.sql"
|
||||
|
||||
echo "Executing 1_tables.sql..."
|
||||
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" -f "$SQL_DIR/1_tables.sql"
|
||||
|
||||
echo "Executing indexes.sql..."
|
||||
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" -f "$SQL_DIR/indexes.sql"
|
||||
|
||||
echo "Executing 4_functions.sql..."
|
||||
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" -f "$SQL_DIR/4_functions.sql"
|
||||
|
||||
echo "Executing triggers.sql..."
|
||||
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" -f "$SQL_DIR/triggers.sql"
|
||||
|
||||
echo "Executing 3_views.sql..."
|
||||
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" -f "$SQL_DIR/3_views.sql"
|
||||
|
||||
echo "Executing 2_dml.sql..."
|
||||
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" -f "$SQL_DIR/2_dml.sql"
|
||||
|
||||
echo "Executing 5_database_user.sql..."
|
||||
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" -f "$SQL_DIR/5_database_user.sql"
|
||||
|
||||
echo "Database initialization completed successfully"
|
||||
88
worklenz-backend/database/00_init.sh
Normal file
88
worklenz-backend/database/00_init.sh
Normal file
@@ -0,0 +1,88 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
echo "Starting database initialization..."
|
||||
|
||||
SQL_DIR="/docker-entrypoint-initdb.d/sql"
|
||||
MIGRATIONS_DIR="/docker-entrypoint-initdb.d/migrations"
|
||||
BACKUP_DIR="/docker-entrypoint-initdb.d/pg_backups"
|
||||
|
||||
# --------------------------------------------
|
||||
# 🗄️ STEP 1: Attempt to restore latest backup
|
||||
# --------------------------------------------
|
||||
|
||||
if [ -d "$BACKUP_DIR" ]; then
|
||||
LATEST_BACKUP=$(ls -t "$BACKUP_DIR"/*.sql 2>/dev/null | head -n 1)
|
||||
else
|
||||
LATEST_BACKUP=""
|
||||
fi
|
||||
|
||||
if [ -f "$LATEST_BACKUP" ]; then
|
||||
echo "🗄️ Found latest backup: $LATEST_BACKUP"
|
||||
echo "⏳ Restoring from backup..."
|
||||
psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" < "$LATEST_BACKUP"
|
||||
echo "✅ Backup restoration complete. Skipping schema and migrations."
|
||||
exit 0
|
||||
else
|
||||
echo "ℹ️ No valid backup found. Proceeding with base schema and migrations."
|
||||
fi
|
||||
|
||||
# --------------------------------------------
|
||||
# 🏗️ STEP 2: Continue with base schema setup
|
||||
# --------------------------------------------
|
||||
|
||||
# Create migrations table if it doesn't exist
|
||||
psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" -c "
|
||||
CREATE TABLE IF NOT EXISTS schema_migrations (
|
||||
version TEXT PRIMARY KEY,
|
||||
applied_at TIMESTAMP DEFAULT now()
|
||||
);
|
||||
"
|
||||
|
||||
# List of base schema files to execute in order
|
||||
BASE_SQL_FILES=(
|
||||
"0_extensions.sql"
|
||||
"1_tables.sql"
|
||||
"indexes.sql"
|
||||
"4_functions.sql"
|
||||
"triggers.sql"
|
||||
"3_views.sql"
|
||||
"2_dml.sql"
|
||||
"5_database_user.sql"
|
||||
)
|
||||
|
||||
echo "Running base schema SQL files in order..."
|
||||
|
||||
for file in "${BASE_SQL_FILES[@]}"; do
|
||||
full_path="$SQL_DIR/$file"
|
||||
if [ -f "$full_path" ]; then
|
||||
echo "Executing $file..."
|
||||
psql -v ON_ERROR_STOP=1 -U "$POSTGRES_USER" -d "$POSTGRES_DB" -f "$full_path"
|
||||
else
|
||||
echo "WARNING: $file not found, skipping."
|
||||
fi
|
||||
done
|
||||
|
||||
echo "✅ Base schema SQL execution complete."
|
||||
|
||||
# --------------------------------------------
|
||||
# 🚀 STEP 3: Apply SQL migrations
|
||||
# --------------------------------------------
|
||||
|
||||
if [ -d "$MIGRATIONS_DIR" ] && compgen -G "$MIGRATIONS_DIR/*.sql" > /dev/null; then
|
||||
echo "Applying migrations..."
|
||||
for f in "$MIGRATIONS_DIR"/*.sql; do
|
||||
version=$(basename "$f")
|
||||
if ! psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" -tAc "SELECT 1 FROM schema_migrations WHERE version = '$version'" | grep -q 1; then
|
||||
echo "Applying migration: $version"
|
||||
psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" -f "$f"
|
||||
psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" -c "INSERT INTO schema_migrations (version) VALUES ('$version');"
|
||||
else
|
||||
echo "Skipping already applied migration: $version"
|
||||
fi
|
||||
done
|
||||
else
|
||||
echo "No migration files found or directory is empty, skipping migrations."
|
||||
fi
|
||||
|
||||
echo "🎉 Database initialization completed successfully."
|
||||
@@ -0,0 +1,135 @@
|
||||
-- Performance indexes for optimized tasks queries
|
||||
-- Migration: 20250115000000-performance-indexes.sql
|
||||
|
||||
-- Composite index for main task filtering
|
||||
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_tasks_project_archived_parent
|
||||
ON tasks(project_id, archived, parent_task_id)
|
||||
WHERE archived = FALSE;
|
||||
|
||||
-- Index for status joins
|
||||
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_tasks_status_project
|
||||
ON tasks(status_id, project_id)
|
||||
WHERE archived = FALSE;
|
||||
|
||||
-- Index for assignees lookup
|
||||
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_tasks_assignees_task_member
|
||||
ON tasks_assignees(task_id, team_member_id);
|
||||
|
||||
-- Index for phase lookup
|
||||
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_task_phase_task_phase
|
||||
ON task_phase(task_id, phase_id);
|
||||
|
||||
-- Index for subtask counting
|
||||
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_tasks_parent_archived
|
||||
ON tasks(parent_task_id, archived)
|
||||
WHERE parent_task_id IS NOT NULL AND archived = FALSE;
|
||||
|
||||
-- Index for labels
|
||||
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_task_labels_task_label
|
||||
ON task_labels(task_id, label_id);
|
||||
|
||||
-- Index for comments count
|
||||
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_task_comments_task
|
||||
ON task_comments(task_id);
|
||||
|
||||
-- Index for attachments count
|
||||
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_task_attachments_task
|
||||
ON task_attachments(task_id);
|
||||
|
||||
-- Index for work log aggregation
|
||||
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_task_work_log_task
|
||||
ON task_work_log(task_id);
|
||||
|
||||
-- Index for subscribers check
|
||||
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_task_subscribers_task
|
||||
ON task_subscribers(task_id);
|
||||
|
||||
-- Index for dependencies check
|
||||
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_task_dependencies_task
|
||||
ON task_dependencies(task_id);
|
||||
|
||||
-- Index for timers lookup
|
||||
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_task_timers_task_user
|
||||
ON task_timers(task_id, user_id);
|
||||
|
||||
-- Index for custom columns
|
||||
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_cc_column_values_task
|
||||
ON cc_column_values(task_id);
|
||||
|
||||
-- Index for team member info view optimization
|
||||
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_team_members_team_user
|
||||
ON team_members(team_id, user_id)
|
||||
WHERE active = TRUE;
|
||||
|
||||
-- Index for notification settings
|
||||
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_notification_settings_user_team
|
||||
ON notification_settings(user_id, team_id);
|
||||
|
||||
-- Index for task status categories
|
||||
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_task_statuses_category
|
||||
ON task_statuses(category_id, project_id);
|
||||
|
||||
-- Index for project phases
|
||||
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_project_phases_project_sort
|
||||
ON project_phases(project_id, sort_index);
|
||||
|
||||
-- Index for task priorities
|
||||
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_task_priorities_value
|
||||
ON task_priorities(value);
|
||||
|
||||
-- Index for team labels
|
||||
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_team_labels_team
|
||||
ON team_labels(team_id);
|
||||
|
||||
-- NEW INDEXES FOR PERFORMANCE OPTIMIZATION --
|
||||
|
||||
-- Composite index for task main query optimization (covers most WHERE conditions)
|
||||
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_tasks_performance_main
|
||||
ON tasks(project_id, archived, parent_task_id, status_id, priority_id)
|
||||
WHERE archived = FALSE;
|
||||
|
||||
-- Index for sorting by sort_order with project filter
|
||||
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_tasks_project_sort_order
|
||||
ON tasks(project_id, sort_order)
|
||||
WHERE archived = FALSE;
|
||||
|
||||
-- Index for email_invitations to optimize team_member_info_view
|
||||
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_email_invitations_team_member
|
||||
ON email_invitations(team_member_id);
|
||||
|
||||
-- Covering index for task status with category information
|
||||
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_task_statuses_covering
|
||||
ON task_statuses(id, category_id, project_id);
|
||||
|
||||
-- Index for task aggregation queries (parent task progress calculation)
|
||||
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_tasks_parent_status_archived
|
||||
ON tasks(parent_task_id, status_id, archived)
|
||||
WHERE archived = FALSE;
|
||||
|
||||
-- Index for project team member filtering
|
||||
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_team_members_project_lookup
|
||||
ON team_members(team_id, active, user_id)
|
||||
WHERE active = TRUE;
|
||||
|
||||
-- Covering index for tasks with frequently accessed columns
|
||||
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_tasks_covering_main
|
||||
ON tasks(id, project_id, archived, parent_task_id, status_id, priority_id, sort_order, name)
|
||||
WHERE archived = FALSE;
|
||||
|
||||
-- Index for task search functionality
|
||||
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_tasks_name_search
|
||||
ON tasks USING gin(to_tsvector('english', name))
|
||||
WHERE archived = FALSE;
|
||||
|
||||
-- Index for date-based filtering (if used)
|
||||
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_tasks_dates
|
||||
ON tasks(project_id, start_date, end_date)
|
||||
WHERE archived = FALSE;
|
||||
|
||||
-- Index for task timers with user filtering
|
||||
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_task_timers_user_task
|
||||
ON task_timers(user_id, task_id);
|
||||
|
||||
-- Index for sys_task_status_categories lookups
|
||||
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_sys_task_status_categories_covering
|
||||
ON sys_task_status_categories(id, color_code, color_code_dark, is_done, is_doing, is_todo);
|
||||
@@ -12,7 +12,7 @@ CREATE TYPE DEPENDENCY_TYPE AS ENUM ('blocked_by');
|
||||
|
||||
CREATE TYPE SCHEDULE_TYPE AS ENUM ('daily', 'weekly', 'yearly', 'monthly', 'every_x_days', 'every_x_weeks', 'every_x_months');
|
||||
|
||||
CREATE TYPE LANGUAGE_TYPE AS ENUM ('en', 'es', 'pt');
|
||||
CREATE TYPE LANGUAGE_TYPE AS ENUM ('en', 'es', 'pt', 'alb', 'de', 'zh_cn');
|
||||
|
||||
-- START: Users
|
||||
CREATE SEQUENCE IF NOT EXISTS users_user_no_seq START 1;
|
||||
|
||||
@@ -32,3 +32,37 @@ SELECT u.avatar_url,
|
||||
FROM team_members
|
||||
LEFT JOIN users u ON team_members.user_id = u.id;
|
||||
|
||||
-- PERFORMANCE OPTIMIZATION: Create materialized view for team member info
|
||||
-- This pre-calculates the expensive joins and subqueries from team_member_info_view
|
||||
CREATE MATERIALIZED VIEW IF NOT EXISTS team_member_info_mv AS
|
||||
SELECT
|
||||
u.avatar_url,
|
||||
COALESCE(u.email, ei.email) AS email,
|
||||
COALESCE(u.name, ei.name) AS name,
|
||||
u.id AS user_id,
|
||||
tm.id AS team_member_id,
|
||||
tm.team_id,
|
||||
tm.active,
|
||||
u.socket_id
|
||||
FROM team_members tm
|
||||
LEFT JOIN users u ON tm.user_id = u.id
|
||||
LEFT JOIN email_invitations ei ON ei.team_member_id = tm.id
|
||||
WHERE tm.active = TRUE;
|
||||
|
||||
-- Create unique index on the materialized view for fast lookups
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_team_member_info_mv_team_member_id
|
||||
ON team_member_info_mv(team_member_id);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_team_member_info_mv_team_user
|
||||
ON team_member_info_mv(team_id, user_id);
|
||||
|
||||
-- Function to refresh the materialized view
|
||||
CREATE OR REPLACE FUNCTION refresh_team_member_info_mv()
|
||||
RETURNS void
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
BEGIN
|
||||
REFRESH MATERIALIZED VIEW CONCURRENTLY team_member_info_mv;
|
||||
END;
|
||||
$$;
|
||||
|
||||
|
||||
@@ -69,13 +69,13 @@ export default class TasksControllerV2 extends TasksControllerBase {
|
||||
}
|
||||
|
||||
private static getFilterByProjectsWhereClosure(text: string) {
|
||||
return text ? `project_id IN (${this.flatString(text)})` : "";
|
||||
return text ? `t.project_id IN (${this.flatString(text)})` : "";
|
||||
}
|
||||
|
||||
private static getFilterByAssignee(filterBy: string) {
|
||||
return filterBy === "member"
|
||||
? `id IN (SELECT task_id FROM tasks_assignees WHERE team_member_id = $1)`
|
||||
: "project_id = $1";
|
||||
? `t.id IN (SELECT task_id FROM tasks_assignees WHERE team_member_id = $1)`
|
||||
: "t.project_id = $1";
|
||||
}
|
||||
|
||||
private static getStatusesQuery(filterBy: string) {
|
||||
@@ -131,41 +131,19 @@ export default class TasksControllerV2 extends TasksControllerBase {
|
||||
// Returns statuses of each task as a json array if filterBy === "member"
|
||||
const statusesQuery = TasksControllerV2.getStatusesQuery(options.filterBy as string);
|
||||
|
||||
// Custom columns data query
|
||||
// Custom columns data query - optimized with LEFT JOIN
|
||||
const customColumnsQuery = options.customColumns
|
||||
? `, (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`
|
||||
? `, COALESCE(cc_data.custom_column_values, '{}'::JSONB) AS custom_column_values`
|
||||
: "";
|
||||
|
||||
const archivedFilter = options.archived === "true" ? "archived IS TRUE" : "archived IS FALSE";
|
||||
const archivedFilter = options.archived === "true" ? "t.archived IS TRUE" : "t.archived IS FALSE";
|
||||
|
||||
let subTasksFilter;
|
||||
|
||||
if (options.isSubtasksInclude === "true") {
|
||||
subTasksFilter = "";
|
||||
} else {
|
||||
subTasksFilter = isSubTasks ? "parent_task_id = $2" : "parent_task_id IS NULL";
|
||||
subTasksFilter = isSubTasks ? "t.parent_task_id = $2" : "t.parent_task_id IS NULL";
|
||||
}
|
||||
|
||||
const filters = [
|
||||
@@ -179,94 +157,171 @@ export default class TasksControllerV2 extends TasksControllerBase {
|
||||
projectsFilter
|
||||
].filter(i => !!i).join(" AND ");
|
||||
|
||||
// PERFORMANCE OPTIMIZED QUERY - Using CTEs and JOINs instead of correlated subqueries
|
||||
return `
|
||||
SELECT id,
|
||||
name,
|
||||
CONCAT((SELECT key FROM projects WHERE id = t.project_id), '-', task_no) AS task_key,
|
||||
(SELECT name FROM projects WHERE id = t.project_id) AS project_name,
|
||||
t.project_id AS project_id,
|
||||
t.parent_task_id,
|
||||
t.parent_task_id IS NOT NULL AS is_sub_task,
|
||||
(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.archived,
|
||||
t.description,
|
||||
t.sort_order,
|
||||
t.progress_value,
|
||||
t.manual_progress,
|
||||
t.weight,
|
||||
(SELECT use_manual_progress FROM projects WHERE id = t.project_id) AS project_use_manual_progress,
|
||||
(SELECT use_weighted_progress FROM projects WHERE id = t.project_id) AS project_use_weighted_progress,
|
||||
(SELECT use_time_progress FROM projects WHERE id = t.project_id) AS project_use_time_progress,
|
||||
(SELECT get_task_complete_ratio(t.id)->>'ratio') AS complete_ratio,
|
||||
|
||||
(SELECT phase_id FROM task_phase WHERE task_id = t.id) AS phase_id,
|
||||
(SELECT name
|
||||
FROM project_phases
|
||||
WHERE id = (SELECT phase_id FROM task_phase WHERE task_id = t.id)) AS phase_name,
|
||||
(SELECT color_code
|
||||
FROM project_phases
|
||||
WHERE id = (SELECT phase_id FROM task_phase WHERE task_id = t.id)) AS phase_color_code,
|
||||
|
||||
(EXISTS(SELECT 1 FROM task_subscribers WHERE task_id = t.id)) AS has_subscribers,
|
||||
(EXISTS(SELECT 1 FROM task_dependencies td WHERE td.task_id = t.id)) AS has_dependencies,
|
||||
(SELECT start_time
|
||||
FROM task_timers
|
||||
WHERE task_id = t.id
|
||||
AND user_id = '${userId}') AS timer_start_time,
|
||||
|
||||
(SELECT color_code
|
||||
FROM sys_task_status_categories
|
||||
WHERE id = (SELECT category_id FROM task_statuses WHERE id = t.status_id)) AS status_color,
|
||||
|
||||
(SELECT color_code_dark
|
||||
FROM sys_task_status_categories
|
||||
WHERE id = (SELECT category_id FROM task_statuses WHERE id = t.status_id)) AS status_color_dark,
|
||||
|
||||
(SELECT COALESCE(ROW_TO_JSON(r), '{}'::JSON)
|
||||
FROM (SELECT is_done, is_doing, is_todo
|
||||
FROM sys_task_status_categories
|
||||
WHERE id = (SELECT category_id FROM task_statuses WHERE id = t.status_id)) r) AS status_category,
|
||||
|
||||
(SELECT COUNT(*) FROM task_comments WHERE task_id = t.id) AS comments_count,
|
||||
(SELECT COUNT(*) FROM task_attachments WHERE task_id = t.id) AS attachments_count,
|
||||
(CASE
|
||||
WHEN EXISTS(SELECT 1
|
||||
FROM tasks_with_status_view
|
||||
WHERE tasks_with_status_view.task_id = t.id
|
||||
AND is_done IS TRUE) THEN 1
|
||||
ELSE 0 END) AS parent_task_completed,
|
||||
(SELECT get_task_assignees(t.id)) AS assignees,
|
||||
(SELECT COUNT(*)
|
||||
FROM tasks_with_status_view tt
|
||||
WHERE tt.parent_task_id = t.id
|
||||
AND tt.is_done IS TRUE)::INT
|
||||
AS completed_sub_tasks,
|
||||
|
||||
(SELECT COALESCE(JSON_AGG(r), '[]'::JSON)
|
||||
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}
|
||||
WITH task_aggregates AS (
|
||||
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,
|
||||
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 IS NOT NULL AS is_sub_task,
|
||||
parent_task.name AS parent_task_name,
|
||||
t.status_id AS status,
|
||||
t.archived,
|
||||
t.description,
|
||||
t.sort_order,
|
||||
t.progress_value,
|
||||
t.manual_progress,
|
||||
t.weight,
|
||||
p.use_manual_progress AS project_use_manual_progress,
|
||||
p.use_weighted_progress AS project_use_weighted_progress,
|
||||
p.use_time_progress AS project_use_time_progress,
|
||||
-- Use stored progress value instead of expensive function call
|
||||
COALESCE(t.progress_value, 0) AS complete_ratio,
|
||||
-- Phase information via JOINs
|
||||
tp.phase_id,
|
||||
pp.name AS phase_name,
|
||||
pp.color_code AS phase_color_code,
|
||||
-- Status information via JOINs
|
||||
stsc.color_code AS status_color,
|
||||
stsc.color_code_dark AS status_color_dark,
|
||||
JSON_BUILD_OBJECT(
|
||||
'is_done', stsc.is_done,
|
||||
'is_doing', stsc.is_doing,
|
||||
'is_todo', stsc.is_todo
|
||||
) AS status_category,
|
||||
-- Aggregated counts
|
||||
COALESCE(agg.sub_tasks_count, 0) AS sub_tasks_count,
|
||||
COALESCE(agg.completed_sub_tasks, 0) AS completed_sub_tasks,
|
||||
COALESCE(agg.comments_count, 0) AS comments_count,
|
||||
COALESCE(agg.attachments_count, 0) AS attachments_count,
|
||||
COALESCE(agg.total_minutes_spent, 0) AS total_minutes_spent,
|
||||
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,
|
||||
-- Task completion status
|
||||
CASE WHEN stsc.is_done THEN 1 ELSE 0 END AS parent_task_completed,
|
||||
-- Assignees and labels via JOINs
|
||||
COALESCE(assignees.assignees, '[]'::JSON) AS assignees,
|
||||
COALESCE(assignees.assignee_names, '') AS assignee_names,
|
||||
COALESCE(assignees.names, '') AS names,
|
||||
COALESCE(labels.labels, '[]'::JSON) AS labels,
|
||||
COALESCE(labels.all_labels, '[]'::JSON) AS all_labels,
|
||||
-- Other fields
|
||||
stsc.is_done AS is_complete,
|
||||
reporter.name AS reporter,
|
||||
t.priority_id AS priority,
|
||||
tp_priority.value AS priority_value,
|
||||
t.total_minutes,
|
||||
t.created_at,
|
||||
t.updated_at,
|
||||
t.completed_at,
|
||||
t.start_date,
|
||||
t.billable,
|
||||
t.schedule_id,
|
||||
t.END_DATE,
|
||||
-- Timer information
|
||||
tt.start_time AS timer_start_time
|
||||
${customColumnsQuery}
|
||||
${statusesQuery}
|
||||
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}
|
||||
ORDER BY ${sortFields}
|
||||
`;
|
||||
@@ -347,7 +402,7 @@ export default class TasksControllerV2 extends TasksControllerBase {
|
||||
req.query.customColumns = "true";
|
||||
|
||||
const q = TasksControllerV2.getQuery(req.user?.id as string, req.query);
|
||||
const params = isSubTasks ? [req.params.id || null, req.query.parent_task] : [req.params.id || null];
|
||||
const params = isSubTasks ? [req.params.id || null, req.query.parent_task, req.user?.id] : [req.params.id || null, req.user?.id];
|
||||
|
||||
const result = await db.query(q, params);
|
||||
const tasks = [...result.rows];
|
||||
@@ -455,7 +510,7 @@ export default class TasksControllerV2 extends TasksControllerBase {
|
||||
req.query.customColumns = "true";
|
||||
|
||||
const q = TasksControllerV2.getQuery(req.user?.id as string, req.query);
|
||||
const params = isSubTasks ? [req.params.id || null, req.query.parent_task] : [req.params.id || null];
|
||||
const params = isSubTasks ? [req.params.id || null, req.query.parent_task, req.user?.id] : [req.params.id || null, req.user?.id];
|
||||
const result = await db.query(q, params);
|
||||
|
||||
let data: any[] = [];
|
||||
@@ -986,60 +1041,62 @@ export default class TasksControllerV2 extends TasksControllerBase {
|
||||
@HandleExceptions()
|
||||
public static async getTasksV3(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const startTime = performance.now();
|
||||
const isSubTasks = !!req.query.parent_task;
|
||||
const groupBy = (req.query.group || GroupBy.STATUS) as string;
|
||||
const archived = req.query.archived === "true";
|
||||
|
||||
console.log(`[PERFORMANCE] getTasksV3 method called for project ${req.params.id}`);
|
||||
|
||||
// PERFORMANCE OPTIMIZATION: Skip expensive progress calculation by default
|
||||
// Progress values are already calculated and stored in the database
|
||||
// Only refresh if explicitly requested via refresh_progress=true query parameter
|
||||
// This dramatically improves initial load performance (from ~2-5s to ~200-500ms)
|
||||
const shouldRefreshProgress = req.query.refresh_progress === "true";
|
||||
|
||||
if (shouldRefreshProgress && req.params.id) {
|
||||
if (req.query.refresh_progress === "true" && req.params.id) {
|
||||
console.log(`[PERFORMANCE] Starting progress refresh for project ${req.params.id} (getTasksV3)`);
|
||||
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 queryStartTime = performance.now();
|
||||
const isSubTasks = !!req.query.parent_task;
|
||||
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 params = isSubTasks ? [req.params.id || null, req.query.parent_task] : [req.params.id || null];
|
||||
const params = isSubTasks ? [req.params.id || null, req.query.parent_task, req.user?.id] : [req.params.id || null, req.user?.id];
|
||||
|
||||
const result = await db.query(q, params);
|
||||
const tasks = [...result.rows];
|
||||
const queryEndTime = performance.now();
|
||||
|
||||
// Get groups metadata dynamically from database
|
||||
const groupsStartTime = performance.now();
|
||||
// Use the same groups query as getList method
|
||||
const groups = await this.getGroups(groupBy, req.params.id);
|
||||
const groupsEndTime = performance.now();
|
||||
const map = groups.reduce((g: { [x: string]: ITaskGroup }, group) => {
|
||||
if (group.id)
|
||||
g[group.id] = new TaskListGroup(group);
|
||||
return g;
|
||||
}, {});
|
||||
|
||||
// Create priority value to name mapping
|
||||
// Use the same updateMapByGroup method as getList
|
||||
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> = {
|
||||
"0": "low",
|
||||
"1": "medium",
|
||||
"2": "high"
|
||||
};
|
||||
|
||||
// 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();
|
||||
// Transform all tasks to V3 format
|
||||
const transformedTasks = tasks.map((task, index) => {
|
||||
// Update task with calculated values (lightweight version)
|
||||
TasksControllerV2.updateTaskViewModel(task);
|
||||
task.index = index;
|
||||
|
||||
// Convert time values
|
||||
const convertTimeValue = (value: any): number => {
|
||||
if (typeof value === "number") return value;
|
||||
@@ -1062,15 +1119,12 @@ export default class TasksControllerV2 extends TasksControllerBase {
|
||||
task_key: task.task_key || "",
|
||||
title: task.name || "",
|
||||
description: task.description || "",
|
||||
// Use dynamic status mapping from database
|
||||
status: statusCategoryMap[task.status] || task.status,
|
||||
// Pre-processed priority using mapping
|
||||
status: task.status || "todo",
|
||||
priority: priorityMap[task.priority_value?.toString()] || "medium",
|
||||
// Use actual phase name from database
|
||||
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.assignee_names || task.names || [],
|
||||
assignee_names: task.assignees || [],
|
||||
labels: task.labels?.map((l: any) => ({
|
||||
id: l.id || l.label_id,
|
||||
name: l.name,
|
||||
@@ -1090,7 +1144,7 @@ export default class TasksControllerV2 extends TasksControllerBase {
|
||||
logged: convertTimeValue(task.time_spent),
|
||||
},
|
||||
customFields: {},
|
||||
custom_column_values: task.custom_column_values || {}, // Include custom column values
|
||||
custom_column_values: task.custom_column_values || {},
|
||||
createdAt: task.created_at || new Date().toISOString(),
|
||||
updatedAt: task.updated_at || new Date().toISOString(),
|
||||
order: typeof task.sort_order === "number" ? task.sort_order : 0,
|
||||
@@ -1109,124 +1163,53 @@ export default class TasksControllerV2 extends TasksControllerBase {
|
||||
schedule_id: task.schedule_id || null,
|
||||
};
|
||||
});
|
||||
const transformEndTime = performance.now();
|
||||
|
||||
// Create groups based on dynamic data from database
|
||||
const groupingStartTime = performance.now();
|
||||
const groupedResponse: Record<string, any> = {};
|
||||
|
||||
// Initialize groups from database data
|
||||
groups.forEach(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, "_");
|
||||
|
||||
groupedResponse[groupKey] = {
|
||||
id: group.id,
|
||||
title: group.name,
|
||||
groupType: groupBy,
|
||||
groupValue: groupKey,
|
||||
collapsed: false,
|
||||
tasks: [],
|
||||
taskIds: [],
|
||||
color: group.color_code || this.getDefaultGroupColor(groupBy, groupKey),
|
||||
// Include additional metadata from database
|
||||
category_id: group.category_id,
|
||||
start_date: group.start_date,
|
||||
end_date: group.end_date,
|
||||
sort_index: (group as any).sort_index,
|
||||
};
|
||||
});
|
||||
// Transform groups to V3 format while preserving the getList logic
|
||||
const responseGroups = updatedGroups.map(group => {
|
||||
// Create status category mapping for consistent group naming
|
||||
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, "_");
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
});
|
||||
// Transform tasks in this group to V3 format
|
||||
const groupTasks = group.tasks.map(task => {
|
||||
const foundTask = transformedTasks.find(t => t.id === task.id);
|
||||
return foundTask || task;
|
||||
});
|
||||
|
||||
// Create unmapped group if there are tasks without proper phase assignment
|
||||
if (unmappedTasks.length > 0 && groupBy === GroupBy.PHASE) {
|
||||
groupedResponse[UNMAPPED.toLowerCase()] = {
|
||||
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
|
||||
};
|
||||
}
|
||||
|
||||
// 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();
|
||||
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),
|
||||
// Include additional metadata from database
|
||||
category_id: group.category_id,
|
||||
start_date: group.start_date,
|
||||
end_date: group.end_date,
|
||||
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");
|
||||
|
||||
const endTime = performance.now();
|
||||
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 request is taking too long
|
||||
// Log warning if this method is taking too long
|
||||
if (totalTime > 1000) {
|
||||
console.warn(`[PERFORMANCE WARNING] Slow request detected: ${totalTime.toFixed(2)}ms for project ${req.params.id} with ${transformedTasks.length} tasks`);
|
||||
console.warn(`[PERFORMANCE WARNING] getTasksV3 method taking ${totalTime.toFixed(2)}ms - Consider optimizing the query or data processing!`);
|
||||
}
|
||||
|
||||
return res.status(200).send(new ServerResponse(true, {
|
||||
@@ -1237,6 +1220,315 @@ 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 {
|
||||
const colorMaps: Record<string, Record<string, string>> = {
|
||||
[GroupBy.STATUS]: {
|
||||
@@ -1332,4 +1624,6 @@ export default class TasksControllerV2 extends TasksControllerBase {
|
||||
return res.status(500).send(new ServerResponse(false, null, "Failed to get task progress status"));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -2,31 +2,35 @@
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<title></title>
|
||||
<title>Worklenz 2.1.0 Release</title>
|
||||
<meta name="subject" content="Worklenz 2.1.0 Release" />
|
||||
<meta content="text/html; charset=utf-8" http-equiv="Content-Type">
|
||||
<meta content="width=device-width,initial-scale=1" name="viewport">
|
||||
<style>
|
||||
* {
|
||||
box-sizing: border-box
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0
|
||||
padding: 0;
|
||||
background: #f6f8fa;
|
||||
font-family: 'Mada', 'Segoe UI', Arial, sans-serif;
|
||||
color: #222;
|
||||
}
|
||||
|
||||
a[x-apple-data-detectors] {
|
||||
color: inherit !important;
|
||||
text-decoration: inherit !important
|
||||
text-decoration: inherit !important;
|
||||
}
|
||||
|
||||
#MessageViewBody a {
|
||||
color: inherit;
|
||||
text-decoration: none
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
p {
|
||||
line-height: inherit
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.padding-30 {
|
||||
@@ -37,272 +41,201 @@
|
||||
padding: 0px 20px;
|
||||
}
|
||||
|
||||
.desktop_hide,
|
||||
.desktop_hide table {
|
||||
mso-hide: all;
|
||||
display: none;
|
||||
max-height: 0;
|
||||
overflow: hidden
|
||||
.card {
|
||||
background: #fff;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 2px 12px rgba(24, 144, 255, 0.08);
|
||||
margin-bottom: 32px;
|
||||
padding: 32px 32px 24px 32px;
|
||||
transition: box-shadow 0.2s;
|
||||
}
|
||||
|
||||
@media (max-width: 525px) {
|
||||
.desktop_hide table.icons-inner {
|
||||
display: inline-block !important
|
||||
.card h3 {
|
||||
color: #1890ff;
|
||||
margin-top: 0;
|
||||
margin-bottom: 12px;
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
.card img {
|
||||
border-radius: 10px;
|
||||
margin: 18px 0 0 0;
|
||||
box-shadow: 0 1px 8px rgba(24, 144, 255, 0.07);
|
||||
max-width: 100%;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.feature-list {
|
||||
padding-left: 18px;
|
||||
margin: 0 0 12px 0;
|
||||
}
|
||||
|
||||
.feature-list li {
|
||||
margin-bottom: 6px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.lang-badge {
|
||||
display: inline-block;
|
||||
background: #e6f7ff;
|
||||
color: #1890ff;
|
||||
border-radius: 8px;
|
||||
padding: 3px 10px;
|
||||
font-size: 14px;
|
||||
margin-right: 8px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.main-btn {
|
||||
background: #1890ff;
|
||||
border: none;
|
||||
outline: none;
|
||||
padding: 14px 28px;
|
||||
font-size: 18px;
|
||||
text-decoration: none;
|
||||
color: white;
|
||||
border-radius: 23px;
|
||||
margin: 32px auto 0 auto;
|
||||
font-family: 'Mada', sans-serif;
|
||||
display: inline-block;
|
||||
box-shadow: 0 2px 8px rgba(24, 144, 255, 0.13);
|
||||
transition: background 0.2s, color 0.2s, border 0.2s;
|
||||
border: 2px solid #1890ff;
|
||||
}
|
||||
|
||||
.main-btn:hover {
|
||||
background: #40a9ff;
|
||||
color: #fff;
|
||||
border-color: #40a9ff;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.card {
|
||||
padding: 18px 8px 16px 8px;
|
||||
}
|
||||
|
||||
.icons-inner {
|
||||
text-align: center
|
||||
.main-btn {
|
||||
width: 90%;
|
||||
font-size: 16px;
|
||||
padding: 12px 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
body {
|
||||
background: #181a1b;
|
||||
color: #e6e6e6;
|
||||
}
|
||||
|
||||
.icons-inner td {
|
||||
margin: 0 auto
|
||||
.card {
|
||||
background: #23272a;
|
||||
box-shadow: 0 2px 12px rgba(24, 144, 255, 0.13);
|
||||
}
|
||||
|
||||
.row-content {
|
||||
width: 95% !important
|
||||
.main-btn {
|
||||
background: #1890ff;
|
||||
color: #fff;
|
||||
border: 2px solid #1890ff;
|
||||
}
|
||||
|
||||
.mobile_hide {
|
||||
display: none
|
||||
.main-btn:hover {
|
||||
background: #40a9ff;
|
||||
color: #fff;
|
||||
border-color: #40a9ff;
|
||||
}
|
||||
|
||||
.stack .column {
|
||||
width: 100%;
|
||||
display: block
|
||||
.logo-light {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.mobile_hide {
|
||||
min-height: 0;
|
||||
max-height: 0;
|
||||
max-width: 0;
|
||||
overflow: hidden;
|
||||
font-size: 0
|
||||
.logo-dark {
|
||||
display: block !important;
|
||||
}
|
||||
}
|
||||
|
||||
.desktop_hide,
|
||||
.desktop_hide table {
|
||||
display: table !important;
|
||||
max-height: none !important
|
||||
}
|
||||
.logo-light {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.logo-dark {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body style="background-color:#fff;margin:0;padding:0;-webkit-text-size-adjust:none;text-size-adjust:none">
|
||||
<table border="0" cellpadding="0" cellspacing="0" class="nl-container" role="presentation"
|
||||
style="mso-table-lspace:0;mso-table-rspace:0;background-image:none;background-position:top left;background-size:auto;background-repeat:no-repeat"
|
||||
width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" class="row row-1" role="presentation"
|
||||
style="mso-table-lspace:0;mso-table-rspace:0" width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
<table border="0" cellpadding="0" cellspacing="0" class="image_block block-1 padding-20"
|
||||
role="presentation" style="mso-table-lspace:0;mso-table-rspace:0;margin-left: auto;margin-right: auto;padding-top: 20px;
|
||||
padding-bottom: 20px;" width="300">
|
||||
<tr>
|
||||
<td class="pad" style="width:100%;padding-right:0;padding-left:0">
|
||||
<div align="left" class="alignment" style="line-height:10px">
|
||||
<a href="https://worklenz.com" style="outline:none;width: 170px;" tabindex="-1"
|
||||
target="_blank"><img
|
||||
src="https://worklenz.s3.amazonaws.com/email-templates-assets/email-logo.png"
|
||||
style="display:block;max-width: 300px;height:auto;border:0;max-width:100%;margin-top: 10px;margin-bottom: 0px;"></a>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" class="row-content stack"
|
||||
role="presentation" style="mso-table-lspace:0;mso-table-rspace:0;color:#000;width:720px;"
|
||||
width="475">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="column column-1"
|
||||
style="mso-table-lspace:0;mso-table-rspace:0;font-weight:400;text-align:left;vertical-align:top;padding-top:5px;padding-bottom:5px;border-top:0;border-right:0;border-bottom:0;border-left:0"
|
||||
width="100%">
|
||||
|
||||
<table border="0" cellpadding="0" cellspacing="0" class="image_block block-3"
|
||||
role="presentation" style="mso-table-lspace:0;mso-table-rspace:0" width="100%">
|
||||
|
||||
<tr>
|
||||
<td class="pad" style="width:100%;padding-right:0;padding-left:0">
|
||||
<div align="center" class="alignment" style="line-height:10px"><img
|
||||
src="https://worklenz.s3.amazonaws.com/email-templates-assets/under-maintenance.png"
|
||||
style="display:block;height:auto;border:0;width:180px;max-width:100%;/* margin-top: 30px; */margin-bottom: 10px;"
|
||||
width="180">
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<table border="0" cellpadding="8" cellspacing="0" class="paragraph_block block-5 padding-30"
|
||||
role="presentation" style="mso-table-lspace:0;mso-table-rspace:0;word-break:break-word"
|
||||
width="100%">
|
||||
<tr>
|
||||
<td class="pad">
|
||||
<div
|
||||
style="color:#505771;font-size:16px;font-family: 'Mada', sans-serif;font-weight:400;line-height:135%;direction:ltr;letter-spacing:0;border-radius: 12px;margin-bottom: 12px;">
|
||||
<h3 style="margin-bottom: 0;">Project Roadmap Redesign</h3>
|
||||
<p>
|
||||
|
||||
Experience a comprehensive visual representation of task progression within your projects.
|
||||
The sequential arrangement unfolds seamlessly in a user-friendly timeline format, allowing
|
||||
for effortless understanding and efficient project management.
|
||||
</p>
|
||||
<img src="https://worklenz.s3.amazonaws.com/gifs/WL20231114/roadmap.gif"
|
||||
style="width: 100%;margin: auto;" alt="Revamped Reporting">
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<table border="0" cellpadding="8" cellspacing="0" class="paragraph_block block-5 padding-30"
|
||||
role="presentation" style="mso-table-lspace:0;mso-table-rspace:0;word-break:break-word"
|
||||
width="100%">
|
||||
<tr>
|
||||
<td class="pad">
|
||||
<div
|
||||
style="color:#505771;font-size:16px;font-family: 'Mada', sans-serif;font-weight:400;line-height:135%;direction:ltr;letter-spacing:0;border-radius: 12px;margin-bottom: 12px;">
|
||||
<h3 style="margin-bottom: 0;">Project Workload Redesign</h3>
|
||||
<p>
|
||||
Gain insights into the optimized allocation and utilization of resources within your project.
|
||||
</p>
|
||||
<img src="https://worklenz.s3.amazonaws.com/gifs/WL20231114/workload-1.gif"
|
||||
style="width: 100%;margin: auto;" alt="Revamped Reporting">
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<table border="0" cellpadding="8" cellspacing="0" class="paragraph_block block-5 padding-30"
|
||||
role="presentation" style="mso-table-lspace:0;mso-table-rspace:0;word-break:break-word"
|
||||
width="100%">
|
||||
<tr>
|
||||
<td class="pad">
|
||||
<div
|
||||
style="color:#505771;font-size:16px;font-family: 'Mada', sans-serif;font-weight:400;line-height:135%;direction:ltr;letter-spacing:0;border-radius: 12px;margin-bottom: 12px;">
|
||||
<h3 style="margin-bottom: 0;">Create new tasks from the roadmap itself</h3>
|
||||
<p>
|
||||
Effortlessly generate and modify tasks directly from the roadmap interface with a simple
|
||||
click-and-drag functionality.
|
||||
<br>Seamlessly adjust the task's date range according to your
|
||||
preferences, providing a user-friendly and intuitive experience for efficient task management.
|
||||
</p>
|
||||
<img src="https://worklenz.s3.amazonaws.com/gifs/WL20231114/roadmap-2.gif"
|
||||
style="width: 100%;margin: auto;" alt="Revamped Reporting">
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<table border="0" cellpadding="8" cellspacing="0" class="paragraph_block block-5 padding-30"
|
||||
role="presentation" style="mso-table-lspace:0;mso-table-rspace:0;word-break:break-word"
|
||||
width="100%">
|
||||
<tr>
|
||||
<td class="pad">
|
||||
<div
|
||||
style="color:#505771;font-size:16px;font-family: 'Mada', sans-serif;font-weight:400;line-height:135%;direction:ltr;letter-spacing:0;border-radius: 12px;margin-bottom: 12px;">
|
||||
<h3 style="margin-bottom: 0;">Deactivate Team Members</h3>
|
||||
<p>
|
||||
Effortlessly manage your team by deactivating members without losing their valuable work.
|
||||
<br>
|
||||
<br>
|
||||
Navigate to the "Settings" section and access "Team Members" to conveniently deactivate
|
||||
team members while preserving the work they have contributed.
|
||||
</p>
|
||||
<img src="https://worklenz.s3.amazonaws.com/gifs/WL20231114/workload-1.gif"
|
||||
style="width: 100%;margin: auto;" alt="Revamped Reporting">
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<table border="0" cellpadding="8" cellspacing="0" class="paragraph_block block-5 padding-30"
|
||||
role="presentation" style="mso-table-lspace:0;mso-table-rspace:0;word-break:break-word"
|
||||
width="100%">
|
||||
<tr>
|
||||
<td class="pad">
|
||||
<div
|
||||
style="color:#505771;font-size:16px;font-family: 'Mada', sans-serif;font-weight:400;line-height:135%;direction:ltr;letter-spacing:0;border-radius: 12px;margin-bottom: 12px;">
|
||||
<h3 style="margin-bottom: 0;">Reporting Enhancements</h3>
|
||||
<p>
|
||||
This release also includes several other miscellaneous bug fixes and performance
|
||||
enhancements to further improve your experience.
|
||||
</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<div style="text-align: center;">
|
||||
<a href="https://worklenz.com/worklenz" target="_blank"
|
||||
style="background: #1890ff;border: none;outline: none;padding: 12px 16px;font-size: 18px;text-decoration: none;color: white;border-radius: 23px;margin: auto;font-family: 'Mada', sans-serif;">See
|
||||
what's new</a>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" class="row row-2" role="presentation"
|
||||
style="mso-table-lspace:0;mso-table-rspace:0" width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" class="row-content stack"
|
||||
role="presentation" style="mso-table-lspace:0;mso-table-rspace:0;color:#000;width:475px"
|
||||
width="505">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="column column-1"
|
||||
style="mso-table-lspace:0;mso-table-rspace:0;font-weight:400;text-align:left;vertical-align:top;padding-top:5px;padding-bottom:5px;border-top:0;border-right:0;border-bottom:0;border-left:0"
|
||||
width="100%">
|
||||
<table border="0" cellpadding="0" cellspacing="0" class="icons_block block-1"
|
||||
role="presentation" style="mso-table-lspace:0;mso-table-rspace:0" width="100%">
|
||||
<tr>
|
||||
<td class="pad"
|
||||
style="vertical-align:middle;color:#9d9d9d;font-family:inherit;font-size:15px;padding-bottom:5px;padding-top:5px;text-align:center">
|
||||
<table cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="mso-table-lspace:0;mso-table-rspace:0" width="100%">
|
||||
<tr>
|
||||
<td class="alignment" style="vertical-align:middle;text-align:center">
|
||||
<!--[if vml]>
|
||||
<table align="left" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="display:inline-block;padding-left:0px;padding-right:0px;mso-table-lspace: 0pt;mso-table-rspace: 0pt;">
|
||||
<![endif]-->
|
||||
<!--[if !vml]><!-->
|
||||
<table cellpadding="0" cellspacing="0" class="icons-inner" role="presentation"
|
||||
style="mso-table-lspace:0;mso-table-rspace:0;display:inline-block;margin-right:-4px;padding-left:0;padding-right:0">
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table><!-- End -->
|
||||
<hr>
|
||||
<p style="font-family:sans-serif;text-decoration:none; text-align: center;">
|
||||
Click <a href="{{{unsubscribe}}}" target="_blank">here</a> to unsubscribe and manage your email preferences.
|
||||
</p>
|
||||
<body>
|
||||
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="background: #f6f8fa;">
|
||||
<tr>
|
||||
<td align="center">
|
||||
<table border="0" cellpadding="0" cellspacing="0" width="720" style="max-width: 98vw;">
|
||||
<tr>
|
||||
<td align="center" style="padding: 32px 0 18px 0;">
|
||||
<a href="https://worklenz.com" target="_blank" style="display: inline-block;">
|
||||
<img class="logo-light"
|
||||
src="https://s3.us-west-2.amazonaws.com/worklenz.com/assets/worklenz-light-mode.png"
|
||||
alt="Worklenz Light Logo" style="width: 170px; margin-bottom: 0; display: block;" />
|
||||
<img class="logo-dark"
|
||||
src="https://s3.us-west-2.amazonaws.com/worklenz.com/assets/worklenz-dark-mode.png"
|
||||
alt="Worklenz Dark Logo" style="width: 170px; margin-bottom: 0; display: none;" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<div class="card">
|
||||
<h3>🚀 New Tasks List & Kanban Board</h3>
|
||||
<ul class="feature-list">
|
||||
<li>Performance optimized for faster loading</li>
|
||||
<li>Redesigned UI for clarity and speed</li>
|
||||
<li>Advanced filters for easier task management</li>
|
||||
</ul>
|
||||
<img src="https://s3.us-west-2.amazonaws.com/worklenz.com/gifs/WL20250708/task-list-v2.gif"
|
||||
alt="New Task List">
|
||||
<img src="https://s3.us-west-2.amazonaws.com/worklenz.com/gifs/WL20250708/kanban-v2.gif"
|
||||
alt="New Kanban Board">
|
||||
</div>
|
||||
<div class="card">
|
||||
<h3>📁 Group View in Projects List</h3>
|
||||
<ul class="feature-list">
|
||||
<li>Toggle between list and group view</li>
|
||||
<li>Group projects by client or category</li>
|
||||
<li>Improved navigation and organization</li>
|
||||
</ul>
|
||||
<img src="https://s3.us-west-2.amazonaws.com/worklenz.com/gifs/WL20250708/project-list-group-view.gif"
|
||||
alt="Project List Group View">
|
||||
</div>
|
||||
<div class="card">
|
||||
<h3>🌐 New Language Support</h3>
|
||||
<span class="lang-badge">Deutsch (DE)</span>
|
||||
<span class="lang-badge">Shqip (ALB)</span>
|
||||
<p style="margin-top: 10px;">Worklenz is now available in German and Albanian!</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h3>🛠️ Bug Fixes & UI Improvements</h3>
|
||||
<ul class="feature-list">
|
||||
<li>General bug fixes</li>
|
||||
<li>UI/UX enhancements for a smoother experience</li>
|
||||
<li>Performance improvements across the platform</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div style="text-align: center;">
|
||||
<a href="https://app.worklenz.com/auth" target="_blank" class="main-btn">See what's new</a>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 32px 0 0 0;">
|
||||
<hr style="border: none; border-top: 1px solid #e6e6e6; margin: 32px 0 16px 0;">
|
||||
<p style="font-family:sans-serif;text-decoration:none; text-align: center; color: #888; font-size: 15px;">
|
||||
Click <a href="{{unsubscribe}}" target="_blank" style="color: #1890ff;">here</a> to unsubscribe and
|
||||
manage your email preferences.
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
</html>
|
||||
@@ -5,43 +5,72 @@
|
||||
<link rel="icon" href="./favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#2b2b2b" />
|
||||
|
||||
<!-- Resource hints for better loading performance -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link rel="dns-prefetch" href="https://www.googletagmanager.com" />
|
||||
<link rel="dns-prefetch" href="https://js.hs-scripts.com" />
|
||||
|
||||
<!-- Preload critical resources -->
|
||||
<link rel="preload" href="/locales/en/common.json" as="fetch" type="application/json" crossorigin />
|
||||
<link rel="preload" href="/locales/en/auth/login.json" as="fetch" type="application/json" crossorigin />
|
||||
<link rel="preload" href="/locales/en/navbar.json" as="fetch" type="application/json" crossorigin />
|
||||
|
||||
<!-- Optimized font loading with font-display: swap -->
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Inter:wght@100;200;300;400;500;600;700;800;900&display=swap"
|
||||
rel="stylesheet"
|
||||
media="print"
|
||||
onload="this.media='all'"
|
||||
/>
|
||||
<noscript>
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Inter:wght@100;200;300;400;500;600;700;800;900&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
</noscript>
|
||||
|
||||
<title>Worklenz</title>
|
||||
|
||||
<!-- Environment configuration -->
|
||||
<script src="/env-config.js"></script>
|
||||
<!-- Google Analytics -->
|
||||
|
||||
<!-- Optimized Google Analytics with reduced blocking -->
|
||||
<script>
|
||||
// Function to initialize Google Analytics
|
||||
// Function to initialize Google Analytics asynchronously
|
||||
function initGoogleAnalytics() {
|
||||
// Load the Google Analytics script
|
||||
const script = document.createElement('script');
|
||||
script.async = true;
|
||||
// Use requestIdleCallback to defer analytics loading
|
||||
const loadAnalytics = () => {
|
||||
// Determine which tracking ID to use based on the environment
|
||||
const isProduction = window.location.hostname === 'app.worklenz.com';
|
||||
|
||||
// Determine which tracking ID to use based on the environment
|
||||
const isProduction =
|
||||
window.location.hostname === 'worklenz.com' ||
|
||||
window.location.hostname === 'app.worklenz.com';
|
||||
const trackingId = isProduction ? 'G-7KSRKQ1397' : 'G-3LM2HGWEXG'; // Open source tracking ID
|
||||
|
||||
const trackingId = isProduction ? 'G-XXXXXXXXXX' : 'G-3LM2HGWEXG'; // Open source tracking ID
|
||||
// Load the Google Analytics script
|
||||
const script = document.createElement('script');
|
||||
script.async = true;
|
||||
script.src = `https://www.googletagmanager.com/gtag/js?id=${trackingId}`;
|
||||
document.head.appendChild(script);
|
||||
|
||||
script.src = `https://www.googletagmanager.com/gtag/js?id=${trackingId}`;
|
||||
document.head.appendChild(script);
|
||||
// Initialize Google Analytics
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
function gtag() {
|
||||
dataLayer.push(arguments);
|
||||
}
|
||||
gtag('js', new Date());
|
||||
gtag('config', trackingId);
|
||||
};
|
||||
|
||||
// Initialize Google Analytics
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
function gtag() {
|
||||
dataLayer.push(arguments);
|
||||
// Use requestIdleCallback if available, otherwise setTimeout
|
||||
if ('requestIdleCallback' in window) {
|
||||
requestIdleCallback(loadAnalytics, { timeout: 2000 });
|
||||
} else {
|
||||
setTimeout(loadAnalytics, 1000);
|
||||
}
|
||||
gtag('js', new Date());
|
||||
gtag('config', trackingId);
|
||||
}
|
||||
|
||||
// Initialize analytics
|
||||
// Initialize analytics after a delay to not block initial render
|
||||
initGoogleAnalytics();
|
||||
|
||||
// Function to show privacy notice
|
||||
@@ -98,14 +127,24 @@
|
||||
<div id="root"></div>
|
||||
<script type="module" src="./src/index.tsx"></script>
|
||||
<script type="text/javascript">
|
||||
// Load HubSpot script asynchronously and only for production
|
||||
if (window.location.hostname === 'app.worklenz.com') {
|
||||
var hs = document.createElement('script');
|
||||
hs.type = 'text/javascript';
|
||||
hs.id = 'hs-script-loader';
|
||||
hs.async = true;
|
||||
hs.defer = true;
|
||||
hs.src = '//js.hs-scripts.com/22348300.js';
|
||||
document.body.appendChild(hs);
|
||||
// Use requestIdleCallback to defer HubSpot loading
|
||||
const loadHubSpot = () => {
|
||||
var hs = document.createElement('script');
|
||||
hs.type = 'text/javascript';
|
||||
hs.id = 'hs-script-loader';
|
||||
hs.async = true;
|
||||
hs.defer = true;
|
||||
hs.src = '//js.hs-scripts.com/22348300.js';
|
||||
document.body.appendChild(hs);
|
||||
};
|
||||
|
||||
if ('requestIdleCallback' in window) {
|
||||
requestIdleCallback(loadHubSpot, { timeout: 3000 });
|
||||
} else {
|
||||
setTimeout(loadHubSpot, 2000);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
|
||||
541
worklenz-frontend/package-lock.json
generated
541
worklenz-frontend/package-lock.json
generated
@@ -722,6 +722,27 @@
|
||||
"react": ">=16.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@asamuzakjp/css-color": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz",
|
||||
"integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@csstools/css-calc": "^2.1.3",
|
||||
"@csstools/css-color-parser": "^3.0.9",
|
||||
"@csstools/css-parser-algorithms": "^3.0.4",
|
||||
"@csstools/css-tokenizer": "^3.0.3",
|
||||
"lru-cache": "^10.4.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@asamuzakjp/css-color/node_modules/lru-cache": {
|
||||
"version": "10.4.3",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
|
||||
"integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/@babel/code-frame": {
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
|
||||
@@ -1014,6 +1035,121 @@
|
||||
"react": ">=16.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@csstools/color-helpers": {
|
||||
"version": "5.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.0.2.tgz",
|
||||
"integrity": "sha512-JqWH1vsgdGcw2RR6VliXXdA0/59LttzlU8UlRT/iUUsEeWfYq8I+K0yhihEUTTHLRm1EXvpsCx3083EU15ecsA==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/csstools"
|
||||
},
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/csstools"
|
||||
}
|
||||
],
|
||||
"license": "MIT-0",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@csstools/css-calc": {
|
||||
"version": "2.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz",
|
||||
"integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/csstools"
|
||||
},
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/csstools"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@csstools/css-parser-algorithms": "^3.0.5",
|
||||
"@csstools/css-tokenizer": "^3.0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@csstools/css-color-parser": {
|
||||
"version": "3.0.10",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.0.10.tgz",
|
||||
"integrity": "sha512-TiJ5Ajr6WRd1r8HSiwJvZBiJOqtH86aHpUjq5aEKWHiII2Qfjqd/HCWKPOW8EP4vcspXbHnXrwIDlu5savQipg==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/csstools"
|
||||
},
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/csstools"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@csstools/color-helpers": "^5.0.2",
|
||||
"@csstools/css-calc": "^2.1.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@csstools/css-parser-algorithms": "^3.0.5",
|
||||
"@csstools/css-tokenizer": "^3.0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@csstools/css-parser-algorithms": {
|
||||
"version": "3.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz",
|
||||
"integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/csstools"
|
||||
},
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/csstools"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@csstools/css-tokenizer": "^3.0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@csstools/css-tokenizer": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz",
|
||||
"integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/csstools"
|
||||
},
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/csstools"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@ctrl/tinycolor": {
|
||||
"version": "3.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-3.6.1.tgz",
|
||||
@@ -2855,6 +2991,16 @@
|
||||
"object-assign": "4.x"
|
||||
}
|
||||
},
|
||||
"node_modules/agent-base": {
|
||||
"version": "7.1.4",
|
||||
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
|
||||
"integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 14"
|
||||
}
|
||||
},
|
||||
"node_modules/ansi-regex": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||
@@ -3586,12 +3732,77 @@
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/cssstyle": {
|
||||
"version": "4.6.0",
|
||||
"resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz",
|
||||
"integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@asamuzakjp/css-color": "^3.2.0",
|
||||
"rrweb-cssom": "^0.8.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/csstype": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
||||
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/data-urls": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz",
|
||||
"integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"whatwg-mimetype": "^4.0.0",
|
||||
"whatwg-url": "^14.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/data-urls/node_modules/tr46": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz",
|
||||
"integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"punycode": "^2.3.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/data-urls/node_modules/webidl-conversions": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
|
||||
"integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/data-urls/node_modules/whatwg-url": {
|
||||
"version": "14.2.0",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz",
|
||||
"integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tr46": "^5.1.0",
|
||||
"webidl-conversions": "^7.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/date-fns": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz",
|
||||
@@ -3625,6 +3836,13 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/decimal.js": {
|
||||
"version": "10.6.0",
|
||||
"resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz",
|
||||
"integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/deep-eql": {
|
||||
"version": "5.0.2",
|
||||
"resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz",
|
||||
@@ -3792,6 +4010,19 @@
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/entities": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz",
|
||||
"integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/error-ex": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
|
||||
@@ -4262,6 +4493,19 @@
|
||||
"react-is": "^16.7.0"
|
||||
}
|
||||
},
|
||||
"node_modules/html-encoding-sniffer": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz",
|
||||
"integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"whatwg-encoding": "^3.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/html-parse-stringify": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz",
|
||||
@@ -4284,6 +4528,34 @@
|
||||
"node": ">=8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/http-proxy-agent": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
|
||||
"integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"agent-base": "^7.1.0",
|
||||
"debug": "^4.3.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14"
|
||||
}
|
||||
},
|
||||
"node_modules/https-proxy-agent": {
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
|
||||
"integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"agent-base": "^7.1.2",
|
||||
"debug": "4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14"
|
||||
}
|
||||
},
|
||||
"node_modules/hyphenate-style-name": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/hyphenate-style-name/-/hyphenate-style-name-1.1.0.tgz",
|
||||
@@ -4331,6 +4603,19 @@
|
||||
"cross-fetch": "4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/iconv-lite": {
|
||||
"version": "0.6.3",
|
||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
|
||||
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/immer": {
|
||||
"version": "10.1.1",
|
||||
"resolved": "https://registry.npmjs.org/immer/-/immer-10.1.1.tgz",
|
||||
@@ -4439,6 +4724,13 @@
|
||||
"node": ">=0.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/is-potential-custom-element-name": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz",
|
||||
"integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/isexe": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
||||
@@ -4520,6 +4812,105 @@
|
||||
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/jsdom": {
|
||||
"version": "26.1.0",
|
||||
"resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz",
|
||||
"integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cssstyle": "^4.2.1",
|
||||
"data-urls": "^5.0.0",
|
||||
"decimal.js": "^10.5.0",
|
||||
"html-encoding-sniffer": "^4.0.0",
|
||||
"http-proxy-agent": "^7.0.2",
|
||||
"https-proxy-agent": "^7.0.6",
|
||||
"is-potential-custom-element-name": "^1.0.1",
|
||||
"nwsapi": "^2.2.16",
|
||||
"parse5": "^7.2.1",
|
||||
"rrweb-cssom": "^0.8.0",
|
||||
"saxes": "^6.0.0",
|
||||
"symbol-tree": "^3.2.4",
|
||||
"tough-cookie": "^5.1.1",
|
||||
"w3c-xmlserializer": "^5.0.0",
|
||||
"webidl-conversions": "^7.0.0",
|
||||
"whatwg-encoding": "^3.1.1",
|
||||
"whatwg-mimetype": "^4.0.0",
|
||||
"whatwg-url": "^14.1.1",
|
||||
"ws": "^8.18.0",
|
||||
"xml-name-validator": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"canvas": "^3.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"canvas": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/jsdom/node_modules/tr46": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz",
|
||||
"integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"punycode": "^2.3.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/jsdom/node_modules/webidl-conversions": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
|
||||
"integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/jsdom/node_modules/whatwg-url": {
|
||||
"version": "14.2.0",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz",
|
||||
"integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tr46": "^5.1.0",
|
||||
"webidl-conversions": "^7.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/jsdom/node_modules/ws": {
|
||||
"version": "8.18.3",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
|
||||
"integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bufferutil": "^4.0.1",
|
||||
"utf-8-validate": ">=5.0.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bufferutil": {
|
||||
"optional": true
|
||||
},
|
||||
"utf-8-validate": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/jsesc": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
|
||||
@@ -5137,6 +5528,13 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/nwsapi": {
|
||||
"version": "2.2.20",
|
||||
"resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.20.tgz",
|
||||
"integrity": "sha512-/ieB+mDe4MrrKMT8z+mQL8klXydZWGR5Dowt4RAGKbJ3kIGEx3X4ljUo+6V73IXtUPWgfOlU5B9MlGxFO5T+cA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/object-assign": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||
@@ -5191,6 +5589,19 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/parse5": {
|
||||
"version": "7.3.0",
|
||||
"resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz",
|
||||
"integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"entities": "^6.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/inikulin/parse5?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/path-key": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
|
||||
@@ -5643,6 +6054,16 @@
|
||||
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/punycode": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
||||
"integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/queue-microtask": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
|
||||
@@ -6779,6 +7200,13 @@
|
||||
"rrweb-snapshot": "^2.0.0-alpha.18"
|
||||
}
|
||||
},
|
||||
"node_modules/rrweb-cssom": {
|
||||
"version": "0.8.0",
|
||||
"resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz",
|
||||
"integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/rrweb-snapshot": {
|
||||
"version": "2.0.0-alpha.18",
|
||||
"resolved": "https://registry.npmjs.org/rrweb-snapshot/-/rrweb-snapshot-2.0.0-alpha.18.tgz",
|
||||
@@ -6820,6 +7248,26 @@
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/safer-buffer": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/saxes": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz",
|
||||
"integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"xmlchars": "^2.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=v12.22.7"
|
||||
}
|
||||
},
|
||||
"node_modules/scheduler": {
|
||||
"version": "0.23.2",
|
||||
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
|
||||
@@ -7210,6 +7658,13 @@
|
||||
"react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/symbol-tree": {
|
||||
"version": "3.2.4",
|
||||
"resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
|
||||
"integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tailwindcss": {
|
||||
"version": "3.4.17",
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz",
|
||||
@@ -7422,6 +7877,26 @@
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tldts": {
|
||||
"version": "6.1.86",
|
||||
"resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz",
|
||||
"integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tldts-core": "^6.1.86"
|
||||
},
|
||||
"bin": {
|
||||
"tldts": "bin/cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/tldts-core": {
|
||||
"version": "6.1.86",
|
||||
"resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz",
|
||||
"integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/to-regex-range": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
||||
@@ -7440,6 +7915,19 @@
|
||||
"integrity": "sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tough-cookie": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz",
|
||||
"integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"tldts": "^6.1.32"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
}
|
||||
},
|
||||
"node_modules/tr46": {
|
||||
"version": "0.0.3",
|
||||
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
|
||||
@@ -7790,6 +8278,19 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/w3c-xmlserializer": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz",
|
||||
"integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"xml-name-validator": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/warning": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz",
|
||||
@@ -7811,6 +8312,29 @@
|
||||
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
|
||||
"license": "BSD-2-Clause"
|
||||
},
|
||||
"node_modules/whatwg-encoding": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz",
|
||||
"integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"iconv-lite": "0.6.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/whatwg-mimetype": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz",
|
||||
"integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/whatwg-url": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
|
||||
@@ -7957,6 +8481,23 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/xml-name-validator": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz",
|
||||
"integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/xmlchars": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz",
|
||||
"integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/xmlhttprequest-ssl": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz",
|
||||
|
||||
14
worklenz-frontend/public/locales/alb/project-view.json
Normal file
14
worklenz-frontend/public/locales/alb/project-view.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"taskList": "Lista e Detyrave",
|
||||
"board": "Tabela Kanban",
|
||||
"insights": "Analiza",
|
||||
"files": "Skedarë",
|
||||
"members": "Anëtarë",
|
||||
"updates": "Përditësime",
|
||||
"projectView": "Pamja e Projektit",
|
||||
"loading": "Duke ngarkuar projektin...",
|
||||
"error": "Gabim në ngarkimin e projektit",
|
||||
"pinnedTab": "E fiksuar si tab i parazgjedhur",
|
||||
"pinTab": "Fikso si tab i parazgjedhur",
|
||||
"unpinTab": "Hiqe fiksimin e tab-it të parazgjedhur"
|
||||
}
|
||||
@@ -4,14 +4,26 @@
|
||||
"createTask": "Krijo detyrë",
|
||||
"settings": "Cilësimet",
|
||||
"subscribe": "Abonohu",
|
||||
"unsubscribe": "Ç'abonohu",
|
||||
"unsubscribe": "Çabonohu",
|
||||
"deleteProject": "Fshi projektin",
|
||||
"startDate": "Data e fillimit",
|
||||
"endDate": "Data e përfundimit",
|
||||
"endDate": "Data e mbarimit",
|
||||
"projectSettings": "Cilësimet e projektit",
|
||||
"projectSummary": "Përmbledhja e projektit",
|
||||
"receiveProjectSummary": "Merrni një përmbledhje të projektit çdo mbrëmje.",
|
||||
"refreshProject": "Rifresko projektin",
|
||||
"saveAsTemplate": "Ruaje si shabllon",
|
||||
"invite": "Fto"
|
||||
"saveAsTemplate": "Ruaj si model",
|
||||
"invite": "Fto",
|
||||
"subscribeTooltip": "Abonohu tek njoftimet e projektit",
|
||||
"unsubscribeTooltip": "Çabonohu nga njoftimet e projektit",
|
||||
"refreshTooltip": "Rifresko të dhënat e projektit",
|
||||
"settingsTooltip": "Hap cilësimet e projektit",
|
||||
"saveAsTemplateTooltip": "Ruaj këtë projekt si model",
|
||||
"inviteTooltip": "Fto anëtarë të ekipit në këtë projekt",
|
||||
"createTaskTooltip": "Krijo një detyrë të re",
|
||||
"importTaskTooltip": "Importo detyrë nga modeli",
|
||||
"navigateBackTooltip": "Kthehu tek lista e projekteve",
|
||||
"projectStatusTooltip": "Statusi i projektit",
|
||||
"projectDatesInfo": "Informacion për kohëzgjatjen e projektit",
|
||||
"projectCategoryTooltip": "Kategoria e projektit"
|
||||
}
|
||||
|
||||
@@ -9,5 +9,6 @@
|
||||
"saveChanges": "Ruaj Ndryshimet",
|
||||
"profileJoinedText": "U bashkua një muaj më parë",
|
||||
"profileLastUpdatedText": "Përditësuar një muaj më parë",
|
||||
"avatarTooltip": "Klikoni për të ngarkuar një avatar"
|
||||
"avatarTooltip": "Klikoni për të ngarkuar një avatar",
|
||||
"title": "Cilësimet e Profilit"
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"title": "Anëtarët e Ekipit",
|
||||
"nameColumn": "Emri",
|
||||
"projectsColumn": "Projektet",
|
||||
"emailColumn": "Email",
|
||||
@@ -40,5 +41,7 @@
|
||||
"ownerText": "Pronar i Ekipit",
|
||||
"addedText": "Shtuar",
|
||||
"updatedText": "Përditësuar",
|
||||
"noResultFound": "Shkruani një adresë email dhe shtypni Enter..."
|
||||
"noResultFound": "Shkruani një adresë email dhe shtypni Enter...",
|
||||
"jobTitlesFetchError": "Dështoi marrja e titujve të punës",
|
||||
"invitationResent": "Ftesa u dërgua sërish me sukses!"
|
||||
}
|
||||
|
||||
16
worklenz-frontend/public/locales/alb/settings/teams.json
Normal file
16
worklenz-frontend/public/locales/alb/settings/teams.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"title": "Ekipet",
|
||||
"team": "Ekip",
|
||||
"teams": "Ekipet",
|
||||
"name": "Emri",
|
||||
"created": "Krijuar",
|
||||
"ownsBy": "I përket",
|
||||
"edit": "Ndrysho",
|
||||
"editTeam": "Ndrysho Ekipin",
|
||||
"pinTooltip": "Kliko për ta fiksuar në menunë kryesore",
|
||||
"editTeamName": "Ndrysho Emrin e Ekipit",
|
||||
"updateName": "Përditëso Emrin",
|
||||
"namePlaceholder": "Emri",
|
||||
"nameRequired": "Ju lutem shkruani një Emër",
|
||||
"updateFailed": "Ndryshimi i emrit të ekipit dështoi!"
|
||||
}
|
||||
@@ -1,28 +1,37 @@
|
||||
{
|
||||
"taskHeader": {
|
||||
"taskNamePlaceholder": "Shkruani detyrën tuaj",
|
||||
"taskNamePlaceholder": "Shkruani Detyrën tuaj",
|
||||
"deleteTask": "Fshi Detyrën"
|
||||
},
|
||||
"taskInfoTab": {
|
||||
"title": "Info",
|
||||
"title": "Informacioni",
|
||||
"details": {
|
||||
"title": "Detajet",
|
||||
"task-key": "Çelësi i Detyrës",
|
||||
"phase": "Faza",
|
||||
"assignees": "Përgjegjësit",
|
||||
"due-date": "Afati i Përfundimit",
|
||||
"assignees": "Të Caktuar",
|
||||
"due-date": "Data e Përfundimit",
|
||||
"time-estimation": "Vlerësimi i Kohës",
|
||||
"priority": "Prioriteti",
|
||||
"labels": "Etiketa",
|
||||
"billable": "Fakturueshme",
|
||||
"labels": "Etiketat",
|
||||
"billable": "E Faturueshme",
|
||||
"notify": "Njofto",
|
||||
"when-done-notify": "Kur të përfundojë, njofto",
|
||||
"when-done-notify": "Kur përfundon, njofto",
|
||||
"start-date": "Data e Fillimit",
|
||||
"end-date": "Data e Përfundimit",
|
||||
"hide-start-date": "Fshih Datën e Fillimit",
|
||||
"show-start-date": "Shfaq Datën e Fillimit",
|
||||
"hours": "Orë",
|
||||
"minutes": "Minuta"
|
||||
"minutes": "Minuta",
|
||||
"progressValue": "Vlera e Progresit",
|
||||
"progressValueTooltip": "Vendosni përqindjen e progresit (0-100%)",
|
||||
"progressValueRequired": "Ju lutemi vendosni një vlerë progresi",
|
||||
"progressValueRange": "Progresi duhet të jetë midis 0 dhe 100",
|
||||
"taskWeight": "Pesha e Detyrës",
|
||||
"taskWeightTooltip": "Vendosni peshën e kësaj nëndetyre (përqindje)",
|
||||
"taskWeightRequired": "Ju lutemi vendosni një peshë detyre",
|
||||
"taskWeightRange": "Pesha duhet të jetë midis 0 dhe 100",
|
||||
"recurring": "E Përsëritur"
|
||||
},
|
||||
"labels": {
|
||||
"labelInputPlaceholder": "Kërko ose krijo",
|
||||
@@ -30,37 +39,48 @@
|
||||
},
|
||||
"description": {
|
||||
"title": "Përshkrimi",
|
||||
"placeholder": "Shtoni një përshkrim më të detajuar..."
|
||||
"placeholder": "Shto një përshkrim më të detajuar..."
|
||||
},
|
||||
"subTasks": {
|
||||
"title": "Nën-Detyrat",
|
||||
"addSubTask": "+ Shto Nën-Detyrë",
|
||||
"addSubTaskInputPlaceholder": "Shkruani detyrën dhe shtypni Enter",
|
||||
"refreshSubTasks": "Rifresko Nën-Detyrat",
|
||||
"title": "Nëndetyrat",
|
||||
"addSubTask": "Shto Nëndetyrë",
|
||||
"addSubTaskInputPlaceholder": "Shkruani detyrën tuaj dhe shtypni enter",
|
||||
"refreshSubTasks": "Rifresko Nëndetyrat",
|
||||
"edit": "Modifiko",
|
||||
"delete": "Fshi",
|
||||
"confirmDeleteSubTask": "Jeni i sigurt që doni të fshini këtë nën-detyrë?",
|
||||
"deleteSubTask": "Fshi Nën-Detyrën"
|
||||
"confirmDeleteSubTask": "Jeni i sigurt që doni të fshini këtë nëndetyrë?",
|
||||
"deleteSubTask": "Fshi Nëndetyrën"
|
||||
},
|
||||
"dependencies": {
|
||||
"title": "Varësitë",
|
||||
"addDependency": "+ Shto varësi të re",
|
||||
"blockedBy": "I bllokuar nga",
|
||||
"searchTask": "Shkruani për të kërkuar detyra",
|
||||
"noTasksFound": "Asnjë detyrë nuk u gjet",
|
||||
"blockedBy": "Bllokuar nga",
|
||||
"searchTask": "Shkruani për të kërkuar detyrë",
|
||||
"noTasksFound": "Nuk u gjetën detyra",
|
||||
"confirmDeleteDependency": "Jeni i sigurt që doni të fshini?"
|
||||
},
|
||||
"attachments": {
|
||||
"title": "Bashkëngjitjet",
|
||||
"chooseOrDropFileToUpload": "Zgjidhni ose lëshoni skedar për ngarkim",
|
||||
"uploading": "Po ngarkohet..."
|
||||
"chooseOrDropFileToUpload": "Zgjidhni ose hidhni skedar për të ngarkuar",
|
||||
"uploading": "Duke ngarkuar..."
|
||||
},
|
||||
"comments": {
|
||||
"title": "Komentet",
|
||||
"addComment": "+ Shto koment të ri",
|
||||
"noComments": "Asnjë koment ende. Bëhu i pari që komenton!",
|
||||
"noComments": "Ende pa komente. Bëhu i pari që komenton!",
|
||||
"delete": "Fshi",
|
||||
"confirmDeleteComment": "Jeni i sigurt që doni të fshini këtë koment?"
|
||||
"confirmDeleteComment": "Jeni i sigurt që doni të fshini këtë koment?",
|
||||
"addCommentPlaceholder": "Shto një koment...",
|
||||
"cancel": "Anulo",
|
||||
"commentButton": "Komento",
|
||||
"attachFiles": "Bashkëngjit skedarë",
|
||||
"addMoreFiles": "Shto më shumë skedarë",
|
||||
"selectedFiles": "Skedarët e Zgjedhur (Deri në 25MB, Maksimumi {count})",
|
||||
"maxFilesError": "Mund të ngarkoni maksimum {count} skedarë",
|
||||
"processFilesError": "Dështoi përpunimi i skedarëve",
|
||||
"addCommentError": "Ju lutemi shtoni një koment ose bashkëngjitni skedarë",
|
||||
"createdBy": "Krijuar {time} nga {user}",
|
||||
"updatedTime": "Përditësuar {time}"
|
||||
},
|
||||
"searchInputPlaceholder": "Kërko sipas emrit",
|
||||
"pendingInvitation": "Ftesë në Pritje"
|
||||
@@ -68,11 +88,36 @@
|
||||
"taskTimeLogTab": {
|
||||
"title": "Regjistri i Kohës",
|
||||
"addTimeLog": "Shto regjistrim të ri kohe",
|
||||
"totalLogged": "Koha totale e regjistruar",
|
||||
"totalLogged": "Totali i Regjistruar",
|
||||
"exportToExcel": "Eksporto në Excel",
|
||||
"noTimeLogsFound": "Asnjë regjistrim kohe nuk u gjet"
|
||||
"noTimeLogsFound": "Nuk u gjetën regjistra kohe",
|
||||
"timeLogForm": {
|
||||
"date": "Data",
|
||||
"startTime": "Koha e Fillimit",
|
||||
"endTime": "Koha e Përfundimit",
|
||||
"workDescription": "Përshkrimi i Punës",
|
||||
"descriptionPlaceholder": "Shto një përshkrim",
|
||||
"logTime": "Regjistro kohën",
|
||||
"updateTime": "Përditëso kohën",
|
||||
"cancel": "Anulo",
|
||||
"selectDateError": "Ju lutemi zgjidhni një datë",
|
||||
"selectStartTimeError": "Ju lutemi zgjidhni kohën e fillimit",
|
||||
"selectEndTimeError": "Ju lutemi zgjidhni kohën e përfundimit",
|
||||
"endTimeAfterStartError": "Koha e përfundimit duhet të jetë pas kohës së fillimit"
|
||||
}
|
||||
},
|
||||
"taskActivityLogTab": {
|
||||
"title": "Regjistri i Aktivitetit"
|
||||
"title": "Regjistri i Aktivitetit",
|
||||
"add": "SHTO",
|
||||
"remove": "HIQE",
|
||||
"none": "Asnjë",
|
||||
"weight": "Pesha",
|
||||
"createdTask": "krijoi detyrën."
|
||||
},
|
||||
"taskProgress": {
|
||||
"markAsDoneTitle": "Shëno Detyrën si të Kryer?",
|
||||
"confirmMarkAsDone": "Po, shëno si të kryer",
|
||||
"cancelMarkAsDone": "Jo, mbaj statusin aktual",
|
||||
"markAsDoneDescription": "Keni vendosur progresin në 100%. Doni të përditësoni statusin e detyrës në \"Kryer\"?"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,6 +68,13 @@
|
||||
"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",
|
||||
|
||||
14
worklenz-frontend/public/locales/de/project-view.json
Normal file
14
worklenz-frontend/public/locales/de/project-view.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"taskList": "Aufgabenliste",
|
||||
"board": "Kanban-Board",
|
||||
"insights": "Insights",
|
||||
"files": "Dateien",
|
||||
"members": "Mitglieder",
|
||||
"updates": "Aktualisierungen",
|
||||
"projectView": "Projektansicht",
|
||||
"loading": "Projekt wird geladen...",
|
||||
"error": "Fehler beim Laden des Projekts",
|
||||
"pinnedTab": "Als Standard-Registerkarte festgesetzt",
|
||||
"pinTab": "Als Standard-Registerkarte festsetzen",
|
||||
"unpinTab": "Standard-Registerkarte lösen"
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
"createTask": "Aufgabe erstellen",
|
||||
"settings": "Einstellungen",
|
||||
"subscribe": "Abonnieren",
|
||||
"unsubscribe": "Abbestellen",
|
||||
"unsubscribe": "Abonnement beenden",
|
||||
"deleteProject": "Projekt löschen",
|
||||
"startDate": "Startdatum",
|
||||
"endDate": "Enddatum",
|
||||
@@ -13,5 +13,17 @@
|
||||
"receiveProjectSummary": "Erhalten Sie jeden Abend eine Projektzusammenfassung.",
|
||||
"refreshProject": "Projekt aktualisieren",
|
||||
"saveAsTemplate": "Als Vorlage speichern",
|
||||
"invite": "Einladen"
|
||||
"invite": "Einladen",
|
||||
"subscribeTooltip": "Projektbenachrichtigungen abonnieren",
|
||||
"unsubscribeTooltip": "Projektbenachrichtigungen beenden",
|
||||
"refreshTooltip": "Projektdaten aktualisieren",
|
||||
"settingsTooltip": "Projekteinstellungen öffnen",
|
||||
"saveAsTemplateTooltip": "Dieses Projekt als Vorlage speichern",
|
||||
"inviteTooltip": "Teammitglieder zu diesem Projekt einladen",
|
||||
"createTaskTooltip": "Neue Aufgabe erstellen",
|
||||
"importTaskTooltip": "Aufgabe aus Vorlage importieren",
|
||||
"navigateBackTooltip": "Zurück zur Projektliste",
|
||||
"projectStatusTooltip": "Projektstatus",
|
||||
"projectDatesInfo": "Informationen zum Projektzeitraum",
|
||||
"projectCategoryTooltip": "Projektkategorie"
|
||||
}
|
||||
|
||||
@@ -9,5 +9,6 @@
|
||||
"saveChanges": "Änderungen speichern",
|
||||
"profileJoinedText": "Vor einem Monat beigetreten",
|
||||
"profileLastUpdatedText": "Vor einem Monat aktualisiert",
|
||||
"avatarTooltip": "Klicken Sie zum Hochladen eines Avatars"
|
||||
"avatarTooltip": "Klicken Sie zum Hochladen eines Avatars",
|
||||
"title": "Profil-Einstellungen"
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"title": "Teammitglieder",
|
||||
"nameColumn": "Name",
|
||||
"projectsColumn": "Projekte",
|
||||
"emailColumn": "E-Mail",
|
||||
@@ -40,5 +41,7 @@
|
||||
"ownerText": "Team-Besitzer",
|
||||
"addedText": "Hinzugefügt",
|
||||
"updatedText": "Aktualisiert",
|
||||
"noResultFound": "Geben Sie eine E-Mail-Adresse ein und drücken Sie Enter..."
|
||||
"noResultFound": "Geben Sie eine E-Mail-Adresse ein und drücken Sie Enter...",
|
||||
"jobTitlesFetchError": "Fehler beim Abrufen der Jobtitel",
|
||||
"invitationResent": "Einladung erfolgreich erneut gesendet!"
|
||||
}
|
||||
|
||||
16
worklenz-frontend/public/locales/de/settings/teams.json
Normal file
16
worklenz-frontend/public/locales/de/settings/teams.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"title": "Teams",
|
||||
"team": "Team",
|
||||
"teams": "Teams",
|
||||
"name": "Name",
|
||||
"created": "Erstellt",
|
||||
"ownsBy": "Gehört zu",
|
||||
"edit": "Bearbeiten",
|
||||
"editTeam": "Team bearbeiten",
|
||||
"pinTooltip": "Klicken Sie hier, um dies im Hauptmenü zu fixieren",
|
||||
"editTeamName": "Team-Name bearbeiten",
|
||||
"updateName": "Name aktualisieren",
|
||||
"namePlaceholder": "Name",
|
||||
"nameRequired": "Bitte geben Sie einen Namen ein",
|
||||
"updateFailed": "Änderung des Team-Namens fehlgeschlagen!"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"taskHeader": {
|
||||
"taskNamePlaceholder": "Aufgabe eingeben",
|
||||
"taskNamePlaceholder": "Geben Sie Ihre Aufgabe ein",
|
||||
"deleteTask": "Aufgabe löschen"
|
||||
},
|
||||
"taskInfoTab": {
|
||||
@@ -9,20 +9,29 @@
|
||||
"title": "Details",
|
||||
"task-key": "Aufgaben-Schlüssel",
|
||||
"phase": "Phase",
|
||||
"assignees": "Zugewiesene",
|
||||
"assignees": "Beauftragte",
|
||||
"due-date": "Fälligkeitsdatum",
|
||||
"time-estimation": "Zeitschätzung",
|
||||
"priority": "Priorität",
|
||||
"labels": "Labels",
|
||||
"billable": "Abrechenbar",
|
||||
"notify": "Benachrichtigen",
|
||||
"when-done-notify": "Bei Fertigstellung benachrichtigen",
|
||||
"when-done-notify": "Bei Abschluss benachrichtigen",
|
||||
"start-date": "Startdatum",
|
||||
"end-date": "Enddatum",
|
||||
"hide-start-date": "Startdatum ausblenden",
|
||||
"show-start-date": "Startdatum anzeigen",
|
||||
"hours": "Stunden",
|
||||
"minutes": "Minuten"
|
||||
"minutes": "Minuten",
|
||||
"progressValue": "Fortschrittswert",
|
||||
"progressValueTooltip": "Fortschritt in Prozent einstellen (0-100%)",
|
||||
"progressValueRequired": "Bitte geben Sie einen Fortschrittswert ein",
|
||||
"progressValueRange": "Fortschritt muss zwischen 0 und 100 liegen",
|
||||
"taskWeight": "Aufgabengewicht",
|
||||
"taskWeightTooltip": "Gewicht dieser Teilaufgabe festlegen (Prozent)",
|
||||
"taskWeightRequired": "Bitte geben Sie ein Aufgabengewicht ein",
|
||||
"taskWeightRange": "Gewicht muss zwischen 0 und 100 liegen",
|
||||
"recurring": "Wiederkehrend"
|
||||
},
|
||||
"labels": {
|
||||
"labelInputPlaceholder": "Suchen oder erstellen",
|
||||
@@ -30,29 +39,29 @@
|
||||
},
|
||||
"description": {
|
||||
"title": "Beschreibung",
|
||||
"placeholder": "Detaillierte Beschreibung hinzufügen..."
|
||||
"placeholder": "Detailliertere Beschreibung hinzufügen..."
|
||||
},
|
||||
"subTasks": {
|
||||
"title": "Unteraufgaben",
|
||||
"addSubTask": "+ Unteraufgabe hinzufügen",
|
||||
"addSubTaskInputPlaceholder": "Aufgabe eingeben und Enter drücken",
|
||||
"refreshSubTasks": "Unteraufgaben aktualisieren",
|
||||
"title": "Teilaufgaben",
|
||||
"addSubTask": "Teilaufgabe hinzufügen",
|
||||
"addSubTaskInputPlaceholder": "Geben Sie Ihre Aufgabe ein und drücken Sie Enter",
|
||||
"refreshSubTasks": "Teilaufgaben aktualisieren",
|
||||
"edit": "Bearbeiten",
|
||||
"delete": "Löschen",
|
||||
"confirmDeleteSubTask": "Sind Sie sicher, dass Sie diese Unteraufgabe löschen möchten?",
|
||||
"deleteSubTask": "Unteraufgabe löschen"
|
||||
"confirmDeleteSubTask": "Sind Sie sicher, dass Sie diese Teilaufgabe löschen möchten?",
|
||||
"deleteSubTask": "Teilaufgabe löschen"
|
||||
},
|
||||
"dependencies": {
|
||||
"title": "Abhängigkeiten",
|
||||
"addDependency": "+ Neue Abhängigkeit hinzufügen",
|
||||
"blockedBy": "Blockiert durch",
|
||||
"blockedBy": "Blockiert von",
|
||||
"searchTask": "Aufgabe suchen",
|
||||
"noTasksFound": "Keine Aufgaben gefunden",
|
||||
"confirmDeleteDependency": "Sind Sie sicher, dass Sie dies löschen möchten?"
|
||||
"confirmDeleteDependency": "Sind Sie sicher, dass Sie löschen möchten?"
|
||||
},
|
||||
"attachments": {
|
||||
"title": "Anhänge",
|
||||
"chooseOrDropFileToUpload": "Datei auswählen oder zum Hochladen ablegen",
|
||||
"chooseOrDropFileToUpload": "Datei zum Hochladen wählen oder ablegen",
|
||||
"uploading": "Wird hochgeladen..."
|
||||
},
|
||||
"comments": {
|
||||
@@ -60,19 +69,55 @@
|
||||
"addComment": "+ Neuen Kommentar hinzufügen",
|
||||
"noComments": "Noch keine Kommentare. Seien Sie der Erste!",
|
||||
"delete": "Löschen",
|
||||
"confirmDeleteComment": "Sind Sie sicher, dass Sie diesen Kommentar löschen möchten?"
|
||||
"confirmDeleteComment": "Sind Sie sicher, dass Sie diesen Kommentar löschen möchten?",
|
||||
"addCommentPlaceholder": "Kommentar hinzufügen...",
|
||||
"cancel": "Abbrechen",
|
||||
"commentButton": "Kommentieren",
|
||||
"attachFiles": "Dateien anhängen",
|
||||
"addMoreFiles": "Weitere Dateien hinzufügen",
|
||||
"selectedFiles": "Ausgewählte Dateien (Bis zu 25MB, Maximum {count})",
|
||||
"maxFilesError": "Sie können maximal {count} Dateien hochladen",
|
||||
"processFilesError": "Fehler beim Verarbeiten der Dateien",
|
||||
"addCommentError": "Bitte fügen Sie einen Kommentar hinzu oder hängen Sie Dateien an",
|
||||
"createdBy": "Erstellt {time} von {user}",
|
||||
"updatedTime": "Aktualisiert {time}"
|
||||
},
|
||||
"searchInputPlaceholder": "Nach Namen suchen",
|
||||
"pendingInvitation": "Einladung ausstehend"
|
||||
"searchInputPlaceholder": "Nach Name suchen",
|
||||
"pendingInvitation": "Ausstehende Einladung"
|
||||
},
|
||||
"taskTimeLogTab": {
|
||||
"title": "Zeiterfassung",
|
||||
"addTimeLog": "Neuen Zeiteintrag hinzufügen",
|
||||
"totalLogged": "Gesamt erfasst",
|
||||
"exportToExcel": "Nach Excel exportieren",
|
||||
"noTimeLogsFound": "Keine Zeiterfassungen gefunden"
|
||||
"noTimeLogsFound": "Keine Zeiteinträge gefunden",
|
||||
"timeLogForm": {
|
||||
"date": "Datum",
|
||||
"startTime": "Startzeit",
|
||||
"endTime": "Endzeit",
|
||||
"workDescription": "Arbeitsbeschreibung",
|
||||
"descriptionPlaceholder": "Beschreibung hinzufügen",
|
||||
"logTime": "Zeit erfassen",
|
||||
"updateTime": "Zeit aktualisieren",
|
||||
"cancel": "Abbrechen",
|
||||
"selectDateError": "Bitte wählen Sie ein Datum",
|
||||
"selectStartTimeError": "Bitte wählen Sie eine Startzeit",
|
||||
"selectEndTimeError": "Bitte wählen Sie eine Endzeit",
|
||||
"endTimeAfterStartError": "Endzeit muss nach der Startzeit liegen"
|
||||
}
|
||||
},
|
||||
"taskActivityLogTab": {
|
||||
"title": "Aktivitätsprotokoll"
|
||||
"title": "Aktivitätsprotokoll",
|
||||
"add": "HINZUFÜGEN",
|
||||
"remove": "ENTFERNEN",
|
||||
"none": "Keine",
|
||||
"weight": "Gewicht",
|
||||
"createdTask": "hat die Aufgabe erstellt."
|
||||
},
|
||||
"taskProgress": {
|
||||
"markAsDoneTitle": "Aufgabe als erledigt markieren?",
|
||||
"confirmMarkAsDone": "Ja, als erledigt markieren",
|
||||
"cancelMarkAsDone": "Nein, aktuellen Status beibehalten",
|
||||
"markAsDoneDescription": "Sie haben den Fortschritt auf 100% gesetzt. Möchten Sie den Aufgabenstatus auf \"Erledigt\" aktualisieren?"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,6 +68,13 @@
|
||||
"dueDatePlaceholder": "Fälligkeitsdatum",
|
||||
"startDatePlaceholder": "Startdatum",
|
||||
|
||||
"emptyStates": {
|
||||
"noTaskGroups": "Keine Aufgabengruppen gefunden",
|
||||
"noTaskGroupsDescription": "Aufgaben werden hier angezeigt, wenn sie erstellt oder Filter angewendet werden.",
|
||||
"errorPrefix": "Fehler:",
|
||||
"dragTaskFallback": "Aufgabe"
|
||||
},
|
||||
|
||||
"customColumns": {
|
||||
"addCustomColumn": "Benutzerdefinierte Spalte hinzufügen",
|
||||
"customColumnHeader": "Benutzerdefinierte Spalte",
|
||||
|
||||
14
worklenz-frontend/public/locales/en/project-view.json
Normal file
14
worklenz-frontend/public/locales/en/project-view.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"taskList": "Task List",
|
||||
"board": "Kanban Board",
|
||||
"insights": "Insights",
|
||||
"files": "Files",
|
||||
"members": "Members",
|
||||
"updates": "Updates",
|
||||
"projectView": "Project View",
|
||||
"loading": "Loading project...",
|
||||
"error": "Error loading project",
|
||||
"pinnedTab": "Pinned as default tab",
|
||||
"pinTab": "Pin as default tab",
|
||||
"unpinTab": "Unpin default tab"
|
||||
}
|
||||
@@ -13,5 +13,17 @@
|
||||
"receiveProjectSummary": "Receive a project summary every evening.",
|
||||
"refreshProject": "Refresh project",
|
||||
"saveAsTemplate": "Save as template",
|
||||
"invite": "Invite"
|
||||
"invite": "Invite",
|
||||
"subscribeTooltip": "Subscribe to project notifications",
|
||||
"unsubscribeTooltip": "Unsubscribe from project notifications",
|
||||
"refreshTooltip": "Refresh project data",
|
||||
"settingsTooltip": "Open project settings",
|
||||
"saveAsTemplateTooltip": "Save this project as a template",
|
||||
"inviteTooltip": "Invite team members to this project",
|
||||
"createTaskTooltip": "Create a new task",
|
||||
"importTaskTooltip": "Import task from template",
|
||||
"navigateBackTooltip": "Go back to projects list",
|
||||
"projectStatusTooltip": "Project status",
|
||||
"projectDatesInfo": "Project timeline information",
|
||||
"projectCategoryTooltip": "Project category"
|
||||
}
|
||||
|
||||
@@ -9,5 +9,6 @@
|
||||
"saveChanges": "Save Changes",
|
||||
"profileJoinedText": "Joined a month ago",
|
||||
"profileLastUpdatedText": "Last updated a month ago",
|
||||
"avatarTooltip": "Click to upload an avatar"
|
||||
"avatarTooltip": "Click to upload an avatar",
|
||||
"title": "Profile Settings"
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"title": "Team Members",
|
||||
"nameColumn": "Name",
|
||||
"projectsColumn": "Projects",
|
||||
"emailColumn": "Email",
|
||||
@@ -40,5 +41,7 @@
|
||||
"ownerText": "Team Owner",
|
||||
"addedText": "Added",
|
||||
"updatedText": "Updated",
|
||||
"noResultFound": "Type an email address and hit enter..."
|
||||
"noResultFound": "Type an email address and hit enter...",
|
||||
"jobTitlesFetchError": "Failed to fetch job titles",
|
||||
"invitationResent": "Invitation resent successfully!"
|
||||
}
|
||||
|
||||
16
worklenz-frontend/public/locales/en/settings/teams.json
Normal file
16
worklenz-frontend/public/locales/en/settings/teams.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"title": "Teams",
|
||||
"team": "Team",
|
||||
"teams": "Teams",
|
||||
"name": "Name",
|
||||
"created": "Created",
|
||||
"ownsBy": "Owns By",
|
||||
"edit": "Edit",
|
||||
"editTeam": "Edit Team",
|
||||
"pinTooltip": "Click to pin this into the main menu",
|
||||
"editTeamName": "Edit Team Name",
|
||||
"updateName": "Update Name",
|
||||
"namePlaceholder": "Name",
|
||||
"nameRequired": "Please enter a Name",
|
||||
"updateFailed": "Team name change failed!"
|
||||
}
|
||||
@@ -69,7 +69,18 @@
|
||||
"addComment": "+ Add new comment",
|
||||
"noComments": "No comments yet. Be the first to comment!",
|
||||
"delete": "Delete",
|
||||
"confirmDeleteComment": "Are you sure you want to delete this comment?"
|
||||
"confirmDeleteComment": "Are you sure you want to delete this comment?",
|
||||
"addCommentPlaceholder": "Add a comment...",
|
||||
"cancel": "Cancel",
|
||||
"commentButton": "Comment",
|
||||
"attachFiles": "Attach files",
|
||||
"addMoreFiles": "Add more files",
|
||||
"selectedFiles": "Selected Files (Up to 25MB, Maximum of {count})",
|
||||
"maxFilesError": "You can only upload a maximum of {count} files",
|
||||
"processFilesError": "Failed to process files",
|
||||
"addCommentError": "Please add a comment or attach files",
|
||||
"createdBy": "Created {time} by {user}",
|
||||
"updatedTime": "Updated {time}"
|
||||
},
|
||||
"searchInputPlaceholder": "Search by name",
|
||||
"pendingInvitation": "Pending Invitation"
|
||||
@@ -79,10 +90,29 @@
|
||||
"addTimeLog": "Add new time log",
|
||||
"totalLogged": "Total Logged",
|
||||
"exportToExcel": "Export to Excel",
|
||||
"noTimeLogsFound": "No time logs found"
|
||||
"noTimeLogsFound": "No time logs found",
|
||||
"timeLogForm": {
|
||||
"date": "Date",
|
||||
"startTime": "Start Time",
|
||||
"endTime": "End Time",
|
||||
"workDescription": "Work Description",
|
||||
"descriptionPlaceholder": "Add a description",
|
||||
"logTime": "Log time",
|
||||
"updateTime": "Update time",
|
||||
"cancel": "Cancel",
|
||||
"selectDateError": "Please select a date",
|
||||
"selectStartTimeError": "Please select start time",
|
||||
"selectEndTimeError": "Please select end time",
|
||||
"endTimeAfterStartError": "End time must be after start time"
|
||||
}
|
||||
},
|
||||
"taskActivityLogTab": {
|
||||
"title": "Activity Log"
|
||||
"title": "Activity Log",
|
||||
"add": "ADD",
|
||||
"remove": "REMOVE",
|
||||
"none": "None",
|
||||
"weight": "Weight",
|
||||
"createdTask": "created the task."
|
||||
},
|
||||
"taskProgress": {
|
||||
"markAsDoneTitle": "Mark Task as Done?",
|
||||
|
||||
@@ -68,6 +68,13 @@
|
||||
"dueDatePlaceholder": "Due Date",
|
||||
"startDatePlaceholder": "Start Date",
|
||||
|
||||
"emptyStates": {
|
||||
"noTaskGroups": "No task groups found",
|
||||
"noTaskGroupsDescription": "Tasks will appear here when they are created or when filters are applied.",
|
||||
"errorPrefix": "Error:",
|
||||
"dragTaskFallback": "Task"
|
||||
},
|
||||
|
||||
"customColumns": {
|
||||
"addCustomColumn": "Add a custom column",
|
||||
"customColumnHeader": "Custom Column",
|
||||
|
||||
14
worklenz-frontend/public/locales/es/project-view.json
Normal file
14
worklenz-frontend/public/locales/es/project-view.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"taskList": "Lista de Tareas",
|
||||
"board": "Tablero Kanban",
|
||||
"insights": "Análisis",
|
||||
"files": "Archivos",
|
||||
"members": "Miembros",
|
||||
"updates": "Actualizaciones",
|
||||
"projectView": "Vista del Proyecto",
|
||||
"loading": "Cargando proyecto...",
|
||||
"error": "Error al cargar el proyecto",
|
||||
"pinnedTab": "Fijado como pestaña predeterminada",
|
||||
"pinTab": "Fijar como pestaña predeterminada",
|
||||
"unpinTab": "Desfijar pestaña predeterminada"
|
||||
}
|
||||
@@ -2,16 +2,28 @@
|
||||
"importTasks": "Importar tareas",
|
||||
"importTask": "Importar tarea",
|
||||
"createTask": "Crear tarea",
|
||||
"settings": "Ajustes",
|
||||
"settings": "Configuración",
|
||||
"subscribe": "Suscribirse",
|
||||
"unsubscribe": "Cancelar suscripción",
|
||||
"deleteProject": "Eliminar proyecto",
|
||||
"startDate": "Fecha de inicio",
|
||||
"endDate": "Fecha de finalización",
|
||||
"projectSettings": "Ajustes del proyecto",
|
||||
"projectSettings": "Configuración del proyecto",
|
||||
"projectSummary": "Resumen del proyecto",
|
||||
"receiveProjectSummary": "Recibir un resumen del proyecto todas las noches.",
|
||||
"receiveProjectSummary": "Recibe un resumen del proyecto cada noche.",
|
||||
"refreshProject": "Actualizar proyecto",
|
||||
"saveAsTemplate": "Guardar como plantilla",
|
||||
"invite": "Invitar"
|
||||
"invite": "Invitar",
|
||||
"subscribeTooltip": "Suscribirse a notificaciones del proyecto",
|
||||
"unsubscribeTooltip": "Cancelar suscripción a notificaciones del proyecto",
|
||||
"refreshTooltip": "Actualizar datos del proyecto",
|
||||
"settingsTooltip": "Abrir configuración del proyecto",
|
||||
"saveAsTemplateTooltip": "Guardar este proyecto como plantilla",
|
||||
"inviteTooltip": "Invitar miembros del equipo a este proyecto",
|
||||
"createTaskTooltip": "Crear una nueva tarea",
|
||||
"importTaskTooltip": "Importar tarea desde plantilla",
|
||||
"navigateBackTooltip": "Volver a la lista de proyectos",
|
||||
"projectStatusTooltip": "Estado del proyecto",
|
||||
"projectDatesInfo": "Información de cronograma del proyecto",
|
||||
"projectCategoryTooltip": "Categoría del proyecto"
|
||||
}
|
||||
|
||||
@@ -9,5 +9,6 @@
|
||||
"saveChanges": "Guardar cambios",
|
||||
"profileJoinedText": "Se unió hace un mes",
|
||||
"profileLastUpdatedText": "Última actualización hace un mes",
|
||||
"avatarTooltip": "Haz clic para subir un avatar"
|
||||
"avatarTooltip": "Haz clic para subir un avatar",
|
||||
"title": "Configuración del Perfil"
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"title": "Miembros del Equipo",
|
||||
"nameColumn": "Nombre",
|
||||
"projectsColumn": "Proyectos",
|
||||
"emailColumn": "Correo electrónico",
|
||||
@@ -40,5 +41,7 @@
|
||||
"ownerText": "Propietario del equipo",
|
||||
"addedText": "Agregado",
|
||||
"updatedText": "Actualizado",
|
||||
"noResultFound": "Escriba una dirección de correo electrónico y presione enter..."
|
||||
"noResultFound": "Escriba una dirección de correo electrónico y presione enter...",
|
||||
"jobTitlesFetchError": "Error al obtener los cargos",
|
||||
"invitationResent": "¡Invitación reenviada exitosamente!"
|
||||
}
|
||||
|
||||
16
worklenz-frontend/public/locales/es/settings/teams.json
Normal file
16
worklenz-frontend/public/locales/es/settings/teams.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"title": "Equipos",
|
||||
"team": "Equipo",
|
||||
"teams": "Equipos",
|
||||
"name": "Nombre",
|
||||
"created": "Creado",
|
||||
"ownsBy": "Pertenece a",
|
||||
"edit": "Editar",
|
||||
"editTeam": "Editar Equipo",
|
||||
"pinTooltip": "Haz clic para fijar esto en el menú principal",
|
||||
"editTeamName": "Editar Nombre del Equipo",
|
||||
"updateName": "Actualizar Nombre",
|
||||
"namePlaceholder": "Nombre",
|
||||
"nameRequired": "Por favor ingresa un Nombre",
|
||||
"updateFailed": "¡Falló el cambio de nombre del equipo!"
|
||||
}
|
||||
@@ -1,93 +1,123 @@
|
||||
{
|
||||
"taskHeader": {
|
||||
"taskNamePlaceholder": "Escribe tu tarea",
|
||||
"deleteTask": "Eliminar tarea"
|
||||
"taskNamePlaceholder": "Escriba su Tarea",
|
||||
"deleteTask": "Eliminar Tarea"
|
||||
},
|
||||
"taskInfoTab": {
|
||||
"title": "Información",
|
||||
"details": {
|
||||
"title": "Detalles",
|
||||
"task-key": "Clave de tarea",
|
||||
"task-key": "Clave de Tarea",
|
||||
"phase": "Fase",
|
||||
"assignees": "Asignados",
|
||||
"due-date": "Fecha de vencimiento",
|
||||
"time-estimation": "Estimación de tiempo",
|
||||
"due-date": "Fecha de Vencimiento",
|
||||
"time-estimation": "Estimación de Tiempo",
|
||||
"priority": "Prioridad",
|
||||
"labels": "Etiquetas",
|
||||
"billable": "Facturable",
|
||||
"notify": "Notificar",
|
||||
"when-done-notify": "Al terminar, notificar",
|
||||
"start-date": "Fecha de inicio",
|
||||
"end-date": "Fecha de finalización",
|
||||
"hide-start-date": "Ocultar fecha de inicio",
|
||||
"show-start-date": "Mostrar fecha de inicio",
|
||||
"start-date": "Fecha de Inicio",
|
||||
"end-date": "Fecha de Fin",
|
||||
"hide-start-date": "Ocultar Fecha de Inicio",
|
||||
"show-start-date": "Mostrar Fecha de Inicio",
|
||||
"hours": "Horas",
|
||||
"minutes": "Minutos",
|
||||
"progressValue": "Valor de Progreso",
|
||||
"progressValueTooltip": "Establecer el porcentaje de progreso (0-100%)",
|
||||
"progressValueRequired": "Por favor, introduce un valor de progreso",
|
||||
"progressValueRequired": "Por favor, introduzca un valor de progreso",
|
||||
"progressValueRange": "El progreso debe estar entre 0 y 100",
|
||||
"taskWeight": "Peso de la Tarea",
|
||||
"taskWeightTooltip": "Establecer el peso de esta subtarea (porcentaje)",
|
||||
"taskWeightRequired": "Por favor, introduce un peso para la tarea",
|
||||
"taskWeightRequired": "Por favor, introduzca un peso de tarea",
|
||||
"taskWeightRange": "El peso debe estar entre 0 y 100",
|
||||
"recurring": "Recurrente"
|
||||
},
|
||||
"labels": {
|
||||
"labelInputPlaceholder": "Buscar o crear",
|
||||
"labelsSelectorInputTip": "Pulse Enter para crear"
|
||||
"labelsSelectorInputTip": "Presiona Enter para crear"
|
||||
},
|
||||
"description": {
|
||||
"title": "Descripción",
|
||||
"placeholder": "Añadir una descripción más detallada..."
|
||||
},
|
||||
"subTasks": {
|
||||
"title": "Subtareas",
|
||||
"addSubTask": "+ Añadir subtarea",
|
||||
"addSubTaskInputPlaceholder": "Escribe tu tarea y pulsa enter",
|
||||
"refreshSubTasks": "Actualizar subtareas",
|
||||
"title": "Sub Tareas",
|
||||
"addSubTask": "Agregar Sub Tarea",
|
||||
"addSubTaskInputPlaceholder": "Escriba su tarea y presione enter",
|
||||
"refreshSubTasks": "Actualizar Sub Tareas",
|
||||
"edit": "Editar",
|
||||
"delete": "Eliminar",
|
||||
"confirmDeleteSubTask": "¿Estás seguro de que quieres eliminar esta subtarea?",
|
||||
"deleteSubTask": "Eliminar subtarea"
|
||||
"confirmDeleteSubTask": "¿Está seguro de que desea eliminar esta subtarea?",
|
||||
"deleteSubTask": "Eliminar Sub Tarea"
|
||||
},
|
||||
"dependencies": {
|
||||
"title": "Dependencias",
|
||||
"addDependency": "+ Añadir nueva dependencia",
|
||||
"addDependency": "+ Agregar nueva dependencia",
|
||||
"blockedBy": "Bloqueado por",
|
||||
"searchTask": "Escribe para buscar tarea",
|
||||
"searchTask": "Escribir para buscar tarea",
|
||||
"noTasksFound": "No se encontraron tareas",
|
||||
"confirmDeleteDependency": "¿Estás seguro de que quieres eliminar?"
|
||||
"confirmDeleteDependency": "¿Está seguro de que desea eliminar?"
|
||||
},
|
||||
"attachments": {
|
||||
"title": "Adjuntos",
|
||||
"chooseOrDropFileToUpload": "Elige o arrastra un archivo para subir",
|
||||
"chooseOrDropFileToUpload": "Elija o arrastre un archivo para subir",
|
||||
"uploading": "Subiendo..."
|
||||
},
|
||||
"comments": {
|
||||
"title": "Comentarios",
|
||||
"addComment": "+ Añadir nuevo comentario",
|
||||
"noComments": "No hay comentarios todavía. ¡Sé el primero en comentar!",
|
||||
"addComment": "+ Agregar nuevo comentario",
|
||||
"noComments": "Aún no hay comentarios. ¡Sé el primero en comentar!",
|
||||
"delete": "Eliminar",
|
||||
"confirmDeleteComment": "¿Estás seguro de que quieres eliminar este comentario?"
|
||||
"confirmDeleteComment": "¿Está seguro de que desea eliminar este comentario?",
|
||||
"addCommentPlaceholder": "Agregar un comentario...",
|
||||
"cancel": "Cancelar",
|
||||
"commentButton": "Comentar",
|
||||
"attachFiles": "Adjuntar archivos",
|
||||
"addMoreFiles": "Agregar más archivos",
|
||||
"selectedFiles": "Archivos Seleccionados (Hasta 25MB, Máximo {count})",
|
||||
"maxFilesError": "Solo puede subir un máximo de {count} archivos",
|
||||
"processFilesError": "Error al procesar archivos",
|
||||
"addCommentError": "Por favor agregue un comentario o adjunte archivos",
|
||||
"createdBy": "Creado {time} por {user}",
|
||||
"updatedTime": "Actualizado {time}"
|
||||
},
|
||||
"searchInputPlaceholder": "Buscar por nombre",
|
||||
"pendingInvitation": "Invitación pendiente"
|
||||
"pendingInvitation": "Invitación Pendiente"
|
||||
},
|
||||
"taskTimeLogTab": {
|
||||
"title": "Registro de tiempo",
|
||||
"title": "Registro de Tiempo",
|
||||
"addTimeLog": "Añadir nuevo registro de tiempo",
|
||||
"totalLogged": "Total registrado",
|
||||
"totalLogged": "Total Registrado",
|
||||
"exportToExcel": "Exportar a Excel",
|
||||
"noTimeLogsFound": "No se encontraron registros de tiempo"
|
||||
"noTimeLogsFound": "No se encontraron registros de tiempo",
|
||||
"timeLogForm": {
|
||||
"date": "Fecha",
|
||||
"startTime": "Hora de Inicio",
|
||||
"endTime": "Hora de Fin",
|
||||
"workDescription": "Descripción del Trabajo",
|
||||
"descriptionPlaceholder": "Agregar una descripción",
|
||||
"logTime": "Registrar tiempo",
|
||||
"updateTime": "Actualizar tiempo",
|
||||
"cancel": "Cancelar",
|
||||
"selectDateError": "Por favor seleccione una fecha",
|
||||
"selectStartTimeError": "Por favor seleccione la hora de inicio",
|
||||
"selectEndTimeError": "Por favor seleccione la hora de fin",
|
||||
"endTimeAfterStartError": "La hora de fin debe ser posterior a la hora de inicio"
|
||||
}
|
||||
},
|
||||
"taskActivityLogTab": {
|
||||
"title": "Registro de actividad"
|
||||
"title": "Registro de Actividad",
|
||||
"add": "AGREGAR",
|
||||
"remove": "QUITAR",
|
||||
"none": "Ninguno",
|
||||
"weight": "Peso",
|
||||
"createdTask": "creó la tarea."
|
||||
},
|
||||
"taskProgress": {
|
||||
"markAsDoneTitle": "¿Marcar Tarea como Completada?",
|
||||
"confirmMarkAsDone": "Sí, marcar como completada",
|
||||
"cancelMarkAsDone": "No, mantener estado actual",
|
||||
"markAsDoneDescription": "Has establecido el progreso al 100%. ¿Quieres actualizar el estado de la tarea a \"Completada\"?"
|
||||
"markAsDoneDescription": "Ha establecido el progreso al 100%. ¿Le gustaría actualizar el estado de la tarea a \"Completada\"?"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,6 +68,13 @@
|
||||
"dueDatePlaceholder": "Fecha de vencimiento",
|
||||
"startDatePlaceholder": "Fecha de inicio",
|
||||
|
||||
"emptyStates": {
|
||||
"noTaskGroups": "No se encontraron grupos de tareas",
|
||||
"noTaskGroupsDescription": "Las tareas aparecerán aquí cuando se creen o cuando se apliquen filtros.",
|
||||
"errorPrefix": "Error:",
|
||||
"dragTaskFallback": "Tarea"
|
||||
},
|
||||
|
||||
"customColumns": {
|
||||
"addCustomColumn": "Agregar una columna personalizada",
|
||||
"customColumnHeader": "Columna Personalizada",
|
||||
|
||||
14
worklenz-frontend/public/locales/pt/project-view.json
Normal file
14
worklenz-frontend/public/locales/pt/project-view.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"taskList": "Lista de Tarefas",
|
||||
"board": "Quadro Kanban",
|
||||
"insights": "Insights",
|
||||
"files": "Arquivos",
|
||||
"members": "Membros",
|
||||
"updates": "Atualizações",
|
||||
"projectView": "Visualização do Projeto",
|
||||
"loading": "Carregando projeto...",
|
||||
"error": "Erro ao carregar projeto",
|
||||
"pinnedTab": "Fixada como aba padrão",
|
||||
"pinTab": "Fixar como aba padrão",
|
||||
"unpinTab": "Desfixar aba padrão"
|
||||
}
|
||||
@@ -7,11 +7,23 @@
|
||||
"unsubscribe": "Cancelar inscrição",
|
||||
"deleteProject": "Excluir projeto",
|
||||
"startDate": "Data de início",
|
||||
"endDate": "Data de fim",
|
||||
"endDate": "Data de término",
|
||||
"projectSettings": "Configurações do projeto",
|
||||
"projectSummary": "Resumo do projeto",
|
||||
"receiveProjectSummary": "Receber um resumo do projeto todas as noites.",
|
||||
"receiveProjectSummary": "Receba um resumo do projeto todas as noites.",
|
||||
"refreshProject": "Atualizar projeto",
|
||||
"saveAsTemplate": "Salvar como modelo",
|
||||
"invite": "Convidar"
|
||||
"invite": "Convidar",
|
||||
"subscribeTooltip": "Inscrever-se nas notificações do projeto",
|
||||
"unsubscribeTooltip": "Cancelar inscrição nas notificações do projeto",
|
||||
"refreshTooltip": "Atualizar dados do projeto",
|
||||
"settingsTooltip": "Abrir configurações do projeto",
|
||||
"saveAsTemplateTooltip": "Salvar este projeto como modelo",
|
||||
"inviteTooltip": "Convidar membros da equipe para este projeto",
|
||||
"createTaskTooltip": "Criar uma nova tarefa",
|
||||
"importTaskTooltip": "Importar tarefa de modelo",
|
||||
"navigateBackTooltip": "Voltar para lista de projetos",
|
||||
"projectStatusTooltip": "Status do projeto",
|
||||
"projectDatesInfo": "Informações do cronograma do projeto",
|
||||
"projectCategoryTooltip": "Categoria do projeto"
|
||||
}
|
||||
|
||||
@@ -9,5 +9,6 @@
|
||||
"saveChanges": "Salvar Alterações",
|
||||
"profileJoinedText": "Entrou há um mês",
|
||||
"profileLastUpdatedText": "Última atualização há um mês",
|
||||
"avatarTooltip": "Clique para carregar um avatar"
|
||||
"avatarTooltip": "Clique para carregar um avatar",
|
||||
"title": "Configurações do Perfil"
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"title": "Membros da Equipe",
|
||||
"nameColumn": "Nome",
|
||||
"projectsColumn": "Projetos",
|
||||
"emailColumn": "Email",
|
||||
@@ -40,5 +41,7 @@
|
||||
"ownerText": "Dono da Equipe",
|
||||
"addedText": "Adicionado",
|
||||
"updatedText": "Atualizado",
|
||||
"noResultFound": "Digite um endereço de email e pressione enter..."
|
||||
"noResultFound": "Digite um endereço de email e pressione enter...",
|
||||
"jobTitlesFetchError": "Falha ao buscar cargos",
|
||||
"invitationResent": "Convite reenviado com sucesso!"
|
||||
}
|
||||
|
||||
16
worklenz-frontend/public/locales/pt/settings/teams.json
Normal file
16
worklenz-frontend/public/locales/pt/settings/teams.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"title": "Equipes",
|
||||
"team": "Equipe",
|
||||
"teams": "Equipes",
|
||||
"name": "Nome",
|
||||
"created": "Criado",
|
||||
"ownsBy": "Pertence a",
|
||||
"edit": "Editar",
|
||||
"editTeam": "Editar Equipe",
|
||||
"pinTooltip": "Clique para fixar isso no menu principal",
|
||||
"editTeamName": "Editar Nome da Equipe",
|
||||
"updateName": "Atualizar Nome",
|
||||
"namePlaceholder": "Nome",
|
||||
"nameRequired": "Por favor digite um Nome",
|
||||
"updateFailed": "Falha na alteração do nome da equipe!"
|
||||
}
|
||||
@@ -1,35 +1,35 @@
|
||||
{
|
||||
"taskHeader": {
|
||||
"taskNamePlaceholder": "Digite sua tarefa",
|
||||
"deleteTask": "Excluir tarefa"
|
||||
"taskNamePlaceholder": "Digite sua Tarefa",
|
||||
"deleteTask": "Deletar Tarefa"
|
||||
},
|
||||
"taskInfoTab": {
|
||||
"title": "Informações",
|
||||
"details": {
|
||||
"title": "Detalhes",
|
||||
"task-key": "Chave da tarefa",
|
||||
"task-key": "Chave da Tarefa",
|
||||
"phase": "Fase",
|
||||
"assignees": "Responsáveis",
|
||||
"due-date": "Data de vencimento",
|
||||
"time-estimation": "Estimativa de tempo",
|
||||
"due-date": "Data de Vencimento",
|
||||
"time-estimation": "Estimativa de Tempo",
|
||||
"priority": "Prioridade",
|
||||
"labels": "Etiquetas",
|
||||
"billable": "Faturável",
|
||||
"notify": "Notificar",
|
||||
"when-done-notify": "Quando concluída, notificar",
|
||||
"start-date": "Data de início",
|
||||
"end-date": "Data de término",
|
||||
"hide-start-date": "Ocultar data de início",
|
||||
"show-start-date": "Mostrar data de início",
|
||||
"when-done-notify": "Quando concluído, notificar",
|
||||
"start-date": "Data de Início",
|
||||
"end-date": "Data de Fim",
|
||||
"hide-start-date": "Ocultar Data de Início",
|
||||
"show-start-date": "Mostrar Data de Início",
|
||||
"hours": "Horas",
|
||||
"minutes": "Minutos",
|
||||
"progressValue": "Valor de Progresso",
|
||||
"progressValue": "Valor do Progresso",
|
||||
"progressValueTooltip": "Definir a porcentagem de progresso (0-100%)",
|
||||
"progressValueRequired": "Por favor, insira um valor de progresso",
|
||||
"progressValueRange": "O progresso deve estar entre 0 e 100",
|
||||
"taskWeight": "Peso da Tarefa",
|
||||
"taskWeightTooltip": "Definir o peso desta subtarefa (porcentagem)",
|
||||
"taskWeightRequired": "Por favor, insira um peso para a tarefa",
|
||||
"taskWeightRequired": "Por favor, insira um peso da tarefa",
|
||||
"taskWeightRange": "O peso deve estar entre 0 e 100",
|
||||
"recurring": "Recorrente"
|
||||
},
|
||||
@@ -42,14 +42,14 @@
|
||||
"placeholder": "Adicionar uma descrição mais detalhada..."
|
||||
},
|
||||
"subTasks": {
|
||||
"title": "Subtarefas",
|
||||
"addSubTask": "+ Adicionar subtarefa",
|
||||
"title": "Sub Tarefas",
|
||||
"addSubTask": "Adicionar Sub Tarefa",
|
||||
"addSubTaskInputPlaceholder": "Digite sua tarefa e pressione enter",
|
||||
"refreshSubTasks": "Atualizar subtarefas",
|
||||
"refreshSubTasks": "Atualizar Sub Tarefas",
|
||||
"edit": "Editar",
|
||||
"delete": "Excluir",
|
||||
"confirmDeleteSubTask": "Tem certeza de que deseja excluir esta subtarefa?",
|
||||
"deleteSubTask": "Excluir subtarefa"
|
||||
"delete": "Deletar",
|
||||
"confirmDeleteSubTask": "Tem certeza de que deseja deletar esta subtarefa?",
|
||||
"deleteSubTask": "Deletar Sub Tarefa"
|
||||
},
|
||||
"dependencies": {
|
||||
"title": "Dependências",
|
||||
@@ -57,37 +57,67 @@
|
||||
"blockedBy": "Bloqueado por",
|
||||
"searchTask": "Digite para pesquisar tarefa",
|
||||
"noTasksFound": "Nenhuma tarefa encontrada",
|
||||
"confirmDeleteDependency": "Tem certeza de que deseja excluir?"
|
||||
"confirmDeleteDependency": "Tem certeza de que deseja deletar?"
|
||||
},
|
||||
"attachments": {
|
||||
"title": "Anexos",
|
||||
"chooseOrDropFileToUpload": "Escolha ou arraste um arquivo para carregar",
|
||||
"uploading": "Carregando..."
|
||||
"chooseOrDropFileToUpload": "Escolha ou arraste um arquivo para upload",
|
||||
"uploading": "Enviando..."
|
||||
},
|
||||
"comments": {
|
||||
"title": "Comentários",
|
||||
"addComment": "+ Adicionar novo comentário",
|
||||
"noComments": "Nenhum comentário ainda. Seja o primeiro a comentar!",
|
||||
"delete": "Excluir",
|
||||
"confirmDeleteComment": "Tem certeza de que deseja excluir este comentário?"
|
||||
"noComments": "Ainda não há comentários. Seja o primeiro a comentar!",
|
||||
"delete": "Deletar",
|
||||
"confirmDeleteComment": "Tem certeza de que deseja deletar este comentário?",
|
||||
"addCommentPlaceholder": "Adicionar um comentário...",
|
||||
"cancel": "Cancelar",
|
||||
"commentButton": "Comentar",
|
||||
"attachFiles": "Anexar arquivos",
|
||||
"addMoreFiles": "Adicionar mais arquivos",
|
||||
"selectedFiles": "Arquivos Selecionados (Até 25MB, Máximo {count})",
|
||||
"maxFilesError": "Você pode fazer upload de no máximo {count} arquivos",
|
||||
"processFilesError": "Falha ao processar arquivos",
|
||||
"addCommentError": "Por favor adicione um comentário ou anexe arquivos",
|
||||
"createdBy": "Criado {time} por {user}",
|
||||
"updatedTime": "Atualizado {time}"
|
||||
},
|
||||
"searchInputPlaceholder": "Pesquisar por nome",
|
||||
"pendingInvitation": "Convite pendente"
|
||||
"pendingInvitation": "Convite Pendente"
|
||||
},
|
||||
"taskTimeLogTab": {
|
||||
"title": "Registro de tempo",
|
||||
"title": "Registro de Tempo",
|
||||
"addTimeLog": "Adicionar novo registro de tempo",
|
||||
"totalLogged": "Total registrado",
|
||||
"totalLogged": "Total Registrado",
|
||||
"exportToExcel": "Exportar para Excel",
|
||||
"noTimeLogsFound": "Nenhum registro de tempo encontrado"
|
||||
"noTimeLogsFound": "Nenhum registro de tempo encontrado",
|
||||
"timeLogForm": {
|
||||
"date": "Data",
|
||||
"startTime": "Hora de Início",
|
||||
"endTime": "Hora de Fim",
|
||||
"workDescription": "Descrição do Trabalho",
|
||||
"descriptionPlaceholder": "Adicionar uma descrição",
|
||||
"logTime": "Registrar tempo",
|
||||
"updateTime": "Atualizar tempo",
|
||||
"cancel": "Cancelar",
|
||||
"selectDateError": "Por favor selecione uma data",
|
||||
"selectStartTimeError": "Por favor selecione a hora de início",
|
||||
"selectEndTimeError": "Por favor selecione a hora de fim",
|
||||
"endTimeAfterStartError": "A hora de fim deve ser posterior à hora de início"
|
||||
}
|
||||
},
|
||||
"taskActivityLogTab": {
|
||||
"title": "Registro de atividade"
|
||||
"title": "Registro de Atividade",
|
||||
"add": "ADICIONAR",
|
||||
"remove": "REMOVER",
|
||||
"none": "Nenhum",
|
||||
"weight": "Peso",
|
||||
"createdTask": "criou a tarefa."
|
||||
},
|
||||
"taskProgress": {
|
||||
"markAsDoneTitle": "Marcar Tarefa como Concluída?",
|
||||
"confirmMarkAsDone": "Sim, marcar como concluída",
|
||||
"cancelMarkAsDone": "Não, manter status atual",
|
||||
"markAsDoneDescription": "Você definiu o progresso como 100%. Deseja atualizar o status da tarefa para \"Concluída\"?"
|
||||
"markAsDoneDescription": "Você definiu o progresso para 100%. Gostaria de atualizar o status da tarefa para \"Concluída\"?"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,6 +68,13 @@
|
||||
"dueDatePlaceholder": "Data de vencimento",
|
||||
"startDatePlaceholder": "Data de início",
|
||||
|
||||
"emptyStates": {
|
||||
"noTaskGroups": "Nenhum grupo de tarefas encontrado",
|
||||
"noTaskGroupsDescription": "As tarefas aparecerão aqui quando forem criadas ou quando filtros forem aplicados.",
|
||||
"errorPrefix": "Erro:",
|
||||
"dragTaskFallback": "Tarefa"
|
||||
},
|
||||
|
||||
"customColumns": {
|
||||
"addCustomColumn": "Adicionar uma coluna personalizada",
|
||||
"customColumnHeader": "Coluna Personalizada",
|
||||
|
||||
4
worklenz-frontend/public/locales/zh/404-page.json
Normal file
4
worklenz-frontend/public/locales/zh/404-page.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"doesNotExistText": "抱歉,您访问的页面不存在。",
|
||||
"backHomeButton": "返回首页"
|
||||
}
|
||||
27
worklenz-frontend/public/locales/zh/account-setup.json
Normal file
27
worklenz-frontend/public/locales/zh/account-setup.json
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"continue": "继续",
|
||||
"setupYourAccount": "设置您的Worklenz账户。",
|
||||
"organizationStepTitle": "命名您的组织",
|
||||
"organizationStepLabel": "为您的Worklenz账户选择一个名称。",
|
||||
"projectStepTitle": "创建您的第一个项目",
|
||||
"projectStepLabel": "您现在正在做什么项目?",
|
||||
"projectStepPlaceholder": "例如:营销计划",
|
||||
"tasksStepTitle": "创建您的第一个任务",
|
||||
"tasksStepLabel": "输入您将在其中完成的几个任务",
|
||||
"tasksStepAddAnother": "添加另一个",
|
||||
"emailPlaceholder": "电子邮件地址",
|
||||
"invalidEmail": "请输入有效的电子邮件地址",
|
||||
"or": "或",
|
||||
"templateButton": "从模板导入",
|
||||
"goBack": "返回",
|
||||
"cancel": "取消",
|
||||
"create": "创建",
|
||||
"templateDrawerTitle": "从模板中选择",
|
||||
"step3InputLabel": "通过电子邮件邀请",
|
||||
"addAnother": "添加另一个",
|
||||
"skipForNow": "暂时跳过",
|
||||
"formTitle": "创建您的第一个任务。",
|
||||
"step3Title": "邀请您的团队一起工作",
|
||||
"maxMembers": "(您最多可以邀请5名成员)",
|
||||
"maxTasks": "(您最多可以创建5个任务)"
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
{
|
||||
"title": "账单",
|
||||
"currentBill": "当前账单",
|
||||
"configuration": "配置",
|
||||
"currentPlanDetails": "当前计划详情",
|
||||
"upgradePlan": "升级计划",
|
||||
"cardBodyText01": "免费试用",
|
||||
"cardBodyText02": "(您的试用计划将在1个月19天后到期)",
|
||||
"redeemCode": "兑换码",
|
||||
"accountStorage": "账户存储",
|
||||
"used": "已用:",
|
||||
"remaining": "剩余:",
|
||||
"charges": "费用",
|
||||
"tooltip": "当前账单周期的费用",
|
||||
"description": "描述",
|
||||
"billingPeriod": "账单周期",
|
||||
"billStatus": "账单状态",
|
||||
"perUserValue": "每用户费用",
|
||||
"users": "用户",
|
||||
"amount": "金额",
|
||||
"invoices": "发票",
|
||||
"transactionId": "交易ID",
|
||||
"transactionDate": "交易日期",
|
||||
"paymentMethod": "支付方式",
|
||||
"status": "状态",
|
||||
"ltdUsers": "您最多可以添加{{ltd_users}}名用户。",
|
||||
"totalSeats": "总席位",
|
||||
"availableSeats": "可用席位",
|
||||
"addMoreSeats": "添加更多席位",
|
||||
"drawerTitle": "兑换码",
|
||||
"label": "兑换码",
|
||||
"drawerPlaceholder": "输入您的兑换码",
|
||||
"redeemSubmit": "提交",
|
||||
"modalTitle": "为您的团队选择最佳计划",
|
||||
"seatLabel": "席位数量",
|
||||
"freePlan": "免费计划",
|
||||
"startup": "初创",
|
||||
"business": "商业",
|
||||
"tag": "最受欢迎",
|
||||
"enterprise": "企业",
|
||||
"freeSubtitle": "永远免费",
|
||||
"freeUsers": "最适合个人使用",
|
||||
"freeText01": "100MB存储",
|
||||
"freeText02": "3个项目",
|
||||
"freeText03": "5名团队成员",
|
||||
"startupSubtitle": "固定费率/月",
|
||||
"startupUsers": "最多15名用户",
|
||||
"startupText01": "25GB存储",
|
||||
"startupText02": "无限活跃项目",
|
||||
"startupText03": "日程",
|
||||
"startupText04": "报告",
|
||||
"startupText05": "订阅项目",
|
||||
"businessSubtitle": "每用户/月",
|
||||
"businessUsers": "16 - 200名用户",
|
||||
"enterpriseUsers": "200 - 500+名用户",
|
||||
"footerTitle": "请提供一个我们可以联系您的电话号码。",
|
||||
"footerLabel": "联系电话",
|
||||
"footerButton": "联系我们",
|
||||
"redeemCodePlaceHolder": "输入您的兑换码",
|
||||
"submit": "提交",
|
||||
"trialPlan": "免费试用",
|
||||
"trialExpireDate": "有效期至{{trial_expire_date}}",
|
||||
"trialExpired": "您的免费试用已于{{trial_expire_string}}到期",
|
||||
"trialInProgress": "您的免费试用将在{{trial_expire_string}}到期",
|
||||
"required": "此字段为必填项",
|
||||
"invalidCode": "无效的代码",
|
||||
"selectPlan": "为您的团队选择最佳计划",
|
||||
"changeSubscriptionPlan": "更改您的订阅计划",
|
||||
"noOfSeats": "席位数量",
|
||||
"annualPlan": "专业 - 年度",
|
||||
"monthlyPlan": "专业 - 月度",
|
||||
"freeForever": "永远免费",
|
||||
"bestForPersonalUse": "最适合个人使用",
|
||||
"storage": "存储",
|
||||
"projects": "项目",
|
||||
"teamMembers": "团队成员",
|
||||
"unlimitedTeamMembers": "无限团队成员",
|
||||
"unlimitedActiveProjects": "无限活跃项目",
|
||||
"schedule": "日程",
|
||||
"reporting": "报告",
|
||||
"subscribeToProjects": "订阅项目",
|
||||
"billedAnnually": "按年计费",
|
||||
"billedMonthly": "按月计费",
|
||||
"pausePlan": "暂停计划",
|
||||
"resumePlan": "恢复计划",
|
||||
"changePlan": "更改计划",
|
||||
"cancelPlan": "取消计划",
|
||||
"perMonthPerUser": "每用户/月",
|
||||
"viewInvoice": "查看发票",
|
||||
"switchToFreePlan": "切换到免费计划",
|
||||
"expirestoday": "今天",
|
||||
"expirestomorrow": "明天",
|
||||
"expiredDaysAgo": "{{days}}天前",
|
||||
"continueWith": "继续使用{{plan}}",
|
||||
"changeToPlan": "更改为{{plan}}"
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"overview": "概览",
|
||||
"name": "组织名称",
|
||||
"owner": "组织所有者",
|
||||
"admins": "组织管理员",
|
||||
"contactNumber": "添加联系电话",
|
||||
"edit": "编辑"
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"membersCount": "成员数量",
|
||||
"createdAt": "创建于",
|
||||
"projectName": "项目名称",
|
||||
"teamName": "团队名称",
|
||||
"refreshProjects": "刷新项目",
|
||||
"searchPlaceholder": "按项目名称搜索",
|
||||
"deleteProject": "您确定要删除此项目吗?",
|
||||
"confirm": "确认",
|
||||
"cancel": "取消",
|
||||
"delete": "删除项目"
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"overview": "概览",
|
||||
"users": "用户",
|
||||
"teams": "团队",
|
||||
"billing": "账单",
|
||||
"projects": "项目",
|
||||
"adminCenter": "管理中心"
|
||||
}
|
||||
33
worklenz-frontend/public/locales/zh/admin-center/teams.json
Normal file
33
worklenz-frontend/public/locales/zh/admin-center/teams.json
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"title": "团队",
|
||||
"subtitle": "团队",
|
||||
"tooltip": "刷新团队",
|
||||
"placeholder": "按名称搜索",
|
||||
"addTeam": "添加团队",
|
||||
"team": "团队",
|
||||
"membersCount": "成员数量",
|
||||
"members": "成员",
|
||||
"drawerTitle": "创建新团队",
|
||||
"label": "团队名称",
|
||||
"drawerPlaceholder": "名称",
|
||||
"create": "创建",
|
||||
"delete": "删除",
|
||||
"settings": "设置",
|
||||
"popTitle": "您确定吗?",
|
||||
"message": "请输入名称",
|
||||
"teamSettings": "团队设置",
|
||||
"teamName": "团队名称",
|
||||
"teamDescription": "团队描述",
|
||||
"teamMembers": "团队成员",
|
||||
"teamMembersCount": "团队成员数量",
|
||||
"teamMembersPlaceholder": "按名称搜索",
|
||||
"addMember": "添加成员",
|
||||
"add": "添加",
|
||||
"update": "更新",
|
||||
"teamNamePlaceholder": "团队名称",
|
||||
"user": "用户",
|
||||
"role": "角色",
|
||||
"owner": "所有者",
|
||||
"admin": "管理员",
|
||||
"member": "成员"
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"title": "用户",
|
||||
"subTitle": "用户",
|
||||
"placeholder": "按名称搜索",
|
||||
"user": "用户",
|
||||
"email": "电子邮件",
|
||||
"lastActivity": "最后活动",
|
||||
"refresh": "刷新用户"
|
||||
}
|
||||
23
worklenz-frontend/public/locales/zh/all-project-list.json
Normal file
23
worklenz-frontend/public/locales/zh/all-project-list.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"name": "名称",
|
||||
"client": "客户",
|
||||
"category": "类别",
|
||||
"status": "状态",
|
||||
"tasksProgress": "任务进度",
|
||||
"updated_at": "最后更新",
|
||||
"members": "成员",
|
||||
"setting": "设置",
|
||||
"projects": "项目",
|
||||
"refreshProjects": "刷新项目",
|
||||
"all": "全部",
|
||||
"favorites": "收藏",
|
||||
"archived": "已归档",
|
||||
"placeholder": "按名称搜索",
|
||||
"archive": "归档",
|
||||
"unarchive": "取消归档",
|
||||
"archiveConfirm": "您确定要归档此项目吗?",
|
||||
"unarchiveConfirm": "您确定要取消归档此项目吗?",
|
||||
"clickToFilter": "点击以筛选",
|
||||
"noProjects": "未找到项目",
|
||||
"addToFavourites": "添加到收藏"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"loggingOut": "正在登出...",
|
||||
"authenticating": "正在认证...",
|
||||
"gettingThingsReady": "正在为您准备..."
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"headerDescription": "重置您的密码",
|
||||
"emailLabel": "电子邮件",
|
||||
"emailPlaceholder": "输入您的电子邮件",
|
||||
"emailRequired": "请输入您的电子邮件!",
|
||||
"resetPasswordButton": "重置密码",
|
||||
"returnToLoginButton": "返回登录",
|
||||
"passwordResetSuccessMessage": "密码重置链接已发送到您的电子邮件。",
|
||||
"orText": "或",
|
||||
"successTitle": "重置指令已发送!",
|
||||
"successMessage": "重置信息已发送到您的电子邮件。请检查您的电子邮件。"
|
||||
}
|
||||
27
worklenz-frontend/public/locales/zh/auth/login.json
Normal file
27
worklenz-frontend/public/locales/zh/auth/login.json
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"headerDescription": "登录到您的账户",
|
||||
"emailLabel": "电子邮件",
|
||||
"emailPlaceholder": "输入您的电子邮件",
|
||||
"emailRequired": "请输入您的电子邮件!",
|
||||
"passwordLabel": "密码",
|
||||
"passwordPlaceholder": "输入您的密码",
|
||||
"passwordRequired": "请输入您的密码!",
|
||||
"rememberMe": "记住我",
|
||||
"loginButton": "登录",
|
||||
"signupButton": "注册",
|
||||
"forgotPasswordButton": "忘记密码?",
|
||||
"signInWithGoogleButton": "使用Google登录",
|
||||
"dontHaveAccountText": "没有账户?",
|
||||
"orText": "或",
|
||||
"successMessage": "您已成功登录!",
|
||||
"loginError": "登录失败",
|
||||
"googleLoginError": "Google登录失败",
|
||||
"validationMessages": {
|
||||
"email": "请输入有效的电子邮件地址",
|
||||
"password": "密码必须至少包含8个字符"
|
||||
},
|
||||
"errorMessages": {
|
||||
"loginErrorTitle": "登录失败",
|
||||
"loginErrorMessage": "请检查您的电子邮件和密码并重试"
|
||||
}
|
||||
}
|
||||
29
worklenz-frontend/public/locales/zh/auth/signup.json
Normal file
29
worklenz-frontend/public/locales/zh/auth/signup.json
Normal file
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"headerDescription": "注册以开始使用",
|
||||
"nameLabel": "全名",
|
||||
"namePlaceholder": "输入您的全名",
|
||||
"nameRequired": "请输入您的全名!",
|
||||
"nameMinCharacterRequired": "全名必须至少包含4个字符!",
|
||||
"emailLabel": "电子邮件",
|
||||
"emailPlaceholder": "输入您的电子邮件",
|
||||
"emailRequired": "请输入您的电子邮件!",
|
||||
"passwordLabel": "密码",
|
||||
"passwordPlaceholder": "输入您的密码",
|
||||
"passwordRequired": "请输入您的密码!",
|
||||
"passwordMinCharacterRequired": "密码必须至少包含8个字符!",
|
||||
"passwordPatternRequired": "密码不符合要求!",
|
||||
"strongPasswordPlaceholder": "输入更强的密码",
|
||||
"passwordValidationAltText": "密码必须至少包含8个字符,包括大小写字母、一个数字和一个符号。",
|
||||
"signupSuccessMessage": "您已成功注册!",
|
||||
"privacyPolicyLink": "隐私政策",
|
||||
"termsOfUseLink": "使用条款",
|
||||
"bySigningUpText": "通过注册,您同意我们的",
|
||||
"andText": "和",
|
||||
"signupButton": "注册",
|
||||
"signInWithGoogleButton": "使用Google登录",
|
||||
"alreadyHaveAccountText": "已经有账户了?",
|
||||
"loginButton": "登录",
|
||||
"orText": "或",
|
||||
"reCAPTCHAVerificationError": "reCAPTCHA验证错误",
|
||||
"reCAPTCHAVerificationErrorMessage": "我们无法验证您的reCAPTCHA。请重试。"
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"title": "验证重置电子邮件",
|
||||
"description": "输入您的新密码",
|
||||
"placeholder": "输入您的新密码",
|
||||
"confirmPasswordPlaceholder": "确认您的新密码",
|
||||
"passwordHint": "至少8个字符,包括大小写字母、一个数字和一个符号。",
|
||||
"resetPasswordButton": "重置密码",
|
||||
"orText": "或",
|
||||
"resendResetEmail": "重新发送重置电子邮件",
|
||||
"passwordRequired": "请输入您的新密码",
|
||||
"returnToLoginButton": "返回登录",
|
||||
"confirmPasswordRequired": "请确认您的新密码",
|
||||
"passwordMismatch": "两次输入的密码不匹配"
|
||||
}
|
||||
9
worklenz-frontend/public/locales/zh/common.json
Normal file
9
worklenz-frontend/public/locales/zh/common.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"login-success": "登录成功!",
|
||||
"login-failed": "登录失败。请检查您的凭据并重试。",
|
||||
"signup-success": "注册成功!欢迎加入。",
|
||||
"signup-failed": "注册失败。请确保填写所有必填字段并重试。",
|
||||
"reconnecting": "与服务器断开连接。",
|
||||
"connection-lost": "无法连接到服务器。请检查您的互联网连接。",
|
||||
"connection-restored": "成功连接到服务器"
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"formTitle": "创建您的第一个项目",
|
||||
"inputLabel": "您现在正在做什么项目?",
|
||||
"or": "或",
|
||||
"templateButton": "从模板导入",
|
||||
"createFromTemplate": "从模板创建",
|
||||
"goBack": "返回",
|
||||
"continue": "继续",
|
||||
"cancel": "取消",
|
||||
"create": "创建",
|
||||
"templateDrawerTitle": "从模板中选择",
|
||||
"createProject": "创建项目"
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"formTitle": "创建您的第一个任务。",
|
||||
"inputLable": "输入您将在其中完成的几个任务",
|
||||
"addAnother": "添加另一个",
|
||||
"goBack": "返回",
|
||||
"continue": "继续"
|
||||
}
|
||||
46
worklenz-frontend/public/locales/zh/home.json
Normal file
46
worklenz-frontend/public/locales/zh/home.json
Normal file
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"todoList": {
|
||||
"title": "待办事项列表",
|
||||
"refreshTasks": "刷新任务",
|
||||
"addTask": "+ 添加任务",
|
||||
"noTasks": "没有任务",
|
||||
"pressEnter": "按",
|
||||
"toCreate": "创建。",
|
||||
"markAsDone": "标记为完成"
|
||||
},
|
||||
"projects": {
|
||||
"title": "项目",
|
||||
"refreshProjects": "刷新项目",
|
||||
"noRecentProjects": "您当前未被分配到任何项目。",
|
||||
"noFavouriteProjects": "没有项目被标记为收藏。",
|
||||
"recent": "最近",
|
||||
"favourites": "收藏"
|
||||
},
|
||||
"tasks": {
|
||||
"assignedToMe": "分配给我",
|
||||
"assignedByMe": "由我分配",
|
||||
"all": "全部",
|
||||
"today": "今天",
|
||||
"upcoming": "即将到来",
|
||||
"overdue": "逾期",
|
||||
"noDueDate": "没有截止日期",
|
||||
"noTasks": "没有任务可显示。",
|
||||
"addTask": "+ 添加任务",
|
||||
"name": "名称",
|
||||
"project": "项目",
|
||||
"status": "状态",
|
||||
"dueDate": "截止日期",
|
||||
"dueDatePlaceholder": "设置截止日期",
|
||||
"tomorrow": "明天",
|
||||
"nextWeek": "下周",
|
||||
"nextMonth": "下个月",
|
||||
"projectRequired": "请选择一个项目",
|
||||
"pressTabToSelectDueDateAndProject": "按Tab键选择截止日期和项目",
|
||||
"dueOn": "任务截止于",
|
||||
"taskRequired": "请添加一个任务",
|
||||
"list": "列表",
|
||||
"calendar": "日历",
|
||||
"tasks": "任务",
|
||||
"refresh": "刷新"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"formTitle": "邀请您的团队一起工作",
|
||||
"inputLable": "通过电子邮件邀请",
|
||||
"addAnother": "添加另一个",
|
||||
"goBack": "返回",
|
||||
"continue": "继续",
|
||||
"skipForNow": "暂时跳过"
|
||||
}
|
||||
19
worklenz-frontend/public/locales/zh/kanban-board.json
Normal file
19
worklenz-frontend/public/locales/zh/kanban-board.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"rename": "重命名",
|
||||
"delete": "删除",
|
||||
"addTask": "添加任务",
|
||||
"addSectionButton": "添加部分",
|
||||
"changeCategory": "更改类别",
|
||||
"deleteTooltip": "删除",
|
||||
"deleteConfirmationTitle": "您确定吗?",
|
||||
"deleteConfirmationOk": "是",
|
||||
"deleteConfirmationCancel": "取消",
|
||||
"dueDate": "截止日期",
|
||||
"cancel": "取消",
|
||||
"today": "今天",
|
||||
"tomorrow": "明天",
|
||||
"assignToMe": "分配给我",
|
||||
"archive": "归档",
|
||||
"newTaskNamePlaceholder": "写一个任务名称",
|
||||
"newSubtaskNamePlaceholder": "写一个子任务名称"
|
||||
}
|
||||
6
worklenz-frontend/public/locales/zh/license-expired.json
Normal file
6
worklenz-frontend/public/locales/zh/license-expired.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"title": "您的Worklenz试用已过期!",
|
||||
"subtitle": "请立即升级。",
|
||||
"button": "立即升级",
|
||||
"checking": "正在检查订阅状态..."
|
||||
}
|
||||
31
worklenz-frontend/public/locales/zh/navbar.json
Normal file
31
worklenz-frontend/public/locales/zh/navbar.json
Normal file
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"logoAlt": "Worklenz Logo",
|
||||
"home": "首页",
|
||||
"projects": "项目",
|
||||
"schedule": "日程",
|
||||
"reporting": "报告",
|
||||
"clients": "客户",
|
||||
"teams": "团队",
|
||||
"labels": "标签",
|
||||
"jobTitles": "职位",
|
||||
"upgradePlan": "升级计划",
|
||||
"upgradePlanTooltip": "升级计划",
|
||||
"invite": "邀请",
|
||||
"inviteTooltip": "邀请团队成员加入",
|
||||
"switchTeamTooltip": "切换团队",
|
||||
"help": "帮助",
|
||||
"notificationTooltip": "查看通知",
|
||||
"profileTooltip": "查看个人资料",
|
||||
"adminCenter": "管理中心",
|
||||
"settings": "设置",
|
||||
"logOut": "登出",
|
||||
"notificationsDrawer": {
|
||||
"read": "已读通知",
|
||||
"unread": "未读通知",
|
||||
"markAsRead": "标记为已读",
|
||||
"readAndJoin": "阅读并加入",
|
||||
"accept": "接受",
|
||||
"acceptAndJoin": "接受并加入",
|
||||
"noNotifications": "没有通知"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"nameYourOrganization": "命名您的组织。",
|
||||
"worklenzAccountTitle": "为您的Worklenz账户选择一个名称。",
|
||||
"continue": "继续"
|
||||
}
|
||||
7
worklenz-frontend/public/locales/zh/phases-drawer.json
Normal file
7
worklenz-frontend/public/locales/zh/phases-drawer.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"configurePhases": "配置阶段",
|
||||
"phaseLabel": "阶段标签",
|
||||
"enterPhaseName": "输入阶段标签名称",
|
||||
"addOption": "添加选项",
|
||||
"phaseOptions": "阶段选项:"
|
||||
}
|
||||
42
worklenz-frontend/public/locales/zh/project-drawer.json
Normal file
42
worklenz-frontend/public/locales/zh/project-drawer.json
Normal file
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"createProject": "创建项目",
|
||||
"editProject": "编辑项目",
|
||||
"enterCategoryName": "输入类别名称",
|
||||
"hitEnterToCreate": "按回车键创建!",
|
||||
"enterNotes": "备注",
|
||||
"youCanManageClientsUnderSettings": "您可以在设置中管理客户",
|
||||
"addCategory": "向项目添加类别",
|
||||
"newCategory": "新类别",
|
||||
"notes": "备注",
|
||||
"startDate": "开始日期",
|
||||
"endDate": "结束日期",
|
||||
"estimateWorkingDays": "估算工作日",
|
||||
"estimateManDays": "估算人天",
|
||||
"hoursPerDay": "每天小时数",
|
||||
"create": "创建",
|
||||
"update": "更新",
|
||||
"delete": "删除",
|
||||
"typeToSearchClients": "输入以搜索客户",
|
||||
"projectColor": "项目颜色",
|
||||
"pleaseEnterAName": "请输入名称",
|
||||
"enterProjectName": "输入项目名称",
|
||||
"name": "名称",
|
||||
"status": "状态",
|
||||
"health": "健康状况",
|
||||
"category": "类别",
|
||||
"projectManager": "项目经理",
|
||||
"client": "客户",
|
||||
"deleteConfirmation": "您确定要删除吗?",
|
||||
"deleteConfirmationDescription": "这将删除所有相关数据且无法撤销。",
|
||||
"yes": "是",
|
||||
"no": "否",
|
||||
"createdAt": "创建于",
|
||||
"updatedAt": "更新于",
|
||||
"by": "由",
|
||||
"add": "添加",
|
||||
"asClient": "作为客户",
|
||||
"createClient": "创建客户",
|
||||
"searchInputPlaceholder": "按名称或电子邮件搜索",
|
||||
"hoursPerDayValidationMessage": "每天小时数必须是1到24之间的数字",
|
||||
"noPermission": "无权限"
|
||||
}
|
||||
14
worklenz-frontend/public/locales/zh/project-view-files.json
Normal file
14
worklenz-frontend/public/locales/zh/project-view-files.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"nameColumn": "名称",
|
||||
"attachedTaskColumn": "附加任务",
|
||||
"sizeColumn": "大小",
|
||||
"uploadedByColumn": "上传者",
|
||||
"uploadedAtColumn": "上传时间",
|
||||
"fileIconAlt": "文件图标",
|
||||
"titleDescriptionText": "此项目中任务的所有附件将显示在这里。",
|
||||
"deleteConfirmationTitle": "您确定吗?",
|
||||
"deleteConfirmationOk": "是",
|
||||
"deleteConfirmationCancel": "取消",
|
||||
"segmentedTooltip": "即将推出!在列表视图和缩略图视图之间切换。",
|
||||
"emptyText": "项目中没有附件。"
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"overview": {
|
||||
"title": "概览",
|
||||
"statusOverview": "状态概览",
|
||||
"priorityOverview": "优先级概览",
|
||||
"lastUpdatedTasks": "最近更新的任务"
|
||||
},
|
||||
"members": {
|
||||
"title": "成员",
|
||||
"tooltip": "成员",
|
||||
"tasksByMembers": "按成员分类任务",
|
||||
"tasksByMembersTooltip": "按成员分类任务",
|
||||
"name": "名称",
|
||||
"taskCount": "任务计数",
|
||||
"contribution": "贡献",
|
||||
"completed": "已完成",
|
||||
"incomplete": "未完成",
|
||||
"overdue": "逾期",
|
||||
"progress": "进度"
|
||||
},
|
||||
"tasks": {
|
||||
"overdueTasks": "逾期任务",
|
||||
"overLoggedTasks": "超额记录任务",
|
||||
"tasksCompletedEarly": "提前完成的任务",
|
||||
"tasksCompletedLate": "延迟完成的任务",
|
||||
"overLoggedTasksTooltip": "记录时间超过预计时间的任务",
|
||||
"overdueTasksTooltip": "超过截止日期的任务"
|
||||
},
|
||||
"common": {
|
||||
"seeAll": "查看全部",
|
||||
"totalLoggedHours": "总记录小时数",
|
||||
"totalEstimation": "总估算",
|
||||
"completedTasks": "已完成任务",
|
||||
"incompleteTasks": "未完成任务",
|
||||
"overdueTasks": "逾期任务",
|
||||
"overdueTasksTooltip": "超过截止日期的任务",
|
||||
"totalLoggedHoursTooltip": "任务估算和任务记录时间。",
|
||||
"includeArchivedTasks": "包含已归档任务",
|
||||
"export": "导出"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"nameColumn": "名称",
|
||||
"jobTitleColumn": "职位",
|
||||
"emailColumn": "电子邮件",
|
||||
"tasksColumn": "任务",
|
||||
"taskProgressColumn": "任务进度",
|
||||
"accessColumn": "访问权限",
|
||||
"fileIconAlt": "文件图标",
|
||||
"deleteConfirmationTitle": "您确定吗?",
|
||||
"deleteConfirmationOk": "是",
|
||||
"deleteConfirmationCancel": "取消",
|
||||
"refreshButtonTooltip": "刷新成员",
|
||||
"deleteButtonTooltip": "从项目中移除",
|
||||
"memberCount": "成员",
|
||||
"membersCountPlural": "成员",
|
||||
"emptyText": "项目中没有附件。"
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"inputPlaceholder": "添加评论",
|
||||
"addButton": "添加",
|
||||
"cancelButton": "取消",
|
||||
"deleteButton": "删除"
|
||||
}
|
||||
14
worklenz-frontend/public/locales/zh/project-view.json
Normal file
14
worklenz-frontend/public/locales/zh/project-view.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"taskList": "任务列表",
|
||||
"board": "看板",
|
||||
"insights": "数据洞察",
|
||||
"files": "文件",
|
||||
"members": "成员",
|
||||
"updates": "动态更新",
|
||||
"projectView": "项目视图",
|
||||
"loading": "正在加载项目...",
|
||||
"error": "加载项目时出错",
|
||||
"pinnedTab": "已固定为默认标签页",
|
||||
"pinTab": "固定为默认标签页",
|
||||
"unpinTab": "取消固定默认标签页"
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"importTaskTemplate": "导入任务模板",
|
||||
"templateName": "模板名称",
|
||||
"templateDescription": "模板描述",
|
||||
"selectedTasks": "已选任务",
|
||||
"tasks": "任务",
|
||||
"templates": "模板",
|
||||
"remove": "移除",
|
||||
"cancel": "取消",
|
||||
"import": "导入"
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"title": "项目成员",
|
||||
"searchLabel": "通过添加名称或电子邮件添加成员",
|
||||
"searchPlaceholder": "输入名称或电子邮件",
|
||||
"inviteAsAMember": "邀请为成员",
|
||||
"inviteNewMemberByEmail": "通过电子邮件邀请新成员"
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"importTasks": "导入任务",
|
||||
"importTask": "导入任务",
|
||||
"createTask": "创建任务",
|
||||
"settings": "设置",
|
||||
"subscribe": "订阅",
|
||||
"unsubscribe": "取消订阅",
|
||||
"deleteProject": "删除项目",
|
||||
"startDate": "开始日期",
|
||||
"endDate": "结束日期",
|
||||
"projectSettings": "项目设置",
|
||||
"projectSummary": "项目摘要",
|
||||
"receiveProjectSummary": "每晚接收项目摘要。",
|
||||
"refreshProject": "刷新项目",
|
||||
"saveAsTemplate": "保存为模板",
|
||||
"invite": "邀请",
|
||||
"subscribeTooltip": "订阅项目通知",
|
||||
"unsubscribeTooltip": "取消订阅项目通知",
|
||||
"refreshTooltip": "刷新项目数据",
|
||||
"settingsTooltip": "打开项目设置",
|
||||
"saveAsTemplateTooltip": "将此项目保存为模板",
|
||||
"inviteTooltip": "邀请团队成员加入此项目",
|
||||
"createTaskTooltip": "创建新任务",
|
||||
"importTaskTooltip": "从模板导入任务",
|
||||
"navigateBackTooltip": "返回项目列表",
|
||||
"projectStatusTooltip": "项目状态",
|
||||
"projectDatesInfo": "项目时间安排信息",
|
||||
"projectCategoryTooltip": "项目类别"
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"title": "保存为模板",
|
||||
"templateName": "模板名称",
|
||||
"includes": "项目中应包含哪些内容到模板中?",
|
||||
"includesOptions": {
|
||||
"statuses": "状态",
|
||||
"phases": "阶段",
|
||||
"labels": "标签"
|
||||
},
|
||||
"taskIncludes": "任务中应包含哪些内容到模板中?",
|
||||
"taskIncludesOptions": {
|
||||
"statuses": "状态",
|
||||
"phases": "阶段",
|
||||
"labels": "标签",
|
||||
"name": "名称",
|
||||
"priority": "优先级",
|
||||
"status": "状态",
|
||||
"phase": "阶段",
|
||||
"label": "标签",
|
||||
"timeEstimate": "预计用时",
|
||||
"description": "描述",
|
||||
"subTasks": "子任务"
|
||||
},
|
||||
"cancel": "取消",
|
||||
"save": "保存",
|
||||
"templateNamePlaceholder": "输入模板名称"
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
{
|
||||
"exportButton": "导出",
|
||||
"timeLogsButton": "时间日志",
|
||||
"activityLogsButton": "活动日志",
|
||||
"tasksButton": "任务",
|
||||
"searchByNameInputPlaceholder": "按名称搜索",
|
||||
"overviewTab": "概览",
|
||||
"timeLogsTab": "时间日志",
|
||||
"activityLogsTab": "活动日志",
|
||||
"tasksTab": "任务",
|
||||
"projectsText": "项目",
|
||||
"totalTasksText": "任务总数",
|
||||
"assignedTasksText": "已分配任务",
|
||||
"completedTasksText": "已完成任务",
|
||||
"ongoingTasksText": "进行中任务",
|
||||
"overdueTasksText": "逾期任务",
|
||||
"loggedHoursText": "记录小时数",
|
||||
"tasksText": "任务",
|
||||
"allText": "全部",
|
||||
"tasksByProjectsText": "按项目分类任务",
|
||||
"tasksByStatusText": "按状态分类任务",
|
||||
"tasksByPriorityText": "按优先级分类任务",
|
||||
"todoText": "待办",
|
||||
"doingText": "进行中",
|
||||
"doneText": "已完成",
|
||||
"lowText": "低",
|
||||
"mediumText": "中",
|
||||
"highText": "高",
|
||||
"billableButton": "可计费",
|
||||
"billableText": "可计费",
|
||||
"nonBillableText": "不可计费",
|
||||
"timeLogsEmptyPlaceholder": "没有时间日志可显示",
|
||||
"loggedText": "记录",
|
||||
"forText": "为",
|
||||
"inText": "在",
|
||||
"updatedText": "更新",
|
||||
"fromText": "从",
|
||||
"toText": "到",
|
||||
"withinText": "在...之内",
|
||||
"activityLogsEmptyPlaceholder": "没有活动日志可显示",
|
||||
"filterByText": "筛选依据:",
|
||||
"selectProjectPlaceholder": "选择项目",
|
||||
"taskColumn": "任务",
|
||||
"nameColumn": "名称",
|
||||
"projectColumn": "项目",
|
||||
"statusColumn": "状态",
|
||||
"priorityColumn": "优先级",
|
||||
"dueDateColumn": "截止日期",
|
||||
"completedDateColumn": "完成日期",
|
||||
"estimatedTimeColumn": "预计用时",
|
||||
"loggedTimeColumn": "记录时间",
|
||||
"overloggedTimeColumn": "超额记录时间",
|
||||
"daysLeftColumn": "剩余天数/逾期",
|
||||
"startDateColumn": "开始日期",
|
||||
"endDateColumn": "结束日期",
|
||||
"actualTimeColumn": "实际时间",
|
||||
"projectHealthColumn": "项目健康状况",
|
||||
"categoryColumn": "类别",
|
||||
"projectManagerColumn": "项目经理",
|
||||
"tasksStatsOverviewDrawerTitle": "的任务",
|
||||
"projectsStatsOverviewDrawerTitle": "的项目",
|
||||
"cancelledText": "已取消",
|
||||
"blockedText": "已阻塞",
|
||||
"onHoldText": "暂停",
|
||||
"proposedText": "提议",
|
||||
"inPlanningText": "规划中",
|
||||
"inProgressText": "进行中",
|
||||
"completedText": "已完成",
|
||||
"continuousText": "持续",
|
||||
"daysLeftText": "天剩余",
|
||||
"daysOverdueText": "天逾期",
|
||||
"notSetText": "未设置",
|
||||
"needsAttentionText": "需要关注",
|
||||
"atRiskText": "有风险",
|
||||
"goodText": "良好"
|
||||
}
|
||||
31
worklenz-frontend/public/locales/zh/reporting-members.json
Normal file
31
worklenz-frontend/public/locales/zh/reporting-members.json
Normal file
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"yesterdayText": "昨天",
|
||||
"lastSevenDaysText": "过去7天",
|
||||
"lastWeekText": "上周",
|
||||
"lastThirtyDaysText": "过去30天",
|
||||
"lastMonthText": "上个月",
|
||||
"lastThreeMonthsText": "过去3个月",
|
||||
"allTimeText": "所有时间",
|
||||
"customRangeText": "自定义范围",
|
||||
"startDateInputPlaceholder": "开始日期",
|
||||
"EndDateInputPlaceholder": "结束日期",
|
||||
"filterButton": "筛选",
|
||||
"membersTitle": "成员",
|
||||
"includeArchivedButton": "包含已归档项目",
|
||||
"exportButton": "导出",
|
||||
"excelButton": "Excel",
|
||||
"searchByNameInputPlaceholder": "按名称搜索",
|
||||
"memberColumn": "成员",
|
||||
"tasksProgressColumn": "任务进度",
|
||||
"tasksAssignedColumn": "分配任务",
|
||||
"completedTasksColumn": "已完成任务",
|
||||
"overdueTasksColumn": "逾期任务",
|
||||
"ongoingTasksColumn": "进行中任务",
|
||||
"tasksAssignedColumnTooltip": "在选定日期范围内分配的任务",
|
||||
"overdueTasksColumnTooltip": "在选定日期范围结束时逾期的任务",
|
||||
"completedTasksColumnTooltip": "在选定日期范围内完成的任务",
|
||||
"ongoingTasksColumnTooltip": "已开始但尚未完成的任务",
|
||||
"todoText": "待办",
|
||||
"doingText": "进行中",
|
||||
"doneText": "已完成"
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"exportButton": "导出",
|
||||
"projectsButton": "项目",
|
||||
"membersButton": "成员",
|
||||
"searchByNameInputPlaceholder": "按名称搜索",
|
||||
"overviewTab": "概览",
|
||||
"projectsTab": "项目",
|
||||
"membersTab": "成员",
|
||||
"projectsByStatusText": "按状态分类项目",
|
||||
"projectsByCategoryText": "按类别分类项目",
|
||||
"projectsByHealthText": "按健康状况分类项目",
|
||||
"projectsText": "项目",
|
||||
"allText": "全部",
|
||||
"cancelledText": "已取消",
|
||||
"blockedText": "已阻塞",
|
||||
"onHoldText": "暂停",
|
||||
"proposedText": "提议",
|
||||
"inPlanningText": "规划中",
|
||||
"inProgressText": "进行中",
|
||||
"completedText": "已完成",
|
||||
"continuousText": "持续",
|
||||
"notSetText": "未设置",
|
||||
"needsAttentionText": "需要关注",
|
||||
"atRiskText": "有风险",
|
||||
"goodText": "良好",
|
||||
"nameColumn": "名称",
|
||||
"emailColumn": "电子邮件",
|
||||
"projectsColumn": "项目",
|
||||
"tasksColumn": "任务",
|
||||
"overdueTasksColumn": "逾期任务",
|
||||
"completedTasksColumn": "已完成任务",
|
||||
"ongoingTasksColumn": "进行中任务"
|
||||
}
|
||||
22
worklenz-frontend/public/locales/zh/reporting-overview.json
Normal file
22
worklenz-frontend/public/locales/zh/reporting-overview.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"overviewTitle": "概览",
|
||||
"includeArchivedButton": "包含已归档项目",
|
||||
"teamCount": "团队",
|
||||
"teamCountPlural": "团队",
|
||||
"projectCount": "项目",
|
||||
"projectCountPlural": "项目",
|
||||
"memberCount": "成员",
|
||||
"memberCountPlural": "成员",
|
||||
"activeProjectCount": "活跃项目",
|
||||
"activeProjectCountPlural": "活跃项目",
|
||||
"overdueProjectCount": "逾期项目",
|
||||
"overdueProjectCountPlural": "逾期项目",
|
||||
"unassignedMemberCount": "未分配成员",
|
||||
"unassignedMemberCountPlural": "未分配成员",
|
||||
"memberWithOverdueTaskCount": "有逾期任务的成员",
|
||||
"memberWithOverdueTaskCountPlural": "有逾期任务的成员",
|
||||
"teamsText": "团队",
|
||||
"nameColumn": "名称",
|
||||
"projectsColumn": "项目",
|
||||
"membersColumn": "成员"
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
{
|
||||
"exportButton": "导出",
|
||||
"membersButton": "成员",
|
||||
"tasksButton": "任务",
|
||||
"searchByNameInputPlaceholder": "按名称搜索",
|
||||
"overviewTab": "概览",
|
||||
"membersTab": "成员",
|
||||
"tasksTab": "任务",
|
||||
"completedTasksText": "已完成任务",
|
||||
"incompleteTasksText": "未完成任务",
|
||||
"overdueTasksText": "逾期任务",
|
||||
"allocatedHoursText": "已分配小时数",
|
||||
"loggedHoursText": "已记录小时数",
|
||||
"tasksText": "任务",
|
||||
"allText": "全部",
|
||||
"tasksByStatusText": "按状态分类任务",
|
||||
"tasksByPriorityText": "按优先级分类任务",
|
||||
"tasksByDueDateText": "按截止日期分类任务",
|
||||
"todoText": "待办",
|
||||
"doingText": "进行中",
|
||||
"doneText": "已完成",
|
||||
"lowText": "低",
|
||||
"mediumText": "中",
|
||||
"highText": "高",
|
||||
"completedText": "已完成",
|
||||
"upcomingText": "即将到来",
|
||||
"overdueText": "逾期",
|
||||
"noDueDateText": "无截止日期",
|
||||
"nameColumn": "名称",
|
||||
"tasksCountColumn": "任务计数",
|
||||
"completedTasksColumn": "已完成任务",
|
||||
"incompleteTasksColumn": "未完成任务",
|
||||
"overdueTasksColumn": "逾期任务",
|
||||
"contributionColumn": "贡献",
|
||||
"progressColumn": "进度",
|
||||
"loggedTimeColumn": "记录时间",
|
||||
"taskColumn": "任务",
|
||||
"projectColumn": "项目",
|
||||
"statusColumn": "状态",
|
||||
"priorityColumn": "优先级",
|
||||
"phaseColumn": "阶段",
|
||||
"dueDateColumn": "截止日期",
|
||||
"completedDateColumn": "完成日期",
|
||||
"estimatedTimeColumn": "预计用时",
|
||||
"overloggedTimeColumn": "超额记录时间",
|
||||
"completedOnColumn": "完成于",
|
||||
"daysOverdueColumn": "逾期天数",
|
||||
"groupByText": "分组依据:",
|
||||
"statusText": "状态",
|
||||
"priorityText": "优先级",
|
||||
"phaseText": "阶段"
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"searchByNamePlaceholder": "按名称搜索",
|
||||
"searchByCategoryPlaceholder": "按类别搜索",
|
||||
"statusText": "状态",
|
||||
"healthText": "健康状况",
|
||||
"categoryText": "类别",
|
||||
"projectManagerText": "项目经理",
|
||||
"showFieldsText": "显示字段",
|
||||
"cancelledText": "已取消",
|
||||
"blockedText": "已阻塞",
|
||||
"onHoldText": "暂停",
|
||||
"proposedText": "提议",
|
||||
"inPlanningText": "规划中",
|
||||
"inProgressText": "进行中",
|
||||
"completedText": "已完成",
|
||||
"continuousText": "持续",
|
||||
"notSetText": "未设置",
|
||||
"needsAttentionText": "需要关注",
|
||||
"atRiskText": "有风险",
|
||||
"goodText": "良好",
|
||||
"nameText": "项目",
|
||||
"estimatedVsActualText": "预计用时 vs 实际用时",
|
||||
"tasksProgressText": "任务进度",
|
||||
"lastActivityText": "最后活动",
|
||||
"datesText": "开始/结束日期",
|
||||
"daysLeftText": "剩余天数/逾期",
|
||||
"projectHealthText": "项目健康状况",
|
||||
"projectUpdateText": "项目更新",
|
||||
"clientText": "客户",
|
||||
"teamText": "团队"
|
||||
}
|
||||
44
worklenz-frontend/public/locales/zh/reporting-projects.json
Normal file
44
worklenz-frontend/public/locales/zh/reporting-projects.json
Normal file
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"projectCount": "项目",
|
||||
"projectCountPlural": "项目",
|
||||
"includeArchivedButton": "包含已归档项目",
|
||||
"exportButton": "导出",
|
||||
"excelButton": "Excel",
|
||||
"projectColumn": "项目",
|
||||
"estimatedVsActualColumn": "预计用时 vs 实际用时",
|
||||
"tasksProgressColumn": "任务进度",
|
||||
"lastActivityColumn": "最后活动",
|
||||
"statusColumn": "状态",
|
||||
"datesColumn": "开始/结束日期",
|
||||
"daysLeftColumn": "剩余天数/逾期",
|
||||
"projectHealthColumn": "项目健康状况",
|
||||
"categoryColumn": "类别",
|
||||
"projectUpdateColumn": "项目更新",
|
||||
"clientColumn": "客户",
|
||||
"teamColumn": "团队",
|
||||
"projectManagerColumn": "项目经理",
|
||||
"openButton": "打开",
|
||||
"estimatedText": "预计",
|
||||
"actualText": "实际",
|
||||
"todoText": "待办",
|
||||
"doingText": "进行中",
|
||||
"doneText": "已完成",
|
||||
"cancelledText": "已取消",
|
||||
"blockedText": "已阻塞",
|
||||
"onHoldText": "暂停",
|
||||
"proposedText": "提议",
|
||||
"inPlanningText": "规划中",
|
||||
"inProgressText": "进行中",
|
||||
"completedText": "已完成",
|
||||
"continuousText": "持续",
|
||||
"daysLeftText": "天剩余",
|
||||
"dayLeftText": "天剩余",
|
||||
"daysOverdueText": "天逾期",
|
||||
"notSetText": "未设置",
|
||||
"needsAttentionText": "需要关注",
|
||||
"atRiskText": "有风险",
|
||||
"goodText": "良好",
|
||||
"setCategoryText": "设置类别",
|
||||
"searchByNameInputPlaceholder": "按名称搜索",
|
||||
"todayText": "今天"
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"overview": "概览",
|
||||
"projects": "项目",
|
||||
"members": "成员",
|
||||
"timeReports": "用时报告",
|
||||
"estimateVsActual": "预计用时 vs 实际用时",
|
||||
"currentOrganizationTooltip": "当前的组织"
|
||||
}
|
||||
34
worklenz-frontend/public/locales/zh/schedule.json
Normal file
34
worklenz-frontend/public/locales/zh/schedule.json
Normal file
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"today": "今天",
|
||||
"week": "周",
|
||||
"month": "月",
|
||||
"settings": "设置",
|
||||
"workingDays": "工作日",
|
||||
"monday": "星期一",
|
||||
"tuesday": "星期二",
|
||||
"wednesday": "星期三",
|
||||
"thursday": "星期四",
|
||||
"friday": "星期五",
|
||||
"saturday": "星期六",
|
||||
"sunday": "星期日",
|
||||
"workingHours": "工作时间",
|
||||
"hours": "小时",
|
||||
"saveButton": "保存",
|
||||
"totalAllocation": "总分配",
|
||||
"timeLogged": "记录时间",
|
||||
"remainingTime": "剩余时间",
|
||||
"total": "总计",
|
||||
"perDay": "每天",
|
||||
"tasks": "任务",
|
||||
"startDate": "开始日期",
|
||||
"endDate": "结束日期",
|
||||
"hoursPerDay": "每天小时数",
|
||||
"totalHours": "总小时数",
|
||||
"deleteButton": "删除",
|
||||
"cancelButton": "取消",
|
||||
"tabTitle": "没有开始和结束日期的任务",
|
||||
"allocatedTime": "分配时间",
|
||||
"totalLogged": "总记录",
|
||||
"loggedBillable": "已记录可计费",
|
||||
"loggedNonBillable": "已记录不可计费"
|
||||
}
|
||||
10
worklenz-frontend/public/locales/zh/settings/categories.json
Normal file
10
worklenz-frontend/public/locales/zh/settings/categories.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"categoryColumn": "类别",
|
||||
"deleteConfirmationTitle": "您确定吗?",
|
||||
"deleteConfirmationOk": "是",
|
||||
"deleteConfirmationCancel": "取消",
|
||||
"associatedTaskColumn": "关联项目",
|
||||
"searchPlaceholder": "按名称搜索",
|
||||
"emptyText": "在更新或创建项目时可以创建类别。",
|
||||
"colorChangeTooltip": "点击更改颜色"
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"title": "更改密码",
|
||||
"currentPassword": "当前密码",
|
||||
"newPassword": "新密码",
|
||||
"confirmPassword": "确认密码",
|
||||
"currentPasswordPlaceholder": "输入您的当前密码",
|
||||
"newPasswordPlaceholder": "新密码",
|
||||
"confirmPasswordPlaceholder": "确认密码",
|
||||
"currentPasswordRequired": "请输入您的当前密码!",
|
||||
"newPasswordRequired": "请输入您的新密码!",
|
||||
"passwordValidationError": "密码必须至少包含8个字符,包括一个大写字母、一个数字和一个符号。",
|
||||
"passwordMismatch": "密码不匹配!",
|
||||
"passwordRequirements": "新密码应至少包含8个字符,包括一个大写字母、一个数字和一个符号。",
|
||||
"updateButton": "更新密码"
|
||||
}
|
||||
22
worklenz-frontend/public/locales/zh/settings/clients.json
Normal file
22
worklenz-frontend/public/locales/zh/settings/clients.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"nameColumn": "名称",
|
||||
"projectColumn": "项目",
|
||||
"noProjectsAvailable": "没有可用的项目",
|
||||
"deleteConfirmationTitle": "您确定吗?",
|
||||
"deleteConfirmationOk": "是",
|
||||
"deleteConfirmationCancel": "取消",
|
||||
"searchPlaceholder": "按名称搜索",
|
||||
"createClient": "创建客户",
|
||||
"pinTooltip": "点击将其固定到主菜单",
|
||||
"createClientDrawerTitle": "创建客户",
|
||||
"updateClientDrawerTitle": "更新客户",
|
||||
"nameLabel": "名称",
|
||||
"namePlaceholder": "名称",
|
||||
"nameRequiredError": "请输入名称",
|
||||
"createButton": "创建",
|
||||
"updateButton": "更新",
|
||||
"createClientSuccessMessage": "客户创建成功!",
|
||||
"createClientErrorMessage": "客户创建失败!",
|
||||
"updateClientSuccessMessage": "客户更新成功!",
|
||||
"updateClientErrorMessage": "客户更新失败!"
|
||||
}
|
||||
20
worklenz-frontend/public/locales/zh/settings/job-titles.json
Normal file
20
worklenz-frontend/public/locales/zh/settings/job-titles.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"nameColumn": "名称",
|
||||
"deleteConfirmationTitle": "您确定吗?",
|
||||
"deleteConfirmationOk": "是",
|
||||
"deleteConfirmationCancel": "取消",
|
||||
"searchPlaceholder": "按名称搜索",
|
||||
"createJobTitleButton": "创建职位",
|
||||
"pinTooltip": "点击将其固定到主菜单",
|
||||
"createJobTitleDrawerTitle": "创建职位",
|
||||
"updateJobTitleDrawerTitle": "更新职位",
|
||||
"nameLabel": "名称",
|
||||
"namePlaceholder": "名称",
|
||||
"nameRequiredError": "请输入名称",
|
||||
"createButton": "创建",
|
||||
"updateButton": "更新",
|
||||
"createJobTitleSuccessMessage": "职位创建成功!",
|
||||
"createJobTitleErrorMessage": "职位创建失败!",
|
||||
"updateJobTitleSuccessMessage": "职位更新成功!",
|
||||
"updateJobTitleErrorMessage": "职位更新失败!"
|
||||
}
|
||||
11
worklenz-frontend/public/locales/zh/settings/labels.json
Normal file
11
worklenz-frontend/public/locales/zh/settings/labels.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"labelColumn": "标签",
|
||||
"deleteConfirmationTitle": "您确定吗?",
|
||||
"deleteConfirmationOk": "是",
|
||||
"deleteConfirmationCancel": "取消",
|
||||
"associatedTaskColumn": "关联任务计数",
|
||||
"searchPlaceholder": "按名称搜索",
|
||||
"emptyText": "标签可以在更新或创建任务时创建。",
|
||||
"pinTooltip": "点击将其固定到主菜单",
|
||||
"colorChangeTooltip": "点击更改颜色"
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"language": "语言",
|
||||
"language_required": "语言是必需的",
|
||||
"time_zone": "时区",
|
||||
"time_zone_required": "时区是必需的",
|
||||
"save_changes": "保存更改"
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"title": "通知设置",
|
||||
"emailTitle": "向我发送电子邮件通知",
|
||||
"emailDescription": "包括新的任务分配",
|
||||
"dailyDigestTitle": "向我发送每日摘要",
|
||||
"dailyDigestDescription": "每天晚上,您将收到任务中最近活动的摘要。",
|
||||
"popupTitle": "当Worklenz打开时,在我的电脑上弹出通知",
|
||||
"popupDescription": "弹出通知可能会被您的浏览器禁用。更改您的浏览器设置以允许它们。",
|
||||
"unreadItemsTitle": "显示未读项目的数量",
|
||||
"unreadItemsDescription": "您将看到每个通知的计数。"
|
||||
}
|
||||
14
worklenz-frontend/public/locales/zh/settings/profile.json
Normal file
14
worklenz-frontend/public/locales/zh/settings/profile.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"uploadError": "您只能上传JPG/PNG文件!",
|
||||
"uploadSizeError": "图片必须小于2MB!",
|
||||
"upload": "上传",
|
||||
"nameLabel": "名称",
|
||||
"nameRequiredError": "名称是必需的",
|
||||
"emailLabel": "电子邮件",
|
||||
"emailRequiredError": "电子邮件是必需的",
|
||||
"saveChanges": "保存更改",
|
||||
"profileJoinedText": "一个月前加入",
|
||||
"profileLastUpdatedText": "一个月前更新",
|
||||
"avatarTooltip": "点击上传头像",
|
||||
"title": "个人资料设置"
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"nameColumn": "名称",
|
||||
"editToolTip": "编辑",
|
||||
"deleteToolTip": "删除",
|
||||
"confirmText": "您确定吗?",
|
||||
"okText": "是",
|
||||
"cancelText": "取消"
|
||||
}
|
||||
15
worklenz-frontend/public/locales/zh/settings/sidebar.json
Normal file
15
worklenz-frontend/public/locales/zh/settings/sidebar.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"profile": "个人资料",
|
||||
"appearance": "外观",
|
||||
"notifications": "通知",
|
||||
"clients": "客户",
|
||||
"job-titles": "职位",
|
||||
"labels": "标签",
|
||||
"categories": "类别",
|
||||
"project-templates": "项目模板",
|
||||
"task-templates": "任务模板",
|
||||
"team-members": "团队成员",
|
||||
"teams": "团队",
|
||||
"change-password": "更改密码",
|
||||
"language-and-region": "语言和地区"
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"nameColumn": "名称",
|
||||
"createdColumn": "创建时间",
|
||||
"editToolTip": "编辑",
|
||||
"deleteToolTip": "删除",
|
||||
"confirmText": "您确定吗?",
|
||||
"okText": "是",
|
||||
"cancelText": "取消"
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user