update for marcos
This commit is contained in:
parent
17eba11f0b
commit
09bdf14c0a
|
|
@ -0,0 +1,228 @@
|
||||||
|
# AGENTS.md
|
||||||
|
|
||||||
|
This file contains guidelines and commands for agentic coding agents working in this repository.
|
||||||
|
|
||||||
|
## Build/Lint/Test Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Development
|
||||||
|
npm run dev # Start development server (Next.js)
|
||||||
|
|
||||||
|
# Build & Production
|
||||||
|
npm run build # Build for production
|
||||||
|
npm run start # Start production server
|
||||||
|
|
||||||
|
# Code Quality
|
||||||
|
npm run lint # Run ESLint
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note**: This project does not have test commands configured. If tests are added, update the scripts in package.json.
|
||||||
|
|
||||||
|
## Project Architecture
|
||||||
|
|
||||||
|
This is a **Next.js 16** application with **TypeScript** that serves as a dashboard for Dolibarr project management. The app uses:
|
||||||
|
|
||||||
|
- **UI Framework**: shadcn/ui components with Radix UI primitives
|
||||||
|
- **Styling**: Tailwind CSS with custom design system
|
||||||
|
- **State Management**: React hooks and context
|
||||||
|
- **API Integration**: Custom Dolibarr client
|
||||||
|
- **Icons**: Lucide React
|
||||||
|
|
||||||
|
## Code Style Guidelines
|
||||||
|
|
||||||
|
### Imports & Dependencies
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 1. React imports first
|
||||||
|
import React from "react";
|
||||||
|
import { forwardRef } from "react";
|
||||||
|
|
||||||
|
// 2. Third-party libraries (alphabetical)
|
||||||
|
import { cva, type VariantProps } from "class-variance-authority";
|
||||||
|
import { Slot } from "@radix-ui/react-slot";
|
||||||
|
|
||||||
|
// 3. Internal imports (use @/ aliases)
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Project } from "@/types/project";
|
||||||
|
```
|
||||||
|
|
||||||
|
### Component Structure
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
"use client"; // Add for client components
|
||||||
|
|
||||||
|
// Imports
|
||||||
|
import { ComponentProps } from "react";
|
||||||
|
|
||||||
|
// Types/Interfaces
|
||||||
|
interface ComponentProps {
|
||||||
|
// props here
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper functions (if any)
|
||||||
|
function helper() {
|
||||||
|
// implementation
|
||||||
|
}
|
||||||
|
|
||||||
|
// Main component
|
||||||
|
export default function Component({ prop }: ComponentProps) {
|
||||||
|
// implementation
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### TypeScript Guidelines
|
||||||
|
|
||||||
|
- **Always use types** for props, function parameters, and return values
|
||||||
|
- **Prefer interfaces** for object shapes, types for unions/primitives
|
||||||
|
- **Use generic types** when appropriate: `React.FC<Props>`, `VariantProps<T>`
|
||||||
|
- **Strict mode enabled** - no implicit `any`
|
||||||
|
|
||||||
|
### Naming Conventions
|
||||||
|
|
||||||
|
- **Components**: PascalCase (`ProjectCard`, `DashboardHeader`)
|
||||||
|
- **Functions**: camelCase (`getInitials`, `mapDolibarrProject`)
|
||||||
|
- **Constants**: UPPER_SNAKE_CASE (`STATUS_CONFIG`, `API_BASE_URL`)
|
||||||
|
- **Files**: kebab-case (`project-card.tsx`, `dolibarr-client.ts`)
|
||||||
|
- **Types**: PascalCase with descriptive suffixes (`ProjectStatus`, `DolibarrProject`)
|
||||||
|
|
||||||
|
### shadcn/ui Component Patterns
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Use cva for variant styling
|
||||||
|
const buttonVariants = cva(
|
||||||
|
"base-classes",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default: "variant-classes",
|
||||||
|
// other variants
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Forward ref for composable components
|
||||||
|
const Component = forwardRef<HTMLDivElement, ComponentProps>(
|
||||||
|
({ className, ...props }, ref) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(variantClasses, className)}
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
Component.displayName = "Component";
|
||||||
|
```
|
||||||
|
|
||||||
|
### Styling Guidelines
|
||||||
|
|
||||||
|
- **Use Tailwind classes** for all styling
|
||||||
|
- **Utility-first approach** - avoid custom CSS when possible
|
||||||
|
- **Responsive design**: `sm:`, `md:`, `lg:`, `xl:` prefixes
|
||||||
|
- **State styling**: `hover:`, `focus:`, `disabled:` prefixes
|
||||||
|
- **Use cn() utility** for conditional class merging
|
||||||
|
- **Design tokens**: Use CSS custom properties from `globals.css`
|
||||||
|
|
||||||
|
### Error Handling
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// API calls - throw errors, handle at call site
|
||||||
|
export async function apiCall() {
|
||||||
|
const res = await fetch(url);
|
||||||
|
if (!res.ok) {
|
||||||
|
console.error("API Error:", res.status, await res.text());
|
||||||
|
throw new Error("API request failed");
|
||||||
|
}
|
||||||
|
return res.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Components - handle errors gracefully
|
||||||
|
try {
|
||||||
|
const data = await apiCall();
|
||||||
|
// render data
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to load data:", error);
|
||||||
|
// render error state or fallback
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### File Organization
|
||||||
|
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
├── app/ # Next.js app router
|
||||||
|
├── components/ # React components
|
||||||
|
│ ├── ui/ # shadcn/ui components
|
||||||
|
│ └── dashboard/ # Feature components
|
||||||
|
├── hooks/ # Custom React hooks
|
||||||
|
├── lib/ # Utilities, API clients
|
||||||
|
├── types/ # TypeScript type definitions
|
||||||
|
└── public/ # Static assets
|
||||||
|
```
|
||||||
|
|
||||||
|
### Path Aliases
|
||||||
|
|
||||||
|
Use these configured path aliases:
|
||||||
|
- `@/components` → `./components`
|
||||||
|
- `@/lib` → `./lib`
|
||||||
|
- `@/hooks` → `./hooks`
|
||||||
|
- `@/utils` → `./lib/utils`
|
||||||
|
- `@/ui` → `./components/ui`
|
||||||
|
|
||||||
|
### API Integration
|
||||||
|
|
||||||
|
- **Environment variables**: Use `NEXT_PUBLIC_*` for client-side access
|
||||||
|
- **Dolibarr client**: Use `dolibarrFetch()` from `@/lib/dolibarrClient`
|
||||||
|
- **Data transformation**: Map API responses to UI types using helper functions
|
||||||
|
- **Error boundaries**: Implement error handling for API failures
|
||||||
|
|
||||||
|
### Performance Guidelines
|
||||||
|
|
||||||
|
- **Dynamic imports**: Use `next/dynamic` for heavy components
|
||||||
|
- **Image optimization**: Use Next.js Image component
|
||||||
|
- **Bundle analysis**: Check bundle size with `npm run build`
|
||||||
|
- **Memoization**: Use `React.memo()` for expensive components
|
||||||
|
|
||||||
|
### Accessibility
|
||||||
|
|
||||||
|
- **Semantic HTML**: Use appropriate elements (`<button>`, `<nav>`, etc.)
|
||||||
|
- **ARIA attributes**: Add when needed for screen readers
|
||||||
|
- **Keyboard navigation**: Ensure all interactive elements are keyboard accessible
|
||||||
|
- **Focus management**: Handle focus in modals and dropdowns
|
||||||
|
|
||||||
|
## Development Workflow
|
||||||
|
|
||||||
|
1. **Start development**: `npm run dev`
|
||||||
|
2. **Run linter**: `npm run lint` (fix any errors before committing)
|
||||||
|
3. **Build test**: `npm run build` (ensure production build works)
|
||||||
|
4. **Type checking**: TypeScript is strict - fix all type errors
|
||||||
|
|
||||||
|
## Key Dependencies
|
||||||
|
|
||||||
|
- **Next.js**: 16.0.7 (App Router)
|
||||||
|
- **React**: 19.2.0
|
||||||
|
- **TypeScript**: 5.x
|
||||||
|
- **Tailwind CSS**: 3.4.18 + tailwindcss-animate
|
||||||
|
- **Radix UI**: Headless components for accessibility
|
||||||
|
- **Lucide React**: Icon library
|
||||||
|
- **shadcn/ui**: Component library built on Radix UI
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
Required environment variables (create `.env.local`):
|
||||||
|
- `NEXT_PUBLIC_API_URL` - Dolibarr API base URL
|
||||||
|
- `NEXT_PUBLIC_DOLIBARR_API_KEY` - Dolibarr API key
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
No test framework is currently configured. Recommended setup:
|
||||||
|
- Add Jest/Vitest for unit tests
|
||||||
|
- Add React Testing Library for component tests
|
||||||
|
- Add Playwright/Cypress for E2E tests
|
||||||
|
- Update package.json with test scripts
|
||||||
|
|
@ -26,7 +26,7 @@ export default function RootLayout({
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}>) {
|
}>) {
|
||||||
return (
|
return (
|
||||||
<html lang="en">
|
<html lang="en" suppressHydrationWarning>
|
||||||
<body className={`${geistSans.variable} ${geistMono.variable} antialiased`}>
|
<body className={`${geistSans.variable} ${geistMono.variable} antialiased`}>
|
||||||
<SidebarProvider>
|
<SidebarProvider>
|
||||||
<AppSidebar />
|
<AppSidebar />
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,62 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Calendar, DollarSign, Users, TrendingUp } from 'lucide-react';
|
import { Calendar, DollarSign, TrendingUp, FileText } from 'lucide-react';
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { Avatar, AvatarFallback } from '@/components/ui/avatar';
|
import { Avatar, AvatarFallback } from '@/components/ui/avatar';
|
||||||
import { Project, STATUS_CONFIG } from '@/types/project';
|
import { Project, ProjectStatus } from '@/types/project';
|
||||||
|
|
||||||
interface ProjectCardProps {
|
interface ProjectCardProps {
|
||||||
project: Project;
|
project: Project;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Función para generar iniciales
|
||||||
|
function getInitials(name: string): string {
|
||||||
|
return name
|
||||||
|
.split(' ')
|
||||||
|
.map(word => word[0])
|
||||||
|
.join('')
|
||||||
|
.toUpperCase()
|
||||||
|
.slice(0, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Componente para el badge de estado con estilos directos
|
||||||
|
function StatusBadge({ status }: { status: ProjectStatus }) {
|
||||||
|
if (status === '0') {
|
||||||
|
return (
|
||||||
|
<Badge variant="outline" className="mt-3 border bg-gray-100 text-gray-800 border-gray-200">
|
||||||
|
Borrador
|
||||||
|
</Badge>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status === '1') {
|
||||||
|
return (
|
||||||
|
<Badge variant="outline" className="mt-3 border bg-blue-100 text-blue-800 border-blue-200">
|
||||||
|
Abierto
|
||||||
|
</Badge>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status === '2') {
|
||||||
|
return (
|
||||||
|
<Badge variant="outline" className="mt-3 border bg-red-100 text-red-800 border-red-200">
|
||||||
|
Cerrado
|
||||||
|
</Badge>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback
|
||||||
|
return (
|
||||||
|
<Badge variant="outline" className="mt-3 border bg-gray-100 text-gray-800 border-gray-200">
|
||||||
|
Desconocido
|
||||||
|
</Badge>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export default function ProjectCard({ project }: ProjectCardProps) {
|
export default function ProjectCard({ project }: ProjectCardProps) {
|
||||||
|
const initials = getInitials(project.name);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="border-gray-200 hover:shadow-lg transition-all duration-300 hover:-translate-y-1">
|
<Card className="border-gray-200 hover:shadow-lg transition-all duration-300 hover:-translate-y-1">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
|
|
@ -18,20 +64,29 @@ export default function ProjectCard({ project }: ProjectCardProps) {
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<Avatar className="w-12 h-12">
|
<Avatar className="w-12 h-12">
|
||||||
<AvatarFallback className="bg-gradient-to-br from-blue-500 to-purple-600 text-white font-semibold">
|
<AvatarFallback className="bg-gradient-to-br from-blue-500 to-purple-600 text-white font-semibold">
|
||||||
{project.avatar}
|
{initials}
|
||||||
</AvatarFallback>
|
</AvatarFallback>
|
||||||
</Avatar>
|
</Avatar>
|
||||||
<div>
|
<div>
|
||||||
<CardTitle className="text-lg text-gray-900">{project.name}</CardTitle>
|
<CardTitle className="text-lg text-gray-900">{project.name}</CardTitle>
|
||||||
<CardDescription className="text-sm">{project.client}</CardDescription>
|
<CardDescription className="text-sm">{project.ref}</CardDescription>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Badge variant="outline" className={`mt-3 ${STATUS_CONFIG[project.status].color} border`}>
|
<StatusBadge status={project.status} />
|
||||||
{STATUS_CONFIG[project.status].label}
|
|
||||||
</Badge>
|
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
|
{/* Description */}
|
||||||
|
{project.description && (
|
||||||
|
<div className="mb-4">
|
||||||
|
<div className="flex items-center gap-1 text-gray-500 mb-1">
|
||||||
|
<FileText className="w-3 h-3" />
|
||||||
|
<span className="text-xs">Descripción</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-gray-700 line-clamp-2">{project.description}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Progress Bar */}
|
{/* Progress Bar */}
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<div className="flex items-center justify-between mb-2">
|
<div className="flex items-center justify-between mb-2">
|
||||||
|
|
@ -53,28 +108,44 @@ export default function ProjectCard({ project }: ProjectCardProps) {
|
||||||
<DollarSign className="w-3 h-3" />
|
<DollarSign className="w-3 h-3" />
|
||||||
<span className="text-xs">Presupuesto</span>
|
<span className="text-xs">Presupuesto</span>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm font-semibold text-gray-900">€{(project.budget / 1000).toFixed(0)}k</p>
|
<p className="text-sm font-semibold text-gray-900">
|
||||||
|
€{project.budget.toLocaleString('es-ES', { minimumFractionDigits: 0, maximumFractionDigits: 0 })}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center gap-1 text-gray-500 mb-1">
|
<div className="flex items-center gap-1 text-gray-500 mb-1">
|
||||||
<TrendingUp className="w-3 h-3" />
|
<TrendingUp className="w-3 h-3" />
|
||||||
<span className="text-xs">Gastado</span>
|
<span className="text-xs">Estimado</span>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm font-semibold text-gray-900">€{(project.spent / 1000).toFixed(0)}k</p>
|
<p className="text-sm font-semibold text-gray-900">
|
||||||
|
€{project.spent.toLocaleString('es-ES', { minimumFractionDigits: 0, maximumFractionDigits: 0 })}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Footer Info */}
|
{/* Footer Info */}
|
||||||
<div className="pt-4 border-t border-gray-100 flex items-center justify-between">
|
<div className="pt-4 border-t border-gray-100 flex items-center justify-between">
|
||||||
<div className="flex items-center gap-1 text-gray-500">
|
<div className="flex items-center gap-1 text-gray-500">
|
||||||
<Users className="w-4 h-4" />
|
<Calendar className="w-4 h-4" />
|
||||||
<span className="text-xs">{project.team} miembros</span>
|
<span className="text-xs">
|
||||||
|
{new Date(project.startDate).toLocaleDateString('es-ES', { day: '2-digit', month: 'short', year: 'numeric' })}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-1 text-gray-500">
|
<div className="flex items-center gap-1 text-gray-500">
|
||||||
<Calendar className="w-4 h-4" />
|
<Calendar className="w-4 h-4" />
|
||||||
<span className="text-xs">{new Date(project.endDate).toLocaleDateString('es-ES')}</span>
|
<span className="text-xs">
|
||||||
|
{new Date(project.endDate).toLocaleDateString('es-ES', { day: '2-digit', month: 'short', year: 'numeric' })}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Cliente */}
|
||||||
|
{project.client !== 'Sin cliente' && (
|
||||||
|
<div className="mt-3 pt-3 border-t border-gray-100">
|
||||||
|
<span className="text-xs text-gray-500">Cliente: </span>
|
||||||
|
<span className="text-xs font-medium text-gray-700">{project.client}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,105 +1,69 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import DashboardHeader from './dashboard-header';
|
import DashboardHeader from './dashboard-header';
|
||||||
import StatsOverview from './stats-overview';
|
import StatsOverview from './stats-overview';
|
||||||
import ProjectGrid from './project-grid';
|
import ProjectGrid from './project-grid';
|
||||||
|
import { getProjects } from '@/lib/projectsService'; // Importar desde el service
|
||||||
import { Project } from '@/types/project';
|
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() {
|
export default function ProjectDashboard() {
|
||||||
const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid');
|
const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid');
|
||||||
const [searchTerm, setSearchTerm] = useState('');
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
|
const [projects, setProjects] = useState<Project[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
const filteredProjects = mockProjects.filter(project =>
|
useEffect(() => {
|
||||||
|
async function loadProjects() {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const data = await getProjects();
|
||||||
|
setProjects(data);
|
||||||
|
setError(null);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error loading projects:', err);
|
||||||
|
setError('Error al cargar los proyectos');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
loadProjects();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const filteredProjects = projects.filter(project =>
|
||||||
project.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
project.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||||
project.client.toLowerCase().includes(searchTerm.toLowerCase())
|
project.client.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||||
|
project.ref.toLowerCase().includes(searchTerm.toLowerCase())
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gradient-to-br from-gray-50 to-gray-100 flex items-center justify-center">
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="inline-block h-8 w-8 animate-spin rounded-full border-4 border-solid border-blue-600 border-r-transparent"></div>
|
||||||
|
<p className="mt-4 text-gray-600">Cargando proyectos...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gradient-to-br from-gray-50 to-gray-100 flex items-center justify-center">
|
||||||
|
<div className="text-center">
|
||||||
|
<p className="text-red-600">{error}</p>
|
||||||
|
<button
|
||||||
|
onClick={() => window.location.reload()}
|
||||||
|
className="mt-4 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700"
|
||||||
|
>
|
||||||
|
Reintentar
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gradient-to-br from-gray-50 to-gray-100">
|
<div className="min-h-screen bg-gradient-to-br from-gray-50 to-gray-100">
|
||||||
<DashboardHeader
|
<DashboardHeader
|
||||||
|
|
@ -110,11 +74,9 @@ export default function ProjectDashboard() {
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||||
<StatsOverview projects={mockProjects} />
|
<StatsOverview projects={projects} />
|
||||||
<ProjectGrid projects={filteredProjects} viewMode={viewMode} />
|
<ProjectGrid projects={filteredProjects} viewMode={viewMode} />
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ interface StatsOverviewProps {
|
||||||
export default function StatsOverview({ projects }: StatsOverviewProps) {
|
export default function StatsOverview({ projects }: StatsOverviewProps) {
|
||||||
const stats = {
|
const stats = {
|
||||||
total: projects.length,
|
total: projects.length,
|
||||||
activos: projects.filter(p => p.progress < 100).length,
|
activos: projects.filter(p => p.progress < 100 && p.status !== "2").length,
|
||||||
totalBudget: projects.reduce((acc, p) => acc + p.budget, 0),
|
totalBudget: projects.reduce((acc, p) => acc + p.budget, 0),
|
||||||
avgProgress: Math.round(projects.reduce((acc, p) => acc + p.progress, 0) / projects.length)
|
avgProgress: Math.round(projects.reduce((acc, p) => acc + p.progress, 0) / projects.length)
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
'use client'
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import { cva, type VariantProps } from "class-variance-authority"
|
import { cva, type VariantProps } from "class-variance-authority"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,5 @@
|
||||||
export async function dolibarrFetch(endpoint: string, options: RequestInit = {}) {
|
export async function dolibarrFetch(endpoint: string, options: RequestInit = {}) {
|
||||||
const apiKey = process.env.NEXT_PUBLIC_DOLIBARR_API_KEY
|
|
||||||
console.log(apiKey)
|
|
||||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/${endpoint}?DOLAPIKEY=${process.env.NEXT_PUBLIC_DOLIBARR_API_KEY}`;
|
const url = `${process.env.NEXT_PUBLIC_API_URL}/${endpoint}?DOLAPIKEY=${process.env.NEXT_PUBLIC_DOLIBARR_API_KEY}`;
|
||||||
// ${process.env.DOLIBARR_API_KEY}
|
|
||||||
console.log(url)
|
|
||||||
|
|
||||||
const res = await fetch(url);
|
const res = await fetch(url);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,37 @@
|
||||||
|
// lib/projectsService.ts
|
||||||
import { dolibarrFetch } from "./dolibarrClient";
|
import { dolibarrFetch } from "./dolibarrClient";
|
||||||
|
import { DolibarrProject, Project, mapDolibarrProject } from "@/types/project";
|
||||||
|
|
||||||
export async function getProjects() {
|
// Obtener proyectos crudos de Dolibarr
|
||||||
|
export async function getDolibarrProjects(): Promise<DolibarrProject[]> {
|
||||||
return dolibarrFetch("projects");
|
return dolibarrFetch("projects");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Obtener proyectos mapeados al formato de la UI
|
||||||
|
export async function getProjects(): Promise<Project[]> {
|
||||||
|
try {
|
||||||
|
const dolibarrProjects = await getDolibarrProjects();
|
||||||
|
|
||||||
|
// Mapear los proyectos de Dolibarr a nuestro formato
|
||||||
|
return dolibarrProjects.map(mapDolibarrProject);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching projects from Dolibarr:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Obtener un proyecto específico por ID
|
||||||
|
export async function getProjectById(id: number): Promise<Project | undefined> {
|
||||||
|
try {
|
||||||
|
const projects = await getProjects();
|
||||||
|
return projects.find(p => p.id === id);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching project by id:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Si necesitas obtener un proyecto específico directamente de Dolibarr
|
||||||
|
export async function getDolibarrProjectById(id: number): Promise<DolibarrProject> {
|
||||||
|
return dolibarrFetch(`projects/${id}`);
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,33 @@
|
||||||
export type ProjectStatus = 'en_progreso' | 'planificacion' | 'completado' | 'revision';
|
// types/project.ts
|
||||||
|
export type ProjectStatus = '0' | '1' | '2'; // 0: borrador, 1: validado/abierto, 2: cerrado
|
||||||
|
|
||||||
|
// Interface para los datos crudos que vienen de Dolibarr
|
||||||
|
export interface DolibarrProject {
|
||||||
|
id: string | number;
|
||||||
|
ref: string;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
date_start: number; // timestamp
|
||||||
|
date_end: number; // timestamp
|
||||||
|
status: string;
|
||||||
|
statut: string;
|
||||||
|
budget_amount: string;
|
||||||
|
opp_amount: string; // opportunity amount (presupuesto estimado)
|
||||||
|
opp_percent: string; // porcentaje de progreso
|
||||||
|
user_author_id: string | number;
|
||||||
|
thirdparty_name?: string;
|
||||||
|
socid?: string | number;
|
||||||
|
public: string | number;
|
||||||
|
usage_opportunity: string | number;
|
||||||
|
usage_task: string | number;
|
||||||
|
date_c: number; // fecha creación
|
||||||
|
date_m: number; // fecha modificación
|
||||||
|
}
|
||||||
|
|
||||||
|
// Interface para usar en la UI (formato normalizado)
|
||||||
export interface Project {
|
export interface Project {
|
||||||
id: number;
|
id: number;
|
||||||
|
ref: string;
|
||||||
name: string;
|
name: string;
|
||||||
client: string;
|
client: string;
|
||||||
status: ProjectStatus;
|
status: ProjectStatus;
|
||||||
|
|
@ -10,13 +36,57 @@ export interface Project {
|
||||||
spent: number;
|
spent: number;
|
||||||
startDate: string;
|
startDate: string;
|
||||||
endDate: string;
|
endDate: string;
|
||||||
team: number;
|
description: string;
|
||||||
avatar: string;
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const STATUS_CONFIG: Record<ProjectStatus, { label: string; color: string }> = {
|
// Configuración de estados - AHORA CON FUNCIÓN QUE RETORNA LAS CLASES COMPLETAS
|
||||||
en_progreso: { label: "En Progreso", color: "bg-blue-100 text-blue-800 border-blue-200" },
|
export const STATUS_CONFIG: Record<ProjectStatus, { label: string; getColorClasses: () => string }> = {
|
||||||
planificacion: { label: "Planificación", color: "bg-purple-100 text-purple-800 border-purple-200" },
|
'0': {
|
||||||
completado: { label: "Completado", color: "bg-green-100 text-green-800 border-green-200" },
|
label: "Borrador",
|
||||||
revision: { label: "En Revisión", color: "bg-amber-100 text-amber-800 border-amber-200" }
|
getColorClasses: () => "bg-gray-100 text-gray-800 border-gray-200"
|
||||||
|
},
|
||||||
|
'1': {
|
||||||
|
label: "Abierto",
|
||||||
|
getColorClasses: () => "bg-blue-100 text-blue-800 border-blue-200"
|
||||||
|
},
|
||||||
|
'2': {
|
||||||
|
label: "Cerrado",
|
||||||
|
getColorClasses: () => "bg-green-100 text-green-800 border-green-200"
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Función helper para obtener las clases de color según el estado
|
||||||
|
export function getStatusColorClasses(status: ProjectStatus): string {
|
||||||
|
const statusMap: Record<ProjectStatus, string> = {
|
||||||
|
'0': 'bg-gray-100 text-gray-800 border-gray-200',
|
||||||
|
'1': 'bg-blue-100 text-blue-800 border-blue-200',
|
||||||
|
'2': 'bg-green-100 text-green-800 border-green-200'
|
||||||
|
};
|
||||||
|
|
||||||
|
return statusMap[status] || 'bg-gray-100 text-gray-800 border-gray-200';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Función helper para convertir datos de Dolibarr a nuestro formato
|
||||||
|
export function mapDolibarrProject(dolibarr: DolibarrProject): Project {
|
||||||
|
const budget = parseFloat(dolibarr.budget_amount || '0');
|
||||||
|
const oppAmount = parseFloat(dolibarr.opp_amount || '0');
|
||||||
|
const progress = parseFloat(dolibarr.opp_percent || '0');
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: typeof dolibarr.id === 'string' ? parseInt(dolibarr.id) : dolibarr.id,
|
||||||
|
ref: dolibarr.ref,
|
||||||
|
name: dolibarr.title,
|
||||||
|
client: dolibarr.thirdparty_name || 'Sin cliente',
|
||||||
|
status: (dolibarr.status || dolibarr.statut) as ProjectStatus,
|
||||||
|
progress: progress,
|
||||||
|
budget: oppAmount || budget,
|
||||||
|
spent: (oppAmount || budget) * (progress / 100), // Calcular gastado según progreso
|
||||||
|
startDate: new Date(dolibarr.date_start * 1000).toISOString().split('T')[0],
|
||||||
|
endDate: new Date(dolibarr.date_end * 1000).toISOString().split('T')[0],
|
||||||
|
description: dolibarr.description || '',
|
||||||
|
createdAt: new Date(dolibarr.date_c * 1000).toISOString().split('T')[0],
|
||||||
|
updatedAt: new Date(dolibarr.date_m * 1000).toISOString().split('T')[0],
|
||||||
|
};
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue