Fix bug 722 (#737)

* Fix project statuses

* Refactor project states

* Add translations
This commit is contained in:
Chris 2025-12-28 07:51:15 +02:00 committed by GitHub
parent e73c354e7e
commit eee1bbc013
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
51 changed files with 947 additions and 302 deletions

View file

@ -133,10 +133,17 @@ const options = {
type: 'string',
description: 'Project description',
},
state: {
status: {
type: 'string',
enum: ['active', 'archived', 'completed'],
description: 'Project state',
enum: [
'not_started',
'planned',
'in_progress',
'waiting',
'done',
'cancelled',
],
description: 'Project status',
},
priority: {
type: 'string',

View file

@ -9,11 +9,11 @@
* - BearerAuth: []
* parameters:
* - in: query
* name: state
* name: status
* schema:
* type: string
* enum: [planned, in_progress, blocked, completed, archived, all]
* description: Filter by project state
* enum: [not_started, planned, in_progress, waiting, done, cancelled, all, not_completed]
* description: Filter by project status
* - in: query
* name: area_id
* schema:
@ -62,10 +62,10 @@
* type: string
* enum: [low, medium, high]
* description: Project priority
* state:
* status:
* type: string
* enum: [idea, planned, in_progress, blocked, completed, archived]
* description: Project state
* enum: [not_started, planned, in_progress, waiting, done, cancelled]
* description: Project status
* area_id:
* type: integer
* description: Associated area ID
@ -127,10 +127,10 @@
* type: string
* enum: [low, medium, high]
* description: Project priority
* state:
* status:
* type: string
* enum: [idea, planned, in_progress, blocked, completed, archived]
* description: Project state
* enum: [not_started, planned, in_progress, waiting, done, cancelled]
* description: Project status
* area_id:
* type: integer
* description: Associated area ID

View file

@ -0,0 +1,182 @@
'use strict';
/**
* Migration to update project state ENUM to use task status values.
* This aligns project states with task statuses for consistency.
*
* Old values: 'idea', 'planned', 'in_progress', 'blocked', 'completed'
* New values: 'not_started', 'in_progress', 'done', 'waiting', 'cancelled', 'planned'
*
* Mapping:
* - 'idea' 'not_started'
* - 'planned' 'planned'
* - 'in_progress' 'in_progress'
* - 'blocked' 'waiting'
* - 'completed' 'done'
*/
module.exports = {
async up(queryInterface, Sequelize) {
// For SQLite: recreate the column with new values
// For PostgreSQL/MySQL: alter the enum type
const dialect = queryInterface.sequelize.getDialect();
if (dialect === 'sqlite') {
// SQLite doesn't support ENUM, it's stored as TEXT
// Just update the data values
await queryInterface.sequelize.query(
`UPDATE projects SET state = 'not_started' WHERE state = 'idea'`
);
await queryInterface.sequelize.query(
`UPDATE projects SET state = 'waiting' WHERE state = 'blocked'`
);
await queryInterface.sequelize.query(
`UPDATE projects SET state = 'done' WHERE state = 'completed'`
);
} else if (dialect === 'postgres') {
// PostgreSQL: Create new enum type, migrate data, swap types
await queryInterface.sequelize.query(`
CREATE TYPE "enum_projects_state_new" AS ENUM(
'not_started', 'in_progress', 'done', 'waiting', 'cancelled', 'planned'
);
`);
// Update values before changing type
await queryInterface.sequelize.query(
`UPDATE projects SET state = 'not_started' WHERE state = 'idea'`
);
await queryInterface.sequelize.query(
`UPDATE projects SET state = 'waiting' WHERE state = 'blocked'`
);
await queryInterface.sequelize.query(
`UPDATE projects SET state = 'done' WHERE state = 'completed'`
);
// Change column type
await queryInterface.sequelize.query(`
ALTER TABLE projects
ALTER COLUMN state TYPE "enum_projects_state_new"
USING state::text::"enum_projects_state_new";
`);
// Drop old enum and rename new one
await queryInterface.sequelize.query(
`DROP TYPE IF EXISTS "enum_projects_state";`
);
await queryInterface.sequelize.query(
`ALTER TYPE "enum_projects_state_new" RENAME TO "enum_projects_state";`
);
// Update default value
await queryInterface.sequelize.query(`
ALTER TABLE projects ALTER COLUMN state SET DEFAULT 'not_started';
`);
} else if (dialect === 'mysql' || dialect === 'mariadb') {
// MySQL: Alter column directly
await queryInterface.sequelize.query(
`UPDATE projects SET state = 'not_started' WHERE state = 'idea'`
);
await queryInterface.sequelize.query(
`UPDATE projects SET state = 'waiting' WHERE state = 'blocked'`
);
await queryInterface.sequelize.query(
`UPDATE projects SET state = 'done' WHERE state = 'completed'`
);
await queryInterface.changeColumn('projects', 'state', {
type: Sequelize.ENUM(
'not_started',
'in_progress',
'done',
'waiting',
'cancelled',
'planned'
),
allowNull: false,
defaultValue: 'not_started',
});
}
},
async down(queryInterface, Sequelize) {
const dialect = queryInterface.sequelize.getDialect();
if (dialect === 'sqlite') {
// Reverse the data mapping
await queryInterface.sequelize.query(
`UPDATE projects SET state = 'idea' WHERE state = 'not_started'`
);
await queryInterface.sequelize.query(
`UPDATE projects SET state = 'blocked' WHERE state = 'waiting'`
);
await queryInterface.sequelize.query(
`UPDATE projects SET state = 'completed' WHERE state = 'done'`
);
await queryInterface.sequelize.query(
`UPDATE projects SET state = 'idea' WHERE state = 'cancelled'`
);
} else if (dialect === 'postgres') {
await queryInterface.sequelize.query(`
CREATE TYPE "enum_projects_state_old" AS ENUM(
'idea', 'planned', 'in_progress', 'blocked', 'completed'
);
`);
// Reverse data mapping
await queryInterface.sequelize.query(
`UPDATE projects SET state = 'idea' WHERE state = 'not_started'`
);
await queryInterface.sequelize.query(
`UPDATE projects SET state = 'blocked' WHERE state = 'waiting'`
);
await queryInterface.sequelize.query(
`UPDATE projects SET state = 'completed' WHERE state = 'done'`
);
await queryInterface.sequelize.query(
`UPDATE projects SET state = 'idea' WHERE state = 'cancelled'`
);
await queryInterface.sequelize.query(`
ALTER TABLE projects
ALTER COLUMN state TYPE "enum_projects_state_old"
USING state::text::"enum_projects_state_old";
`);
await queryInterface.sequelize.query(
`DROP TYPE IF EXISTS "enum_projects_state";`
);
await queryInterface.sequelize.query(
`ALTER TYPE "enum_projects_state_old" RENAME TO "enum_projects_state";`
);
await queryInterface.sequelize.query(`
ALTER TABLE projects ALTER COLUMN state SET DEFAULT 'idea';
`);
} else if (dialect === 'mysql' || dialect === 'mariadb') {
await queryInterface.sequelize.query(
`UPDATE projects SET state = 'idea' WHERE state = 'not_started'`
);
await queryInterface.sequelize.query(
`UPDATE projects SET state = 'blocked' WHERE state = 'waiting'`
);
await queryInterface.sequelize.query(
`UPDATE projects SET state = 'completed' WHERE state = 'done'`
);
await queryInterface.sequelize.query(
`UPDATE projects SET state = 'idea' WHERE state = 'cancelled'`
);
await queryInterface.changeColumn('projects', 'state', {
type: Sequelize.ENUM(
'idea',
'planned',
'in_progress',
'blocked',
'completed'
),
allowNull: false,
defaultValue: 'idea',
});
}
},
};

View file

@ -0,0 +1,14 @@
'use strict';
/**
* Migration to rename project 'state' column to 'status' for consistency.
*/
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.renameColumn('projects', 'state', 'status');
},
async down(queryInterface, Sequelize) {
await queryInterface.renameColumn('projects', 'status', 'state');
},
};

View file

@ -71,16 +71,17 @@ module.exports = (sequelize) => {
allowNull: true,
defaultValue: 'created_at:desc',
},
state: {
status: {
type: DataTypes.ENUM(
'idea',
'planned',
'not_started',
'in_progress',
'blocked',
'completed'
'done',
'waiting',
'cancelled',
'planned'
),
allowNull: false,
defaultValue: 'idea',
defaultValue: 'not_started',
},
},
{

View file

@ -158,7 +158,10 @@ router.post(
router.get('/projects', async (req, res) => {
try {
const { state, active, pin_to_sidebar, area_id, area } = req.query;
const { status, state, active, pin_to_sidebar, area_id, area } =
req.query;
// Support both 'status' (new) and 'state' (legacy) query params
const statusFilter = status || state;
// Base: owned or shared projects
const ownedOrShared =
@ -168,22 +171,22 @@ router.get('/projects', async (req, res) => {
);
let whereClause = ownedOrShared;
// Filter by state (new primary filter)
if (state && state !== 'all') {
if (Array.isArray(state)) {
whereClause.state = { [Op.in]: state };
// Filter by status
if (statusFilter && statusFilter !== 'all') {
if (Array.isArray(statusFilter)) {
whereClause.status = { [Op.in]: statusFilter };
} else {
whereClause.state = state;
whereClause.status = statusFilter;
}
}
// Legacy support for active filter - map to states
// Legacy support for active filter - map to statuses
if (active === 'true') {
whereClause.state = {
[Op.in]: ['planned', 'in_progress', 'blocked'],
whereClause.status = {
[Op.in]: ['planned', 'in_progress', 'waiting'],
};
} else if (active === 'false') {
whereClause.state = { [Op.in]: ['idea', 'completed'] };
whereClause.status = { [Op.in]: ['not_started', 'done'] };
}
// Filter by pinned status
@ -490,7 +493,8 @@ router.post('/project', async (req, res) => {
priority,
due_date_at,
image_url,
state,
status,
state, // Legacy support
tags,
Tags,
} = req.body;
@ -514,7 +518,7 @@ router.post('/project', async (req, res) => {
priority: priority || null,
due_date_at: due_date_at || null,
image_url: image_url || null,
state: state || 'idea',
status: status || state || 'not_started',
user_id: req.authUserId,
};
@ -578,7 +582,8 @@ router.patch(
priority,
due_date_at,
image_url,
state,
status,
state, // Legacy support
tags,
Tags,
} = req.body;
@ -595,7 +600,9 @@ router.patch(
if (priority !== undefined) updateData.priority = priority;
if (due_date_at !== undefined) updateData.due_date_at = due_date_at;
if (image_url !== undefined) updateData.image_url = image_url;
if (state !== undefined) updateData.state = state;
// Support both status (new) and state (legacy)
if (status !== undefined) updateData.status = status;
else if (state !== undefined) updateData.status = state;
await project.update(updateData);
await updateProjectTags(project, tagsData, req.authUserId);

View file

@ -432,7 +432,7 @@ router.get('/', async (req, res) => {
name: project.name,
description: project.description,
priority: project.priority,
status: project.state,
status: project.status,
}))
);
}

View file

@ -83,7 +83,7 @@ async function filterTasksByParams(
},
{
model: Project,
attributes: ['id', 'name', 'state', 'uid'],
attributes: ['id', 'name', 'status', 'uid'],
required: false,
},
{
@ -373,7 +373,7 @@ function getTaskIncludeConfig() {
},
{
model: Project,
attributes: ['id', 'name', 'state', 'uid'],
attributes: ['id', 'name', 'status', 'uid'],
required: false,
},
{

View file

@ -364,7 +364,7 @@ async function importUserData(userId, backupData, options = { merge: true }) {
task_show_completed:
projectData.task_show_completed,
task_sort_order: projectData.task_sort_order,
state: projectData.state,
status: projectData.status || projectData.state,
user_id: userId,
area_id: areaId,
},

View file

@ -29,7 +29,7 @@ describe('Projects Routes', () => {
const projectData = {
name: 'Test Project',
description: 'Test Description',
state: 'planned',
status: 'planned',
pin_to_sidebar: false,
priority: 1,
area_id: area.id,
@ -40,7 +40,7 @@ describe('Projects Routes', () => {
expect(response.status).toBe(201);
expect(response.body.name).toBe(projectData.name);
expect(response.body.description).toBe(projectData.description);
expect(response.body.state).toBe(projectData.state);
expect(response.body.status).toBe(projectData.status);
expect(response.body.pin_to_sidebar).toBe(
projectData.pin_to_sidebar
);
@ -217,7 +217,7 @@ describe('Projects Routes', () => {
project = await Project.create({
name: 'Test Project',
description: 'Test Description',
state: 'idea',
status: 'not_started',
priority: 0,
user_id: user.id,
});
@ -227,7 +227,7 @@ describe('Projects Routes', () => {
const updateData = {
name: 'Updated Project',
description: 'Updated Description',
state: 'in_progress',
status: 'in_progress',
priority: 2,
};
@ -238,7 +238,7 @@ describe('Projects Routes', () => {
expect(response.status).toBe(200);
expect(response.body.name).toBe(updateData.name);
expect(response.body.description).toBe(updateData.description);
expect(response.body.state).toBe(updateData.state);
expect(response.body.status).toBe(updateData.status);
expect(response.body.priority).toBe(updateData.priority);
});

View file

@ -961,7 +961,7 @@ describe('Universal Search Routes', () => {
expect(project.name).toBe('Test project');
expect(project.description).toBe('Project description');
expect(project.priority).toBe('medium');
expect(project.status).toBe('active');
expect(project.status).toBe('not_started');
});
it('should return note with correct structure', async () => {

View file

@ -21,7 +21,7 @@ describe('Project Model', () => {
const projectData = {
name: 'Test Project',
description: 'Test Description',
state: 'planned',
status: 'planned',
pin_to_sidebar: false,
priority: 1,
user_id: user.id,
@ -32,7 +32,7 @@ describe('Project Model', () => {
expect(project.name).toBe(projectData.name);
expect(project.description).toBe(projectData.description);
expect(project.state).toBe(projectData.state);
expect(project.status).toBe(projectData.status);
expect(project.pin_to_sidebar).toBe(projectData.pin_to_sidebar);
expect(project.priority).toBe(projectData.priority);
expect(project.user_id).toBe(user.id);
@ -85,7 +85,7 @@ describe('Project Model', () => {
user_id: user.id,
});
expect(project.state).toBe('idea');
expect(project.status).toBe('not_started');
expect(project.pin_to_sidebar).toBe(false);
expect(project.task_show_completed).toBe(false);
expect(project.task_sort_order).toBe('created_at:desc');

View file

@ -219,7 +219,7 @@ const Layout: React.FC<LayoutProps> = ({
try {
const newProject = await createProject({
name,
state: 'planned',
status: 'planned',
});
return newProject;
} catch (error) {

View file

@ -418,7 +418,7 @@ const InboxItemDetail: React.FC<InboxItemDetailProps> = ({
const newProject: Project = {
name: payload.cleanedContent || displayText,
description: '',
state: 'planned',
status: 'planned',
tags: payload.tagObjects,
};

View file

@ -374,7 +374,7 @@ const InboxItems: React.FC = () => {
const handleCreateProject = async (name: string): Promise<Project> => {
try {
const project = await createProject({ name, state: 'planned' });
const project = await createProject({ name, status: 'planned' });
showSuccessToast(t('project.createSuccess'));
return project;
} catch (error) {

View file

@ -1065,7 +1065,7 @@ const QuickCaptureInput = React.forwardRef<
try {
await createProject({
name: projectName,
state: 'planned',
status: 'planned',
});
} catch (error) {
console.error(
@ -1411,7 +1411,7 @@ const QuickCaptureInput = React.forwardRef<
const newProject: Project = {
name: cleaned,
description: '',
state: 'planned',
status: 'planned',
tags: buildTagObjects(
composerFooterContext.hashtags
),

View file

@ -72,8 +72,8 @@ const ProductivityAssistant: React.FC<ProductivityAssistantProps> = ({
// 1. Stalled Projects (no tasks/actions)
const stalledProjects = projects.filter(
(project) =>
(project.state === 'planned' ||
project.state === 'in_progress') &&
(project.status === 'planned' ||
project.status === 'in_progress') &&
!activeTasks.some((task) => task.project_id === project.id)
);
@ -110,8 +110,8 @@ const ProductivityAssistant: React.FC<ProductivityAssistantProps> = ({
task.status === 'in_progress')
);
return (
(project.state === 'planned' ||
project.state === 'in_progress') &&
(project.status === 'planned' ||
project.status === 'in_progress') &&
hasCompletedTasks &&
!hasNextAction
);
@ -214,8 +214,8 @@ const ProductivityAssistant: React.FC<ProductivityAssistantProps> = ({
const stuckProjects = projects.filter((project) => {
if (
!(
project.state === 'planned' ||
project.state === 'in_progress'
project.status === 'planned' ||
project.status === 'in_progress'
)
)
return false;

View file

@ -22,7 +22,7 @@ interface ProjectBannerProps {
project: Project;
areas: Area[];
t: TFunction;
getStateIcon: (state: string) => React.ReactNode;
getStatusIcon: (status: string) => React.ReactNode;
onDeleteClick: () => void;
editButtonRef: RefObject<HTMLButtonElement>;
onEditBannerClick?: () => void;
@ -32,7 +32,7 @@ const ProjectBanner: React.FC<ProjectBannerProps> = ({
project,
areas,
t,
getStateIcon,
getStatusIcon,
onDeleteClick,
editButtonRef,
onEditBannerClick,
@ -76,11 +76,11 @@ const ProjectBanner: React.FC<ProjectBannerProps> = ({
</div>
<div className="absolute bottom-2 left-2 right-14 flex items-center flex-wrap gap-2">
{project.state && (
{project.status && (
<BannerBadge>
{getStateIcon(project.state)}
{getStatusIcon(project.status)}
<span className="text-xs text-white/90 font-medium">
{t(`projects.states.${project.state}`)}
{t(`projectStatus.${project.status}`)}
</span>
</BannerBadge>
)}

View file

@ -3,11 +3,12 @@ import { useParams, useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import {
MagnifyingGlassIcon,
LightBulbIcon,
EllipsisHorizontalCircleIcon,
ClipboardDocumentListIcon,
PlayIcon,
ExclamationTriangleIcon,
ClockIcon,
CheckCircleIcon,
XCircleIcon,
ChartBarIcon,
CheckIcon,
} from '@heroicons/react/24/outline';
@ -700,32 +701,35 @@ const ProjectDetails: React.FC = () => {
monthlyCompleted,
} = useProjectMetrics(tasks, handleTaskUpdate, t, showSuccessToast);
const getStateIcon = (state: string) => {
switch (state) {
case 'idea':
const getStatusIcon = (status: string) => {
switch (status) {
case 'not_started':
return (
<LightBulbIcon className="h-3 w-3 text-yellow-500 flex-shrink-0 mt-0.5" />
<EllipsisHorizontalCircleIcon className="h-3 w-3 text-gray-500 flex-shrink-0 mt-0.5" />
);
case 'planned':
return (
<ClipboardDocumentListIcon className="h-3 w-3 text-blue-500 flex-shrink-0 mt-0.5" />
);
case 'in_progress':
case 'active':
return (
<PlayIcon className="h-3 w-3 text-green-500 flex-shrink-0 mt-0.5" />
);
case 'blocked':
case 'waiting':
return (
<ExclamationTriangleIcon className="h-3 w-3 text-red-500 flex-shrink-0 mt-0.5" />
<ClockIcon className="h-3 w-3 text-yellow-500 flex-shrink-0 mt-0.5" />
);
case 'completed':
case 'done':
return (
<CheckCircleIcon className="h-3 w-3 text-gray-500 flex-shrink-0 mt-0.5" />
<CheckCircleIcon className="h-3 w-3 text-green-600 flex-shrink-0 mt-0.5" />
);
case 'cancelled':
return (
<XCircleIcon className="h-3 w-3 text-red-500 flex-shrink-0 mt-0.5" />
);
default:
return (
<PlayIcon className="h-3 w-3 text-white/70 flex-shrink-0 mt-0.5" />
<EllipsisHorizontalCircleIcon className="h-3 w-3 text-white/70 flex-shrink-0 mt-0.5" />
);
}
};
@ -834,7 +838,7 @@ const ProjectDetails: React.FC = () => {
project={project}
areas={areas}
t={t}
getStateIcon={getStateIcon}
getStatusIcon={getStatusIcon}
onDeleteClick={() => {
setNoteToDelete(null);
setIsConfirmDialogOpen(true);

View file

@ -4,15 +4,16 @@ import { EllipsisVerticalIcon } from '@heroicons/react/24/solid';
import {
PencilSquareIcon,
TrashIcon,
LightBulbIcon,
DocumentTextIcon,
EllipsisHorizontalCircleIcon,
ClipboardDocumentListIcon,
PlayIcon,
StopIcon,
ClockIcon,
CheckCircleIcon,
XCircleIcon,
ShareIcon,
ExclamationTriangleIcon,
} from '@heroicons/react/24/outline';
import { Project, ProjectState } from '../../entities/Project';
import { Project, ProjectStatus } from '../../entities/Project';
import { useTranslation } from 'react-i18next';
import { useToast } from '../Shared/ToastContext';
import { getCurrentUser } from '../../utils/userUtils';
@ -48,38 +49,41 @@ const getProjectInitials = (name: string, maxLetters?: number) => {
return maxLetters ? initials.substring(0, maxLetters) : initials;
};
const getStateIcon = (state: ProjectState | undefined) => {
switch (state) {
case 'idea':
return { icon: LightBulbIcon };
const getStatusIcon = (status: ProjectStatus | undefined) => {
switch (status) {
case 'not_started':
return { icon: EllipsisHorizontalCircleIcon };
case 'planned':
return { icon: DocumentTextIcon };
return { icon: ClipboardDocumentListIcon };
case 'in_progress':
return { icon: PlayIcon };
case 'blocked':
return { icon: StopIcon };
case 'completed':
case 'waiting':
return { icon: ClockIcon };
case 'done':
return { icon: CheckCircleIcon };
case 'cancelled':
return { icon: XCircleIcon };
default:
return { icon: LightBulbIcon };
return { icon: EllipsisHorizontalCircleIcon };
}
};
const getStateLabel = (state: ProjectState | undefined, t: any): string => {
switch (state) {
case 'idea':
return t('projects.states.idea', 'Idea');
const getStatusLabel = (status: ProjectStatus | undefined, t: any): string => {
switch (status) {
case 'not_started':
return t('projectStatus.not_started', 'Not Started');
case 'planned':
return t('projects.states.planned', 'Planned');
return t('projectStatus.planned', 'Planned');
case 'in_progress':
case 'active':
return t('projects.states.in_progress', 'In Progress');
case 'blocked':
return t('projects.states.blocked', 'Blocked');
case 'completed':
return t('projects.states.completed', 'Completed');
return t('projectStatus.in_progress', 'In Progress');
case 'waiting':
return t('projectStatus.waiting', 'Waiting');
case 'done':
return t('projectStatus.done', 'Completed');
case 'cancelled':
return t('projectStatus.cancelled', 'Cancelled');
default:
return t('projects.states.idea', 'Idea');
return t('projectStatus.not_started', 'Not Started');
}
};
@ -299,14 +303,14 @@ const ProjectItem: React.FC<ProjectItemProps> = ({
/>
)}
{(() => {
const { icon: StateIcon } = getStateIcon(
project.state
const { icon: StatusIcon } = getStatusIcon(
project.status
);
return (
<StateIcon
<StatusIcon
className="h-4 w-4 text-white/80 drop-shadow-sm"
title={getStateLabel(
project.state,
title={getStatusLabel(
project.status,
t
)}
/>

View file

@ -45,7 +45,7 @@ const ProjectModal: React.FC<ProjectModalProps> = ({
name: '',
description: '',
area_id: null,
state: 'idea',
status: 'not_started',
tags: [],
priority: null,
due_date_at: null,
@ -71,7 +71,7 @@ const ProjectModal: React.FC<ProjectModalProps> = ({
// Collapsible sections state
const [expandedSections, setExpandedSections] = useState({
state: false,
status: false,
tags: false,
area: false,
priority: false,
@ -128,7 +128,7 @@ const ProjectModal: React.FC<ProjectModalProps> = ({
name: '',
description: '',
area_id: null,
state: 'idea',
status: 'not_started',
tags: [],
priority: null,
due_date_at: null,
@ -328,7 +328,7 @@ const ProjectModal: React.FC<ProjectModalProps> = ({
formData.name.trim() !== '' ||
formData.description?.trim() !== '' ||
formData.area_id !== null ||
formData.state !== 'idea' ||
formData.status !== 'not_started' ||
tags.length > 0 ||
formData.priority !== null ||
formData.due_date_at !== null
@ -340,7 +340,7 @@ const ProjectModal: React.FC<ProjectModalProps> = ({
formData.name !== project.name ||
formData.description !== project.description ||
formData.area_id !== project.area_id ||
formData.state !== project.state ||
formData.status !== project.status ||
formData.priority !== project.priority ||
formData.due_date_at !== project.due_date_at;
@ -511,25 +511,25 @@ const ProjectModal: React.FC<ProjectModalProps> = ({
</div>
{/* Expandable Sections - Only show when expanded */}
{/* State Section - First */}
{expandedSections.state && (
{/* Status Section - First */}
{expandedSections.status && (
<div className="border-b border-gray-200 dark:border-gray-700 pb-4 mb-4 px-4">
<h3 className="text-sm font-medium text-gray-700 dark:text-gray-300 mb-3">
{t(
'projects.state',
'Project State'
'projects.status',
'Project Status'
)}
</h3>
<ProjectStateDropdown
value={
formData.state ||
'idea'
formData.status ||
'not_started'
}
onChange={(state) =>
onChange={(status) =>
setFormData(
(prev) => ({
...prev,
state,
status,
})
)
}
@ -646,25 +646,26 @@ const ProjectModal: React.FC<ProjectModalProps> = ({
<div className="flex items-center justify-between">
{/* Left side: Section icons */}
<div className="flex items-center space-x-1">
{/* State Toggle - First */}
{/* Status Toggle - First */}
<button
type="button"
onClick={() =>
toggleSection('state')
toggleSection('status')
}
className={`relative p-2 rounded-full transition-colors ${
expandedSections.state
expandedSections.status
? 'bg-blue-100 dark:bg-blue-900 text-blue-600 dark:text-blue-400'
: 'text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700'
}`}
title={t(
'projects.state',
'Project State'
'projects.status',
'Project Status'
)}
>
<PlayIcon className="h-5 w-5" />
{formData.state &&
formData.state !== 'idea' && (
{formData.status &&
formData.status !==
'not_started' && (
<span className="absolute -top-1 -right-1 w-3 h-3 bg-green-500 rounded-full"></span>
)}
</button>

View file

@ -69,7 +69,7 @@ const Projects: React.FC = () => {
const [orderBy, setOrderBy] = useState<string>('created_at:desc');
const [searchParams, setSearchParams] = useSearchParams();
const stateFilter = searchParams.get('state') || 'all';
const statusFilter = searchParams.get('status') || 'not_completed';
// Get area UID from URL parameters
const getAreaUidFromParams = () => {
@ -94,16 +94,25 @@ const Projects: React.FC = () => {
// Filter options for dropdowns
const statusOptions: FilterOption[] = [
{ value: 'all', label: t('projects.filters.all') },
{ value: 'idea', label: t('projects.states.idea', 'Idea') },
{ value: 'planned', label: t('projects.states.planned', 'Planned') },
{
value: 'not_started',
label: t('projectStatus.not_started', 'Not Started'),
},
{ value: 'planned', label: t('projectStatus.planned', 'Planned') },
{
value: 'in_progress',
label: t('projects.states.in_progress', 'In Progress'),
label: t('projectStatus.in_progress', 'In Progress'),
},
{ value: 'blocked', label: t('projects.states.blocked', 'Blocked') },
{ value: 'waiting', label: t('projectStatus.waiting', 'Waiting') },
{ value: 'done', label: t('projectStatus.done', 'Completed') },
{
value: 'completed',
label: t('projects.states.completed', 'Completed'),
value: 'cancelled',
label: t('projectStatus.cancelled', 'Cancelled'),
},
{ value: 'divider', label: '' },
{
value: 'not_completed',
label: t('projects.filters.notCompleted', 'Not Completed'),
},
];
@ -241,13 +250,13 @@ const Projects: React.FC = () => {
return (project as any).completion_percentage || 0;
};
const handleStateFilterChange = (value: string) => {
const handleStatusFilterChange = (value: string) => {
const params = new URLSearchParams(searchParams);
if (value === 'all') {
params.delete('state');
if (value === 'not_completed') {
params.delete('status');
} else {
params.set('state', value);
params.set('status', value);
}
setSearchParams(params);
};
@ -273,10 +282,15 @@ const Projects: React.FC = () => {
const displayProjects = useMemo(() => {
let filteredProjects = [...projects];
// Apply state filter
if (stateFilter !== 'all') {
// Apply status filter
if (statusFilter === 'not_completed') {
filteredProjects = filteredProjects.filter(
(project) => project.state === stateFilter
(project) =>
project.status !== 'done' && project.status !== 'cancelled'
);
} else if (statusFilter !== 'all') {
filteredProjects = filteredProjects.filter(
(project) => project.status === statusFilter
);
}
@ -347,7 +361,7 @@ const Projects: React.FC = () => {
});
return filteredProjects;
}, [projects, stateFilter, actualAreaFilter, searchQuery, orderBy]);
}, [projects, statusFilter, actualAreaFilter, searchQuery, orderBy]);
if (isLoading) {
return (
@ -426,8 +440,8 @@ const Projects: React.FC = () => {
<div className="w-full md:w-auto mb-4 md:mb-0">
<FilterDropdown
options={statusOptions}
value={stateFilter}
onChange={handleStateFilterChange}
value={statusFilter}
onChange={handleStatusFilterChange}
size="desktop"
autoWidth={true}
/>

View file

@ -103,26 +103,33 @@ const FilterDropdown: React.FC<FilterDropdownProps> = ({
style={autoWidth ? dynamicStyles : {}}
>
<div className="p-1">
{options.map((option) => (
<button
key={option.value}
onClick={(e) => {
e.stopPropagation();
onChange(option.value);
setIsOpen(false);
}}
className={`block ${isMobile ? 'px-4 py-2 text-xs' : 'px-4 py-2 text-sm'} text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-600 w-full text-left`}
>
<span className="flex items-center justify-between">
<span>{option.label}</span>
{value === option.value && (
<CheckIcon
className={`${iconSize} ml-2`}
/>
)}
</span>
</button>
))}
{options.map((option, index) =>
option.value === 'divider' ? (
<div
key={`divider-${index}`}
className="my-1 -mx-1 border-t border-gray-100 dark:border-gray-700"
/>
) : (
<button
key={option.value}
onClick={(e) => {
e.stopPropagation();
onChange(option.value);
setIsOpen(false);
}}
className={`block ${isMobile ? 'px-4 py-2 text-xs' : 'px-4 py-2 text-sm'} text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-600 w-full text-left`}
>
<span className="flex items-center justify-between">
<span>{option.label}</span>
{value === option.value && (
<CheckIcon
className={`${iconSize} ml-2`}
/>
)}
</span>
</button>
)
)}
</div>
</div>
)}

View file

@ -1,18 +1,19 @@
import React, { useState, useRef, useEffect } from 'react';
import {
ChevronDownIcon,
LightBulbIcon,
ClipboardDocumentListIcon,
PlayIcon,
ExclamationTriangleIcon,
CheckCircleIcon,
ClockIcon,
XCircleIcon,
EllipsisHorizontalCircleIcon,
} from '@heroicons/react/24/outline';
import { ProjectState } from '../../entities/Project';
import { ProjectStatus } from '../../entities/Project';
import { useTranslation } from 'react-i18next';
interface ProjectStateDropdownProps {
value: ProjectState;
onChange: (value: ProjectState) => void;
value: ProjectStatus;
onChange: (value: ProjectStatus) => void;
}
const ProjectStateDropdown: React.FC<ProjectStateDropdownProps> = ({
@ -23,19 +24,21 @@ const ProjectStateDropdown: React.FC<ProjectStateDropdownProps> = ({
const states = [
{
value: 'idea' as ProjectState,
label: t('projects.states.idea', 'Idea'),
value: 'not_started' as ProjectStatus,
label: t('projectStatus.not_started', 'Not Started'),
description: t(
'projects.states.idea_desc',
'captured but not planned yet'
'projectStatus.not_started_desc',
'captured but not started yet'
),
icon: (
<EllipsisHorizontalCircleIcon className="w-5 h-5 text-gray-500" />
),
icon: <LightBulbIcon className="w-5 h-5 text-yellow-500" />,
},
{
value: 'planned' as ProjectState,
label: t('projects.states.planned', 'Planned'),
value: 'planned' as ProjectStatus,
label: t('projectStatus.planned', 'Planned'),
description: t(
'projects.states.planned_desc',
'projectStatus.planned_desc',
'scoped and ready to start'
),
icon: (
@ -43,31 +46,37 @@ const ProjectStateDropdown: React.FC<ProjectStateDropdownProps> = ({
),
},
{
value: 'in_progress' as ProjectState,
label: t('projects.states.in_progress', 'In Progress'),
value: 'in_progress' as ProjectStatus,
label: t('projectStatus.in_progress', 'In Progress'),
description: t(
'projects.states.in_progress_desc',
'projectStatus.in_progress_desc',
'active work happening'
),
icon: <PlayIcon className="w-5 h-5 text-green-500" />,
},
{
value: 'blocked' as ProjectState,
label: t('projects.states.blocked', 'Blocked'),
value: 'waiting' as ProjectStatus,
label: t('projectStatus.waiting', 'Waiting'),
description: t(
'projects.states.blocked_desc',
'temporarily paused or stuck'
'projectStatus.waiting_desc',
'waiting on external input'
),
icon: <ExclamationTriangleIcon className="w-5 h-5 text-red-500" />,
icon: <ClockIcon className="w-5 h-5 text-yellow-500" />,
},
{
value: 'completed' as ProjectState,
label: t('projects.states.completed', 'Completed'),
value: 'done' as ProjectStatus,
label: t('projectStatus.done', 'Completed'),
description: t('projectStatus.done_desc', 'finished and done'),
icon: <CheckCircleIcon className="w-5 h-5 text-green-600" />,
},
{
value: 'cancelled' as ProjectStatus,
label: t('projectStatus.cancelled', 'Cancelled'),
description: t(
'projects.states.completed_desc',
'finished and done'
'projectStatus.cancelled_desc',
'will not be completed'
),
icon: <CheckCircleIcon className="w-5 h-5 text-gray-500" />,
icon: <XCircleIcon className="w-5 h-5 text-red-500" />,
},
];
@ -110,8 +119,8 @@ const ProjectStateDropdown: React.FC<ProjectStateDropdownProps> = ({
}
};
const handleSelect = (state: ProjectState) => {
onChange(state);
const handleSelect = (status: ProjectStatus) => {
onChange(status);
setIsOpen(false);
};
@ -182,7 +191,7 @@ const ProjectStateDropdown: React.FC<ProjectStateDropdownProps> = ({
{selectedState ? (
selectedState.icon
) : (
<LightBulbIcon className="w-5 h-5 text-gray-400" />
<EllipsisHorizontalCircleIcon className="w-5 h-5 text-gray-400" />
)}
<span>
{selectedState

View file

@ -1196,12 +1196,12 @@ const TasksToday: React.FC = () => {
{Array.isArray(localProjects)
? localProjects.filter(
(project) =>
project.state &&
project.status &&
[
'planned',
'in_progress',
'blocked',
].includes(project.state)
'waiting',
].includes(project.status)
).length
: 0}
</p>

View file

@ -3,13 +3,13 @@ import { Tag } from './Tag';
import { PriorityType, Task } from './Task';
import { Note } from './Note';
export type ProjectState =
| 'idea'
export type ProjectStatus =
| 'not_started'
| 'planned'
| 'in_progress'
| 'active'
| 'blocked'
| 'completed';
| 'waiting'
| 'done'
| 'cancelled';
export interface Project {
id?: number;
@ -30,7 +30,7 @@ export interface Project {
image_url?: string;
task_show_completed?: boolean;
task_sort_order?: string;
state?: ProjectState;
status?: ProjectStatus;
created_at?: string;
updated_at?: string;
share_count?: number;

View file

@ -151,7 +151,9 @@
"open": "فتح",
"completed": "مكتمل",
"noCompletedTasksToday": "لا توجد مهام مكتملة اليوم.",
"markAsDone": "Mark as done"
"markAsDone": "Mark as done",
"notStarted": "لم يبدأ بعد",
"done": "تم"
},
"timeline": {
"activityTimeline": "جدول الأنشطة",
@ -820,7 +822,8 @@
"active": "نشط",
"inactive": "غير نشط",
"all": "الكل",
"allAreas": "جميع المناطق"
"allAreas": "جميع المناطق",
"notCompleted": "لم يكتمل"
},
"selectState": "اختر الحالة",
"state": "حالة المشروع",
@ -1342,5 +1345,19 @@
"title": "ملاحظة:",
"message": "ستكون إشعارات البريد الإلكتروني وإشعارات الدفع متاحة قريبًا. الإشعارات داخل التطبيق وإشعارات تليجرام متاحة حاليًا."
}
},
"projectStatus": {
"not_started": "لم يبدأ بعد",
"not_started_desc": "تم التقاطه ولكن لم يبدأ بعد",
"planned": "مخطط",
"planned_desc": "تم تحديد نطاقه وجاهز للبدء",
"in_progress": "قيد التنفيذ",
"in_progress_desc": "عمل نشط جارٍ",
"waiting": "انتظار",
"waiting_desc": "في انتظار مدخلات خارجية",
"done": "مكتمل",
"done_desc": "انتهى وتم",
"cancelled": "ملغى",
"cancelled_desc": "لن يكتمل"
}
}
}

View file

@ -151,7 +151,9 @@
"open": "Отвори",
"completed": "Завършено",
"noCompletedTasksToday": "Няма завършени задачи днес.",
"markAsDone": "Mark as done"
"markAsDone": "Mark as done",
"notStarted": "Не е започнато",
"done": "Завършено"
},
"timeline": {
"activityTimeline": "Хронология на активността",
@ -820,7 +822,8 @@
"active": "Активен",
"inactive": "Неактивен",
"all": "Всички",
"allAreas": "Всички области"
"allAreas": "Всички области",
"notCompleted": "Не е завършено"
},
"selectState": "Изберете състояние",
"state": "Състояние на проекта",
@ -1342,5 +1345,19 @@
"title": "Забележка:",
"message": "Имейл и Push известия ще бъдат налични скоро. В приложението и Telegram известията в момента са налични."
}
},
"projectStatus": {
"not_started": "Не е започнато",
"not_started_desc": "Записано, но все още не е започнато",
"planned": "Планирано",
"planned_desc": "Определено и готово за започване",
"in_progress": "В процес",
"in_progress_desc": "Активна работа в ход",
"waiting": "Изчакване",
"waiting_desc": "Изчакване на външна информация",
"done": "Завършено",
"done_desc": "Завършено и готово",
"cancelled": "Отменено",
"cancelled_desc": "Няма да бъде завършено"
}
}
}

View file

@ -151,7 +151,9 @@
"open": "Åbn",
"completed": "Fuldført",
"noCompletedTasksToday": "Ingen fuldførte opgaver i dag.",
"markAsDone": "Mark as done"
"markAsDone": "Mark as done",
"notStarted": "Ikke startet",
"done": "Færdig"
},
"timeline": {
"activityTimeline": "Aktivitets Tidslinje",
@ -820,7 +822,8 @@
"active": "Aktiv",
"inactive": "Inaktiv",
"all": "Alle",
"allAreas": "Alle områder"
"allAreas": "Alle områder",
"notCompleted": "Ikke færdiggjort"
},
"selectState": "Vælg tilstand",
"state": "Projektstatus",
@ -1342,5 +1345,19 @@
"title": "Bemærk:",
"message": "E-mail og push-notifikationer kommer snart. Notifikationer i appen og Telegram er i øjeblikket tilgængelige."
}
},
"projectStatus": {
"not_started": "Ikke startet",
"not_started_desc": "Registreret, men ikke startet endnu",
"planned": "Planlagt",
"planned_desc": "Afgrænset og klar til at starte",
"in_progress": "I gang",
"in_progress_desc": "Aktivt arbejde i gang",
"waiting": "Venter",
"waiting_desc": "Venter på ekstern input",
"done": "Færdiggjort",
"done_desc": "Afsluttet og færdig",
"cancelled": "Annulleret",
"cancelled_desc": "Vil ikke blive færdiggjort"
}
}
}

View file

@ -151,7 +151,9 @@
"open": "Öffnen",
"completed": "Abgeschlossen",
"noCompletedTasksToday": "Keine abgeschlossenen Aufgaben heute.",
"markAsDone": "Mark as done"
"markAsDone": "Mark as done",
"notStarted": "Nicht gestartet",
"done": "Fertig"
},
"timeline": {
"activityTimeline": "Aktivitätsverlauf",
@ -912,7 +914,8 @@
"active": "Aktiv",
"inactive": "Inaktiv",
"all": "Alle",
"allAreas": "Alle Bereiche"
"allAreas": "Alle Bereiche",
"notCompleted": "Nicht abgeschlossen"
},
"selectState": "Zustand auswählen",
"state": "Projektzustand",
@ -1351,5 +1354,19 @@
"title": "Hinweis:",
"message": "E-Mail- und Push-Benachrichtigungen kommen bald. In-App- und Telegram-Benachrichtigungen sind derzeit verfügbar."
}
},
"projectStatus": {
"not_started": "Nicht gestartet",
"not_started_desc": "Erfasst, aber noch nicht gestartet",
"planned": "Geplant",
"planned_desc": "Abgegrenzt und bereit zum Start",
"in_progress": "In Bearbeitung",
"in_progress_desc": "Aktive Arbeit läuft",
"waiting": "Warten",
"waiting_desc": "Warten auf externe Eingaben",
"done": "Abgeschlossen",
"done_desc": "Fertig und abgeschlossen",
"cancelled": "Abgebrochen",
"cancelled_desc": "Wird nicht abgeschlossen"
}
}
}

View file

@ -366,7 +366,9 @@
"open": "Άνοιγμα",
"completed": "Ολοκληρώθηκε",
"noCompletedTasksToday": "Καμία ολοκληρωμένη εργασία σήμερα.",
"markAsDone": "Mark as done"
"markAsDone": "Mark as done",
"notStarted": "Δεν έχει ξεκινήσει",
"done": "Ολοκληρώθηκε"
},
"timeline": {
"activityTimeline": "Χρονοδιάγραμμα Δραστηριότητας",
@ -427,7 +429,8 @@
"active": "Ενεργά",
"inactive": "Ανενεργά",
"all": "Όλα",
"allAreas": "Όλες οι περιοχές"
"allAreas": "Όλες οι περιοχές",
"notCompleted": "Δεν έχει ολοκληρωθεί"
},
"selectState": "Επιλέξτε Κατάσταση",
"state": "Κατάσταση Έργου",
@ -1346,5 +1349,19 @@
"title": "Σημείωση:",
"message": "Οι ειδοποιήσεις μέσω Email και Push έρχονται σύντομα. Οι ειδοποιήσεις εντός της εφαρμογής και μέσω Telegram είναι διαθέσιμες αυτή τη στιγμή."
}
},
"projectStatus": {
"not_started": "Δεν έχει ξεκινήσει",
"not_started_desc": "Καταγεγραμμένο αλλά δεν έχει ξεκινήσει ακόμα",
"planned": "Προγραμματισμένο",
"planned_desc": "Καθορισμένο και έτοιμο για εκκίνηση",
"in_progress": "Σε εξέλιξη",
"in_progress_desc": "Ενεργή εργασία σε εξέλιξη",
"waiting": "Αναμονή",
"waiting_desc": "Αναμονή για εξωτερική είσοδο",
"done": "Ολοκληρώθηκε",
"done_desc": "Ολοκληρωμένο και τελειωμένο",
"cancelled": "Ακυρώθηκε",
"cancelled_desc": "Δεν θα ολοκληρωθεί"
}
}
}

View file

@ -812,26 +812,6 @@
"metrics": "Projects",
"selectState": "Select State",
"state": "Project State",
"filters": {
"active": "Active",
"inactive": "Inactive",
"all": "All",
"allAreas": "All Areas"
},
"states": {
"idea": "Idea",
"planned": "Planned",
"in_progress": "In Progress",
"active": "In Progress",
"blocked": "Blocked",
"completed": "Completed",
"idea_desc": "Captured but not planned yet",
"planned_desc": "Scoped and ready to start",
"in_progress_desc": "Active work happening",
"active_desc": "Active work happening",
"blocked_desc": "Temporarily paused or stuck",
"completed_desc": "Finished and done"
},
"showMetrics": "Show metrics",
"hideMetrics": "Hide metrics",
"progress": "Progress",
@ -854,7 +834,28 @@
"nextUp": "Next best action",
"focusTask": "Most impactful task",
"focusHint": "Shifts this task to in progress and today",
"noNextAction": "All clear—no outstanding tasks."
"noNextAction": "All clear—no outstanding tasks.",
"filters": {
"active": "Active",
"inactive": "Inactive",
"all": "All",
"allAreas": "All Areas",
"notCompleted": "Not Completed"
}
},
"projectStatus": {
"not_started": "Not Started",
"not_started_desc": "Captured but not started yet",
"planned": "Planned",
"planned_desc": "Scoped and ready to start",
"in_progress": "In Progress",
"in_progress_desc": "Active work happening",
"waiting": "Waiting",
"waiting_desc": "Waiting on external input",
"done": "Completed",
"done_desc": "Finished and done",
"cancelled": "Cancelled",
"cancelled_desc": "Will not be completed"
},
"projectItem": {
"edit": "Edit",

View file

@ -366,7 +366,9 @@
"open": "Abrir",
"completed": "Completado",
"noCompletedTasksToday": "No hay tareas completadas hoy.",
"markAsDone": "Mark as done"
"markAsDone": "Mark as done",
"notStarted": "No Iniciado",
"done": "Hecho"
},
"timeline": {
"activityTimeline": "Línea de Tiempo de Actividad",
@ -427,24 +429,11 @@
"active": "Activos",
"inactive": "Inactivos",
"all": "Todos",
"allAreas": "Todas las áreas"
"allAreas": "Todas las áreas",
"notCompleted": "No Completados"
},
"selectState": "Seleccionar Estado",
"state": "Estado del Proyecto",
"states": {
"idea": "Idea",
"planned": "Planificado",
"in_progress": "En Progreso",
"blocked": "Bloqueado",
"completed": "Completado",
"idea_desc": "Capturado pero aún no planificado",
"planned_desc": "Definido y listo para comenzar",
"in_progress_desc": "Trabajo activo en curso",
"blocked_desc": "Pausado temporalmente o atascado",
"completed_desc": "Terminado y completado",
"active": "En Progreso",
"active_desc": "Trabajo activo en curso"
},
"showMetrics": "Mostrar métricas",
"hideMetrics": "Ocultar métricas",
"progress": "Progreso",
@ -1343,5 +1332,19 @@
"title": "Nota:",
"message": "Las notificaciones por correo electrónico y Push llegarán pronto. Las notificaciones en la aplicación y de Telegram están disponibles actualmente."
}
},
"projectStatus": {
"not_started": "No Iniciado",
"not_started_desc": "Capturado pero aún no iniciado",
"planned": "Planificado",
"planned_desc": "Definido y listo para comenzar",
"in_progress": "En Progreso",
"in_progress_desc": "Trabajo activo en curso",
"waiting": "Esperando",
"waiting_desc": "Esperando entrada externa",
"done": "Completado",
"done_desc": "Terminado y hecho",
"cancelled": "Cancelado",
"cancelled_desc": "No se completará"
}
}

View file

@ -151,7 +151,9 @@
"open": "Avaa",
"completed": "Valmis",
"noCompletedTasksToday": "Ei suoritettuja tehtäviä tänään.",
"markAsDone": "Mark as done"
"markAsDone": "Mark as done",
"notStarted": "Ei aloitettu",
"done": "Valmis"
},
"timeline": {
"activityTimeline": "Toiminta-aikajana",
@ -820,7 +822,8 @@
"active": "Aktiivinen",
"inactive": "Passiivinen",
"all": "Kaikki",
"allAreas": "Kaikki alueet"
"allAreas": "Kaikki alueet",
"notCompleted": "Ei valmis"
},
"selectState": "Valitse tila",
"state": "Projektin tila",
@ -1342,5 +1345,19 @@
"title": "Huom:",
"message": "Sähköposti- ja Push-ilmoitukset ovat tulossa pian. Sovelluksen sisäiset ja Telegram-ilmoitukset ovat tällä hetkellä saatavilla."
}
},
"projectStatus": {
"not_started": "Ei aloitettu",
"not_started_desc": "Tallennettu mutta ei vielä aloitettu",
"planned": "Suunniteltu",
"planned_desc": "Määritelty ja valmis aloittamaan",
"in_progress": "Käynnissä",
"in_progress_desc": "Aktiivista työtä käynnissä",
"waiting": "Odottaa",
"waiting_desc": "Odottaa ulkoista syötettä",
"done": "Valmistunut",
"done_desc": "Valmis ja tehty",
"cancelled": "Peruutettu",
"cancelled_desc": "Ei tule valmistumaan"
}
}
}

View file

@ -151,7 +151,9 @@
"open": "Ouvrir",
"completed": "Terminé",
"noCompletedTasksToday": "Aucune tâche terminée aujourd'hui.",
"markAsDone": "Mark as done"
"markAsDone": "Mark as done",
"notStarted": "Non commencé",
"done": "Fait"
},
"timeline": {
"activityTimeline": "Chronologie d'Activité",
@ -820,7 +822,8 @@
"active": "Actif",
"inactive": "Inactif",
"all": "Tous",
"allAreas": "Toutes les zones"
"allAreas": "Toutes les zones",
"notCompleted": "Non terminé"
},
"selectState": "Sélectionner l'état",
"state": "État du projet",
@ -1342,5 +1345,19 @@
"title": "Remarque :",
"message": "Les notifications par e-mail et push arrivent bientôt. Les notifications dans l'application et sur Telegram sont actuellement disponibles."
}
},
"projectStatus": {
"not_started": "Non commencé",
"not_started_desc": "Capturé mais pas encore commencé",
"planned": "Prévu",
"planned_desc": "Défini et prêt à commencer",
"in_progress": "En cours",
"in_progress_desc": "Travail actif en cours",
"waiting": "En attente",
"waiting_desc": "En attente d'une entrée externe",
"done": "Terminé",
"done_desc": "Fini et terminé",
"cancelled": "Annulé",
"cancelled_desc": "Ne sera pas terminé"
}
}
}

View file

@ -151,7 +151,9 @@
"open": "Buka",
"completed": "Selesai",
"noCompletedTasksToday": "Tidak ada tugas yang diselesaikan hari ini.",
"markAsDone": "Mark as done"
"markAsDone": "Mark as done",
"notStarted": "Belum Dimulai",
"done": "Selesai"
},
"timeline": {
"activityTimeline": "Garis Waktu Aktivitas",
@ -820,7 +822,8 @@
"active": "Aktif",
"inactive": "Tidak Aktif",
"all": "Semua",
"allAreas": "Semua Area"
"allAreas": "Semua Area",
"notCompleted": "Belum Selesai"
},
"selectState": "Pilih Status",
"state": "Status Proyek",
@ -1342,5 +1345,19 @@
"title": "Catatan:",
"message": "Notifikasi Email dan Push akan segera hadir. Notifikasi di dalam aplikasi dan Telegram saat ini tersedia."
}
},
"projectStatus": {
"not_started": "Belum Dimulai",
"not_started_desc": "Tercatat tetapi belum dimulai",
"planned": "Direncanakan",
"planned_desc": "Sudah ditentukan dan siap untuk dimulai",
"in_progress": "Sedang Berlangsung",
"in_progress_desc": "Pekerjaan aktif sedang berlangsung",
"waiting": "Menunggu",
"waiting_desc": "Menunggu masukan eksternal",
"done": "Selesai",
"done_desc": "Telah selesai dan selesai",
"cancelled": "Dibatalkan",
"cancelled_desc": "Tidak akan diselesaikan"
}
}
}

View file

@ -151,7 +151,9 @@
"open": "Apri",
"completed": "Completato",
"noCompletedTasksToday": "Nessuna attività completata oggi.",
"markAsDone": "Mark as done"
"markAsDone": "Mark as done",
"notStarted": "Non Iniziato",
"done": "Fatto"
},
"timeline": {
"activityTimeline": "Timeline delle Attività",
@ -820,7 +822,8 @@
"active": "Attivo",
"inactive": "Inattivo",
"all": "Tutti",
"allAreas": "Tutte le Aree"
"allAreas": "Tutte le Aree",
"notCompleted": "Non Completato"
},
"selectState": "Seleziona Stato",
"state": "Stato del Progetto",
@ -1342,5 +1345,19 @@
"title": "Nota:",
"message": "Le notifiche via Email e Push arriveranno presto. Le notifiche in-app e su Telegram sono attualmente disponibili."
}
},
"projectStatus": {
"not_started": "Non Iniziato",
"not_started_desc": "Catturato ma non ancora iniziato",
"planned": "Pianificato",
"planned_desc": "Definito e pronto per iniziare",
"in_progress": "In Corso",
"in_progress_desc": "Lavoro attivo in corso",
"waiting": "In Attesa",
"waiting_desc": "In attesa di input esterno",
"done": "Completato",
"done_desc": "Finito e completato",
"cancelled": "Annullato",
"cancelled_desc": "Non sarà completato"
}
}
}

View file

@ -151,7 +151,9 @@
"open": "開く",
"completed": "完了",
"noCompletedTasksToday": "本日完了したタスクはありません。",
"markAsDone": "Mark as done"
"markAsDone": "Mark as done",
"notStarted": "未開始",
"done": "完了"
},
"timeline": {
"activityTimeline": "アクティビティタイムライン",
@ -589,7 +591,8 @@
"active": "有効",
"inactive": "無効",
"all": "すべて",
"allAreas": "すべてのエリア"
"allAreas": "すべてのエリア",
"notCompleted": "未完了"
},
"active": "アクティブ",
"inactive": "非アクティブ",
@ -1342,5 +1345,19 @@
"title": "注意:",
"message": "メールおよびプッシュ通知は近日中に提供予定です。アプリ内およびTelegram通知は現在利用可能です。"
}
},
"projectStatus": {
"not_started": "未開始",
"not_started_desc": "キャプチャされたが、まだ開始されていません",
"planned": "計画中",
"planned_desc": "スコープが決まり、開始準備が整いました",
"in_progress": "進行中",
"in_progress_desc": "アクティブな作業が行われています",
"waiting": "待機中",
"waiting_desc": "外部の入力を待っています",
"done": "完了",
"done_desc": "終了しました",
"cancelled": "キャンセル",
"cancelled_desc": "完了しません"
}
}

View file

@ -151,7 +151,9 @@
"open": "열기",
"completed": "완료됨",
"noCompletedTasksToday": "오늘 완료된 작업이 없습니다.",
"markAsDone": "Mark as done"
"markAsDone": "Mark as done",
"notStarted": "시작되지 않음",
"done": "완료됨"
},
"timeline": {
"activityTimeline": "활동 타임라인",
@ -820,7 +822,8 @@
"active": "활성",
"inactive": "비활성",
"all": "모두",
"allAreas": "모든 영역"
"allAreas": "모든 영역",
"notCompleted": "완료되지 않음"
},
"selectState": "상태 선택",
"state": "프로젝트 상태",
@ -1342,5 +1345,19 @@
"title": "참고:",
"message": "이메일 및 푸시 알림이 곧 제공됩니다. 현재 앱 내 및 텔레그램 알림이 가능합니다."
}
},
"projectStatus": {
"not_started": "시작되지 않음",
"not_started_desc": "포착되었지만 아직 시작되지 않음",
"planned": "계획됨",
"planned_desc": "범위가 정해지고 시작할 준비가 됨",
"in_progress": "진행 중",
"in_progress_desc": "활동적인 작업 진행 중",
"waiting": "대기 중",
"waiting_desc": "외부 입력 대기 중",
"done": "완료됨",
"done_desc": "끝났고 완료됨",
"cancelled": "취소됨",
"cancelled_desc": "완료되지 않을 것임"
}
}

View file

@ -151,7 +151,9 @@
"open": "Open",
"completed": "Voltooid",
"noCompletedTasksToday": "Geen voltooide taken vandaag.",
"markAsDone": "Mark as done"
"markAsDone": "Mark as done",
"notStarted": "Niet Begonnen",
"done": "Voltooid"
},
"timeline": {
"activityTimeline": "Activiteit Tijdlijn",
@ -820,7 +822,8 @@
"active": "Actief",
"inactive": "Inactief",
"all": "Alle",
"allAreas": "Alle gebieden"
"allAreas": "Alle gebieden",
"notCompleted": "Niet Voltooid"
},
"selectState": "Selecteer Staat",
"state": "Projectstaat",
@ -1342,5 +1345,19 @@
"title": "Opmerking:",
"message": "E-mail- en pushmeldingen komen binnenkort. In-app en Telegram-meldingen zijn momenteel beschikbaar."
}
},
"projectStatus": {
"not_started": "Niet Begonnen",
"not_started_desc": "Vastgelegd maar nog niet begonnen",
"planned": "Gepland",
"planned_desc": "Afgebakend en klaar om te beginnen",
"in_progress": "Bezig",
"in_progress_desc": "Actief werk bezig",
"waiting": "Wachten",
"waiting_desc": "Wachten op externe input",
"done": "Voltooid",
"done_desc": "Afgerond en voltooid",
"cancelled": "Geannuleerd",
"cancelled_desc": "Zal niet worden voltooid"
}
}
}

View file

@ -151,7 +151,9 @@
"open": "Åpne",
"completed": "Fullført",
"noCompletedTasksToday": "Ingen fullførte oppgaver i dag.",
"markAsDone": "Mark as done"
"markAsDone": "Mark as done",
"notStarted": "Ikke startet",
"done": "Ferdig"
},
"timeline": {
"activityTimeline": "Aktivitetslinje",
@ -820,7 +822,8 @@
"active": "Aktiv",
"inactive": "Inaktiv",
"all": "Alle",
"allAreas": "Alle områder"
"allAreas": "Alle områder",
"notCompleted": "Ikke fullført"
},
"selectState": "Velg tilstand",
"state": "Prosjektstatus",
@ -1342,5 +1345,19 @@
"title": "Merk:",
"message": "E-post og Push-varsler kommer snart. Varsler i appen og Telegram er for øyeblikket tilgjengelige."
}
},
"projectStatus": {
"not_started": "Ikke startet",
"not_started_desc": "Registrert, men ikke startet ennå",
"planned": "Planlagt",
"planned_desc": "Avgrenset og klar til å starte",
"in_progress": "Pågår",
"in_progress_desc": "Aktivt arbeid pågår",
"waiting": "Venter",
"waiting_desc": "Venter på ekstern input",
"done": "Fullført",
"done_desc": "Ferdig og avsluttet",
"cancelled": "Avbrutt",
"cancelled_desc": "Vil ikke bli fullført"
}
}
}

View file

@ -151,7 +151,9 @@
"open": "Otwórz",
"completed": "Zakończone",
"noCompletedTasksToday": "Brak ukończonych zadań dzisiaj.",
"markAsDone": "Mark as done"
"markAsDone": "Mark as done",
"notStarted": "Nie rozpoczęto",
"done": "Zrobione"
},
"timeline": {
"activityTimeline": "Oś czasu aktywności",
@ -820,7 +822,8 @@
"active": "Aktywne",
"inactive": "Nieaktywne",
"all": "Wszystkie",
"allAreas": "Wszystkie obszary"
"allAreas": "Wszystkie obszary",
"notCompleted": "Nie ukończono"
},
"selectState": "Wybierz stan",
"state": "Stan projektu",
@ -1342,5 +1345,19 @@
"title": "Uwaga:",
"message": "Powiadomienia e-mailowe i push będą dostępne wkrótce. Powiadomienia w aplikacji i na Telegramie są obecnie dostępne."
}
},
"projectStatus": {
"not_started": "Nie rozpoczęto",
"not_started_desc": "Zarejestrowano, ale jeszcze nie rozpoczęto",
"planned": "Zaplanowane",
"planned_desc": "Zakres ustalony i gotowy do rozpoczęcia",
"in_progress": "W trakcie",
"in_progress_desc": "Aktywna praca w toku",
"waiting": "Oczekiwanie",
"waiting_desc": "Oczekiwanie na zewnętrzne dane",
"done": "Ukończono",
"done_desc": "Zakończono i wykonano",
"cancelled": "Anulowane",
"cancelled_desc": "Nie zostanie ukończone"
}
}
}

View file

@ -151,7 +151,9 @@
"open": "Abrir",
"completed": "Concluído",
"noCompletedTasksToday": "Nenhuma tarefa concluída hoje.",
"markAsDone": "Mark as done"
"markAsDone": "Mark as done",
"notStarted": "Não Iniciado",
"done": "Concluído"
},
"timeline": {
"activityTimeline": "Linha do Tempo de Atividades",
@ -820,7 +822,8 @@
"active": "Ativo",
"inactive": "Inativo",
"all": "Todos",
"allAreas": "Todas as Áreas"
"allAreas": "Todas as Áreas",
"notCompleted": "Não Concluído"
},
"selectState": "Selecionar Estado",
"state": "Estado do Projeto",
@ -1342,5 +1345,19 @@
"title": "Nota:",
"message": "Notificações por Email e Push estarão disponíveis em breve. Notificações no aplicativo e no Telegram estão atualmente disponíveis."
}
},
"projectStatus": {
"not_started": "Não Iniciado",
"not_started_desc": "Capturado, mas ainda não iniciado",
"planned": "Planejado",
"planned_desc": "Escopado e pronto para começar",
"in_progress": "Em Andamento",
"in_progress_desc": "Trabalho ativo em andamento",
"waiting": "Aguardando",
"waiting_desc": "Aguardando entrada externa",
"done": "Concluído",
"done_desc": "Finalizado e concluído",
"cancelled": "Cancelado",
"cancelled_desc": "Não será concluído"
}
}
}

View file

@ -151,7 +151,9 @@
"open": "Deschide",
"completed": "Finalizat",
"noCompletedTasksToday": "Nicio sarcină finalizată astăzi.",
"markAsDone": "Mark as done"
"markAsDone": "Mark as done",
"notStarted": "Nefăcut",
"done": "Finalizat"
},
"timeline": {
"activityTimeline": "Cronologia activităților",
@ -820,7 +822,8 @@
"active": "Activ",
"inactive": "Inactiv",
"all": "Toate",
"allAreas": "Toate zonele"
"allAreas": "Toate zonele",
"notCompleted": "Nefinalizat"
},
"selectState": "Selectați Starea",
"state": "Starea Proiectului",
@ -1342,5 +1345,19 @@
"title": "Notă:",
"message": "Notificările prin Email și Push vor fi disponibile în curând. Notificările în aplicație și pe Telegram sunt disponibile în prezent."
}
},
"projectStatus": {
"not_started": "Nefăcut",
"not_started_desc": "Capturat, dar încă nefăcut",
"planned": "Planificat",
"planned_desc": "Definit și gata de început",
"in_progress": "În desfășurare",
"in_progress_desc": "Lucrări active în desfășurare",
"waiting": "Așteptând",
"waiting_desc": "Așteptând input extern",
"done": "Finalizat",
"done_desc": "Terminată și finalizată",
"cancelled": "Anulat",
"cancelled_desc": "Nu va fi finalizat"
}
}
}

View file

@ -151,7 +151,9 @@
"open": "Открыть",
"completed": "Завершено",
"noCompletedTasksToday": "Сегодня нет завершенных задач.",
"markAsDone": "Mark as done"
"markAsDone": "Mark as done",
"notStarted": "Не начато",
"done": "Выполнено"
},
"timeline": {
"activityTimeline": "Хронология активности",
@ -820,7 +822,8 @@
"active": "Активные",
"inactive": "Неактивные",
"all": "Все",
"allAreas": "Все области"
"allAreas": "Все области",
"notCompleted": "Не завершено"
},
"selectState": "Выберите состояние",
"state": "Состояние проекта",
@ -1342,5 +1345,19 @@
"title": "Примечание:",
"message": "Уведомления по электронной почте и Push скоро будут доступны. Уведомления в приложении и Telegram в настоящее время доступны."
}
},
"projectStatus": {
"not_started": "Не начато",
"not_started_desc": "Зафиксировано, но еще не начато",
"planned": "Запланировано",
"planned_desc": "Определено и готово к началу",
"in_progress": "В процессе",
"in_progress_desc": "Активная работа ведется",
"waiting": "Ожидание",
"waiting_desc": "Ожидание внешнего ввода",
"done": "Завершено",
"done_desc": "Закончено и выполнено",
"cancelled": "Отменено",
"cancelled_desc": "Не будет завершено"
}
}
}

View file

@ -151,7 +151,9 @@
"open": "Odpri",
"completed": "Dokončano",
"noCompletedTasksToday": "Danes ni zaključenih nalog.",
"markAsDone": "Mark as done"
"markAsDone": "Mark as done",
"notStarted": "Niso začeli",
"done": "Dokončano"
},
"timeline": {
"activityTimeline": "Časovnica aktivnosti",
@ -820,7 +822,8 @@
"active": "Aktivno",
"inactive": "Neaktivno",
"all": "Vse",
"allAreas": "Vse področja"
"allAreas": "Vse področja",
"notCompleted": "Nedokončano"
},
"selectState": "Izberite stanje",
"state": "Stanje projekta",
@ -1342,5 +1345,19 @@
"title": "Opomba:",
"message": "Obvestila po e-pošti in potisna obvestila kmalu prihajajo. Obvestila v aplikaciji in na Telegramu so trenutno na voljo."
}
},
"projectStatus": {
"not_started": "Niso začeli",
"not_started_desc": "Zajeto, a še ni začeto",
"planned": "Načrtovano",
"planned_desc": "Opredeljeno in pripravljeno za začetek",
"in_progress": "V teku",
"in_progress_desc": "Aktivno delo poteka",
"waiting": "Čaka",
"waiting_desc": "Čaka na zunanje vnose",
"done": "Dokončano",
"done_desc": "Končano in opravljeno",
"cancelled": "Preklicano",
"cancelled_desc": "Ne bo dokončano"
}
}
}

View file

@ -151,7 +151,9 @@
"open": "Öppna",
"completed": "Avslutad",
"noCompletedTasksToday": "Inga slutförda uppgifter idag.",
"markAsDone": "Mark as done"
"markAsDone": "Mark as done",
"notStarted": "Ej påbörjad",
"done": "Utförd"
},
"timeline": {
"activityTimeline": "Aktivitetslinje",
@ -820,7 +822,8 @@
"active": "Aktiva",
"inactive": "Inaktiva",
"all": "Alla",
"allAreas": "Alla områden"
"allAreas": "Alla områden",
"notCompleted": "Ej slutförd"
},
"selectState": "Välj status",
"state": "Projektstatus",
@ -1342,5 +1345,19 @@
"title": "Notera:",
"message": "E-post och push-notifikationer kommer snart. In-app och Telegram-notifikationer är för närvarande tillgängliga."
}
},
"projectStatus": {
"not_started": "Ej påbörjad",
"not_started_desc": "Fångad men ej påbörjad än",
"planned": "Planerad",
"planned_desc": "Definierad och redo att starta",
"in_progress": "Pågående",
"in_progress_desc": "Aktivt arbete pågår",
"waiting": "Väntar",
"waiting_desc": "Väntar på extern input",
"done": "Slutförd",
"done_desc": "Avslutad och klar",
"cancelled": "Avbruten",
"cancelled_desc": "Kommer inte att slutföras"
}
}
}

View file

@ -151,7 +151,9 @@
"open": "Aç",
"completed": "Tamamlandı",
"noCompletedTasksToday": "Bugün tamamlanan görev yok.",
"markAsDone": "Mark as done"
"markAsDone": "Mark as done",
"notStarted": "Başlanmadı",
"done": "Tamamlandı"
},
"timeline": {
"activityTimeline": "Etkinlik Zaman Çizelgesi",
@ -820,7 +822,8 @@
"active": "Aktif",
"inactive": "Pasif",
"all": "Hepsi",
"allAreas": "Tüm Alanlar"
"allAreas": "Tüm Alanlar",
"notCompleted": "Tamamlanmadı"
},
"selectState": "Durum Seç",
"state": "Proje Durumu",
@ -1342,5 +1345,19 @@
"title": "Not:",
"message": "E-posta ve Push bildirimleri yakında geliyor. Uygulama içi ve Telegram bildirimleri şu anda mevcuttur."
}
},
"projectStatus": {
"not_started": "Başlanmadı",
"not_started_desc": "Kaydedildi ama henüz başlanmadı",
"planned": "Planlandı",
"planned_desc": "Kapsam belirlendi ve başlamaya hazır",
"in_progress": "Devam Ediyor",
"in_progress_desc": "Aktif çalışma yapılıyor",
"waiting": "Bekliyor",
"waiting_desc": "Dışsal girdi bekleniyor",
"done": "Tamamlandı",
"done_desc": "Bitirildi ve tamamlandı",
"cancelled": "İptal Edildi",
"cancelled_desc": "Tamamlanmayacak"
}
}
}

View file

@ -151,7 +151,9 @@
"open": "Відкрити",
"completed": "Завершено",
"noCompletedTasksToday": "Сьогодні немає завершених завдань.",
"markAsDone": "Mark as done"
"markAsDone": "Mark as done",
"notStarted": "Не розпочато",
"done": "Завершено"
},
"timeline": {
"activityTimeline": "Хронологія Активності",
@ -209,7 +211,8 @@
"active": "Активні",
"inactive": "Неактивні",
"all": "Всі",
"allAreas": "Всі області"
"allAreas": "Всі області",
"notCompleted": "Не завершено"
},
"active": "Активні",
"inactive": "Неактивні",
@ -1342,5 +1345,19 @@
"title": "Примітка:",
"message": "Сповіщення електронною поштою та Push скоро з'являться. Сповіщення в додатку та Telegram наразі доступні."
}
},
"projectStatus": {
"not_started": "Не розпочато",
"not_started_desc": "Зафіксовано, але ще не розпочато",
"planned": "Заплановано",
"planned_desc": "Окреслено і готово до початку",
"in_progress": "В процесі",
"in_progress_desc": "Активна робота триває",
"waiting": "Очікування",
"waiting_desc": "Очікування на зовнішній внесок",
"done": "Завершено",
"done_desc": "Закінчено і виконано",
"cancelled": "Скасовано",
"cancelled_desc": "Не буде завершено"
}
}
}

View file

@ -151,7 +151,9 @@
"open": "Mở",
"completed": "Đã hoàn thành",
"noCompletedTasksToday": "Không có nhiệm vụ hoàn thành hôm nay.",
"markAsDone": "Mark as done"
"markAsDone": "Mark as done",
"notStarted": "Chưa bắt đầu",
"done": "Đã hoàn thành"
},
"timeline": {
"activityTimeline": "Dòng Thời Gian Hoạt Động",
@ -820,7 +822,8 @@
"active": "Đang hoạt động",
"inactive": "Không hoạt động",
"all": "Tất cả",
"allAreas": "Tất cả các khu vực"
"allAreas": "Tất cả các khu vực",
"notCompleted": "Chưa hoàn thành"
},
"selectState": "Chọn Trạng Thái",
"state": "Trạng Thái Dự Án",
@ -1342,5 +1345,19 @@
"title": "Lưu ý:",
"message": "Thông báo Email và Push sẽ sớm có. Thông báo trong ứng dụng và Telegram hiện đang có sẵn."
}
},
"projectStatus": {
"not_started": "Chưa bắt đầu",
"not_started_desc": "Đã ghi nhận nhưng chưa bắt đầu",
"planned": "Đã lên kế hoạch",
"planned_desc": "Đã xác định và sẵn sàng để bắt đầu",
"in_progress": "Đang tiến hành",
"in_progress_desc": "Công việc đang diễn ra",
"waiting": "Đang chờ",
"waiting_desc": "Đang chờ thông tin từ bên ngoài",
"done": "Đã hoàn thành",
"done_desc": "Đã hoàn tất và xong",
"cancelled": "Đã hủy",
"cancelled_desc": "Sẽ không được hoàn thành"
}
}
}

View file

@ -151,7 +151,9 @@
"open": "打开",
"completed": "已完成",
"noCompletedTasksToday": "今天没有完成的任务。",
"markAsDone": "Mark as done"
"markAsDone": "Mark as done",
"notStarted": "未开始",
"done": "完成"
},
"timeline": {
"activityTimeline": "活动时间线",
@ -820,7 +822,8 @@
"active": "活动",
"inactive": "非活动",
"all": "所有",
"allAreas": "所有区域"
"allAreas": "所有区域",
"notCompleted": "未完成"
},
"selectState": "选择状态",
"state": "项目状态",
@ -1342,5 +1345,19 @@
"title": "注意:",
"message": "电子邮件和推送通知即将推出。应用内和 Telegram 通知目前可用。"
}
},
"projectStatus": {
"not_started": "未开始",
"not_started_desc": "已捕获但尚未开始",
"planned": "已规划",
"planned_desc": "已确定范围并准备开始",
"in_progress": "进行中",
"in_progress_desc": "正在进行的工作",
"waiting": "等待中",
"waiting_desc": "等待外部输入",
"done": "已完成",
"done_desc": "已完成并结束",
"cancelled": "已取消",
"cancelled_desc": "将不会完成"
}
}