prjct-cli 0.13.3 → 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.
Files changed (193) hide show
  1. package/CHANGELOG.md +106 -0
  2. package/bin/prjct +10 -13
  3. package/core/agentic/memory-system/semantic-memories.ts +2 -1
  4. package/core/agentic/plan-mode/plan-mode.ts +2 -1
  5. package/core/agentic/prompt-builder.ts +22 -43
  6. package/core/agentic/services.ts +5 -5
  7. package/core/agentic/smart-context.ts +7 -2
  8. package/core/command-registry/core-commands.ts +54 -29
  9. package/core/command-registry/optional-commands.ts +64 -0
  10. package/core/command-registry/setup-commands.ts +18 -3
  11. package/core/commands/analysis.ts +21 -68
  12. package/core/commands/analytics.ts +247 -213
  13. package/core/commands/base.ts +1 -1
  14. package/core/commands/index.ts +41 -36
  15. package/core/commands/maintenance.ts +300 -31
  16. package/core/commands/planning.ts +233 -22
  17. package/core/commands/setup.ts +3 -8
  18. package/core/commands/shipping.ts +14 -18
  19. package/core/commands/types.ts +8 -6
  20. package/core/commands/workflow.ts +105 -100
  21. package/core/context/generator.ts +317 -0
  22. package/core/context-sync.ts +7 -350
  23. package/core/data/index.ts +13 -32
  24. package/core/data/md-ideas-manager.ts +155 -0
  25. package/core/data/md-queue-manager.ts +4 -3
  26. package/core/data/md-shipped-manager.ts +90 -0
  27. package/core/data/md-state-manager.ts +11 -7
  28. package/core/domain/agent-generator.ts +23 -63
  29. package/core/events/index.ts +143 -0
  30. package/core/index.ts +17 -14
  31. package/core/infrastructure/capability-installer.ts +13 -149
  32. package/core/infrastructure/migrator/project-scanner.ts +2 -1
  33. package/core/infrastructure/path-manager.ts +4 -6
  34. package/core/infrastructure/setup.ts +3 -0
  35. package/core/infrastructure/uuid-migration.ts +750 -0
  36. package/core/outcomes/recorder.ts +2 -1
  37. package/core/plugin/loader.ts +4 -7
  38. package/core/plugin/registry.ts +3 -3
  39. package/core/schemas/index.ts +23 -25
  40. package/core/schemas/state.ts +1 -0
  41. package/core/serializers/ideas-serializer.ts +187 -0
  42. package/core/serializers/index.ts +16 -0
  43. package/core/serializers/shipped-serializer.ts +108 -0
  44. package/core/session/utils.ts +3 -9
  45. package/core/storage/ideas-storage.ts +273 -0
  46. package/core/storage/index.ts +204 -0
  47. package/core/storage/queue-storage.ts +297 -0
  48. package/core/storage/shipped-storage.ts +223 -0
  49. package/core/storage/state-storage.ts +235 -0
  50. package/core/storage/storage-manager.ts +175 -0
  51. package/package.json +1 -1
  52. package/packages/web/app/api/projects/[id]/momentum/route.ts +257 -0
  53. package/packages/web/app/api/sessions/current/route.ts +132 -0
  54. package/packages/web/app/api/sessions/history/route.ts +96 -14
  55. package/packages/web/app/globals.css +5 -0
  56. package/packages/web/app/layout.tsx +2 -0
  57. package/packages/web/app/project/[id]/code/layout.tsx +18 -0
  58. package/packages/web/app/project/[id]/code/page.tsx +408 -0
  59. package/packages/web/app/project/[id]/page.tsx +359 -389
  60. package/packages/web/app/project/[id]/reports/page.tsx +59 -0
  61. package/packages/web/app/project/[id]/reports/print/page.tsx +58 -0
  62. package/packages/web/components/ActivityTimeline/ActivityTimeline.tsx +0 -1
  63. package/packages/web/components/AgentsCard/AgentsCard.tsx +64 -34
  64. package/packages/web/components/AgentsCard/AgentsCard.types.ts +1 -0
  65. package/packages/web/components/AppSidebar/AppSidebar.tsx +135 -11
  66. package/packages/web/components/BentoCard/BentoCard.constants.ts +3 -3
  67. package/packages/web/components/BentoCard/BentoCard.tsx +2 -1
  68. package/packages/web/components/BentoGrid/BentoGrid.tsx +2 -2
  69. package/packages/web/components/BlockersCard/BlockersCard.tsx +65 -57
  70. package/packages/web/components/BlockersCard/BlockersCard.types.ts +1 -0
  71. package/packages/web/components/CommandBar/CommandBar.tsx +67 -0
  72. package/packages/web/components/CommandBar/index.ts +1 -0
  73. package/packages/web/components/DashboardContent/DashboardContent.tsx +35 -5
  74. package/packages/web/components/DateGroup/DateGroup.tsx +1 -1
  75. package/packages/web/components/EmptyState/EmptyState.tsx +39 -21
  76. package/packages/web/components/EmptyState/EmptyState.types.ts +1 -0
  77. package/packages/web/components/EventRow/EventRow.tsx +4 -4
  78. package/packages/web/components/EventRow/EventRow.utils.ts +3 -3
  79. package/packages/web/components/HeroSection/HeroSection.tsx +52 -15
  80. package/packages/web/components/HeroSection/HeroSection.types.ts +4 -4
  81. package/packages/web/components/HeroSection/HeroSection.utils.ts +7 -3
  82. package/packages/web/components/IdeasCard/IdeasCard.tsx +94 -27
  83. package/packages/web/components/IdeasCard/IdeasCard.types.ts +1 -0
  84. package/packages/web/components/MasonryGrid/MasonryGrid.tsx +18 -0
  85. package/packages/web/components/MasonryGrid/index.ts +1 -0
  86. package/packages/web/components/MomentumWidget/MomentumWidget.tsx +119 -0
  87. package/packages/web/components/MomentumWidget/MomentumWidget.types.ts +16 -0
  88. package/packages/web/components/MomentumWidget/index.ts +2 -0
  89. package/packages/web/components/NowCard/NowCard.tsx +81 -56
  90. package/packages/web/components/NowCard/NowCard.types.ts +1 -0
  91. package/packages/web/components/PageHeader/PageHeader.tsx +24 -0
  92. package/packages/web/components/PageHeader/index.ts +1 -0
  93. package/packages/web/components/ProgressRing/ProgressRing.constants.ts +2 -2
  94. package/packages/web/components/ProjectAvatar/ProjectAvatar.tsx +2 -2
  95. package/packages/web/components/ProjectColorDot/ProjectColorDot.tsx +37 -0
  96. package/packages/web/components/ProjectColorDot/index.ts +1 -0
  97. package/packages/web/components/ProjectSelectorModal/ProjectSelectorModal.tsx +104 -0
  98. package/packages/web/components/ProjectSelectorModal/index.ts +1 -0
  99. package/packages/web/components/Providers/Providers.tsx +4 -1
  100. package/packages/web/components/QueueCard/QueueCard.tsx +78 -25
  101. package/packages/web/components/QueueCard/QueueCard.types.ts +1 -0
  102. package/packages/web/components/QueueCard/QueueCard.utils.ts +3 -3
  103. package/packages/web/components/RecoverCard/RecoverCard.tsx +72 -0
  104. package/packages/web/components/RecoverCard/RecoverCard.types.ts +16 -0
  105. package/packages/web/components/RecoverCard/index.ts +2 -0
  106. package/packages/web/components/RoadmapCard/RoadmapCard.tsx +101 -33
  107. package/packages/web/components/RoadmapCard/RoadmapCard.types.ts +1 -0
  108. package/packages/web/components/ShipsCard/ShipsCard.tsx +71 -28
  109. package/packages/web/components/ShipsCard/ShipsCard.types.ts +2 -0
  110. package/packages/web/components/SparklineChart/SparklineChart.tsx +20 -18
  111. package/packages/web/components/StatsMasonry/StatsMasonry.tsx +95 -0
  112. package/packages/web/components/StatsMasonry/index.ts +1 -0
  113. package/packages/web/components/StreakCard/StreakCard.tsx +37 -35
  114. package/packages/web/components/TasksCounter/TasksCounter.tsx +1 -1
  115. package/packages/web/components/TechStackBadges/TechStackBadges.tsx +12 -4
  116. package/packages/web/components/TerminalDock/DockToggleTab.tsx +29 -0
  117. package/packages/web/components/TerminalDock/TerminalDock.tsx +386 -0
  118. package/packages/web/components/TerminalDock/TerminalDockTab.tsx +130 -0
  119. package/packages/web/components/TerminalDock/TerminalTabBar.tsx +142 -0
  120. package/packages/web/components/TerminalDock/index.ts +2 -0
  121. package/packages/web/components/VelocityBadge/VelocityBadge.tsx +8 -3
  122. package/packages/web/components/VelocityCard/VelocityCard.tsx +49 -47
  123. package/packages/web/components/WeeklyReports/PrintableReport.tsx +259 -0
  124. package/packages/web/components/WeeklyReports/ReportPreviewCard.tsx +187 -0
  125. package/packages/web/components/WeeklyReports/WeekCalendar.tsx +288 -0
  126. package/packages/web/components/WeeklyReports/WeeklyReports.tsx +149 -0
  127. package/packages/web/components/WeeklyReports/index.ts +4 -0
  128. package/packages/web/components/WeeklySparkline/WeeklySparkline.tsx +16 -4
  129. package/packages/web/components/WeeklySparkline/WeeklySparkline.types.ts +1 -0
  130. package/packages/web/components/charts/SessionsChart.tsx +6 -3
  131. package/packages/web/components/ui/dialog.tsx +143 -0
  132. package/packages/web/components/ui/drawer.tsx +135 -0
  133. package/packages/web/components/ui/select.tsx +187 -0
  134. package/packages/web/context/GlobalTerminalContext.tsx +538 -0
  135. package/packages/web/lib/commands.ts +81 -0
  136. package/packages/web/lib/generate-week-report.ts +285 -0
  137. package/packages/web/lib/parse-prjct-files.ts +56 -55
  138. package/packages/web/lib/project-colors.ts +58 -0
  139. package/packages/web/lib/projects.ts +58 -5
  140. package/packages/web/lib/services/projects.server.ts +11 -1
  141. package/packages/web/next-env.d.ts +1 -1
  142. package/packages/web/package.json +5 -1
  143. package/templates/commands/analyze.md +39 -3
  144. package/templates/commands/ask.md +58 -3
  145. package/templates/commands/bug.md +117 -26
  146. package/templates/commands/dash.md +95 -158
  147. package/templates/commands/done.md +130 -148
  148. package/templates/commands/feature.md +125 -103
  149. package/templates/commands/git.md +18 -3
  150. package/templates/commands/idea.md +121 -38
  151. package/templates/commands/init.md +124 -20
  152. package/templates/commands/migrate-all.md +63 -28
  153. package/templates/commands/migrate.md +140 -0
  154. package/templates/commands/next.md +115 -5
  155. package/templates/commands/now.md +146 -82
  156. package/templates/commands/pause.md +89 -74
  157. package/templates/commands/redo.md +6 -4
  158. package/templates/commands/resume.md +141 -59
  159. package/templates/commands/ship.md +103 -231
  160. package/templates/commands/spec.md +98 -8
  161. package/templates/commands/suggest.md +22 -2
  162. package/templates/commands/sync.md +192 -203
  163. package/templates/commands/undo.md +6 -4
  164. package/core/data/agents-manager.ts +0 -76
  165. package/core/data/analysis-manager.ts +0 -83
  166. package/core/data/base-manager.ts +0 -156
  167. package/core/data/ideas-manager.ts +0 -81
  168. package/core/data/outcomes-manager.ts +0 -96
  169. package/core/data/project-manager.ts +0 -75
  170. package/core/data/roadmap-manager.ts +0 -118
  171. package/core/data/shipped-manager.ts +0 -65
  172. package/core/data/state-manager.ts +0 -214
  173. package/core/state/index.ts +0 -25
  174. package/core/state/manager.ts +0 -376
  175. package/core/state/types.ts +0 -185
  176. package/core/utils/project-capabilities.ts +0 -156
  177. package/core/view-generator.ts +0 -536
  178. package/packages/web/app/project/[id]/stats/loading.tsx +0 -43
  179. package/packages/web/app/project/[id]/stats/page.tsx +0 -253
  180. package/templates/agent-assignment.md +0 -72
  181. package/templates/analysis/project-analysis.md +0 -78
  182. package/templates/checklists/accessibility.md +0 -33
  183. package/templates/commands/build.md +0 -17
  184. package/templates/commands/decision.md +0 -226
  185. package/templates/commands/fix.md +0 -79
  186. package/templates/commands/help.md +0 -61
  187. package/templates/commands/progress.md +0 -14
  188. package/templates/commands/recap.md +0 -14
  189. package/templates/commands/roadmap.md +0 -52
  190. package/templates/commands/status.md +0 -17
  191. package/templates/commands/task.md +0 -63
  192. package/templates/commands/work.md +0 -44
  193. 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
+ }
@@ -0,0 +1,2 @@
1
+ export { TerminalDock } from './TerminalDock'
2
+ export { TerminalDockTab } from './TerminalDockTab'
@@ -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
- isPositive
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
- import { BentoCard } from '@/components/BentoCard'
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
- <BentoCard
16
- size="1x1"
17
- title="Velocity"
18
- icon={Zap}
19
- className={className}
20
- >
21
- <div className="flex flex-col h-full justify-between">
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="mt-2">
29
- <SparklineChart data={weeklyData} height={28} />
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
- <div className="flex items-center justify-between mt-2">
34
- {change !== 0 && (
35
- <div className="flex items-center gap-1">
36
- {change >= 0 ? (
37
- <TrendingUp className="h-3 w-3 text-emerald-500" />
38
- ) : (
39
- <TrendingDown className="h-3 w-3 text-muted-foreground" />
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
- </div>
69
- </BentoCard>
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">&#x2022;</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
+ }