1171 lines
50 KiB
TypeScript
1171 lines
50 KiB
TypeScript
import React, { useState, useEffect, useCallback, useMemo } from 'react'
|
|
import { Link, useNavigate, useSearchParams } from 'react-router-dom'
|
|
import {
|
|
Clock,
|
|
CheckCircle2,
|
|
FolderKanban,
|
|
Play,
|
|
Square,
|
|
TrendingUp,
|
|
Plus,
|
|
Sparkles,
|
|
ArrowRight,
|
|
Flame,
|
|
RefreshCw,
|
|
Folder,
|
|
Tag,
|
|
Activity,
|
|
AlertCircle,
|
|
Zap,
|
|
Target,
|
|
Settings2,
|
|
BarChart3,
|
|
ExternalLink,
|
|
User,
|
|
Users,
|
|
FileDown,
|
|
} from 'lucide-react'
|
|
import { useAuth } from '../contexts/AuthContext'
|
|
import { useTimer } from '../contexts/TimerContext'
|
|
import { taskService } from '../services/taskService'
|
|
import { projectService } from '../services/projectService'
|
|
import { reportService, type ReportTemplateOutput } from '../services/reportService'
|
|
import type { Task, Project, ParsedAiTask } from '../types/task'
|
|
import { calculateTaskTotalSeconds, formatSecondsToHuman, formatSecondsToTimer } from '../lib/timeUtils'
|
|
import { CreateTaskModal } from '../components/tasks/CreateTaskModal'
|
|
import { CreateWithAiModal } from '../components/tasks/CreateWithAiModal'
|
|
import { GenerateReportModal } from '../components/reports/GenerateReportModal'
|
|
import { PeriodSelector } from '../components/dashboard/PeriodSelector'
|
|
import { CategoryTimePieChart } from '../components/dashboard/CategoryTimePieChart'
|
|
import {
|
|
type PeriodPreset,
|
|
getDateRangeForPreset,
|
|
calculateTaskSecondsInRange,
|
|
isTaskFinishedInRange,
|
|
hasTaskActivityInRange,
|
|
} from '../lib/periodUtils'
|
|
import {
|
|
getUserWorkGoal,
|
|
calculateUserPeriodWorkedSeconds,
|
|
type WorkGoal,
|
|
} from '../lib/userGoals'
|
|
|
|
export const DashboardPage: React.FC = () => {
|
|
const { user, activeOrg } = useAuth()
|
|
const { activeTask, activeSeconds, startTimer, stopTimer, syncWithTasks } = useTimer()
|
|
const navigate = useNavigate()
|
|
|
|
// Check if active task belongs to current user (as assignee or creator)
|
|
const isMyActiveTask = Boolean(
|
|
activeTask &&
|
|
user?.email &&
|
|
((activeTask.user?.email && activeTask.user.email.toLowerCase() === user.email.toLowerCase()) ||
|
|
(activeTask.createdBy?.email && activeTask.createdBy.email.toLowerCase() === user.email.toLowerCase()))
|
|
)
|
|
const currentActiveTask = isMyActiveTask ? activeTask : null
|
|
|
|
const [tasks, setTasks] = useState<Task[]>([])
|
|
const [projects, setProjects] = useState<Project[]>([])
|
|
const [reportTemplates, setReportTemplates] = useState<ReportTemplateOutput[]>([])
|
|
const [isReportModalOpen, setIsReportModalOpen] = useState(false)
|
|
const [isAiModalOpen, setIsAiModalOpen] = useState(false)
|
|
const [aiDraftTask, setAiDraftTask] = useState<ParsedAiTask | null>(null)
|
|
const [loading, setLoading] = useState(true)
|
|
const [refreshing, setRefreshing] = useState(false)
|
|
const [error, setError] = useState<string | null>(null)
|
|
|
|
// Work Goal State (Global)
|
|
const [workGoal, setWorkGoal] = useState<WorkGoal>(() => getUserWorkGoal(user?.email))
|
|
|
|
// Listen to goal changes in localStorage or custom event
|
|
useEffect(() => {
|
|
if (user?.email) {
|
|
setWorkGoal(getUserWorkGoal(user.email))
|
|
}
|
|
const handleGoalChange = (e: Event) => {
|
|
const custom = e as CustomEvent<WorkGoal>
|
|
if (custom.detail) {
|
|
setWorkGoal(custom.detail)
|
|
} else if (user?.email) {
|
|
setWorkGoal(getUserWorkGoal(user.email))
|
|
}
|
|
}
|
|
window.addEventListener('beetask_work_goal_changed', handleGoalChange)
|
|
return () => {
|
|
window.removeEventListener('beetask_work_goal_changed', handleGoalChange)
|
|
}
|
|
}, [user?.email])
|
|
|
|
// Modal
|
|
const [isModalOpen, setIsModalOpen] = useState(false)
|
|
|
|
// Scope Toggle State: 'mine' (Individual) vs 'team' (Equipe)
|
|
const [searchParams, setSearchParams] = useSearchParams()
|
|
const initialScope =
|
|
(searchParams.get('scope') as 'mine' | 'team') ||
|
|
(localStorage.getItem('beetask_dashboard_scope') as 'mine' | 'team') ||
|
|
'mine'
|
|
const [viewScope, setViewScope] = useState<'mine' | 'team'>(initialScope)
|
|
|
|
const handleScopeChange = (newScope: 'mine' | 'team') => {
|
|
setViewScope(newScope)
|
|
try {
|
|
localStorage.setItem('beetask_dashboard_scope', newScope)
|
|
} catch (_) {}
|
|
setSearchParams(
|
|
(prev) => {
|
|
const next = new URLSearchParams(prev)
|
|
next.set('scope', newScope)
|
|
return next
|
|
},
|
|
{ replace: true }
|
|
)
|
|
}
|
|
|
|
// Period Preset State (Hoje, Esta Semana, Este Mês, Trimestre, Ano, etc.)
|
|
const [periodPreset, setPeriodPreset] = useState<PeriodPreset>(() => {
|
|
return (localStorage.getItem('beetask_dashboard_period') as PeriodPreset) || 'this_month'
|
|
})
|
|
const [customStart, setCustomStart] = useState<string | undefined>(() => {
|
|
return localStorage.getItem('beetask_dashboard_custom_start') || undefined
|
|
})
|
|
const [customEnd, setCustomEnd] = useState<string | undefined>(() => {
|
|
return localStorage.getItem('beetask_dashboard_custom_end') || undefined
|
|
})
|
|
|
|
const handlePeriodChange = (preset: PeriodPreset, start?: string, end?: string) => {
|
|
setPeriodPreset(preset)
|
|
setCustomStart(start)
|
|
setCustomEnd(end)
|
|
try {
|
|
localStorage.setItem('beetask_dashboard_period', preset)
|
|
if (start) localStorage.setItem('beetask_dashboard_custom_start', start)
|
|
if (end) localStorage.setItem('beetask_dashboard_custom_end', end)
|
|
} catch (_) {}
|
|
}
|
|
|
|
const activeDateRange = useMemo(() => {
|
|
return getDateRangeForPreset(periodPreset, customStart, customEnd)
|
|
}, [periodPreset, customStart, customEnd])
|
|
|
|
const activeOrgId = activeOrg?.id
|
|
|
|
const loadData = useCallback(async () => {
|
|
if (!activeOrgId) return
|
|
try {
|
|
setRefreshing(true)
|
|
const [tasksRes, projsRes, tplsRes] = await Promise.allSettled([
|
|
taskService.listTasks({ organizationId: activeOrgId }),
|
|
projectService.listProjects(activeOrgId),
|
|
reportService.listTemplates(activeOrgId),
|
|
])
|
|
|
|
const loadedTasks = tasksRes.status === 'fulfilled' ? tasksRes.value || [] : []
|
|
setTasks(loadedTasks)
|
|
syncWithTasks(loadedTasks)
|
|
|
|
if (projsRes.status === 'fulfilled') {
|
|
setProjects(projsRes.value || [])
|
|
}
|
|
|
|
if (tplsRes.status === 'fulfilled') {
|
|
setReportTemplates(tplsRes.value || [])
|
|
}
|
|
} catch (err: unknown) {
|
|
console.error(err)
|
|
setError('Falha ao atualizar dados do dashboard.')
|
|
} finally {
|
|
setLoading(false)
|
|
setRefreshing(false)
|
|
}
|
|
}, [activeOrgId, syncWithTasks])
|
|
|
|
useEffect(() => {
|
|
loadData()
|
|
}, [loadData])
|
|
|
|
// Filter user's own tasks
|
|
const myTasks = useMemo(() => {
|
|
return tasks.filter((t) => {
|
|
if (!user?.email) return false
|
|
return Boolean(
|
|
t.user?.email &&
|
|
user?.email &&
|
|
t.user.email.toLowerCase() === user.email.toLowerCase()
|
|
)
|
|
})
|
|
}, [tasks, user])
|
|
|
|
// Scope tasks based on viewScope
|
|
const scopeTasks = viewScope === 'mine' ? myTasks : tasks
|
|
|
|
// KPIs dynamically calculated based on active scope AND active period
|
|
const stats = useMemo(() => {
|
|
const isAll = periodPreset === 'all'
|
|
const start = activeDateRange.startDate
|
|
const end = activeDateRange.endDate
|
|
|
|
let inProgress = 0
|
|
let completed = 0
|
|
let notStarted = 0
|
|
let totalSeconds = 0
|
|
const activeProjectIds = new Set<number>()
|
|
|
|
for (const t of scopeTasks) {
|
|
// Calculate period-accurate seconds
|
|
const sec = isAll
|
|
? calculateTaskTotalSeconds(t)
|
|
: calculateTaskSecondsInRange(t, start, end, currentActiveTask?.id, activeSeconds)
|
|
totalSeconds += sec
|
|
|
|
const hadActivity = isAll || sec > 0 || hasTaskActivityInRange(t, start, end)
|
|
|
|
// Completed in period
|
|
if (isAll) {
|
|
if (t.status === 'COMPLETED') completed++
|
|
else if (t.status === 'IN_PROGRESS') inProgress++
|
|
else if (t.status === 'NOT_STARTED') notStarted++
|
|
} else {
|
|
if (isTaskFinishedInRange(t, start, end)) {
|
|
completed++
|
|
} else if (t.status === 'IN_PROGRESS' || sec > 0) {
|
|
inProgress++
|
|
} else if (t.status === 'NOT_STARTED') {
|
|
notStarted++
|
|
}
|
|
}
|
|
|
|
if (hadActivity && t.project?.id) {
|
|
activeProjectIds.add(t.project.id)
|
|
}
|
|
}
|
|
|
|
const projectsCount = isAll
|
|
? (viewScope === 'mine' ? new Set(scopeTasks.map((t) => t.project?.id).filter(Boolean)).size : projects.length)
|
|
: activeProjectIds.size
|
|
|
|
return {
|
|
inProgress,
|
|
completed,
|
|
notStarted,
|
|
totalSeconds,
|
|
projectsCount,
|
|
periodLabel: activeDateRange.label,
|
|
}
|
|
}, [
|
|
scopeTasks,
|
|
projects,
|
|
viewScope,
|
|
periodPreset,
|
|
activeDateRange,
|
|
currentActiveTask?.id,
|
|
activeSeconds,
|
|
])
|
|
|
|
// Work Goal Progress in Current Period
|
|
const goalProgress = useMemo(() => {
|
|
const targetSeconds = (workGoal?.targetHours || 8) * 3600
|
|
const workedSeconds = calculateUserPeriodWorkedSeconds(
|
|
tasks,
|
|
workGoal?.period || 'DAILY',
|
|
currentActiveTask?.id,
|
|
activeSeconds
|
|
)
|
|
const percent = Math.min(100, Math.round((workedSeconds / targetSeconds) * 100))
|
|
const remainingSeconds = Math.max(0, targetSeconds - workedSeconds)
|
|
|
|
const periodLabels: Record<string, string> = {
|
|
DAILY: 'Meta Diária',
|
|
WEEKLY: 'Meta Semanal',
|
|
MONTHLY: 'Meta Mensal',
|
|
}
|
|
|
|
const periodSubtitles: Record<string, string> = {
|
|
DAILY: 'Progresso de hoje',
|
|
WEEKLY: 'Progresso desta semana',
|
|
MONTHLY: 'Progresso deste mês',
|
|
}
|
|
|
|
return {
|
|
title: periodLabels[workGoal?.period || 'DAILY'],
|
|
subtitle: periodSubtitles[workGoal?.period || 'DAILY'],
|
|
targetSeconds,
|
|
workedSeconds,
|
|
percent,
|
|
remainingSeconds,
|
|
targetHours: workGoal?.targetHours || 8,
|
|
isCompleted: workedSeconds >= targetSeconds,
|
|
}
|
|
}, [tasks, workGoal, currentActiveTask?.id, activeSeconds])
|
|
|
|
// Recent tasks based on active scope & period activity
|
|
const recentTasks = useMemo(() => {
|
|
if (periodPreset === 'all') {
|
|
return [...scopeTasks].slice(0, 5)
|
|
}
|
|
const withActivity = scopeTasks.filter((t) =>
|
|
hasTaskActivityInRange(t, activeDateRange.startDate, activeDateRange.endDate)
|
|
)
|
|
if (withActivity.length > 0) {
|
|
return withActivity.slice(0, 5)
|
|
}
|
|
return [...scopeTasks].slice(0, 5)
|
|
}, [scopeTasks, periodPreset, activeDateRange])
|
|
|
|
// Toggle timer on a task
|
|
const handleToggleTimer = async (task: Task) => {
|
|
try {
|
|
if (currentActiveTask?.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 handleModalSubmit = async (taskData: {
|
|
title: string
|
|
description: string
|
|
status: string
|
|
statusId?: number
|
|
projectId?: number
|
|
categoryId?: number
|
|
assigneeUserId?: number
|
|
history?: import('../types/task').TaskHistoryItemInput[]
|
|
externalUrl?: string
|
|
}) => {
|
|
if (!activeOrgId) return
|
|
const created = await taskService.createTask({
|
|
title: taskData.title,
|
|
description: taskData.description,
|
|
status: taskData.status,
|
|
statusId: taskData.statusId,
|
|
projectId: taskData.projectId,
|
|
categoryId: taskData.categoryId,
|
|
assigneeUserId: taskData.assigneeUserId,
|
|
organizationId: activeOrgId,
|
|
externalUrl: taskData.externalUrl,
|
|
})
|
|
setTasks((prev) => [created, ...prev])
|
|
}
|
|
|
|
// Priority task to focus on if none running (only user's own tasks)
|
|
const suggestedTask = useMemo(() => {
|
|
const myTasks = tasks.filter((t) => {
|
|
if (!user?.email || !t.user?.email) return false
|
|
return t.user.email.toLowerCase() === user.email.toLowerCase()
|
|
})
|
|
return (
|
|
myTasks.find((t) => t.status === 'IN_PROGRESS') ||
|
|
myTasks.find((t) => t.status === 'NOT_STARTED') ||
|
|
myTasks[0] ||
|
|
null
|
|
)
|
|
}, [tasks, user])
|
|
|
|
const firstName = user?.name ? user.name.split(' ')[0] : 'Desenvolvedor'
|
|
|
|
return (
|
|
<div className="space-y-8 max-w-7xl mx-auto">
|
|
{/* Top Banner with Landing Page aesthetic */}
|
|
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-5 pb-2">
|
|
<div className="space-y-1.5">
|
|
{/* Top pill badge with View Switcher */}
|
|
<div className="flex items-center gap-2 flex-wrap">
|
|
<div className="inline-flex items-center gap-2 px-3.5 py-1 rounded-full bg-surface/90 backdrop-blur-md border border-border-soft shadow-xs text-xs font-semibold text-brand">
|
|
<span className="flex h-2 w-2 rounded-full bg-accent animate-pulse" />
|
|
<span>Workspace: {activeOrg?.name || 'BeeTask'}</span>
|
|
<Sparkles className="w-3.5 h-3.5 text-accent" />
|
|
</div>
|
|
|
|
<Link
|
|
to="/dashboard-v2"
|
|
className="inline-flex items-center gap-1.5 px-3 py-1 rounded-full bg-brand-soft/60 hover:bg-brand-soft border border-brand/25 text-xs font-bold text-brand transition-all shadow-xs"
|
|
>
|
|
<BarChart3 className="w-3.5 h-3.5" />
|
|
<span>Experimentar BI v2</span>
|
|
</Link>
|
|
</div>
|
|
|
|
<h1 className="text-3xl sm:text-4xl font-extrabold tracking-tight text-foreground leading-tight">
|
|
Olá, {firstName}!{' '}
|
|
<span className="text-transparent bg-clip-text bg-gradient-to-r from-brand via-brand-strong to-accent">
|
|
Bom trabalho hoje.
|
|
</span>
|
|
</h1>
|
|
<p className="text-xs sm:text-sm text-muted max-w-2xl leading-relaxed">
|
|
Acompanhe o ritmo de entregas da sua equipe, audite suas horas e mantenha o foco na próxima entrega.
|
|
</p>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-3 flex-wrap">
|
|
<button
|
|
type="button"
|
|
onClick={loadData}
|
|
disabled={refreshing}
|
|
title="Recarregar dados"
|
|
className="p-2.5 rounded-xl border border-border-soft bg-surface/90 backdrop-blur-md text-muted hover:text-foreground hover:bg-surface-raised transition-all shadow-xs disabled:opacity-50"
|
|
>
|
|
<RefreshCw className={`w-4 h-4 ${refreshing ? 'animate-spin text-brand' : ''}`} />
|
|
</button>
|
|
|
|
<Link to="/tasks" className="inline-flex shrink-0">
|
|
<span
|
|
className="px-4 py-2.5 rounded-xl border border-border-soft bg-surface/90 backdrop-blur-md text-xs font-bold text-foreground hover:bg-surface-raised hover:border-brand/40 transition-all shadow-xs inline-flex items-center cursor-pointer"
|
|
>
|
|
Quadro Kanban
|
|
</span>
|
|
</Link>
|
|
|
|
<button
|
|
type="button"
|
|
onClick={() => setIsReportModalOpen(true)}
|
|
className="flex items-center gap-2 px-4 py-2.5 rounded-xl border border-brand/30 bg-brand/10 hover:bg-brand/20 text-brand text-xs font-bold transition-all shadow-xs cursor-pointer shrink-0"
|
|
>
|
|
<FileDown className="w-4 h-4" />
|
|
<span>Gerar Relatório</span>
|
|
</button>
|
|
|
|
<button
|
|
type="button"
|
|
onClick={() => setIsAiModalOpen(true)}
|
|
className="flex items-center gap-1.5 px-3.5 py-2.5 rounded-xl bg-purple-600/15 border border-purple-500/30 text-purple-300 font-bold text-xs hover:bg-purple-600/25 hover:text-purple-200 shadow-sm transition-all duration-200 hover:scale-[1.02] active:scale-[0.98] cursor-pointer shrink-0"
|
|
title="Criar tarefa em linguagem natural com inteligência artificial"
|
|
>
|
|
<Sparkles className="w-4 h-4 text-purple-400" />
|
|
<span>Criar com IA</span>
|
|
</button>
|
|
|
|
<button
|
|
type="button"
|
|
onClick={() => {
|
|
setAiDraftTask(null)
|
|
setIsModalOpen(true)
|
|
}}
|
|
className="flex items-center gap-2 px-5 py-2.5 rounded-xl bg-brand hover:bg-brand-strong text-white font-bold text-xs shadow-lg shadow-brand/25 transition-all duration-200 hover:scale-[1.02] active:scale-[0.98] shrink-0"
|
|
>
|
|
<Plus className="w-4 h-4" />
|
|
<span>Nova Tarefa</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{error && (
|
|
<div className="flex items-center justify-between p-4 rounded-2xl bg-rose-500/10 border border-rose-500/25 text-rose-300 text-xs backdrop-blur-xs">
|
|
<div className="flex items-center gap-2.5">
|
|
<AlertCircle className="w-4 h-4 text-rose-400" />
|
|
<span>{error}</span>
|
|
</div>
|
|
<button
|
|
type="button"
|
|
onClick={() => setError(null)}
|
|
className="text-rose-400 hover:text-rose-200 text-xs font-bold"
|
|
>
|
|
Fechar
|
|
</button>
|
|
</div>
|
|
)}
|
|
|
|
{/* Scope Selector: Individual (Minhas Tarefas) vs Equipe (Workspace) */}
|
|
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4 p-3 sm:p-4 rounded-3xl bg-surface/90 border border-border-soft backdrop-blur-md shadow-panel">
|
|
<div className="flex items-center gap-3">
|
|
<span className="text-xs font-bold text-muted uppercase tracking-wider hidden sm:inline">
|
|
Modo de Visualização:
|
|
</span>
|
|
<div className="inline-flex p-1 rounded-xl bg-surface-sunken border border-border-soft shadow-inner">
|
|
<button
|
|
type="button"
|
|
onClick={() => handleScopeChange('mine')}
|
|
className={`flex items-center gap-2 px-4 py-2 rounded-lg text-xs font-bold transition-all ${
|
|
viewScope === 'mine'
|
|
? 'bg-brand text-white shadow-md shadow-brand/25'
|
|
: 'text-muted hover:text-foreground'
|
|
}`}
|
|
>
|
|
<User className="w-3.5 h-3.5" />
|
|
<span>Individual (Minhas Tarefas)</span>
|
|
<span
|
|
className={`px-1.5 py-0.2 rounded-full text-[10px] font-bold ${
|
|
viewScope === 'mine' ? 'bg-white/20 text-white' : 'bg-surface text-muted'
|
|
}`}
|
|
>
|
|
{myTasks.length}
|
|
</span>
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() => handleScopeChange('team')}
|
|
className={`flex items-center gap-2 px-4 py-2 rounded-lg text-xs font-bold transition-all ${
|
|
viewScope === 'team'
|
|
? 'bg-brand text-white shadow-md shadow-brand/25'
|
|
: 'text-muted hover:text-foreground'
|
|
}`}
|
|
>
|
|
<Users className="w-3.5 h-3.5" />
|
|
<span>Equipe (Workspace Completo)</span>
|
|
<span
|
|
className={`px-1.5 py-0.2 rounded-full text-[10px] font-bold ${
|
|
viewScope === 'team' ? 'bg-white/20 text-white' : 'bg-surface text-muted'
|
|
}`}
|
|
>
|
|
{tasks.length}
|
|
</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-2 text-xs text-muted font-medium">
|
|
<span className="w-2 h-2 rounded-full bg-emerald-500 animate-pulse shrink-0" />
|
|
<span className="text-[11px] truncate">
|
|
{viewScope === 'mine'
|
|
? 'KPIs e métricas filtrados estritamente para demandas atribuídas a você'
|
|
: 'KPIs e métricas consolidados de todos os integrantes do workspace'}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Global Period Selector */}
|
|
<PeriodSelector
|
|
value={periodPreset}
|
|
customStart={customStart}
|
|
customEnd={customEnd}
|
|
onChange={handlePeriodChange}
|
|
/>
|
|
|
|
{/* 4 Premium KPI Cards with shadow-panel & backdrop-blur */}
|
|
{loading ? (
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-5">
|
|
{[1, 2, 3, 4].map((i) => (
|
|
<div
|
|
key={i}
|
|
className="h-32 rounded-3xl border border-border-soft bg-surface/60 backdrop-blur-md animate-pulse p-5"
|
|
/>
|
|
))}
|
|
</div>
|
|
) : (
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-5">
|
|
{/* In Progress */}
|
|
<div className="rounded-3xl border border-border-soft bg-surface/90 backdrop-blur-md p-5 shadow-panel hover:shadow-xl hover:-translate-y-0.5 transition-all duration-200 flex flex-col justify-between">
|
|
<div className="flex items-center justify-between mb-3">
|
|
<div className="flex items-center gap-1.5">
|
|
<span className="text-xs font-bold text-muted">
|
|
{viewScope === 'mine' ? 'Suas Tarefas em Andamento' : 'Tarefas da Equipe em Andamento'}
|
|
</span>
|
|
</div>
|
|
<div className="w-9 h-9 rounded-xl bg-teal-500/15 border border-teal-500/30 flex items-center justify-center text-teal-700 dark:text-teal-400 shadow-xs">
|
|
<Clock className="w-4 h-4" />
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<div className="flex items-baseline justify-between">
|
|
<p className="text-3xl font-extrabold text-teal-700 dark:text-teal-400 tracking-tight font-mono">
|
|
{stats.inProgress}
|
|
</p>
|
|
<span
|
|
className="text-muted/70 hover:text-foreground transition-colors shrink-0"
|
|
title={viewScope === 'mine' ? 'Métrica Individual' : 'Métrica da Equipe'}
|
|
>
|
|
{viewScope === 'mine' ? (
|
|
<User className="w-4 h-4 text-brand" />
|
|
) : (
|
|
<Users className="w-4 h-4 text-muted" />
|
|
)}
|
|
</span>
|
|
</div>
|
|
<p className="text-[11px] text-muted font-medium mt-1">
|
|
{periodPreset === 'all'
|
|
? (viewScope === 'mine'
|
|
? `${stats.notStarted} na sua fila aguardando`
|
|
: `${stats.notStarted} tarefas aguardando no backlog`)
|
|
: `Ativas no período (${stats.periodLabel})`}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Completed */}
|
|
<div className="rounded-3xl border border-border-soft bg-surface/90 backdrop-blur-md p-5 shadow-panel hover:shadow-xl hover:-translate-y-0.5 transition-all duration-200 flex flex-col justify-between">
|
|
<div className="flex items-center justify-between mb-3">
|
|
<span className="text-xs font-bold text-muted">
|
|
{viewScope === 'mine' ? 'Suas Entregas Concluídas' : 'Entregas Concluídas da Equipe'}
|
|
</span>
|
|
<div className="w-9 h-9 rounded-xl bg-emerald-500/15 border border-emerald-500/30 flex items-center justify-center text-emerald-700 dark:text-emerald-400 shadow-xs">
|
|
<CheckCircle2 className="w-4 h-4" />
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<div className="flex items-baseline justify-between">
|
|
<p className="text-3xl font-extrabold text-emerald-700 dark:text-emerald-400 tracking-tight font-mono">
|
|
{stats.completed}
|
|
</p>
|
|
<span
|
|
className="text-muted/70 hover:text-foreground transition-colors shrink-0"
|
|
title={viewScope === 'mine' ? 'Métrica Individual' : 'Métrica da Equipe'}
|
|
>
|
|
{viewScope === 'mine' ? (
|
|
<User className="w-4 h-4 text-brand" />
|
|
) : (
|
|
<Users className="w-4 h-4 text-muted" />
|
|
)}
|
|
</span>
|
|
</div>
|
|
<p className="text-[11px] text-muted font-medium mt-1">
|
|
{periodPreset === 'all'
|
|
? (viewScope === 'mine'
|
|
? `de ${myTasks.length} demandas suas`
|
|
: `de ${tasks.length} demandas cadastradas`)
|
|
: `Entregas no período (${stats.periodLabel})`}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Time Tracked */}
|
|
<div className="rounded-3xl border border-border-soft bg-surface/90 backdrop-blur-md p-5 shadow-panel hover:shadow-xl hover:-translate-y-0.5 transition-all duration-200 flex flex-col justify-between">
|
|
<div className="flex items-center justify-between mb-3">
|
|
<span className="text-xs font-bold text-muted">
|
|
{viewScope === 'mine' ? 'Seu Tempo Dedicado' : 'Tempo Total da Equipe'}
|
|
</span>
|
|
<div className="w-9 h-9 rounded-xl bg-amber-500/15 border border-amber-500/30 flex items-center justify-center text-amber-400 shadow-xs">
|
|
<TrendingUp className="w-4 h-4" />
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<div className="flex items-baseline justify-between">
|
|
<p className="text-3xl font-extrabold text-amber-400 tracking-tight font-mono">
|
|
{formatSecondsToTimer(stats.totalSeconds)}
|
|
</p>
|
|
<span
|
|
className="text-muted/70 hover:text-foreground transition-colors shrink-0"
|
|
title={viewScope === 'mine' ? 'Métrica Individual' : 'Métrica da Equipe'}
|
|
>
|
|
{viewScope === 'mine' ? (
|
|
<User className="w-4 h-4 text-brand" />
|
|
) : (
|
|
<Users className="w-4 h-4 text-muted" />
|
|
)}
|
|
</span>
|
|
</div>
|
|
<p className="text-[11px] text-muted font-medium mt-1">
|
|
{formatSecondsToHuman(stats.totalSeconds)} em {stats.periodLabel}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Projects */}
|
|
<div className="rounded-3xl border border-border-soft bg-surface/90 backdrop-blur-md p-5 shadow-panel hover:shadow-xl hover:-translate-y-0.5 transition-all duration-200 flex flex-col justify-between">
|
|
<div className="flex items-center justify-between mb-3">
|
|
<span className="text-xs font-bold text-muted">
|
|
{viewScope === 'mine' ? 'Seus Projetos Ativos' : 'Projetos da Organização'}
|
|
</span>
|
|
<div className="w-9 h-9 rounded-xl bg-brand-soft text-brand border border-brand/25 flex items-center justify-center shadow-xs">
|
|
<FolderKanban className="w-4 h-4 text-brand" />
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<div className="flex items-baseline justify-between">
|
|
<p className="text-3xl font-extrabold text-foreground tracking-tight font-mono">
|
|
{stats.projectsCount}
|
|
</p>
|
|
<span
|
|
className="text-muted/70 hover:text-foreground transition-colors shrink-0"
|
|
title={viewScope === 'mine' ? 'Métrica Individual' : 'Métrica da Equipe'}
|
|
>
|
|
{viewScope === 'mine' ? (
|
|
<User className="w-4 h-4 text-brand" />
|
|
) : (
|
|
<Users className="w-4 h-4 text-muted" />
|
|
)}
|
|
</span>
|
|
</div>
|
|
<p className="text-[11px] text-muted font-medium mt-1">
|
|
{periodPreset === 'all'
|
|
? (viewScope === 'mine'
|
|
? 'Iniciativas com tarefas suas'
|
|
: 'Iniciativas ativas no workspace')
|
|
: `Com atuação no período (${stats.periodLabel})`}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Main Grid: Focus Session (5 cols) + Recent Sprint Tasks (7 cols) */}
|
|
<div className="grid grid-cols-1 lg:grid-cols-12 gap-8 items-start">
|
|
{/* Focus Session Hero Card (Left 5 cols) */}
|
|
<div className="lg:col-span-5 space-y-6">
|
|
<div className="rounded-3xl border border-border-soft bg-surface/90 backdrop-blur-md p-6 shadow-panel space-y-5 transition-all hover:shadow-2xl">
|
|
{/* Header with macOS style dots */}
|
|
<div className="flex items-center justify-between pb-4 border-b border-border-soft">
|
|
<div className="flex items-center gap-2">
|
|
<span className="w-2.5 h-2.5 rounded-full bg-rose-400/80" />
|
|
<span className="w-2.5 h-2.5 rounded-full bg-amber-400/80" />
|
|
<span className="w-2.5 h-2.5 rounded-full bg-emerald-400/80" />
|
|
<span className="text-xs font-bold text-foreground ml-1.5 flex items-center gap-1.5">
|
|
<Flame className="w-3.5 h-3.5 text-accent" />
|
|
Sessão em Foco
|
|
</span>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-1.5">
|
|
<span
|
|
className={`inline-block w-2 h-2 rounded-full ${
|
|
currentActiveTask ? 'bg-emerald-500 animate-ping' : 'bg-zinc-400'
|
|
}`}
|
|
/>
|
|
<span
|
|
className={`text-xs font-bold ${
|
|
currentActiveTask ? 'text-teal-700 dark:text-teal-400' : 'text-muted'
|
|
}`}
|
|
>
|
|
{currentActiveTask ? 'CRONOMETRANDO' : 'EM ESPERA'}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Active or Suggested Task Info Box */}
|
|
{currentActiveTask ? (
|
|
<div className="p-4 rounded-2xl bg-surface-raised border border-border-soft space-y-2 shadow-xs">
|
|
<div className="flex items-center gap-2 flex-wrap">
|
|
{currentActiveTask.project && (
|
|
<span
|
|
className="px-2 py-0.5 rounded-md text-[10px] font-semibold border"
|
|
style={{
|
|
backgroundColor: currentActiveTask.project.color ? `${currentActiveTask.project.color}15` : 'rgba(15,118,110,0.1)',
|
|
color: currentActiveTask.project.color || '#0f766e',
|
|
borderColor: currentActiveTask.project.color ? `${currentActiveTask.project.color}35` : 'rgba(15,118,110,0.25)',
|
|
}}
|
|
>
|
|
{currentActiveTask.project.name}
|
|
</span>
|
|
)}
|
|
{currentActiveTask.category && (
|
|
<span className="px-2 py-0.5 rounded-md text-[10px] font-semibold bg-accent-soft text-accent border border-accent/25">
|
|
{currentActiveTask.category.name}
|
|
</span>
|
|
)}
|
|
</div>
|
|
<h3 className="text-sm font-bold text-foreground line-clamp-2 leading-snug">
|
|
{currentActiveTask.title}
|
|
</h3>
|
|
</div>
|
|
) : suggestedTask ? (
|
|
<div className="p-4 rounded-2xl bg-surface-muted/60 border border-border-soft space-y-2">
|
|
<div className="flex items-center justify-between">
|
|
<span className="text-[10px] font-bold text-muted uppercase tracking-wider">
|
|
Sugerida para Focar
|
|
</span>
|
|
{suggestedTask.project && (
|
|
<span className="text-[11px] text-muted font-medium truncate max-w-[140px]">
|
|
{suggestedTask.project.name}
|
|
</span>
|
|
)}
|
|
</div>
|
|
<h3 className="text-sm font-bold text-foreground line-clamp-2 leading-snug">
|
|
{suggestedTask.title}
|
|
</h3>
|
|
</div>
|
|
) : (
|
|
<div className="p-6 rounded-2xl bg-surface-muted/40 border border-dashed border-border-soft text-center text-xs text-muted">
|
|
Nenhuma tarefa ativa. Crie uma demanda para iniciar sua sessão produtiva!
|
|
</div>
|
|
)}
|
|
|
|
{/* Big Bold Timer Display */}
|
|
<div className="text-center py-5 rounded-2xl bg-surface border border-border-soft shadow-xs">
|
|
<span className="text-[11px] font-bold uppercase tracking-wider text-muted">
|
|
{currentActiveTask ? 'Tempo Decorrido na Tarefa' : 'Pronto para Iniciar'}
|
|
</span>
|
|
<p className="text-4xl sm:text-5xl font-extrabold font-mono text-foreground mt-1.5 tracking-tight">
|
|
{currentActiveTask ? formatSecondsToTimer(activeSeconds) : '00:00:00'}
|
|
</p>
|
|
</div>
|
|
|
|
{/* Timer Actions */}
|
|
{currentActiveTask ? (
|
|
<button
|
|
type="button"
|
|
onClick={() => stopTimer(currentActiveTask)}
|
|
className="w-full py-3.5 rounded-xl bg-rose-500/15 border border-rose-500/30 text-rose-300 hover:bg-rose-500/25 text-xs font-bold transition-all flex items-center justify-center gap-2 shadow-xs cursor-pointer"
|
|
>
|
|
<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-3.5 rounded-xl bg-brand hover:bg-brand-strong text-white text-xs font-bold shadow-lg shadow-brand/25 transition-all flex items-center justify-center gap-2 duration-200 hover:scale-[1.01] active:scale-[0.99]"
|
|
>
|
|
<Play className="w-4 h-4 fill-white" />
|
|
<span>Iniciar Tempo nesta Tarefa</span>
|
|
</button>
|
|
) : (
|
|
<button
|
|
type="button"
|
|
onClick={() => setIsModalOpen(true)}
|
|
className="w-full py-3.5 rounded-xl bg-brand hover:bg-brand-strong text-white text-xs font-bold shadow-lg shadow-brand/25 transition-all flex items-center justify-center gap-2"
|
|
>
|
|
<Plus className="w-4 h-4" />
|
|
<span>Criar Tarefa para Focar</span>
|
|
</button>
|
|
)}
|
|
|
|
<div className="pt-2 border-t border-border-soft flex items-center justify-between text-[11px] text-muted">
|
|
<span>Usuário conectado</span>
|
|
<span className="font-bold text-foreground truncate max-w-[160px]">
|
|
{user?.name || user?.email}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Workload Goal Progress Card */}
|
|
<div className="rounded-3xl border border-border-soft bg-surface/90 backdrop-blur-md p-5 shadow-panel space-y-4 transition-all hover:shadow-xl">
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center gap-2.5">
|
|
<div className="w-8 h-8 rounded-xl bg-amber-500/10 text-amber-700 dark:text-amber-400 border border-amber-600/20 dark:border-amber-500/25 flex items-center justify-center shadow-xs">
|
|
<Target className="w-4 h-4" />
|
|
</div>
|
|
<div>
|
|
<h4 className="text-xs font-bold text-foreground flex items-center gap-1.5">
|
|
{goalProgress.title}
|
|
<span className="text-[10px] text-muted font-normal">
|
|
({goalProgress.subtitle})
|
|
</span>
|
|
</h4>
|
|
<p className="text-[11px] text-muted font-medium">
|
|
Planejado: {goalProgress.targetHours}h {workGoal.period === 'DAILY' ? '/ dia' : workGoal.period === 'WEEKLY' ? '/ semana' : '/ mês'}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<Link
|
|
to="/profile"
|
|
title="Ajustar jornada e meta no Perfil"
|
|
className="p-1.5 rounded-lg border border-border-soft bg-surface hover:bg-surface-sunken text-muted hover:text-foreground text-xs transition-colors flex items-center gap-1"
|
|
>
|
|
<Settings2 className="w-3.5 h-3.5" />
|
|
<span className="text-[10px] font-semibold hidden sm:inline">Ajustar</span>
|
|
</Link>
|
|
</div>
|
|
|
|
{/* Time progress values */}
|
|
<div className="flex items-baseline justify-between pt-1">
|
|
<div className="flex items-baseline gap-1.5">
|
|
<span className="text-2xl font-extrabold text-foreground font-mono tracking-tight">
|
|
{formatSecondsToTimer(goalProgress.workedSeconds)}
|
|
</span>
|
|
<span className="text-xs text-muted font-medium">
|
|
/ {goalProgress.targetHours}h
|
|
</span>
|
|
</div>
|
|
<span
|
|
className={`text-xs font-extrabold px-2.5 py-0.5 rounded-full border ${
|
|
goalProgress.isCompleted
|
|
? 'bg-emerald-500/10 text-emerald-700 dark:text-emerald-400 border-emerald-600/25'
|
|
: 'bg-brand/10 text-brand border-brand/20'
|
|
}`}
|
|
>
|
|
{goalProgress.percent}%
|
|
</span>
|
|
</div>
|
|
|
|
{/* Progress Bar */}
|
|
<div className="space-y-1.5">
|
|
<div className="w-full h-2.5 rounded-full bg-surface-sunken overflow-hidden p-0.5 border border-border-soft/60">
|
|
<div
|
|
className={`h-full rounded-full transition-all duration-500 ${
|
|
goalProgress.isCompleted
|
|
? 'bg-gradient-to-r from-emerald-500 to-teal-400'
|
|
: 'bg-gradient-to-r from-brand to-teal-400'
|
|
}`}
|
|
style={{ width: `${goalProgress.percent}%` }}
|
|
/>
|
|
</div>
|
|
|
|
<div className="flex items-center justify-between text-[10px] text-muted font-medium">
|
|
<span>
|
|
{goalProgress.isCompleted
|
|
? '🎉 Meta da jornada concluída!'
|
|
: `Faltam ${formatSecondsToHuman(goalProgress.remainingSeconds)}`}
|
|
</span>
|
|
<span>{goalProgress.percent}% da meta</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Quick Shortcuts Card */}
|
|
<div className="rounded-3xl border border-border-soft bg-surface/90 backdrop-blur-md p-5 shadow-panel space-y-3">
|
|
<span className="text-[11px] font-bold uppercase tracking-wider text-muted block">
|
|
Acesso Rápido ao Workspace
|
|
</span>
|
|
<div className="grid grid-cols-2 gap-2 text-xs font-semibold">
|
|
<Link
|
|
to="/projects"
|
|
className="p-3.5 rounded-xl bg-surface-muted/60 hover:bg-brand-soft/50 hover:text-brand border border-border-soft transition-all flex items-center gap-2.5 text-foreground shadow-xs"
|
|
>
|
|
<FolderKanban className="w-4 h-4 text-brand" />
|
|
<span>Ver Projetos</span>
|
|
</Link>
|
|
<Link
|
|
to="/analytics"
|
|
className="p-3.5 rounded-xl bg-surface-muted/60 hover:bg-accent-soft/50 hover:text-accent border border-border-soft transition-all flex items-center gap-2.5 text-foreground shadow-xs"
|
|
>
|
|
<TrendingUp className="w-4 h-4 text-accent" />
|
|
<span>Analytics da Org</span>
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Right 7 cols: Category Time Pie Chart + Recent Tasks */}
|
|
<div className="lg:col-span-7 space-y-6">
|
|
{/* Pie Chart: Time Worked by Category */}
|
|
<CategoryTimePieChart
|
|
tasks={scopeTasks}
|
|
startDate={activeDateRange.startDate}
|
|
endDate={activeDateRange.endDate}
|
|
isAllPeriod={periodPreset === 'all'}
|
|
activeTaskId={currentActiveTask?.id}
|
|
activeSeconds={activeSeconds}
|
|
viewScope={viewScope}
|
|
periodLabel={stats.periodLabel}
|
|
/>
|
|
|
|
{/* Recent Sprint Tasks */}
|
|
<div className="space-y-4">
|
|
<div className="flex items-center justify-between">
|
|
<h2 className="text-lg font-bold text-foreground flex items-center gap-2 flex-wrap">
|
|
<Zap className="w-4 h-4 text-accent" />
|
|
<span>{viewScope === 'mine' ? 'Minhas Demandas' : 'Demandas da Equipe'}</span>
|
|
<span className="text-xs font-semibold px-2 py-0.5 rounded-full bg-brand-soft text-brand border border-brand/20">
|
|
{stats.periodLabel}
|
|
</span>
|
|
</h2>
|
|
<Link
|
|
to="/tasks"
|
|
className="text-xs font-bold text-brand hover:underline flex items-center gap-1"
|
|
>
|
|
<span>Ver todas</span>
|
|
<ArrowRight className="w-3.5 h-3.5" />
|
|
</Link>
|
|
</div>
|
|
|
|
{loading ? (
|
|
<div className="space-y-3">
|
|
{[1, 2, 3].map((i) => (
|
|
<div
|
|
key={i}
|
|
className="h-20 rounded-2xl border border-border-soft bg-surface/60 backdrop-blur-md animate-pulse p-4"
|
|
/>
|
|
))}
|
|
</div>
|
|
) : recentTasks.length === 0 ? (
|
|
<div className="text-center py-20 rounded-3xl border border-dashed border-border-soft bg-surface/60 backdrop-blur-md">
|
|
<Activity className="w-8 h-8 text-muted mx-auto mb-2" />
|
|
<p className="text-xs text-muted mb-4 font-medium">
|
|
{viewScope === 'mine'
|
|
? 'Você não possui nenhuma demanda atribuída no momento.'
|
|
: 'Nenhuma tarefa encontrada neste workspace.'}
|
|
</p>
|
|
<button
|
|
type="button"
|
|
onClick={() => setIsModalOpen(true)}
|
|
className="inline-flex items-center gap-2 px-5 py-2.5 rounded-xl bg-brand text-white text-xs font-bold hover:bg-brand-strong shadow-md shadow-brand/20 transition-all"
|
|
>
|
|
<Plus className="w-4 h-4" />
|
|
<span>Criar Primeira Tarefa</span>
|
|
</button>
|
|
</div>
|
|
) : (
|
|
<div className="space-y-3">
|
|
{recentTasks.map((task) => {
|
|
const isTracking = currentActiveTask?.id === task.id
|
|
const taskSec = isTracking ? activeSeconds : calculateTaskTotalSeconds(task)
|
|
|
|
return (
|
|
<div
|
|
key={task.id}
|
|
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 shadow-panel ${
|
|
isTracking
|
|
? 'border-brand/60 bg-brand/5 ring-1 ring-brand/30 shadow-lg'
|
|
: 'border-border-soft bg-surface/90 backdrop-blur-md hover:border-brand/30 hover:shadow-xl'
|
|
}`}
|
|
>
|
|
<div className="space-y-1.5 flex-1 min-w-0">
|
|
<div className="flex items-center gap-2 flex-wrap">
|
|
{/* Assignee Indicator */}
|
|
{task.user?.name ? (
|
|
<span
|
|
className={`inline-flex items-center gap-1 px-2 py-0.5 rounded-md text-[10px] font-semibold border ${
|
|
user?.email &&
|
|
task.user.email &&
|
|
task.user.email.toLowerCase() === user.email.toLowerCase()
|
|
? 'bg-brand/10 text-brand border-brand/20'
|
|
: 'bg-surface-sunken text-muted border-border-soft'
|
|
}`}
|
|
title={`Responsável: ${task.user.name} (${task.user.email || ''})`}
|
|
>
|
|
<User className="w-3 h-3" />
|
|
<span className="truncate max-w-[110px]">
|
|
{user?.email &&
|
|
task.user.email &&
|
|
task.user.email.toLowerCase() === user.email.toLowerCase()
|
|
? 'Você'
|
|
: task.user.name.split(' ')[0]}
|
|
</span>
|
|
</span>
|
|
) : (
|
|
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded-md text-[10px] font-medium bg-surface-sunken text-muted/60 border border-border-soft">
|
|
<User className="w-3 h-3" />
|
|
<span>Sem responsável</span>
|
|
</span>
|
|
)}
|
|
|
|
{task.project && (
|
|
<span
|
|
className="inline-flex items-center gap-1 px-2 py-0.5 rounded-md text-[10px] font-semibold border"
|
|
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-3 h-3" />
|
|
{task.project.name}
|
|
</span>
|
|
)}
|
|
{task.category && (
|
|
<span
|
|
className="inline-flex items-center gap-1 px-2 py-0.5 rounded-md text-[10px] font-semibold 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-3 h-3" />
|
|
{task.category.name}
|
|
</span>
|
|
)}
|
|
<span
|
|
className={`text-[10px] font-bold px-2 py-0.5 rounded-md border ${
|
|
task.status === 'COMPLETED'
|
|
? 'bg-emerald-500/10 text-emerald-700 dark:bg-emerald-500/15 dark:text-emerald-400 border-emerald-600/20 dark:border-emerald-500/25'
|
|
: task.status === 'IN_PROGRESS'
|
|
? 'bg-teal-500/10 text-teal-700 dark:bg-teal-500/15 dark:text-teal-400 border-teal-600/20 dark:border-teal-500/25'
|
|
: 'bg-surface-muted text-muted border-border-soft'
|
|
}`}
|
|
>
|
|
{task.status === 'COMPLETED'
|
|
? 'Concluído'
|
|
: task.status === 'IN_PROGRESS'
|
|
? 'Em Andamento'
|
|
: task.status === 'CANCELED'
|
|
? 'Cancelado'
|
|
: 'A Fazer'}
|
|
</span>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-1.5 min-w-0">
|
|
<h3
|
|
onClick={() => navigate('/tasks')}
|
|
className="text-sm font-bold text-foreground hover:text-brand cursor-pointer truncate transition-colors"
|
|
>
|
|
{task.title}
|
|
</h3>
|
|
{task.externalUrl && (
|
|
<a
|
|
href={task.externalUrl.startsWith('http') ? task.externalUrl : `https://${task.externalUrl}`}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
onClick={(e) => e.stopPropagation()}
|
|
title={`Link externo: ${task.externalUrl}`}
|
|
className="inline-flex items-center text-muted hover:text-brand transition-colors shrink-0 cursor-pointer"
|
|
>
|
|
<ExternalLink className="w-3.5 h-3.5" />
|
|
</a>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-3 shrink-0 self-end sm:self-center">
|
|
<div
|
|
className={`inline-flex items-center gap-1.5 px-3 py-1 rounded-lg font-mono text-[11px] font-bold ${
|
|
isTracking
|
|
? 'bg-teal-500/15 text-teal-700 dark:bg-teal-500/20 dark:text-teal-300'
|
|
: 'bg-surface-muted text-muted'
|
|
}`}
|
|
>
|
|
<Clock className={`w-3.5 h-3.5 ${isTracking ? 'animate-pulse text-teal-700 dark:text-brand' : ''}`} />
|
|
<span>{formatSecondsToTimer(taskSec)}</span>
|
|
</div>
|
|
|
|
<button
|
|
type="button"
|
|
onClick={() => handleToggleTimer(task)}
|
|
title={isTracking ? 'Pausar cronômetro' : 'Iniciar cronômetro'}
|
|
className={`p-2 rounded-xl transition-all shadow-xs ${
|
|
isTracking
|
|
? 'bg-rose-500/20 text-rose-300 hover:bg-rose-500/30 ring-1 ring-rose-500/40'
|
|
: 'bg-teal-500/10 text-teal-700 hover:bg-teal-500/20 dark:bg-brand/15 dark:text-teal-300 dark:hover:bg-brand/25 ring-1 ring-teal-600/20 dark:ring-brand/30'
|
|
}`}
|
|
>
|
|
{isTracking ? (
|
|
<Square className="w-3.5 h-3.5 fill-rose-300" />
|
|
) : (
|
|
<Play className="w-3.5 h-3.5 fill-teal-700 dark:fill-teal-300" />
|
|
)}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)
|
|
})}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Task Creation Modal */}
|
|
{activeOrgId && (
|
|
<>
|
|
<CreateTaskModal
|
|
isOpen={isModalOpen}
|
|
onClose={() => {
|
|
setIsModalOpen(false)
|
|
setAiDraftTask(null)
|
|
}}
|
|
onSubmit={handleModalSubmit}
|
|
organizationId={activeOrgId}
|
|
initialStatus="NOT_STARTED"
|
|
initialDraft={aiDraftTask}
|
|
/>
|
|
|
|
<CreateWithAiModal
|
|
isOpen={isAiModalOpen}
|
|
onClose={() => setIsAiModalOpen(false)}
|
|
organizationId={activeOrgId}
|
|
onSuccess={(parsed) => {
|
|
setAiDraftTask(parsed)
|
|
setIsModalOpen(true)
|
|
}}
|
|
/>
|
|
|
|
<GenerateReportModal
|
|
isOpen={isReportModalOpen}
|
|
onClose={() => setIsReportModalOpen(false)}
|
|
orgId={activeOrgId}
|
|
templates={reportTemplates}
|
|
projects={projects}
|
|
/>
|
|
</>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|