prjct-cli 0.13.2 → 0.15.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/CHANGELOG.md +106 -0
- package/bin/prjct +10 -13
- package/core/agentic/memory-system/semantic-memories.ts +2 -1
- package/core/agentic/plan-mode/plan-mode.ts +2 -1
- package/core/agentic/prompt-builder.ts +22 -43
- package/core/agentic/services.ts +5 -5
- package/core/agentic/smart-context.ts +7 -2
- package/core/command-registry/core-commands.ts +54 -29
- package/core/command-registry/optional-commands.ts +64 -0
- package/core/command-registry/setup-commands.ts +18 -3
- package/core/commands/analysis.ts +21 -68
- package/core/commands/analytics.ts +247 -213
- package/core/commands/base.ts +1 -1
- package/core/commands/index.ts +41 -36
- package/core/commands/maintenance.ts +300 -31
- package/core/commands/planning.ts +233 -22
- package/core/commands/setup.ts +3 -8
- package/core/commands/shipping.ts +14 -18
- package/core/commands/types.ts +8 -6
- package/core/commands/workflow.ts +105 -100
- package/core/context/generator.ts +317 -0
- package/core/context-sync.ts +7 -350
- package/core/data/index.ts +13 -32
- package/core/data/md-ideas-manager.ts +155 -0
- package/core/data/md-queue-manager.ts +4 -3
- package/core/data/md-shipped-manager.ts +90 -0
- package/core/data/md-state-manager.ts +11 -7
- package/core/domain/agent-generator.ts +23 -63
- package/core/events/index.ts +143 -0
- package/core/index.ts +17 -14
- package/core/infrastructure/capability-installer.ts +13 -149
- package/core/infrastructure/migrator/project-scanner.ts +2 -1
- package/core/infrastructure/path-manager.ts +4 -6
- package/core/infrastructure/setup.ts +3 -0
- package/core/infrastructure/uuid-migration.ts +750 -0
- package/core/outcomes/recorder.ts +2 -1
- package/core/plugin/loader.ts +4 -7
- package/core/plugin/registry.ts +3 -3
- package/core/schemas/index.ts +23 -25
- package/core/schemas/state.ts +1 -0
- package/core/serializers/ideas-serializer.ts +187 -0
- package/core/serializers/index.ts +16 -0
- package/core/serializers/shipped-serializer.ts +108 -0
- package/core/session/utils.ts +3 -9
- package/core/storage/ideas-storage.ts +273 -0
- package/core/storage/index.ts +204 -0
- package/core/storage/queue-storage.ts +297 -0
- package/core/storage/shipped-storage.ts +223 -0
- package/core/storage/state-storage.ts +235 -0
- package/core/storage/storage-manager.ts +175 -0
- package/package.json +1 -1
- package/packages/web/app/api/projects/[id]/momentum/route.ts +257 -0
- package/packages/web/app/api/sessions/current/route.ts +132 -0
- package/packages/web/app/api/sessions/history/route.ts +96 -14
- package/packages/web/app/globals.css +5 -0
- package/packages/web/app/layout.tsx +2 -0
- package/packages/web/app/project/[id]/code/layout.tsx +18 -0
- package/packages/web/app/project/[id]/code/page.tsx +408 -0
- package/packages/web/app/project/[id]/page.tsx +359 -389
- package/packages/web/app/project/[id]/reports/page.tsx +59 -0
- package/packages/web/app/project/[id]/reports/print/page.tsx +58 -0
- package/packages/web/components/ActivityTimeline/ActivityTimeline.tsx +0 -1
- package/packages/web/components/AgentsCard/AgentsCard.tsx +64 -34
- package/packages/web/components/AgentsCard/AgentsCard.types.ts +1 -0
- package/packages/web/components/AppSidebar/AppSidebar.tsx +135 -11
- package/packages/web/components/BentoCard/BentoCard.constants.ts +3 -3
- package/packages/web/components/BentoCard/BentoCard.tsx +2 -1
- package/packages/web/components/BentoGrid/BentoGrid.tsx +2 -2
- package/packages/web/components/BlockersCard/BlockersCard.tsx +65 -57
- package/packages/web/components/BlockersCard/BlockersCard.types.ts +1 -0
- package/packages/web/components/CommandBar/CommandBar.tsx +67 -0
- package/packages/web/components/CommandBar/index.ts +1 -0
- package/packages/web/components/DashboardContent/DashboardContent.tsx +35 -5
- package/packages/web/components/DateGroup/DateGroup.tsx +1 -1
- package/packages/web/components/EmptyState/EmptyState.tsx +39 -21
- package/packages/web/components/EmptyState/EmptyState.types.ts +1 -0
- package/packages/web/components/EventRow/EventRow.tsx +4 -4
- package/packages/web/components/EventRow/EventRow.utils.ts +3 -3
- package/packages/web/components/HeroSection/HeroSection.tsx +52 -15
- package/packages/web/components/HeroSection/HeroSection.types.ts +4 -4
- package/packages/web/components/HeroSection/HeroSection.utils.ts +7 -3
- package/packages/web/components/IdeasCard/IdeasCard.tsx +94 -27
- package/packages/web/components/IdeasCard/IdeasCard.types.ts +1 -0
- package/packages/web/components/MasonryGrid/MasonryGrid.tsx +18 -0
- package/packages/web/components/MasonryGrid/index.ts +1 -0
- package/packages/web/components/MomentumWidget/MomentumWidget.tsx +119 -0
- package/packages/web/components/MomentumWidget/MomentumWidget.types.ts +16 -0
- package/packages/web/components/MomentumWidget/index.ts +2 -0
- package/packages/web/components/NowCard/NowCard.tsx +81 -56
- package/packages/web/components/NowCard/NowCard.types.ts +1 -0
- package/packages/web/components/PageHeader/PageHeader.tsx +24 -0
- package/packages/web/components/PageHeader/index.ts +1 -0
- package/packages/web/components/ProgressRing/ProgressRing.constants.ts +2 -2
- package/packages/web/components/ProjectAvatar/ProjectAvatar.tsx +2 -2
- package/packages/web/components/ProjectColorDot/ProjectColorDot.tsx +37 -0
- package/packages/web/components/ProjectColorDot/index.ts +1 -0
- package/packages/web/components/ProjectSelectorModal/ProjectSelectorModal.tsx +104 -0
- package/packages/web/components/ProjectSelectorModal/index.ts +1 -0
- package/packages/web/components/Providers/Providers.tsx +4 -1
- package/packages/web/components/QueueCard/QueueCard.tsx +78 -25
- package/packages/web/components/QueueCard/QueueCard.types.ts +1 -0
- package/packages/web/components/QueueCard/QueueCard.utils.ts +3 -3
- package/packages/web/components/RecoverCard/RecoverCard.tsx +72 -0
- package/packages/web/components/RecoverCard/RecoverCard.types.ts +16 -0
- package/packages/web/components/RecoverCard/index.ts +2 -0
- package/packages/web/components/RoadmapCard/RoadmapCard.tsx +101 -33
- package/packages/web/components/RoadmapCard/RoadmapCard.types.ts +1 -0
- package/packages/web/components/ShipsCard/ShipsCard.tsx +71 -28
- package/packages/web/components/ShipsCard/ShipsCard.types.ts +2 -0
- package/packages/web/components/SparklineChart/SparklineChart.tsx +20 -18
- package/packages/web/components/StatsMasonry/StatsMasonry.tsx +95 -0
- package/packages/web/components/StatsMasonry/index.ts +1 -0
- package/packages/web/components/StreakCard/StreakCard.tsx +37 -35
- package/packages/web/components/TasksCounter/TasksCounter.tsx +1 -1
- package/packages/web/components/TechStackBadges/TechStackBadges.tsx +12 -4
- package/packages/web/components/TerminalDock/DockToggleTab.tsx +29 -0
- package/packages/web/components/TerminalDock/TerminalDock.tsx +386 -0
- package/packages/web/components/TerminalDock/TerminalDockTab.tsx +130 -0
- package/packages/web/components/TerminalDock/TerminalTabBar.tsx +142 -0
- package/packages/web/components/TerminalDock/index.ts +2 -0
- package/packages/web/components/VelocityBadge/VelocityBadge.tsx +8 -3
- package/packages/web/components/VelocityCard/VelocityCard.tsx +49 -47
- package/packages/web/components/WeeklyReports/PrintableReport.tsx +259 -0
- package/packages/web/components/WeeklyReports/ReportPreviewCard.tsx +187 -0
- package/packages/web/components/WeeklyReports/WeekCalendar.tsx +288 -0
- package/packages/web/components/WeeklyReports/WeeklyReports.tsx +149 -0
- package/packages/web/components/WeeklyReports/index.ts +4 -0
- package/packages/web/components/WeeklySparkline/WeeklySparkline.tsx +16 -4
- package/packages/web/components/WeeklySparkline/WeeklySparkline.types.ts +1 -0
- package/packages/web/components/charts/SessionsChart.tsx +6 -3
- package/packages/web/components/ui/dialog.tsx +143 -0
- package/packages/web/components/ui/drawer.tsx +135 -0
- package/packages/web/components/ui/select.tsx +187 -0
- package/packages/web/context/GlobalTerminalContext.tsx +538 -0
- package/packages/web/lib/commands.ts +81 -0
- package/packages/web/lib/generate-week-report.ts +285 -0
- package/packages/web/lib/parse-prjct-files.ts +56 -55
- package/packages/web/lib/project-colors.ts +58 -0
- package/packages/web/lib/projects.ts +58 -5
- package/packages/web/lib/services/projects.server.ts +11 -1
- package/packages/web/next-env.d.ts +1 -1
- package/packages/web/package.json +5 -1
- package/templates/commands/analyze.md +39 -3
- package/templates/commands/ask.md +58 -3
- package/templates/commands/bug.md +117 -26
- package/templates/commands/dash.md +95 -158
- package/templates/commands/done.md +130 -148
- package/templates/commands/feature.md +125 -103
- package/templates/commands/git.md +18 -3
- package/templates/commands/idea.md +121 -38
- package/templates/commands/init.md +124 -20
- package/templates/commands/migrate-all.md +63 -28
- package/templates/commands/migrate.md +140 -0
- package/templates/commands/next.md +115 -5
- package/templates/commands/now.md +146 -82
- package/templates/commands/pause.md +89 -74
- package/templates/commands/redo.md +6 -4
- package/templates/commands/resume.md +141 -59
- package/templates/commands/ship.md +103 -231
- package/templates/commands/spec.md +98 -8
- package/templates/commands/suggest.md +22 -2
- package/templates/commands/sync.md +192 -203
- package/templates/commands/undo.md +6 -4
- package/core/data/agents-manager.ts +0 -76
- package/core/data/analysis-manager.ts +0 -83
- package/core/data/base-manager.ts +0 -156
- package/core/data/ideas-manager.ts +0 -81
- package/core/data/outcomes-manager.ts +0 -96
- package/core/data/project-manager.ts +0 -75
- package/core/data/roadmap-manager.ts +0 -118
- package/core/data/shipped-manager.ts +0 -65
- package/core/data/state-manager.ts +0 -214
- package/core/state/index.ts +0 -25
- package/core/state/manager.ts +0 -376
- package/core/state/types.ts +0 -185
- package/core/utils/project-capabilities.ts +0 -156
- package/core/view-generator.ts +0 -536
- package/packages/web/app/project/[id]/stats/loading.tsx +0 -43
- package/packages/web/app/project/[id]/stats/page.tsx +0 -253
- package/templates/agent-assignment.md +0 -72
- package/templates/analysis/project-analysis.md +0 -78
- package/templates/checklists/accessibility.md +0 -33
- package/templates/commands/build.md +0 -17
- package/templates/commands/decision.md +0 -226
- package/templates/commands/fix.md +0 -79
- package/templates/commands/help.md +0 -61
- package/templates/commands/progress.md +0 -14
- package/templates/commands/recap.md +0 -14
- package/templates/commands/roadmap.md +0 -52
- package/templates/commands/status.md +0 -17
- package/templates/commands/task.md +0 -63
- package/templates/commands/work.md +0 -44
- package/templates/commands/workflow.md +0 -12
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* TerminalDockTab - Single terminal instance for the dock
|
|
5
|
+
* With ResizeObserver for proper fit on resize
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { useEffect, useRef, useCallback } from 'react'
|
|
9
|
+
import { useClaudeTerminal } from '@/hooks/useClaudeTerminal'
|
|
10
|
+
import { useGlobalTerminal, type GlobalTerminalSession } from '@/context/GlobalTerminalContext'
|
|
11
|
+
|
|
12
|
+
interface TerminalDockTabProps {
|
|
13
|
+
session: GlobalTerminalSession
|
|
14
|
+
isActive: boolean
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function TerminalDockTab({ session, isActive }: TerminalDockTabProps) {
|
|
18
|
+
const containerRef = useRef<HTMLDivElement>(null)
|
|
19
|
+
const wrapperRef = useRef<HTMLDivElement>(null)
|
|
20
|
+
const hasInitializedRef = useRef(false)
|
|
21
|
+
const hasConnectedRef = useRef(false)
|
|
22
|
+
const fitTimeoutRef = useRef<NodeJS.Timeout | null>(null)
|
|
23
|
+
const { updateSession, registerSendInput, registerFocusTerminal, dockHeight } = useGlobalTerminal()
|
|
24
|
+
|
|
25
|
+
const handleConnect = useCallback(() => {
|
|
26
|
+
updateSession(session.id, { isConnected: true, isLoading: false })
|
|
27
|
+
}, [session.id, updateSession])
|
|
28
|
+
|
|
29
|
+
const handleDisconnect = useCallback(() => {
|
|
30
|
+
updateSession(session.id, { isConnected: false, isLoading: false })
|
|
31
|
+
}, [session.id, updateSession])
|
|
32
|
+
|
|
33
|
+
const handleError = useCallback((error: string) => {
|
|
34
|
+
console.error(`[TerminalDock ${session.id}] Error:`, error)
|
|
35
|
+
updateSession(session.id, { isLoading: false })
|
|
36
|
+
}, [session.id, updateSession])
|
|
37
|
+
|
|
38
|
+
const {
|
|
39
|
+
initTerminal,
|
|
40
|
+
connect,
|
|
41
|
+
disconnect,
|
|
42
|
+
sendInput,
|
|
43
|
+
focusTerminal,
|
|
44
|
+
fit,
|
|
45
|
+
} = useClaudeTerminal({
|
|
46
|
+
sessionId: session.id,
|
|
47
|
+
projectDir: session.projectPath,
|
|
48
|
+
onConnect: handleConnect,
|
|
49
|
+
onDisconnect: handleDisconnect,
|
|
50
|
+
onError: handleError,
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
// Debounced fit function
|
|
54
|
+
const debouncedFit = useCallback(() => {
|
|
55
|
+
if (fitTimeoutRef.current) {
|
|
56
|
+
clearTimeout(fitTimeoutRef.current)
|
|
57
|
+
}
|
|
58
|
+
fitTimeoutRef.current = setTimeout(() => {
|
|
59
|
+
if (isActive && hasInitializedRef.current) {
|
|
60
|
+
fit()
|
|
61
|
+
}
|
|
62
|
+
}, 50)
|
|
63
|
+
}, [fit, isActive])
|
|
64
|
+
|
|
65
|
+
// Initialize terminal AND connect - only once
|
|
66
|
+
useEffect(() => {
|
|
67
|
+
if (containerRef.current && !hasInitializedRef.current) {
|
|
68
|
+
hasInitializedRef.current = true
|
|
69
|
+
|
|
70
|
+
initTerminal(containerRef.current).then(() => {
|
|
71
|
+
if (!hasConnectedRef.current) {
|
|
72
|
+
hasConnectedRef.current = true
|
|
73
|
+
connect()
|
|
74
|
+
}
|
|
75
|
+
})
|
|
76
|
+
}
|
|
77
|
+
}, []) // Empty deps - run only on mount
|
|
78
|
+
|
|
79
|
+
// Re-fit terminal when tab becomes active or dock height changes
|
|
80
|
+
useEffect(() => {
|
|
81
|
+
if (isActive && hasInitializedRef.current) {
|
|
82
|
+
requestAnimationFrame(() => {
|
|
83
|
+
fit()
|
|
84
|
+
})
|
|
85
|
+
}
|
|
86
|
+
}, [isActive, fit, dockHeight])
|
|
87
|
+
|
|
88
|
+
// ResizeObserver for container size changes
|
|
89
|
+
useEffect(() => {
|
|
90
|
+
if (!wrapperRef.current) return
|
|
91
|
+
|
|
92
|
+
const resizeObserver = new ResizeObserver(() => {
|
|
93
|
+
debouncedFit()
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
resizeObserver.observe(wrapperRef.current)
|
|
97
|
+
|
|
98
|
+
return () => {
|
|
99
|
+
resizeObserver.disconnect()
|
|
100
|
+
if (fitTimeoutRef.current) {
|
|
101
|
+
clearTimeout(fitTimeoutRef.current)
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}, [debouncedFit])
|
|
105
|
+
|
|
106
|
+
// Register sendInput and focusTerminal for this session
|
|
107
|
+
useEffect(() => {
|
|
108
|
+
registerSendInput(session.id, sendInput)
|
|
109
|
+
registerFocusTerminal(session.id, focusTerminal)
|
|
110
|
+
}, [session.id, sendInput, focusTerminal, registerSendInput, registerFocusTerminal])
|
|
111
|
+
|
|
112
|
+
// Expose disconnect for external use
|
|
113
|
+
useEffect(() => {
|
|
114
|
+
const key = `terminal_disconnect_${session.id}`
|
|
115
|
+
;(window as unknown as Record<string, () => void>)[key] = disconnect
|
|
116
|
+
return () => {
|
|
117
|
+
delete (window as unknown as Record<string, () => void>)[key]
|
|
118
|
+
}
|
|
119
|
+
}, [session.id, disconnect])
|
|
120
|
+
|
|
121
|
+
return (
|
|
122
|
+
<div
|
|
123
|
+
ref={wrapperRef}
|
|
124
|
+
className="absolute inset-0 bg-[#0a0a0f] px-2 py-2"
|
|
125
|
+
style={{ display: isActive ? 'block' : 'none' }}
|
|
126
|
+
>
|
|
127
|
+
<div ref={containerRef} className="h-full w-full" />
|
|
128
|
+
</div>
|
|
129
|
+
)
|
|
130
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useState, useRef, useEffect } from 'react'
|
|
4
|
+
import { X, Plus } from 'lucide-react'
|
|
5
|
+
import { cn } from '@/lib/utils'
|
|
6
|
+
import type { GlobalTerminalSession } from '@/context/GlobalTerminalContext'
|
|
7
|
+
|
|
8
|
+
interface TerminalTabBarProps {
|
|
9
|
+
sessions: GlobalTerminalSession[]
|
|
10
|
+
activeSessionId: string | null
|
|
11
|
+
onSwitchSession: (sessionId: string) => void
|
|
12
|
+
onCloseSession: (sessionId: string) => void
|
|
13
|
+
onNewTerminal: () => void
|
|
14
|
+
onRenameSession: (sessionId: string, newLabel: string) => void
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function TerminalTabBar({
|
|
18
|
+
sessions,
|
|
19
|
+
activeSessionId,
|
|
20
|
+
onSwitchSession,
|
|
21
|
+
onCloseSession,
|
|
22
|
+
onNewTerminal,
|
|
23
|
+
onRenameSession,
|
|
24
|
+
}: TerminalTabBarProps) {
|
|
25
|
+
const [editingSessionId, setEditingSessionId] = useState<string | null>(null)
|
|
26
|
+
const [editValue, setEditValue] = useState('')
|
|
27
|
+
const inputRef = useRef<HTMLInputElement>(null)
|
|
28
|
+
|
|
29
|
+
// Group sessions by project
|
|
30
|
+
const sessionsByProject = sessions.reduce((acc, session) => {
|
|
31
|
+
const existing = acc.find(g => g.projectId === session.projectId)
|
|
32
|
+
if (existing) {
|
|
33
|
+
existing.sessions.push(session)
|
|
34
|
+
} else {
|
|
35
|
+
acc.push({
|
|
36
|
+
projectId: session.projectId,
|
|
37
|
+
projectName: session.projectName,
|
|
38
|
+
sessions: [session],
|
|
39
|
+
})
|
|
40
|
+
}
|
|
41
|
+
return acc
|
|
42
|
+
}, [] as { projectId: string; projectName: string; sessions: GlobalTerminalSession[] }[])
|
|
43
|
+
|
|
44
|
+
// Focus input when editing starts
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
if (editingSessionId && inputRef.current) {
|
|
47
|
+
inputRef.current.focus()
|
|
48
|
+
inputRef.current.select()
|
|
49
|
+
}
|
|
50
|
+
}, [editingSessionId])
|
|
51
|
+
|
|
52
|
+
const handleDoubleClick = (session: GlobalTerminalSession) => {
|
|
53
|
+
setEditingSessionId(session.id)
|
|
54
|
+
setEditValue(session.label)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const handleBlur = () => {
|
|
58
|
+
if (editingSessionId && editValue.trim()) {
|
|
59
|
+
onRenameSession(editingSessionId, editValue.trim())
|
|
60
|
+
}
|
|
61
|
+
setEditingSessionId(null)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const handleKeyDown = (e: React.KeyboardEvent) => {
|
|
65
|
+
if (e.key === 'Enter') {
|
|
66
|
+
handleBlur()
|
|
67
|
+
} else if (e.key === 'Escape') {
|
|
68
|
+
setEditingSessionId(null)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
<div className="flex items-center gap-0.5 flex-1 overflow-x-auto py-1 px-1">
|
|
74
|
+
{sessionsByProject.map((group, groupIndex) => (
|
|
75
|
+
<div key={group.projectId} className="flex items-center">
|
|
76
|
+
{/* Group separator */}
|
|
77
|
+
{groupIndex > 0 && (
|
|
78
|
+
<div className="w-px h-5 bg-border mx-2" />
|
|
79
|
+
)}
|
|
80
|
+
|
|
81
|
+
{/* Tabs for this project */}
|
|
82
|
+
{group.sessions.map((session) => {
|
|
83
|
+
const isActive = session.id === activeSessionId
|
|
84
|
+
const isEditing = session.id === editingSessionId
|
|
85
|
+
|
|
86
|
+
return (
|
|
87
|
+
<button
|
|
88
|
+
key={session.id}
|
|
89
|
+
onClick={() => onSwitchSession(session.id)}
|
|
90
|
+
onDoubleClick={() => handleDoubleClick(session)}
|
|
91
|
+
className={cn(
|
|
92
|
+
'group flex items-center gap-1.5 px-3 py-1.5 text-xs transition-all relative',
|
|
93
|
+
// Chrome-style: rounded top corners, flat bottom
|
|
94
|
+
'rounded-t-md',
|
|
95
|
+
isActive
|
|
96
|
+
? 'bg-card border-t border-l border-r border-border text-foreground -mb-px z-10'
|
|
97
|
+
: 'text-muted-foreground hover:text-foreground hover:bg-muted/50'
|
|
98
|
+
)}
|
|
99
|
+
>
|
|
100
|
+
{/* Label - editable or display */}
|
|
101
|
+
{isEditing ? (
|
|
102
|
+
<input
|
|
103
|
+
ref={inputRef}
|
|
104
|
+
type="text"
|
|
105
|
+
value={editValue}
|
|
106
|
+
onChange={(e) => setEditValue(e.target.value)}
|
|
107
|
+
onBlur={handleBlur}
|
|
108
|
+
onKeyDown={handleKeyDown}
|
|
109
|
+
className="w-24 bg-transparent border-b border-orange-500 outline-none text-xs"
|
|
110
|
+
onClick={(e) => e.stopPropagation()}
|
|
111
|
+
/>
|
|
112
|
+
) : (
|
|
113
|
+
<span className="truncate max-w-[120px]">
|
|
114
|
+
{session.label}
|
|
115
|
+
</span>
|
|
116
|
+
)}
|
|
117
|
+
|
|
118
|
+
{/* Close button */}
|
|
119
|
+
<X
|
|
120
|
+
className="w-3 h-3 opacity-0 group-hover:opacity-100 hover:text-destructive transition-opacity shrink-0"
|
|
121
|
+
onClick={(e) => {
|
|
122
|
+
e.stopPropagation()
|
|
123
|
+
onCloseSession(session.id)
|
|
124
|
+
}}
|
|
125
|
+
/>
|
|
126
|
+
</button>
|
|
127
|
+
)
|
|
128
|
+
})}
|
|
129
|
+
</div>
|
|
130
|
+
))}
|
|
131
|
+
|
|
132
|
+
{/* Add Terminal Button */}
|
|
133
|
+
<button
|
|
134
|
+
onClick={onNewTerminal}
|
|
135
|
+
className="p-1.5 rounded text-muted-foreground hover:bg-accent hover:text-accent-foreground transition-colors ml-1"
|
|
136
|
+
title="New terminal"
|
|
137
|
+
>
|
|
138
|
+
<Plus className="w-4 h-4" />
|
|
139
|
+
</button>
|
|
140
|
+
</div>
|
|
141
|
+
)
|
|
142
|
+
}
|
|
@@ -2,6 +2,13 @@ import { TrendingUp, TrendingDown } from 'lucide-react'
|
|
|
2
2
|
import { cn } from '@/lib/utils'
|
|
3
3
|
import type { VelocityBadgeProps } from './VelocityBadge.types'
|
|
4
4
|
|
|
5
|
+
function getChangeColor(change: number): string {
|
|
6
|
+
if (change >= 10) return 'bg-emerald-500/10 text-emerald-600 dark:text-emerald-400'
|
|
7
|
+
if (change >= 0) return 'bg-amber-500/10 text-amber-600 dark:text-amber-400'
|
|
8
|
+
if (change >= -10) return 'bg-amber-500/10 text-amber-600 dark:text-amber-400'
|
|
9
|
+
return 'bg-red-500/10 text-red-600 dark:text-red-400'
|
|
10
|
+
}
|
|
11
|
+
|
|
5
12
|
export function VelocityBadge({ change }: VelocityBadgeProps) {
|
|
6
13
|
if (change === 0) return null
|
|
7
14
|
|
|
@@ -13,9 +20,7 @@ export function VelocityBadge({ change }: VelocityBadgeProps) {
|
|
|
13
20
|
<span
|
|
14
21
|
className={cn(
|
|
15
22
|
'inline-flex items-center gap-1 text-xs sm:text-sm font-medium px-2 py-0.5 rounded-md',
|
|
16
|
-
|
|
17
|
-
? 'bg-emerald-500/10 text-emerald-600 dark:text-emerald-400'
|
|
18
|
-
: 'bg-muted text-muted-foreground'
|
|
23
|
+
getChangeColor(change)
|
|
19
24
|
)}
|
|
20
25
|
>
|
|
21
26
|
<Icon className="h-3 w-3 sm:h-3.5 sm:w-3.5" />
|
|
@@ -1,9 +1,17 @@
|
|
|
1
|
-
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
2
3
|
import { SparklineChart } from '@/components/SparklineChart'
|
|
3
4
|
import { Zap, TrendingUp, TrendingDown, Target } from 'lucide-react'
|
|
4
5
|
import { cn } from '@/lib/utils'
|
|
5
6
|
import type { VelocityCardProps } from './VelocityCard.types'
|
|
6
7
|
|
|
8
|
+
function getChangeColor(change: number): string {
|
|
9
|
+
if (change >= 10) return 'text-emerald-600 dark:text-emerald-400'
|
|
10
|
+
if (change >= 0) return 'text-amber-600 dark:text-amber-400'
|
|
11
|
+
if (change >= -10) return 'text-amber-600 dark:text-amber-400'
|
|
12
|
+
return 'text-red-600 dark:text-red-400'
|
|
13
|
+
}
|
|
14
|
+
|
|
7
15
|
export function VelocityCard({
|
|
8
16
|
tasksPerDay,
|
|
9
17
|
weeklyData = [],
|
|
@@ -12,60 +20,54 @@ export function VelocityCard({
|
|
|
12
20
|
className,
|
|
13
21
|
}: VelocityCardProps) {
|
|
14
22
|
return (
|
|
15
|
-
<
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
className=
|
|
20
|
-
|
|
21
|
-
|
|
23
|
+
<div className={cn(
|
|
24
|
+
'relative overflow-hidden rounded-xl border bg-card p-4',
|
|
25
|
+
className
|
|
26
|
+
)}>
|
|
27
|
+
<div className="flex items-center justify-between mb-3">
|
|
28
|
+
<div className="flex items-center gap-2">
|
|
29
|
+
<Zap className="h-4 w-4 text-muted-foreground" />
|
|
30
|
+
<span className="text-xs font-bold uppercase tracking-[0.15em] text-muted-foreground">
|
|
31
|
+
Velocity
|
|
32
|
+
</span>
|
|
33
|
+
</div>
|
|
34
|
+
{change !== 0 && (
|
|
35
|
+
<div className={cn("flex items-center gap-1", getChangeColor(change))}>
|
|
36
|
+
{change >= 0 ? (
|
|
37
|
+
<TrendingUp className="h-3.5 w-3.5" />
|
|
38
|
+
) : (
|
|
39
|
+
<TrendingDown className="h-3.5 w-3.5" />
|
|
40
|
+
)}
|
|
41
|
+
<span className="text-xs font-bold">
|
|
42
|
+
{change >= 0 ? '+' : ''}{change}%
|
|
43
|
+
</span>
|
|
44
|
+
</div>
|
|
45
|
+
)}
|
|
46
|
+
</div>
|
|
47
|
+
|
|
48
|
+
<div className="flex items-end justify-between gap-4">
|
|
22
49
|
<div>
|
|
23
50
|
<p className="text-3xl font-bold tabular-nums">{tasksPerDay}</p>
|
|
24
|
-
<p className="text-xs text-muted-foreground">tasks/day</p>
|
|
51
|
+
<p className="text-xs text-muted-foreground">tasks/day avg</p>
|
|
25
52
|
</div>
|
|
26
53
|
|
|
27
54
|
{weeklyData.length > 0 && (
|
|
28
|
-
<div className="
|
|
29
|
-
<SparklineChart data={weeklyData} height={
|
|
55
|
+
<div className="flex-1 max-w-[120px]">
|
|
56
|
+
<SparklineChart data={weeklyData} height={40} />
|
|
57
|
+
<p className="text-xs text-muted-foreground text-right mt-1">Last 7 days</p>
|
|
30
58
|
</div>
|
|
31
59
|
)}
|
|
60
|
+
</div>
|
|
32
61
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
)}
|
|
41
|
-
<span
|
|
42
|
-
className={cn(
|
|
43
|
-
'text-xs font-medium',
|
|
44
|
-
change >= 0 ? 'text-emerald-600 dark:text-emerald-400' : 'text-muted-foreground'
|
|
45
|
-
)}
|
|
46
|
-
>
|
|
47
|
-
{change >= 0 ? '+' : ''}{change}%
|
|
48
|
-
</span>
|
|
49
|
-
</div>
|
|
50
|
-
)}
|
|
51
|
-
|
|
52
|
-
{estimateAccuracy !== undefined && estimateAccuracy > 0 && (
|
|
53
|
-
<div className="flex items-center gap-1">
|
|
54
|
-
<Target className="h-3 w-3 text-muted-foreground" />
|
|
55
|
-
<span
|
|
56
|
-
className={cn(
|
|
57
|
-
'text-xs font-medium',
|
|
58
|
-
estimateAccuracy >= 70 ? 'text-emerald-600 dark:text-emerald-400' :
|
|
59
|
-
estimateAccuracy >= 40 ? 'text-amber-600 dark:text-amber-400' :
|
|
60
|
-
'text-muted-foreground'
|
|
61
|
-
)}
|
|
62
|
-
>
|
|
63
|
-
{estimateAccuracy}% acc
|
|
64
|
-
</span>
|
|
65
|
-
</div>
|
|
66
|
-
)}
|
|
62
|
+
{estimateAccuracy !== undefined && estimateAccuracy > 0 && (
|
|
63
|
+
<div className="flex items-center gap-1.5 mt-3 pt-3 border-t">
|
|
64
|
+
<Target className="h-3 w-3 text-muted-foreground" />
|
|
65
|
+
<span className="text-xs text-muted-foreground">Estimate accuracy:</span>
|
|
66
|
+
<span className="text-xs font-bold text-muted-foreground">
|
|
67
|
+
{estimateAccuracy}%
|
|
68
|
+
</span>
|
|
67
69
|
</div>
|
|
68
|
-
|
|
69
|
-
</
|
|
70
|
+
)}
|
|
71
|
+
</div>
|
|
70
72
|
)
|
|
71
73
|
}
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useMemo } from 'react'
|
|
4
|
+
import { useParams } from 'next/navigation'
|
|
5
|
+
import Link from 'next/link'
|
|
6
|
+
import { Rocket, CheckCircle2, Bug, Calendar, Printer, ArrowLeft } from 'lucide-react'
|
|
7
|
+
import {
|
|
8
|
+
filterDataByWeek,
|
|
9
|
+
formatDateRange,
|
|
10
|
+
type WeekData,
|
|
11
|
+
} from '@/lib/generate-week-report'
|
|
12
|
+
import type { StatsResult } from '@/lib/services/stats.server'
|
|
13
|
+
|
|
14
|
+
interface PrintableReportProps {
|
|
15
|
+
stats: StatsResult
|
|
16
|
+
projectName: string
|
|
17
|
+
selectedWeeks: number[]
|
|
18
|
+
year: number
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function PrintableReport({
|
|
22
|
+
stats,
|
|
23
|
+
projectName,
|
|
24
|
+
selectedWeeks,
|
|
25
|
+
year,
|
|
26
|
+
}: PrintableReportProps) {
|
|
27
|
+
const params = useParams()
|
|
28
|
+
const projectId = params.id as string
|
|
29
|
+
|
|
30
|
+
const weekDataList = useMemo(() => {
|
|
31
|
+
return selectedWeeks.map(w => filterDataByWeek(stats, year, w))
|
|
32
|
+
}, [stats, year, selectedWeeks])
|
|
33
|
+
|
|
34
|
+
// Aggregate data
|
|
35
|
+
const allShipped = weekDataList.flatMap(w => w.shipped)
|
|
36
|
+
const uniqueShipsMap = new Map<string, typeof allShipped[0]>()
|
|
37
|
+
for (const ship of allShipped) {
|
|
38
|
+
if (!uniqueShipsMap.has(ship.name)) {
|
|
39
|
+
uniqueShipsMap.set(ship.name, ship)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
// Sort by date descending (most recent first)
|
|
43
|
+
const uniqueShips = Array.from(uniqueShipsMap.values())
|
|
44
|
+
.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime())
|
|
45
|
+
|
|
46
|
+
// Group ships by date for display
|
|
47
|
+
const shipsByDate = new Map<string, typeof uniqueShips>()
|
|
48
|
+
for (const ship of uniqueShips) {
|
|
49
|
+
const dateKey = ship.date
|
|
50
|
+
if (!shipsByDate.has(dateKey)) {
|
|
51
|
+
shipsByDate.set(dateKey, [])
|
|
52
|
+
}
|
|
53
|
+
shipsByDate.get(dateKey)!.push(ship)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const totalTasks = weekDataList.reduce((sum, w) => sum + w.tasksCompleted, 0)
|
|
57
|
+
const totalBugs = weekDataList.reduce((sum, w) => sum + w.bugsFixed, 0)
|
|
58
|
+
const totalDays = weekDataList.reduce((sum, w) => sum + w.activeDays, 0)
|
|
59
|
+
|
|
60
|
+
// Date range
|
|
61
|
+
const firstWeek = weekDataList[0]
|
|
62
|
+
const lastWeek = weekDataList[weekDataList.length - 1]
|
|
63
|
+
const dateRangeStr = weekDataList.length === 1
|
|
64
|
+
? formatDateRange(firstWeek.startDate, firstWeek.endDate)
|
|
65
|
+
: formatDateRange(firstWeek.startDate, lastWeek.endDate)
|
|
66
|
+
|
|
67
|
+
const weekLabel = weekDataList.length === 1
|
|
68
|
+
? `Semana ${firstWeek.week}`
|
|
69
|
+
: `Semanas ${firstWeek.week}-${lastWeek.week}`
|
|
70
|
+
|
|
71
|
+
const handlePrint = () => {
|
|
72
|
+
window.print()
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return (
|
|
76
|
+
<div className="min-h-screen bg-white">
|
|
77
|
+
{/* Action buttons - hidden when printing */}
|
|
78
|
+
<div className="print:hidden fixed top-4 right-4 z-50 flex items-center gap-2">
|
|
79
|
+
<Link
|
|
80
|
+
href={`/project/${projectId}/reports`}
|
|
81
|
+
className="flex items-center gap-2 px-4 py-2 bg-white text-gray-700 border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors shadow-lg"
|
|
82
|
+
>
|
|
83
|
+
<ArrowLeft className="h-4 w-4" />
|
|
84
|
+
Regresar
|
|
85
|
+
</Link>
|
|
86
|
+
<button
|
|
87
|
+
onClick={handlePrint}
|
|
88
|
+
className="flex items-center gap-2 px-4 py-2 bg-gray-900 text-white rounded-lg hover:bg-gray-800 transition-colors shadow-lg"
|
|
89
|
+
>
|
|
90
|
+
<Printer className="h-4 w-4" />
|
|
91
|
+
Imprimir / PDF
|
|
92
|
+
</button>
|
|
93
|
+
</div>
|
|
94
|
+
|
|
95
|
+
{/* Printable content */}
|
|
96
|
+
<div className="max-w-2xl mx-auto p-8 print:p-0 print:max-w-none">
|
|
97
|
+
{/* Header */}
|
|
98
|
+
<header className="mb-8 pb-6 border-b-2 border-gray-200">
|
|
99
|
+
<h1 className="text-3xl font-bold text-gray-900 mb-2">{projectName}</h1>
|
|
100
|
+
<p className="text-lg text-gray-600">
|
|
101
|
+
Reporte de Progreso - {weekLabel}
|
|
102
|
+
</p>
|
|
103
|
+
<p className="text-gray-500">{dateRangeStr}, {year}</p>
|
|
104
|
+
</header>
|
|
105
|
+
|
|
106
|
+
{/* Stats Summary */}
|
|
107
|
+
<section className="mb-8">
|
|
108
|
+
<div className="grid grid-cols-4 gap-4">
|
|
109
|
+
<StatBox
|
|
110
|
+
icon={<Rocket className="h-6 w-6" />}
|
|
111
|
+
value={uniqueShips.length}
|
|
112
|
+
label="Entregados"
|
|
113
|
+
color="text-emerald-600"
|
|
114
|
+
/>
|
|
115
|
+
<StatBox
|
|
116
|
+
icon={<CheckCircle2 className="h-6 w-6" />}
|
|
117
|
+
value={totalTasks}
|
|
118
|
+
label="Tareas"
|
|
119
|
+
color="text-blue-600"
|
|
120
|
+
/>
|
|
121
|
+
<StatBox
|
|
122
|
+
icon={<Bug className="h-6 w-6" />}
|
|
123
|
+
value={totalBugs}
|
|
124
|
+
label="Bugs"
|
|
125
|
+
color="text-orange-600"
|
|
126
|
+
/>
|
|
127
|
+
<StatBox
|
|
128
|
+
icon={<Calendar className="h-6 w-6" />}
|
|
129
|
+
value={totalDays}
|
|
130
|
+
label="Dias Activos"
|
|
131
|
+
color="text-purple-600"
|
|
132
|
+
/>
|
|
133
|
+
</div>
|
|
134
|
+
</section>
|
|
135
|
+
|
|
136
|
+
{/* Shipped Features grouped by date */}
|
|
137
|
+
{uniqueShips.length > 0 && (
|
|
138
|
+
<section className="mb-8">
|
|
139
|
+
<h2 className="text-xl font-semibold text-gray-900 mb-4 flex items-center gap-2">
|
|
140
|
+
<Rocket className="h-5 w-5 text-emerald-600" />
|
|
141
|
+
Entregado
|
|
142
|
+
</h2>
|
|
143
|
+
<div className="space-y-5">
|
|
144
|
+
{Array.from(shipsByDate.entries()).map(([date, ships]) => (
|
|
145
|
+
<div key={date}>
|
|
146
|
+
<p className="text-sm font-medium text-gray-500 mb-2">
|
|
147
|
+
{new Date(date).toLocaleDateString('es-MX', {
|
|
148
|
+
weekday: 'long',
|
|
149
|
+
month: 'long',
|
|
150
|
+
day: 'numeric'
|
|
151
|
+
})}
|
|
152
|
+
</p>
|
|
153
|
+
<ul className="space-y-2 pl-4 border-l-2 border-emerald-200">
|
|
154
|
+
{ships.map((ship, i) => (
|
|
155
|
+
<li key={i} className="pl-3 text-gray-700">
|
|
156
|
+
<span className="font-medium">{ship.name}</span>
|
|
157
|
+
{ship.version && (
|
|
158
|
+
<span className="ml-2 text-sm text-gray-500 bg-gray-100 px-2 py-0.5 rounded">
|
|
159
|
+
{ship.version}
|
|
160
|
+
</span>
|
|
161
|
+
)}
|
|
162
|
+
</li>
|
|
163
|
+
))}
|
|
164
|
+
</ul>
|
|
165
|
+
</div>
|
|
166
|
+
))}
|
|
167
|
+
</div>
|
|
168
|
+
</section>
|
|
169
|
+
)}
|
|
170
|
+
|
|
171
|
+
{/* Activity Details */}
|
|
172
|
+
{(totalTasks > 0 || totalBugs > 0) && (
|
|
173
|
+
<section className="mb-8">
|
|
174
|
+
<h2 className="text-xl font-semibold text-gray-900 mb-4 flex items-center gap-2">
|
|
175
|
+
<CheckCircle2 className="h-5 w-5 text-blue-600" />
|
|
176
|
+
Actividad
|
|
177
|
+
</h2>
|
|
178
|
+
<ul className="space-y-2 text-gray-700">
|
|
179
|
+
{totalTasks > 0 && (
|
|
180
|
+
<li className="flex items-center gap-2">
|
|
181
|
+
<CheckCircle2 className="h-4 w-4 text-blue-500" />
|
|
182
|
+
{totalTasks} tarea{totalTasks !== 1 ? 's' : ''} completada{totalTasks !== 1 ? 's' : ''}
|
|
183
|
+
</li>
|
|
184
|
+
)}
|
|
185
|
+
{totalBugs > 0 && (
|
|
186
|
+
<li className="flex items-center gap-2">
|
|
187
|
+
<Bug className="h-4 w-4 text-orange-500" />
|
|
188
|
+
{totalBugs} bug{totalBugs !== 1 ? 's' : ''} corregido{totalBugs !== 1 ? 's' : ''}
|
|
189
|
+
</li>
|
|
190
|
+
)}
|
|
191
|
+
{totalDays > 0 && (
|
|
192
|
+
<li className="flex items-center gap-2">
|
|
193
|
+
<Calendar className="h-4 w-4 text-purple-500" />
|
|
194
|
+
{totalDays} dia{totalDays !== 1 ? 's' : ''} activo{totalDays !== 1 ? 's' : ''}
|
|
195
|
+
</li>
|
|
196
|
+
)}
|
|
197
|
+
</ul>
|
|
198
|
+
</section>
|
|
199
|
+
)}
|
|
200
|
+
|
|
201
|
+
{/* No activity message */}
|
|
202
|
+
{uniqueShips.length === 0 && totalTasks === 0 && totalBugs === 0 && (
|
|
203
|
+
<section className="mb-8 p-6 bg-gray-50 rounded-lg text-center text-gray-500">
|
|
204
|
+
Sin actividad registrada para este periodo
|
|
205
|
+
</section>
|
|
206
|
+
)}
|
|
207
|
+
|
|
208
|
+
{/* Next Steps placeholder */}
|
|
209
|
+
<section className="mb-8">
|
|
210
|
+
<h2 className="text-xl font-semibold text-gray-900 mb-4">
|
|
211
|
+
Siguiente
|
|
212
|
+
</h2>
|
|
213
|
+
<ul className="space-y-2 text-gray-600">
|
|
214
|
+
<li className="flex items-center gap-2">
|
|
215
|
+
<span className="text-gray-400">•</span>
|
|
216
|
+
<span className="italic text-gray-400">[Pendiente por definir]</span>
|
|
217
|
+
</li>
|
|
218
|
+
</ul>
|
|
219
|
+
</section>
|
|
220
|
+
|
|
221
|
+
{/* Footer */}
|
|
222
|
+
<footer className="mt-12 pt-6 border-t border-gray-200 text-sm text-gray-400 text-center print:mt-8">
|
|
223
|
+
<p>Generado con prjct - {new Date().toLocaleDateString('es-MX')}</p>
|
|
224
|
+
</footer>
|
|
225
|
+
</div>
|
|
226
|
+
|
|
227
|
+
{/* Print styles */}
|
|
228
|
+
<style jsx global>{`
|
|
229
|
+
@media print {
|
|
230
|
+
@page {
|
|
231
|
+
size: letter;
|
|
232
|
+
margin: 1in;
|
|
233
|
+
}
|
|
234
|
+
body {
|
|
235
|
+
-webkit-print-color-adjust: exact;
|
|
236
|
+
print-color-adjust: exact;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
`}</style>
|
|
240
|
+
</div>
|
|
241
|
+
)
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
interface StatBoxProps {
|
|
245
|
+
icon: React.ReactNode
|
|
246
|
+
value: number
|
|
247
|
+
label: string
|
|
248
|
+
color: string
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function StatBox({ icon, value, label, color }: StatBoxProps) {
|
|
252
|
+
return (
|
|
253
|
+
<div className="text-center p-4 border border-gray-200 rounded-lg">
|
|
254
|
+
<div className={`${color} flex justify-center mb-2`}>{icon}</div>
|
|
255
|
+
<div className="text-2xl font-bold text-gray-900">{value}</div>
|
|
256
|
+
<div className="text-sm text-gray-500">{label}</div>
|
|
257
|
+
</div>
|
|
258
|
+
)
|
|
259
|
+
}
|