fix(dashboard): unify styles, theme tokens, and integrate live timer with real tasks
This commit is contained in:
parent
61128c0a1a
commit
5c32665b55
1 changed files with 487 additions and 284 deletions
|
|
@ -1,374 +1,577 @@
|
||||||
import { useState, useEffect } from 'react'
|
import React, { useState, useEffect, useCallback, useMemo } from 'react'
|
||||||
import { Link } from 'react-router-dom'
|
import { Link, useNavigate } from 'react-router-dom'
|
||||||
import {
|
|
||||||
Card,
|
|
||||||
CardHeader,
|
|
||||||
CardTitle,
|
|
||||||
CardDescription,
|
|
||||||
CardContent,
|
|
||||||
CardFooter,
|
|
||||||
Button,
|
|
||||||
Chip,
|
|
||||||
ChipLabel,
|
|
||||||
Avatar,
|
|
||||||
AvatarFallback,
|
|
||||||
} from '@heroui/react'
|
|
||||||
import {
|
import {
|
||||||
Clock,
|
Clock,
|
||||||
CheckCircle2,
|
CheckCircle2,
|
||||||
FolderKanban,
|
FolderKanban,
|
||||||
Play,
|
Play,
|
||||||
Square,
|
Square,
|
||||||
Users,
|
|
||||||
TrendingUp,
|
TrendingUp,
|
||||||
Plus,
|
Plus,
|
||||||
Sparkles,
|
Sparkles,
|
||||||
ArrowRight,
|
ArrowRight,
|
||||||
Flame,
|
Flame,
|
||||||
|
RefreshCw,
|
||||||
|
Folder,
|
||||||
|
Tag,
|
||||||
|
Activity,
|
||||||
|
AlertCircle,
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import { useAuth } from '../contexts/AuthContext'
|
import { useAuth } from '../contexts/AuthContext'
|
||||||
|
import { useTimer } from '../contexts/TimerContext'
|
||||||
|
import { taskService } from '../services/taskService'
|
||||||
|
import { projectService } from '../services/projectService'
|
||||||
|
import type { Task, Project } from '../types/task'
|
||||||
|
import { calculateTaskTotalSeconds, formatSecondsToHuman, formatSecondsToTimer } from '../lib/timeUtils'
|
||||||
|
import { CreateTaskModal } from '../components/tasks/CreateTaskModal'
|
||||||
|
|
||||||
export const DashboardPage = () => {
|
export const DashboardPage: React.FC = () => {
|
||||||
const { user, activeOrg } = useAuth()
|
const { user, activeOrg } = useAuth()
|
||||||
|
const { activeTask, activeSeconds, startTimer, stopTimer, syncWithTasks } = useTimer()
|
||||||
|
const navigate = useNavigate()
|
||||||
|
|
||||||
// Simulação de cronômetro local interativo
|
const [tasks, setTasks] = useState<Task[]>([])
|
||||||
const [isRunning, setIsRunning] = useState(false)
|
const [projects, setProjects] = useState<Project[]>([])
|
||||||
const [timerSeconds, setTimerSeconds] = useState(1450)
|
const [loading, setLoading] = useState(true)
|
||||||
|
const [refreshing, setRefreshing] = useState(false)
|
||||||
|
const [error, setError] = useState<string | null>(null)
|
||||||
|
|
||||||
|
// Modal
|
||||||
|
const [isModalOpen, setIsModalOpen] = useState(false)
|
||||||
|
|
||||||
|
const activeOrgId = activeOrg?.id
|
||||||
|
|
||||||
|
const loadData = useCallback(async () => {
|
||||||
|
if (!activeOrgId) return
|
||||||
|
try {
|
||||||
|
setRefreshing(true)
|
||||||
|
const [tasksRes, projsRes] = await Promise.allSettled([
|
||||||
|
taskService.listTasks({ organizationId: activeOrgId }),
|
||||||
|
projectService.listProjects(activeOrgId),
|
||||||
|
])
|
||||||
|
|
||||||
|
const loadedTasks = tasksRes.status === 'fulfilled' ? tasksRes.value || [] : []
|
||||||
|
setTasks(loadedTasks)
|
||||||
|
syncWithTasks(loadedTasks)
|
||||||
|
|
||||||
|
if (projsRes.status === 'fulfilled') {
|
||||||
|
setProjects(projsRes.value || [])
|
||||||
|
}
|
||||||
|
} catch (err: unknown) {
|
||||||
|
console.error(err)
|
||||||
|
setError('Falha ao atualizar dados do dashboard.')
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
setRefreshing(false)
|
||||||
|
}
|
||||||
|
}, [activeOrgId, syncWithTasks])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let interval: ReturnType<typeof setInterval>
|
loadData()
|
||||||
if (isRunning) {
|
}, [loadData])
|
||||||
interval = setInterval(() => {
|
|
||||||
setTimerSeconds((prev) => prev + 1)
|
|
||||||
}, 1000)
|
|
||||||
}
|
|
||||||
return () => clearInterval(interval)
|
|
||||||
}, [isRunning])
|
|
||||||
|
|
||||||
const formatTimer = (total: number) => {
|
// KPIs
|
||||||
const h = Math.floor(total / 3600)
|
const stats = useMemo(() => {
|
||||||
const m = Math.floor((total % 3600) / 60)
|
const inProgress = tasks.filter((t) => t.status === 'IN_PROGRESS').length
|
||||||
const s = total % 60
|
const completed = tasks.filter((t) => t.status === 'COMPLETED').length
|
||||||
return `${h.toString().padStart(2, '0')}:${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`
|
const notStarted = tasks.filter((t) => t.status === 'NOT_STARTED').length
|
||||||
|
const totalSeconds = tasks.reduce((acc, t) => acc + calculateTaskTotalSeconds(t), 0)
|
||||||
|
|
||||||
|
return {
|
||||||
|
inProgress,
|
||||||
|
completed,
|
||||||
|
notStarted,
|
||||||
|
totalSeconds,
|
||||||
|
projectsCount: projects.length,
|
||||||
|
}
|
||||||
|
}, [tasks, projects])
|
||||||
|
|
||||||
|
// Recent 5 tasks
|
||||||
|
const recentTasks = useMemo(() => {
|
||||||
|
return [...tasks].slice(0, 5)
|
||||||
|
}, [tasks])
|
||||||
|
|
||||||
|
// Toggle timer on a task
|
||||||
|
const handleToggleTimer = async (task: Task) => {
|
||||||
|
try {
|
||||||
|
if (activeTask?.id === task.id) {
|
||||||
|
const stopped = await stopTimer(task)
|
||||||
|
if (stopped) {
|
||||||
|
setTasks((prev) => prev.map((t) => (t.id === stopped.id ? stopped : t)))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const started = await startTimer(task)
|
||||||
|
setTasks((prev) => prev.map((t) => (t.id === started.id ? started : t)))
|
||||||
|
}
|
||||||
|
} catch (err: unknown) {
|
||||||
|
console.error(err)
|
||||||
|
setError('Erro ao acionar cronômetro.')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const kpis = [
|
const handleModalSubmit = async (taskData: {
|
||||||
{
|
title: string
|
||||||
label: 'Tarefas em Andamento',
|
description: string
|
||||||
value: '4',
|
status: 'NOT_STARTED' | 'IN_PROGRESS' | 'COMPLETED' | 'CANCELED'
|
||||||
sub: '2 com prazo hoje',
|
projectId?: number
|
||||||
icon: <Clock className="w-5 h-5 text-brand" />,
|
categoryId?: number
|
||||||
bg: 'bg-brand-soft',
|
assigneeUserId?: number
|
||||||
},
|
}) => {
|
||||||
{
|
if (!activeOrgId) return
|
||||||
label: 'Finalizadas nesta Semana',
|
const created = await taskService.createTask({
|
||||||
value: '18',
|
title: taskData.title,
|
||||||
sub: '+25% vs semana anterior',
|
description: taskData.description,
|
||||||
icon: <CheckCircle2 className="w-5 h-5 text-emerald-600" />,
|
status: taskData.status,
|
||||||
bg: 'bg-emerald-50',
|
projectId: taskData.projectId,
|
||||||
},
|
categoryId: taskData.categoryId,
|
||||||
{
|
assigneeUserId: taskData.assigneeUserId,
|
||||||
label: 'Horas Rastreadas (Você)',
|
organizationId: activeOrgId,
|
||||||
value: '26.4h',
|
})
|
||||||
sub: 'Média de 6.6h / dia',
|
setTasks((prev) => [created, ...prev])
|
||||||
icon: <TrendingUp className="w-5 h-5 text-accent" />,
|
}
|
||||||
bg: 'bg-accent-soft',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Projetos Conectados',
|
|
||||||
value: '5',
|
|
||||||
sub: 'No workspace ativo',
|
|
||||||
icon: <FolderKanban className="w-5 h-5 text-brand" />,
|
|
||||||
bg: 'bg-brand-soft',
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
const recentTasks = [
|
// Priority task to focus on if none running
|
||||||
{
|
const suggestedTask = useMemo(() => {
|
||||||
id: 1,
|
return tasks.find((t) => t.status === 'IN_PROGRESS') || tasks.find((t) => t.status === 'NOT_STARTED') || tasks[0]
|
||||||
title: 'Implementação do AppShell e Sidebar retrátil',
|
}, [tasks])
|
||||||
project: 'Frontend v3',
|
|
||||||
category: 'Feature',
|
|
||||||
status: 'EM ANDAMENTO',
|
|
||||||
time: '02h 15m',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
title: 'Ajuste de Headers CORS e preflight no Quarkus',
|
|
||||||
project: 'Core Backend',
|
|
||||||
category: 'Bugfix',
|
|
||||||
status: 'CONCLUÍDO',
|
|
||||||
time: '01h 05m',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 3,
|
|
||||||
title: 'Configuração de balde MinIO e expiração de URLs',
|
|
||||||
project: 'Infra & Storage',
|
|
||||||
category: 'DevOps',
|
|
||||||
status: 'A FAZER',
|
|
||||||
time: '00h 00m',
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-8">
|
<div className="space-y-6 max-w-7xl mx-auto">
|
||||||
{/* Top Welcome Banner */}
|
{/* Header */}
|
||||||
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4">
|
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2 mb-1">
|
||||||
<h1 className="text-2xl sm:text-3xl font-extrabold tracking-tight text-text-main">
|
<h1 className="text-2xl font-bold tracking-tight text-foreground">
|
||||||
Olá, {user?.name?.split(' ')[0] || 'Desenvolvedor'}! 👋
|
Olá, {user?.name?.split(' ')[0] || 'Desenvolvedor'}! 👋
|
||||||
</h1>
|
</h1>
|
||||||
<Chip className="bg-brand-soft text-brand text-xs font-semibold px-2 py-0.5 border border-brand/20">
|
<span className="px-2.5 py-0.5 rounded-full text-xs font-semibold bg-brand/15 text-brand border border-brand/25">
|
||||||
<ChipLabel>{activeOrg?.name || 'Workspace'}</ChipLabel>
|
{activeOrg?.name || 'Workspace'}
|
||||||
</Chip>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-text-muted mt-1">
|
<p className="text-xs text-muted">
|
||||||
Aqui está o resumo da sua produtividade e das atividades da equipe hoje.
|
Visão geral da sua produtividade, metas e atividades recentes no workspace.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-2.5 flex-wrap">
|
||||||
<Link to="/tasks">
|
<button
|
||||||
<Button className="bg-surface hover:bg-surface-muted text-text-main font-semibold text-xs px-4 py-2.5 rounded-xl border border-border-soft transition-colors shadow-xs">
|
type="button"
|
||||||
Ver Quadro de Tarefas
|
onClick={loadData}
|
||||||
</Button>
|
disabled={refreshing}
|
||||||
</Link>
|
title="Recarregar dashboard"
|
||||||
<Link to="/tasks/new">
|
className="p-2 rounded-xl border border-border bg-surface text-muted hover:text-foreground hover:bg-surface-raised transition-colors disabled:opacity-50"
|
||||||
<Button className="bg-brand hover:bg-brand-strong text-white font-bold text-xs px-4 py-2.5 rounded-xl shadow-md shadow-brand/20 transition-all flex items-center gap-1.5">
|
>
|
||||||
<Plus className="w-4 h-4" />
|
<RefreshCw className={`w-4 h-4 ${refreshing ? 'animate-spin text-brand' : ''}`} />
|
||||||
Nova Tarefa
|
</button>
|
||||||
</Button>
|
|
||||||
</Link>
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => navigate('/tasks')}
|
||||||
|
className="px-3.5 py-2 rounded-xl border border-border bg-surface text-xs font-semibold text-foreground hover:bg-surface-raised transition-colors shadow-xs"
|
||||||
|
>
|
||||||
|
Quadro Kanban
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setIsModalOpen(true)}
|
||||||
|
className="flex items-center gap-1.5 px-4 py-2 rounded-xl bg-brand text-white font-semibold text-xs hover:bg-brand-hover shadow-md shadow-brand/20 transition-all"
|
||||||
|
>
|
||||||
|
<Plus className="w-4 h-4" />
|
||||||
|
<span>Nova Tarefa</span>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* KPI Stats Grid */}
|
{error && (
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-5">
|
<div className="flex items-center justify-between p-3.5 rounded-xl bg-rose-500/10 border border-rose-500/20 text-rose-300 text-xs">
|
||||||
{kpis.map((kpi, idx) => (
|
<div className="flex items-center gap-2">
|
||||||
<Card
|
<AlertCircle className="w-4 h-4 text-rose-400" />
|
||||||
key={idx}
|
<span>{error}</span>
|
||||||
className="bg-surface border border-border-soft rounded-2xl p-5 shadow-xs hover:shadow-panel transition-all"
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setError(null)}
|
||||||
|
className="text-rose-400 hover:text-rose-200 text-xs font-bold"
|
||||||
>
|
>
|
||||||
<CardContent className="p-0 space-y-3">
|
Fechar
|
||||||
<div className="flex items-center justify-between">
|
</button>
|
||||||
<span className="text-xs font-bold text-text-muted">
|
</div>
|
||||||
{kpi.label}
|
)}
|
||||||
</span>
|
|
||||||
<div className={`p-2.5 rounded-xl ${kpi.bg}`}>
|
|
||||||
{kpi.icon}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p className="text-2xl sm:text-3xl font-extrabold font-mono text-text-main">
|
|
||||||
{kpi.value}
|
|
||||||
</p>
|
|
||||||
<p className="text-[11px] text-text-muted font-medium mt-0.5">
|
|
||||||
{kpi.sub}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Main Grid: Active Task Tracker + Recent Tasks */}
|
{/* KPI Cards */}
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-12 gap-8">
|
{loading ? (
|
||||||
{/* Active Session & Pomodoro Card (Left 5 cols) */}
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||||
<div className="lg:col-span-5 space-y-6">
|
{[1, 2, 3, 4].map((i) => (
|
||||||
<Card className="bg-surface border border-border-soft rounded-3xl p-6 shadow-panel space-y-5">
|
<div
|
||||||
<CardHeader className="p-0 flex items-center justify-between pb-3 border-b border-border-soft">
|
key={i}
|
||||||
<div className="flex items-center gap-2">
|
className="h-28 rounded-2xl border border-border bg-surface-sunken/40 animate-pulse p-4"
|
||||||
<div className="p-2 rounded-xl bg-accent-soft text-accent">
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||||
|
{/* In Progress */}
|
||||||
|
<div className="p-5 rounded-2xl border border-border bg-surface flex flex-col justify-between shadow-xs hover:border-zinc-700 transition-colors">
|
||||||
|
<div className="flex items-center justify-between mb-3">
|
||||||
|
<span className="text-xs font-semibold text-muted">Tarefas em Andamento</span>
|
||||||
|
<div className="w-8 h-8 rounded-lg bg-teal-500/15 border border-teal-500/30 flex items-center justify-center text-teal-400">
|
||||||
|
<Clock className="w-4 h-4" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-2xl font-extrabold text-teal-400 tracking-tight">
|
||||||
|
{stats.inProgress}
|
||||||
|
</p>
|
||||||
|
<p className="text-[11px] text-muted mt-1">
|
||||||
|
{stats.notStarted} no backlog para iniciar
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Completed */}
|
||||||
|
<div className="p-5 rounded-2xl border border-border bg-surface flex flex-col justify-between shadow-xs hover:border-zinc-700 transition-colors">
|
||||||
|
<div className="flex items-center justify-between mb-3">
|
||||||
|
<span className="text-xs font-semibold text-muted">Entregas Concluídas</span>
|
||||||
|
<div className="w-8 h-8 rounded-lg bg-emerald-500/15 border border-emerald-500/30 flex items-center justify-center text-emerald-400">
|
||||||
|
<CheckCircle2 className="w-4 h-4" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-2xl font-extrabold text-emerald-400 tracking-tight">
|
||||||
|
{stats.completed}
|
||||||
|
</p>
|
||||||
|
<p className="text-[11px] text-muted mt-1">
|
||||||
|
de {tasks.length} tarefas cadastradas
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Time Tracked */}
|
||||||
|
<div className="p-5 rounded-2xl border border-border bg-surface flex flex-col justify-between shadow-xs hover:border-zinc-700 transition-colors">
|
||||||
|
<div className="flex items-center justify-between mb-3">
|
||||||
|
<span className="text-xs font-semibold text-muted">Tempo Total Dedicado</span>
|
||||||
|
<div className="w-8 h-8 rounded-lg bg-amber-500/15 border border-amber-500/30 flex items-center justify-center text-amber-400">
|
||||||
|
<TrendingUp className="w-4 h-4" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-2xl font-extrabold text-amber-400 tracking-tight font-mono">
|
||||||
|
{formatSecondsToTimer(stats.totalSeconds)}
|
||||||
|
</p>
|
||||||
|
<p className="text-[11px] text-muted mt-1">
|
||||||
|
{formatSecondsToHuman(stats.totalSeconds)} acumuladas
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Projects */}
|
||||||
|
<div className="p-5 rounded-2xl border border-border bg-surface flex flex-col justify-between shadow-xs hover:border-zinc-700 transition-colors">
|
||||||
|
<div className="flex items-center justify-between mb-3">
|
||||||
|
<span className="text-xs font-semibold text-muted">Projetos Ativos</span>
|
||||||
|
<div className="w-8 h-8 rounded-lg bg-brand/15 border border-brand/30 flex items-center justify-center text-brand">
|
||||||
|
<FolderKanban className="w-4 h-4" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-2xl font-extrabold text-foreground tracking-tight">
|
||||||
|
{stats.projectsCount}
|
||||||
|
</p>
|
||||||
|
<p className="text-[11px] text-muted mt-1">
|
||||||
|
Iniciativas no workspace
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Main Content: Focus Session (Left) + Recent Tasks (Right) */}
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-12 gap-6">
|
||||||
|
{/* Focus Session Card (Left 5 cols) */}
|
||||||
|
<div className="lg:col-span-5 space-y-5">
|
||||||
|
<div className="rounded-2xl border border-border bg-surface p-6 shadow-xs space-y-5">
|
||||||
|
<div className="flex items-center justify-between pb-3 border-b border-border">
|
||||||
|
<div className="flex items-center gap-2.5">
|
||||||
|
<div className="w-8 h-8 rounded-lg bg-brand/15 border border-brand/30 flex items-center justify-center text-brand">
|
||||||
<Flame className="w-4 h-4" />
|
<Flame className="w-4 h-4" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<CardTitle className="text-sm font-bold text-text-main">
|
<h2 className="text-sm font-bold text-foreground">Sessão em Foco</h2>
|
||||||
Sessão em Foco
|
<p className="text-xs text-muted">Cronômetro em tempo real</p>
|
||||||
</CardTitle>
|
|
||||||
<CardDescription className="text-xs text-text-muted">
|
|
||||||
Time tracking em tempo real
|
|
||||||
</CardDescription>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Chip className={isRunning ? "bg-emerald-100 text-emerald-800 text-xs font-bold" : "bg-surface-muted text-text-muted text-xs font-semibold"}>
|
|
||||||
<ChipLabel>{isRunning ? "ATIVO" : "PAUSADO"}</ChipLabel>
|
|
||||||
</Chip>
|
|
||||||
</CardHeader>
|
|
||||||
|
|
||||||
<CardContent className="p-0 space-y-4">
|
<span
|
||||||
<div className="p-4 rounded-2xl bg-surface-muted/60 border border-border-soft space-y-2">
|
className={`px-2.5 py-0.5 rounded-full text-[10px] font-bold border ${
|
||||||
|
activeTask
|
||||||
|
? 'bg-teal-500/15 text-teal-300 border-teal-500/30 animate-pulse'
|
||||||
|
: 'bg-zinc-800 text-zinc-400 border-zinc-700'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{activeTask ? 'ATIVO' : 'EM ESPERA'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Active or Suggested Task Info */}
|
||||||
|
{activeTask ? (
|
||||||
|
<div className="p-4 rounded-xl bg-surface-raised border border-border space-y-2">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Chip className="bg-brand-soft text-brand text-[10px] font-bold">
|
{activeTask.project && (
|
||||||
<ChipLabel>Frontend v3</ChipLabel>
|
<span
|
||||||
</Chip>
|
className="px-2 py-0.5 rounded-md text-[10px] font-medium border"
|
||||||
<span className="text-xs font-medium text-text-muted">
|
style={{
|
||||||
Squad Core
|
backgroundColor: activeTask.project.color ? `${activeTask.project.color}15` : 'rgba(15,118,110,0.1)',
|
||||||
</span>
|
color: activeTask.project.color || '#0f766e',
|
||||||
|
borderColor: activeTask.project.color ? `${activeTask.project.color}30` : 'rgba(15,118,110,0.25)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{activeTask.project.name}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{activeTask.category && (
|
||||||
|
<span className="px-2 py-0.5 rounded-md text-[10px] font-medium bg-amber-500/15 text-amber-300 border border-amber-500/25">
|
||||||
|
{activeTask.category.name}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<h3 className="text-sm font-bold text-text-main">
|
<h3 className="text-sm font-bold text-foreground line-clamp-2">
|
||||||
Refatoração do Layout Principal & Navegação com HeroUI
|
{activeTask.title}
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
|
) : suggestedTask ? (
|
||||||
{/* Big Timer display */}
|
<div className="p-4 rounded-xl bg-surface-sunken/60 border border-border space-y-2">
|
||||||
<div className="text-center py-3 bg-surface rounded-2xl border border-border-soft">
|
<div className="flex items-center justify-between">
|
||||||
<p className="text-xs font-semibold uppercase tracking-wider text-text-muted">
|
<span className="text-[10px] font-semibold text-muted uppercase tracking-wider">
|
||||||
Tempo na Tarefa
|
Próxima da fila
|
||||||
</p>
|
</span>
|
||||||
<p className="text-3xl sm:text-4xl font-extrabold font-mono text-text-main mt-1">
|
{suggestedTask.project && (
|
||||||
{formatTimer(timerSeconds)}
|
<span className="text-[10px] text-muted truncate max-w-[120px]">
|
||||||
</p>
|
{suggestedTask.project.name}
|
||||||
</div>
|
</span>
|
||||||
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<Button
|
|
||||||
onClick={() => setIsRunning(!isRunning)}
|
|
||||||
className={`flex-1 font-bold text-xs py-3 rounded-xl flex items-center justify-center gap-2 transition-all ${
|
|
||||||
isRunning
|
|
||||||
? 'bg-danger-soft text-danger hover:bg-danger/20 border border-danger/30'
|
|
||||||
: 'bg-brand hover:bg-brand-strong text-white shadow-md shadow-brand/20'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{isRunning ? (
|
|
||||||
<>
|
|
||||||
<Square className="w-4 h-4 fill-current" />
|
|
||||||
Pausar Tempo
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<Play className="w-4 h-4 fill-current" />
|
|
||||||
Iniciar Tempo
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
</Button>
|
</div>
|
||||||
|
<h3 className="text-sm font-semibold text-foreground line-clamp-2">
|
||||||
|
{suggestedTask.title}
|
||||||
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
) : (
|
||||||
|
<div className="p-6 rounded-xl bg-surface-sunken/40 border border-dashed border-border text-center text-xs text-muted">
|
||||||
|
Nenhuma tarefa pendente. Crie uma nova tarefa para iniciar o foco!
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<CardFooter className="p-0 pt-3 border-t border-border-soft flex items-center justify-between text-xs text-text-muted">
|
{/* Big Timer Display */}
|
||||||
<span>Sessão vinculada ao usuário</span>
|
<div className="text-center py-4 rounded-xl bg-surface-sunken border border-border">
|
||||||
<span className="font-mono text-[11px] font-bold text-brand">
|
<span className="text-[10px] font-semibold uppercase tracking-wider text-muted">
|
||||||
{user?.email}
|
{activeTask ? 'Tempo Decorrido na Tarefa' : 'Pronto para Cronometrar'}
|
||||||
</span>
|
</span>
|
||||||
</CardFooter>
|
<p className="text-3xl sm:text-4xl font-extrabold font-mono text-foreground mt-1">
|
||||||
</Card>
|
{activeTask ? formatSecondsToTimer(activeSeconds) : '00:00:00'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Controls */}
|
||||||
|
{activeTask ? (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => stopTimer(activeTask)}
|
||||||
|
className="w-full py-2.5 rounded-xl bg-rose-500/20 border border-rose-500/30 text-rose-300 hover:bg-rose-500/30 text-xs font-bold transition-all flex items-center justify-center gap-2"
|
||||||
|
>
|
||||||
|
<Square className="w-4 h-4 fill-rose-300" />
|
||||||
|
<span>Pausar Cronômetro</span>
|
||||||
|
</button>
|
||||||
|
) : suggestedTask ? (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => handleToggleTimer(suggestedTask)}
|
||||||
|
className="w-full py-2.5 rounded-xl bg-brand text-white hover:bg-brand-hover shadow-md shadow-brand/20 text-xs font-bold transition-all flex items-center justify-center gap-2"
|
||||||
|
>
|
||||||
|
<Play className="w-4 h-4 fill-white" />
|
||||||
|
<span>Iniciar Foco Nesta Tarefa</span>
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setIsModalOpen(true)}
|
||||||
|
className="w-full py-2.5 rounded-xl bg-brand text-white hover:bg-brand-hover text-xs font-bold transition-all flex items-center justify-center gap-2"
|
||||||
|
>
|
||||||
|
<Plus className="w-4 h-4" />
|
||||||
|
<span>Criar Tarefa para Iniciar</span>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="pt-2 border-t border-border flex items-center justify-between text-[11px] text-muted">
|
||||||
|
<span>Usuário ativo:</span>
|
||||||
|
<span className="font-semibold text-foreground truncate max-w-[150px]">
|
||||||
|
{user?.name || user?.email}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Quick Shortcuts */}
|
{/* Quick Shortcuts */}
|
||||||
<Card className="bg-surface border border-border-soft rounded-2xl p-5 shadow-xs space-y-3">
|
<div className="rounded-2xl border border-border bg-surface p-5 shadow-xs space-y-3">
|
||||||
<h4 className="text-xs font-bold uppercase tracking-wider text-text-muted">
|
<span className="text-[11px] font-bold uppercase tracking-wider text-muted block">
|
||||||
Acesso Rápido
|
Atalhos de Gestão
|
||||||
</h4>
|
</span>
|
||||||
<div className="grid grid-cols-2 gap-2 text-xs font-semibold">
|
<div className="grid grid-cols-2 gap-2 text-xs font-semibold">
|
||||||
<Link
|
<Link
|
||||||
to="/projects"
|
to="/projects"
|
||||||
className="p-3 rounded-xl bg-surface-muted hover:bg-brand-soft/50 hover:text-brand transition-colors flex items-center gap-2"
|
className="p-3 rounded-xl bg-surface-raised hover:bg-brand/10 hover:text-brand border border-border transition-colors flex items-center gap-2 text-foreground"
|
||||||
>
|
>
|
||||||
<FolderKanban className="w-4 h-4 text-brand" />
|
<FolderKanban className="w-4 h-4 text-brand" />
|
||||||
<span>Ver Projetos</span>
|
<span>Projetos</span>
|
||||||
</Link>
|
</Link>
|
||||||
<Link
|
<Link
|
||||||
to="/analytics"
|
to="/analytics"
|
||||||
className="p-3 rounded-xl bg-surface-muted hover:bg-accent-soft/50 hover:text-accent transition-colors flex items-center gap-2"
|
className="p-3 rounded-xl bg-surface-raised hover:bg-amber-500/10 hover:text-amber-400 border border-border transition-colors flex items-center gap-2 text-foreground"
|
||||||
>
|
>
|
||||||
<TrendingUp className="w-4 h-4 text-accent" />
|
<TrendingUp className="w-4 h-4 text-amber-400" />
|
||||||
<span>Métricas da Org</span>
|
<span>Analytics</span>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Recent Tasks List (Right 7 cols) */}
|
{/* Recent Tasks List (Right 7 cols) */}
|
||||||
<div className="lg:col-span-7 space-y-4">
|
<div className="lg:col-span-7 space-y-4">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<h2 className="text-lg font-bold text-text-main flex items-center gap-2">
|
<h2 className="text-base font-bold text-foreground flex items-center gap-2">
|
||||||
<Sparkles className="w-4 h-4 text-accent" />
|
<Sparkles className="w-4 h-4 text-accent" />
|
||||||
Tarefas Recentes da Sprint
|
Tarefas Recentes
|
||||||
</h2>
|
</h2>
|
||||||
<Link
|
<Link
|
||||||
to="/tasks"
|
to="/tasks"
|
||||||
className="text-xs font-bold text-brand hover:underline flex items-center gap-1"
|
className="text-xs font-bold text-brand hover:underline flex items-center gap-1"
|
||||||
>
|
>
|
||||||
Ver todas
|
<span>Ver todas</span>
|
||||||
<ArrowRight className="w-3.5 h-3.5" />
|
<ArrowRight className="w-3.5 h-3.5" />
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-3">
|
{loading ? (
|
||||||
{recentTasks.map((task) => (
|
<div className="space-y-3">
|
||||||
<Card
|
{[1, 2, 3].map((i) => (
|
||||||
key={task.id}
|
<div
|
||||||
className="bg-surface border border-border-soft rounded-2xl p-4 shadow-xs hover:shadow-panel hover:border-brand/30 transition-all flex flex-col sm:flex-row items-start sm:items-center justify-between gap-3"
|
key={i}
|
||||||
|
className="h-20 rounded-2xl border border-border bg-surface-sunken/40 animate-pulse p-4"
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : recentTasks.length === 0 ? (
|
||||||
|
<div className="text-center py-16 rounded-2xl border border-dashed border-border bg-surface/50">
|
||||||
|
<Activity className="w-8 h-8 text-muted mx-auto mb-2" />
|
||||||
|
<p className="text-xs text-muted mb-4">
|
||||||
|
Nenhuma tarefa encontrada neste workspace.
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setIsModalOpen(true)}
|
||||||
|
className="inline-flex items-center gap-1.5 px-4 py-2 rounded-xl bg-brand text-white text-xs font-semibold hover:bg-brand-hover shadow-xs"
|
||||||
>
|
>
|
||||||
<div className="space-y-1.5 flex-1 min-w-0">
|
<Plus className="w-3.5 h-3.5" />
|
||||||
<div className="flex items-center gap-2">
|
<span>Criar Primeira Tarefa</span>
|
||||||
<Chip className="bg-surface-muted text-text-muted text-[10px] font-semibold border border-border-soft">
|
</button>
|
||||||
<ChipLabel>{task.project}</ChipLabel>
|
</div>
|
||||||
</Chip>
|
) : (
|
||||||
<Chip className="bg-brand-soft text-brand text-[10px] font-bold">
|
<div className="space-y-3">
|
||||||
<ChipLabel>{task.category}</ChipLabel>
|
{recentTasks.map((task) => {
|
||||||
</Chip>
|
const isTracking = activeTask?.id === task.id
|
||||||
</div>
|
const taskSec = isTracking ? activeSeconds : calculateTaskTotalSeconds(task)
|
||||||
<h3 className="text-sm font-bold text-text-main truncate">
|
|
||||||
{task.title}
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center gap-3 shrink-0 self-end sm:self-center">
|
return (
|
||||||
<span className="text-xs font-mono font-semibold text-text-muted">
|
<div
|
||||||
{task.time}
|
key={task.id}
|
||||||
</span>
|
className={`rounded-2xl border p-4 transition-all duration-200 flex flex-col sm:flex-row items-start sm:items-center justify-between gap-3 ${
|
||||||
<Chip
|
isTracking
|
||||||
className={`text-[10px] font-bold ${
|
? 'border-brand/60 bg-brand/5 ring-1 ring-brand/30 shadow-md'
|
||||||
task.status === 'CONCLUÍDO'
|
: 'border-border bg-surface hover:border-zinc-700 shadow-xs'
|
||||||
? 'bg-emerald-100 text-emerald-800'
|
|
||||||
: task.status === 'EM ANDAMENTO'
|
|
||||||
? 'bg-amber-100 text-amber-800'
|
|
||||||
: 'bg-surface-muted text-text-muted'
|
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<ChipLabel>{task.status}</ChipLabel>
|
<div className="space-y-1.5 flex-1 min-w-0">
|
||||||
</Chip>
|
<div className="flex items-center gap-1.5 flex-wrap">
|
||||||
</div>
|
{task.project && (
|
||||||
</Card>
|
<span
|
||||||
))}
|
className="inline-flex items-center gap-1 px-2 py-0.5 rounded-md text-[10px] font-medium border"
|
||||||
</div>
|
style={{
|
||||||
|
backgroundColor: task.project.color ? `${task.project.color}15` : 'rgba(255,255,255,0.05)',
|
||||||
|
color: task.project.color || '#94a3b8',
|
||||||
|
borderColor: task.project.color ? `${task.project.color}30` : 'rgba(255,255,255,0.1)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Folder className="w-2.5 h-2.5" />
|
||||||
|
{task.project.name}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{task.category && (
|
||||||
|
<span
|
||||||
|
className="inline-flex items-center gap-1 px-2 py-0.5 rounded-md text-[10px] font-medium border"
|
||||||
|
style={{
|
||||||
|
backgroundColor: task.category.color ? `${task.category.color}15` : 'rgba(217,119,6,0.1)',
|
||||||
|
color: task.category.color || '#d97706',
|
||||||
|
borderColor: task.category.color ? `${task.category.color}30` : 'rgba(217,119,6,0.2)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Tag className="w-2.5 h-2.5" />
|
||||||
|
{task.category.name}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Team Active Members Banner */}
|
<h3
|
||||||
<Card className="bg-gradient-to-r from-surface to-surface-muted border border-border-soft rounded-2xl p-4 flex items-center justify-between">
|
onClick={() => navigate('/tasks')}
|
||||||
<div className="flex items-center gap-3">
|
className="text-xs font-bold text-foreground hover:text-brand cursor-pointer truncate"
|
||||||
<div className="w-9 h-9 rounded-xl bg-brand-soft text-brand flex items-center justify-center font-bold">
|
>
|
||||||
<Users className="w-5 h-5 text-brand" />
|
{task.title}
|
||||||
</div>
|
</h3>
|
||||||
<div>
|
</div>
|
||||||
<p className="text-xs font-bold text-text-main">
|
|
||||||
Squad Ativo no Workspace
|
|
||||||
</p>
|
|
||||||
<p className="text-[11px] text-text-muted">
|
|
||||||
3 colaboradores com sessões registradas hoje
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex -space-x-2">
|
<div className="flex items-center gap-3 shrink-0 self-end sm:self-center">
|
||||||
<Avatar className="w-7 h-7 rounded-full ring-2 ring-surface">
|
<div
|
||||||
<AvatarFallback className="bg-brand text-white text-[10px] font-bold">
|
className={`inline-flex items-center gap-1 px-2.5 py-1 rounded-md font-mono text-[11px] ${
|
||||||
GB
|
isTracking
|
||||||
</AvatarFallback>
|
? 'bg-teal-500/20 text-teal-300 font-bold'
|
||||||
</Avatar>
|
: 'bg-surface-sunken text-muted'
|
||||||
<Avatar className="w-7 h-7 rounded-full ring-2 ring-surface">
|
}`}
|
||||||
<AvatarFallback className="bg-accent text-white text-[10px] font-bold">
|
>
|
||||||
AL
|
<Clock className={`w-3 h-3 ${isTracking ? 'animate-pulse text-brand' : ''}`} />
|
||||||
</AvatarFallback>
|
<span>{formatSecondsToTimer(taskSec)}</span>
|
||||||
</Avatar>
|
</div>
|
||||||
<Avatar className="w-7 h-7 rounded-full ring-2 ring-surface">
|
|
||||||
<AvatarFallback className="bg-brand-strong text-white text-[10px] font-bold">
|
<button
|
||||||
MS
|
type="button"
|
||||||
</AvatarFallback>
|
onClick={() => handleToggleTimer(task)}
|
||||||
</Avatar>
|
title={isTracking ? 'Pausar cronômetro' : 'Iniciar cronômetro'}
|
||||||
|
className={`p-1.5 rounded-lg transition-colors ${
|
||||||
|
isTracking
|
||||||
|
? 'bg-rose-500/20 text-rose-300 hover:bg-rose-500/30'
|
||||||
|
: 'bg-brand/15 text-teal-300 hover:bg-brand/25'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{isTracking ? (
|
||||||
|
<Square className="w-3.5 h-3.5 fill-rose-300" />
|
||||||
|
) : (
|
||||||
|
<Play className="w-3.5 h-3.5 fill-teal-300" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Task Creation Modal */}
|
||||||
|
{activeOrgId && (
|
||||||
|
<CreateTaskModal
|
||||||
|
isOpen={isModalOpen}
|
||||||
|
onClose={() => setIsModalOpen(false)}
|
||||||
|
onSubmit={handleModalSubmit}
|
||||||
|
organizationId={activeOrgId}
|
||||||
|
initialStatus="NOT_STARTED"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue