floq 1.4.1 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/done.js +1 -0
- package/dist/commands/insights.js +13 -10
- package/dist/commands/move.js +2 -1
- package/dist/db/index.js +36 -0
- package/dist/db/schema.d.ts +116 -0
- package/dist/db/schema.js +8 -0
- package/dist/i18n/en.d.ts +2 -0
- package/dist/i18n/en.js +1 -0
- package/dist/i18n/ja.js +1 -0
- package/dist/ui/App.js +7 -4
- package/dist/ui/components/GtdDQ.js +4 -1
- package/dist/ui/components/GtdMario.js +4 -1
- package/dist/ui/components/InsightsModal.js +13 -7
- package/dist/ui/components/KanbanBoard.js +3 -0
- package/dist/ui/components/KanbanDQ.js +2 -0
- package/dist/ui/components/KanbanMario.js +2 -0
- package/dist/ui/components/TaskItem.d.ts +2 -1
- package/dist/ui/components/TaskItem.js +4 -3
- package/dist/ui/history/HistoryContext.js +8 -0
- package/dist/ui/history/HistoryManager.d.ts +9 -1
- package/dist/ui/history/HistoryManager.js +140 -16
- package/dist/ui/history/commands/ConvertToProjectCommand.d.ts +5 -1
- package/dist/ui/history/commands/ConvertToProjectCommand.js +13 -0
- package/dist/ui/history/commands/CreateCommentCommand.d.ts +5 -1
- package/dist/ui/history/commands/CreateCommentCommand.js +22 -0
- package/dist/ui/history/commands/CreateTaskCommand.d.ts +5 -1
- package/dist/ui/history/commands/CreateTaskCommand.js +26 -1
- package/dist/ui/history/commands/DeleteCommentCommand.d.ts +5 -1
- package/dist/ui/history/commands/DeleteCommentCommand.js +22 -0
- package/dist/ui/history/commands/DeleteTaskCommand.d.ts +7 -2
- package/dist/ui/history/commands/DeleteTaskCommand.js +41 -0
- package/dist/ui/history/commands/LinkTaskCommand.d.ts +5 -1
- package/dist/ui/history/commands/LinkTaskCommand.js +14 -0
- package/dist/ui/history/commands/MoveTaskCommand.d.ts +7 -1
- package/dist/ui/history/commands/MoveTaskCommand.js +25 -0
- package/dist/ui/history/commands/SetContextCommand.d.ts +5 -1
- package/dist/ui/history/commands/SetContextCommand.js +14 -0
- package/dist/ui/history/commands/SetEffortCommand.d.ts +5 -1
- package/dist/ui/history/commands/SetEffortCommand.js +14 -0
- package/dist/ui/history/commands/SetFocusCommand.d.ts +5 -1
- package/dist/ui/history/commands/SetFocusCommand.js +14 -0
- package/dist/ui/history/commands/index.d.ts +1 -0
- package/dist/ui/history/commands/index.js +1 -0
- package/dist/ui/history/commands/registry.d.ts +2 -0
- package/dist/ui/history/commands/registry.js +28 -0
- package/dist/ui/history/index.d.ts +2 -2
- package/dist/ui/history/index.js +1 -1
- package/dist/ui/history/types.d.ts +9 -0
- package/package.json +1 -1
package/dist/commands/done.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { eq, and
|
|
1
|
+
import { eq, and } from 'drizzle-orm';
|
|
2
2
|
import { getDb, schema } from '../db/index.js';
|
|
3
3
|
import { t, fmt } from '../i18n/index.js';
|
|
4
4
|
function getWeekStart(date) {
|
|
@@ -53,8 +53,8 @@ function groupByWeek(tasks, weeks) {
|
|
|
53
53
|
const weekEnd = new Date(weekStart);
|
|
54
54
|
weekEnd.setDate(weekEnd.getDate() + 7);
|
|
55
55
|
const weekTasks = tasks.filter(task => {
|
|
56
|
-
const
|
|
57
|
-
return
|
|
56
|
+
const completionDate = task.completedAt ?? task.updatedAt;
|
|
57
|
+
return completionDate >= weekStart && completionDate < weekEnd;
|
|
58
58
|
});
|
|
59
59
|
result.push({
|
|
60
60
|
weekStart,
|
|
@@ -69,7 +69,7 @@ function groupByDayOfWeek(tasks) {
|
|
|
69
69
|
for (let i = 0; i < 7; i++)
|
|
70
70
|
days.set(i, 0);
|
|
71
71
|
for (const task of tasks) {
|
|
72
|
-
const day = task.updatedAt.getDay();
|
|
72
|
+
const day = (task.completedAt ?? task.updatedAt).getDay();
|
|
73
73
|
days.set(day, (days.get(day) || 0) + 1);
|
|
74
74
|
}
|
|
75
75
|
return days;
|
|
@@ -95,9 +95,9 @@ function calculateAverageCompletionDays(tasks) {
|
|
|
95
95
|
let validCount = 0;
|
|
96
96
|
for (const task of tasks) {
|
|
97
97
|
const created = task.createdAt.getTime();
|
|
98
|
-
const
|
|
99
|
-
if (
|
|
100
|
-
totalMs +=
|
|
98
|
+
const completed = (task.completedAt ?? task.updatedAt).getTime();
|
|
99
|
+
if (completed > created) {
|
|
100
|
+
totalMs += completed - created;
|
|
101
101
|
validCount++;
|
|
102
102
|
}
|
|
103
103
|
}
|
|
@@ -138,11 +138,14 @@ export async function showInsights(weeks) {
|
|
|
138
138
|
startDate.setDate(startDate.getDate() - (weeks - 1) * 7);
|
|
139
139
|
const endDate = new Date(now);
|
|
140
140
|
endDate.setHours(23, 59, 59, 999);
|
|
141
|
-
// Query
|
|
142
|
-
const completedTasks = await db
|
|
141
|
+
// Query completed tasks (use completedAt with updatedAt fallback)
|
|
142
|
+
const completedTasks = (await db
|
|
143
143
|
.select()
|
|
144
144
|
.from(schema.tasks)
|
|
145
|
-
.where(and(eq(schema.tasks.status, 'done'),
|
|
145
|
+
.where(and(eq(schema.tasks.status, 'done'), eq(schema.tasks.isProject, false)))).filter(task => {
|
|
146
|
+
const completionDate = task.completedAt ?? task.updatedAt;
|
|
147
|
+
return completionDate >= startDate;
|
|
148
|
+
});
|
|
146
149
|
// Header
|
|
147
150
|
console.log();
|
|
148
151
|
console.log(l.title);
|
package/dist/commands/move.js
CHANGED
|
@@ -4,7 +4,7 @@ import { t, fmt } from '../i18n/index.js';
|
|
|
4
4
|
export async function moveTask(taskId, targetStatus, waitingFor) {
|
|
5
5
|
const db = getDb();
|
|
6
6
|
const i18n = t();
|
|
7
|
-
const validStatuses = ['inbox', 'next', 'waiting', 'someday'];
|
|
7
|
+
const validStatuses = ['inbox', 'next', 'waiting', 'someday', 'done'];
|
|
8
8
|
if (!validStatuses.includes(targetStatus)) {
|
|
9
9
|
console.error(fmt(i18n.commands.move.invalidStatus, { status: targetStatus }));
|
|
10
10
|
console.error(fmt(i18n.commands.move.validStatuses, { statuses: validStatuses.join(', ') }));
|
|
@@ -35,6 +35,7 @@ export async function moveTask(taskId, targetStatus, waitingFor) {
|
|
|
35
35
|
.set({
|
|
36
36
|
status: targetStatus,
|
|
37
37
|
waitingFor: targetStatus === 'waiting' ? waitingFor : null,
|
|
38
|
+
completedAt: targetStatus === 'done' ? new Date() : null,
|
|
38
39
|
updatedAt: new Date(),
|
|
39
40
|
})
|
|
40
41
|
.where(eq(schema.tasks.id, task.id));
|
package/dist/db/index.js
CHANGED
|
@@ -26,6 +26,7 @@ async function initializeRemoteSchema(tursoUrl, authToken) {
|
|
|
26
26
|
const hasContext = tableInfo.some(col => col.name === 'context');
|
|
27
27
|
const hasIsFocused = tableInfo.some(col => col.name === 'is_focused');
|
|
28
28
|
const hasEffort = tableInfo.some(col => col.name === 'effort');
|
|
29
|
+
const hasCompletedAt = tableInfo.some(col => col.name === 'completed_at');
|
|
29
30
|
if (!tableExists) {
|
|
30
31
|
// Fresh install: create new schema on remote
|
|
31
32
|
await remoteClient.execute(`
|
|
@@ -41,6 +42,7 @@ async function initializeRemoteSchema(tursoUrl, authToken) {
|
|
|
41
42
|
is_focused INTEGER NOT NULL DEFAULT 0,
|
|
42
43
|
effort TEXT,
|
|
43
44
|
due_date INTEGER,
|
|
45
|
+
completed_at INTEGER,
|
|
44
46
|
created_at INTEGER NOT NULL,
|
|
45
47
|
updated_at INTEGER NOT NULL
|
|
46
48
|
)
|
|
@@ -63,6 +65,11 @@ async function initializeRemoteSchema(tursoUrl, authToken) {
|
|
|
63
65
|
if (tableExists && !hasEffort) {
|
|
64
66
|
await remoteClient.execute("ALTER TABLE tasks ADD COLUMN effort TEXT");
|
|
65
67
|
}
|
|
68
|
+
// Migration: add completed_at column if missing
|
|
69
|
+
if (tableExists && !hasCompletedAt) {
|
|
70
|
+
await remoteClient.execute("ALTER TABLE tasks ADD COLUMN completed_at INTEGER");
|
|
71
|
+
await remoteClient.execute("UPDATE tasks SET completed_at = updated_at WHERE status = 'done'");
|
|
72
|
+
}
|
|
66
73
|
// Create comments table
|
|
67
74
|
await remoteClient.execute(`
|
|
68
75
|
CREATE TABLE IF NOT EXISTS comments (
|
|
@@ -86,6 +93,17 @@ async function initializeRemoteSchema(tursoUrl, authToken) {
|
|
|
86
93
|
updated_at INTEGER NOT NULL
|
|
87
94
|
)
|
|
88
95
|
`);
|
|
96
|
+
// Create operation_history table for crash-safe undo
|
|
97
|
+
await remoteClient.execute(`
|
|
98
|
+
CREATE TABLE IF NOT EXISTS operation_history (
|
|
99
|
+
id TEXT PRIMARY KEY,
|
|
100
|
+
command_type TEXT NOT NULL,
|
|
101
|
+
command_data TEXT NOT NULL,
|
|
102
|
+
executed_at INTEGER NOT NULL,
|
|
103
|
+
is_undone INTEGER NOT NULL DEFAULT 0
|
|
104
|
+
)
|
|
105
|
+
`);
|
|
106
|
+
await remoteClient.execute("CREATE INDEX IF NOT EXISTS idx_operation_history_executed_at ON operation_history(executed_at)");
|
|
89
107
|
}
|
|
90
108
|
finally {
|
|
91
109
|
remoteClient.close();
|
|
@@ -102,6 +120,7 @@ async function initializeLocalSchema() {
|
|
|
102
120
|
const hasContext = tableInfo.some(col => col.name === 'context');
|
|
103
121
|
const hasIsFocused = tableInfo.some(col => col.name === 'is_focused');
|
|
104
122
|
const hasEffort = tableInfo.some(col => col.name === 'effort');
|
|
123
|
+
const hasCompletedAt = tableInfo.some(col => col.name === 'completed_at');
|
|
105
124
|
const tableExists = tableInfo.length > 0;
|
|
106
125
|
if (tableExists && hasProjectId && !hasIsProject) {
|
|
107
126
|
// Migration: old schema -> new schema
|
|
@@ -136,6 +155,7 @@ async function initializeLocalSchema() {
|
|
|
136
155
|
is_focused INTEGER NOT NULL DEFAULT 0,
|
|
137
156
|
effort TEXT,
|
|
138
157
|
due_date INTEGER,
|
|
158
|
+
completed_at INTEGER,
|
|
139
159
|
created_at INTEGER NOT NULL,
|
|
140
160
|
updated_at INTEGER NOT NULL
|
|
141
161
|
)
|
|
@@ -158,6 +178,11 @@ async function initializeLocalSchema() {
|
|
|
158
178
|
if (tableExists && !hasEffort) {
|
|
159
179
|
await client.execute("ALTER TABLE tasks ADD COLUMN effort TEXT");
|
|
160
180
|
}
|
|
181
|
+
// Migration: add completed_at column if missing
|
|
182
|
+
if (tableExists && !hasCompletedAt) {
|
|
183
|
+
await client.execute("ALTER TABLE tasks ADD COLUMN completed_at INTEGER");
|
|
184
|
+
await client.execute("UPDATE tasks SET completed_at = updated_at WHERE status = 'done'");
|
|
185
|
+
}
|
|
161
186
|
// Create comments table
|
|
162
187
|
await client.execute(`
|
|
163
188
|
CREATE TABLE IF NOT EXISTS comments (
|
|
@@ -181,6 +206,17 @@ async function initializeLocalSchema() {
|
|
|
181
206
|
updated_at INTEGER NOT NULL
|
|
182
207
|
)
|
|
183
208
|
`);
|
|
209
|
+
// Create operation_history table for crash-safe undo
|
|
210
|
+
await client.execute(`
|
|
211
|
+
CREATE TABLE IF NOT EXISTS operation_history (
|
|
212
|
+
id TEXT PRIMARY KEY,
|
|
213
|
+
command_type TEXT NOT NULL,
|
|
214
|
+
command_data TEXT NOT NULL,
|
|
215
|
+
executed_at INTEGER NOT NULL,
|
|
216
|
+
is_undone INTEGER NOT NULL DEFAULT 0
|
|
217
|
+
)
|
|
218
|
+
`);
|
|
219
|
+
await client.execute("CREATE INDEX IF NOT EXISTS idx_operation_history_executed_at ON operation_history(executed_at)");
|
|
184
220
|
}
|
|
185
221
|
// DB 初期化
|
|
186
222
|
export async function initDb() {
|
package/dist/db/schema.d.ts
CHANGED
|
@@ -205,6 +205,23 @@ export declare const tasks: import("drizzle-orm/sqlite-core").SQLiteTableWithCol
|
|
|
205
205
|
identity: undefined;
|
|
206
206
|
generated: undefined;
|
|
207
207
|
}, {}, {}>;
|
|
208
|
+
completedAt: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
209
|
+
name: "completed_at";
|
|
210
|
+
tableName: "tasks";
|
|
211
|
+
dataType: "date";
|
|
212
|
+
columnType: "SQLiteTimestamp";
|
|
213
|
+
data: Date;
|
|
214
|
+
driverParam: number;
|
|
215
|
+
notNull: false;
|
|
216
|
+
hasDefault: false;
|
|
217
|
+
isPrimaryKey: false;
|
|
218
|
+
isAutoincrement: false;
|
|
219
|
+
hasRuntimeDefault: false;
|
|
220
|
+
enumValues: undefined;
|
|
221
|
+
baseColumn: never;
|
|
222
|
+
identity: undefined;
|
|
223
|
+
generated: undefined;
|
|
224
|
+
}, {}, {}>;
|
|
208
225
|
createdAt: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
209
226
|
name: "created_at";
|
|
210
227
|
tableName: "tasks";
|
|
@@ -482,3 +499,102 @@ export declare const pomodoroSessions: import("drizzle-orm/sqlite-core").SQLiteT
|
|
|
482
499
|
}>;
|
|
483
500
|
export type PomodoroSession = typeof pomodoroSessions.$inferSelect;
|
|
484
501
|
export type NewPomodoroSession = typeof pomodoroSessions.$inferInsert;
|
|
502
|
+
export declare const operationHistory: import("drizzle-orm/sqlite-core").SQLiteTableWithColumns<{
|
|
503
|
+
name: "operation_history";
|
|
504
|
+
schema: undefined;
|
|
505
|
+
columns: {
|
|
506
|
+
id: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
507
|
+
name: "id";
|
|
508
|
+
tableName: "operation_history";
|
|
509
|
+
dataType: "string";
|
|
510
|
+
columnType: "SQLiteText";
|
|
511
|
+
data: string;
|
|
512
|
+
driverParam: string;
|
|
513
|
+
notNull: true;
|
|
514
|
+
hasDefault: false;
|
|
515
|
+
isPrimaryKey: true;
|
|
516
|
+
isAutoincrement: false;
|
|
517
|
+
hasRuntimeDefault: false;
|
|
518
|
+
enumValues: [string, ...string[]];
|
|
519
|
+
baseColumn: never;
|
|
520
|
+
identity: undefined;
|
|
521
|
+
generated: undefined;
|
|
522
|
+
}, {}, {
|
|
523
|
+
length: number | undefined;
|
|
524
|
+
}>;
|
|
525
|
+
commandType: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
526
|
+
name: "command_type";
|
|
527
|
+
tableName: "operation_history";
|
|
528
|
+
dataType: "string";
|
|
529
|
+
columnType: "SQLiteText";
|
|
530
|
+
data: string;
|
|
531
|
+
driverParam: string;
|
|
532
|
+
notNull: true;
|
|
533
|
+
hasDefault: false;
|
|
534
|
+
isPrimaryKey: false;
|
|
535
|
+
isAutoincrement: false;
|
|
536
|
+
hasRuntimeDefault: false;
|
|
537
|
+
enumValues: [string, ...string[]];
|
|
538
|
+
baseColumn: never;
|
|
539
|
+
identity: undefined;
|
|
540
|
+
generated: undefined;
|
|
541
|
+
}, {}, {
|
|
542
|
+
length: number | undefined;
|
|
543
|
+
}>;
|
|
544
|
+
commandData: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
545
|
+
name: "command_data";
|
|
546
|
+
tableName: "operation_history";
|
|
547
|
+
dataType: "string";
|
|
548
|
+
columnType: "SQLiteText";
|
|
549
|
+
data: string;
|
|
550
|
+
driverParam: string;
|
|
551
|
+
notNull: true;
|
|
552
|
+
hasDefault: false;
|
|
553
|
+
isPrimaryKey: false;
|
|
554
|
+
isAutoincrement: false;
|
|
555
|
+
hasRuntimeDefault: false;
|
|
556
|
+
enumValues: [string, ...string[]];
|
|
557
|
+
baseColumn: never;
|
|
558
|
+
identity: undefined;
|
|
559
|
+
generated: undefined;
|
|
560
|
+
}, {}, {
|
|
561
|
+
length: number | undefined;
|
|
562
|
+
}>;
|
|
563
|
+
executedAt: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
564
|
+
name: "executed_at";
|
|
565
|
+
tableName: "operation_history";
|
|
566
|
+
dataType: "date";
|
|
567
|
+
columnType: "SQLiteTimestamp";
|
|
568
|
+
data: Date;
|
|
569
|
+
driverParam: number;
|
|
570
|
+
notNull: true;
|
|
571
|
+
hasDefault: false;
|
|
572
|
+
isPrimaryKey: false;
|
|
573
|
+
isAutoincrement: false;
|
|
574
|
+
hasRuntimeDefault: false;
|
|
575
|
+
enumValues: undefined;
|
|
576
|
+
baseColumn: never;
|
|
577
|
+
identity: undefined;
|
|
578
|
+
generated: undefined;
|
|
579
|
+
}, {}, {}>;
|
|
580
|
+
isUndone: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
581
|
+
name: "is_undone";
|
|
582
|
+
tableName: "operation_history";
|
|
583
|
+
dataType: "boolean";
|
|
584
|
+
columnType: "SQLiteBoolean";
|
|
585
|
+
data: boolean;
|
|
586
|
+
driverParam: number;
|
|
587
|
+
notNull: true;
|
|
588
|
+
hasDefault: true;
|
|
589
|
+
isPrimaryKey: false;
|
|
590
|
+
isAutoincrement: false;
|
|
591
|
+
hasRuntimeDefault: false;
|
|
592
|
+
enumValues: undefined;
|
|
593
|
+
baseColumn: never;
|
|
594
|
+
identity: undefined;
|
|
595
|
+
generated: undefined;
|
|
596
|
+
}, {}, {}>;
|
|
597
|
+
};
|
|
598
|
+
dialect: "sqlite";
|
|
599
|
+
}>;
|
|
600
|
+
export type OperationHistory = typeof operationHistory.$inferSelect;
|
package/dist/db/schema.js
CHANGED
|
@@ -11,6 +11,7 @@ export const tasks = sqliteTable('tasks', {
|
|
|
11
11
|
isFocused: integer('is_focused', { mode: 'boolean' }).notNull().default(false),
|
|
12
12
|
effort: text('effort'),
|
|
13
13
|
dueDate: integer('due_date', { mode: 'timestamp' }),
|
|
14
|
+
completedAt: integer('completed_at', { mode: 'timestamp' }),
|
|
14
15
|
createdAt: integer('created_at', { mode: 'timestamp' }).notNull(),
|
|
15
16
|
updatedAt: integer('updated_at', { mode: 'timestamp' }).notNull(),
|
|
16
17
|
});
|
|
@@ -32,3 +33,10 @@ export const pomodoroSessions = sqliteTable('pomodoro_sessions', {
|
|
|
32
33
|
completedCount: integer('completed_count').notNull().default(0),
|
|
33
34
|
updatedAt: integer('updated_at', { mode: 'timestamp' }).notNull(),
|
|
34
35
|
});
|
|
36
|
+
export const operationHistory = sqliteTable('operation_history', {
|
|
37
|
+
id: text('id').primaryKey(),
|
|
38
|
+
commandType: text('command_type').notNull(),
|
|
39
|
+
commandData: text('command_data').notNull(), // JSON serialized
|
|
40
|
+
executedAt: integer('executed_at', { mode: 'timestamp' }).notNull(),
|
|
41
|
+
isUndone: integer('is_undone', { mode: 'boolean' }).notNull().default(false),
|
|
42
|
+
});
|
package/dist/i18n/en.d.ts
CHANGED
|
@@ -243,6 +243,7 @@ export declare const en: {
|
|
|
243
243
|
taskDetailTitle: string;
|
|
244
244
|
taskDetailFooter: string;
|
|
245
245
|
taskDetailStatus: string;
|
|
246
|
+
taskDetailCompletedAt: string;
|
|
246
247
|
deleteConfirm: string;
|
|
247
248
|
deleted: string;
|
|
248
249
|
deleteCancelled: string;
|
|
@@ -670,6 +671,7 @@ export type TuiTranslations = {
|
|
|
670
671
|
taskDetailTitle: string;
|
|
671
672
|
taskDetailFooter: string;
|
|
672
673
|
taskDetailStatus: string;
|
|
674
|
+
taskDetailCompletedAt?: string;
|
|
673
675
|
deleteConfirm: string;
|
|
674
676
|
deleted: string;
|
|
675
677
|
deleteCancelled: string;
|
package/dist/i18n/en.js
CHANGED
|
@@ -256,6 +256,7 @@ export const en = {
|
|
|
256
256
|
taskDetailTitle: 'Task Details',
|
|
257
257
|
taskDetailFooter: 'j/k=select i=comment d=delete P=link b/Esc=back',
|
|
258
258
|
taskDetailStatus: 'Status',
|
|
259
|
+
taskDetailCompletedAt: 'Completed',
|
|
259
260
|
deleteConfirm: 'Delete "{title}"? (y/n)',
|
|
260
261
|
deleted: 'Deleted: "{title}"',
|
|
261
262
|
deleteCancelled: 'Delete cancelled',
|
package/dist/i18n/ja.js
CHANGED
|
@@ -256,6 +256,7 @@ export const ja = {
|
|
|
256
256
|
taskDetailTitle: 'タスク詳細',
|
|
257
257
|
taskDetailFooter: 'j/k=選択 i=コメント d=削除 P=紐づけ b/Esc=戻る',
|
|
258
258
|
taskDetailStatus: 'ステータス',
|
|
259
|
+
taskDetailCompletedAt: '完了日',
|
|
259
260
|
deleteConfirm: '「{title}」を削除しますか? (y/n)',
|
|
260
261
|
deleted: '削除しました: 「{title}」',
|
|
261
262
|
deleteCancelled: '削除をキャンセルしました',
|
package/dist/ui/App.js
CHANGED
|
@@ -407,6 +407,7 @@ function AppContent({ onOpenSettings }) {
|
|
|
407
407
|
toStatus: 'done',
|
|
408
408
|
fromWaitingFor: task.waitingFor,
|
|
409
409
|
toWaitingFor: null,
|
|
410
|
+
fromCompletedAt: task.completedAt,
|
|
410
411
|
description: fmt(i18n.tui.completed, { title: task.title }),
|
|
411
412
|
});
|
|
412
413
|
await history.execute(command);
|
|
@@ -420,6 +421,7 @@ function AppContent({ onOpenSettings }) {
|
|
|
420
421
|
toStatus: status,
|
|
421
422
|
fromWaitingFor: task.waitingFor,
|
|
422
423
|
toWaitingFor: null,
|
|
424
|
+
fromCompletedAt: task.completedAt,
|
|
423
425
|
description: fmt(i18n.tui.movedTo, { title: task.title, status: i18n.status[status] }),
|
|
424
426
|
});
|
|
425
427
|
await history.execute(command);
|
|
@@ -433,6 +435,7 @@ function AppContent({ onOpenSettings }) {
|
|
|
433
435
|
toStatus: 'waiting',
|
|
434
436
|
fromWaitingFor: task.waitingFor,
|
|
435
437
|
toWaitingFor: waitingFor.trim(),
|
|
438
|
+
fromCompletedAt: task.completedAt,
|
|
436
439
|
description: fmt(i18n.tui.movedToWaiting, { title: task.title, person: waitingFor.trim() }),
|
|
437
440
|
});
|
|
438
441
|
await history.execute(command);
|
|
@@ -1109,7 +1112,7 @@ function AppContent({ onOpenSettings }) {
|
|
|
1109
1112
|
return;
|
|
1110
1113
|
}
|
|
1111
1114
|
// Move to inbox
|
|
1112
|
-
if (input === 'i' && currentTasks.length > 0 && currentTab !== 'inbox' && currentTab !== 'projects'
|
|
1115
|
+
if (input === 'i' && currentTasks.length > 0 && currentTab !== 'inbox' && currentTab !== 'projects') {
|
|
1113
1116
|
const task = currentTasks[selectedTaskIndex];
|
|
1114
1117
|
moveTaskToStatus(task, 'inbox').then(() => {
|
|
1115
1118
|
if (selectedTaskIndex >= currentTasks.length - 1) {
|
|
@@ -1188,17 +1191,17 @@ function AppContent({ onOpenSettings }) {
|
|
|
1188
1191
|
const count = tasks[tab].length;
|
|
1189
1192
|
const label = `${index + 1}:${getTabLabel(tab)}(${count})`;
|
|
1190
1193
|
return (_jsx(Box, { marginRight: 1, children: _jsx(Text, { color: isActive ? theme.colors.textSelected : theme.colors.textMuted, bold: isActive, inverse: isActive && theme.style.tabActiveInverse, children: formatTabLabel(` ${label} `, isActive) }) }, tab));
|
|
1191
|
-
}) }), mode === 'project-detail' && selectedProject && (_jsxs(_Fragment, { children: [_jsxs(Box, { marginBottom: 1, children: [_jsxs(Text, { color: theme.colors.accent, bold: true, children: [theme.name === 'modern' ? '📁 ' : '>> ', selectedProject.title] }), _jsxs(Text, { color: theme.colors.textMuted, children: [" (Esc/b: ", i18n.tui.back || 'back', ")"] })] }), _jsx(Box, { marginBottom: 1, children: _jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.projectTasks || 'Tasks', " (", projectTasks.length, ")"] }) })] })), (mode === 'task-detail' || mode === 'add-comment') && selectedTask && (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { marginBottom: 1, children: [_jsxs(Text, { color: theme.colors.accent, bold: true, children: [theme.name === 'modern' ? '📋 ' : '>> ', i18n.tui.taskDetailTitle || 'Task Details'] }), _jsxs(Text, { color: theme.colors.textMuted, children: [" (Esc/b: ", i18n.tui.back || 'back', ", ", i18n.tui.commentHint || 'i: add comment', ")"] })] }), _jsxs(Box, { flexDirection: "column", borderStyle: theme.borders.list, borderColor: theme.colors.border, paddingX: 1, paddingY: 1, marginBottom: 1, children: [_jsx(Text, { color: theme.colors.text, bold: true, children: selectedTask.title }), selectedTask.description && (_jsx(Text, { color: theme.colors.textMuted, children: selectedTask.description })), _jsxs(Box, { marginTop: 1, children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.taskDetailStatus, ": "] }), _jsxs(Text, { color: theme.colors.accent, children: [i18n.status[selectedTask.status], selectedTask.waitingFor && ` (${selectedTask.waitingFor})`] })] }), _jsxs(Box, { children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.context?.label || 'Context', ": "] }), _jsx(Text, { color: theme.colors.accent, children: selectedTask.context ? `@${selectedTask.context}` : (i18n.tui.context?.none || 'No context') })] }), _jsxs(Box, { children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.effort?.label || 'Effort', ": "] }), _jsx(Text, { color: theme.colors.accent, children: selectedTask.effort ? (i18n.tui.effort?.[selectedTask.effort] || selectedTask.effort) : '-' })] }), _jsxs(Box, { children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.focus?.label || 'Focus', ": "] }), _jsx(Text, { color: theme.colors.accent, children: selectedTask.isFocused ? '★' : '-' })] })] }), _jsx(Box, { marginBottom: 1, children: _jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.comments || 'Comments', " (", taskComments.length, ")"] }) }), _jsx(Box, { flexDirection: "column", borderStyle: theme.borders.list, borderColor: theme.colors.border, paddingX: 1, paddingY: 1, minHeight: 5, children: taskComments.length === 0 ? (_jsx(Text, { color: theme.colors.textMuted, italic: true, children: i18n.tui.noComments || 'No comments yet' })) : (taskComments.map((comment, index) => {
|
|
1194
|
+
}) }), mode === 'project-detail' && selectedProject && (_jsxs(_Fragment, { children: [_jsxs(Box, { marginBottom: 1, children: [_jsxs(Text, { color: theme.colors.accent, bold: true, children: [theme.name === 'modern' ? '📁 ' : '>> ', selectedProject.title] }), _jsxs(Text, { color: theme.colors.textMuted, children: [" (Esc/b: ", i18n.tui.back || 'back', ")"] })] }), _jsx(Box, { marginBottom: 1, children: _jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.projectTasks || 'Tasks', " (", projectTasks.length, ")"] }) })] })), (mode === 'task-detail' || mode === 'add-comment') && selectedTask && (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { marginBottom: 1, children: [_jsxs(Text, { color: theme.colors.accent, bold: true, children: [theme.name === 'modern' ? '📋 ' : '>> ', i18n.tui.taskDetailTitle || 'Task Details'] }), _jsxs(Text, { color: theme.colors.textMuted, children: [" (Esc/b: ", i18n.tui.back || 'back', ", ", i18n.tui.commentHint || 'i: add comment', ")"] })] }), _jsxs(Box, { flexDirection: "column", borderStyle: theme.borders.list, borderColor: theme.colors.border, paddingX: 1, paddingY: 1, marginBottom: 1, children: [_jsx(Text, { color: theme.colors.text, bold: true, children: selectedTask.title }), selectedTask.description && (_jsx(Text, { color: theme.colors.textMuted, children: selectedTask.description })), _jsxs(Box, { marginTop: 1, children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.taskDetailStatus, ": "] }), _jsxs(Text, { color: theme.colors.accent, children: [i18n.status[selectedTask.status], selectedTask.waitingFor && ` (${selectedTask.waitingFor})`] })] }), _jsxs(Box, { children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.context?.label || 'Context', ": "] }), _jsx(Text, { color: theme.colors.accent, children: selectedTask.context ? `@${selectedTask.context}` : (i18n.tui.context?.none || 'No context') })] }), _jsxs(Box, { children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.effort?.label || 'Effort', ": "] }), _jsx(Text, { color: theme.colors.accent, children: selectedTask.effort ? (i18n.tui.effort?.[selectedTask.effort] || selectedTask.effort) : '-' })] }), _jsxs(Box, { children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.focus?.label || 'Focus', ": "] }), _jsx(Text, { color: theme.colors.accent, children: selectedTask.isFocused ? '★' : '-' })] }), selectedTask.status === 'done' && (_jsxs(Box, { children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.taskDetailCompletedAt || 'Completed', ": "] }), _jsx(Text, { color: theme.colors.accent, children: (selectedTask.completedAt ?? selectedTask.updatedAt).toLocaleString() })] }))] }), _jsx(Box, { marginBottom: 1, children: _jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.comments || 'Comments', " (", taskComments.length, ")"] }) }), _jsx(Box, { flexDirection: "column", borderStyle: theme.borders.list, borderColor: theme.colors.border, paddingX: 1, paddingY: 1, minHeight: 5, children: taskComments.length === 0 ? (_jsx(Text, { color: theme.colors.textMuted, italic: true, children: i18n.tui.noComments || 'No comments yet' })) : (taskComments.map((comment, index) => {
|
|
1192
1195
|
const isSelected = index === selectedCommentIndex && mode === 'task-detail';
|
|
1193
1196
|
return (_jsxs(Box, { flexDirection: "row", marginBottom: 1, children: [_jsx(Text, { color: isSelected ? theme.colors.textSelected : theme.colors.textMuted, children: isSelected ? theme.style.selectedPrefix : theme.style.unselectedPrefix }), _jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: theme.colors.textMuted, children: ["[", comment.createdAt.toLocaleString(), "]"] }), _jsx(Text, { color: isSelected ? theme.colors.textSelected : theme.colors.text, bold: isSelected, children: comment.content })] })] }, comment.id));
|
|
1194
1197
|
})) }), mode === 'add-comment' && (_jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: theme.colors.secondary, bold: true, children: i18n.tui.addComment || 'New comment: ' }), _jsx(TextInput, { value: inputValue, onChange: setInputValue, onSubmit: handleInputSubmit, placeholder: "" }), _jsxs(Text, { color: theme.colors.textMuted, children: [" ", i18n.tui.inputHelp] })] }))] })), mode === 'search' && searchQuery && (_jsx(SearchResults, { results: searchResults, selectedIndex: searchResultIndex, query: searchQuery })), mode !== 'task-detail' && mode !== 'add-comment' && mode !== 'search' && (theme.uiStyle === 'titled-box' ? (_jsx(TitledBox, { title: mode === 'project-detail' && selectedProject ? selectedProject.title : getTabLabel(currentTab), borderColor: theme.colors.border, minHeight: 10, children: currentTasks.length === 0 ? (_jsx(Text, { color: theme.colors.textMuted, italic: true, children: i18n.tui.noTasks })) : (currentTasks.map((task, index) => {
|
|
1195
1198
|
const parentProject = getParentProject(task.parentId);
|
|
1196
1199
|
const progress = currentTab === 'projects' ? projectProgress[task.id] : undefined;
|
|
1197
|
-
return (_jsx(TaskItem, { task: task, isSelected: index === selectedTaskIndex, projectName: parentProject?.title, progress: progress }, task.id));
|
|
1200
|
+
return (_jsx(TaskItem, { task: task, isSelected: index === selectedTaskIndex, projectName: parentProject?.title, progress: progress, showStatus: mode === 'project-detail' }, task.id));
|
|
1198
1201
|
})) })) : (_jsx(Box, { flexDirection: "column", borderStyle: theme.borders.list, borderColor: theme.colors.border, paddingX: 1, paddingY: 1, minHeight: 10, children: currentTasks.length === 0 ? (_jsx(Text, { color: theme.colors.textMuted, italic: true, children: i18n.tui.noTasks })) : (currentTasks.map((task, index) => {
|
|
1199
1202
|
const parentProject = getParentProject(task.parentId);
|
|
1200
1203
|
const progress = currentTab === 'projects' ? projectProgress[task.id] : undefined;
|
|
1201
|
-
return (_jsx(TaskItem, { task: task, isSelected: index === selectedTaskIndex, projectName: parentProject?.title, progress: progress }, task.id));
|
|
1204
|
+
return (_jsx(TaskItem, { task: task, isSelected: index === selectedTaskIndex, projectName: parentProject?.title, progress: progress, showStatus: mode === 'project-detail' }, task.id));
|
|
1202
1205
|
})) }))), (mode === 'add' || mode === 'add-to-project') && (_jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: theme.colors.secondary, bold: true, children: mode === 'add-to-project' && selectedProject
|
|
1203
1206
|
? `${i18n.tui.newTask}[${selectedProject.title}] `
|
|
1204
1207
|
: i18n.tui.newTask }), _jsx(TextInput, { value: inputValue, onChange: setInputValue, onSubmit: handleInputSubmit, placeholder: i18n.tui.placeholder }), _jsxs(Text, { color: theme.colors.textMuted, children: [" ", i18n.tui.inputHelp] })] })), mode === 'move-to-waiting' && taskToWaiting && (_jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: theme.colors.secondary, bold: true, children: i18n.tui.waitingFor }), _jsx(TextInput, { value: inputValue, onChange: setInputValue, onSubmit: handleInputSubmit, placeholder: "" }), _jsxs(Text, { color: theme.colors.textMuted, children: [" ", i18n.tui.inputHelp] })] })), mode === 'select-project' && taskToLink && (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.selectProject || 'Select project for', ": ", taskToLink.title] }), _jsx(Box, { flexDirection: "column", marginTop: 1, borderStyle: theme.borders.list, borderColor: theme.colors.borderActive, paddingX: 1, children: tasks.projects.map((project, index) => (_jsxs(Text, { color: index === projectSelectIndex ? theme.colors.textSelected : theme.colors.text, bold: index === projectSelectIndex, children: [index === projectSelectIndex ? theme.style.selectedPrefix : theme.style.unselectedPrefix, project.title] }, project.id))) }), _jsx(Text, { color: theme.colors.textMuted, children: i18n.tui.selectProjectHelp || 'j/k: select, Enter: confirm, Esc: cancel' })] })), mode === 'context-filter' && (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { color: theme.colors.secondary, bold: true, children: i18n.tui.context?.filter || 'Filter by context' }), _jsx(Box, { flexDirection: "column", marginTop: 1, borderStyle: theme.borders.list, borderColor: theme.colors.borderActive, paddingX: 1, children: ['all', 'none', ...availableContexts].map((ctx, index) => {
|
|
@@ -347,6 +347,7 @@ export function GtdDQ({ onOpenSettings }) {
|
|
|
347
347
|
toStatus: 'done',
|
|
348
348
|
fromWaitingFor: task.waitingFor,
|
|
349
349
|
toWaitingFor: null,
|
|
350
|
+
fromCompletedAt: task.completedAt,
|
|
350
351
|
description: fmt(i18n.tui.completed, { title: task.title }),
|
|
351
352
|
});
|
|
352
353
|
await history.execute(command);
|
|
@@ -360,6 +361,7 @@ export function GtdDQ({ onOpenSettings }) {
|
|
|
360
361
|
toStatus: status,
|
|
361
362
|
fromWaitingFor: task.waitingFor,
|
|
362
363
|
toWaitingFor: null,
|
|
364
|
+
fromCompletedAt: task.completedAt,
|
|
363
365
|
description: fmt(i18n.tui.movedTo, { title: task.title, status: i18n.status[status] }),
|
|
364
366
|
});
|
|
365
367
|
await history.execute(command);
|
|
@@ -373,6 +375,7 @@ export function GtdDQ({ onOpenSettings }) {
|
|
|
373
375
|
toStatus: 'waiting',
|
|
374
376
|
fromWaitingFor: task.waitingFor,
|
|
375
377
|
toWaitingFor: waitingFor.trim(),
|
|
378
|
+
fromCompletedAt: task.completedAt,
|
|
376
379
|
description: fmt(i18n.tui.movedToWaiting, { title: task.title, person: waitingFor.trim() }),
|
|
377
380
|
});
|
|
378
381
|
await history.execute(command);
|
|
@@ -1176,7 +1179,7 @@ export function GtdDQ({ onOpenSettings }) {
|
|
|
1176
1179
|
}) })] })) : mode === 'set-context' && currentTasks.length > 0 ? (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.context?.setContext || 'Set context for', ": ", currentTasks[selectedTaskIndex]?.title] }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: ['clear', ...availableContexts, 'new'].map((ctx, index) => {
|
|
1177
1180
|
const label = ctx === 'clear' ? (i18n.tui.context?.none || 'Clear context') : ctx === 'new' ? (i18n.tui.context?.addNew || 'New context...') : `@${ctx}`;
|
|
1178
1181
|
return (_jsxs(Text, { color: index === contextSelectIndex ? theme.colors.textSelected : theme.colors.text, bold: index === contextSelectIndex, children: [index === contextSelectIndex ? '▶ ' : ' ', label] }, ctx));
|
|
1179
|
-
}) })] })) : mode === 'set-effort' && getCurrentTask() ? (_jsx(Box, { flexDirection: "column", children: _jsx(TitledBoxInline, { title: i18n.tui.effort?.set || 'Set Effort', width: Math.min(40, terminalWidth - 4), minHeight: EFFORT_OPTIONS.length, isActive: true, children: EFFORT_OPTIONS.map((option, index) => (_jsxs(Text, { color: index === effortSelectIndex ? theme.colors.textSelected : theme.colors.text, bold: index === effortSelectIndex, children: [index === effortSelectIndex ? '▶ ' : ' ', option.label] }, option.label))) }) })) : mode === 'select-project' && taskToLink ? (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.selectProject || 'Select project', ": ", taskToLink.title] }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: tasks.projects.map((project, index) => (_jsxs(Text, { color: index === projectSelectIndex ? theme.colors.textSelected : theme.colors.text, bold: index === projectSelectIndex, children: [index === projectSelectIndex ? '▶ ' : ' ', project.title] }, project.id))) })] })) : mode === 'confirm-delete' && taskToDelete ? (_jsx(Box, { flexDirection: "column", children: _jsx(Text, { color: theme.colors.accent, bold: true, children: fmt(i18n.tui.deleteConfirm || 'Delete "{title}"? (y/n)', { title: taskToDelete.title }) }) })) : (mode === 'task-detail' || mode === 'add-comment') && selectedTask ? (_jsxs(Box, { flexDirection: "column", children: [_jsxs(TitledBoxInline, { title: i18n.tui.taskDetailTitle || 'Task Details', width: terminalWidth - 4, minHeight: 4, isActive: true, children: [_jsx(Text, { color: theme.colors.text, bold: true, children: selectedTask.title }), selectedTask.description && (_jsx(Text, { color: theme.colors.textMuted, children: selectedTask.description })), _jsxs(Box, { children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.taskDetailStatus || 'Status', ": "] }), _jsxs(Text, { color: theme.colors.accent, children: [i18n.status[selectedTask.status], selectedTask.waitingFor && ` (${selectedTask.waitingFor})`] })] }), _jsxs(Box, { children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.context?.label || 'Context', ": "] }), _jsx(Text, { color: theme.colors.accent, children: selectedTask.context ? `@${selectedTask.context}` : (i18n.tui.context?.none || 'No context') })] }), _jsxs(Box, { children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.effort?.label || 'Effort', ": "] }), _jsx(Text, { color: theme.colors.accent, children: selectedTask.effort ? (i18n.tui.effort?.[selectedTask.effort] || selectedTask.effort) : '-' })] }), _jsxs(Box, { children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.focus?.label || 'Focus', ": "] }), _jsx(Text, { color: theme.colors.accent, children: selectedTask.isFocused ? '★' : '-' })] })] }), _jsx(Box, { marginTop: 1, children: _jsx(TitledBoxInline, { title: `${i18n.tui.comments || 'Comments'} (${taskComments.length})`, width: terminalWidth - 4, minHeight: 5, isActive: mode === 'task-detail', children: taskComments.length === 0 ? (_jsx(Text, { color: theme.colors.textMuted, italic: true, children: i18n.tui.noComments || 'No comments yet' })) : (taskComments.map((comment, index) => {
|
|
1182
|
+
}) })] })) : mode === 'set-effort' && getCurrentTask() ? (_jsx(Box, { flexDirection: "column", children: _jsx(TitledBoxInline, { title: i18n.tui.effort?.set || 'Set Effort', width: Math.min(40, terminalWidth - 4), minHeight: EFFORT_OPTIONS.length, isActive: true, children: EFFORT_OPTIONS.map((option, index) => (_jsxs(Text, { color: index === effortSelectIndex ? theme.colors.textSelected : theme.colors.text, bold: index === effortSelectIndex, children: [index === effortSelectIndex ? '▶ ' : ' ', option.label] }, option.label))) }) })) : mode === 'select-project' && taskToLink ? (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.selectProject || 'Select project', ": ", taskToLink.title] }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: tasks.projects.map((project, index) => (_jsxs(Text, { color: index === projectSelectIndex ? theme.colors.textSelected : theme.colors.text, bold: index === projectSelectIndex, children: [index === projectSelectIndex ? '▶ ' : ' ', project.title] }, project.id))) })] })) : mode === 'confirm-delete' && taskToDelete ? (_jsx(Box, { flexDirection: "column", children: _jsx(Text, { color: theme.colors.accent, bold: true, children: fmt(i18n.tui.deleteConfirm || 'Delete "{title}"? (y/n)', { title: taskToDelete.title }) }) })) : (mode === 'task-detail' || mode === 'add-comment') && selectedTask ? (_jsxs(Box, { flexDirection: "column", children: [_jsxs(TitledBoxInline, { title: i18n.tui.taskDetailTitle || 'Task Details', width: terminalWidth - 4, minHeight: 4, isActive: true, children: [_jsx(Text, { color: theme.colors.text, bold: true, children: selectedTask.title }), selectedTask.description && (_jsx(Text, { color: theme.colors.textMuted, children: selectedTask.description })), _jsxs(Box, { children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.taskDetailStatus || 'Status', ": "] }), _jsxs(Text, { color: theme.colors.accent, children: [i18n.status[selectedTask.status], selectedTask.waitingFor && ` (${selectedTask.waitingFor})`] })] }), _jsxs(Box, { children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.context?.label || 'Context', ": "] }), _jsx(Text, { color: theme.colors.accent, children: selectedTask.context ? `@${selectedTask.context}` : (i18n.tui.context?.none || 'No context') })] }), _jsxs(Box, { children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.effort?.label || 'Effort', ": "] }), _jsx(Text, { color: theme.colors.accent, children: selectedTask.effort ? (i18n.tui.effort?.[selectedTask.effort] || selectedTask.effort) : '-' })] }), _jsxs(Box, { children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.focus?.label || 'Focus', ": "] }), _jsx(Text, { color: theme.colors.accent, children: selectedTask.isFocused ? '★' : '-' })] }), selectedTask.status === 'done' && (_jsxs(Box, { children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.taskDetailCompletedAt || 'Completed', ": "] }), _jsx(Text, { color: theme.colors.accent, children: (selectedTask.completedAt ?? selectedTask.updatedAt).toLocaleString() })] }))] }), _jsx(Box, { marginTop: 1, children: _jsx(TitledBoxInline, { title: `${i18n.tui.comments || 'Comments'} (${taskComments.length})`, width: terminalWidth - 4, minHeight: 5, isActive: mode === 'task-detail', children: taskComments.length === 0 ? (_jsx(Text, { color: theme.colors.textMuted, italic: true, children: i18n.tui.noComments || 'No comments yet' })) : (taskComments.map((comment, index) => {
|
|
1180
1183
|
const isSelected = index === selectedCommentIndex && mode === 'task-detail';
|
|
1181
1184
|
return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Text, { color: theme.colors.textMuted, children: [isSelected ? '▶ ' : ' ', "[", comment.createdAt.toLocaleString(), "]"] }), _jsxs(Text, { color: isSelected ? theme.colors.textSelected : theme.colors.text, bold: isSelected, children: [' ', comment.content] })] }, comment.id));
|
|
1182
1185
|
})) }) })] })) : (_jsxs(Box, { flexDirection: "row", children: [_jsx(Box, { marginRight: 2, children: _jsx(TitledBoxInline, { title: mode === 'project-detail' && selectedProject ? selectedProject.title : 'GTD', width: leftPaneWidth, minHeight: 8, isActive: paneFocus === 'tabs' && mode !== 'project-detail', children: mode === 'project-detail' ? (_jsx(Text, { color: theme.colors.textMuted, children: "\u2190 Esc/b: back" })) : (TABS.map((tab, index) => {
|
|
@@ -293,6 +293,7 @@ export function GtdMario({ onOpenSettings }) {
|
|
|
293
293
|
toStatus: 'done',
|
|
294
294
|
fromWaitingFor: task.waitingFor,
|
|
295
295
|
toWaitingFor: null,
|
|
296
|
+
fromCompletedAt: task.completedAt,
|
|
296
297
|
description: fmt(i18n.tui.completed, { title: task.title }),
|
|
297
298
|
});
|
|
298
299
|
await history.execute(command);
|
|
@@ -306,6 +307,7 @@ export function GtdMario({ onOpenSettings }) {
|
|
|
306
307
|
toStatus: status,
|
|
307
308
|
fromWaitingFor: task.waitingFor,
|
|
308
309
|
toWaitingFor: null,
|
|
310
|
+
fromCompletedAt: task.completedAt,
|
|
309
311
|
description: fmt(i18n.tui.movedTo, { title: task.title, status: i18n.status[status] }),
|
|
310
312
|
});
|
|
311
313
|
await history.execute(command);
|
|
@@ -319,6 +321,7 @@ export function GtdMario({ onOpenSettings }) {
|
|
|
319
321
|
toStatus: 'waiting',
|
|
320
322
|
fromWaitingFor: task.waitingFor,
|
|
321
323
|
toWaitingFor: waitingFor.trim(),
|
|
324
|
+
fromCompletedAt: task.completedAt,
|
|
322
325
|
description: fmt(i18n.tui.movedToWaiting, { title: task.title, person: waitingFor.trim() }),
|
|
323
326
|
});
|
|
324
327
|
await history.execute(command);
|
|
@@ -1070,7 +1073,7 @@ export function GtdMario({ onOpenSettings }) {
|
|
|
1070
1073
|
}) })] })) : mode === 'set-context' && currentTasks.length > 0 ? (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.context?.setContext || 'Set context for', ": ", currentTasks[selectedTaskIndex]?.title] }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: ['clear', ...availableContexts, 'new'].map((ctx, index) => {
|
|
1071
1074
|
const label = ctx === 'clear' ? (i18n.tui.context?.none || 'Clear context') : ctx === 'new' ? (i18n.tui.context?.addNew || 'New context...') : `@${ctx}`;
|
|
1072
1075
|
return (_jsxs(Text, { color: index === contextSelectIndex ? theme.colors.textSelected : theme.colors.text, bold: index === contextSelectIndex, children: [index === contextSelectIndex ? '🍄 ' : ' ', label] }, ctx));
|
|
1073
|
-
}) })] })) : mode === 'set-effort' && currentTasks.length > 0 ? (_jsx(Box, { flexDirection: "column", children: _jsxs(MarioBoxInline, { title: i18n.tui.effort?.set || 'Set Effort', width: terminalWidth - 4, minHeight: 4, isActive: true, children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.effort?.set || 'Set effort for', ": ", currentTasks[selectedTaskIndex]?.title] }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: EFFORT_OPTIONS.map((opt, index) => (_jsxs(Text, { color: index === effortSelectIndex ? theme.colors.textSelected : theme.colors.text, bold: index === effortSelectIndex, children: [index === effortSelectIndex ? '🍄 ' : ' ', opt.label] }, opt.value || 'clear'))) })] }) })) : mode === 'select-project' && taskToLink ? (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.selectProject || 'Select project', ": ", taskToLink.title] }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: tasks.projects.map((project, index) => (_jsxs(Text, { color: index === projectSelectIndex ? theme.colors.textSelected : theme.colors.text, bold: index === projectSelectIndex, children: [index === projectSelectIndex ? '🍄 ' : ' ', project.title] }, project.id))) })] })) : mode === 'confirm-delete' && taskToDelete ? (_jsx(Box, { flexDirection: "column", children: _jsx(Text, { color: theme.colors.accent, bold: true, children: fmt(i18n.tui.deleteConfirm || 'Delete "{title}"? (y/n)', { title: taskToDelete.title }) }) })) : (mode === 'task-detail' || mode === 'add-comment') && selectedTask ? (_jsxs(Box, { flexDirection: "column", children: [_jsxs(MarioBoxInline, { title: i18n.tui.taskDetailTitle || 'Task Details', width: terminalWidth - 4, minHeight: 4, isActive: true, children: [_jsx(Text, { color: theme.colors.text, bold: true, children: selectedTask.title }), selectedTask.description && (_jsx(Text, { color: theme.colors.textMuted, children: selectedTask.description })), _jsxs(Box, { children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.taskDetailStatus || 'Status', ": "] }), _jsxs(Text, { color: theme.colors.accent, children: [i18n.status[selectedTask.status], selectedTask.waitingFor && ` (${selectedTask.waitingFor})`] })] }), _jsxs(Box, { children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.context?.label || 'Context', ": "] }), _jsx(Text, { color: theme.colors.accent, children: selectedTask.context ? `@${selectedTask.context}` : (i18n.tui.context?.none || 'No context') })] }), _jsxs(Box, { children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.effort?.label || 'Effort', ": "] }), _jsx(Text, { color: theme.colors.accent, children: selectedTask.effort ? (i18n.tui.effort?.[selectedTask.effort] || selectedTask.effort) : '-' })] }), _jsxs(Box, { children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.focus?.label || 'Focus', ": "] }), _jsx(Text, { color: theme.colors.accent, children: selectedTask.isFocused ? '★' : '-' })] })] }), _jsx(Box, { marginTop: 1, children: _jsx(MarioBoxInline, { title: `${i18n.tui.comments || 'Comments'} (${taskComments.length})`, width: terminalWidth - 4, minHeight: 5, isActive: mode === 'task-detail', children: taskComments.length === 0 ? (_jsx(Text, { color: theme.colors.textMuted, italic: true, children: i18n.tui.noComments || 'No comments yet' })) : (taskComments.map((comment, index) => {
|
|
1076
|
+
}) })] })) : mode === 'set-effort' && currentTasks.length > 0 ? (_jsx(Box, { flexDirection: "column", children: _jsxs(MarioBoxInline, { title: i18n.tui.effort?.set || 'Set Effort', width: terminalWidth - 4, minHeight: 4, isActive: true, children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.effort?.set || 'Set effort for', ": ", currentTasks[selectedTaskIndex]?.title] }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: EFFORT_OPTIONS.map((opt, index) => (_jsxs(Text, { color: index === effortSelectIndex ? theme.colors.textSelected : theme.colors.text, bold: index === effortSelectIndex, children: [index === effortSelectIndex ? '🍄 ' : ' ', opt.label] }, opt.value || 'clear'))) })] }) })) : mode === 'select-project' && taskToLink ? (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.selectProject || 'Select project', ": ", taskToLink.title] }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: tasks.projects.map((project, index) => (_jsxs(Text, { color: index === projectSelectIndex ? theme.colors.textSelected : theme.colors.text, bold: index === projectSelectIndex, children: [index === projectSelectIndex ? '🍄 ' : ' ', project.title] }, project.id))) })] })) : mode === 'confirm-delete' && taskToDelete ? (_jsx(Box, { flexDirection: "column", children: _jsx(Text, { color: theme.colors.accent, bold: true, children: fmt(i18n.tui.deleteConfirm || 'Delete "{title}"? (y/n)', { title: taskToDelete.title }) }) })) : (mode === 'task-detail' || mode === 'add-comment') && selectedTask ? (_jsxs(Box, { flexDirection: "column", children: [_jsxs(MarioBoxInline, { title: i18n.tui.taskDetailTitle || 'Task Details', width: terminalWidth - 4, minHeight: 4, isActive: true, children: [_jsx(Text, { color: theme.colors.text, bold: true, children: selectedTask.title }), selectedTask.description && (_jsx(Text, { color: theme.colors.textMuted, children: selectedTask.description })), _jsxs(Box, { children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.taskDetailStatus || 'Status', ": "] }), _jsxs(Text, { color: theme.colors.accent, children: [i18n.status[selectedTask.status], selectedTask.waitingFor && ` (${selectedTask.waitingFor})`] })] }), _jsxs(Box, { children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.context?.label || 'Context', ": "] }), _jsx(Text, { color: theme.colors.accent, children: selectedTask.context ? `@${selectedTask.context}` : (i18n.tui.context?.none || 'No context') })] }), _jsxs(Box, { children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.effort?.label || 'Effort', ": "] }), _jsx(Text, { color: theme.colors.accent, children: selectedTask.effort ? (i18n.tui.effort?.[selectedTask.effort] || selectedTask.effort) : '-' })] }), _jsxs(Box, { children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.focus?.label || 'Focus', ": "] }), _jsx(Text, { color: theme.colors.accent, children: selectedTask.isFocused ? '★' : '-' })] }), selectedTask.status === 'done' && (_jsxs(Box, { children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.taskDetailCompletedAt || 'Completed', ": "] }), _jsx(Text, { color: theme.colors.accent, children: (selectedTask.completedAt ?? selectedTask.updatedAt).toLocaleString() })] }))] }), _jsx(Box, { marginTop: 1, children: _jsx(MarioBoxInline, { title: `${i18n.tui.comments || 'Comments'} (${taskComments.length})`, width: terminalWidth - 4, minHeight: 5, isActive: mode === 'task-detail', children: taskComments.length === 0 ? (_jsx(Text, { color: theme.colors.textMuted, italic: true, children: i18n.tui.noComments || 'No comments yet' })) : (taskComments.map((comment, index) => {
|
|
1074
1077
|
const isSelected = index === selectedCommentIndex && mode === 'task-detail';
|
|
1075
1078
|
return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Text, { color: theme.colors.textMuted, children: [isSelected ? '🍄 ' : ' ', "[", comment.createdAt.toLocaleString(), "]"] }), _jsxs(Text, { color: isSelected ? theme.colors.textSelected : theme.colors.text, bold: isSelected, children: [' ', comment.content] })] }, comment.id));
|
|
1076
1079
|
})) }) })] })) : (_jsxs(Box, { flexDirection: "row", children: [_jsx(Box, { marginRight: 2, children: _jsx(MarioBoxInline, { title: mode === 'project-detail' && selectedProject ? selectedProject.title : 'STAGE', width: leftPaneWidth, minHeight: 8, isActive: paneFocus === 'tabs' && mode !== 'project-detail', children: mode === 'project-detail' ? (_jsx(Text, { color: theme.colors.textMuted, children: "\u2190 Esc/b: back" })) : (TABS.map((tab, index) => {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { useState, useEffect, useMemo } from 'react';
|
|
3
3
|
import { Box, Text, useInput } from 'ink';
|
|
4
|
-
import { eq, and
|
|
4
|
+
import { eq, and } from 'drizzle-orm';
|
|
5
5
|
import { getDb, schema } from '../../db/index.js';
|
|
6
6
|
import { t, fmt } from '../../i18n/index.js';
|
|
7
7
|
import { useTheme } from '../theme/index.js';
|
|
@@ -80,11 +80,14 @@ export function InsightsModal({ onClose }) {
|
|
|
80
80
|
const now = new Date();
|
|
81
81
|
const startDate = getWeekStart(now);
|
|
82
82
|
startDate.setDate(startDate.getDate() - (weeks - 1) * 7);
|
|
83
|
-
// Query completed tasks
|
|
84
|
-
const completedTasks = await db
|
|
83
|
+
// Query completed tasks (use completedAt with updatedAt fallback)
|
|
84
|
+
const completedTasks = (await db
|
|
85
85
|
.select()
|
|
86
86
|
.from(schema.tasks)
|
|
87
|
-
.where(and(eq(schema.tasks.status, 'done'),
|
|
87
|
+
.where(and(eq(schema.tasks.status, 'done'), eq(schema.tasks.isProject, false)))).filter(task => {
|
|
88
|
+
const completionDate = task.completedAt ?? task.updatedAt;
|
|
89
|
+
return completionDate >= startDate;
|
|
90
|
+
});
|
|
88
91
|
// Period header
|
|
89
92
|
lines.push({ type: 'text', value: `${l.period}: ${startDate.toLocaleDateString()} ~ ${now.toLocaleDateString()}` });
|
|
90
93
|
lines.push({ type: 'spacer', value: '' });
|
|
@@ -101,7 +104,10 @@ export function InsightsModal({ onClose }) {
|
|
|
101
104
|
ws.setDate(ws.getDate() - i * 7);
|
|
102
105
|
const we = new Date(ws);
|
|
103
106
|
we.setDate(we.getDate() + 7);
|
|
104
|
-
const weekTasks = completedTasks.filter(t =>
|
|
107
|
+
const weekTasks = completedTasks.filter(t => {
|
|
108
|
+
const d = t.completedAt ?? t.updatedAt;
|
|
109
|
+
return d >= ws && d < we;
|
|
110
|
+
});
|
|
105
111
|
const weekLabel = fmt(l.weekLabel, { date: ws.toLocaleDateString() });
|
|
106
112
|
const countLabel = fmt(l.tasksCompleted, { count: weekTasks.length });
|
|
107
113
|
lines.push({ type: 'text', value: `${weekLabel}: ${countLabel}` });
|
|
@@ -119,7 +125,7 @@ export function InsightsModal({ onClose }) {
|
|
|
119
125
|
lines.push({ type: 'header', value: l.dailyBreakdown });
|
|
120
126
|
const dayCounts = new Array(7).fill(0);
|
|
121
127
|
for (const task of completedTasks) {
|
|
122
|
-
dayCounts[task.updatedAt.getDay()]++;
|
|
128
|
+
dayCounts[(task.completedAt ?? task.updatedAt).getDay()]++;
|
|
123
129
|
}
|
|
124
130
|
const maxDaily = Math.max(...dayCounts);
|
|
125
131
|
const dayNames = isJa
|
|
@@ -211,7 +217,7 @@ export function InsightsModal({ onClose }) {
|
|
|
211
217
|
let totalMs = 0;
|
|
212
218
|
let validCount = 0;
|
|
213
219
|
for (const task of completedTasks) {
|
|
214
|
-
const diff = task.updatedAt.getTime() - task.createdAt.getTime();
|
|
220
|
+
const diff = (task.completedAt ?? task.updatedAt).getTime() - task.createdAt.getTime();
|
|
215
221
|
if (diff > 0) {
|
|
216
222
|
totalMs += diff;
|
|
217
223
|
validCount++;
|
|
@@ -306,6 +306,7 @@ export function KanbanBoard({ onSwitchToGtd, onOpenSettings }) {
|
|
|
306
306
|
toStatus: newStatus,
|
|
307
307
|
fromWaitingFor: task.waitingFor,
|
|
308
308
|
toWaitingFor: null,
|
|
309
|
+
fromCompletedAt: task.completedAt,
|
|
309
310
|
description: fmt(i18n.tui.movedTo, { title: task.title, status: i18n.status[newStatus] }),
|
|
310
311
|
});
|
|
311
312
|
await history.execute(command);
|
|
@@ -333,6 +334,7 @@ export function KanbanBoard({ onSwitchToGtd, onOpenSettings }) {
|
|
|
333
334
|
toStatus: newStatus,
|
|
334
335
|
fromWaitingFor: task.waitingFor,
|
|
335
336
|
toWaitingFor: null,
|
|
337
|
+
fromCompletedAt: task.completedAt,
|
|
336
338
|
description: fmt(i18n.tui.movedTo, { title: task.title, status: i18n.status[newStatus] }),
|
|
337
339
|
});
|
|
338
340
|
await history.execute(command);
|
|
@@ -346,6 +348,7 @@ export function KanbanBoard({ onSwitchToGtd, onOpenSettings }) {
|
|
|
346
348
|
toStatus: 'done',
|
|
347
349
|
fromWaitingFor: task.waitingFor,
|
|
348
350
|
toWaitingFor: null,
|
|
351
|
+
fromCompletedAt: task.completedAt,
|
|
349
352
|
description: fmt(i18n.tui.completed, { title: task.title }),
|
|
350
353
|
});
|
|
351
354
|
await history.execute(command);
|
|
@@ -276,6 +276,7 @@ export function KanbanDQ({ onOpenSettings }) {
|
|
|
276
276
|
toStatus: newStatus,
|
|
277
277
|
fromWaitingFor: task.waitingFor,
|
|
278
278
|
toWaitingFor: null,
|
|
279
|
+
fromCompletedAt: task.completedAt,
|
|
279
280
|
description: fmt(i18n.tui.movedTo, { title: task.title, status: i18n.status[newStatus] }),
|
|
280
281
|
});
|
|
281
282
|
await history.execute(command);
|
|
@@ -289,6 +290,7 @@ export function KanbanDQ({ onOpenSettings }) {
|
|
|
289
290
|
toStatus: 'done',
|
|
290
291
|
fromWaitingFor: task.waitingFor,
|
|
291
292
|
toWaitingFor: null,
|
|
293
|
+
fromCompletedAt: task.completedAt,
|
|
292
294
|
description: fmt(i18n.tui.completed, { title: task.title }),
|
|
293
295
|
});
|
|
294
296
|
await history.execute(command);
|