Merge branch 'KIN-126-frontend_dev'
This commit is contained in:
commit
1487e84eb6
6 changed files with 39 additions and 3 deletions
|
|
@ -773,6 +773,12 @@ def _migrate(conn: sqlite3.Connection):
|
||||||
PRAGMA foreign_keys=ON;
|
PRAGMA foreign_keys=ON;
|
||||||
""")
|
""")
|
||||||
|
|
||||||
|
# KIN-126: Add completed_at to tasks — set when task transitions to 'done'
|
||||||
|
task_cols_final = {r[1] for r in conn.execute("PRAGMA table_info(tasks)").fetchall()}
|
||||||
|
if "completed_at" not in task_cols_final:
|
||||||
|
conn.execute("ALTER TABLE tasks ADD COLUMN completed_at DATETIME DEFAULT NULL")
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
|
||||||
def _seed_default_hooks(conn: sqlite3.Connection):
|
def _seed_default_hooks(conn: sqlite3.Connection):
|
||||||
"""Seed default hooks for the kin project (idempotent).
|
"""Seed default hooks for the kin project (idempotent).
|
||||||
|
|
|
||||||
|
|
@ -304,13 +304,15 @@ def list_tasks(
|
||||||
|
|
||||||
|
|
||||||
def update_task(conn: sqlite3.Connection, id: str, **fields) -> dict:
|
def update_task(conn: sqlite3.Connection, id: str, **fields) -> dict:
|
||||||
"""Update task fields. Auto-sets updated_at."""
|
"""Update task fields. Auto-sets updated_at. Sets completed_at when status transitions to 'done'."""
|
||||||
if not fields:
|
if not fields:
|
||||||
return get_task(conn, id)
|
return get_task(conn, id)
|
||||||
json_cols = ("brief", "spec", "review", "test_result", "security_result", "labels")
|
json_cols = ("brief", "spec", "review", "test_result", "security_result", "labels")
|
||||||
for key in json_cols:
|
for key in json_cols:
|
||||||
if key in fields:
|
if key in fields:
|
||||||
fields[key] = _json_encode(fields[key])
|
fields[key] = _json_encode(fields[key])
|
||||||
|
if "status" in fields and fields["status"] == "done":
|
||||||
|
fields["completed_at"] = datetime.now().isoformat()
|
||||||
fields["updated_at"] = datetime.now().isoformat()
|
fields["updated_at"] = datetime.now().isoformat()
|
||||||
sets = ", ".join(f"{k} = ?" for k in fields)
|
sets = ", ".join(f"{k} = ?" for k in fields)
|
||||||
vals = list(fields.values()) + [id]
|
vals = list(fields.values()) + [id]
|
||||||
|
|
|
||||||
|
|
@ -122,6 +122,7 @@ export interface Task {
|
||||||
feedback?: string | null
|
feedback?: string | null
|
||||||
created_at: string
|
created_at: string
|
||||||
updated_at: string
|
updated_at: string
|
||||||
|
completed_at?: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Decision {
|
export interface Decision {
|
||||||
|
|
|
||||||
|
|
@ -221,7 +221,9 @@
|
||||||
"settings_integrations_section": "Integrations",
|
"settings_integrations_section": "Integrations",
|
||||||
"settings_execution_mode": "Execution mode",
|
"settings_execution_mode": "Execution mode",
|
||||||
"settings_autocommit": "Autocommit",
|
"settings_autocommit": "Autocommit",
|
||||||
"settings_autocommit_hint": "— git commit after pipeline"
|
"settings_autocommit_hint": "— git commit after pipeline",
|
||||||
|
"done_date_from": "From",
|
||||||
|
"done_date_to": "To"
|
||||||
},
|
},
|
||||||
"escalation": {
|
"escalation": {
|
||||||
"watchdog_blocked": "Watchdog: task {task_id} blocked — {reason}",
|
"watchdog_blocked": "Watchdog: task {task_id} blocked — {reason}",
|
||||||
|
|
|
||||||
|
|
@ -221,7 +221,9 @@
|
||||||
"settings_integrations_section": "Интеграции",
|
"settings_integrations_section": "Интеграции",
|
||||||
"settings_execution_mode": "Режим выполнения",
|
"settings_execution_mode": "Режим выполнения",
|
||||||
"settings_autocommit": "Автокоммит",
|
"settings_autocommit": "Автокоммит",
|
||||||
"settings_autocommit_hint": "— git commit после pipeline"
|
"settings_autocommit_hint": "— git commit после pipeline",
|
||||||
|
"done_date_from": "От",
|
||||||
|
"done_date_to": "До"
|
||||||
},
|
},
|
||||||
"escalation": {
|
"escalation": {
|
||||||
"watchdog_blocked": "Watchdog: задача {task_id} заблокирована — {reason}",
|
"watchdog_blocked": "Watchdog: задача {task_id} заблокирована — {reason}",
|
||||||
|
|
|
||||||
|
|
@ -186,6 +186,8 @@ function initStatusFilter(): string[] {
|
||||||
const selectedStatuses = ref<string[]>(initStatusFilter())
|
const selectedStatuses = ref<string[]>(initStatusFilter())
|
||||||
const selectedCategory = ref('')
|
const selectedCategory = ref('')
|
||||||
const taskSearch = ref('')
|
const taskSearch = ref('')
|
||||||
|
const dateFrom = ref('')
|
||||||
|
const dateTo = ref('')
|
||||||
|
|
||||||
function toggleStatus(s: string) {
|
function toggleStatus(s: string) {
|
||||||
const idx = selectedStatuses.value.indexOf(s)
|
const idx = selectedStatuses.value.indexOf(s)
|
||||||
|
|
@ -659,6 +661,16 @@ const filteredTasks = computed(() => {
|
||||||
let tasks = searchFilteredTasks.value
|
let tasks = searchFilteredTasks.value
|
||||||
if (selectedStatuses.value.length > 0) tasks = tasks.filter(t => selectedStatuses.value.includes(t.status))
|
if (selectedStatuses.value.length > 0) tasks = tasks.filter(t => selectedStatuses.value.includes(t.status))
|
||||||
if (selectedCategory.value) tasks = tasks.filter(t => t.category === selectedCategory.value)
|
if (selectedCategory.value) tasks = tasks.filter(t => t.category === selectedCategory.value)
|
||||||
|
if ((dateFrom.value || dateTo.value) && selectedStatuses.value.includes('done')) {
|
||||||
|
tasks = tasks.filter(t => {
|
||||||
|
if (t.status !== 'done') return true
|
||||||
|
const dateStr = (t.completed_at || t.updated_at) ?? ''
|
||||||
|
const d = dateStr.substring(0, 10)
|
||||||
|
if (dateFrom.value && d < dateFrom.value) return false
|
||||||
|
if (dateTo.value && d > dateTo.value) return false
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
}
|
||||||
return tasks
|
return tasks
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -1077,6 +1089,17 @@ async function addDecision() {
|
||||||
<button v-if="taskSearch" @click="taskSearch = ''"
|
<button v-if="taskSearch" @click="taskSearch = ''"
|
||||||
class="text-gray-600 hover:text-red-400 text-xs px-1">✕</button>
|
class="text-gray-600 hover:text-red-400 text-xs px-1">✕</button>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- Date filter for done tasks -->
|
||||||
|
<div v-if="selectedStatuses.includes('done')" class="flex items-center gap-2">
|
||||||
|
<span class="text-xs text-gray-600">{{ t('projectView.done_date_from') }}</span>
|
||||||
|
<input type="date" v-model="dateFrom" data-testid="date-from"
|
||||||
|
class="bg-gray-800 border border-gray-700 rounded px-2 py-0.5 text-xs text-gray-300 focus:border-gray-500 outline-none" />
|
||||||
|
<span class="text-xs text-gray-600">{{ t('projectView.done_date_to') }}</span>
|
||||||
|
<input type="date" v-model="dateTo" data-testid="date-to"
|
||||||
|
class="bg-gray-800 border border-gray-700 rounded px-2 py-0.5 text-xs text-gray-300 focus:border-gray-500 outline-none" />
|
||||||
|
<button v-if="dateFrom || dateTo" @click="dateFrom = ''; dateTo = ''"
|
||||||
|
class="text-gray-600 hover:text-red-400 text-xs px-1">✕</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- Manual escalation tasks -->
|
<!-- Manual escalation tasks -->
|
||||||
<div v-if="manualEscalationTasks.length" class="mb-4">
|
<div v-if="manualEscalationTasks.length" class="mb-4">
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue