Commit 7264ecf5 authored by Paulo Avila's avatar Paulo Avila
Browse files

alteração na exibição das pastas

parent 47104fa0
...@@ -13,7 +13,7 @@ import { ...@@ -13,7 +13,7 @@ import {
import { Progress } from "@/components/ui/progress"; import { Progress } from "@/components/ui/progress";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { DueBadge } from "@/components/tasks/due-badge"; import { DueBadge } from "@/components/tasks/due-badge";
import { STATUS_CHIPS } from "@/components/tasks/task-by-person"; import { STATUS_CHIPS } from "@/components/tasks/task-utils";
import { formatDate } from "@/lib/dates"; import { formatDate } from "@/lib/dates";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
......
...@@ -40,7 +40,7 @@ export function Sidebar({ ...@@ -40,7 +40,7 @@ export function Sidebar({
return ( return (
<aside <aside
className={cn( className={cn(
"hidden shrink-0 flex-col bg-sidebar text-sidebar-foreground transition-[width] duration-200 md:flex", "sticky top-0 hidden h-screen shrink-0 flex-col bg-sidebar text-sidebar-foreground transition-[width] duration-200 md:flex",
collapsed ? "w-16" : "w-64" collapsed ? "w-16" : "w-64"
)} )}
> >
......
"use client";
import { useMemo, useState } from "react";
import Link from "next/link"; import Link from "next/link";
import { import { ChevronRight, Plus, Search, UserRound } from "lucide-react";
CheckCircle2,
Circle,
Eye,
LoaderCircle,
Plus,
UserRound,
} from "lucide-react";
import type { Subtask, Task, WorkspaceMember } from "@/lib/data"; import type { Subtask, Task, WorkspaceMember } from "@/lib/data";
import { Avatar, AvatarFallback } from "@/components/ui/avatar"; import { Avatar, AvatarFallback } from "@/components/ui/avatar";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Progress } from "@/components/ui/progress"; import { Progress } from "@/components/ui/progress";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { TaskDialog } from "./task-dialog"; import { TaskDialog } from "./task-dialog";
import { taskPercent } from "./task-progress"; import { taskPercent } from "./task-progress";
import { initials, STATUS_CHIPS } from "./task-utils";
function initials(name: string): string { export { STATUS_CHIPS };
return name
.split(" ")
.filter(Boolean)
.slice(0, 2)
.map((part) => part[0]?.toUpperCase())
.join("");
}
export const STATUS_CHIPS = [
{ value: "todo", label: "A Fazer", icon: Circle, className: "text-slate-600 dark:text-slate-300", chipBg: "bg-slate-500/10" },
{ value: "in_progress", label: "Em Andamento", icon: LoaderCircle, className: "text-blue-600 dark:text-blue-400", chipBg: "bg-blue-500/10" },
{ value: "review", label: "Em Revisão", icon: Eye, className: "text-purple-600 dark:text-purple-400", chipBg: "bg-purple-500/10" },
{ value: "done", label: "Concluídas", icon: CheckCircle2, className: "text-green-600 dark:text-green-400", chipBg: "bg-green-500/10" },
] as const;
interface PersonGroup { interface PersonGroup {
key: string; key: string;
...@@ -39,8 +23,9 @@ interface PersonGroup { ...@@ -39,8 +23,9 @@ interface PersonGroup {
} }
/** /**
* Pastas por membro em grade: clicar no card abre a página da pessoa * Lista de pastas por membro: busca por nome no topo e linhas compactas
* com apenas as tarefas dela. * (mais pessoas visíveis de uma vez). Clicar na linha abre a página da
* pessoa com apenas as tarefas dela.
*/ */
export function TaskByPerson({ export function TaskByPerson({
tasks, tasks,
...@@ -56,145 +41,174 @@ export function TaskByPerson({ ...@@ -56,145 +41,174 @@ export function TaskByPerson({
isAdmin: boolean; isAdmin: boolean;
subtasksByTask?: Record<number, Subtask[]>; subtasksByTask?: Record<number, Subtask[]>;
}) { }) {
const groups: PersonGroup[] = members.map((member) => ({ const [search, setSearch] = useState("");
key: String(member.UserId),
name: member.Name, const groups: PersonGroup[] = useMemo(() => {
userId: member.UserId, const list: PersonGroup[] = members.map((member) => ({
tasks: tasks.filter((t) => t.AssigneeId === member.UserId), key: String(member.UserId),
})); name: member.Name,
const unassigned = tasks.filter((t) => t.AssigneeId === null); userId: member.UserId,
if (unassigned.length > 0) { tasks: tasks.filter((t) => t.AssigneeId === member.UserId),
groups.push({ }));
key: "unassigned", const unassigned = tasks.filter((t) => t.AssigneeId === null);
name: null, if (unassigned.length > 0) {
userId: null, list.push({
tasks: unassigned, key: "unassigned",
}); name: null,
} userId: null,
tasks: unassigned,
});
}
return list;
}, [members, tasks]);
const term = search.trim().toLowerCase();
const visibleGroups = term
? groups.filter((g) => (g.name ?? "sem responsável").toLowerCase().includes(term))
: groups;
return ( return (
<div className="grid gap-4 sm:grid-cols-2 xl:grid-cols-3"> <div className="space-y-3">
{groups.map((group) => { <div className="relative max-w-sm">
const total = group.tasks.length; <Search className="absolute left-2.5 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
// Média do % das tarefas: reflete progresso parcial, não só concluídas. <Input
const percent = value={search}
total > 0 onChange={(e) => setSearch(e.target.value)}
? Math.round( placeholder="Buscar pessoa..."
group.tasks.reduce( className="pl-9"
(sum, t) => />
sum + </div>
taskPercent(t.Status, t.SubtaskCount, t.SubtaskDone, t.Progress),
0 <div className="overflow-hidden rounded-xl border bg-card">
) / total {visibleGroups.length === 0 ? (
) <p className="p-8 text-center text-sm text-muted-foreground">
: 0; Nenhuma pessoa encontrada para “{search}”.
const isOwn = group.userId === currentUserId; </p>
const canQuickCreate = ) : (
group.userId !== null && (isAdmin || isOwn); <ul className="divide-y">
{visibleGroups.map((group) => {
const total = group.tasks.length;
const done = group.tasks.filter((t) => t.Status === "done").length;
// Média do % das tarefas: reflete progresso parcial.
const percent =
total > 0
? Math.round(
group.tasks.reduce(
(sum, t) =>
sum +
taskPercent(
t.Status,
t.SubtaskCount,
t.SubtaskDone,
t.Progress
),
0
) / total
)
: 0;
const isOwn = group.userId === currentUserId;
const canQuickCreate =
group.userId !== null && (isAdmin || isOwn);
return ( return (
<div <li key={group.key} className="group relative">
key={group.key} <Link
className="group relative rounded-xl border bg-card transition hover:border-primary/50 hover:shadow-md" href={`/workspaces/${workspaceId}/membro/${group.userId ?? 0}`}
> className="flex items-center gap-3 py-3 pl-4 pr-24 transition-colors hover:bg-muted/40"
<Link
href={`/workspaces/${workspaceId}/membro/${group.userId ?? 0}`}
className="block space-y-4 p-5"
>
<div className="flex items-center gap-3">
<Avatar className="h-12 w-12 ring-2 ring-primary/15">
<AvatarFallback
className={
group.name
? "bg-primary text-primary-foreground text-sm font-semibold"
: "bg-muted text-muted-foreground"
}
> >
{group.name ? ( <Avatar className="h-9 w-9 shrink-0">
initials(group.name) <AvatarFallback
) : ( className={
<UserRound className="h-5 w-5" /> group.name
)} ? "bg-primary text-primary-foreground text-xs font-semibold"
</AvatarFallback> : "bg-muted text-muted-foreground"
</Avatar> }
<div className="min-w-0"> >
<p className="truncate font-semibold leading-tight"> {group.name ? (
{group.name ?? "Sem responsável"} initials(group.name)
{isOwn && ( ) : (
<span className="ml-1.5 text-xs font-normal text-primary"> <UserRound className="h-4 w-4" />
(você) )}
</span> </AvatarFallback>
)} </Avatar>
</p>
<p className="text-xs text-muted-foreground">
{total === 0
? "Nenhuma tarefa"
: `${total} tarefa${total > 1 ? "s" : ""}`}
</p>
</div>
</div>
<div className="flex flex-wrap gap-1.5"> <div className="min-w-0 flex-1">
{STATUS_CHIPS.map((chip) => { <p className="truncate text-sm font-medium">
const count = group.tasks.filter( {group.name ?? "Sem responsável"}
(t) => t.Status === chip.value {isOwn && (
).length; <span className="ml-1.5 text-xs font-normal text-primary">
if (count === 0) return null; (você)
const Icon = chip.icon; </span>
return ( )}
<span </p>
key={chip.value} <p className="text-xs text-muted-foreground">
title={chip.label} {total === 0
className={cn( ? "Nenhuma tarefa"
"inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-xs font-medium", : `${done}/${total} concluída${total > 1 ? "s" : ""}`}
chip.chipBg, </p>
chip.className </div>
)}
>
<Icon className="h-3 w-3" />
{count}
</span>
);
})}
{total === 0 && (
<span className="rounded-full bg-muted px-2 py-0.5 text-xs text-muted-foreground">
Pasta vazia
</span>
)}
</div>
<div className="flex items-center gap-2"> {/* Contadores por status — escondem em telas estreitas */}
<Progress value={percent} className="h-1.5" /> <div className="hidden items-center gap-3 lg:flex">
<span className="text-xs font-semibold text-muted-foreground"> {STATUS_CHIPS.map((chip) => {
{percent}% const count = group.tasks.filter(
</span> (t) => t.Status === chip.value
</div> ).length;
</Link> const Icon = chip.icon;
return (
<span
key={chip.value}
title={chip.label}
className={cn(
"flex w-9 items-center justify-center gap-1 text-xs tabular-nums",
count > 0 ? chip.className : "text-muted-foreground/30"
)}
>
<Icon className="h-3.5 w-3.5" />
{count}
</span>
);
})}
</div>
{canQuickCreate && ( {/* Progresso */}
<div className="absolute right-3 top-3"> <div className="hidden w-40 items-center gap-2 sm:flex">
<TaskDialog <Progress value={percent} className="h-1.5" />
workspaceId={workspaceId} <span className="w-9 shrink-0 text-right text-xs font-semibold tabular-nums text-muted-foreground">
members={members} {percent}%
defaultAssigneeId={group.userId ?? undefined} </span>
currentUserId={currentUserId} </div>
isAdmin={isAdmin} </Link>
trigger={
<Button {/* Ações à direita, fora do Link para HTML válido */}
variant="ghost" <div className="absolute right-3 top-1/2 flex -translate-y-1/2 items-center gap-1">
size="icon" {canQuickCreate && (
className="h-8 w-8 text-muted-foreground transition-opacity md:opacity-0 md:group-hover:opacity-100" <TaskDialog
title={`Nova tarefa para ${group.name}`} workspaceId={workspaceId}
> members={members}
<Plus className="h-4 w-4" /> defaultAssigneeId={group.userId ?? undefined}
</Button> currentUserId={currentUserId}
} isAdmin={isAdmin}
/> trigger={
</div> <Button
)} variant="ghost"
</div> size="icon"
); className="h-8 w-8 text-muted-foreground transition-opacity md:opacity-0 md:group-hover:opacity-100"
})} title={`Nova tarefa para ${group.name ?? "ninguém"}`}
>
<Plus className="h-4 w-4" />
</Button>
}
/>
)}
<ChevronRight className="h-4 w-4 text-muted-foreground" />
</div>
</li>
);
})}
</ul>
)}
</div>
</div> </div>
); );
} }
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
import { Fragment, useState } from "react"; import { Fragment, useState } from "react";
import { CheckCircle2, ChevronRight, Circle, Pencil } from "lucide-react"; import { CheckCircle2, ChevronRight, Circle, Pencil } from "lucide-react";
import type { Subtask, Task, WorkspaceMember } from "@/lib/data"; import type { Subtask, Task, WorkspaceMember } from "@/lib/data";
import { TASK_STATUSES } from "@/lib/constants";
import { formatDate } from "@/lib/dates"; import { formatDate } from "@/lib/dates";
import { canEditTask } from "@/lib/permissions"; import { canEditTask } from "@/lib/permissions";
import { initials, toFormValues } from "./task-utils"; import { initials, toFormValues } from "./task-utils";
...@@ -45,6 +46,7 @@ export function TaskTable({ ...@@ -45,6 +46,7 @@ export function TaskTable({
isAdmin: boolean; isAdmin: boolean;
}) { }) {
const [expanded, setExpanded] = useState<number[]>([]); const [expanded, setExpanded] = useState<number[]>([]);
const [statusFilter, setStatusFilter] = useState<string>("all");
function toggleExpanded(taskId: number) { function toggleExpanded(taskId: number) {
setExpanded((prev) => setExpanded((prev) =>
...@@ -62,8 +64,51 @@ export function TaskTable({ ...@@ -62,8 +64,51 @@ export function TaskTable({
); );
} }
const filteredTasks =
statusFilter === "all"
? tasks
: tasks.filter((t) => t.Status === statusFilter);
const filterOptions = [
{ value: "all", label: "Todas", count: tasks.length },
...TASK_STATUSES.map((s) => ({
value: s.value,
label: s.label,
count: tasks.filter((t) => t.Status === s.value).length,
})),
];
return ( return (
<div className="overflow-hidden rounded-xl border bg-card"> <div className="space-y-3">
<div className="flex flex-wrap items-center gap-2">
{filterOptions.map((option) => (
<button
key={option.value}
type="button"
onClick={() => setStatusFilter(option.value)}
className={cn(
"inline-flex items-center gap-1.5 rounded-full border px-3 py-1 text-xs font-medium transition-colors",
statusFilter === option.value
? "border-primary bg-primary text-primary-foreground"
: "border-border bg-card text-muted-foreground hover:bg-muted"
)}
>
{option.label}
<span
className={cn(
"rounded-full px-1.5 text-[10px]",
statusFilter === option.value
? "bg-primary-foreground/20"
: "bg-muted"
)}
>
{option.count}
</span>
</button>
))}
</div>
<div className="overflow-hidden rounded-xl border bg-card">
<Table> <Table>
<TableHeader> <TableHeader>
<TableRow className="border-b bg-muted/40 hover:bg-muted/40"> <TableRow className="border-b bg-muted/40 hover:bg-muted/40">
...@@ -86,7 +131,7 @@ export function TaskTable({ ...@@ -86,7 +131,7 @@ export function TaskTable({
</TableRow> </TableRow>
</TableHeader> </TableHeader>
<TableBody> <TableBody>
{tasks.map((task) => { {filteredTasks.map((task) => {
const editable = canEditTask(task, currentUserId, isAdmin); const editable = canEditTask(task, currentUserId, isAdmin);
const subtasks = subtasksByTask[task.Id] ?? []; const subtasks = subtasksByTask[task.Id] ?? [];
const hasSubtasks = subtasks.length > 0; const hasSubtasks = subtasks.length > 0;
...@@ -99,8 +144,8 @@ export function TaskTable({ ...@@ -99,8 +144,8 @@ export function TaskTable({
isOpen ? "border-0 bg-muted/20" : "last:border-0" isOpen ? "border-0 bg-muted/20" : "last:border-0"
)} )}
> >
<TableCell className="max-w-[300px] py-3"> <TableCell className="min-w-[260px] py-3 align-top">
<div className="flex items-center gap-1.5"> <div className="flex items-start gap-1.5">
{hasSubtasks ? ( {hasSubtasks ? (
<button <button
type="button" type="button"
...@@ -110,7 +155,7 @@ export function TaskTable({ ...@@ -110,7 +155,7 @@ export function TaskTable({
? "Recolher subtarefas" ? "Recolher subtarefas"
: "Expandir subtarefas" : "Expandir subtarefas"
} }
className="flex shrink-0 items-center gap-1 rounded-md px-1 py-0.5 text-xs text-muted-foreground hover:bg-muted hover:text-foreground" className="mt-0.5 flex shrink-0 items-center gap-1 rounded-md px-1 py-0.5 text-xs text-muted-foreground hover:bg-muted hover:text-foreground"
> >
<ChevronRight <ChevronRight
className={cn( className={cn(
...@@ -124,14 +169,20 @@ export function TaskTable({ ...@@ -124,14 +169,20 @@ export function TaskTable({
) : ( ) : (
<span className="w-5 shrink-0" /> <span className="w-5 shrink-0" />
)} )}
<p className="truncate font-medium">{task.Title}</p> <div className="min-w-0 flex-1">
<PriorityBadge priority={task.Priority} /> <div className="flex flex-wrap items-center gap-1.5">
<span className="font-medium leading-snug">
{task.Title}
</span>
<PriorityBadge priority={task.Priority} />
</div>
{task.Description && (
<p className="mt-0.5 text-xs text-muted-foreground">
{task.Description}
</p>
)}
</div>
</div> </div>
{task.Description && (
<p className="ml-6 mt-0.5 truncate text-xs text-muted-foreground">
{task.Description}
</p>
)}
</TableCell> </TableCell>
<TableCell className="py-3"> <TableCell className="py-3">
{editable ? ( {editable ? (
...@@ -257,6 +308,12 @@ export function TaskTable({ ...@@ -257,6 +308,12 @@ export function TaskTable({
})} })}
</TableBody> </TableBody>
</Table> </Table>
{filteredTasks.length === 0 && (
<p className="p-8 text-center text-sm text-muted-foreground">
Nenhuma tarefa com este status.
</p>
)}
</div>
</div> </div>
); );
} }
// Utilitários compartilhados entre componentes de servidor e de cliente — // Utilitários compartilhados entre componentes de servidor e de cliente —
// este módulo não pode ter "use client" nem importar módulos cliente. // este módulo não pode ter "use client" nem importar módulos cliente.
import { CheckCircle2, Circle, Eye, LoaderCircle } from "lucide-react";
import type { Task } from "@/lib/data"; import type { Task } from "@/lib/data";
import { toISODate } from "@/lib/dates"; import { toISODate } from "@/lib/dates";
import type { TaskFormValues } from "./task-dialog"; import type { TaskFormValues } from "./task-dialog";
export const STATUS_CHIPS = [
{ value: "todo", label: "A Fazer", icon: Circle, className: "text-slate-600 dark:text-slate-300", chipBg: "bg-slate-500/10" },
{ value: "in_progress", label: "Em Andamento", icon: LoaderCircle, className: "text-blue-600 dark:text-blue-400", chipBg: "bg-blue-500/10" },
{ value: "review", label: "Em Revisão", icon: Eye, className: "text-purple-600 dark:text-purple-400", chipBg: "bg-purple-500/10" },
{ value: "done", label: "Concluídas", icon: CheckCircle2, className: "text-green-600 dark:text-green-400", chipBg: "bg-green-500/10" },
] as const;
export function toFormValues(task: Task): TaskFormValues { export function toFormValues(task: Task): TaskFormValues {
return { return {
id: task.Id, id: task.Id,
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment