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;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<html lang="en" suppressHydrationWarning>
|
||||
<body className={`${geistSans.variable} ${geistMono.variable} antialiased`}>
|
||||
<SidebarProvider>
|
||||
<AppSidebar />
|
||||
|
|
|
|||
|
|
@ -1,16 +1,62 @@
|
|||
"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 { Badge } from '@/components/ui/badge';
|
||||
import { Avatar, AvatarFallback } from '@/components/ui/avatar';
|
||||
import { Project, STATUS_CONFIG } from '@/types/project';
|
||||
import { Project, ProjectStatus } from '@/types/project';
|
||||
|
||||
interface ProjectCardProps {
|
||||
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) {
|
||||
const initials = getInitials(project.name);
|
||||
|
||||
return (
|
||||
<Card className="border-gray-200 hover:shadow-lg transition-all duration-300 hover:-translate-y-1">
|
||||
<CardHeader>
|
||||
|
|
@ -18,20 +64,29 @@ export default function ProjectCard({ project }: ProjectCardProps) {
|
|||
<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}
|
||||
{initials}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<div>
|
||||
<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>
|
||||
<Badge variant="outline" className={`mt-3 ${STATUS_CONFIG[project.status].color} border`}>
|
||||
{STATUS_CONFIG[project.status].label}
|
||||
</Badge>
|
||||
<StatusBadge status={project.status} />
|
||||
</CardHeader>
|
||||
<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 */}
|
||||
<div className="mb-4">
|
||||
<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" />
|
||||
<span className="text-xs">Presupuesto</span>
|
||||
</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 className="flex items-center gap-1 text-gray-500 mb-1">
|
||||
<TrendingUp className="w-3 h-3" />
|
||||
<span className="text-xs">Gastado</span>
|
||||
<span className="text-xs">Estimado</span>
|
||||
</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>
|
||||
|
||||
{/* 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>
|
||||
<Calendar className="w-4 h-4" />
|
||||
<span className="text-xs">
|
||||
{new Date(project.startDate).toLocaleDateString('es-ES', { day: '2-digit', month: 'short', year: 'numeric' })}
|
||||
</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>
|
||||
<span className="text-xs">
|
||||
{new Date(project.endDate).toLocaleDateString('es-ES', { day: '2-digit', month: 'short', year: 'numeric' })}
|
||||
</span>
|
||||
</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>
|
||||
</Card>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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<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.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 (
|
||||
<div className="min-h-screen bg-gradient-to-br from-gray-50 to-gray-100">
|
||||
<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">
|
||||
<StatsOverview projects={mockProjects} />
|
||||
<StatsOverview projects={projects} />
|
||||
<ProjectGrid projects={filteredProjects} viewMode={viewMode} />
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -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)
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
'use client'
|
||||
import * as React from "react"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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<DolibarrProject[]> {
|
||||
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 {
|
||||
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<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" }
|
||||
};
|
||||
// Configuración de estados - AHORA CON FUNCIÓN QUE RETORNA LAS CLASES COMPLETAS
|
||||
export const STATUS_CONFIG: Record<ProjectStatus, { label: string; getColorClasses: () => 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<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