feat: modulo de proyectos frontend (Next 16 + Bootstrap 5)
This commit is contained in:
commit
2dc41ed4fa
43 files changed
+5894
No files matched your search
@@ -0,0 +1,135 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { api } from "@/lib/api";
|
||||
import type { Issue, Project } from "@/lib/types";
|
||||
import { PROJECT_STATUS_META } from "@/lib/meta";
|
||||
import { formatShortDate, percent } from "@/lib/format";
|
||||
import { useReferenceLookups } from "@/lib/store";
|
||||
import { AppShell } from "@/components/layout/AppShell";
|
||||
import { Avatar } from "@/components/ui/Avatar";
|
||||
import { EmptyState } from "@/components/ui/EmptyState";
|
||||
import { ProgressBar } from "@/components/ui/ProgressBar";
|
||||
|
||||
/**
|
||||
* Listado de proyectos con su progreso calculado a partir de los issues
|
||||
* completados sobre el total.
|
||||
*/
|
||||
export default function ProjectsPage() {
|
||||
const [projects, setProjects] = useState<Project[]>([]);
|
||||
const [issues, setIssues] = useState<Issue[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const { userById } = useReferenceLookups();
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
|
||||
void Promise.all([api.listProjects(), api.listIssues()]).then(([projectData, issueData]) => {
|
||||
if (!cancelled) {
|
||||
setProjects(projectData);
|
||||
setIssues(issueData);
|
||||
setLoading(false);
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, []);
|
||||
|
||||
const progressByProject = useMemo(() => {
|
||||
const map = new Map<string, { total: number; done: number }>();
|
||||
|
||||
for (const issue of issues) {
|
||||
if (!issue.projectId) continue;
|
||||
const entry = map.get(issue.projectId) ?? { total: 0, done: 0 };
|
||||
entry.total += 1;
|
||||
if (issue.status === "done") entry.done += 1;
|
||||
map.set(issue.projectId, entry);
|
||||
}
|
||||
|
||||
return map;
|
||||
}, [issues]);
|
||||
|
||||
return (
|
||||
<AppShell title="Proyectos">
|
||||
{loading && (
|
||||
<div className="p-4">
|
||||
{Array.from({ length: 3 }).map((_, index) => (
|
||||
<div key={index} className="skeleton mb-3" style={{ height: 88, borderRadius: 10 }} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!loading && projects.length === 0 && (
|
||||
<EmptyState
|
||||
icon="bx-folder"
|
||||
title="No hay proyectos"
|
||||
description="Crea un proyecto para agrupar issues relacionados."
|
||||
/>
|
||||
)}
|
||||
|
||||
{!loading && (
|
||||
<div className="p-4 d-flex flex-column gap-3">
|
||||
{projects.map((project) => {
|
||||
const progress = progressByProject.get(project.id) ?? { total: 0, done: 0 };
|
||||
const lead = project.leadId ? userById.get(project.leadId) : undefined;
|
||||
const meta = PROJECT_STATUS_META[project.status];
|
||||
|
||||
return (
|
||||
<Link
|
||||
key={project.id}
|
||||
href={`/projects/${project.id}`}
|
||||
className="border rounded-3 p-3 text-decoration-none"
|
||||
style={{
|
||||
background: "var(--lp-bg-elevated)",
|
||||
borderColor: "var(--lp-border)",
|
||||
display: "block",
|
||||
}}
|
||||
>
|
||||
<div className="d-flex align-items-center gap-2 mb-1">
|
||||
<span style={{ fontWeight: 600, fontSize: 14, color: "var(--lp-text)" }}>
|
||||
{project.name}
|
||||
</span>
|
||||
<span className="issue-identifier">{project.key}</span>
|
||||
<span className="label-chip ms-1" style={{ color: meta.color }}>
|
||||
<i className={`bx ${meta.icon}`} />
|
||||
{meta.label}
|
||||
</span>
|
||||
|
||||
<span className="ms-auto d-flex align-items-center gap-2">
|
||||
<span style={{ fontSize: 12.5, color: "var(--lp-text-faint)" }}>
|
||||
{progress.done}/{progress.total}
|
||||
</span>
|
||||
<Avatar user={lead} />
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<p
|
||||
className="mb-3"
|
||||
style={{ fontSize: 13, color: "var(--lp-text-muted)", maxWidth: 720 }}
|
||||
>
|
||||
{project.description}
|
||||
</p>
|
||||
|
||||
<div className="d-flex align-items-center gap-3">
|
||||
<div className="flex-grow-1">
|
||||
<ProgressBar value={percent(progress.done, progress.total)} />
|
||||
</div>
|
||||
|
||||
{project.targetDate && (
|
||||
<span style={{ fontSize: 12, color: "var(--lp-text-faint)", whiteSpace: "nowrap" }}>
|
||||
<i className="bx bx-calendar me-1" />
|
||||
{formatShortDate(project.targetDate)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</AppShell>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user