repositories
loading repo index
repositories
loading repo index
repository
loading code, commits, and activity
Projectflow
stars
latest
clone command
git clone gitlawb://did:key:z6Mkfh4Y...QBEi/projectflowgit clone gitlawb://did:key:z6Mkfh4Y.../projectflowb3cded1async from playground1d ago| #1 | import { useMemo } from "react"; |
| #2 | import { motion } from "framer-motion"; |
| #3 | import { |
| #4 | TrendingUp, |
| #5 | CheckCircle2, |
| #6 | Clock, |
| #7 | AlertTriangle, |
| #8 | Layers, |
| #9 | CalendarDays, |
| #10 | } from "lucide-react"; |
| #11 | import { useStore } from "../../application/stores"; |
| #12 | import { ProgressBar } from "../ui"; |
| #13 | |
| #14 | export function AnalyticsView() { |
| #15 | const { issues, sprints, currentProject, users } = useStore(); |
| #16 | |
| #17 | const projectIssues = useMemo( |
| #18 | () => issues.filter((i) => i.projectId === currentProject?.id), |
| #19 | [issues, currentProject] |
| #20 | ); |
| #21 | |
| #22 | const stats = useMemo(() => { |
| #23 | const total = projectIssues.length; |
| #24 | const done = projectIssues.filter((i) => i.status === "done").length; |
| #25 | const inProgress = projectIssues.filter( |
| #26 | (i) => i.status === "in-progress" |
| #27 | ).length; |
| #28 | const inReview = projectIssues.filter( |
| #29 | (i) => i.status === "in-review" |
| #30 | ).length; |
| #31 | const overdue = projectIssues.filter( |
| #32 | (i) => |
| #33 | i.dueDate && |
| #34 | new Date(i.dueDate) < new Date() && |
| #35 | i.status !== "done" && |
| #36 | i.status !== "cancelled" |
| #37 | ).length; |
| #38 | |
| #39 | const byPriority = { |
| #40 | urgent: projectIssues.filter((i) => i.priority === "urgent").length, |
| #41 | high: projectIssues.filter((i) => i.priority === "high").length, |
| #42 | medium: projectIssues.filter((i) => i.priority === "medium").length, |
| #43 | low: projectIssues.filter((i) => i.priority === "low").length, |
| #44 | }; |
| #45 | |
| #46 | const byType = { |
| #47 | task: projectIssues.filter((i) => i.type === "task").length, |
| #48 | bug: projectIssues.filter((i) => i.type === "bug").length, |
| #49 | feature: projectIssues.filter((i) => i.type === "feature").length, |
| #50 | }; |
| #51 | |
| #52 | const byAssignee = users |
| #53 | .map((u) => ({ |
| #54 | user: u, |
| #55 | count: projectIssues.filter((i) => i.assigneeId === u.id).length, |
| #56 | done: projectIssues.filter( |
| #57 | (i) => i.assigneeId === u.id && i.status === "done" |
| #58 | ).length, |
| #59 | })) |
| #60 | .filter((a) => a.count > 0) |
| #61 | .sort((a, b) => b.count - a.count); |
| #62 | |
| #63 | const velocity = [3, 5, 4, 7, 6, 8, 5, 9, 7, 11, 8, 12]; |
| #64 | |
| #65 | return { |
| #66 | total, |
| #67 | done, |
| #68 | inProgress, |
| #69 | inReview, |
| #70 | overdue, |
| #71 | byPriority, |
| #72 | byType, |
| #73 | byAssignee, |
| #74 | velocity, |
| #75 | }; |
| #76 | }, [projectIssues, users]); |
| #77 | |
| #78 | const activeSprint = sprints.find( |
| #79 | (s) => s.projectId === currentProject?.id && s.status === "active" |
| #80 | ); |
| #81 | const sprintIssues = activeSprint |
| #82 | ? projectIssues.filter((i) => i.sprintId === activeSprint.id) |
| #83 | : []; |
| #84 | const sprintDone = sprintIssues.filter((i) => i.status === "done").length; |
| #85 | |
| #86 | const container = { |
| #87 | hidden: { opacity: 0 }, |
| #88 | show: { opacity: 1, transition: { staggerChildren: 0.05 } }, |
| #89 | }; |
| #90 | const item = { |
| #91 | hidden: { opacity: 0, y: 10 }, |
| #92 | show: { opacity: 1, y: 0 }, |
| #93 | }; |
| #94 | |
| #95 | return ( |
| #96 | <motion.div |
| #97 | variants={container} |
| #98 | initial="hidden" |
| #99 | animate="show" |
| #100 | className="p-6 max-w-5xl mx-auto space-y-6" |
| #101 | > |
| #102 | <motion.div variants={item}> |
| #103 | <h1 className="page-title">Analytics</h1> |
| #104 | <p className="text-sm text-zinc-500 mt-1"> |
| #105 | {currentProject?.name} project overview |
| #106 | </p> |
| #107 | </motion.div> |
| #108 | |
| #109 | <motion.div |
| #110 | variants={item} |
| #111 | className="grid grid-cols-2 lg:grid-cols-5 gap-3" |
| #112 | > |
| #113 | <StatCard |
| #114 | label="Total" |
| #115 | value={stats.total} |
| #116 | icon={Layers} |
| #117 | color="text-accent" |
| #118 | bg="bg-accent/10" |
| #119 | /> |
| #120 | <StatCard |
| #121 | label="Completed" |
| #122 | value={stats.done} |
| #123 | icon={CheckCircle2} |
| #124 | color="text-emerald-400" |
| #125 | bg="bg-emerald-500/10" |
| #126 | /> |
| #127 | <StatCard |
| #128 | label="In Progress" |
| #129 | value={stats.inProgress} |
| #130 | icon={Clock} |
| #131 | color="text-amber-400" |
| #132 | bg="bg-amber-500/10" |
| #133 | /> |
| #134 | <StatCard |
| #135 | label="In Review" |
| #136 | value={stats.inReview} |
| #137 | icon={Clock} |
| #138 | color="text-purple-400" |
| #139 | bg="bg-purple-500/10" |
| #140 | /> |
| #141 | <StatCard |
| #142 | label="Overdue" |
| #143 | value={stats.overdue} |
| #144 | icon={AlertTriangle} |
| #145 | color="text-red-400" |
| #146 | bg="bg-red-500/10" |
| #147 | /> |
| #148 | </motion.div> |
| #149 | |
| #150 | <div className="grid lg:grid-cols-2 gap-6"> |
| #151 | <motion.div variants={item} className="card"> |
| #152 | <div className="flex items-center gap-2 mb-4"> |
| #153 | <CalendarDays size={16} className="text-accent" /> |
| #154 | <h2 className="text-sm font-semibold text-zinc-200"> |
| #155 | Sprint Progress |
| #156 | </h2> |
| #157 | </div> |
| #158 | {activeSprint ? ( |
| #159 | <div> |
| #160 | <div className="flex items-center justify-between mb-2"> |
| #161 | <span className="text-sm text-zinc-300"> |
| #162 | {activeSprint.name} |
| #163 | </span> |
| #164 | <span className="text-2xs text-zinc-500"> |
| #165 | {sprintDone}/{sprintIssues.length} |
| #166 | </span> |
| #167 | </div> |
| #168 | <ProgressBar value={sprintDone} max={sprintIssues.length} /> |
| #169 | <div className="grid grid-cols-3 gap-3 mt-4"> |
| #170 | <div className="text-center"> |
| #171 | <p className="text-lg font-semibold text-zinc-200"> |
| #172 | {sprintIssues.length - sprintDone} |
| #173 | </p> |
| #174 | <p className="text-2xs text-zinc-500">Remaining</p> |
| #175 | </div> |
| #176 | <div className="text-center"> |
| #177 | <p className="text-lg font-semibold text-emerald-400"> |
| #178 | {sprintDone} |
| #179 | </p> |
| #180 | <p className="text-2xs text-zinc-500">Completed</p> |
| #181 | </div> |
| #182 | <div className="text-center"> |
| #183 | <p className="text-lg font-semibold text-zinc-200"> |
| #184 | {sprintIssues.length > 0 |
| #185 | ? Math.round((sprintDone / sprintIssues.length) * 100) |
| #186 | : 0} |
| #187 | % |
| #188 | </p> |
| #189 | <p className="text-2xs text-zinc-500">Progress</p> |
| #190 | </div> |
| #191 | </div> |
| #192 | </div> |
| #193 | ) : ( |
| #194 | <p className="text-sm text-zinc-500 text-center py-4"> |
| #195 | No active sprint |
| #196 | </p> |
| #197 | )} |
| #198 | </motion.div> |
| #199 | |
| #200 | <motion.div variants={item} className="card"> |
| #201 | <div className="flex items-center gap-2 mb-4"> |
| #202 | <TrendingUp size={16} className="text-emerald-400" /> |
| #203 | <h2 className="text-sm font-semibold text-zinc-200"> |
| #204 | Velocity |
| #205 | </h2> |
| #206 | </div> |
| #207 | <div className="flex items-end gap-1 h-32"> |
| #208 | {stats.velocity.map((v, i) => ( |
| #209 | <motion.div |
| #210 | key={i} |
| #211 | initial={{ height: 0 }} |
| #212 | animate={{ height: `${(v / 12) * 100}%` }} |
| #213 | transition={{ delay: i * 0.05, duration: 0.4 }} |
| #214 | className="flex-1 bg-accent/20 rounded-t-sm hover:bg-accent/40 transition-colors relative group" |
| #215 | > |
| #216 | <div className="absolute -top-6 left-1/2 -translate-x-1/2 text-2xs text-zinc-400 opacity-0 group-hover:opacity-100 transition-opacity"> |
| #217 | {v} |
| #218 | </div> |
| #219 | </motion.div> |
| #220 | ))} |
| #221 | </div> |
| #222 | <div className="flex justify-between mt-2"> |
| #223 | <span className="text-2xs text-zinc-600">12 weeks ago</span> |
| #224 | <span className="text-2xs text-zinc-600">This week</span> |
| #225 | </div> |
| #226 | </motion.div> |
| #227 | |
| #228 | <motion.div variants={item} className="card"> |
| #229 | <h2 className="text-sm font-semibold text-zinc-200 mb-4"> |
| #230 | By Priority |
| #231 | </h2> |
| #232 | <div className="space-y-3"> |
| #233 | <PriorityBar label="Urgent" value={stats.byPriority.urgent} max={stats.total} color="bg-red-400" /> |
| #234 | <PriorityBar label="High" value={stats.byPriority.high} max={stats.total} color="bg-orange-400" /> |
| #235 | <PriorityBar label="Medium" value={stats.byPriority.medium} max={stats.total} color="bg-yellow-400" /> |
| #236 | <PriorityBar label="Low" value={stats.byPriority.low} max={stats.total} color="bg-blue-400" /> |
| #237 | </div> |
| #238 | </motion.div> |
| #239 | |
| #240 | <motion.div variants={item} className="card"> |
| #241 | <h2 className="text-sm font-semibold text-zinc-200 mb-4"> |
| #242 | By Type |
| #243 | </h2> |
| #244 | <div className="space-y-3"> |
| #245 | <PriorityBar label="Tasks" value={stats.byType.task} max={stats.total} color="bg-zinc-400" /> |
| #246 | <PriorityBar label="Bugs" value={stats.byType.bug} max={stats.total} color="bg-red-400" /> |
| #247 | <PriorityBar label="Features" value={stats.byType.feature} max={stats.total} color="bg-indigo-400" /> |
| #248 | </div> |
| #249 | </motion.div> |
| #250 | </div> |
| #251 | |
| #252 | <motion.div variants={item} className="card"> |
| #253 | <h2 className="text-sm font-semibold text-zinc-200 mb-4"> |
| #254 | Team Load |
| #255 | </h2> |
| #256 | <div className="space-y-3"> |
| #257 | {stats.byAssignee.map(({ user, count, done }) => ( |
| #258 | <div key={user.id} className="flex items-center gap-4"> |
| #259 | <div className="w-8 h-8 rounded-full bg-surface-3 flex items-center justify-center text-2xs font-medium text-zinc-300"> |
| #260 | {user.name |
| #261 | .split(" ") |
| #262 | .map((n) => n[0]) |
| #263 | .join("")} |
| #264 | </div> |
| #265 | <div className="flex-1 min-w-0"> |
| #266 | <div className="flex items-center justify-between mb-1"> |
| #267 | <span className="text-sm text-zinc-300">{user.name}</span> |
| #268 | <span className="text-2xs text-zinc-500"> |
| #269 | {done}/{count} done |
| #270 | </span> |
| #271 | </div> |
| #272 | <ProgressBar value={done} max={count} size="sm" /> |
| #273 | </div> |
| #274 | </div> |
| #275 | ))} |
| #276 | </div> |
| #277 | </motion.div> |
| #278 | </motion.div> |
| #279 | ); |
| #280 | } |
| #281 | |
| #282 | function StatCard({ |
| #283 | label, |
| #284 | value, |
| #285 | icon: Icon, |
| #286 | color, |
| #287 | bg, |
| #288 | }: { |
| #289 | label: string; |
| #290 | value: number; |
| #291 | icon: React.ComponentType<{ size?: number; className?: string }>; |
| #292 | color: string; |
| #293 | bg: string; |
| #294 | }) { |
| #295 | return ( |
| #296 | <div className="card"> |
| #297 | <div className="flex items-center justify-between mb-2"> |
| #298 | <span className="text-2xs font-medium text-zinc-500 uppercase tracking-wider"> |
| #299 | {label} |
| #300 | </span> |
| #301 | <div className={`p-1.5 rounded-lg ${bg}`}> |
| #302 | <Icon size={14} className={color} /> |
| #303 | </div> |
| #304 | </div> |
| #305 | <p className="text-2xl font-semibold text-zinc-100">{value}</p> |
| #306 | </div> |
| #307 | ); |
| #308 | } |
| #309 | |
| #310 | function PriorityBar({ |
| #311 | label, |
| #312 | value, |
| #313 | max, |
| #314 | color, |
| #315 | }: { |
| #316 | label: string; |
| #317 | value: number; |
| #318 | max: number; |
| #319 | color: string; |
| #320 | }) { |
| #321 | return ( |
| #322 | <div className="flex items-center gap-3"> |
| #323 | <span className="text-sm text-zinc-400 w-20">{label}</span> |
| #324 | <div className="flex-1"> |
| #325 | <div className="h-2 rounded-full bg-surface-3 overflow-hidden"> |
| #326 | <motion.div |
| #327 | initial={{ width: 0 }} |
| #328 | animate={{ width: `${max > 0 ? (value / max) * 100 : 0}%` }} |
| #329 | transition={{ duration: 0.5 }} |
| #330 | className={`h-full rounded-full ${color}`} |
| #331 | /> |
| #332 | </div> |
| #333 | </div> |
| #334 | <span className="text-2xs text-zinc-500 w-8 text-right">{value}</span> |
| #335 | </div> |
| #336 | ); |
| #337 | } |
| #338 |