readme actualizado para la Rama

This commit is contained in:
2025-12-10 11:26:03 -03:00
parent 9df97a144a
commit 40186f76b3
20 changed files with 8858 additions and 20 deletions

17
frontend/components.json Normal file
View File

@@ -0,0 +1,17 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "default",
"rsc": false,
"tsx": true,
"tailwind": {
"config": "tailwind.config.ts",
"css": "src/index.css",
"baseColor": "slate",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils"
}
}

7305
frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -9,22 +9,38 @@
"preview": "vite preview"
},
"dependencies": {
"@radix-ui/react-avatar": "^1.1.11",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-label": "^2.1.8",
"@radix-ui/react-select": "^2.2.6",
"@radix-ui/react-separator": "^1.1.8",
"@radix-ui/react-slot": "^1.2.4",
"@radix-ui/react-tabs": "^1.1.13",
"@radix-ui/react-toast": "^1.2.15",
"axios": "^1.6.5",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.0",
"lucide-react": "^0.303.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-markdown": "^9.0.1",
"react-router-dom": "^6.21.1",
"axios": "^1.6.5",
"react-signature-canvas": "^1.0.6",
"lucide-react": "^0.303.0",
"clsx": "^2.1.0",
"react-markdown": "^9.0.1"
"tailwind-merge": "^3.4.0",
"tailwindcss-animate": "^1.0.7"
},
"devDependencies": {
"@types/react": "^18.2.48",
"@types/react-dom": "^18.2.18",
"@types/node": "^24.10.2",
"@types/react": "^18.3.27",
"@types/react-dom": "^18.3.7",
"@typescript-eslint/eslint-plugin": "^8.49.0",
"@typescript-eslint/parser": "^8.49.0",
"@vitejs/plugin-react": "^4.2.1",
"autoprefixer": "^10.4.16",
"postcss": "^8.4.33",
"tailwindcss": "^3.4.1",
"typescript": "^5.9.3",
"vite": "^5.0.11"
}
}
}

View File

