Adding components for the main dashboard
This commit is contained in:
parent
f7595fc900
commit
5a1489a432
131
app/globals.css
131
app/globals.css
|
|
@ -1,26 +1,121 @@
|
||||||
@import "tailwindcss";
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
:root {
|
|
||||||
--background: #ffffff;
|
|
||||||
--foreground: #171717;
|
|
||||||
}
|
|
||||||
|
|
||||||
@theme inline {
|
|
||||||
--color-background: var(--background);
|
|
||||||
--color-foreground: var(--foreground);
|
|
||||||
--font-sans: var(--font-geist-sans);
|
|
||||||
--font-mono: var(--font-geist-mono);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
@layer base {
|
||||||
:root {
|
:root {
|
||||||
--background: #0a0a0a;
|
|
||||||
--foreground: #ededed;
|
--background: 0 0% 100%;
|
||||||
|
|
||||||
|
--foreground: 0 0% 3.9%;
|
||||||
|
|
||||||
|
--card: 0 0% 100%;
|
||||||
|
|
||||||
|
--card-foreground: 0 0% 3.9%;
|
||||||
|
|
||||||
|
--popover: 0 0% 100%;
|
||||||
|
|
||||||
|
--popover-foreground: 0 0% 3.9%;
|
||||||
|
|
||||||
|
--primary: 0 0% 9%;
|
||||||
|
|
||||||
|
--primary-foreground: 0 0% 98%;
|
||||||
|
|
||||||
|
--secondary: 0 0% 96.1%;
|
||||||
|
|
||||||
|
--secondary-foreground: 0 0% 9%;
|
||||||
|
|
||||||
|
--muted: 0 0% 96.1%;
|
||||||
|
|
||||||
|
--muted-foreground: 0 0% 45.1%;
|
||||||
|
|
||||||
|
--accent: 0 0% 96.1%;
|
||||||
|
|
||||||
|
--accent-foreground: 0 0% 9%;
|
||||||
|
|
||||||
|
--destructive: 0 84.2% 60.2%;
|
||||||
|
|
||||||
|
--destructive-foreground: 0 0% 98%;
|
||||||
|
|
||||||
|
--border: 0 0% 89.8%;
|
||||||
|
|
||||||
|
--input: 0 0% 89.8%;
|
||||||
|
|
||||||
|
--ring: 0 0% 3.9%;
|
||||||
|
|
||||||
|
--chart-1: 12 76% 61%;
|
||||||
|
|
||||||
|
--chart-2: 173 58% 39%;
|
||||||
|
|
||||||
|
--chart-3: 197 37% 24%;
|
||||||
|
|
||||||
|
--chart-4: 43 74% 66%;
|
||||||
|
|
||||||
|
--chart-5: 27 87% 67%;
|
||||||
|
|
||||||
|
--radius: 0.5rem
|
||||||
|
}
|
||||||
|
.dark {
|
||||||
|
|
||||||
|
--background: 0 0% 3.9%;
|
||||||
|
|
||||||
|
--foreground: 0 0% 98%;
|
||||||
|
|
||||||
|
--card: 0 0% 3.9%;
|
||||||
|
|
||||||
|
--card-foreground: 0 0% 98%;
|
||||||
|
|
||||||
|
--popover: 0 0% 3.9%;
|
||||||
|
|
||||||
|
--popover-foreground: 0 0% 98%;
|
||||||
|
|
||||||
|
--primary: 0 0% 98%;
|
||||||
|
|
||||||
|
--primary-foreground: 0 0% 9%;
|
||||||
|
|
||||||
|
--secondary: 0 0% 14.9%;
|
||||||
|
|
||||||
|
--secondary-foreground: 0 0% 98%;
|
||||||
|
|
||||||
|
--muted: 0 0% 14.9%;
|
||||||
|
|
||||||
|
--muted-foreground: 0 0% 63.9%;
|
||||||
|
|
||||||
|
--accent: 0 0% 14.9%;
|
||||||
|
|
||||||
|
--accent-foreground: 0 0% 98%;
|
||||||
|
|
||||||
|
--destructive: 0 62.8% 30.6%;
|
||||||
|
|
||||||
|
--destructive-foreground: 0 0% 98%;
|
||||||
|
|
||||||
|
--border: 0 0% 14.9%;
|
||||||
|
|
||||||
|
--input: 0 0% 14.9%;
|
||||||
|
|
||||||
|
--ring: 0 0% 83.1%;
|
||||||
|
|
||||||
|
--chart-1: 220 70% 50%;
|
||||||
|
|
||||||
|
--chart-2: 160 60% 45%;
|
||||||
|
|
||||||
|
--chart-3: 30 80% 55%;
|
||||||
|
|
||||||
|
--chart-4: 280 65% 60%;
|
||||||
|
|
||||||
|
--chart-5: 340 75% 55%
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
|
||||||
background: var(--background);
|
|
||||||
color: var(--foreground);
|
@layer base {
|
||||||
font-family: Arial, Helvetica, sans-serif;
|
* {
|
||||||
|
@apply border-border;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
@apply bg-background text-foreground;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
64
app/page.tsx
64
app/page.tsx
|
|
@ -1,65 +1,5 @@
|
||||||
import Image from "next/image";
|
import ProjectDashboard from "@/components/dashboard/project-dashboard";
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
return (
|
return <ProjectDashboard />;
|
||||||
<div className="flex min-h-screen items-center justify-center bg-zinc-50 font-sans dark:bg-black">
|
|
||||||
<main className="flex min-h-screen w-full max-w-3xl flex-col items-center justify-between py-32 px-16 bg-white dark:bg-black sm:items-start">
|
|
||||||
<Image
|
|
||||||
className="dark:invert"
|
|
||||||
src="/next.svg"
|
|
||||||
alt="Next.js logo"
|
|
||||||
width={100}
|
|
||||||
height={20}
|
|
||||||
priority
|
|
||||||
/>
|
|
||||||
<div className="flex flex-col items-center gap-6 text-center sm:items-start sm:text-left">
|
|
||||||
<h1 className="max-w-xs text-3xl font-semibold leading-10 tracking-tight text-black dark:text-zinc-50">
|
|
||||||
To get started, edit the page.tsx file.
|
|
||||||
</h1>
|
|
||||||
<p className="max-w-md text-lg leading-8 text-zinc-600 dark:text-zinc-400">
|
|
||||||
Looking for a starting point or more instructions? Head over to{" "}
|
|
||||||
<a
|
|
||||||
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
|
||||||
className="font-medium text-zinc-950 dark:text-zinc-50"
|
|
||||||
>
|
|
||||||
Templates
|
|
||||||
</a>{" "}
|
|
||||||
or the{" "}
|
|
||||||
<a
|
|
||||||
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
|
||||||
className="font-medium text-zinc-950 dark:text-zinc-50"
|
|
||||||
>
|
|
||||||
Learning
|
|
||||||
</a>{" "}
|
|
||||||
center.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col gap-4 text-base font-medium sm:flex-row">
|
|
||||||
<a
|
|
||||||
className="flex h-12 w-full items-center justify-center gap-2 rounded-full bg-foreground px-5 text-background transition-colors hover:bg-[#383838] dark:hover:bg-[#ccc] md:w-[158px]"
|
|
||||||
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
<Image
|
|
||||||
className="dark:invert"
|
|
||||||
src="/vercel.svg"
|
|
||||||
alt="Vercel logomark"
|
|
||||||
width={16}
|
|
||||||
height={16}
|
|
||||||
/>
|
|
||||||
Deploy Now
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
className="flex h-12 w-full items-center justify-center rounded-full border border-solid border-black/[.08] px-5 transition-colors hover:border-transparent hover:bg-black/[.04] dark:border-white/[.145] dark:hover:bg-[#1a1a1a] md:w-[158px]"
|
|
||||||
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
Documentation
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://ui.shadcn.com/schema.json",
|
||||||
|
"style": "default",
|
||||||
|
"rsc": true,
|
||||||
|
"tsx": true,
|
||||||
|
"tailwind": {
|
||||||
|
"config": "tailwind.config.js",
|
||||||
|
"css": "app/globals.css",
|
||||||
|
"baseColor": "neutral",
|
||||||
|
"cssVariables": true,
|
||||||
|
"prefix": ""
|
||||||
|
},
|
||||||
|
"iconLibrary": "lucide",
|
||||||
|
"aliases": {
|
||||||
|
"components": "@/components",
|
||||||
|
"utils": "@/lib/utils",
|
||||||
|
"ui": "@/components/ui",
|
||||||
|
"lib": "@/lib",
|
||||||
|
"hooks": "@/hooks"
|
||||||
|
},
|
||||||
|
"registries": {}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,56 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { Search, LayoutGrid, List } from 'lucide-react';
|
||||||
|
|
||||||
|
interface DashboardHeaderProps {
|
||||||
|
searchTerm: string;
|
||||||
|
onSearchChange: (value: string) => void;
|
||||||
|
viewMode: 'grid' | 'list';
|
||||||
|
onViewModeChange: (mode: 'grid' | 'list') => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function DashboardHeader({
|
||||||
|
searchTerm,
|
||||||
|
onSearchChange,
|
||||||
|
viewMode,
|
||||||
|
onViewModeChange
|
||||||
|
}: DashboardHeaderProps) {
|
||||||
|
return (
|
||||||
|
<header className="bg-white border-b border-gray-200">
|
||||||
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h1 className="text-2xl font-semibold text-gray-900">Dashboard de Proyectos</h1>
|
||||||
|
<p className="text-sm text-gray-500 mt-1">Gestiona y visualiza todos tus proyectos</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="relative">
|
||||||
|
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4" />
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Buscar proyectos..."
|
||||||
|
value={searchTerm}
|
||||||
|
onChange={(e) => onSearchChange(e.target.value)}
|
||||||
|
className="pl-10 pr-4 py-2 border border-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent w-64 text-sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-1 border border-gray-200 rounded-lg p-1">
|
||||||
|
<button
|
||||||
|
onClick={() => onViewModeChange('grid')}
|
||||||
|
className={`p-2 rounded ${viewMode === 'grid' ? 'bg-gray-100' : 'hover:bg-gray-50'}`}
|
||||||
|
>
|
||||||
|
<LayoutGrid className="w-4 h-4 text-gray-600" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => onViewModeChange('list')}
|
||||||
|
className={`p-2 rounded ${viewMode === 'list' ? 'bg-gray-100' : 'hover:bg-gray-50'}`}
|
||||||
|
>
|
||||||
|
<List className="w-4 h-4 text-gray-600" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,81 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { Calendar, DollarSign, Users, TrendingUp } from 'lucide-react';
|
||||||
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { Avatar, AvatarFallback } from '@/components/ui/avatar';
|
||||||
|
import { Project, STATUS_CONFIG } from '@/types/project';
|
||||||
|
|
||||||
|
interface ProjectCardProps {
|
||||||
|
project: Project;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ProjectCard({ project }: ProjectCardProps) {
|
||||||
|
return (
|
||||||
|
<Card className="border-gray-200 hover:shadow-lg transition-all duration-300 hover:-translate-y-1">
|
||||||
|
<CardHeader>
|
||||||
|
<div className="flex items-start justify-between">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<Avatar className="w-12 h-12">
|
||||||
|
<AvatarFallback className="bg-gradient-to-br from-blue-500 to-purple-600 text-white font-semibold">
|
||||||
|
{project.avatar}
|
||||||
|
</AvatarFallback>
|
||||||
|
</Avatar>
|
||||||
|
<div>
|
||||||
|
<CardTitle className="text-lg text-gray-900">{project.name}</CardTitle>
|
||||||
|
<CardDescription className="text-sm">{project.client}</CardDescription>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Badge variant="outline" className={`mt-3 ${STATUS_CONFIG[project.status].color} border`}>
|
||||||
|
{STATUS_CONFIG[project.status].label}
|
||||||
|
</Badge>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
{/* Progress Bar */}
|
||||||
|
<div className="mb-4">
|
||||||
|
<div className="flex items-center justify-between mb-2">
|
||||||
|
<span className="text-xs font-medium text-gray-600">Progreso</span>
|
||||||
|
<span className="text-xs font-semibold text-gray-900">{project.progress}%</span>
|
||||||
|
</div>
|
||||||
|
<div className="w-full bg-gray-200 rounded-full h-2">
|
||||||
|
<div
|
||||||
|
className="bg-gradient-to-r from-blue-500 to-purple-600 h-2 rounded-full transition-all duration-500"
|
||||||
|
style={{ width: `${project.progress}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Budget Info */}
|
||||||
|
<div className="grid grid-cols-2 gap-4 mb-4">
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center gap-1 text-gray-500 mb-1">
|
||||||
|
<DollarSign className="w-3 h-3" />
|
||||||
|
<span className="text-xs">Presupuesto</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm font-semibold text-gray-900">€{(project.budget / 1000).toFixed(0)}k</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center gap-1 text-gray-500 mb-1">
|
||||||
|
<TrendingUp className="w-3 h-3" />
|
||||||
|
<span className="text-xs">Gastado</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm font-semibold text-gray-900">€{(project.spent / 1000).toFixed(0)}k</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Footer Info */}
|
||||||
|
<div className="pt-4 border-t border-gray-100 flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-1 text-gray-500">
|
||||||
|
<Users className="w-4 h-4" />
|
||||||
|
<span className="text-xs">{project.team} miembros</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1 text-gray-500">
|
||||||
|
<Calendar className="w-4 h-4" />
|
||||||
|
<span className="text-xs">{new Date(project.endDate).toLocaleDateString('es-ES')}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,120 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from 'react';
|
||||||
|
import DashboardHeader from './dashboard-header';
|
||||||
|
import StatsOverview from './stats-overview';
|
||||||
|
import ProjectGrid from './project-grid';
|
||||||
|
import { Project } from '@/types/project';
|
||||||
|
|
||||||
|
// Tipos
|
||||||
|
type ProjectStatus = 'en_progreso' | 'planificacion' | 'completado' | 'revision';
|
||||||
|
|
||||||
|
// Datos mock (luego esto vendrá de Dolibarr)
|
||||||
|
const mockProjects: Project[] = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
name: "Rediseño Web Corporativa",
|
||||||
|
client: "Tech Solutions SA",
|
||||||
|
status: "en_progreso",
|
||||||
|
progress: 65,
|
||||||
|
budget: 45000,
|
||||||
|
spent: 29250,
|
||||||
|
startDate: "2024-10-15",
|
||||||
|
endDate: "2024-12-30",
|
||||||
|
team: 4,
|
||||||
|
avatar: "TS"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
name: "App Móvil E-commerce",
|
||||||
|
client: "Fashion Store",
|
||||||
|
status: "en_progreso",
|
||||||
|
progress: 40,
|
||||||
|
budget: 80000,
|
||||||
|
spent: 32000,
|
||||||
|
startDate: "2024-11-01",
|
||||||
|
endDate: "2025-02-28",
|
||||||
|
team: 6,
|
||||||
|
avatar: "FS"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
name: "Sistema CRM Interno",
|
||||||
|
client: "Industrial Corp",
|
||||||
|
status: "planificacion",
|
||||||
|
progress: 10,
|
||||||
|
budget: 120000,
|
||||||
|
spent: 12000,
|
||||||
|
startDate: "2024-12-01",
|
||||||
|
endDate: "2025-05-30",
|
||||||
|
team: 8,
|
||||||
|
avatar: "IC"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
name: "Dashboard Analytics",
|
||||||
|
client: "Data Insights",
|
||||||
|
status: "completado",
|
||||||
|
progress: 100,
|
||||||
|
budget: 35000,
|
||||||
|
spent: 33500,
|
||||||
|
startDate: "2024-08-01",
|
||||||
|
endDate: "2024-11-15",
|
||||||
|
team: 3,
|
||||||
|
avatar: "DI"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 5,
|
||||||
|
name: "Migración Cloud AWS",
|
||||||
|
client: "StartUp Innovate",
|
||||||
|
status: "en_progreso",
|
||||||
|
progress: 75,
|
||||||
|
budget: 95000,
|
||||||
|
spent: 71250,
|
||||||
|
startDate: "2024-09-15",
|
||||||
|
endDate: "2024-12-20",
|
||||||
|
team: 5,
|
||||||
|
avatar: "SI"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 6,
|
||||||
|
name: "Portal de Clientes",
|
||||||
|
client: "Services Group",
|
||||||
|
status: "revision",
|
||||||
|
progress: 90,
|
||||||
|
budget: 52000,
|
||||||
|
spent: 48880,
|
||||||
|
startDate: "2024-10-01",
|
||||||
|
endDate: "2024-12-10",
|
||||||
|
team: 4,
|
||||||
|
avatar: "SG"
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function ProjectDashboard() {
|
||||||
|
const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid');
|
||||||
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
|
|
||||||
|
const filteredProjects = mockProjects.filter(project =>
|
||||||
|
project.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||||
|
project.client.toLowerCase().includes(searchTerm.toLowerCase())
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gradient-to-br from-gray-50 to-gray-100">
|
||||||
|
<DashboardHeader
|
||||||
|
searchTerm={searchTerm}
|
||||||
|
onSearchChange={setSearchTerm}
|
||||||
|
viewMode={viewMode}
|
||||||
|
onViewModeChange={setViewMode}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||||
|
<StatsOverview projects={mockProjects} />
|
||||||
|
<ProjectGrid projects={filteredProjects} viewMode={viewMode} />
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import ProjectCard from './project-card';
|
||||||
|
import { Project } from '@/types/project';
|
||||||
|
|
||||||
|
interface ProjectGridProps {
|
||||||
|
projects: Project[];
|
||||||
|
viewMode: 'grid' | 'list';
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ProjectGrid({ projects, viewMode }: ProjectGridProps) {
|
||||||
|
if (projects.length === 0) {
|
||||||
|
return (
|
||||||
|
<div className="text-center py-12">
|
||||||
|
<p className="text-gray-500">No se encontraron proyectos</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`grid gap-6 ${viewMode === 'grid' ? 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3' : 'grid-cols-1'}`}>
|
||||||
|
{projects.map((project) => (
|
||||||
|
<ProjectCard key={project.id} project={project} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,68 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { LayoutGrid, TrendingUp, DollarSign, Clock } from 'lucide-react';
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
|
import { Project } from '@/types/project'; // o la ruta donde pongas types.ts
|
||||||
|
|
||||||
|
interface StatsOverviewProps {
|
||||||
|
projects: Project[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function StatsOverview({ projects }: StatsOverviewProps) {
|
||||||
|
const stats = {
|
||||||
|
total: projects.length,
|
||||||
|
activos: projects.filter(p => p.status === 'en_progreso').length,
|
||||||
|
totalBudget: projects.reduce((acc, p) => acc + p.budget, 0),
|
||||||
|
avgProgress: Math.round(projects.reduce((acc, p) => acc + p.progress, 0) / projects.length)
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
|
||||||
|
<Card className="border-gray-200 hover:shadow-md transition-shadow">
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium text-gray-600">Total Proyectos</CardTitle>
|
||||||
|
<LayoutGrid className="w-4 h-4 text-gray-400" />
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-3xl font-bold text-gray-900">{stats.total}</div>
|
||||||
|
<p className="text-xs text-gray-500 mt-1">Todos los proyectos</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="border-gray-200 hover:shadow-md transition-shadow">
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium text-gray-600">Activos</CardTitle>
|
||||||
|
<TrendingUp className="w-4 h-4 text-blue-500" />
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-3xl font-bold text-gray-900">{stats.activos}</div>
|
||||||
|
<p className="text-xs text-gray-500 mt-1">En progreso ahora</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="border-gray-200 hover:shadow-md transition-shadow">
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium text-gray-600">Presupuesto Total</CardTitle>
|
||||||
|
<DollarSign className="w-4 h-4 text-green-500" />
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-3xl font-bold text-gray-900">
|
||||||
|
€{(stats.totalBudget / 1000).toFixed(0)}k
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-gray-500 mt-1">Suma de presupuestos</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="border-gray-200 hover:shadow-md transition-shadow">
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium text-gray-600">Progreso Medio</CardTitle>
|
||||||
|
<Clock className="w-4 h-4 text-purple-500" />
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-3xl font-bold text-gray-900">{stats.avgProgress}%</div>
|
||||||
|
<p className="text-xs text-gray-500 mt-1">De todos los proyectos</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,50 @@
|
||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import * as AvatarPrimitive from "@radix-ui/react-avatar"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const Avatar = React.forwardRef<
|
||||||
|
React.ElementRef<typeof AvatarPrimitive.Root>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<AvatarPrimitive.Root
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
Avatar.displayName = AvatarPrimitive.Root.displayName
|
||||||
|
|
||||||
|
const AvatarImage = React.forwardRef<
|
||||||
|
React.ElementRef<typeof AvatarPrimitive.Image>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<AvatarPrimitive.Image
|
||||||
|
ref={ref}
|
||||||
|
className={cn("aspect-square h-full w-full", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
AvatarImage.displayName = AvatarPrimitive.Image.displayName
|
||||||
|
|
||||||
|
const AvatarFallback = React.forwardRef<
|
||||||
|
React.ElementRef<typeof AvatarPrimitive.Fallback>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<AvatarPrimitive.Fallback
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"flex h-full w-full items-center justify-center rounded-full bg-muted",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
|
||||||
|
|
||||||
|
export { Avatar, AvatarImage, AvatarFallback }
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
import * as React from "react"
|
||||||
|
import { cva, type VariantProps } from "class-variance-authority"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const badgeVariants = cva(
|
||||||
|
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default:
|
||||||
|
"border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
|
||||||
|
secondary:
|
||||||
|
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||||
|
destructive:
|
||||||
|
"border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
|
||||||
|
outline: "text-foreground",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
export interface BadgeProps
|
||||||
|
extends React.HTMLAttributes<HTMLDivElement>,
|
||||||
|
VariantProps<typeof badgeVariants> {}
|
||||||
|
|
||||||
|
function Badge({ className, variant, ...props }: BadgeProps) {
|
||||||
|
return (
|
||||||
|
<div className={cn(badgeVariants({ variant }), className)} {...props} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Badge, badgeVariants }
|
||||||
|
|
@ -0,0 +1,56 @@
|
||||||
|
import * as React from "react"
|
||||||
|
import { Slot } from "@radix-ui/react-slot"
|
||||||
|
import { cva, type VariantProps } from "class-variance-authority"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const buttonVariants = cva(
|
||||||
|
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
||||||
|
destructive:
|
||||||
|
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
||||||
|
outline:
|
||||||
|
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
|
||||||
|
secondary:
|
||||||
|
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||||
|
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||||
|
link: "text-primary underline-offset-4 hover:underline",
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
default: "h-10 px-4 py-2",
|
||||||
|
sm: "h-9 rounded-md px-3",
|
||||||
|
lg: "h-11 rounded-md px-8",
|
||||||
|
icon: "h-10 w-10",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
size: "default",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
export interface ButtonProps
|
||||||
|
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||||
|
VariantProps<typeof buttonVariants> {
|
||||||
|
asChild?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||||
|
({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||||
|
const Comp = asChild ? Slot : "button"
|
||||||
|
return (
|
||||||
|
<Comp
|
||||||
|
className={cn(buttonVariants({ variant, size, className }))}
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
Button.displayName = "Button"
|
||||||
|
|
||||||
|
export { Button, buttonVariants }
|
||||||
|
|
@ -0,0 +1,79 @@
|
||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const Card = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
React.HTMLAttributes<HTMLDivElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"rounded-lg border bg-card text-card-foreground shadow-sm",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
Card.displayName = "Card"
|
||||||
|
|
||||||
|
const CardHeader = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
React.HTMLAttributes<HTMLDivElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className={cn("flex flex-col space-y-1.5 p-6", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
CardHeader.displayName = "CardHeader"
|
||||||
|
|
||||||
|
const CardTitle = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
React.HTMLAttributes<HTMLDivElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"text-2xl font-semibold leading-none tracking-tight",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
CardTitle.displayName = "CardTitle"
|
||||||
|
|
||||||
|
const CardDescription = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
React.HTMLAttributes<HTMLDivElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className={cn("text-sm text-muted-foreground", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
CardDescription.displayName = "CardDescription"
|
||||||
|
|
||||||
|
const CardContent = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
React.HTMLAttributes<HTMLDivElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
|
||||||
|
))
|
||||||
|
CardContent.displayName = "CardContent"
|
||||||
|
|
||||||
|
const CardFooter = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
React.HTMLAttributes<HTMLDivElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className={cn("flex items-center p-6 pt-0", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
CardFooter.displayName = "CardFooter"
|
||||||
|
|
||||||
|
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function Skeleton({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.HTMLAttributes<HTMLDivElement>) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn("animate-pulse rounded-md bg-muted", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Skeleton }
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
import { clsx, type ClassValue } from "clsx"
|
||||||
|
import { twMerge } from "tailwind-merge"
|
||||||
|
|
||||||
|
export function cn(...inputs: ClassValue[]) {
|
||||||
|
return twMerge(clsx(inputs))
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
13
package.json
13
package.json
|
|
@ -9,18 +9,27 @@
|
||||||
"lint": "eslint"
|
"lint": "eslint"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@radix-ui/react-avatar": "^1.1.11",
|
||||||
|
"@radix-ui/react-slot": "^1.2.4",
|
||||||
|
"class-variance-authority": "^0.7.1",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"lucide-react": "^0.556.0",
|
||||||
"next": "16.0.7",
|
"next": "16.0.7",
|
||||||
"react": "19.2.0",
|
"react": "19.2.0",
|
||||||
"react-dom": "19.2.0"
|
"react-dom": "19.2.0",
|
||||||
|
"tailwind-merge": "^3.4.0",
|
||||||
|
"tailwindcss-animate": "^1.0.7"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/postcss": "^4",
|
"@tailwindcss/postcss": "^4",
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
"@types/react": "^19",
|
"@types/react": "^19",
|
||||||
"@types/react-dom": "^19",
|
"@types/react-dom": "^19",
|
||||||
|
"autoprefixer": "^10.4.22",
|
||||||
"eslint": "^9",
|
"eslint": "^9",
|
||||||
"eslint-config-next": "16.0.7",
|
"eslint-config-next": "16.0.7",
|
||||||
"tailwindcss": "^4",
|
"postcss": "^8.5.6",
|
||||||
|
"tailwindcss": "^3.4.18",
|
||||||
"typescript": "^5"
|
"typescript": "^5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
module.exports = {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,60 @@
|
||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
module.exports = {
|
||||||
|
darkMode: ["class"],
|
||||||
|
content: [
|
||||||
|
"./app/**/*.{js,ts,jsx,tsx,mdx}",
|
||||||
|
"./components/**/*.{js,ts,jsx,tsx,mdx}",
|
||||||
|
],
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
borderRadius: {
|
||||||
|
lg: 'var(--radius)',
|
||||||
|
md: 'calc(var(--radius) - 2px)',
|
||||||
|
sm: 'calc(var(--radius) - 4px)'
|
||||||
|
},
|
||||||
|
colors: {
|
||||||
|
background: 'hsl(var(--background))',
|
||||||
|
foreground: 'hsl(var(--foreground))',
|
||||||
|
card: {
|
||||||
|
DEFAULT: 'hsl(var(--card))',
|
||||||
|
foreground: 'hsl(var(--card-foreground))'
|
||||||
|
},
|
||||||
|
popover: {
|
||||||
|
DEFAULT: 'hsl(var(--popover))',
|
||||||
|
foreground: 'hsl(var(--popover-foreground))'
|
||||||
|
},
|
||||||
|
primary: {
|
||||||
|
DEFAULT: 'hsl(var(--primary))',
|
||||||
|
foreground: 'hsl(var(--primary-foreground))'
|
||||||
|
},
|
||||||
|
secondary: {
|
||||||
|
DEFAULT: 'hsl(var(--secondary))',
|
||||||
|
foreground: 'hsl(var(--secondary-foreground))'
|
||||||
|
},
|
||||||
|
muted: {
|
||||||
|
DEFAULT: 'hsl(var(--muted))',
|
||||||
|
foreground: 'hsl(var(--muted-foreground))'
|
||||||
|
},
|
||||||
|
accent: {
|
||||||
|
DEFAULT: 'hsl(var(--accent))',
|
||||||
|
foreground: 'hsl(var(--accent-foreground))'
|
||||||
|
},
|
||||||
|
destructive: {
|
||||||
|
DEFAULT: 'hsl(var(--destructive))',
|
||||||
|
foreground: 'hsl(var(--destructive-foreground))'
|
||||||
|
},
|
||||||
|
border: 'hsl(var(--border))',
|
||||||
|
input: 'hsl(var(--input))',
|
||||||
|
ring: 'hsl(var(--ring))',
|
||||||
|
chart: {
|
||||||
|
'1': 'hsl(var(--chart-1))',
|
||||||
|
'2': 'hsl(var(--chart-2))',
|
||||||
|
'3': 'hsl(var(--chart-3))',
|
||||||
|
'4': 'hsl(var(--chart-4))',
|
||||||
|
'5': 'hsl(var(--chart-5))'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
plugins: [require("tailwindcss-animate")],
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
export type ProjectStatus = 'en_progreso' | 'planificacion' | 'completado' | 'revision';
|
||||||
|
|
||||||
|
export interface Project {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
client: string;
|
||||||
|
status: ProjectStatus;
|
||||||
|
progress: number;
|
||||||
|
budget: number;
|
||||||
|
spent: number;
|
||||||
|
startDate: string;
|
||||||
|
endDate: string;
|
||||||
|
team: number;
|
||||||
|
avatar: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const STATUS_CONFIG: Record<ProjectStatus, { label: string; color: string }> = {
|
||||||
|
en_progreso: { label: "En Progreso", color: "bg-blue-100 text-blue-800 border-blue-200" },
|
||||||
|
planificacion: { label: "Planificación", color: "bg-purple-100 text-purple-800 border-purple-200" },
|
||||||
|
completado: { label: "Completado", color: "bg-green-100 text-green-800 border-green-200" },
|
||||||
|
revision: { label: "En Revisión", color: "bg-amber-100 text-amber-800 border-amber-200" }
|
||||||
|
};
|
||||||
Loading…
Reference in New Issue