Merge pull request #247 from Worklenz/release/v2.0.4-bug-fix

Release/v2.0.4 bug fix
This commit is contained in:
Chamika J
2025-07-09 06:22:56 +05:30
committed by GitHub
159 changed files with 6010 additions and 2651 deletions

16
backup.sh Normal file
View 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"

View File

@@ -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
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 '
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:

View File

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

View 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."

View File

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

View File

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

View File

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

View File

@@ -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,19 +157,100 @@ 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,
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,
(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,
parent_task.name AS parent_task_name,
t.status_id AS status,
t.archived,
t.description,
@@ -199,74 +258,70 @@ export default class TasksControllerV2 extends TasksControllerBase {
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}
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> = {};
// 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, "_");
}
// 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, "_");
// 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;
});
groupedResponse[groupKey] = {
return {
id: group.id,
title: group.name,
groupType: groupBy,
groupValue: groupKey,
groupValue,
collapsed: false,
tasks: [],
taskIds: [],
color: group.color_code || this.getDefaultGroupColor(groupBy, groupKey),
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,
};
});
// 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);
}
}
});
// 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();
}).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"));
}
}
}

View File

@@ -2,31 +2,35 @@
<html lang="en">
<head>
<title></title>
<title>Worklenz 2.1.0 Release</title>
<meta name="subject" content="Worklenz 2.1.0 Release" />
<meta content="text/html; charset=utf-8" http-equiv="Content-Type">
<meta content="width=device-width,initial-scale=1" name="viewport">
<style>
* {
box-sizing: border-box
box-sizing: border-box;
}
body {
margin: 0;
padding: 0
padding: 0;
background: #f6f8fa;
font-family: 'Mada', 'Segoe UI', Arial, sans-serif;
color: #222;
}
a[x-apple-data-detectors] {
color: inherit !important;
text-decoration: inherit !important
text-decoration: inherit !important;
}
#MessageViewBody a {
color: inherit;
text-decoration: none
text-decoration: none;
}
p {
line-height: inherit
line-height: 1.6;
}
.padding-30 {
@@ -37,272 +41,201 @@
padding: 0px 20px;
}
.desktop_hide,
.desktop_hide table {
mso-hide: all;
.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;
}
.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;
}
.main-btn {
width: 90%;
font-size: 16px;
padding: 12px 0;
}
}
@media (prefers-color-scheme: dark) {
body {
background: #181a1b;
color: #e6e6e6;
}
.card {
background: #23272a;
box-shadow: 0 2px 12px rgba(24, 144, 255, 0.13);
}
.main-btn {
background: #1890ff;
color: #fff;
border: 2px solid #1890ff;
}
.main-btn:hover {
background: #40a9ff;
color: #fff;
border-color: #40a9ff;
}
.logo-light {
display: none !important;
}
.logo-dark {
display: block !important;
}
}
.logo-light {
display: block;
}
.logo-dark {
display: none;
max-height: 0;
overflow: hidden
}
@media (max-width: 525px) {
.desktop_hide table.icons-inner {
display: inline-block !important
}
.icons-inner {
text-align: center
}
.icons-inner td {
margin: 0 auto
}
.row-content {
width: 95% !important
}
.mobile_hide {
display: none
}
.stack .column {
width: 100%;
display: block
}
.mobile_hide {
min-height: 0;
max-height: 0;
max-width: 0;
overflow: hidden;
font-size: 0
}
.desktop_hide,
.desktop_hide table {
display: table !important;
max-height: none !important
}
}
</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>
<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>
<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 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>
</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 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>
</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 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>
</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 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>
</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>
<a href="https://app.worklenz.com/auth" target="_blank" class="main-btn">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 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>
</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>
</html>

View File