@@ -5,6 +5,7 @@ import ReactMarkdown from 'react-markdown'
import Sidebar from './Sidebar'
import QuestionTypeEditor from './QuestionTypeEditor'
import QuestionAnswerInput from './QuestionAnswerInput'
import RecambiosApp from './modules/recambios/RecambiosApp'
function App() {
const [user, setUser] = useState(null)
@@ -70,13 +71,18 @@ function App() {
};
useEffect(() => {
// Verificar si hay token guardado
const token = localStorage.getItem('token')
const userData = localStorage.getItem('user')
if (token && userData) {
setUser(JSON.parse(userData))
// MODO TEST: Auto-login como admin sin verificación
const testUser = {
id: 1,
username: 'admin',
full_name: 'Administrador Test',
role: 'admin',
employee_code: 'ADMIN001'
}
localStorage.setItem('token', 'test-token-bypass')
localStorage.setItem('user', JSON.stringify(testUser))
setUser(testUser)
setLoading(false)
}, [])
@@ -420,6 +426,7 @@ function DashboardPage({ user, setUser }) {
{activeTab === 'inspections' && '🔍'}
{activeTab === 'users' && '👥'}
{activeTab === 'reports' && '📊'}
{activeTab === 'recambios' && '📦'}
{activeTab === 'settings' && '⚙️'}
</span>
<span className="text-white font-semibold text-sm sm:text-base">
@@ -427,6 +434,7 @@ function DashboardPage({ user, setUser }) {
{activeTab === 'inspections' && 'Inspecciones'}
{activeTab === 'users' && 'Usuarios'}
{activeTab === 'reports' && 'Reportes'}
{activeTab === 'recambios' && 'Recambios'}
{activeTab === 'settings' && 'Configuración'}
</span>
</div>
@@ -437,6 +445,7 @@ function DashboardPage({ user, setUser }) {
{activeTab === 'inspections' && '🔍'}
{activeTab === 'users' && '👥'}
{activeTab === 'reports' && '📊'}
{activeTab === 'recambios' && '📦'}
{activeTab === 'settings' && '⚙️'}
</span>
</div>
@@ -461,6 +470,8 @@ function DashboardPage({ user, setUser }) {
/>
) : activeTab === 'inspections' ? (
<InspectionsTab inspections={inspections} user={user} onUpdate={loadData} onContinue={setActiveInspection} />
) : activeTab === 'recambios' ? (
<RecambiosApp />
) : activeTab === 'settings' ? (
<SettingsTab user={user} />
) : activeTab === 'api-tokens' ? (

View File

@@ -101,6 +101,22 @@ export default function Sidebar({ user, activeTab, setActiveTab, sidebarOpen, se
</button>
</li>
)}
{user.role === 'admin' && (
<li>
<button
onClick={() => setActiveTab('recambios')}
className={`w-full flex items-center ${sidebarOpen ? 'gap-3 px-4' : 'justify-center px-2'} py-3 rounded-lg transition ${
activeTab === 'recambios'
? 'bg-gradient-to-r from-emerald-600 to-teal-600 text-white shadow-lg'
: 'text-indigo-200 hover:bg-indigo-900/50'
}`}
title={!sidebarOpen ? 'Recambios' : ''}
>
<span className="text-xl">📦</span>
{sidebarOpen && <span>Recambios</span>}
</button>
</li>
)}
{user.role === 'admin' && (
<>
<li>

View File

@@ -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 }

View File

@@ -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 }

View File

@@ -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 }

View File

@@ -0,0 +1,53 @@
import * as React from "react"
import * as TabsPrimitive from "@radix-ui/react-tabs"
import { cn } from "@/lib/utils"
const Tabs = TabsPrimitive.Root
const TabsList = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.List>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
>(({ className, ...props }, ref) => (
<TabsPrimitive.List
ref={ref}
className={cn(
"inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground",
className
)}
{...props}
/>
))
TabsList.displayName = TabsPrimitive.List.displayName
const TabsTrigger = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Trigger
ref={ref}
className={cn(
"inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm",
className
)}
{...props}
/>
))
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
const TabsContent = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Content
ref={ref}
className={cn(
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
className
)}
{...props}
/>
))
TabsContent.displayName = TabsPrimitive.Content.displayName
export { Tabs, TabsList, TabsTrigger, TabsContent }

View File

@@ -2,13 +2,76 @@
@tailwind components;
@tailwind utilities;
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--card: 0 0% 100%;
--card-foreground: 222.2 84% 4.9%;
--popover: 0 0% 100%;
--popover-foreground: 222.2 84% 4.9%;
--primary: 222.2 47.4% 11.2%;
--primary-foreground: 210 40% 98%;
--secondary: 210 40% 96.1%;
--secondary-foreground: 222.2 47.4% 11.2%;
--muted: 210 40% 96.1%;
--muted-foreground: 215.4 16.3% 46.9%;
--accent: 210 40% 96.1%;
--accent-foreground: 222.2 47.4% 11.2%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 210 40% 98%;
--border: 214.3 31.8% 91.4%;
--input: 214.3 31.8% 91.4%;
--ring: 222.2 84% 4.9%;
--radius: 0.5rem;
--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%;
}
.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
--card: 222.2 84% 4.9%;
--card-foreground: 210 40% 98%;
--popover: 222.2 84% 4.9%;
--popover-foreground: 210 40% 98%;
--primary: 210 40% 98%;
--primary-foreground: 222.2 47.4% 11.2%;
--secondary: 217.2 32.6% 17.5%;
--secondary-foreground: 210 40% 98%;
--muted: 217.2 32.6% 17.5%;
--muted-foreground: 215 20.2% 65.1%;
--accent: 217.2 32.6% 17.5%;
--accent-foreground: 210 40% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 210 40% 98%;
--border: 217.2 32.6% 17.5%;
--input: 217.2 32.6% 17.5%;
--ring: 212.7 26.8% 83.9%;
--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%;
}
* {
border-color: hsl(var(--border));
}
body {
background-color: hsl(var(--background));
color: hsl(var(--foreground));
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
}
code {

View File

@@ -0,0 +1,6 @@
import { type ClassValue, clsx } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}

