From 09bdf14c0a3245424416f3320297d19e2853756e Mon Sep 17 00:00:00 2001 From: Levi Planelles Date: Fri, 9 Jan 2026 18:39:19 +0100 Subject: [PATCH] update for marcos --- AGENTS.md | 228 +++++++++++++++++++++ app/layout.tsx | 2 +- components/dashboard/project-card.tsx | 97 +++++++-- components/dashboard/project-dashboard.tsx | 146 +++++-------- components/dashboard/stats-overview.tsx | 2 +- components/ui/badge.tsx | 1 + lib/dolibarrClient.ts | 4 - lib/projectsService.ts | 34 ++- types/project.ts | 88 +++++++- 9 files changed, 481 insertions(+), 121 deletions(-) create mode 100644 AGENTS.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..5acf2a9 --- /dev/null +++ b/AGENTS.md @@ -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`, `VariantProps` +- **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( + ({ className, ...props }, ref) => { + return ( +
+ ); + } +); +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 (`
- - {STATUS_CONFIG[project.status].label} - + + {/* Description */} + {project.description && ( +
+
+ + Descripción +
+

{project.description}

+
+ )} + {/* Progress Bar */}
@@ -53,28 +108,44 @@ export default function ProjectCard({ project }: ProjectCardProps) { Presupuesto
-

€{(project.budget / 1000).toFixed(0)}k

+

+ €{project.budget.toLocaleString('es-ES', { minimumFractionDigits: 0, maximumFractionDigits: 0 })} +

- Gastado + Estimado
-

€{(project.spent / 1000).toFixed(0)}k

+

+ €{project.spent.toLocaleString('es-ES', { minimumFractionDigits: 0, maximumFractionDigits: 0 })} +

{/* Footer Info */}
- - {project.team} miembros + + + {new Date(project.startDate).toLocaleDateString('es-ES', { day: '2-digit', month: 'short', year: 'numeric' })} +
- {new Date(project.endDate).toLocaleDateString('es-ES')} + + {new Date(project.endDate).toLocaleDateString('es-ES', { day: '2-digit', month: 'short', year: 'numeric' })} +
+ + {/* Cliente */} + {project.client !== 'Sin cliente' && ( +
+ Cliente: + {project.client} +
+ )}
); diff --git a/components/dashboard/project-dashboard.tsx b/components/dashboard/project-dashboard.tsx index 84bc3b2..8332f84 100644 --- a/components/dashboard/project-dashboard.tsx +++ b/components/dashboard/project-dashboard.tsx @@ -1,105 +1,69 @@ "use client"; -import { useState } from 'react'; +import { useState, useEffect } from 'react'; import DashboardHeader from './dashboard-header'; import StatsOverview from './stats-overview'; import ProjectGrid from './project-grid'; +import { getProjects } from '@/lib/projectsService'; // Importar desde el service 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 [projects, setProjects] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(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.client.toLowerCase().includes(searchTerm.toLowerCase()) + project.client.toLowerCase().includes(searchTerm.toLowerCase()) || + project.ref.toLowerCase().includes(searchTerm.toLowerCase()) ); + if (loading) { + return ( +
+
+
+

Cargando proyectos...

+
+
+ ); + } + + if (error) { + return ( +
+
+

{error}

+ +
+
+ ); + } + return (
- +
); -} - - +} \ No newline at end of file diff --git a/components/dashboard/stats-overview.tsx b/components/dashboard/stats-overview.tsx index f576066..8352f74 100644 --- a/components/dashboard/stats-overview.tsx +++ b/components/dashboard/stats-overview.tsx @@ -11,7 +11,7 @@ interface StatsOverviewProps { export default function StatsOverview({ projects }: StatsOverviewProps) { const stats = { 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), avgProgress: Math.round(projects.reduce((acc, p) => acc + p.progress, 0) / projects.length) }; diff --git a/components/ui/badge.tsx b/components/ui/badge.tsx index f000e3e..2b4beb8 100644 --- a/components/ui/badge.tsx +++ b/components/ui/badge.tsx @@ -1,3 +1,4 @@ +'use client' import * as React from "react" import { cva, type VariantProps } from "class-variance-authority" diff --git a/lib/dolibarrClient.ts b/lib/dolibarrClient.ts index c4803c8..204d57f 100644 --- a/lib/dolibarrClient.ts +++ b/lib/dolibarrClient.ts @@ -1,9 +1,5 @@ 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}`; - // ${process.env.DOLIBARR_API_KEY} - console.log(url) const res = await fetch(url); diff --git a/lib/projectsService.ts b/lib/projectsService.ts index 8091c60..01476cf 100644 --- a/lib/projectsService.ts +++ b/lib/projectsService.ts @@ -1,5 +1,37 @@ +// lib/projectsService.ts 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 { return dolibarrFetch("projects"); } + +// Obtener proyectos mapeados al formato de la UI +export async function getProjects(): Promise { + 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 { + 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 { + return dolibarrFetch(`projects/${id}`); +} \ No newline at end of file diff --git a/types/project.ts b/types/project.ts index 3a001d6..9c1f145 100644 --- a/types/project.ts +++ b/types/project.ts @@ -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 { id: number; + ref: string; name: string; client: string; status: ProjectStatus; @@ -10,13 +36,57 @@ export interface Project { spent: number; startDate: string; endDate: string; - team: number; - avatar: string; + description: string; + createdAt: string; + updatedAt: string; } -export const STATUS_CONFIG: Record = { - 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" } -}; \ No newline at end of file +// Configuración de estados - AHORA CON FUNCIÓN QUE RETORNA LAS CLASES COMPLETAS +export const STATUS_CONFIG: Record string }> = { + '0': { + label: "Borrador", + 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 = { + '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], + }; +} \ No newline at end of file