@@ -5,30 +5,51 @@
<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() {
// 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';
const trackingId = isProduction ? 'G-7KSRKQ1397' : 'G-3LM2HGWEXG'; // Open source tracking ID
// Load the Google Analytics script
const script = document.createElement('script');
script.async = true;
// Determine which tracking ID to use based on the environment
const isProduction =
window.location.hostname === 'worklenz.com' ||
window.location.hostname === 'app.worklenz.com';
const trackingId = isProduction ? 'G-XXXXXXXXXX' : 'G-3LM2HGWEXG'; // Open source tracking ID
script.src = `https://www.googletagmanager.com/gtag/js?id=${trackingId}`;
document.head.appendChild(script);
@@ -39,9 +60,17 @@
}
gtag('js', new Date());
gtag('config', trackingId);
};
// Use requestIdleCallback if available, otherwise setTimeout
if ('requestIdleCallback' in window) {
requestIdleCallback(loadAnalytics, { timeout: 2000 });
} else {
setTimeout(loadAnalytics, 1000);
}
}
// Initialize analytics
// Initialize analytics after a delay to not block initial render
initGoogleAnalytics();
// Function to show privacy notice
@@ -98,7 +127,10 @@
<div id="root"></div>
<script type="module" src="./src/index.tsx"></script>
<script type="text/javascript">
// Load HubSpot script asynchronously and only for production
if (window.location.hostname === 'app.worklenz.com') {
// Use requestIdleCallback to defer HubSpot loading
const loadHubSpot = () => {
var hs = document.createElement('script');
hs.type = 'text/javascript';
hs.id = 'hs-script-loader';
@@ -106,6 +138,13 @@
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>

View File

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

View File

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

View File

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

View File

@@ -9,5 +9,6 @@
"saveChanges": "Ruaj Ndryshimet",
"profileJoinedText": "U bashkua një muaj më parë",
"profileLastUpdatedText": "Përditësuar një muaj më parë",
"avatarTooltip": "Klikoni për të ngarkuar një avatar"
"avatarTooltip": "Klikoni për të ngarkuar një avatar",
"title": "Cilësimet e Profilit"
}

View File

@@ -1,4 +1,5 @@
{
"title": "Anëtarët e Ekipit",
"nameColumn": "Emri",
"projectsColumn": "Projektet",
"emailColumn": "Email",
@@ -40,5 +41,7 @@
"ownerText": "Pronar i Ekipit",
"addedText": "Shtuar",
"updatedText": "Përditësuar",
"noResultFound": "Shkruani një adresë email dhe shtypni Enter..."
"noResultFound": "Shkruani një adresë email dhe shtypni Enter...",
"jobTitlesFetchError": "Dështoi marrja e titujve të punës",
"invitationResent": "Ftesa u dërgua sërish me sukses!"
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View 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!"
}

View File

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

View File

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

View File

@@ -0,0 +1,4 @@
{
"doesNotExistText": "抱歉,您访问的页面不存在。",
"backHomeButton": "返回首页"
}

View 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个任务"
}

View File

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

View File

@@ -0,0 +1,8 @@
{
"overview": "概览",
"name": "组织名称",
"owner": "组织所有者",
"admins": "组织管理员",
"contactNumber": "添加联系电话",
"edit": "编辑"
}

View File

@@ -0,0 +1,12 @@
{
"membersCount": "成员数量",
"createdAt": "创建于",
"projectName": "项目名称",
"teamName": "团队名称",
"refreshProjects": "刷新项目",
"searchPlaceholder": "按项目名称搜索",
"deleteProject": "您确定要删除此项目吗?",
"confirm": "确认",
"cancel": "取消",
"delete": "删除项目"
}

View File

@@ -0,0 +1,8 @@
{
"overview": "概览",
"users": "用户",
"teams": "团队",
"billing": "账单",
"projects": "项目",
"adminCenter": "管理中心"
}

View 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": "成员"
}

View File

@@ -0,0 +1,9 @@
{
"title": "用户",
"subTitle": "用户",
"placeholder": "按名称搜索",
"user": "用户",
"email": "电子邮件",
"lastActivity": "最后活动",
"refresh": "刷新用户"
}

View 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": "添加到收藏"
}

View File

@@ -0,0 +1,5 @@
{
"loggingOut": "正在登出...",
"authenticating": "正在认证...",
"gettingThingsReady": "正在为您准备..."
}

View File

@@ -0,0 +1,12 @@
{
"headerDescription": "重置您的密码",
"emailLabel": "电子邮件",
"emailPlaceholder": "输入您的电子邮件",
"emailRequired": "请输入您的电子邮件!",
"resetPasswordButton": "重置密码",
"returnToLoginButton": "返回登录",
"passwordResetSuccessMessage": "密码重置链接已发送到您的电子邮件。",
"orText": "或",
"successTitle": "重置指令已发送!",
"successMessage": "重置信息已发送到您的电子邮件。请检查您的电子邮件。"
}

View 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": "请检查您的电子邮件和密码并重试"
}
}

View 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。请重试。"
}

View File

@@ -0,0 +1,14 @@
{
"title": "验证重置电子邮件",
"description": "输入您的新密码",
"placeholder": "输入您的新密码",
"confirmPasswordPlaceholder": "确认您的新密码",
"passwordHint": "至少8个字符包括大小写字母、一个数字和一个符号。",
"resetPasswordButton": "重置密码",
"orText": "或",
"resendResetEmail": "重新发送重置电子邮件",
"passwordRequired": "请输入您的新密码",
"returnToLoginButton": "返回登录",
"confirmPasswordRequired": "请确认您的新密码",
"passwordMismatch": "两次输入的密码不匹配"
}

View File

@@ -0,0 +1,9 @@
{
"login-success": "登录成功!",
"login-failed": "登录失败。请检查您的凭据并重试。",
"signup-success": "注册成功!欢迎加入。",
"signup-failed": "注册失败。请确保填写所有必填字段并重试。",
"reconnecting": "与服务器断开连接。",
"connection-lost": "无法连接到服务器。请检查您的互联网连接。",
"connection-restored": "成功连接到服务器"
}

View File

@@ -0,0 +1,13 @@
{
"formTitle": "创建您的第一个项目",
"inputLabel": "您现在正在做什么项目?",
"or": "或",
"templateButton": "从模板导入",
"createFromTemplate": "从模板创建",
"goBack": "返回",
"continue": "继续",
"cancel": "取消",
"create": "创建",
"templateDrawerTitle": "从模板中选择",
"createProject": "创建项目"
}

View File

@@ -0,0 +1,7 @@
{
"formTitle": "创建您的第一个任务。",
"inputLable": "输入您将在其中完成的几个任务",
"addAnother": "添加另一个",
"goBack": "返回",
"continue": "继续"
}

View 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": "刷新"
}
}

View File

@@ -0,0 +1,8 @@
{
"formTitle": "邀请您的团队一起工作",
"inputLable": "通过电子邮件邀请",
"addAnother": "添加另一个",
"goBack": "返回",
"continue": "继续",
"skipForNow": "暂时跳过"
}

View File

@@ -0,0 +1,19 @@
{
"rename": "重命名",
"delete": "删除",
"addTask": "添加任务",
"addSectionButton": "添加部分",
"changeCategory": "更改类别",
"deleteTooltip": "删除",
"deleteConfirmationTitle": "您确定吗?",
"deleteConfirmationOk": "是",
"deleteConfirmationCancel": "取消",
"dueDate": "截止日期",
"cancel": "取消",
"today": "今天",
"tomorrow": "明天",
"assignToMe": "分配给我",
"archive": "归档",
"newTaskNamePlaceholder": "写一个任务名称",
"newSubtaskNamePlaceholder": "写一个子任务名称"
}

View File

@@ -0,0 +1,6 @@
{
"title": "您的Worklenz试用已过期",
"subtitle": "请立即升级。",
"button": "立即升级",
"checking": "正在检查订阅状态..."
}

View 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": "没有通知"
}
}

View File

@@ -0,0 +1,5 @@
{
"nameYourOrganization": "命名您的组织。",
"worklenzAccountTitle": "为您的Worklenz账户选择一个名称。",
"continue": "继续"
}

