klg-asutk-app/app/jira-tasks/page.tsx
Yuriy 0150aba4f5 Consolidation: KLG ASUTK + PAPA integration
- Unify API: lib/api.ts uses /api/v1, inbox uses /api/inbox (rewrites)
- Remove localhost refs: openapi, inbox page
- Add rewrites: /api/inbox|tmc -> inbox-server, /api/v1 -> FastAPI
- Add stub routes: knowledge/insights, recommendations, search, log-error
- Transfer from PAPA: prompts (inspection, tmc), scripts, supabase, data/tmc-requests
- Fix inbox-server: ORDER BY created_at, package.json
- Remove redundant app/api/inbox/files route (rewrites handle it)
- knowledge/ in gitignore (large PDFs)

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-08 17:18:31 +03:00

352 lines
13 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Страница для просмотра задач из Jira
* Данные импортируются из CSV файлов в папке "новая папка"
*/
'use client';
import { useState, useEffect } from 'react';
import Sidebar from '@/components/Sidebar';
interface JiraEpic {
issueId: string;
summary: string;
description: string;
priority: string;
components: string[];
labels: string[];
stories?: JiraStory[];
createdAt: string;
updatedAt: string;
}
interface JiraStory {
issueId: string;
summary: string;
description: string;
priority: string;
storyPoints?: number;
components: string[];
labels: string[];
acceptanceCriteria?: string;
subtasks?: JiraSubtask[];
createdAt: string;
}
interface JiraSubtask {
issueId: string;
summary: string;
description: string;
priority: string;
components: string[];
labels: string[];
createdAt: string;
}
interface Dependency {
fromIssueId: string;
toIssueId: string;
linkType: string;
}
export default function JiraTasksPage() {
const [epics, setEpics] = useState<JiraEpic[]>([]);
const [dependencies, setDependencies] = useState<Dependency[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [selectedEpic, setSelectedEpic] = useState<string | null>(null);
useEffect(() => {
loadTasks();
}, []);
const loadTasks = async () => {
try {
setLoading(true);
setError(null);
const response = await fetch('/api/jira-tasks?type=epic');
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
setEpics(data.data || []);
setDependencies(data.dependencies || []);
} catch (err) {
setError(err instanceof Error ? err.message : 'Ошибка загрузки задач');
console.error('Ошибка загрузки задач:', err);
} finally {
setLoading(false);
}
};
const getPriorityColor = (priority: string) => {
switch (priority?.toLowerCase()) {
case 'high':
return '#f44336';
case 'medium':
return '#ff9800';
case 'low':
return '#4caf50';
default:
return '#757575';
}
};
const getPriorityLabel = (priority: string) => {
switch (priority?.toLowerCase()) {
case 'high':
return 'Высокий';
case 'medium':
return 'Средний';
case 'low':
return 'Низкий';
default:
return priority || 'Не указан';
}
};
if (loading) {
return (
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '100vh' }}>
<div>Загрузка задач...</div>
</div>
);
}
if (error) {
return (
<div style={{ padding: '20px' }}>
<div style={{ color: '#f44336', marginBottom: '20px' }}>
Ошибка: {error}
</div>
<button
onClick={loadTasks}
style={{
padding: '10px 20px',
backgroundColor: '#1e3a5f',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
}}
>
Повторить загрузку
</button>
</div>
);
}
return (
<div style={{ display: 'flex', minHeight: '100vh' }}>
<Sidebar />
<div style={{ flex: 1, padding: '20px', marginLeft: '250px' }}>
<div style={{ marginBottom: '30px', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<div>
<h1 style={{ fontSize: '28px', fontWeight: 'bold', margin: '0 0 10px 0' }}>
Задачи Jira (REFLY)
</h1>
<p style={{ color: '#666', margin: 0 }}>
Эпики, истории и подзадачи из импортированных CSV файлов
</p>
</div>
<button
onClick={loadTasks}
style={{
padding: '10px 20px',
backgroundColor: '#1e3a5f',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
}}
>
🔄 Обновить
</button>
</div>
{epics.length === 0 ? (
<div style={{ padding: '40px', textAlign: 'center', color: '#666' }}>
<p>Нет данных. Запустите импорт:</p>
<code style={{ display: 'block', marginTop: '10px', padding: '10px', backgroundColor: '#f5f5f5', borderRadius: '4px' }}>
npm run import:jira
</code>
</div>
) : (
<div style={{ display: 'flex', flexDirection: 'column', gap: '20px' }}>
{epics.map((epic) => (
<div
key={epic.issueId}
style={{
border: '1px solid #e0e0e0',
borderRadius: '8px',
padding: '20px',
backgroundColor: 'white',
boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
}}
>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: '15px' }}>
<div style={{ flex: 1 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '10px', marginBottom: '8px' }}>
<span style={{ fontWeight: 'bold', fontSize: '18px' }}>📋 {epic.summary}</span>
<span
style={{
padding: '4px 8px',
borderRadius: '4px',
backgroundColor: getPriorityColor(epic.priority),
color: 'white',
fontSize: '12px',
fontWeight: 'bold',
}}
>
{getPriorityLabel(epic.priority)}
</span>
<span style={{ color: '#666', fontSize: '14px' }}>ID: {epic.issueId}</span>
</div>
{epic.description && (
<p style={{ color: '#666', margin: '8px 0', fontSize: '14px' }}>{epic.description}</p>
)}
<div style={{ display: 'flex', gap: '10px', flexWrap: 'wrap', marginTop: '10px' }}>
{epic.components && epic.components.length > 0 && (
<div>
<strong>Компоненты:</strong> {epic.components.join(', ')}
</div>
)}
{epic.labels && epic.labels.length > 0 && (
<div>
<strong>Метки:</strong>{' '}
{epic.labels.map((label) => (
<span
key={label}
style={{
display: 'inline-block',
padding: '2px 6px',
backgroundColor: '#e3f2fd',
borderRadius: '3px',
fontSize: '12px',
marginLeft: '4px',
}}
>
{label}
</span>
))}
</div>
)}
</div>
</div>
<button
onClick={() => setSelectedEpic(selectedEpic === epic.issueId ? null : epic.issueId)}
style={{
padding: '8px 16px',
backgroundColor: selectedEpic === epic.issueId ? '#1e3a5f' : '#f5f5f5',
color: selectedEpic === epic.issueId ? 'white' : '#333',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
}}
>
{selectedEpic === epic.issueId ? 'Скрыть' : 'Показать'} истории
</button>
</div>
{selectedEpic === epic.issueId && epic.stories && epic.stories.length > 0 && (
<div style={{ marginTop: '20px', paddingTop: '20px', borderTop: '1px solid #e0e0e0' }}>
<h3 style={{ fontSize: '16px', fontWeight: 'bold', marginBottom: '15px' }}>
Истории ({epic.stories.length})
</h3>
<div style={{ display: 'flex', flexDirection: 'column', gap: '15px' }}>
{epic.stories.map((story) => (
<div
key={story.issueId}
style={{
padding: '15px',
backgroundColor: '#f9f9f9',
borderRadius: '6px',
borderLeft: '4px solid #2196f3',
}}
>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: '8px' }}>
<div style={{ flex: 1 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '10px', marginBottom: '5px' }}>
<span style={{ fontWeight: 'bold' }}>📝 {story.summary}</span>
<span
style={{
padding: '2px 6px',
borderRadius: '3px',
backgroundColor: getPriorityColor(story.priority),
color: 'white',
fontSize: '11px',
}}
>
{getPriorityLabel(story.priority)}
</span>
{story.storyPoints && (
<span style={{ fontSize: '12px', color: '#666' }}>
{story.storyPoints} SP
</span>
)}
<span style={{ fontSize: '12px', color: '#999' }}>ID: {story.issueId}</span>
</div>
{story.description && (
<p style={{ color: '#666', fontSize: '13px', margin: '5px 0' }}>{story.description}</p>
)}
{story.acceptanceCriteria && (
<div style={{ marginTop: '8px', padding: '8px', backgroundColor: '#fff3cd', borderRadius: '4px' }}>
<strong style={{ fontSize: '12px' }}>Критерии приемки:</strong>
<pre style={{ fontSize: '12px', margin: '5px 0 0 0', whiteSpace: 'pre-wrap' }}>
{story.acceptanceCriteria}
</pre>
</div>
)}
</div>
</div>
{story.subtasks && story.subtasks.length > 0 && (
<div style={{ marginTop: '10px', paddingTop: '10px', borderTop: '1px solid #ddd' }}>
<strong style={{ fontSize: '12px' }}>Подзадачи ({story.subtasks.length}):</strong>
<ul style={{ margin: '5px 0', paddingLeft: '20px' }}>
{story.subtasks.map((subtask) => (
<li key={subtask.issueId} style={{ fontSize: '12px', margin: '3px 0' }}>
{subtask.summary} <span style={{ color: '#999' }}>({subtask.issueId})</span>
</li>
))}
</ul>
</div>
)}
</div>
))}
</div>
</div>
)}
</div>
))}
</div>
)}
{dependencies.length > 0 && (
<div style={{ marginTop: '30px', padding: '20px', backgroundColor: '#f5f5f5', borderRadius: '8px' }}>
<h2 style={{ fontSize: '18px', fontWeight: 'bold', marginBottom: '15px' }}>
Зависимости между задачами ({dependencies.length})
</h2>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(300px, 1fr))', gap: '10px' }}>
{dependencies.map((dep, index) => (
<div
key={index}
style={{
padding: '10px',
backgroundColor: 'white',
borderRadius: '4px',
fontSize: '12px',
}}
>
<strong>{dep.fromIssueId}</strong> <strong>{dep.toIssueId}</strong>
<span style={{ color: '#666', marginLeft: '8px' }}>({dep.linkType})</span>
</div>
))}
</div>
</div>
)}
</div>
</div>
);
}