roadmap-kit 1.0.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/INSTALL.md +358 -0
- package/LICENSE +21 -0
- package/README.md +503 -0
- package/cli.js +548 -0
- package/dashboard/dist/assets/index-BzYzLB7u.css +1 -0
- package/dashboard/dist/assets/index-DIonhzlK.js +506 -0
- package/dashboard/dist/index.html +18 -0
- package/dashboard/dist/roadmap.json +268 -0
- package/dashboard/index.html +17 -0
- package/dashboard/package-lock.json +4172 -0
- package/dashboard/package.json +37 -0
- package/dashboard/postcss.config.js +6 -0
- package/dashboard/public/roadmap.json +268 -0
- package/dashboard/server.js +1366 -0
- package/dashboard/src/App.jsx +6979 -0
- package/dashboard/src/components/CircularProgress.jsx +55 -0
- package/dashboard/src/components/ProgressBar.jsx +33 -0
- package/dashboard/src/components/ProjectSettings.jsx +420 -0
- package/dashboard/src/components/SharedResources.jsx +239 -0
- package/dashboard/src/components/TaskList.jsx +273 -0
- package/dashboard/src/components/TechnicalDebt.jsx +170 -0
- package/dashboard/src/components/ui/accordion.jsx +46 -0
- package/dashboard/src/components/ui/badge.jsx +38 -0
- package/dashboard/src/components/ui/card.jsx +60 -0
- package/dashboard/src/components/ui/progress.jsx +22 -0
- package/dashboard/src/components/ui/tabs.jsx +47 -0
- package/dashboard/src/index.css +440 -0
- package/dashboard/src/lib/utils.js +6 -0
- package/dashboard/src/main.jsx +10 -0
- package/dashboard/tailwind.config.js +142 -0
- package/dashboard/vite.config.js +18 -0
- package/docker/Dockerfile +35 -0
- package/docker/docker-compose.yml +30 -0
- package/docker/entrypoint.sh +31 -0
- package/package.json +68 -0
- package/scanner.js +351 -0
- package/setup.sh +354 -0
- package/templates/clinerules.template +130 -0
- package/templates/roadmap.template.json +30 -0
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { Package, Code, Database, Copy, Check, Layers, FileCode, ChevronDown, AlertTriangle } from 'lucide-react';
|
|
3
|
+
|
|
4
|
+
export default function SharedResources({ resources = {} }) {
|
|
5
|
+
const [copiedItem, setCopiedItem] = useState(null);
|
|
6
|
+
const [expanded, setExpanded] = useState({ ui: true, utils: true, db: true });
|
|
7
|
+
|
|
8
|
+
const copyToClipboard = (text, id) => {
|
|
9
|
+
navigator.clipboard.writeText(text);
|
|
10
|
+
setCopiedItem(id);
|
|
11
|
+
setTimeout(() => setCopiedItem(null), 2000);
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const { ui_components = [], utilities = [], database_tables = [] } = resources;
|
|
15
|
+
const total = ui_components.length + utilities.length + database_tables.length;
|
|
16
|
+
|
|
17
|
+
if (total === 0) {
|
|
18
|
+
return (
|
|
19
|
+
<div className="card-dark rounded-2xl p-12 text-center">
|
|
20
|
+
<Package className="w-12 h-12 mx-auto mb-4 text-gray-600" />
|
|
21
|
+
<h3 className="text-lg font-semibold text-gray-400 mb-2">Sin recursos compartidos</h3>
|
|
22
|
+
<p className="text-gray-500 text-sm">Agrega componentes en roadmap.json</p>
|
|
23
|
+
</div>
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<div className="space-y-6">
|
|
29
|
+
{/* Summary */}
|
|
30
|
+
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 stagger">
|
|
31
|
+
<StatCard icon={Layers} label="Componentes UI" value={ui_components.length} gradient="from-blue-500 to-cyan-500" />
|
|
32
|
+
<StatCard icon={Code} label="Utilidades" value={utilities.length} gradient="from-cyan-500 to-teal-500" />
|
|
33
|
+
<StatCard icon={Database} label="Tablas DB" value={database_tables.length} gradient="from-teal-500 to-emerald-500" />
|
|
34
|
+
</div>
|
|
35
|
+
|
|
36
|
+
{/* UI Components */}
|
|
37
|
+
{ui_components.length > 0 && (
|
|
38
|
+
<Section
|
|
39
|
+
title="Componentes UI"
|
|
40
|
+
icon={Layers}
|
|
41
|
+
color="blue"
|
|
42
|
+
isOpen={expanded.ui}
|
|
43
|
+
onToggle={() => setExpanded(p => ({ ...p, ui: !p.ui }))}
|
|
44
|
+
>
|
|
45
|
+
<div className="space-y-2">
|
|
46
|
+
{ui_components.map((c, i) => (
|
|
47
|
+
<ResourceItem
|
|
48
|
+
key={i}
|
|
49
|
+
path={c.path}
|
|
50
|
+
description={c.description}
|
|
51
|
+
usage={c.usage}
|
|
52
|
+
color="blue"
|
|
53
|
+
copyId={`ui-${i}`}
|
|
54
|
+
copiedItem={copiedItem}
|
|
55
|
+
onCopy={copyToClipboard}
|
|
56
|
+
/>
|
|
57
|
+
))}
|
|
58
|
+
</div>
|
|
59
|
+
</Section>
|
|
60
|
+
)}
|
|
61
|
+
|
|
62
|
+
{/* Utilities */}
|
|
63
|
+
{utilities.length > 0 && (
|
|
64
|
+
<Section
|
|
65
|
+
title="Utilidades"
|
|
66
|
+
icon={Code}
|
|
67
|
+
color="cyan"
|
|
68
|
+
isOpen={expanded.utils}
|
|
69
|
+
onToggle={() => setExpanded(p => ({ ...p, utils: !p.utils }))}
|
|
70
|
+
>
|
|
71
|
+
<div className="space-y-2">
|
|
72
|
+
{utilities.map((u, i) => (
|
|
73
|
+
<ResourceItem
|
|
74
|
+
key={i}
|
|
75
|
+
path={u.path}
|
|
76
|
+
description={u.description}
|
|
77
|
+
usage={u.usage}
|
|
78
|
+
exports={u.exports}
|
|
79
|
+
warning={u.warning}
|
|
80
|
+
color="cyan"
|
|
81
|
+
copyId={`util-${i}`}
|
|
82
|
+
copiedItem={copiedItem}
|
|
83
|
+
onCopy={copyToClipboard}
|
|
84
|
+
/>
|
|
85
|
+
))}
|
|
86
|
+
</div>
|
|
87
|
+
</Section>
|
|
88
|
+
)}
|
|
89
|
+
|
|
90
|
+
{/* Database */}
|
|
91
|
+
{database_tables.length > 0 && (
|
|
92
|
+
<Section
|
|
93
|
+
title="Tablas de Base de Datos"
|
|
94
|
+
icon={Database}
|
|
95
|
+
color="emerald"
|
|
96
|
+
isOpen={expanded.db}
|
|
97
|
+
onToggle={() => setExpanded(p => ({ ...p, db: !p.db }))}
|
|
98
|
+
>
|
|
99
|
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
|
100
|
+
{database_tables.map((t, i) => (
|
|
101
|
+
<TableCard
|
|
102
|
+
key={i}
|
|
103
|
+
table={t}
|
|
104
|
+
copyId={`table-${i}`}
|
|
105
|
+
copiedItem={copiedItem}
|
|
106
|
+
onCopy={copyToClipboard}
|
|
107
|
+
/>
|
|
108
|
+
))}
|
|
109
|
+
</div>
|
|
110
|
+
</Section>
|
|
111
|
+
)}
|
|
112
|
+
</div>
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function StatCard({ icon: Icon, label, value, gradient }) {
|
|
117
|
+
return (
|
|
118
|
+
<div className="card-dark rounded-2xl p-5 hover:scale-[1.02] transition-all">
|
|
119
|
+
<div className="flex items-center justify-between">
|
|
120
|
+
<div>
|
|
121
|
+
<p className="text-xs text-gray-500 uppercase tracking-wider mb-1">{label}</p>
|
|
122
|
+
<p className={`text-3xl font-bold bg-gradient-to-r ${gradient} bg-clip-text text-transparent`}>{value}</p>
|
|
123
|
+
</div>
|
|
124
|
+
<div className={`w-12 h-12 rounded-xl bg-gradient-to-br ${gradient} flex items-center justify-center opacity-80`}>
|
|
125
|
+
<Icon className="w-6 h-6 text-white" />
|
|
126
|
+
</div>
|
|
127
|
+
</div>
|
|
128
|
+
</div>
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function Section({ title, icon: Icon, color, isOpen, onToggle, children }) {
|
|
133
|
+
const colors = {
|
|
134
|
+
blue: 'text-blue-400',
|
|
135
|
+
cyan: 'text-cyan-400',
|
|
136
|
+
emerald: 'text-emerald-400',
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
return (
|
|
140
|
+
<div className="card-dark rounded-2xl overflow-hidden">
|
|
141
|
+
<button
|
|
142
|
+
onClick={onToggle}
|
|
143
|
+
className="w-full px-6 py-4 flex items-center justify-between hover:bg-white/[0.02] transition-colors"
|
|
144
|
+
>
|
|
145
|
+
<div className="flex items-center gap-3">
|
|
146
|
+
<Icon className={`w-5 h-5 ${colors[color]}`} />
|
|
147
|
+
<h3 className={`text-lg font-semibold ${colors[color]}`}>{title}</h3>
|
|
148
|
+
</div>
|
|
149
|
+
<ChevronDown className={`w-5 h-5 text-gray-500 transition-transform ${isOpen ? 'rotate-180' : ''}`} />
|
|
150
|
+
</button>
|
|
151
|
+
{isOpen && <div className="px-6 pb-6 pt-2 border-t border-white/5">{children}</div>}
|
|
152
|
+
</div>
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function ResourceItem({ path, description, usage, exports, warning, color, copyId, copiedItem, onCopy }) {
|
|
157
|
+
const [open, setOpen] = useState(false);
|
|
158
|
+
const colors = {
|
|
159
|
+
blue: 'text-blue-400 border-blue-500/20 bg-blue-500/5',
|
|
160
|
+
cyan: 'text-cyan-400 border-cyan-500/20 bg-cyan-500/5',
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
return (
|
|
164
|
+
<div className={`rounded-xl border ${colors[color]} overflow-hidden`}>
|
|
165
|
+
<button
|
|
166
|
+
onClick={() => setOpen(!open)}
|
|
167
|
+
className="w-full px-4 py-3 flex items-center gap-3 hover:bg-white/[0.02] transition-colors text-left"
|
|
168
|
+
>
|
|
169
|
+
<FileCode className={`w-4 h-4 flex-shrink-0 ${colors[color].split(' ')[0]}`} />
|
|
170
|
+
<div className="flex-1 min-w-0">
|
|
171
|
+
<code className={`text-sm ${colors[color].split(' ')[0]}`}>{path}</code>
|
|
172
|
+
<p className="text-xs text-gray-500 truncate mt-0.5">{description}</p>
|
|
173
|
+
</div>
|
|
174
|
+
<ChevronDown className={`w-4 h-4 text-gray-500 transition-transform ${open ? 'rotate-180' : ''}`} />
|
|
175
|
+
</button>
|
|
176
|
+
|
|
177
|
+
{open && (
|
|
178
|
+
<div className="px-4 pb-4 pt-2 border-t border-white/5 space-y-3 animate-fade-in">
|
|
179
|
+
{exports?.length > 0 && (
|
|
180
|
+
<div>
|
|
181
|
+
<span className="text-[10px] text-gray-500 uppercase tracking-wider">Exports:</span>
|
|
182
|
+
<div className="flex flex-wrap gap-1.5 mt-1.5">
|
|
183
|
+
{exports.map((e, i) => (
|
|
184
|
+
<span key={i} className="px-2 py-1 text-xs bg-white/5 text-blue-400 rounded border border-blue-500/20">{e}</span>
|
|
185
|
+
))}
|
|
186
|
+
</div>
|
|
187
|
+
</div>
|
|
188
|
+
)}
|
|
189
|
+
{usage && (
|
|
190
|
+
<div>
|
|
191
|
+
<div className="flex items-center justify-between mb-1.5">
|
|
192
|
+
<span className="text-[10px] text-gray-500 uppercase tracking-wider">Uso:</span>
|
|
193
|
+
<button onClick={() => onCopy(usage, copyId)} className="text-xs text-blue-400 hover:text-blue-300 flex items-center gap-1">
|
|
194
|
+
{copiedItem === copyId ? <><Check className="w-3 h-3" /> Copiado</> : <><Copy className="w-3 h-3" /> Copiar</>}
|
|
195
|
+
</button>
|
|
196
|
+
</div>
|
|
197
|
+
<div className="bg-white/5 rounded-lg p-3 border border-white/5">
|
|
198
|
+
<code className="text-sm text-blue-400">{usage}</code>
|
|
199
|
+
</div>
|
|
200
|
+
</div>
|
|
201
|
+
)}
|
|
202
|
+
{warning && (
|
|
203
|
+
<div className="flex items-start gap-2 bg-orange-500/10 border border-orange-500/20 rounded-lg p-3">
|
|
204
|
+
<AlertTriangle className="w-4 h-4 text-orange-400 flex-shrink-0 mt-0.5" />
|
|
205
|
+
<span className="text-xs text-orange-300">{warning}</span>
|
|
206
|
+
</div>
|
|
207
|
+
)}
|
|
208
|
+
</div>
|
|
209
|
+
)}
|
|
210
|
+
</div>
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function TableCard({ table, copyId, copiedItem, onCopy }) {
|
|
215
|
+
return (
|
|
216
|
+
<div className="bg-emerald-500/5 border border-emerald-500/20 rounded-xl p-4 hover:border-emerald-500/40 transition-all">
|
|
217
|
+
<div className="flex items-start justify-between mb-3">
|
|
218
|
+
<div className="flex items-center gap-2">
|
|
219
|
+
<Database className="w-4 h-4 text-emerald-400" />
|
|
220
|
+
<code className="text-base font-semibold text-emerald-400">{table.name}</code>
|
|
221
|
+
</div>
|
|
222
|
+
<button onClick={() => onCopy(table.name, copyId)} className="p-1.5 hover:bg-white/5 rounded-lg transition-colors">
|
|
223
|
+
{copiedItem === copyId ? <Check className="w-3.5 h-3.5 text-emerald-400" /> : <Copy className="w-3.5 h-3.5 text-gray-500" />}
|
|
224
|
+
</button>
|
|
225
|
+
</div>
|
|
226
|
+
<p className="text-sm text-gray-400 mb-3">{table.description}</p>
|
|
227
|
+
{table.fields?.length > 0 && (
|
|
228
|
+
<div>
|
|
229
|
+
<span className="text-[10px] text-gray-500 uppercase tracking-wider">Campos:</span>
|
|
230
|
+
<div className="flex flex-wrap gap-1 mt-1.5">
|
|
231
|
+
{table.fields.map((f, i) => (
|
|
232
|
+
<span key={i} className="text-xs bg-white/5 px-2 py-1 rounded text-gray-300 border border-emerald-500/10">{f}</span>
|
|
233
|
+
))}
|
|
234
|
+
</div>
|
|
235
|
+
</div>
|
|
236
|
+
)}
|
|
237
|
+
</div>
|
|
238
|
+
);
|
|
239
|
+
}
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
CheckCircle2, Clock, Circle, GitCommit, ExternalLink,
|
|
4
|
+
Copy, Check, ChevronDown, FileCode, Recycle, Bot, AlertTriangle
|
|
5
|
+
} from 'lucide-react';
|
|
6
|
+
|
|
7
|
+
export default function TaskList({ tasks = [], featureId = '', onUpdateStatus }) {
|
|
8
|
+
const [copiedItem, setCopiedItem] = useState(null);
|
|
9
|
+
const [expandedTask, setExpandedTask] = useState(null);
|
|
10
|
+
|
|
11
|
+
const copyToClipboard = (text, id) => {
|
|
12
|
+
navigator.clipboard.writeText(text);
|
|
13
|
+
setCopiedItem(id);
|
|
14
|
+
setTimeout(() => setCopiedItem(null), 2000);
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const statusConfig = {
|
|
18
|
+
completed: {
|
|
19
|
+
icon: CheckCircle2,
|
|
20
|
+
label: 'Completado',
|
|
21
|
+
color: 'text-emerald-400',
|
|
22
|
+
bg: 'bg-emerald-500/10',
|
|
23
|
+
border: 'border-emerald-500/30',
|
|
24
|
+
glow: 'shadow-emerald-500/20'
|
|
25
|
+
},
|
|
26
|
+
in_progress: {
|
|
27
|
+
icon: Clock,
|
|
28
|
+
label: 'En Progreso',
|
|
29
|
+
color: 'text-amber-400',
|
|
30
|
+
bg: 'bg-amber-500/10',
|
|
31
|
+
border: 'border-amber-500/30',
|
|
32
|
+
glow: 'shadow-amber-500/20'
|
|
33
|
+
},
|
|
34
|
+
pending: {
|
|
35
|
+
icon: Circle,
|
|
36
|
+
label: 'Pendiente',
|
|
37
|
+
color: 'text-gray-400',
|
|
38
|
+
bg: 'bg-gray-500/10',
|
|
39
|
+
border: 'border-gray-500/30',
|
|
40
|
+
glow: ''
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const priorityConfig = {
|
|
45
|
+
high: { label: 'Alta', color: 'text-rose-400', bg: 'bg-rose-500/10', border: 'border-rose-500/30' },
|
|
46
|
+
medium: { label: 'Media', color: 'text-amber-400', bg: 'bg-amber-500/10', border: 'border-amber-500/30' },
|
|
47
|
+
low: { label: 'Baja', color: 'text-blue-400', bg: 'bg-blue-500/10', border: 'border-blue-500/30' }
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const cycleStatus = (current) => {
|
|
51
|
+
const order = ['pending', 'in_progress', 'completed'];
|
|
52
|
+
return order[(order.indexOf(current) + 1) % order.length];
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
if (!tasks?.length) {
|
|
56
|
+
return (
|
|
57
|
+
<div className="py-8 text-center">
|
|
58
|
+
<Circle className="w-10 h-10 mx-auto mb-3 text-gray-600" />
|
|
59
|
+
<p className="text-gray-500 text-sm">No hay tareas definidas</p>
|
|
60
|
+
</div>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
<div className="space-y-2">
|
|
66
|
+
{tasks.map((task) => {
|
|
67
|
+
const status = statusConfig[task.status] || statusConfig.pending;
|
|
68
|
+
const priority = priorityConfig[task.priority] || priorityConfig.medium;
|
|
69
|
+
const StatusIcon = status.icon;
|
|
70
|
+
const isExpanded = expandedTask === task.id;
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
<div key={task.id} className={`rounded-xl border ${status.border} ${status.bg} transition-all hover:border-blue-500/30`}>
|
|
74
|
+
{/* Header */}
|
|
75
|
+
<div className="flex items-center gap-3 p-4">
|
|
76
|
+
<button
|
|
77
|
+
onClick={(e) => {
|
|
78
|
+
e.stopPropagation();
|
|
79
|
+
if (onUpdateStatus) {
|
|
80
|
+
onUpdateStatus(featureId, task.id, cycleStatus(task.status));
|
|
81
|
+
}
|
|
82
|
+
}}
|
|
83
|
+
className={`flex-shrink-0 w-8 h-8 rounded-lg ${status.bg} ${status.border} border flex items-center justify-center transition-all hover:scale-110 shadow-lg ${status.glow}`}
|
|
84
|
+
title={`Click para cambiar estado (Actual: ${status.label})`}
|
|
85
|
+
>
|
|
86
|
+
<StatusIcon className={`w-4 h-4 ${status.color}`} />
|
|
87
|
+
</button>
|
|
88
|
+
|
|
89
|
+
<div className="flex-1 min-w-0">
|
|
90
|
+
<div className="flex items-center gap-2 flex-wrap">
|
|
91
|
+
<h4 className={`font-medium ${task.status === 'completed' ? 'text-gray-500 line-through' : 'text-white'}`}>
|
|
92
|
+
{task.name}
|
|
93
|
+
</h4>
|
|
94
|
+
<span className={`px-2 py-0.5 text-[10px] font-medium rounded-full border ${priority.bg} ${priority.border} ${priority.color}`}>
|
|
95
|
+
{priority.label}
|
|
96
|
+
</span>
|
|
97
|
+
</div>
|
|
98
|
+
<p className="text-sm text-gray-500 line-clamp-1 mt-0.5">{task.description}</p>
|
|
99
|
+
</div>
|
|
100
|
+
|
|
101
|
+
{/* Quick stats */}
|
|
102
|
+
<div className="hidden sm:flex items-center gap-3 text-xs text-gray-500">
|
|
103
|
+
{task.metrics?.lines_added > 0 && (
|
|
104
|
+
<span>
|
|
105
|
+
<span className="text-emerald-400">+{task.metrics.lines_added}</span>
|
|
106
|
+
<span className="mx-0.5">/</span>
|
|
107
|
+
<span className="text-rose-400">-{task.metrics.lines_removed}</span>
|
|
108
|
+
</span>
|
|
109
|
+
)}
|
|
110
|
+
{task.technical_debt?.length > 0 && (
|
|
111
|
+
<span className="flex items-center gap-1 text-orange-400">
|
|
112
|
+
<AlertTriangle className="w-3 h-3" />
|
|
113
|
+
{task.technical_debt.length}
|
|
114
|
+
</span>
|
|
115
|
+
)}
|
|
116
|
+
</div>
|
|
117
|
+
|
|
118
|
+
<button
|
|
119
|
+
onClick={() => setExpandedTask(isExpanded ? null : task.id)}
|
|
120
|
+
className={`p-2 rounded-lg hover:bg-white/5 transition-all ${isExpanded ? 'bg-white/5' : ''}`}
|
|
121
|
+
>
|
|
122
|
+
<ChevronDown className={`w-4 h-4 text-gray-500 transition-transform ${isExpanded ? 'rotate-180' : ''}`} />
|
|
123
|
+
</button>
|
|
124
|
+
</div>
|
|
125
|
+
|
|
126
|
+
{/* Expanded */}
|
|
127
|
+
{isExpanded && (
|
|
128
|
+
<div className="px-4 pb-4 pt-2 border-t border-white/5 space-y-4 animate-fade-in">
|
|
129
|
+
<p className="text-sm text-gray-300">{task.description}</p>
|
|
130
|
+
|
|
131
|
+
{/* Metrics */}
|
|
132
|
+
{task.metrics?.lines_added > 0 && (
|
|
133
|
+
<div className="grid grid-cols-2 sm:grid-cols-4 gap-2">
|
|
134
|
+
<MetricCard label="Lineas" value={<><span className="text-emerald-400">+{task.metrics.lines_added}</span> / <span className="text-rose-400">-{task.metrics.lines_removed}</span></>} />
|
|
135
|
+
<MetricCard label="Archivos" value={task.metrics.files_created + task.metrics.files_modified} color="text-blue-400" />
|
|
136
|
+
<MetricCard label="Complejidad" value={`${task.metrics.complexity_score}/10`} color="text-amber-400" />
|
|
137
|
+
<MetricCard label="Commits" value={task.git?.commits?.length || 0} color="text-cyan-400" />
|
|
138
|
+
</div>
|
|
139
|
+
)}
|
|
140
|
+
|
|
141
|
+
{/* Files */}
|
|
142
|
+
{task.affected_files?.length > 0 && (
|
|
143
|
+
<div>
|
|
144
|
+
<h5 className="flex items-center gap-2 text-xs text-gray-500 uppercase tracking-wider mb-2">
|
|
145
|
+
<FileCode className="w-3.5 h-3.5" /> Archivos
|
|
146
|
+
</h5>
|
|
147
|
+
<div className="flex flex-wrap gap-2">
|
|
148
|
+
{task.affected_files.map((file, idx) => (
|
|
149
|
+
<button
|
|
150
|
+
key={idx}
|
|
151
|
+
onClick={() => copyToClipboard(file, `file-${task.id}-${idx}`)}
|
|
152
|
+
className="group flex items-center gap-2 text-xs bg-white/5 px-3 py-1.5 rounded-lg border border-white/10 hover:border-blue-500/30 transition-all"
|
|
153
|
+
>
|
|
154
|
+
<code className="text-blue-400">{file}</code>
|
|
155
|
+
{copiedItem === `file-${task.id}-${idx}` ? (
|
|
156
|
+
<Check className="w-3 h-3 text-emerald-400" />
|
|
157
|
+
) : (
|
|
158
|
+
<Copy className="w-3 h-3 text-gray-500 group-hover:text-blue-400" />
|
|
159
|
+
)}
|
|
160
|
+
</button>
|
|
161
|
+
))}
|
|
162
|
+
</div>
|
|
163
|
+
</div>
|
|
164
|
+
)}
|
|
165
|
+
|
|
166
|
+
{/* Resources */}
|
|
167
|
+
{task.reused_resources?.length > 0 && (
|
|
168
|
+
<div>
|
|
169
|
+
<h5 className="flex items-center gap-2 text-xs text-gray-500 uppercase tracking-wider mb-2">
|
|
170
|
+
<Recycle className="w-3.5 h-3.5" /> Recursos
|
|
171
|
+
</h5>
|
|
172
|
+
<div className="flex flex-wrap gap-2">
|
|
173
|
+
{task.reused_resources.map((r, idx) => (
|
|
174
|
+
<span key={idx} className="px-3 py-1.5 text-xs bg-cyan-500/10 text-cyan-400 rounded-lg border border-cyan-500/20">
|
|
175
|
+
{r}
|
|
176
|
+
</span>
|
|
177
|
+
))}
|
|
178
|
+
</div>
|
|
179
|
+
</div>
|
|
180
|
+
)}
|
|
181
|
+
|
|
182
|
+
{/* AI Notes */}
|
|
183
|
+
{task.ai_notes?.trim() && (
|
|
184
|
+
<div className="bg-blue-500/5 border border-blue-500/20 rounded-xl p-4">
|
|
185
|
+
<h5 className="flex items-center gap-2 text-xs text-blue-400 uppercase tracking-wider mb-2">
|
|
186
|
+
<Bot className="w-3.5 h-3.5" /> Notas IA
|
|
187
|
+
</h5>
|
|
188
|
+
<p className="text-sm text-gray-300">{task.ai_notes}</p>
|
|
189
|
+
</div>
|
|
190
|
+
)}
|
|
191
|
+
|
|
192
|
+
{/* Debt */}
|
|
193
|
+
{task.technical_debt?.length > 0 && (
|
|
194
|
+
<div className="bg-orange-500/5 border border-orange-500/20 rounded-xl p-4">
|
|
195
|
+
<h5 className="flex items-center gap-2 text-xs text-orange-400 uppercase tracking-wider mb-3">
|
|
196
|
+
<AlertTriangle className="w-3.5 h-3.5" /> Deuda ({task.technical_debt.length})
|
|
197
|
+
</h5>
|
|
198
|
+
<div className="space-y-2">
|
|
199
|
+
{task.technical_debt.map((debt, idx) => (
|
|
200
|
+
<div key={idx} className="flex items-start gap-3">
|
|
201
|
+
<span className={`flex-shrink-0 px-2 py-0.5 text-[10px] font-bold rounded uppercase ${
|
|
202
|
+
debt.severity === 'high' ? 'bg-rose-500 text-white' :
|
|
203
|
+
debt.severity === 'medium' ? 'bg-amber-500 text-black' :
|
|
204
|
+
'bg-blue-500 text-white'
|
|
205
|
+
}`}>
|
|
206
|
+
{debt.severity}
|
|
207
|
+
</span>
|
|
208
|
+
<div>
|
|
209
|
+
<p className="text-sm text-gray-300">{debt.description}</p>
|
|
210
|
+
<p className="text-xs text-gray-500 mt-1">{debt.estimated_effort}</p>
|
|
211
|
+
</div>
|
|
212
|
+
</div>
|
|
213
|
+
))}
|
|
214
|
+
</div>
|
|
215
|
+
</div>
|
|
216
|
+
)}
|
|
217
|
+
|
|
218
|
+
{/* Git */}
|
|
219
|
+
{task.git?.last_commit && (
|
|
220
|
+
<div className="flex items-center gap-4 pt-3 border-t border-white/5">
|
|
221
|
+
<div className="flex items-center gap-2 text-xs text-gray-500">
|
|
222
|
+
<GitCommit className="w-3.5 h-3.5" />
|
|
223
|
+
<code className="text-blue-400">{task.git.last_commit.substring(0, 7)}</code>
|
|
224
|
+
</div>
|
|
225
|
+
{task.git.pr_url && (
|
|
226
|
+
<a href={task.git.pr_url} target="_blank" rel="noopener noreferrer" className="flex items-center gap-1.5 text-xs text-cyan-400 hover:text-cyan-300">
|
|
227
|
+
PR #{task.git.pr_number}
|
|
228
|
+
<ExternalLink className="w-3 h-3" />
|
|
229
|
+
</a>
|
|
230
|
+
)}
|
|
231
|
+
</div>
|
|
232
|
+
)}
|
|
233
|
+
|
|
234
|
+
{/* Status buttons */}
|
|
235
|
+
<div className="flex items-center gap-2 pt-3 border-t border-white/5">
|
|
236
|
+
<span className="text-xs text-gray-500">Estado:</span>
|
|
237
|
+
<div className="flex gap-1">
|
|
238
|
+
{['pending', 'in_progress', 'completed'].map((s) => {
|
|
239
|
+
const cfg = statusConfig[s];
|
|
240
|
+
const Icon = cfg.icon;
|
|
241
|
+
const active = task.status === s;
|
|
242
|
+
return (
|
|
243
|
+
<button
|
|
244
|
+
key={s}
|
|
245
|
+
onClick={() => onUpdateStatus?.(featureId, task.id, s)}
|
|
246
|
+
className={`flex items-center gap-1.5 px-3 py-1.5 text-xs rounded-lg border transition-all ${
|
|
247
|
+
active ? `${cfg.bg} ${cfg.border} ${cfg.color}` : 'border-white/10 text-gray-500 hover:border-white/20'
|
|
248
|
+
}`}
|
|
249
|
+
>
|
|
250
|
+
<Icon className="w-3 h-3" />
|
|
251
|
+
{cfg.label}
|
|
252
|
+
</button>
|
|
253
|
+
);
|
|
254
|
+
})}
|
|
255
|
+
</div>
|
|
256
|
+
</div>
|
|
257
|
+
</div>
|
|
258
|
+
)}
|
|
259
|
+
</div>
|
|
260
|
+
);
|
|
261
|
+
})}
|
|
262
|
+
</div>
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function MetricCard({ label, value, color = 'text-white' }) {
|
|
267
|
+
return (
|
|
268
|
+
<div className="bg-white/5 rounded-lg p-3 border border-white/5">
|
|
269
|
+
<div className="text-[10px] text-gray-500 uppercase tracking-wider mb-1">{label}</div>
|
|
270
|
+
<div className={`text-lg font-bold ${color}`}>{value}</div>
|
|
271
|
+
</div>
|
|
272
|
+
);
|
|
273
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { AlertTriangle, Clock, TrendingDown, Zap } from 'lucide-react';
|
|
3
|
+
|
|
4
|
+
export default function TechnicalDebt({ roadmap = {} }) {
|
|
5
|
+
const allDebts = [];
|
|
6
|
+
|
|
7
|
+
if (roadmap.features) {
|
|
8
|
+
roadmap.features.forEach(feature => {
|
|
9
|
+
feature.tasks?.forEach(task => {
|
|
10
|
+
task.technical_debt?.forEach(debt => {
|
|
11
|
+
allDebts.push({
|
|
12
|
+
...debt,
|
|
13
|
+
taskName: task.name,
|
|
14
|
+
featureName: feature.name,
|
|
15
|
+
taskId: task.id
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const grouped = {
|
|
23
|
+
high: allDebts.filter(d => d.severity === 'high'),
|
|
24
|
+
medium: allDebts.filter(d => d.severity === 'medium'),
|
|
25
|
+
low: allDebts.filter(d => d.severity === 'low')
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
if (allDebts.length === 0) {
|
|
29
|
+
return (
|
|
30
|
+
<div className="card-dark rounded-2xl p-12 text-center">
|
|
31
|
+
<div className="w-16 h-16 mx-auto mb-4 rounded-full bg-emerald-500/10 flex items-center justify-center">
|
|
32
|
+
<TrendingDown className="w-8 h-8 text-emerald-400" />
|
|
33
|
+
</div>
|
|
34
|
+
<h3 className="text-lg font-semibold text-emerald-400 mb-2">Sin deuda tecnica</h3>
|
|
35
|
+
<p className="text-gray-500 text-sm">No hay deuda tecnica registrada</p>
|
|
36
|
+
</div>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<div className="space-y-6">
|
|
42
|
+
{/* Stats */}
|
|
43
|
+
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 stagger">
|
|
44
|
+
<StatCard label="Total" value={allDebts.length} gradient="from-blue-500 to-cyan-500" icon={AlertTriangle} />
|
|
45
|
+
<StatCard label="Alta" value={grouped.high.length} gradient="from-rose-500 to-orange-500" icon={Zap} />
|
|
46
|
+
<StatCard label="Media" value={grouped.medium.length} gradient="from-amber-500 to-yellow-500" icon={AlertTriangle} />
|
|
47
|
+
<StatCard label="Baja" value={grouped.low.length} gradient="from-cyan-500 to-teal-500" icon={Clock} />
|
|
48
|
+
</div>
|
|
49
|
+
|
|
50
|
+
{/* Distribution Bar */}
|
|
51
|
+
<div className="card-dark rounded-2xl p-6">
|
|
52
|
+
<h3 className="text-sm text-gray-500 uppercase tracking-wider mb-4">Distribucion</h3>
|
|
53
|
+
<div className="flex h-4 rounded-full overflow-hidden bg-white/5">
|
|
54
|
+
{grouped.high.length > 0 && (
|
|
55
|
+
<div
|
|
56
|
+
className="bg-gradient-to-r from-rose-500 to-orange-500 transition-all"
|
|
57
|
+
style={{ width: `${(grouped.high.length / allDebts.length) * 100}%` }}
|
|
58
|
+
/>
|
|
59
|
+
)}
|
|
60
|
+
{grouped.medium.length > 0 && (
|
|
61
|
+
<div
|
|
62
|
+
className="bg-gradient-to-r from-amber-500 to-yellow-500 transition-all"
|
|
63
|
+
style={{ width: `${(grouped.medium.length / allDebts.length) * 100}%` }}
|
|
64
|
+
/>
|
|
65
|
+
)}
|
|
66
|
+
{grouped.low.length > 0 && (
|
|
67
|
+
<div
|
|
68
|
+
className="bg-gradient-to-r from-cyan-500 to-teal-500 transition-all"
|
|
69
|
+
style={{ width: `${(grouped.low.length / allDebts.length) * 100}%` }}
|
|
70
|
+
/>
|
|
71
|
+
)}
|
|
72
|
+
</div>
|
|
73
|
+
<div className="flex justify-between mt-3 text-xs">
|
|
74
|
+
<span className="text-rose-400">{Math.round((grouped.high.length / allDebts.length) * 100)}% Alta</span>
|
|
75
|
+
<span className="text-amber-400">{Math.round((grouped.medium.length / allDebts.length) * 100)}% Media</span>
|
|
76
|
+
<span className="text-cyan-400">{Math.round((grouped.low.length / allDebts.length) * 100)}% Baja</span>
|
|
77
|
+
</div>
|
|
78
|
+
</div>
|
|
79
|
+
|
|
80
|
+
{/* Debt by Severity */}
|
|
81
|
+
{['high', 'medium', 'low'].map(sev => (
|
|
82
|
+
grouped[sev].length > 0 && (
|
|
83
|
+
<DebtSection key={sev} severity={sev} debts={grouped[sev]} />
|
|
84
|
+
)
|
|
85
|
+
))}
|
|
86
|
+
</div>
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function StatCard({ label, value, gradient, icon: Icon }) {
|
|
91
|
+
return (
|
|
92
|
+
<div className="card-dark rounded-2xl p-5 hover:scale-[1.02] transition-all">
|
|
93
|
+
<div className="flex items-center justify-between">
|
|
94
|
+
<div>
|
|
95
|
+
<p className="text-xs text-gray-500 uppercase tracking-wider mb-1">{label}</p>
|
|
96
|
+
<p className={`text-3xl font-bold bg-gradient-to-r ${gradient} bg-clip-text text-transparent`}>{value}</p>
|
|
97
|
+
</div>
|
|
98
|
+
<Icon className={`w-8 h-8 opacity-50 bg-gradient-to-r ${gradient} bg-clip-text`} style={{ color: 'currentColor' }} />
|
|
99
|
+
</div>
|
|
100
|
+
</div>
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function DebtSection({ severity, debts }) {
|
|
105
|
+
const config = {
|
|
106
|
+
high: {
|
|
107
|
+
title: 'Severidad Alta',
|
|
108
|
+
textColor: 'text-rose-400',
|
|
109
|
+
bgColor: 'bg-rose-500/5',
|
|
110
|
+
borderColor: 'border-rose-500/20',
|
|
111
|
+
badgeBg: 'bg-gradient-to-r from-rose-500 to-orange-500',
|
|
112
|
+
},
|
|
113
|
+
medium: {
|
|
114
|
+
title: 'Severidad Media',
|
|
115
|
+
textColor: 'text-amber-400',
|
|
116
|
+
bgColor: 'bg-amber-500/5',
|
|
117
|
+
borderColor: 'border-amber-500/20',
|
|
118
|
+
badgeBg: 'bg-gradient-to-r from-amber-500 to-yellow-500',
|
|
119
|
+
},
|
|
120
|
+
low: {
|
|
121
|
+
title: 'Severidad Baja',
|
|
122
|
+
textColor: 'text-cyan-400',
|
|
123
|
+
bgColor: 'bg-cyan-500/5',
|
|
124
|
+
borderColor: 'border-cyan-500/20',
|
|
125
|
+
badgeBg: 'bg-gradient-to-r from-cyan-500 to-teal-500',
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const cfg = config[severity];
|
|
130
|
+
|
|
131
|
+
return (
|
|
132
|
+
<div className="space-y-3">
|
|
133
|
+
<div className="flex items-center gap-3">
|
|
134
|
+
<AlertTriangle className={`w-5 h-5 ${cfg.textColor}`} />
|
|
135
|
+
<h3 className={`text-lg font-semibold ${cfg.textColor}`}>{cfg.title}</h3>
|
|
136
|
+
<span className={`px-2.5 py-0.5 text-xs font-bold text-white rounded-full ${cfg.badgeBg}`}>
|
|
137
|
+
{debts.length}
|
|
138
|
+
</span>
|
|
139
|
+
</div>
|
|
140
|
+
|
|
141
|
+
<div className="space-y-2">
|
|
142
|
+
{debts.map((debt, idx) => (
|
|
143
|
+
<div
|
|
144
|
+
key={idx}
|
|
145
|
+
className={`card-dark rounded-xl p-5 ${cfg.bgColor} ${cfg.borderColor} border hover:scale-[1.01] transition-all`}
|
|
146
|
+
>
|
|
147
|
+
<div className="flex items-start gap-4">
|
|
148
|
+
<div className={`p-2.5 bg-white/5 rounded-lg ${cfg.borderColor} border`}>
|
|
149
|
+
<AlertTriangle className={`w-5 h-5 ${cfg.textColor}`} />
|
|
150
|
+
</div>
|
|
151
|
+
<div className="flex-1 min-w-0">
|
|
152
|
+
<p className="text-white font-medium mb-2">{debt.description}</p>
|
|
153
|
+
<div className="flex flex-wrap items-center gap-x-4 gap-y-2 text-sm">
|
|
154
|
+
<span className="text-gray-500">Feature: <span className="text-blue-400">{debt.featureName}</span></span>
|
|
155
|
+
<span className="text-gray-500">Tarea: <span className="text-cyan-400">{debt.taskName}</span></span>
|
|
156
|
+
<span className="flex items-center gap-1 text-gray-500">
|
|
157
|
+
<Clock className="w-3.5 h-3.5" /> {debt.estimated_effort}
|
|
158
|
+
</span>
|
|
159
|
+
</div>
|
|
160
|
+
</div>
|
|
161
|
+
<span className={`flex-shrink-0 px-2.5 py-1 text-[10px] font-bold text-white rounded-full uppercase tracking-wider ${cfg.badgeBg}`}>
|
|
162
|
+
{severity}
|
|
163
|
+
</span>
|
|
164
|
+
</div>
|
|
165
|
+
</div>
|
|
166
|
+
))}
|
|
167
|
+
</div>
|
|
168
|
+
</div>
|
|
169
|
+
);
|
|
170
|
+
}
|