View File

@@ -0,0 +1,7 @@
{
"configurePhases": "配置阶段",
"phaseLabel": "阶段标签",
"enterPhaseName": "输入阶段标签名称",
"addOption": "添加选项",
"phaseOptions": "阶段选项:"
}

View 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": "无权限"
}

View File

@@ -0,0 +1,14 @@
{
"nameColumn": "名称",
"attachedTaskColumn": "附加任务",
"sizeColumn": "大小",
"uploadedByColumn": "上传者",
"uploadedAtColumn": "上传时间",
"fileIconAlt": "文件图标",
"titleDescriptionText": "此项目中任务的所有附件将显示在这里。",
"deleteConfirmationTitle": "您确定吗?",
"deleteConfirmationOk": "是",
"deleteConfirmationCancel": "取消",
"segmentedTooltip": "即将推出!在列表视图和缩略图视图之间切换。",
"emptyText": "项目中没有附件。"
}

View File

@@ -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": "导出"
}
}

View File

@@ -0,0 +1,17 @@
{
"nameColumn": "名称",
"jobTitleColumn": "职位",
"emailColumn": "电子邮件",
"tasksColumn": "任务",
"taskProgressColumn": "任务进度",
"accessColumn": "访问权限",
"fileIconAlt": "文件图标",
"deleteConfirmationTitle": "您确定吗?",
"deleteConfirmationOk": "是",
"deleteConfirmationCancel": "取消",
"refreshButtonTooltip": "刷新成员",
"deleteButtonTooltip": "从项目中移除",
"memberCount": "成员",
"membersCountPlural": "成员",
"emptyText": "项目中没有附件。"
}

View File

@@ -0,0 +1,6 @@
{
"inputPlaceholder": "添加评论",
"addButton": "添加",
"cancelButton": "取消",
"deleteButton": "删除"
}

View File

@@ -0,0 +1,14 @@
{
"taskList": "任务列表",
"board": "看板",
"insights": "数据洞察",
"files": "文件",
"members": "成员",
"updates": "动态更新",
"projectView": "项目视图",
"loading": "正在加载项目...",
"error": "加载项目时出错",
"pinnedTab": "已固定为默认标签页",
"pinTab": "固定为默认标签页",
"unpinTab": "取消固定默认标签页"
}

View File

@@ -0,0 +1,11 @@
{
"importTaskTemplate": "导入任务模板",
"templateName": "模板名称",
"templateDescription": "模板描述",
"selectedTasks": "已选任务",
"tasks": "任务",
"templates": "模板",
"remove": "移除",
"cancel": "取消",
"import": "导入"
}

View File

@@ -0,0 +1,7 @@
{
"title": "项目成员",
"searchLabel": "通过添加名称或电子邮件添加成员",
"searchPlaceholder": "输入名称或电子邮件",
"inviteAsAMember": "邀请为成员",
"inviteNewMemberByEmail": "通过电子邮件邀请新成员"
}

View File

@@ -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": "项目类别"
}

View File

@@ -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": "输入模板名称"
}

View File

@@ -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": "良好"
}

View 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": "已完成"
}

View File

@@ -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": "进行中任务"
}

View 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": "成员"
}

View File

@@ -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": "阶段"
}

View File

@@ -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": "团队"
}

View 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": "今天"
}

View File

@@ -0,0 +1,8 @@
{
"overview": "概览",
"projects": "项目",
"members": "成员",
"timeReports": "用时报告",
"estimateVsActual": "预计用时 vs 实际用时",
"currentOrganizationTooltip": "当前的组织"
}

View 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": "已记录不可计费"
}

View File

@@ -0,0 +1,10 @@
{
"categoryColumn": "类别",
"deleteConfirmationTitle": "您确定吗?",
"deleteConfirmationOk": "是",
"deleteConfirmationCancel": "取消",
"associatedTaskColumn": "关联项目",
"searchPlaceholder": "按名称搜索",
"emptyText": "在更新或创建项目时可以创建类别。",
"colorChangeTooltip": "点击更改颜色"
}