View File

@@ -0,0 +1,405 @@
import React, { useState } from 'react';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import {
Package,
Truck,
ClipboardList,
FileText,
Users,
Settings,
TrendingUp,
AlertCircle,
CheckCircle2,
Clock,
Search
} from 'lucide-react';
interface Pedido {
id: number;
referencia: string;
cliente: string;
estado: 'pendiente_decidir' | 'pedido_proveedor' | 'recibida' | 'entregada';
prioridad: 'alta' | 'media' | 'baja';
total: number;
fecha: string;
}
const RecambiosApp: React.FC = () => {
const [activeTab, setActiveTab] = useState('kanban');
// Datos de ejemplo
const pedidosEjemplo: Pedido[] = [
{
id: 1,
referencia: 'REF-2024-001',
cliente: 'Juan Pérez - VW Golf GTI',
estado: 'pendiente_decidir',
prioridad: 'alta',
total: 450.00,
fecha: '2024-12-09'
},
{
id: 2,
referencia: 'REF-2024-002',
cliente: 'María González - Seat León',
estado: 'pedido_proveedor',
prioridad: 'media',
total: 320.00,
fecha: '2024-12-08'
},
{
id: 3,
referencia: 'REF-2024-003',
cliente: 'Carlos Díaz - Audi A4',
estado: 'recibida',
prioridad: 'alta',
total: 680.00,
fecha: '2024-12-07'
}
];
const getEstadoColor = (estado: Pedido['estado']) => {
const colores = {
pendiente_decidir: 'bg-yellow-100 text-yellow-800 border-yellow-300',
pedido_proveedor: 'bg-blue-100 text-blue-800 border-blue-300',
recibida: 'bg-green-100 text-green-800 border-green-300',
entregada: 'bg-gray-100 text-gray-800 border-gray-300'
};
return colores[estado];
};
const getPrioridadColor = (prioridad: Pedido['prioridad']) => {
const colores = {
alta: 'bg-red-500 text-white',
media: 'bg-orange-500 text-white',
baja: 'bg-green-500 text-white'
};
return colores[prioridad];
};
return (
<div className="min-h-screen bg-gradient-to-br from-slate-50 to-slate-100 p-6">
{/* Header */}
<div className="mb-8">
<div className="flex items-center justify-between mb-4">
<div>
<h1 className="text-4xl font-bold text-slate-900 mb-2">
Sistema de Gestión de Recambios
</h1>
<p className="text-slate-600">
Panel unificado para gestión integral de pedidos, proveedores y entregas
</p>
</div>
<div className="flex gap-3">
<Button variant="outline" className="gap-2">
<Search className="w-4 h-4" />
Buscar
</Button>
<Button className="gap-2">
<Package className="w-4 h-4" />
Nuevo Pedido
</Button>
</div>
</div>
{/* Stats Cards */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
<Card className="border-l-4 border-l-yellow-500">
<CardHeader className="pb-2">
<CardDescription>Pendientes Decidir</CardDescription>
<CardTitle className="text-3xl">12</CardTitle>
</CardHeader>
<CardContent>
<div className="flex items-center gap-2 text-sm text-yellow-600">
<AlertCircle className="w-4 h-4" />
<span>Requieren atención</span>
</div>
</CardContent>
</Card>
<Card className="border-l-4 border-l-blue-500">
<CardHeader className="pb-2">
<CardDescription>Pedidos Proveedor</CardDescription>
<CardTitle className="text-3xl">8</CardTitle>
</CardHeader>
<CardContent>
<div className="flex items-center gap-2 text-sm text-blue-600">
<Clock className="w-4 h-4" />
<span>En proceso</span>
</div>
</CardContent>
</Card>
<Card className="border-l-4 border-l-green-500">
<CardHeader className="pb-2">
<CardDescription>Recibidas</CardDescription>
<CardTitle className="text-3xl">5</CardTitle>
</CardHeader>
<CardContent>
<div className="flex items-center gap-2 text-sm text-green-600">
<CheckCircle2 className="w-4 h-4" />
<span>Listas para entregar</span>
</div>
</CardContent>
</Card>
<Card className="border-l-4 border-l-purple-500">
<CardHeader className="pb-2">
<CardDescription>Volumen Mes</CardDescription>
<CardTitle className="text-3xl">15.4K</CardTitle>
</CardHeader>
<CardContent>
<div className="flex items-center gap-2 text-sm text-purple-600">
<TrendingUp className="w-4 h-4" />
<span>+12% vs mes anterior</span>
</div>
</CardContent>
</Card>
</div>
</div>
{/* Tabs Navigation */}
<Tabs value={activeTab} onValueChange={setActiveTab} className="space-y-6">
<TabsList className="grid grid-cols-6 w-full max-w-4xl">
<TabsTrigger value="kanban" className="gap-2">
<ClipboardList className="w-4 h-4" />
Kanban
</TabsTrigger>
<TabsTrigger value="proveedores" className="gap-2">
<Truck className="w-4 h-4" />
Proveedores
</TabsTrigger>
<TabsTrigger value="albaranes" className="gap-2">
<FileText className="w-4 h-4" />
Albaranes
</TabsTrigger>
<TabsTrigger value="clientes" className="gap-2">
<Users className="w-4 h-4" />
Clientes
</TabsTrigger>
<TabsTrigger value="inventario" className="gap-2">
<Package className="w-4 h-4" />
Inventario
</TabsTrigger>
<TabsTrigger value="config" className="gap-2">
<Settings className="w-4 h-4" />
Config
</TabsTrigger>
</TabsList>
{/* Kanban View */}
<TabsContent value="kanban" className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-4 gap-6">
{/* Columna: Pendiente Decidir */}
<div className="space-y-3">
<div className="flex items-center gap-2 mb-4">
<div className="w-3 h-3 rounded-full bg-yellow-500"></div>
<h3 className="font-semibold text-slate-700">Pendiente Decidir</h3>
<Badge variant="secondary">1</Badge>
</div>
{pedidosEjemplo
.filter(p => p.estado === 'pendiente_decidir')
.map(pedido => (
<Card key={pedido.id} className="cursor-pointer hover:shadow-lg transition-shadow">
<CardHeader className="pb-3">
<div className="flex items-start justify-between mb-2">
<Badge className={getPrioridadColor(pedido.prioridad)}>
{pedido.prioridad}
</Badge>
<span className="text-xs text-slate-500">{pedido.referencia}</span>
</div>
<CardTitle className="text-base">{pedido.cliente}</CardTitle>
</CardHeader>
<CardContent>
<div className="flex justify-between items-center">
<span className="text-lg font-bold text-slate-900">
{pedido.total.toFixed(2)}
</span>
<span className="text-xs text-slate-500">{pedido.fecha}</span>
</div>
</CardContent>
</Card>
))}
</div>
{/* Columna: Pedido Proveedor */}
<div className="space-y-3">
<div className="flex items-center gap-2 mb-4">
<div className="w-3 h-3 rounded-full bg-blue-500"></div>
<h3 className="font-semibold text-slate-700">Pedido Proveedor</h3>
<Badge variant="secondary">1</Badge>
</div>
{pedidosEjemplo
.filter(p => p.estado === 'pedido_proveedor')
.map(pedido => (
<Card key={pedido.id} className="cursor-pointer hover:shadow-lg transition-shadow">
<CardHeader className="pb-3">
<div className="flex items-start justify-between mb-2">
<Badge className={getPrioridadColor(pedido.prioridad)}>
{pedido.prioridad}
</Badge>
<span className="text-xs text-slate-500">{pedido.referencia}</span>
</div>
<CardTitle className="text-base">{pedido.cliente}</CardTitle>
</CardHeader>
<CardContent>
<div className="flex justify-between items-center">
<span className="text-lg font-bold text-slate-900">
{pedido.total.toFixed(2)}
</span>
<span className="text-xs text-slate-500">{pedido.fecha}</span>
</div>
</CardContent>
</Card>
))}
</div>
{/* Columna: Recibida */}
<div className="space-y-3">
<div className="flex items-center gap-2 mb-4">
<div className="w-3 h-3 rounded-full bg-green-500"></div>
<h3 className="font-semibold text-slate-700">Recibida</h3>
<Badge variant="secondary">1</Badge>
</div>
{pedidosEjemplo
.filter(p => p.estado === 'recibida')
.map(pedido => (
<Card key={pedido.id} className="cursor-pointer hover:shadow-lg transition-shadow">
<CardHeader className="pb-3">
<div className="flex items-start justify-between mb-2">
<Badge className={getPrioridadColor(pedido.prioridad)}>
{pedido.prioridad}
</Badge>
<span className="text-xs text-slate-500">{pedido.referencia}</span>
</div>
<CardTitle className="text-base">{pedido.cliente}</CardTitle>
</CardHeader>
<CardContent>
<div className="flex justify-between items-center">
<span className="text-lg font-bold text-slate-900">
{pedido.total.toFixed(2)}
</span>
<span className="text-xs text-slate-500">{pedido.fecha}</span>
</div>
</CardContent>
</Card>
))}
</div>
{/* Columna: Entregada */}
<div className="space-y-3">
<div className="flex items-center gap-2 mb-4">
<div className="w-3 h-3 rounded-full bg-gray-400"></div>
<h3 className="font-semibold text-slate-700">Entregada</h3>
<Badge variant="secondary">0</Badge>
</div>
<Card className="border-dashed border-2 bg-slate-50">
<CardContent className="flex items-center justify-center h-32 text-slate-400">
Sin pedidos entregados
</CardContent>
</Card>
</div>
</div>
</TabsContent>
{/* Proveedores View */}
<TabsContent value="proveedores">
<Card>
<CardHeader>
<CardTitle>Panel de Proveedores</CardTitle>
<CardDescription>
Gestión de proveedores, pedidos y seguimiento de entregas
</CardDescription>
</CardHeader>
<CardContent className="flex items-center justify-center h-64 text-slate-400">
<div className="text-center">
<Truck className="w-16 h-16 mx-auto mb-4 text-slate-300" />
<p>Próximamente: Panel completo de proveedores</p>
</div>
</CardContent>
</Card>
</TabsContent>
{/* Albaranes View */}
<TabsContent value="albaranes">
<Card>
<CardHeader>
<CardTitle>Gestión de Albaranes</CardTitle>
<CardDescription>
Registro y seguimiento de albaranes de entrega
</CardDescription>
</CardHeader>
<CardContent className="flex items-center justify-center h-64 text-slate-400">
<div className="text-center">
<FileText className="w-16 h-16 mx-auto mb-4 text-slate-300" />
<p>Próximamente: Sistema de albaranes</p>
</div>
</CardContent>
</Card>
</TabsContent>
{/* Clientes View */}
<TabsContent value="clientes">
<Card>
<CardHeader>
<CardTitle>Gestión de Clientes</CardTitle>
<CardDescription>
Base de datos de clientes y su historial de pedidos
</CardDescription>
</CardHeader>
<CardContent className="flex items-center justify-center h-64 text-slate-400">
<div className="text-center">
<Users className="w-16 h-16 mx-auto mb-4 text-slate-300" />
<p>Próximamente: Panel de clientes</p>
</div>
</CardContent>
</Card>
</TabsContent>
{/* Inventario View */}
<TabsContent value="inventario">
<Card>
<CardHeader>
<CardTitle>Control de Inventario</CardTitle>
<CardDescription>
Stock, ubicaciones y movimientos de recambios
</CardDescription>
</CardHeader>
<CardContent className="flex items-center justify-center h-64 text-slate-400">
<div className="text-center">
<Package className="w-16 h-16 mx-auto mb-4 text-slate-300" />
<p>Próximamente: Sistema de inventario</p>
</div>
</CardContent>
</Card>
</TabsContent>
{/* Configuración View */}
<TabsContent value="config">
<Card>
<CardHeader>
<CardTitle>Configuración del Sistema</CardTitle>
<CardDescription>
Preferencias, categorías y parámetros del módulo
</CardDescription>
</CardHeader>
<CardContent className="flex items-center justify-center h-64 text-slate-400">
<div className="text-center">
<Settings className="w-16 h-16 mx-auto mb-4 text-slate-300" />
<p>Próximamente: Panel de configuración</p>
</div>
</CardContent>
</Card>
</TabsContent>
</Tabs>
</div>
);
};
export default RecambiosApp;

9
frontend/src/vite-env.d.ts vendored Normal file
View File

@@ -0,0 +1,9 @@
/// <reference types="vite/client" />
interface ImportMetaEnv {
readonly VITE_API_URL: string
}
interface ImportMeta {
readonly env: ImportMetaEnv
}

View File

@@ -0,0 +1,80 @@
import type { Config } from 'tailwindcss'
const config = {
darkMode: ["class"],
content: [
'./pages/**/*.{ts,tsx}',
'./components/**/*.{ts,tsx}',
'./app/**/*.{ts,tsx}',
'./src/**/*.{ts,tsx}',
],
prefix: "",
theme: {
container: {
center: true,
padding: "2rem",
screens: {
"2xl": "1400px",
},
},
extend: {
colors: {
border: "hsl(var(--border))",
input: "hsl(var(--input))",
ring: "hsl(var(--ring))",
background: "hsl(var(--background))",
foreground: "hsl(var(--foreground))",
primary: {
DEFAULT: "hsl(var(--primary))",
foreground: "hsl(var(--primary-foreground))",
},
secondary: {
DEFAULT: "hsl(var(--secondary))",
foreground: "hsl(var(--secondary-foreground))",
},
destructive: {
DEFAULT: "hsl(var(--destructive))",
foreground: "hsl(var(--destructive-foreground))",
},
muted: {
DEFAULT: "hsl(var(--muted))",
foreground: "hsl(var(--muted-foreground))",
},
accent: {
DEFAULT: "hsl(var(--accent))",
foreground: "hsl(var(--accent-foreground))",
},
popover: {
DEFAULT: "hsl(var(--popover))",
foreground: "hsl(var(--popover-foreground))",
},
card: {
DEFAULT: "hsl(var(--card))",
foreground: "hsl(var(--card-foreground))",
},
},
borderRadius: {
lg: "var(--radius)",
md: "calc(var(--radius) - 2px)",
sm: "calc(var(--radius) - 4px)",
},
keyframes: {
"accordion-down": {
from: { height: "0" },
to: { height: "var(--radix-accordion-content-height)" },
},
"accordion-up": {
from: { height: "var(--radix-accordion-content-height)" },
to: { height: "0" },
},
},
animation: {
"accordion-down": "accordion-down 0.2s ease-out",
"accordion-up": "accordion-up 0.2s ease-out",
},
},
},
plugins: [require("tailwindcss-animate")],
} satisfies Config
export default config

31
frontend/tsconfig.json Normal file
View File

@@ -0,0 +1,31 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
/* Path mapping */
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
}

View File

@@ -0,0 +1,10 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}

View File

@@ -1,8 +1,14 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import path from 'path'
export default defineConfig({
plugins: [react()],
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
},
},
server: {
host: true,
port: 5173,

22
frontend/vite.config.ts Normal file
View File

@@ -0,0 +1,22 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import path from 'path'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
},
},
server: {
port: 5173,
proxy: {
'/api': {
target: 'http://localhost:8000',
changeOrigin: true,
},
},
},
})