View File

@@ -0,0 +1,15 @@
{
"title": "更改密码",
"currentPassword": "当前密码",
"newPassword": "新密码",
"confirmPassword": "确认密码",
"currentPasswordPlaceholder": "输入您的当前密码",
"newPasswordPlaceholder": "新密码",
"confirmPasswordPlaceholder": "确认密码",
"currentPasswordRequired": "请输入您的当前密码!",
"newPasswordRequired": "请输入您的新密码!",
"passwordValidationError": "密码必须至少包含8个字符包括一个大写字母、一个数字和一个符号。",
"passwordMismatch": "密码不匹配!",
"passwordRequirements": "新密码应至少包含8个字符包括一个大写字母、一个数字和一个符号。",
"updateButton": "更新密码"
}

View 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": "客户更新失败!"
}

View File

@@ -0,0 +1,20 @@
{
"nameColumn": "名称",
"deleteConfirmationTitle": "您确定吗?",
"deleteConfirmationOk": "是",
"deleteConfirmationCancel": "取消",
"searchPlaceholder": "按名称搜索",
"createJobTitleButton": "创建职位",
"pinTooltip": "点击将其固定到主菜单",
"createJobTitleDrawerTitle": "创建职位",
"updateJobTitleDrawerTitle": "更新职位",
"nameLabel": "名称",
"namePlaceholder": "名称",
"nameRequiredError": "请输入名称",
"createButton": "创建",
"updateButton": "更新",
"createJobTitleSuccessMessage": "职位创建成功!",
"createJobTitleErrorMessage": "职位创建失败!",
"updateJobTitleSuccessMessage": "职位更新成功!",
"updateJobTitleErrorMessage": "职位更新失败!"
}

View File

@@ -0,0 +1,11 @@
{
"labelColumn": "标签",
"deleteConfirmationTitle": "您确定吗?",
"deleteConfirmationOk": "是",
"deleteConfirmationCancel": "取消",
"associatedTaskColumn": "关联任务计数",
"searchPlaceholder": "按名称搜索",
"emptyText": "标签可以在更新或创建任务时创建。",
"pinTooltip": "点击将其固定到主菜单",
"colorChangeTooltip": "点击更改颜色"
}

View File

@@ -0,0 +1,7 @@
{
"language": "语言",
"language_required": "语言是必需的",
"time_zone": "时区",
"time_zone_required": "时区是必需的",
"save_changes": "保存更改"
}

View File

@@ -0,0 +1,11 @@
{
"title": "通知设置",
"emailTitle": "向我发送电子邮件通知",
"emailDescription": "包括新的任务分配",
"dailyDigestTitle": "向我发送每日摘要",
"dailyDigestDescription": "每天晚上,您将收到任务中最近活动的摘要。",
"popupTitle": "当Worklenz打开时在我的电脑上弹出通知",
"popupDescription": "弹出通知可能会被您的浏览器禁用。更改您的浏览器设置以允许它们。",
"unreadItemsTitle": "显示未读项目的数量",
"unreadItemsDescription": "您将看到每个通知的计数。"
}

View File

@@ -0,0 +1,14 @@
{
"uploadError": "您只能上传JPG/PNG文件",
"uploadSizeError": "图片必须小于2MB",
"upload": "上传",
"nameLabel": "名称",
"nameRequiredError": "名称是必需的",
"emailLabel": "电子邮件",
"emailRequiredError": "电子邮件是必需的",
"saveChanges": "保存更改",
"profileJoinedText": "一个月前加入",
"profileLastUpdatedText": "一个月前更新",
"avatarTooltip": "点击上传头像",
"title": "个人资料设置"
}

View File

@@ -0,0 +1,8 @@
{
"nameColumn": "名称",
"editToolTip": "编辑",
"deleteToolTip": "删除",
"confirmText": "您确定吗?",
"okText": "是",
"cancelText": "取消"
}

View 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": "语言和地区"
}

View File

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