Compare commits
104 Commits
ef9c37dcdd
...
feature/re
| Author | SHA1 | Date | |
|---|---|---|---|
| 40186f76b3 | |||
| 9df97a144a | |||
| 0e19957e34 | |||
| a0927131c4 | |||
| ff0386402d | |||
| b078d2f2cf | |||
| a4f2f0af02 | |||
| 037c4baf1b | |||
| 1b0a50338e | |||
| 5ba6d31046 | |||
| 95543d5858 | |||
| 216b9ceb20 | |||
| 27763bf155 | |||
| b3c0030a52 | |||
| 0c0812efe9 | |||
| 7fd37d0992 | |||
| 49d3ef9db1 | |||
| 7111550fb7 | |||
| f73319046e | |||
| e9a184f087 | |||
| 954c5b4a7b | |||
| 6455d351dd | |||
| ae3a50054a | |||
| 387897acfc | |||
| 56decba945 | |||
| 65a74cf754 | |||
| 289b4b6b93 | |||
| a1ab955556 | |||
| 14d5027170 | |||
| a8afaa044f | |||
| 7f2e9add29 | |||
| 24eb039302 | |||
| 9de059e9ca | |||
| 9ed41c9ee4 | |||
| b191030321 | |||
| 023a004c53 | |||
| 59a0f56b99 | |||
| 3bf8b44581 | |||
| 311d363e31 | |||
| d3676172e1 | |||
| e3524b32d4 | |||
| 44cd81956f | |||
| 58bf1bfc69 | |||
| 50909e4499 | |||
| 582114a55a | |||
| c4f5d960de | |||
| 35b419a654 | |||
| fce31467d8 | |||
| c6a6ba976e | |||
| 31f5edae84 | |||
| de5f09a351 | |||
| 7f50bfd8c6 | |||
| c374909fa8 | |||
| bf30b1a2bf | |||
| d51d912962 | |||
| 1450d443d4 | |||
| 1988ec95da | |||
| d86a216766 | |||
| 4174774702 | |||
| 54006d5756 | |||
| c226fbd34b | |||
| b2398efead | |||
| 14a64778b8 | |||
| a692948a6f | |||
| 45ad650bac | |||
| 7820f143ac | |||
| 2db2833f27 | |||
| 7b39648be5 | |||
| c76f803871 | |||
| b6440130ac | |||
| 886f0bafbd | |||
| 00218a1a92 | |||
| ed037ef4cc | |||
| 37daf6b8d3 | |||
| f57d7328e1 | |||
| 6d006d3b13 | |||
| 96426a4259 | |||
| 16f431cbad | |||
| 91711ac95f | |||
| 4e70f1f9b0 | |||
| 14b3376a4a | |||
| 185b9fc631 | |||
| 320f41c0ff | |||
| e79aa1f212 | |||
| 34221c4726 | |||
| 58672c52d7 | |||
| 416588a327 | |||
| 32c7f79dd6 | |||
| 1c9d7348ed | |||
| ce151631ab | |||
| 2d520e03d6 | |||
| bd2b11d543 | |||
| 97c5aab93d | |||
| d6c0f117a1 | |||
| 651aa138cf | |||
| 826c5fce5e | |||
| ed3f513075 | |||
| 027f22551c | |||
| 0117ba34f8 | |||
| efbf57e6bc | |||
| afe57fba1d | |||
| 409cbd437a | |||
| ac17c26c66 | |||
| e7d64e0094 |
133
.github/copilot-instructions.md
vendored
Normal file
133
.github/copilot-instructions.md
vendored
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
# No olvides dar lo comentarios de los cambios que se hicieron para el backend y el front en para los comentarios de git
|
||||||
|
# Siempre actuliza la version del front y del back la version del front esta en el archivo package.json y la del backend en el archivo main.py en una variable llamada BACKEND_VERSION
|
||||||
|
# Si el FrontEnd no sufre modificaciones no es necesario actualizar su version, al igual que el backend, solo poner en el comentario de git que no se hicieron cambios en el front o en el backend segun sea el caso
|
||||||
|
|
||||||
|
# Ayudetec - Intelligent Checklist System for Automotive Workshops
|
||||||
|
|
||||||
|
## Architecture Overview
|
||||||
|
|
||||||
|
**Tech Stack**: FastAPI (Python 3.11) + React 18 + PostgreSQL 15 + MinIO/S3
|
||||||
|
**Deployment**: Docker Compose for dev, Docker Stack for production
|
||||||
|
**Key Feature**: AI-powered inspection analysis with OpenAI/Gemini integration
|
||||||
|
|
||||||
|
### Service Structure
|
||||||
|
- `backend/` - FastAPI monolith with JWT auth, S3 file storage, PDF generation
|
||||||
|
- `frontend/` - React SPA with Vite, TailwindCSS, client-side routing
|
||||||
|
- `postgres` - Main data store with checklist templates, inspections, answers
|
||||||
|
- MinIO/S3 - Image and PDF storage (configurable endpoint)
|
||||||
|
|
||||||
|
### Core Data Model
|
||||||
|
**Users** (`role`: admin/mechanic/asesor) → **Checklists** (templates with questions) → **Inspections** (mechanic executions) → **Answers** (responses with photos/scores)
|
||||||
|
|
||||||
|
- **Permissions**: `ChecklistPermission` table controls mechanic access (empty = global access)
|
||||||
|
- **Nested Questions**: 5-level deep conditional subquestions via `parent_question_id` + `show_if_answer`
|
||||||
|
- **Scoring**: Auto-calculated from answer points, stored in `Inspection.score/percentage`
|
||||||
|
- **Question Types**: boolean, single_choice, multiple_choice, scale, text, number, date, time (config in `Question.options` JSON)
|
||||||
|
|
||||||
|
## Development Workflows
|
||||||
|
|
||||||
|
### Running Locally
|
||||||
|
```powershell
|
||||||
|
docker-compose up -d # Start all services
|
||||||
|
docker-compose logs backend # Debug backend issues
|
||||||
|
```
|
||||||
|
**URLs**: Frontend `http://localhost:5173`, Backend API `http://localhost:8000/docs`
|
||||||
|
|
||||||
|
### Database Initialization
|
||||||
|
Use `init_users.py` to create default admin/mechanic users:
|
||||||
|
```powershell
|
||||||
|
docker-compose exec backend python init_users.py
|
||||||
|
```
|
||||||
|
**Default credentials**: `admin/admin123`, `mecanico/mec123`
|
||||||
|
|
||||||
|
### Migrations
|
||||||
|
Manual SQL scripts in `migrations/` - run via:
|
||||||
|
```powershell
|
||||||
|
docker-compose exec backend python -c "
|
||||||
|
from app.core.database import engine
|
||||||
|
with open('migrations/your_migration.sql') as f:
|
||||||
|
engine.execute(f.read())
|
||||||
|
"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Building for Production
|
||||||
|
```powershell
|
||||||
|
.\build-and-push.ps1 # Builds images, pushes to Docker Hub (configured in script)
|
||||||
|
```
|
||||||
|
Then deploy with `docker-compose.prod.yml` or `docker-stack.yml` (Swarm mode)
|
||||||
|
|
||||||
|
## Project-Specific Conventions
|
||||||
|
|
||||||
|
### API Architecture (`backend/app/main.py`)
|
||||||
|
- **Single 2800+ line file** - all endpoints in main.py (no routers/controllers split)
|
||||||
|
- Auth via `get_current_user()` dependency returning `models.User`
|
||||||
|
- File uploads use boto3 S3 client configured from `app/core/config.py` (MinIO compatible)
|
||||||
|
- PDF generation inline with ReportLab (starts ~line 1208, function `generate_pdf`)
|
||||||
|
|
||||||
|
### AI Integration Modes (see `AI_FUNCTIONALITY.md`)
|
||||||
|
- `ai_mode` on Checklist: `"off"` (manual) | `"assisted"` (suggestions) | `"copilot"` (auto-complete)
|
||||||
|
- AI analysis triggered on photo upload, stored in `Answer.ai_analysis` JSON
|
||||||
|
- Webhook notifications to n8n on completion: `send_completed_inspection_to_n8n()`
|
||||||
|
|
||||||
|
### Frontend Patterns (`frontend/src/App.jsx`)
|
||||||
|
- **5400+ line single-file component** - all views in App.jsx (Login, Dashboard, Admin panels)
|
||||||
|
- Auth state in `localStorage` (token + user object)
|
||||||
|
- API calls use `fetch()` with `import.meta.env.VITE_API_URL` base
|
||||||
|
- Signature capture via `react-signature-canvas` (saved as base64)
|
||||||
|
|
||||||
|
### Permission Model
|
||||||
|
When fetching checklists:
|
||||||
|
- Admins see all checklists
|
||||||
|
- Mechanics only see checklists with either:
|
||||||
|
- No `ChecklistPermission` records (global access), OR
|
||||||
|
- A permission record linking that mechanic
|
||||||
|
- Implement via JOIN in `GET /api/checklists` endpoint
|
||||||
|
|
||||||
|
### Environment Variables
|
||||||
|
Critical settings in `.env`:
|
||||||
|
- `DATABASE_URL` - Postgres connection string
|
||||||
|
- `SECRET_KEY` - JWT signing (min 32 chars)
|
||||||
|
- `OPENAI_API_KEY` / `GEMINI_API_KEY` - Optional for AI features
|
||||||
|
- `MINIO_*` vars - S3-compatible storage (MinIO/AWS S3)
|
||||||
|
- `NOTIFICACION_ENDPOINT` - n8n webhook for inspection events
|
||||||
|
- `ALLOWED_ORIGINS` - Comma-separated CORS origins
|
||||||
|
|
||||||
|
## Key Integration Points
|
||||||
|
|
||||||
|
### S3/MinIO Storage
|
||||||
|
- Configured globally in `main.py` via `boto3.client()` + `Config(signature_version='s3v4')`
|
||||||
|
- Two buckets: `MINIO_IMAGE_BUCKET` (photos), `MINIO_PDF_BUCKET` (reports)
|
||||||
|
- Upload pattern: generate UUID filename, `s3_client.upload_fileobj()`, store URL in DB
|
||||||
|
|
||||||
|
### PDF Generation
|
||||||
|
- ReportLab library generates inspection reports with photos, signatures, scoring
|
||||||
|
- Triggered on inspection completion, stored to S3, URL saved in `Inspection.pdf_url`
|
||||||
|
- Uses checklist logo from `Checklist.logo_url` if available
|
||||||
|
|
||||||
|
### Webhook Notifications
|
||||||
|
When `Question.send_notification = true`, answering triggers `send_answer_notification()` to `NOTIFICACION_ENDPOINT`
|
||||||
|
On inspection completion, sends full data via `send_completed_inspection_to_n8n()`
|
||||||
|
|
||||||
|
## Common Gotchas
|
||||||
|
|
||||||
|
- **No Alembic auto-migrations** - use manual SQL scripts in `migrations/`
|
||||||
|
- **Token expiry is 7 days** - set in `config.ACCESS_TOKEN_EXPIRE_MINUTES = 10080`
|
||||||
|
- **Photos stored as S3 URLs** - not base64 in DB (except signatures)
|
||||||
|
- **Nested questions limited to 5 levels** - enforced in `Question.depth_level`
|
||||||
|
- **PowerShell scripts** - Windows-first project (see `.ps1` build scripts)
|
||||||
|
- **Frontend has no state management** - uses React `useState` only, no Redux/Context
|
||||||
|
|
||||||
|
## Checklist Management
|
||||||
|
|
||||||
|
### Editing Checklists
|
||||||
|
- Admins can edit checklist name, description, AI mode, and scoring settings via "✏️ Editar" button
|
||||||
|
- Edit modal (`showEditChecklistModal`) uses PUT `/api/checklists/{id}` endpoint
|
||||||
|
- Backend endpoint supports partial updates via `exclude_unset=True` in Pydantic model
|
||||||
|
- Logo and permissions have separate management modals for focused UI
|
||||||
|
|
||||||
|
## Example Tasks
|
||||||
|
|
||||||
|
**Add new question type**: Update `Question.type` validation, modify `QuestionTypeEditor.jsx` UI, handle in `QuestionAnswerInput.jsx`
|
||||||
|
**Change scoring logic**: Edit `backend/app/main.py` answer submission endpoint, recalculate `Inspection.score`
|
||||||
|
**Add new user role**: Update `User.role` enum, modify `get_current_user()` checks, adjust frontend role conditionals in `App.jsx`
|
||||||
|
**Edit checklist properties**: Use existing PUT endpoint, add fields to `editChecklistData` state, update modal form
|
||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -29,6 +29,8 @@ dist-ssr/
|
|||||||
*.swp
|
*.swp
|
||||||
*.swo
|
*.swo
|
||||||
*~
|
*~
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
|
||||||
# OS
|
# OS
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|||||||
207
MIGRATION_GUIDE.md
Normal file
207
MIGRATION_GUIDE.md
Normal file
@@ -0,0 +1,207 @@
|
|||||||
|
# Guía de Migración a TypeScript + shadcn/ui
|
||||||
|
|
||||||
|
## ✅ Sprint 0 Completado - Setup Inicial
|
||||||
|
|
||||||
|
Se han creado los siguientes archivos de configuración:
|
||||||
|
|
||||||
|
### Archivos TypeScript
|
||||||
|
- ✅ `frontend/tsconfig.json` - Configuración principal de TypeScript
|
||||||
|
- ✅ `frontend/tsconfig.node.json` - Configuración para archivos de Node (vite.config)
|
||||||
|
- ✅ `frontend/vite.config.ts` - Vite config migrado a TypeScript
|
||||||
|
- ✅ `frontend/tailwind.config.ts` - Tailwind config migrado a TypeScript
|
||||||
|
- ✅ `frontend/src/vite-env.d.ts` - Types para variables de entorno
|
||||||
|
|
||||||
|
### shadcn/ui
|
||||||
|
- ✅ `frontend/components.json` - Configuración de shadcn/ui
|
||||||
|
- ✅ `frontend/src/lib/utils.ts` - Helper cn() para clases de Tailwind
|
||||||
|
|
||||||
|
## 📋 Siguientes Pasos
|
||||||
|
|
||||||
|
### 1. Instalar Dependencias
|
||||||
|
|
||||||
|
Ejecuta en PowerShell desde `c:\Users\ADM\Downloads\checklist-mvp\frontend`:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# Instalar TypeScript y tipos
|
||||||
|
npm install -D typescript @types/react @types/react-dom @types/node
|
||||||
|
npm install -D @typescript-eslint/eslint-plugin @typescript-eslint/parser
|
||||||
|
|
||||||
|
# Instalar shadcn/ui y dependencias
|
||||||
|
npm install @radix-ui/react-avatar @radix-ui/react-dialog @radix-ui/react-dropdown-menu
|
||||||
|
npm install @radix-ui/react-label @radix-ui/react-select @radix-ui/react-separator
|
||||||
|
npm install @radix-ui/react-slot @radix-ui/react-tabs @radix-ui/react-toast
|
||||||
|
npm install class-variance-authority tailwind-merge tailwindcss-animate vaul
|
||||||
|
|
||||||
|
# Verificar que ya tienes instalados (según package.json actual)
|
||||||
|
# - clsx (ya instalado)
|
||||||
|
# - lucide-react (ya instalado)
|
||||||
|
# - react 18.2.0 (ya instalado)
|
||||||
|
# - tailwindcss (ya instalado)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Instalar Componentes shadcn/ui
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# Ejecutar desde frontend/
|
||||||
|
npx shadcn-ui@latest add button
|
||||||
|
npx shadcn-ui@latest add card
|
||||||
|
npx shadcn-ui@latest add badge
|
||||||
|
npx shadcn-ui@latest add input
|
||||||
|
npx shadcn-ui@latest add select
|
||||||
|
npx shadcn-ui@latest add dialog
|
||||||
|
npx shadcn-ui@latest add table
|
||||||
|
npx shadcn-ui@latest add tabs
|
||||||
|
npx shadcn-ui@latest add toast
|
||||||
|
npx shadcn-ui@latest add separator
|
||||||
|
npx shadcn-ui@latest add dropdown-menu
|
||||||
|
```
|
||||||
|
|
||||||
|
Esto creará automáticamente los componentes en `frontend/src/components/ui/`
|
||||||
|
|
||||||
|
### 3. Actualizar CSS (index.css)
|
||||||
|
|
||||||
|
Agregar al **inicio** de `frontend/src/index.css` (antes del código existente):
|
||||||
|
|
||||||
|
```css
|
||||||
|
@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: 221.2 83.2% 53.3%;
|
||||||
|
--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: 221.2 83.2% 53.3%;
|
||||||
|
--radius: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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: 217.2 91.2% 59.8%;
|
||||||
|
--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: 224.3 76.3% 48%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
* {
|
||||||
|
@apply border-border;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
@apply bg-background text-foreground;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Renombrar Archivos (IMPORTANTE)
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# Renombrar main.jsx a main.tsx
|
||||||
|
Rename-Item frontend/src/main.jsx frontend/src/main.tsx
|
||||||
|
|
||||||
|
# Eliminar archivos .js antiguos (ya tenemos .ts)
|
||||||
|
Remove-Item frontend/vite.config.js
|
||||||
|
Remove-Item frontend/tailwind.config.js
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Actualizar index.html
|
||||||
|
|
||||||
|
Cambiar la línea del script en `frontend/index.html`:
|
||||||
|
|
||||||
|
**De:**
|
||||||
|
```html
|
||||||
|
<script type="module" src="/src/main.jsx"></script>
|
||||||
|
```
|
||||||
|
|
||||||
|
**A:**
|
||||||
|
```html
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. Verificar que Compila
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
cd frontend
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Si todo está bien, deberías ver:
|
||||||
|
```
|
||||||
|
VITE v5.0.11 ready in XXX ms
|
||||||
|
|
||||||
|
➜ Local: http://localhost:5173/
|
||||||
|
➜ Network: use --host to expose
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7. Crear Rama Git
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# Desde la raíz del proyecto
|
||||||
|
git checkout -b feature/typescript-shadcn-migration
|
||||||
|
git add frontend/tsconfig.json frontend/tsconfig.node.json
|
||||||
|
git add frontend/vite.config.ts frontend/tailwind.config.ts
|
||||||
|
git add frontend/components.json frontend/src/vite-env.d.ts
|
||||||
|
git add frontend/src/lib/utils.ts MIGRATION_GUIDE.md
|
||||||
|
git commit -m "feat(frontend): configure TypeScript + shadcn/ui setup
|
||||||
|
|
||||||
|
- Add tsconfig.json with strict mode
|
||||||
|
- Configure path aliases (@/*)
|
||||||
|
- Add shadcn/ui configuration
|
||||||
|
- Create lib/utils.ts helper
|
||||||
|
- Add TypeScript environment types
|
||||||
|
|
||||||
|
Backend version: sin cambios
|
||||||
|
Frontend version: 1.4.0 -> 1.5.0-beta"
|
||||||
|
git push -u origin feature/typescript-shadcn-migration
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🎯 Estado Actual
|
||||||
|
|
||||||
|
- ✅ Configuración TypeScript creada
|
||||||
|
- ✅ Configuración shadcn/ui creada
|
||||||
|
- ⏳ Pendiente: Instalar dependencias npm
|
||||||
|
- ⏳ Pendiente: Renombrar archivos
|
||||||
|
- ⏳ Pendiente: Instalar componentes shadcn
|
||||||
|
- ⏳ Pendiente: Actualizar index.css
|
||||||
|
- ⏳ Pendiente: Verificar compilación
|
||||||
|
|
||||||
|
## 📝 Notas
|
||||||
|
|
||||||
|
- El sistema actual (App.jsx) seguirá funcionando mientras no se renombre a .tsx
|
||||||
|
- Los archivos TypeScript coexisten con JavaScript hasta migración completa
|
||||||
|
- shadcn/ui se puede usar inmediatamente después de instalar componentes
|
||||||
|
- No hay cambios en el backend por ahora
|
||||||
|
|
||||||
|
## 🚀 Siguiente Sprint
|
||||||
|
|
||||||
|
Una vez completados estos pasos, continuaremos con:
|
||||||
|
- Sprint 1: Crear types para el módulo de Checklists
|
||||||
|
- Sprint 2: Backend del módulo de Recambios
|
||||||
|
- Sprint 3: Frontend del módulo de Recambios con TypeScript + shadcn
|
||||||
410
README_RECAMBIOS.md
Normal file
410
README_RECAMBIOS.md
Normal file
@@ -0,0 +1,410 @@
|
|||||||
|
# 📦 Sistema de Gestión de Recambios - Feature Branch
|
||||||
|
|
||||||
|
> **Rama**: `feature/typescript-shadcn-migration`
|
||||||
|
> **Estado**: 🚧 En Desarrollo
|
||||||
|
> **Stack**: TypeScript + shadcn/ui + React 18 + FastAPI
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 Objetivo de esta Rama
|
||||||
|
|
||||||
|
Desarrollar un **Sistema Integral de Gestión de Recambios** para talleres mecánicos, integrado con el sistema existente de checklists. Este módulo añade capacidades completas de gestión de pedidos, proveedores, inventario y facturación.
|
||||||
|
|
||||||
|
## 🏗️ Arquitectura del Módulo
|
||||||
|
|
||||||
|
### Frontend (TypeScript + shadcn/ui)
|
||||||
|
|
||||||
|
```
|
||||||
|
frontend/src/
|
||||||
|
├── modules/
|
||||||
|
│ └── recambios/
|
||||||
|
│ ├── RecambiosApp.tsx ✅ Componente principal con navegación
|
||||||
|
│ ├── pages/
|
||||||
|
│ │ ├── KanbanPedidos.tsx 🚧 Tablero Kanban de estados
|
||||||
|
│ │ ├── PanelProveedor.tsx 🚧 Gestión de proveedores
|
||||||
|
│ │ ├── GestionAlbaranes.tsx 🚧 Registro de albaranes
|
||||||
|
│ │ ├── GestionClientes.tsx 🚧 Base de datos clientes
|
||||||
|
│ │ ├── Inventario.tsx 🚧 Control de stock
|
||||||
|
│ │ └── Configuracion.tsx 🚧 Ajustes del módulo
|
||||||
|
│ ├── components/
|
||||||
|
│ │ ├── PedidoCard.tsx 🚧 Tarjeta de pedido
|
||||||
|
│ │ ├── ProveedorForm.tsx 🚧 Formulario proveedor
|
||||||
|
│ │ ├── AlbaranForm.tsx 🚧 Formulario albarán
|
||||||
|
│ │ └── EstadoBadge.tsx 🚧 Badge de estado
|
||||||
|
│ └── types/
|
||||||
|
│ └── recambios.types.ts 🚧 Interfaces TypeScript
|
||||||
|
├── components/ui/ ✅ shadcn/ui components
|
||||||
|
│ ├── button.tsx
|
||||||
|
│ ├── card.tsx
|
||||||
|
│ ├── badge.tsx
|
||||||
|
│ └── tabs.tsx
|
||||||
|
└── lib/
|
||||||
|
└── utils.ts ✅ Utilidades (cn helper)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Backend (FastAPI + SQLAlchemy)
|
||||||
|
|
||||||
|
```
|
||||||
|
backend/app/
|
||||||
|
├── modules/
|
||||||
|
│ └── recambios/
|
||||||
|
│ ├── __init__.py
|
||||||
|
│ ├── models.py 🚧 15+ modelos SQLAlchemy
|
||||||
|
│ ├── schemas.py 🚧 Pydantic schemas
|
||||||
|
│ ├── routes.py 🚧 Endpoints REST
|
||||||
|
│ └── services.py 🚧 Lógica de negocio
|
||||||
|
└── migrations/
|
||||||
|
└── add_recambios_tables.sql 🚧 Schema inicial
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 Modelo de Datos
|
||||||
|
|
||||||
|
### Entidades Principales
|
||||||
|
|
||||||
|
#### 1. **rec_clientes**
|
||||||
|
```sql
|
||||||
|
- id, nombre, telefono, email, direccion, cif_nif
|
||||||
|
- vehiculos_asociados (JSON)
|
||||||
|
- historial_pedidos
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. **rec_proveedores**
|
||||||
|
```sql
|
||||||
|
- id, nombre, contacto, telefono, email
|
||||||
|
- plazo_entrega_dias, condiciones_pago
|
||||||
|
- rating, notas
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. **rec_referencias**
|
||||||
|
```sql
|
||||||
|
- id, codigo_referencia, descripcion
|
||||||
|
- proveedor_id, precio_compra, precio_venta
|
||||||
|
- estado (13 estados posibles)
|
||||||
|
- stock_actual, ubicacion_almacen
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 4. **rec_pedidos_cliente**
|
||||||
|
```sql
|
||||||
|
- id, cliente_id, fecha_pedido, total
|
||||||
|
- estado, prioridad, asesor_id
|
||||||
|
- vehiculo_datos (JSON)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 5. **rec_lineas_pedido**
|
||||||
|
```sql
|
||||||
|
- id, pedido_cliente_id, referencia_id
|
||||||
|
- cantidad, precio_unitario, subtotal
|
||||||
|
- estado_linea
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 6. **rec_pedidos_proveedor**
|
||||||
|
```sql
|
||||||
|
- id, proveedor_id, fecha_pedido
|
||||||
|
- estado, total_pedido, recambista_id
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 7. **rec_albaranes**
|
||||||
|
```sql
|
||||||
|
- id, pedido_proveedor_id, numero_albaran
|
||||||
|
- fecha_recepcion, recibido_por
|
||||||
|
- observaciones, foto_albaran_url
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 8. **rec_devoluciones**
|
||||||
|
```sql
|
||||||
|
- id, referencia_id, motivo, estado
|
||||||
|
- fecha_devolucion, fecha_abono
|
||||||
|
- importe_abonado
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 9. **rec_movimientos_stock**
|
||||||
|
```sql
|
||||||
|
- id, referencia_id, tipo_movimiento
|
||||||
|
- cantidad, fecha, usuario_id
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 10. **rec_facturas**
|
||||||
|
```sql
|
||||||
|
- id, cliente_id, numero_factura
|
||||||
|
- fecha_emision, total, estado
|
||||||
|
- archivo_pdf_url
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔄 Máquina de Estados (13 Estados)
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────┐
|
||||||
|
│ pendiente_decidir │ ← Estado inicial
|
||||||
|
└──────────┬──────────┘
|
||||||
|
│
|
||||||
|
├─→ pedido_proveedor
|
||||||
|
├─→ alternativa
|
||||||
|
├─→ usado
|
||||||
|
└─→ descartada
|
||||||
|
│
|
||||||
|
┌───────────┴───────────┐
|
||||||
|
│ │
|
||||||
|
▼ ▼
|
||||||
|
recibida recibida_parcial
|
||||||
|
│ │
|
||||||
|
├─→ devolucion │
|
||||||
|
├─→ entregada ──────────┘
|
||||||
|
└─→ instalada
|
||||||
|
│
|
||||||
|
└─→ facturada
|
||||||
|
```
|
||||||
|
|
||||||
|
### Transiciones de Estado
|
||||||
|
|
||||||
|
1. **pendiente_decidir** → Usuario decide acción
|
||||||
|
2. **pedido_proveedor** → Pedido enviado al proveedor
|
||||||
|
3. **alternativa** → Buscando opción alternativa
|
||||||
|
4. **usado** → Referencia de segunda mano
|
||||||
|
5. **recibida** → Llegó completa del proveedor
|
||||||
|
6. **recibida_parcial** → Llegó solo parte del pedido
|
||||||
|
7. **devolucion** → En proceso de devolución
|
||||||
|
8. **entregada** → Entregada al cliente
|
||||||
|
9. **instalada** → Instalada en el vehículo
|
||||||
|
10. **facturada** → Facturada al cliente
|
||||||
|
11. **descartada** → No se procede con la referencia
|
||||||
|
12. **pendiente_abono** → Esperando abono de devolución
|
||||||
|
13. **abonada** → Devolución abonada
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 👥 Roles y Permisos
|
||||||
|
|
||||||
|
### Asesor de Servicio
|
||||||
|
- ✅ Crear pedidos de cliente
|
||||||
|
- ✅ Consultar estado de referencias
|
||||||
|
- ✅ Modificar prioridades
|
||||||
|
- ✅ Ver historial de cliente
|
||||||
|
|
||||||
|
### Recambista
|
||||||
|
- ✅ Gestionar pedidos a proveedores
|
||||||
|
- ✅ Registrar albaranes
|
||||||
|
- ✅ Gestionar stock
|
||||||
|
- ✅ Actualizar estados de referencias
|
||||||
|
- ✅ Procesar devoluciones
|
||||||
|
|
||||||
|
### Administración
|
||||||
|
- ✅ Generar facturas
|
||||||
|
- ✅ Ver reportes financieros
|
||||||
|
- ✅ Gestionar proveedores
|
||||||
|
- ✅ Configurar precios
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎨 Diseño UI con shadcn/ui
|
||||||
|
|
||||||
|
### Componentes Implementados ✅
|
||||||
|
|
||||||
|
- **Button** - Botones con variantes (default, outline, ghost, destructive)
|
||||||
|
- **Card** - Tarjetas con header, content, footer
|
||||||
|
- **Badge** - Etiquetas de estado con colores
|
||||||
|
- **Tabs** - Navegación entre secciones
|
||||||
|
|
||||||
|
### Componentes Pendientes 🚧
|
||||||
|
|
||||||
|
- **Input** - Campos de texto
|
||||||
|
- **Select** - Dropdowns
|
||||||
|
- **Dialog** - Modales
|
||||||
|
- **Table** - Tablas de datos
|
||||||
|
- **Toast** - Notificaciones
|
||||||
|
- **Dropdown Menu** - Menús contextuales
|
||||||
|
- **Date Picker** - Selector de fechas
|
||||||
|
- **Form** - Formularios validados
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 Plan de Implementación
|
||||||
|
|
||||||
|
### ✅ Sprint 0: Fundación (COMPLETADO)
|
||||||
|
- [x] Configuración TypeScript
|
||||||
|
- [x] Setup shadcn/ui
|
||||||
|
- [x] Componentes base UI
|
||||||
|
- [x] Estructura de carpetas
|
||||||
|
- [x] Prototipo Kanban visual
|
||||||
|
|
||||||
|
### 🚧 Sprint 1: Backend Core (EN PROGRESO)
|
||||||
|
- [ ] Modelos SQLAlchemy (15 tablas)
|
||||||
|
- [ ] Migraciones de base de datos
|
||||||
|
- [ ] Schemas Pydantic
|
||||||
|
- [ ] Endpoints CRUD básicos
|
||||||
|
- [ ] Autenticación y permisos
|
||||||
|
|
||||||
|
### 🔜 Sprint 2: Frontend Básico
|
||||||
|
- [ ] Páginas principales (6 tabs)
|
||||||
|
- [ ] Formularios con validación
|
||||||
|
- [ ] Integración API
|
||||||
|
- [ ] Gestión de estado
|
||||||
|
- [ ] Tipos TypeScript completos
|
||||||
|
|
||||||
|
### 🔜 Sprint 3: Funcionalidades Avanzadas
|
||||||
|
- [ ] Kanban drag & drop
|
||||||
|
- [ ] Upload de fotos (albaranes)
|
||||||
|
- [ ] Generación de PDFs
|
||||||
|
- [ ] Sistema de notificaciones
|
||||||
|
- [ ] Búsqueda y filtros
|
||||||
|
|
||||||
|
### 🔜 Sprint 4: Integración y Testing
|
||||||
|
- [ ] Integración con sistema de checklists
|
||||||
|
- [ ] Tests unitarios
|
||||||
|
- [ ] Tests E2E
|
||||||
|
- [ ] Optimización de rendimiento
|
||||||
|
- [ ] Documentación completa
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Comandos de Desarrollo
|
||||||
|
|
||||||
|
### Levantar Entorno de Desarrollo
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# Frontend
|
||||||
|
cd frontend
|
||||||
|
npm run dev
|
||||||
|
|
||||||
|
# Backend
|
||||||
|
cd backend
|
||||||
|
docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
### Ver el Módulo de Recambios
|
||||||
|
|
||||||
|
1. Inicia sesión como **admin**
|
||||||
|
2. Haz clic en **📦 Recambios** en el sidebar
|
||||||
|
3. Navega entre las 6 pestañas
|
||||||
|
|
||||||
|
### Compilar TypeScript
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
cd frontend
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔧 Tecnologías Utilizadas
|
||||||
|
|
||||||
|
### Frontend
|
||||||
|
- **React 18** - Framework UI
|
||||||
|
- **TypeScript 5.9** - Type safety
|
||||||
|
- **shadcn/ui** - Component library
|
||||||
|
- **Radix UI** - Primitive components
|
||||||
|
- **Tailwind CSS 3.4** - Utility-first CSS
|
||||||
|
- **Lucide React** - Icon library
|
||||||
|
- **Vite 5** - Build tool
|
||||||
|
|
||||||
|
### Backend
|
||||||
|
- **FastAPI** - Python web framework
|
||||||
|
- **SQLAlchemy** - ORM
|
||||||
|
- **PostgreSQL 15** - Base de datos
|
||||||
|
- **Pydantic** - Data validation
|
||||||
|
- **JWT** - Autenticación
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 Convenciones de Código
|
||||||
|
|
||||||
|
### TypeScript
|
||||||
|
```typescript
|
||||||
|
// Interfaces con prefijo 'I' o sufijo del dominio
|
||||||
|
interface Pedido {
|
||||||
|
id: number;
|
||||||
|
cliente: string;
|
||||||
|
estado: EstadoPedido;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tipos para estados
|
||||||
|
type EstadoPedido = 'pendiente_decidir' | 'pedido_proveedor' | 'recibida';
|
||||||
|
|
||||||
|
// Componentes con tipo React.FC
|
||||||
|
const PedidoCard: React.FC<PedidoCardProps> = ({ pedido }) => { ... }
|
||||||
|
```
|
||||||
|
|
||||||
|
### Python
|
||||||
|
```python
|
||||||
|
# Modelos con tabla explícita
|
||||||
|
class RecCliente(Base):
|
||||||
|
__tablename__ = "rec_clientes"
|
||||||
|
|
||||||
|
# Schemas con validación
|
||||||
|
class PedidoCreate(BaseModel):
|
||||||
|
cliente_id: int
|
||||||
|
referencias: List[LineaPedido]
|
||||||
|
|
||||||
|
# Rutas con prefijo /api/recambios
|
||||||
|
@router.post("/api/recambios/pedidos")
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🐛 Debugging
|
||||||
|
|
||||||
|
### Frontend
|
||||||
|
```powershell
|
||||||
|
# Ver errores TypeScript
|
||||||
|
npm run dev
|
||||||
|
|
||||||
|
# Limpiar caché
|
||||||
|
rm -r node_modules .vite
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
### Backend
|
||||||
|
```powershell
|
||||||
|
# Ver logs
|
||||||
|
docker-compose logs -f backend
|
||||||
|
|
||||||
|
# Entrar al contenedor
|
||||||
|
docker-compose exec backend bash
|
||||||
|
|
||||||
|
# Verificar tablas
|
||||||
|
docker-compose exec postgres psql -U user -d checklist_db -c "\dt rec_*"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📚 Documentación Relacionada
|
||||||
|
|
||||||
|
- [MIGRATION_GUIDE.md](./MIGRATION_GUIDE.md) - Guía de migración TypeScript
|
||||||
|
- [AI_FUNCTIONALITY.md](./AI_FUNCTIONALITY.md) - Sistema de IA (checklists)
|
||||||
|
- [API_DOCUMENTATION.md](./API_DOCUMENTATION.md) - Documentación API
|
||||||
|
- [shadcn/ui Docs](https://ui.shadcn.com/) - Documentación componentes
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 Próximos Pasos
|
||||||
|
|
||||||
|
1. **Crear modelos de backend** (15 tablas)
|
||||||
|
2. **Escribir schemas Pydantic**
|
||||||
|
3. **Implementar endpoints CRUD**
|
||||||
|
4. **Desarrollar páginas TypeScript**
|
||||||
|
5. **Conectar frontend con API**
|
||||||
|
6. **Agregar drag & drop al Kanban**
|
||||||
|
7. **Implementar búsqueda y filtros**
|
||||||
|
8. **Testing completo**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 👨💻 Contribución
|
||||||
|
|
||||||
|
Esta es una rama de desarrollo activo. Para contribuir:
|
||||||
|
|
||||||
|
1. Crea una sub-rama desde `feature/typescript-shadcn-migration`
|
||||||
|
2. Implementa tu funcionalidad
|
||||||
|
3. Haz commit con mensajes descriptivos
|
||||||
|
4. Crea PR hacia esta rama (NO hacia main)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📞 Contacto
|
||||||
|
|
||||||
|
**Proyecto**: Ayutec - Sistema Inteligente de Inspecciones
|
||||||
|
**Versión Recambios**: 0.1.0 (Alpha)
|
||||||
|
**Última Actualización**: Diciembre 2025
|
||||||
209
TIMEZONE_SETUP.md
Normal file
209
TIMEZONE_SETUP.md
Normal file
@@ -0,0 +1,209 @@
|
|||||||
|
# Configuración de Zona Horaria - Atlantic/Canary
|
||||||
|
|
||||||
|
## Cambios Implementados
|
||||||
|
|
||||||
|
Se ha configurado la zona horaria de **Atlantic/Canary (Islas Canarias, España)** en toda la aplicación:
|
||||||
|
|
||||||
|
### 1. Base de Datos PostgreSQL
|
||||||
|
- **Zona horaria**: `Atlantic/Canary` (UTC+0 en invierno, UTC+1 en verano con horario de verano)
|
||||||
|
- Variables de entorno agregadas:
|
||||||
|
- `TZ=Atlantic/Canary`
|
||||||
|
- `PGTZ=Atlantic/Canary`
|
||||||
|
|
||||||
|
### 2. Backend FastAPI
|
||||||
|
- Configuración de zona horaria de Python al inicio de la aplicación
|
||||||
|
- Conexión a PostgreSQL configurada con timezone
|
||||||
|
- Event listener para establecer timezone en cada conexión
|
||||||
|
- Variable de entorno: `TZ=Atlantic/Canary`
|
||||||
|
|
||||||
|
### 3. Frontend React
|
||||||
|
- Los filtros de fecha usan el constructor de Date con zona horaria local
|
||||||
|
- Las fechas se muestran en formato español (es-ES)
|
||||||
|
|
||||||
|
## Aplicar los Cambios
|
||||||
|
|
||||||
|
### Desarrollo Local
|
||||||
|
|
||||||
|
1. **Parar los contenedores actuales**:
|
||||||
|
```powershell
|
||||||
|
docker-compose down
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Aplicar la migración de zona horaria a la base de datos**:
|
||||||
|
```powershell
|
||||||
|
docker-compose up -d postgres
|
||||||
|
|
||||||
|
# Esperar a que PostgreSQL esté listo
|
||||||
|
Start-Sleep -Seconds 5
|
||||||
|
|
||||||
|
# Aplicar migración
|
||||||
|
docker-compose exec postgres psql -U checklist_user -d checklist_db -f /docker-entrypoint-initdb.d/../migrations/set_timezone_canary.sql
|
||||||
|
```
|
||||||
|
|
||||||
|
**Alternativa manual**:
|
||||||
|
```powershell
|
||||||
|
# Copiar el archivo SQL al contenedor
|
||||||
|
docker cp migrations/set_timezone_canary.sql checklist-db:/tmp/
|
||||||
|
|
||||||
|
# Ejecutarlo
|
||||||
|
docker-compose exec postgres psql -U checklist_user -d checklist_db -f /tmp/set_timezone_canary.sql
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Reconstruir y levantar todos los servicios**:
|
||||||
|
```powershell
|
||||||
|
docker-compose build
|
||||||
|
docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Verificar la configuración**:
|
||||||
|
```powershell
|
||||||
|
# Verificar timezone en PostgreSQL
|
||||||
|
docker-compose exec postgres psql -U checklist_user -d checklist_db -c "SHOW timezone;"
|
||||||
|
|
||||||
|
# Verificar timezone en backend
|
||||||
|
docker-compose exec backend python -c "import time; print(time.tzname)"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Producción
|
||||||
|
|
||||||
|
#### Docker Compose Production
|
||||||
|
|
||||||
|
1. **Parar servicios**:
|
||||||
|
```bash
|
||||||
|
docker-compose -f docker-compose.prod.yml down
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Aplicar migración**:
|
||||||
|
```bash
|
||||||
|
docker-compose -f docker-compose.prod.yml up -d postgres
|
||||||
|
docker cp migrations/set_timezone_canary.sql syntria-db-prod:/tmp/
|
||||||
|
docker-compose -f docker-compose.prod.yml exec postgres psql -U syntria_user -d syntria_db -f /tmp/set_timezone_canary.sql
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Reconstruir imágenes y desplegar**:
|
||||||
|
```bash
|
||||||
|
./build-and-push.sh # o build-and-push.ps1 en Windows
|
||||||
|
docker-compose -f docker-compose.prod.yml pull
|
||||||
|
docker-compose -f docker-compose.prod.yml up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Docker Swarm
|
||||||
|
|
||||||
|
1. **Aplicar migración en el nodo manager**:
|
||||||
|
```bash
|
||||||
|
# Encontrar el contenedor de PostgreSQL
|
||||||
|
docker ps | grep syntria_db
|
||||||
|
|
||||||
|
# Copiar y ejecutar migración
|
||||||
|
docker cp migrations/set_timezone_canary.sql <container_id>:/tmp/
|
||||||
|
docker exec <container_id> psql -U syntria_user -d syntria_db -f /tmp/set_timezone_canary.sql
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Actualizar stack**:
|
||||||
|
```bash
|
||||||
|
docker stack deploy -c docker-stack.yml syntria
|
||||||
|
```
|
||||||
|
|
||||||
|
## Verificación Post-Despliegue
|
||||||
|
|
||||||
|
### 1. Verificar Zona Horaria de PostgreSQL
|
||||||
|
```sql
|
||||||
|
-- Debería mostrar: Atlantic/Canary
|
||||||
|
SHOW timezone;
|
||||||
|
|
||||||
|
-- Verificar hora actual del servidor
|
||||||
|
SELECT NOW();
|
||||||
|
SELECT CURRENT_TIMESTAMP;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Verificar Backend
|
||||||
|
```bash
|
||||||
|
docker-compose exec backend python -c "
|
||||||
|
from datetime import datetime
|
||||||
|
import time
|
||||||
|
print('Timezone:', time.tzname)
|
||||||
|
print('Hora actual:', datetime.now())
|
||||||
|
print('UTC:', datetime.utcnow())
|
||||||
|
"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Probar en la Aplicación
|
||||||
|
1. Crear una nueva inspección
|
||||||
|
2. Verificar que la fecha/hora de inicio coincida con la hora local de Canarias
|
||||||
|
3. Filtrar por fecha y verificar que el filtro funcione correctamente
|
||||||
|
|
||||||
|
## Comportamiento de las Fechas
|
||||||
|
|
||||||
|
### Fechas Existentes
|
||||||
|
- Las fechas ya guardadas en la base de datos **no se modifican**
|
||||||
|
- PostgreSQL las almacena internamente en UTC
|
||||||
|
- Se mostrarán en hora de Canarias al consultarlas
|
||||||
|
|
||||||
|
### Nuevas Fechas
|
||||||
|
- Se guardarán con timezone de Canarias
|
||||||
|
- Se convertirán automáticamente a UTC para almacenamiento
|
||||||
|
- Se mostrarán en hora de Canarias al recuperarlas
|
||||||
|
|
||||||
|
## Horario de Verano
|
||||||
|
|
||||||
|
La zona horaria `Atlantic/Canary` maneja automáticamente el horario de verano:
|
||||||
|
- **Invierno (octubre - marzo)**: UTC+0 / WET (Western European Time)
|
||||||
|
- **Verano (marzo - octubre)**: UTC+1 / WEST (Western European Summer Time)
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Las fechas siguen mostrándose incorrectas
|
||||||
|
|
||||||
|
1. Verificar que los contenedores se reiniciaron después de los cambios:
|
||||||
|
```powershell
|
||||||
|
docker-compose restart
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Verificar logs del backend:
|
||||||
|
```powershell
|
||||||
|
docker-compose logs backend | Select-String -Pattern "timezone|TZ"
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Limpiar caché del navegador y recargar la aplicación
|
||||||
|
|
||||||
|
### Error al conectar a la base de datos
|
||||||
|
|
||||||
|
Si ves errores relacionados con timezone al conectar:
|
||||||
|
```
|
||||||
|
could not find timezone "Atlantic/Canary"
|
||||||
|
```
|
||||||
|
|
||||||
|
Solución:
|
||||||
|
```bash
|
||||||
|
# Entrar al contenedor de PostgreSQL
|
||||||
|
docker-compose exec postgres sh
|
||||||
|
|
||||||
|
# Instalar datos de zona horaria (si no están instalados)
|
||||||
|
apk add --no-cache tzdata
|
||||||
|
|
||||||
|
# Salir y reiniciar
|
||||||
|
exit
|
||||||
|
docker-compose restart postgres
|
||||||
|
```
|
||||||
|
|
||||||
|
### Las fechas en el frontend no coinciden
|
||||||
|
|
||||||
|
El frontend usa la zona horaria del navegador del usuario. Si el usuario está en una zona diferente a Canarias, verá las fechas en su hora local. Para forzar visualización en hora de Canarias en el frontend, se pueden usar bibliotecas como `date-fns-tz` o `luxon`.
|
||||||
|
|
||||||
|
## Archivos Modificados
|
||||||
|
|
||||||
|
- ✅ `docker-compose.yml` - Variables TZ para postgres y backend
|
||||||
|
- ✅ `docker-compose.prod.yml` - Variables TZ para postgres y backend
|
||||||
|
- ✅ `docker-stack.yml` - Variables TZ para db y backend
|
||||||
|
- ✅ `backend/app/core/database.py` - Configuración de conexión con timezone
|
||||||
|
- ✅ `backend/app/main.py` - Configuración de TZ de Python
|
||||||
|
- ✅ `migrations/set_timezone_canary.sql` - Script de migración
|
||||||
|
- ✅ `frontend/src/App.jsx` - Uso correcto de fechas locales
|
||||||
|
|
||||||
|
## Notas Importantes
|
||||||
|
|
||||||
|
⚠️ **Después de aplicar estos cambios en producción**:
|
||||||
|
- Hacer backup de la base de datos antes de aplicar cambios
|
||||||
|
- Aplicar en horario de bajo tráfico
|
||||||
|
- Verificar que todas las funcionalidades de fecha/hora funcionan correctamente
|
||||||
|
- Informar a los usuarios si hay cambios visibles en las fechas mostradas
|
||||||
40
apply-timezone.bat
Normal file
40
apply-timezone.bat
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
@echo off
|
||||||
|
echo ====================================
|
||||||
|
echo Aplicando configuracion de timezone
|
||||||
|
echo ====================================
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo 1. Copiando script de migracion...
|
||||||
|
docker cp migrations/force_timezone_all_sessions.sql checklist-db:/tmp/
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo 2. Ejecutando migracion en checklist_db...
|
||||||
|
docker exec checklist-db psql -U checklist_user -d checklist_db -f /tmp/force_timezone_all_sessions.sql
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo 3. Ejecutando migracion en syntria_db (si existe)...
|
||||||
|
docker exec checklist-db psql -U checklist_user -d postgres -c "SELECT 1 FROM pg_database WHERE datname = 'syntria_db'" | find "1" >nul
|
||||||
|
if %ERRORLEVEL% equ 0 (
|
||||||
|
docker exec checklist-db psql -U syntria_user -d syntria_db -f /tmp/force_timezone_all_sessions.sql
|
||||||
|
echo Migración aplicada a syntria_db
|
||||||
|
) else (
|
||||||
|
echo syntria_db no existe, omitiendo...
|
||||||
|
)
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo 4. Recargando configuracion de PostgreSQL...
|
||||||
|
docker exec checklist-db psql -U checklist_user -d checklist_db -c "SELECT pg_reload_conf();"
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo 5. Verificando timezone...
|
||||||
|
docker exec checklist-db psql -U checklist_user -d checklist_db -c "SHOW timezone;"
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo ====================================
|
||||||
|
echo Completado!
|
||||||
|
echo ====================================
|
||||||
|
echo.
|
||||||
|
echo IMPORTANTE: Desconecta y reconecta tu cliente PostgreSQL
|
||||||
|
echo para que aplique la nueva zona horaria.
|
||||||
|
echo.
|
||||||
|
pause
|
||||||
39
apply-timezone.sh
Normal file
39
apply-timezone.sh
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
echo "===================================="
|
||||||
|
echo "Aplicando configuración de timezone"
|
||||||
|
echo "===================================="
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "1. Copiando script de migración..."
|
||||||
|
docker cp migrations/force_timezone_all_sessions.sql checklist-db:/tmp/
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "2. Ejecutando migración en checklist_db..."
|
||||||
|
docker exec checklist-db psql -U checklist_user -d checklist_db -f /tmp/force_timezone_all_sessions.sql
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "3. Ejecutando migración en syntria_db (si existe)..."
|
||||||
|
if docker exec checklist-db psql -U checklist_user -d postgres -tc "SELECT 1 FROM pg_database WHERE datname = 'syntria_db'" | grep -q 1; then
|
||||||
|
docker exec checklist-db psql -U syntria_user -d syntria_db -f /tmp/force_timezone_all_sessions.sql
|
||||||
|
echo "Migración aplicada a syntria_db"
|
||||||
|
else
|
||||||
|
echo "syntria_db no existe, omitiendo..."
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "4. Recargando configuración de PostgreSQL..."
|
||||||
|
docker exec checklist-db psql -U checklist_user -d checklist_db -c "SELECT pg_reload_conf();"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "5. Verificando timezone..."
|
||||||
|
docker exec checklist-db psql -U checklist_user -d checklist_db -c "SHOW timezone;"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "===================================="
|
||||||
|
echo "Completado!"
|
||||||
|
echo "===================================="
|
||||||
|
echo ""
|
||||||
|
echo "IMPORTANTE: Desconecta y reconecta tu cliente PostgreSQL"
|
||||||
|
echo "para que aplique la nueva zona horaria."
|
||||||
|
echo ""
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
from sqlalchemy import create_engine
|
from sqlalchemy import create_engine, event
|
||||||
from sqlalchemy.ext.declarative import declarative_base
|
from sqlalchemy.ext.declarative import declarative_base
|
||||||
from sqlalchemy.orm import sessionmaker
|
from sqlalchemy.orm import sessionmaker
|
||||||
from app.core.config import settings
|
from app.core.config import settings
|
||||||
@@ -6,9 +6,17 @@ from app.core.config import settings
|
|||||||
engine = create_engine(
|
engine = create_engine(
|
||||||
settings.DATABASE_URL,
|
settings.DATABASE_URL,
|
||||||
pool_pre_ping=True,
|
pool_pre_ping=True,
|
||||||
echo=settings.ENVIRONMENT == "development"
|
echo=settings.ENVIRONMENT == "development",
|
||||||
|
connect_args={"options": "-c timezone=Atlantic/Canary"}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Configurar zona horaria al conectar
|
||||||
|
@event.listens_for(engine, "connect")
|
||||||
|
def set_timezone(dbapi_conn, connection_record):
|
||||||
|
cursor = dbapi_conn.cursor()
|
||||||
|
cursor.execute("SET TIME ZONE 'Atlantic/Canary';")
|
||||||
|
cursor.close()
|
||||||
|
|
||||||
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||||
|
|
||||||
Base = declarative_base()
|
Base = declarative_base()
|
||||||
|
|||||||
1626
backend/app/main.py
1626
backend/app/main.py
File diff suppressed because it is too large
Load Diff
@@ -47,6 +47,7 @@ class Checklist(Base):
|
|||||||
scoring_enabled = Column(Boolean, default=True)
|
scoring_enabled = Column(Boolean, default=True)
|
||||||
max_score = Column(Integer, default=0)
|
max_score = Column(Integer, default=0)
|
||||||
logo_url = Column(String(500))
|
logo_url = Column(String(500))
|
||||||
|
generate_pdf = Column(Boolean, default=True) # Controla si se genera PDF al completar
|
||||||
is_active = Column(Boolean, default=True)
|
is_active = Column(Boolean, default=True)
|
||||||
created_by = Column(Integer, ForeignKey("users.id"))
|
created_by = Column(Integer, ForeignKey("users.id"))
|
||||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||||
@@ -70,7 +71,8 @@ class Question(Base):
|
|||||||
points = Column(Integer, default=1)
|
points = Column(Integer, default=1)
|
||||||
options = Column(JSON) # Configuración flexible según tipo de pregunta
|
options = Column(JSON) # Configuración flexible según tipo de pregunta
|
||||||
order = Column(Integer, default=0)
|
order = Column(Integer, default=0)
|
||||||
allow_photos = Column(Boolean, default=True)
|
allow_photos = Column(Boolean, default=True) # DEPRECATED: usar photo_requirement
|
||||||
|
photo_requirement = Column(String(20), default='optional') # none, optional, required
|
||||||
max_photos = Column(Integer, default=3)
|
max_photos = Column(Integer, default=3)
|
||||||
requires_comment_on_fail = Column(Boolean, default=False)
|
requires_comment_on_fail = Column(Boolean, default=False)
|
||||||
send_notification = Column(Boolean, default=False)
|
send_notification = Column(Boolean, default=False)
|
||||||
@@ -83,7 +85,11 @@ class Question(Base):
|
|||||||
# AI Analysis
|
# AI Analysis
|
||||||
ai_prompt = Column(Text, nullable=True) # Prompt personalizado para análisis de IA de esta pregunta
|
ai_prompt = Column(Text, nullable=True) # Prompt personalizado para análisis de IA de esta pregunta
|
||||||
|
|
||||||
|
# Soft Delete
|
||||||
|
is_deleted = Column(Boolean, default=False) # Soft delete: mantiene integridad de respuestas históricas
|
||||||
|
|
||||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||||
|
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
||||||
|
|
||||||
# Relationships
|
# Relationships
|
||||||
checklist = relationship("Checklist", back_populates="questions")
|
checklist = relationship("Checklist", back_populates="questions")
|
||||||
@@ -120,7 +126,7 @@ class Inspection(Base):
|
|||||||
flagged_items_count = Column(Integer, default=0)
|
flagged_items_count = Column(Integer, default=0)
|
||||||
|
|
||||||
# Estado
|
# Estado
|
||||||
status = Column(String(20), default="draft") # draft, completed, inactive
|
status = Column(String(20), default="incomplete") # incomplete, completed, inactive
|
||||||
is_active = Column(Boolean, default=True)
|
is_active = Column(Boolean, default=True)
|
||||||
|
|
||||||
# Firma
|
# Firma
|
||||||
@@ -153,6 +159,7 @@ class Answer(Base):
|
|||||||
comment = Column(Text) # Comentarios adicionales
|
comment = Column(Text) # Comentarios adicionales
|
||||||
|
|
||||||
ai_analysis = Column(JSON) # Análisis de IA si aplica
|
ai_analysis = Column(JSON) # Análisis de IA si aplica
|
||||||
|
chat_history = Column(JSON) # Historial de chat con AI Assistant (para tipo ai_assistant)
|
||||||
is_flagged = Column(Boolean, default=False) # Si requiere atención
|
is_flagged = Column(Boolean, default=False) # Si requiere atención
|
||||||
|
|
||||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||||
|
|||||||
@@ -71,6 +71,7 @@ class ChecklistBase(BaseModel):
|
|||||||
ai_mode: str = "off"
|
ai_mode: str = "off"
|
||||||
scoring_enabled: bool = True
|
scoring_enabled: bool = True
|
||||||
logo_url: Optional[str] = None
|
logo_url: Optional[str] = None
|
||||||
|
generate_pdf: bool = True
|
||||||
|
|
||||||
class ChecklistCreate(ChecklistBase):
|
class ChecklistCreate(ChecklistBase):
|
||||||
mechanic_ids: Optional[List[int]] = [] # IDs de mecánicos autorizados
|
mechanic_ids: Optional[List[int]] = [] # IDs de mecánicos autorizados
|
||||||
@@ -81,12 +82,14 @@ class ChecklistUpdate(BaseModel):
|
|||||||
ai_mode: Optional[str] = None
|
ai_mode: Optional[str] = None
|
||||||
scoring_enabled: Optional[bool] = None
|
scoring_enabled: Optional[bool] = None
|
||||||
logo_url: Optional[str] = None
|
logo_url: Optional[str] = None
|
||||||
|
generate_pdf: Optional[bool] = None
|
||||||
is_active: Optional[bool] = None
|
is_active: Optional[bool] = None
|
||||||
mechanic_ids: Optional[List[int]] = None # IDs de mecánicos autorizados
|
mechanic_ids: Optional[List[int]] = None # IDs de mecánicos autorizados
|
||||||
|
|
||||||
class Checklist(ChecklistBase):
|
class Checklist(ChecklistBase):
|
||||||
id: int
|
id: int
|
||||||
max_score: int
|
max_score: int
|
||||||
|
generate_pdf: bool
|
||||||
is_active: bool
|
is_active: bool
|
||||||
created_by: int
|
created_by: int
|
||||||
created_at: datetime
|
created_at: datetime
|
||||||
@@ -119,13 +122,15 @@ class QuestionBase(BaseModel):
|
|||||||
# Scale: {"type": "scale", "min": 1, "max": 5, "step": 1, "labels": {"min": "Muy malo", "max": "Excelente"}}
|
# Scale: {"type": "scale", "min": 1, "max": 5, "step": 1, "labels": {"min": "Muy malo", "max": "Excelente"}}
|
||||||
# Text: {"type": "text", "multiline": true, "max_length": 500}
|
# Text: {"type": "text", "multiline": true, "max_length": 500}
|
||||||
order: int = 0
|
order: int = 0
|
||||||
allow_photos: bool = True
|
allow_photos: bool = True # DEPRECATED: mantener por compatibilidad
|
||||||
|
photo_requirement: Optional[str] = 'optional' # none, optional, required
|
||||||
max_photos: int = 3
|
max_photos: int = 3
|
||||||
requires_comment_on_fail: bool = False
|
requires_comment_on_fail: bool = False
|
||||||
send_notification: bool = False
|
send_notification: bool = False
|
||||||
parent_question_id: Optional[int] = None
|
parent_question_id: Optional[int] = None
|
||||||
show_if_answer: Optional[str] = None
|
show_if_answer: Optional[str] = None
|
||||||
ai_prompt: Optional[str] = None
|
ai_prompt: Optional[str] = None
|
||||||
|
is_deleted: bool = False
|
||||||
|
|
||||||
class QuestionCreate(QuestionBase):
|
class QuestionCreate(QuestionBase):
|
||||||
checklist_id: int
|
checklist_id: int
|
||||||
@@ -133,10 +138,15 @@ class QuestionCreate(QuestionBase):
|
|||||||
class QuestionUpdate(QuestionBase):
|
class QuestionUpdate(QuestionBase):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
class QuestionReorder(BaseModel):
|
||||||
|
question_id: int
|
||||||
|
new_order: int
|
||||||
|
|
||||||
class Question(QuestionBase):
|
class Question(QuestionBase):
|
||||||
id: int
|
id: int
|
||||||
checklist_id: int
|
checklist_id: int
|
||||||
created_at: datetime
|
created_at: datetime
|
||||||
|
updated_at: Optional[datetime] = None
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
from_attributes = True
|
from_attributes = True
|
||||||
@@ -209,6 +219,7 @@ class AnswerCreate(AnswerBase):
|
|||||||
inspection_id: int
|
inspection_id: int
|
||||||
question_id: int
|
question_id: int
|
||||||
ai_analysis: Optional[list] = None # Lista de análisis de IA (soporta múltiples imágenes)
|
ai_analysis: Optional[list] = None # Lista de análisis de IA (soporta múltiples imágenes)
|
||||||
|
chat_history: Optional[list] = None # Historial de chat con AI Assistant
|
||||||
|
|
||||||
class AnswerUpdate(AnswerBase):
|
class AnswerUpdate(AnswerBase):
|
||||||
pass
|
pass
|
||||||
@@ -219,6 +230,7 @@ class Answer(AnswerBase):
|
|||||||
question_id: int
|
question_id: int
|
||||||
points_earned: int
|
points_earned: int
|
||||||
ai_analysis: Optional[list] = None # Lista de análisis de IA
|
ai_analysis: Optional[list] = None # Lista de análisis de IA
|
||||||
|
chat_history: Optional[list] = None # Historial de chat con AI Assistant
|
||||||
created_at: datetime
|
created_at: datetime
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
@@ -260,9 +272,10 @@ class InspectionDetail(Inspection):
|
|||||||
|
|
||||||
# AI Configuration Schemas
|
# AI Configuration Schemas
|
||||||
class AIConfigurationBase(BaseModel):
|
class AIConfigurationBase(BaseModel):
|
||||||
provider: str # openai, gemini
|
provider: str # openai, gemini, anthropic
|
||||||
api_key: str
|
api_key: str
|
||||||
model_name: Optional[str] = None
|
model_name: Optional[str] = None
|
||||||
|
logo_url: Optional[str] = None
|
||||||
|
|
||||||
class AIConfigurationCreate(AIConfigurationBase):
|
class AIConfigurationCreate(AIConfigurationBase):
|
||||||
pass
|
pass
|
||||||
@@ -271,6 +284,7 @@ class AIConfigurationUpdate(BaseModel):
|
|||||||
provider: Optional[str] = None
|
provider: Optional[str] = None
|
||||||
api_key: Optional[str] = None
|
api_key: Optional[str] = None
|
||||||
model_name: Optional[str] = None
|
model_name: Optional[str] = None
|
||||||
|
logo_url: Optional[str] = None
|
||||||
is_active: Optional[bool] = None
|
is_active: Optional[bool] = None
|
||||||
|
|
||||||
class AIConfiguration(AIConfigurationBase):
|
class AIConfiguration(AIConfigurationBase):
|
||||||
|
|||||||
@@ -10,10 +10,12 @@ python-jose[cryptography]==3.3.0
|
|||||||
passlib==1.7.4
|
passlib==1.7.4
|
||||||
bcrypt==4.0.1
|
bcrypt==4.0.1
|
||||||
python-multipart==0.0.6
|
python-multipart==0.0.6
|
||||||
openai==1.10.0
|
openai==1.57.4
|
||||||
|
anthropic==0.40.0
|
||||||
google-generativeai==0.3.2
|
google-generativeai==0.3.2
|
||||||
Pillow==10.2.0
|
Pillow==10.2.0
|
||||||
reportlab==4.0.9
|
reportlab==4.0.9
|
||||||
|
pypdf==4.0.1
|
||||||
python-dotenv==1.0.0
|
python-dotenv==1.0.0
|
||||||
boto3==1.34.89
|
boto3==1.34.89
|
||||||
requests==2.31.0
|
requests==2.31.0
|
||||||
@@ -6,6 +6,8 @@ services:
|
|||||||
POSTGRES_DB: ${POSTGRES_DB:-syntria_db}
|
POSTGRES_DB: ${POSTGRES_DB:-syntria_db}
|
||||||
POSTGRES_USER: ${POSTGRES_USER:-syntria_user}
|
POSTGRES_USER: ${POSTGRES_USER:-syntria_user}
|
||||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||||
|
TZ: Atlantic/Canary
|
||||||
|
PGTZ: Atlantic/Canary
|
||||||
ports:
|
ports:
|
||||||
- "5432:5432"
|
- "5432:5432"
|
||||||
volumes:
|
volumes:
|
||||||
@@ -29,6 +31,7 @@ services:
|
|||||||
OPENAI_API_KEY: ${OPENAI_API_KEY}
|
OPENAI_API_KEY: ${OPENAI_API_KEY}
|
||||||
ENVIRONMENT: production
|
ENVIRONMENT: production
|
||||||
ALLOWED_ORIGINS: ${ALLOWED_ORIGINS:-http://localhost,http://localhost:5173}
|
ALLOWED_ORIGINS: ${ALLOWED_ORIGINS:-http://localhost,http://localhost:5173}
|
||||||
|
TZ: Atlantic/Canary
|
||||||
ports:
|
ports:
|
||||||
- "8000:8000"
|
- "8000:8000"
|
||||||
volumes:
|
volumes:
|
||||||
|
|||||||
@@ -6,11 +6,15 @@ services:
|
|||||||
POSTGRES_DB: ${POSTGRES_DB:-checklist_db}
|
POSTGRES_DB: ${POSTGRES_DB:-checklist_db}
|
||||||
POSTGRES_USER: ${POSTGRES_USER:-checklist_user}
|
POSTGRES_USER: ${POSTGRES_USER:-checklist_user}
|
||||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-checklist_pass_2024}
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-checklist_pass_2024}
|
||||||
|
TZ: Atlantic/Canary
|
||||||
|
PGTZ: Atlantic/Canary
|
||||||
ports:
|
ports:
|
||||||
- "5432:5432"
|
- "5432:5432"
|
||||||
volumes:
|
volumes:
|
||||||
- postgres_data:/var/lib/postgresql/data
|
- postgres_data:/var/lib/postgresql/data
|
||||||
- ./init-db.sh:/docker-entrypoint-initdb.d/init-db.sh
|
- ./init-db.sh:/docker-entrypoint-initdb.d/init-db.sh
|
||||||
|
- ./postgres-custom.conf:/etc/postgresql/postgresql.conf
|
||||||
|
command: postgres -c config_file=/etc/postgresql/postgresql.conf
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-checklist_user} -d ${POSTGRES_DB:-checklist_db}"]
|
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-checklist_user} -d ${POSTGRES_DB:-checklist_db}"]
|
||||||
interval: 10s
|
interval: 10s
|
||||||
@@ -25,6 +29,7 @@ services:
|
|||||||
SECRET_KEY: ${SECRET_KEY:-your-secret-key-change-in-production-min-32-chars}
|
SECRET_KEY: ${SECRET_KEY:-your-secret-key-change-in-production-min-32-chars}
|
||||||
OPENAI_API_KEY: ${OPENAI_API_KEY}
|
OPENAI_API_KEY: ${OPENAI_API_KEY}
|
||||||
ENVIRONMENT: ${ENVIRONMENT:-development}
|
ENVIRONMENT: ${ENVIRONMENT:-development}
|
||||||
|
TZ: Atlantic/Canary
|
||||||
ports:
|
ports:
|
||||||
- "8000:8000"
|
- "8000:8000"
|
||||||
volumes:
|
volumes:
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ services:
|
|||||||
POSTGRES_DB: syntria_db
|
POSTGRES_DB: syntria_db
|
||||||
POSTGRES_USER: syntria_user
|
POSTGRES_USER: syntria_user
|
||||||
POSTGRES_PASSWORD: syntria_secure_2024
|
POSTGRES_PASSWORD: syntria_secure_2024
|
||||||
|
TZ: Atlantic/Canary
|
||||||
|
PGTZ: Atlantic/Canary
|
||||||
volumes:
|
volumes:
|
||||||
- postgres_data:/var/lib/postgresql/data
|
- postgres_data:/var/lib/postgresql/data
|
||||||
networks:
|
networks:
|
||||||
@@ -35,6 +37,7 @@ services:
|
|||||||
GEMINI_API_KEY: tu_api_key_de_gemini
|
GEMINI_API_KEY: tu_api_key_de_gemini
|
||||||
ENVIRONMENT: production
|
ENVIRONMENT: production
|
||||||
ALLOWED_ORIGINS: http://localhost,https://syntria.tudominio.com
|
ALLOWED_ORIGINS: http://localhost,https://syntria.tudominio.com
|
||||||
|
TZ: Atlantic/Canary
|
||||||
networks:
|
networks:
|
||||||
- syntria_network
|
- syntria_network
|
||||||
- network_public
|
- network_public
|
||||||
|
|||||||
155
docs/pdf-extraction-improvements.md
Normal file
155
docs/pdf-extraction-improvements.md
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
# Mejoras en la Extracción de PDFs con IA
|
||||||
|
|
||||||
|
## Versión Backend: 1.0.95
|
||||||
|
|
||||||
|
## Problema Original
|
||||||
|
|
||||||
|
El sistema tenía limitaciones al procesar PDFs con IA:
|
||||||
|
|
||||||
|
1. **Límites muy pequeños**: Solo extraía 2,000-4,000 caracteres
|
||||||
|
2. **Sin manejo de duplicaciones**: Páginas repetidas se procesaban múltiples veces
|
||||||
|
3. **No aprovechaba contextos largos**: Los modelos modernos soportan millones de tokens
|
||||||
|
4. **Falta de información**: No reportaba páginas procesadas o si el contenido fue truncado
|
||||||
|
|
||||||
|
## Solución Implementada
|
||||||
|
|
||||||
|
### 1. Función Centralizada de Extracción
|
||||||
|
|
||||||
|
Nueva función `extract_pdf_text_smart()` que:
|
||||||
|
- ✅ Extrae texto de forma inteligente
|
||||||
|
- ✅ Detecta y evita páginas duplicadas
|
||||||
|
- ✅ Maneja límites configurables
|
||||||
|
- ✅ Reporta estadísticas completas (páginas, caracteres, truncado)
|
||||||
|
- ✅ Manejo robusto de errores
|
||||||
|
|
||||||
|
```python
|
||||||
|
pdf_result = extract_pdf_text_smart(pdf_content, max_chars=50000)
|
||||||
|
# Retorna:
|
||||||
|
# {
|
||||||
|
# 'text': '...',
|
||||||
|
# 'pages': 10,
|
||||||
|
# 'pages_processed': 9, # Si una página estaba duplicada
|
||||||
|
# 'total_chars': 45000,
|
||||||
|
# 'truncated': False,
|
||||||
|
# 'success': True
|
||||||
|
# }
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Límites Optimizados por Caso de Uso
|
||||||
|
|
||||||
|
| Endpoint | Límite Anterior | Límite Nuevo | Modelo Objetivo |
|
||||||
|
|----------|----------------|--------------|-----------------|
|
||||||
|
| `/api/analyze-image` (OpenAI) | 4,000 chars | 30,000 chars | GPT-4 (128k tokens) |
|
||||||
|
| `/api/analyze-image` (Gemini) | 4,000 chars | 100,000 chars | Gemini 1.5/2.0 (2M tokens) |
|
||||||
|
| `/api/ai/chat-assistant` | 2,000 chars | 50,000 chars | Equilibrado para contexto |
|
||||||
|
|
||||||
|
### 3. Detección de Duplicaciones
|
||||||
|
|
||||||
|
El sistema ahora verifica si el contenido de una página ya existe antes de agregarlo:
|
||||||
|
|
||||||
|
```python
|
||||||
|
if page_text.strip() not in full_text:
|
||||||
|
full_text += f"\n--- Página {page_num}/{total_pages} ---\n{page_text.strip()}\n"
|
||||||
|
```
|
||||||
|
|
||||||
|
Esto previene:
|
||||||
|
- PDFs con páginas idénticas repetidas
|
||||||
|
- Documentos mal generados con contenido duplicado
|
||||||
|
- Uso innecesario de tokens en el análisis IA
|
||||||
|
|
||||||
|
### 4. Información Mejorada
|
||||||
|
|
||||||
|
El sistema ahora reporta:
|
||||||
|
- **Páginas totales**: Total de páginas en el PDF
|
||||||
|
- **Páginas procesadas**: Páginas únicas con contenido
|
||||||
|
- **Caracteres totales**: Tamaño real del texto extraído
|
||||||
|
- **Indicador de truncado**: Si el PDF fue limitado
|
||||||
|
|
||||||
|
Ejemplo de output:
|
||||||
|
```
|
||||||
|
📄 PDF procesado: manual-vehiculo.pdf - 87450 caracteres, 8/10 páginas (TRUNCADO)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Capacidades de Contexto por Modelo
|
||||||
|
|
||||||
|
### OpenAI GPT-4
|
||||||
|
- **Contexto**: ~128,000 tokens (~500,000 caracteres)
|
||||||
|
- **Límite aplicado**: 30,000 caracteres
|
||||||
|
- **Razón**: Balance entre contexto útil y costo
|
||||||
|
|
||||||
|
### Gemini 1.5/2.0 Pro
|
||||||
|
- **Contexto**: 2,000,000 tokens (~8,000,000 caracteres)
|
||||||
|
- **Límite aplicado**: 100,000 caracteres
|
||||||
|
- **Razón**: Aprovechar contexto masivo sin sobrecargar
|
||||||
|
|
||||||
|
### Chat Assistant
|
||||||
|
- **Límite**: 50,000 caracteres
|
||||||
|
- **Razón**: Incluye historial + contexto de fotos + PDF
|
||||||
|
|
||||||
|
## Casos de Uso Soportados
|
||||||
|
|
||||||
|
### ✅ PDFs Pequeños (1-5 páginas)
|
||||||
|
Extracción completa sin truncado
|
||||||
|
|
||||||
|
### ✅ PDFs Medianos (5-20 páginas)
|
||||||
|
Extracción completa o parcial según contenido
|
||||||
|
|
||||||
|
### ✅ PDFs Grandes (20+ páginas)
|
||||||
|
Extracción inteligente con truncado después de límite
|
||||||
|
|
||||||
|
### ✅ PDFs con Páginas Duplicadas
|
||||||
|
Detección automática y eliminación
|
||||||
|
|
||||||
|
### ✅ Múltiples PDFs en Chat
|
||||||
|
Cada uno procesado independientemente con su límite
|
||||||
|
|
||||||
|
## Indicadores de Estado
|
||||||
|
|
||||||
|
### En Logs del Servidor
|
||||||
|
```
|
||||||
|
📄 PDF procesado: documento.pdf - 25000 caracteres, 10/10 páginas
|
||||||
|
📄 PDF procesado: manual.pdf - 50000 caracteres, 15/20 páginas (TRUNCADO)
|
||||||
|
```
|
||||||
|
|
||||||
|
### En Respuesta al Cliente
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"attached_files": [
|
||||||
|
{
|
||||||
|
"filename": "manual.pdf",
|
||||||
|
"type": "application/pdf",
|
||||||
|
"pages": 20,
|
||||||
|
"pages_processed": 15,
|
||||||
|
"total_chars": 75000,
|
||||||
|
"truncated": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Próximas Mejoras Potenciales
|
||||||
|
|
||||||
|
1. **Chunking Inteligente**: Para PDFs muy grandes, dividir en chunks semánticos
|
||||||
|
2. **OCR Integrado**: Detectar PDFs escaneados y aplicar OCR automático
|
||||||
|
3. **Resumen Automático**: Para PDFs grandes, generar resumen antes de análisis
|
||||||
|
4. **Cache de Extracciones**: Guardar texto extraído en DB para reutilización
|
||||||
|
|
||||||
|
## Migración
|
||||||
|
|
||||||
|
No requiere migración de base de datos. Los cambios son retrocompatibles.
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
Para probar las mejoras:
|
||||||
|
|
||||||
|
1. **PDF pequeño** (< 10 páginas): Debe procesarse completo
|
||||||
|
2. **PDF grande** (> 50 páginas): Debe truncarse y reportar info
|
||||||
|
3. **PDF con duplicados**: Debe eliminar páginas repetidas
|
||||||
|
4. **Múltiples PDFs**: Cada uno procesado independientemente
|
||||||
|
|
||||||
|
## Notas Técnicas
|
||||||
|
|
||||||
|
- La función `extract_pdf_text_smart()` está en `main.py` línea ~210
|
||||||
|
- Usa `pypdf.PdfReader` para extracción
|
||||||
|
- Maneja encoding UTF-8 automáticamente
|
||||||
|
- Thread-safe (usa BytesIO)
|
||||||
168
frontend/PWA-UPDATE-GUIDE.md
Normal file
168
frontend/PWA-UPDATE-GUIDE.md
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
# Sistema de Actualización PWA - AYUTEC
|
||||||
|
|
||||||
|
## 🚀 Características
|
||||||
|
|
||||||
|
- ✅ **Detección automática** de nuevas versiones
|
||||||
|
- ✅ **Modal de actualización** grande y visible
|
||||||
|
- ✅ **Service Worker** con estrategia Network-First
|
||||||
|
- ✅ **Cache inteligente** para funcionamiento offline
|
||||||
|
- ✅ **Actualización forzada** al usuario cuando hay nueva versión
|
||||||
|
|
||||||
|
## 📱 Instalación como PWA
|
||||||
|
|
||||||
|
### En Android/iOS:
|
||||||
|
1. Abre la app en Chrome/Safari
|
||||||
|
2. Toca el menú (⋮)
|
||||||
|
3. Selecciona "Agregar a pantalla de inicio"
|
||||||
|
4. Confirma la instalación
|
||||||
|
|
||||||
|
### En Desktop:
|
||||||
|
1. Abre la app en Chrome/Edge
|
||||||
|
2. Haz clic en el ícono de instalación (➕) en la barra de direcciones
|
||||||
|
3. Confirma "Instalar"
|
||||||
|
|
||||||
|
## 🔄 Proceso de Actualización
|
||||||
|
|
||||||
|
### Para el Usuario:
|
||||||
|
1. Cuando hay una actualización, aparece automáticamente un **modal grande**
|
||||||
|
2. El modal muestra: "¡Nueva Actualización!"
|
||||||
|
3. Botón grande: **"🚀 ACTUALIZAR AHORA"**
|
||||||
|
4. Al presionar, la app se recarga con la nueva versión
|
||||||
|
|
||||||
|
### Para el Desarrollador:
|
||||||
|
|
||||||
|
#### Opción 1: Script Automático (Recomendado)
|
||||||
|
```powershell
|
||||||
|
cd frontend
|
||||||
|
.\update-version.ps1
|
||||||
|
```
|
||||||
|
Este script:
|
||||||
|
- Incrementa automáticamente la versión patch (1.0.87 → 1.0.88)
|
||||||
|
- Actualiza `package.json`
|
||||||
|
- Actualiza `public/service-worker.js`
|
||||||
|
|
||||||
|
#### Opción 2: Manual
|
||||||
|
1. **Actualizar `package.json`:**
|
||||||
|
```json
|
||||||
|
"version": "1.0.88" // Incrementar número
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Actualizar `public/service-worker.js`:**
|
||||||
|
```javascript
|
||||||
|
const CACHE_NAME = 'ayutec-v1.0.88'; // Mismo número
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Hacer build y deploy:**
|
||||||
|
```powershell
|
||||||
|
npm run build
|
||||||
|
docker build -t tu-registry/checklist-frontend:latest .
|
||||||
|
docker push tu-registry/checklist-frontend:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔧 Cómo Funciona
|
||||||
|
|
||||||
|
### 1. Service Worker
|
||||||
|
- Registrado en `App.jsx`
|
||||||
|
- Cache con nombre versionado: `ayutec-v1.0.87`
|
||||||
|
- Estrategia: **Network First, Cache Fallback**
|
||||||
|
- Al cambiar la versión, se crea nuevo cache
|
||||||
|
|
||||||
|
### 2. Detección de Actualización
|
||||||
|
```javascript
|
||||||
|
// En App.jsx
|
||||||
|
registration.addEventListener('updatefound', () => {
|
||||||
|
// Nueva versión detectada
|
||||||
|
setUpdateAvailable(true)
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Modal de Actualización
|
||||||
|
- Overlay negro semi-transparente (z-index: 9999)
|
||||||
|
- Modal animado con bounce
|
||||||
|
- Botón grande con gradiente
|
||||||
|
- **No se puede cerrar** - obliga a actualizar
|
||||||
|
|
||||||
|
### 4. Aplicación de Actualización
|
||||||
|
```javascript
|
||||||
|
waitingWorker.postMessage({ type: 'SKIP_WAITING' });
|
||||||
|
// Activa el nuevo service worker
|
||||||
|
// Recarga la página automáticamente
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📊 Versionado
|
||||||
|
|
||||||
|
Seguimos **Semantic Versioning**:
|
||||||
|
- **MAJOR**: Cambios incompatibles (1.0.0 → 2.0.0)
|
||||||
|
- **MINOR**: Nueva funcionalidad compatible (1.0.0 → 1.1.0)
|
||||||
|
- **PATCH**: Correcciones de bugs (1.0.0 → 1.0.1)
|
||||||
|
|
||||||
|
El script `update-version.ps1` incrementa automáticamente **PATCH**.
|
||||||
|
|
||||||
|
## 🧪 Probar Localmente
|
||||||
|
|
||||||
|
1. **Compilar en modo producción:**
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
npm run preview
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Simular actualización:**
|
||||||
|
- Abre la app en navegador
|
||||||
|
- Incrementa versión en `service-worker.js`
|
||||||
|
- Recarga la página (Ctrl+F5)
|
||||||
|
- Debe aparecer el modal de actualización
|
||||||
|
|
||||||
|
## 🐛 Troubleshooting
|
||||||
|
|
||||||
|
### El modal no aparece
|
||||||
|
- Verifica que el service worker esté registrado (F12 → Application → Service Workers)
|
||||||
|
- Asegúrate de cambiar el `CACHE_NAME` en `service-worker.js`
|
||||||
|
- Desregistra el SW antiguo: `Application → Service Workers → Unregister`
|
||||||
|
|
||||||
|
### La app no se actualiza
|
||||||
|
- Fuerza actualización: Ctrl+Shift+R (hard reload)
|
||||||
|
- Limpia cache del navegador
|
||||||
|
- Verifica que la nueva versión esté deployada
|
||||||
|
|
||||||
|
### PWA no se instala
|
||||||
|
- Verifica que `site.webmanifest` esté accesible
|
||||||
|
- Requiere HTTPS (excepto localhost)
|
||||||
|
- Verifica íconos en `/public/`
|
||||||
|
|
||||||
|
## 📝 Checklist de Deploy
|
||||||
|
|
||||||
|
- [ ] Incrementar versión con `update-version.ps1`
|
||||||
|
- [ ] Verificar que ambos archivos tengan la misma versión
|
||||||
|
- [ ] Hacer commit: `git commit -m "chore: bump version to X.X.X"`
|
||||||
|
- [ ] Build de producción: `npm run build`
|
||||||
|
- [ ] Build de Docker: `docker build -t frontend:vX.X.X .`
|
||||||
|
- [ ] Push a registry
|
||||||
|
- [ ] Deploy en servidor
|
||||||
|
- [ ] Verificar que usuarios vean el modal de actualización
|
||||||
|
|
||||||
|
## 🎯 Mejores Prácticas
|
||||||
|
|
||||||
|
1. **Siempre** incrementar versión antes de deploy
|
||||||
|
2. **Nunca** reutilizar números de versión
|
||||||
|
3. **Probar** localmente antes de deploy
|
||||||
|
4. **Documentar** cambios en commit message
|
||||||
|
5. **Notificar** a usuarios si es actualización crítica
|
||||||
|
|
||||||
|
## 🔐 Seguridad
|
||||||
|
|
||||||
|
- Service Worker solo funciona en HTTPS
|
||||||
|
- Manifest require `start_url` y `scope` correctos
|
||||||
|
- Cache no almacena datos sensibles (solo assets estáticos)
|
||||||
|
|
||||||
|
## 📱 Compatibilidad
|
||||||
|
|
||||||
|
- ✅ Chrome/Edge (Desktop y Mobile)
|
||||||
|
- ✅ Safari (iOS 11.3+)
|
||||||
|
- ✅ Firefox (Desktop y Mobile)
|
||||||
|
- ✅ Samsung Internet
|
||||||
|
- ⚠️ IE11 no soportado
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Versión actual:** 1.0.87
|
||||||
|
**Última actualización:** 2025-11-30
|
||||||
17
frontend/components.json
Normal file
17
frontend/components.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,7 +7,11 @@
|
|||||||
<link rel="shortcut icon" href="/favicon.ico" />
|
<link rel="shortcut icon" href="/favicon.ico" />
|
||||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
|
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
|
||||||
<link rel="manifest" href="/site.webmanifest" />
|
<link rel="manifest" href="/site.webmanifest" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||||
|
<meta name="theme-color" content="#4f46e5" />
|
||||||
|
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||||
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||||
|
<meta name="mobile-web-app-capable" content="yes" />
|
||||||
<title>AYUTEC - Sistema Inteligente de Inspecciones</title>
|
<title>AYUTEC - Sistema Inteligente de Inspecciones</title>
|
||||||
<meta name="description" content="AYUTEC: Sistema avanzado de inspecciones vehiculares con inteligencia artificial" />
|
<meta name="description" content="AYUTEC: Sistema avanzado de inspecciones vehiculares con inteligencia artificial" />
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
7305
frontend/package-lock.json
generated
Normal file
7305
frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "checklist-frontend",
|
"name": "checklist-frontend",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.0.64",
|
"version": "1.4.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
@@ -9,21 +9,38 @@
|
|||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"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": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
|
"react-markdown": "^9.0.1",
|
||||||
"react-router-dom": "^6.21.1",
|
"react-router-dom": "^6.21.1",
|
||||||
"axios": "^1.6.5",
|
|
||||||
"react-signature-canvas": "^1.0.6",
|
"react-signature-canvas": "^1.0.6",
|
||||||
"lucide-react": "^0.303.0",
|
"tailwind-merge": "^3.4.0",
|
||||||
"clsx": "^2.1.0"
|
"tailwindcss-animate": "^1.0.7"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/react": "^18.2.48",
|
"@types/node": "^24.10.2",
|
||||||
"@types/react-dom": "^18.2.18",
|
"@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",
|
"@vitejs/plugin-react": "^4.2.1",
|
||||||
"autoprefixer": "^10.4.16",
|
"autoprefixer": "^10.4.16",
|
||||||
"postcss": "^8.4.33",
|
"postcss": "^8.4.33",
|
||||||
"tailwindcss": "^3.4.1",
|
"tailwindcss": "^3.4.1",
|
||||||
|
"typescript": "^5.9.3",
|
||||||
"vite": "^5.0.11"
|
"vite": "^5.0.11"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
frontend/public/ayutec_logo.webp
Normal file
BIN
frontend/public/ayutec_logo.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 101 KiB |
72
frontend/public/service-worker.js
Normal file
72
frontend/public/service-worker.js
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
// Service Worker para PWA con detección de actualizaciones
|
||||||
|
// IMPORTANTE: Actualizar esta versión cada vez que se despliegue una nueva versión
|
||||||
|
const CACHE_NAME = 'ayutec-v1.4.0';
|
||||||
|
const urlsToCache = [
|
||||||
|
'/',
|
||||||
|
'/index.html'
|
||||||
|
];
|
||||||
|
|
||||||
|
// Instalación del service worker
|
||||||
|
self.addEventListener('install', (event) => {
|
||||||
|
console.log('Service Worker: Installing version', CACHE_NAME);
|
||||||
|
event.waitUntil(
|
||||||
|
caches.open(CACHE_NAME)
|
||||||
|
.then((cache) => {
|
||||||
|
console.log('Service Worker: Caching files');
|
||||||
|
return cache.addAll(urlsToCache);
|
||||||
|
})
|
||||||
|
// NO hacer skipWaiting automáticamente - esperar a que el usuario lo active
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Activación del service worker
|
||||||
|
self.addEventListener('activate', (event) => {
|
||||||
|
console.log('Service Worker: Activating...');
|
||||||
|
event.waitUntil(
|
||||||
|
caches.keys().then((cacheNames) => {
|
||||||
|
return Promise.all(
|
||||||
|
cacheNames.map((cacheName) => {
|
||||||
|
if (cacheName !== CACHE_NAME) {
|
||||||
|
console.log('Service Worker: Deleting old cache:', cacheName);
|
||||||
|
return caches.delete(cacheName);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
})
|
||||||
|
// NO hacer claim automáticamente - solo cuando el usuario actualice manualmente
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Estrategia: Network First, fallback to Cache
|
||||||
|
self.addEventListener('fetch', (event) => {
|
||||||
|
// Solo cachear peticiones GET
|
||||||
|
if (event.request.method !== 'GET') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
event.respondWith(
|
||||||
|
fetch(event.request)
|
||||||
|
.then((response) => {
|
||||||
|
// Clone la respuesta
|
||||||
|
const responseToCache = response.clone();
|
||||||
|
|
||||||
|
// Actualizar cache con la nueva respuesta
|
||||||
|
caches.open(CACHE_NAME).then((cache) => {
|
||||||
|
cache.put(event.request, responseToCache);
|
||||||
|
});
|
||||||
|
|
||||||
|
return response;
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
// Si falla la red, usar cache
|
||||||
|
return caches.match(event.request);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mensaje para notificar actualización
|
||||||
|
self.addEventListener('message', (event) => {
|
||||||
|
if (event.data && event.data.type === 'SKIP_WAITING') {
|
||||||
|
self.skipWaiting();
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
{
|
{
|
||||||
"name": "AYUTEC - Sistema de Inspecciones",
|
"name": "AYUTEC - Sistema de Inspecciones",
|
||||||
"short_name": "AYUTEC",
|
"short_name": "AYUTEC",
|
||||||
|
"start_url": "/",
|
||||||
|
"scope": "/",
|
||||||
"icons": [
|
"icons": [
|
||||||
{
|
{
|
||||||
"src": "/web-app-manifest-192x192.png",
|
"src": "/web-app-manifest-192x192.png",
|
||||||
@@ -17,5 +19,6 @@
|
|||||||
],
|
],
|
||||||
"theme_color": "#4f46e5",
|
"theme_color": "#4f46e5",
|
||||||
"background_color": "#ffffff",
|
"background_color": "#ffffff",
|
||||||
"display": "standalone"
|
"display": "standalone",
|
||||||
|
"orientation": "portrait"
|
||||||
}
|
}
|
||||||
|
|||||||
2674
frontend/src/App.jsx
2674
frontend/src/App.jsx
File diff suppressed because it is too large
Load Diff
@@ -281,6 +281,22 @@ export function QuestionAnswerInput({ question, value, onChange, onSave }) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PHOTO_ONLY (solo foto, sin campo de respuesta)
|
||||||
|
if (questionType === 'photo_only') {
|
||||||
|
return (
|
||||||
|
<div className="p-4 bg-blue-50 border border-blue-200 rounded-lg">
|
||||||
|
<p className="text-sm text-blue-800">
|
||||||
|
📸 Esta pregunta solo requiere fotografías. Adjunta las imágenes en la sección de fotos abajo.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AI_ASSISTANT (Chat con asistente) - No requiere UI aquí, el botón está en App.jsx
|
||||||
|
if (questionType === 'ai_assistant') {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
// Fallback para tipos desconocidos
|
// Fallback para tipos desconocidos
|
||||||
return (
|
return (
|
||||||
<div className="p-4 bg-yellow-50 border border-yellow-200 rounded-lg">
|
<div className="p-4 bg-yellow-50 border border-yellow-200 rounded-lg">
|
||||||
|
|||||||
@@ -22,7 +22,9 @@ const QUESTION_TYPES = [
|
|||||||
{ value: 'text', label: '📝 Texto Libre', icon: '✏️' },
|
{ value: 'text', label: '📝 Texto Libre', icon: '✏️' },
|
||||||
{ value: 'number', label: '🔢 Número', icon: '#️⃣' },
|
{ value: 'number', label: '🔢 Número', icon: '#️⃣' },
|
||||||
{ value: 'date', label: '📅 Fecha', icon: '📆' },
|
{ value: 'date', label: '📅 Fecha', icon: '📆' },
|
||||||
{ value: 'time', label: '🕐 Hora', icon: '⏰' }
|
{ value: 'time', label: '🕐 Hora', icon: '⏰' },
|
||||||
|
{ value: 'photo_only', label: '📸 Solo Fotografía', icon: '📷' },
|
||||||
|
{ value: 'ai_assistant', label: '🤖 Asistente (Chat)', icon: '💬' }
|
||||||
]
|
]
|
||||||
|
|
||||||
const STATUS_OPTIONS = [
|
const STATUS_OPTIONS = [
|
||||||
@@ -195,6 +197,28 @@ export function QuestionTypeEditor({ value, onChange, maxPoints = 1 }) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Opciones Globales */}
|
||||||
|
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
|
||||||
|
<h4 className="font-semibold text-blue-900 mb-3 text-sm">⚙️ Opciones Generales</h4>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{/* Checkbox para campo de observaciones */}
|
||||||
|
<label className="flex items-center gap-2 cursor-pointer">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={config.show_observations !== false}
|
||||||
|
onChange={(e) => updateConfig({ show_observations: e.target.checked })}
|
||||||
|
className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
|
||||||
|
/>
|
||||||
|
<span className="text-sm text-gray-700">
|
||||||
|
📝 Mostrar campo de observaciones al mecánico
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<p className="text-xs text-gray-500 ml-6">
|
||||||
|
Si está marcado, el mecánico podrá agregar notas adicionales en esta pregunta
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Configuración específica según tipo */}
|
{/* Configuración específica según tipo */}
|
||||||
<div className="bg-gray-50 border border-gray-200 rounded-lg p-4">
|
<div className="bg-gray-50 border border-gray-200 rounded-lg p-4">
|
||||||
{/* BOOLEAN */}
|
{/* BOOLEAN */}
|
||||||
@@ -511,6 +535,121 @@ export function QuestionTypeEditor({ value, onChange, maxPoints = 1 }) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* AI ASSISTANT (CHAT) */}
|
||||||
|
{config.type === 'ai_assistant' && (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<span className="text-3xl">💬</span>
|
||||||
|
<div>
|
||||||
|
<h4 className="font-semibold text-blue-900 mb-1">Asistente Conversacional</h4>
|
||||||
|
<p className="text-sm text-blue-700">
|
||||||
|
El mecánico podrá chatear con el asistente usando fotos de preguntas anteriores como contexto.
|
||||||
|
Configura qué preguntas anteriores usar y el comportamiento del asistente.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Prompt del asistente */}
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
🎯 Prompt del Asistente (Rol y Comportamiento)
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
value={config.assistant_prompt || ''}
|
||||||
|
onChange={(e) => updateConfig({ assistant_prompt: e.target.value })}
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 font-mono text-sm"
|
||||||
|
rows="6"
|
||||||
|
placeholder="Ejemplo: Eres un experto mecánico especializado en sistemas de frenos. Ayuda al mecánico a diagnosticar problemas basándote en las fotos que has visto. Sé directo y técnico. Si ves algo anormal en las fotos, menciónalo proactivamente."
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-gray-500 mt-1">
|
||||||
|
Define cómo debe comportarse el asistente: su rol, tono, especialidad, etc.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Preguntas de contexto */}
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
📸 Preguntas Anteriores para Contexto
|
||||||
|
</label>
|
||||||
|
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-3 mb-3">
|
||||||
|
<p className="text-xs text-yellow-800">
|
||||||
|
<strong>💡 Cómo funciona:</strong> El asistente verá las fotos de las preguntas que selecciones abajo.
|
||||||
|
Elige preguntas cuyas fotos sean relevantes para el diagnóstico (ej: si es asistente de frenos, selecciona preguntas sobre pastillas, discos, líquido de frenos, etc.)
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<textarea
|
||||||
|
value={config.context_questions || ''}
|
||||||
|
onChange={(e) => updateConfig({ context_questions: e.target.value })}
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 text-sm"
|
||||||
|
rows="3"
|
||||||
|
placeholder="IDs de preguntas separados por comas. Ejemplo: 5,8,12,15
|
||||||
|
Dejar vacío para usar TODAS las preguntas anteriores con fotos."
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-gray-500 mt-1">
|
||||||
|
Especifica los IDs de preguntas anteriores cuyas fotos debe analizar el asistente, o déjalo vacío para usar todas.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Instrucciones adicionales */}
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
📋 Instrucciones Adicionales (Opcional)
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
value={config.assistant_instructions || ''}
|
||||||
|
onChange={(e) => updateConfig({ assistant_instructions: e.target.value })}
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 text-sm"
|
||||||
|
rows="4"
|
||||||
|
placeholder="Instrucciones específicas adicionales.
|
||||||
|
Ejemplo:
|
||||||
|
- Si detectas pastillas con menos de 3mm, recomienda cambio inmediato
|
||||||
|
- Siempre verifica si hay fugas de líquido
|
||||||
|
- Menciona el código OBD2 si es relevante"
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-gray-500 mt-1">
|
||||||
|
Reglas o criterios específicos que el asistente debe seguir al dar consejos.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Configuración de respuestas */}
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
💬 Mensajes Máximos
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min="1"
|
||||||
|
max="50"
|
||||||
|
value={config.max_messages || 20}
|
||||||
|
onChange={(e) => updateConfig({ max_messages: parseInt(e.target.value) })}
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg"
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-gray-500 mt-1">
|
||||||
|
Límite de mensajes en el chat
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
📏 Longitud de Respuesta
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
value={config.response_length || 'medium'}
|
||||||
|
onChange={(e) => updateConfig({ response_length: e.target.value })}
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg"
|
||||||
|
>
|
||||||
|
<option value="short">Corta (concisa)</option>
|
||||||
|
<option value="medium">Media (balanceada)</option>
|
||||||
|
<option value="long">Larga (detallada)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,19 +1,35 @@
|
|||||||
export default function Sidebar({ user, activeTab, setActiveTab, sidebarOpen, setSidebarOpen, onLogout }) {
|
export default function Sidebar({ user, activeTab, setActiveTab, sidebarOpen, setSidebarOpen, onLogout }) {
|
||||||
return (
|
return (
|
||||||
<aside className={`bg-gradient-to-b from-gray-900 via-indigo-950 to-purple-950 text-white transition-all duration-300 ${sidebarOpen ? 'w-64' : 'w-16'} flex flex-col fixed h-full z-10 shadow-2xl`}>
|
<>
|
||||||
|
{/* Overlay para cerrar sidebar en móvil */}
|
||||||
|
{sidebarOpen && (
|
||||||
|
<div
|
||||||
|
className="fixed inset-0 bg-black/50 z-20 lg:hidden"
|
||||||
|
onClick={() => setSidebarOpen(false)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Sidebar */}
|
||||||
|
<aside className={`bg-gradient-to-b from-gray-900 via-indigo-950 to-purple-950 text-white transition-all duration-300 flex flex-col fixed h-full shadow-2xl
|
||||||
|
${sidebarOpen ? 'w-64' : 'w-16'}
|
||||||
|
${sidebarOpen ? 'translate-x-0' : '-translate-x-full lg:translate-x-0'}
|
||||||
|
z-30 lg:z-10
|
||||||
|
`}>
|
||||||
{/* Sidebar Header */}
|
{/* Sidebar Header */}
|
||||||
<div className={`p-4 flex items-center ${sidebarOpen ? 'justify-between' : 'justify-center'} border-b border-indigo-800/50`}>
|
<div className={`p-4 flex items-center ${sidebarOpen ? 'justify-between' : 'justify-center'} border-b border-indigo-800/50`}>
|
||||||
{sidebarOpen && (
|
{sidebarOpen && (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className="w-8 h-8 bg-gradient-to-br from-indigo-500 to-purple-500 rounded-lg flex items-center justify-center">
|
<img
|
||||||
<span className="text-white font-bold text-lg">A</span>
|
src="/ayutec_logo.png"
|
||||||
</div>
|
alt="Ayutec"
|
||||||
|
className="w-8 h-8 object-contain bg-white rounded-lg p-1"
|
||||||
|
/>
|
||||||
<h2 className="text-xl font-bold bg-gradient-to-r from-indigo-400 to-purple-400 bg-clip-text text-transparent">Ayutec</h2>
|
<h2 className="text-xl font-bold bg-gradient-to-r from-indigo-400 to-purple-400 bg-clip-text text-transparent">Ayutec</h2>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<button
|
<button
|
||||||
onClick={() => setSidebarOpen(!sidebarOpen)}
|
onClick={() => setSidebarOpen(!sidebarOpen)}
|
||||||
className="p-2 rounded-lg hover:bg-indigo-800/50 transition"
|
className="p-2 rounded-lg hover:bg-indigo-800/50 transition lg:block"
|
||||||
title={sidebarOpen ? 'Ocultar sidebar' : 'Mostrar sidebar'}
|
title={sidebarOpen ? 'Ocultar sidebar' : 'Mostrar sidebar'}
|
||||||
>
|
>
|
||||||
{sidebarOpen ? '☰' : '☰'}
|
{sidebarOpen ? '☰' : '☰'}
|
||||||
@@ -85,6 +101,22 @@ export default function Sidebar({ user, activeTab, setActiveTab, sidebarOpen, se
|
|||||||
</button>
|
</button>
|
||||||
</li>
|
</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' && (
|
{user.role === 'admin' && (
|
||||||
<>
|
<>
|
||||||
<li>
|
<li>
|
||||||
@@ -122,6 +154,27 @@ export default function Sidebar({ user, activeTab, setActiveTab, sidebarOpen, se
|
|||||||
|
|
||||||
{/* User Info */}
|
{/* User Info */}
|
||||||
<div className="p-4 border-t border-indigo-800/50">
|
<div className="p-4 border-t border-indigo-800/50">
|
||||||
|
{/* Versión */}
|
||||||
|
{sidebarOpen && (
|
||||||
|
<div className="mb-3 px-2 py-1.5 bg-indigo-900/30 rounded-lg border border-indigo-700/30">
|
||||||
|
<a
|
||||||
|
href="https://ayutec.es"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="flex items-center justify-center gap-2 hover:opacity-80 transition-opacity"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src="/ayutec_logo.webp"
|
||||||
|
alt="Ayutec"
|
||||||
|
className="w-10 h-10 object-contain bg-white rounded p-1"
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-indigo-300 font-medium hover:text-indigo-200">
|
||||||
|
Ayutec v1.4.0
|
||||||
|
</p>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className={`flex items-center gap-3 ${!sidebarOpen && 'justify-center'}`}>
|
<div className={`flex items-center gap-3 ${!sidebarOpen && 'justify-center'}`}>
|
||||||
<div className="w-10 h-10 bg-gradient-to-br from-indigo-500 to-purple-500 rounded-full flex items-center justify-center text-white font-bold flex-shrink-0 shadow-lg">
|
<div className="w-10 h-10 bg-gradient-to-br from-indigo-500 to-purple-500 rounded-full flex items-center justify-center text-white font-bold flex-shrink-0 shadow-lg">
|
||||||
{user.username.charAt(0).toUpperCase()}
|
{user.username.charAt(0).toUpperCase()}
|
||||||
@@ -150,5 +203,6 @@ export default function Sidebar({ user, activeTab, setActiveTab, sidebarOpen, se
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
36
frontend/src/components/ui/badge.tsx
Normal file
36
frontend/src/components/ui/badge.tsx
Normal 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 }
|
||||||
56
frontend/src/components/ui/button.tsx
Normal file
56
frontend/src/components/ui/button.tsx
Normal 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 }
|
||||||
79
frontend/src/components/ui/card.tsx
Normal file
79
frontend/src/components/ui/card.tsx
Normal 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 }
|
||||||
53
frontend/src/components/ui/tabs.tsx
Normal file
53
frontend/src/components/ui/tabs.tsx
Normal 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 }
|
||||||
@@ -2,7 +2,69 @@
|
|||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
|
@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 {
|
body {
|
||||||
|
background-color: hsl(var(--background));
|
||||||
|
color: hsl(var(--foreground));
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||||
@@ -10,6 +72,7 @@ body {
|
|||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
code {
|
code {
|
||||||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
||||||
|
|||||||
6
frontend/src/lib/utils.ts
Normal file
6
frontend/src/lib/utils.ts
Normal 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))
|
||||||
|
}
|
||||||
405
frontend/src/modules/recambios/RecambiosApp.tsx
Normal file
405
frontend/src/modules/recambios/RecambiosApp.tsx
Normal 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
9
frontend/src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
|
|
||||||
|
interface ImportMetaEnv {
|
||||||
|
readonly VITE_API_URL: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ImportMeta {
|
||||||
|
readonly env: ImportMetaEnv
|
||||||
|
}
|
||||||
80
frontend/tailwind.config.ts
Normal file
80
frontend/tailwind.config.ts
Normal 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
31
frontend/tsconfig.json
Normal 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" }]
|
||||||
|
}
|
||||||
10
frontend/tsconfig.node.json
Normal file
10
frontend/tsconfig.node.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"composite": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowSyntheticDefaultImports": true
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts"]
|
||||||
|
}
|
||||||
41
frontend/update-version.ps1
Normal file
41
frontend/update-version.ps1
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
# Script para actualizar la versión del frontend y service worker automáticamente
|
||||||
|
|
||||||
|
Write-Host "🔄 Actualizando versión del frontend..." -ForegroundColor Cyan
|
||||||
|
|
||||||
|
# Leer package.json
|
||||||
|
$packageJson = Get-Content "package.json" -Raw | ConvertFrom-Json
|
||||||
|
$currentVersion = $packageJson.version
|
||||||
|
Write-Host "📦 Versión actual: $currentVersion" -ForegroundColor Yellow
|
||||||
|
|
||||||
|
# Separar versión en partes (major.minor.patch)
|
||||||
|
$versionParts = $currentVersion -split '\.'
|
||||||
|
$major = [int]$versionParts[0]
|
||||||
|
$minor = [int]$versionParts[1]
|
||||||
|
$patch = [int]$versionParts[2]
|
||||||
|
|
||||||
|
# Incrementar patch
|
||||||
|
$patch++
|
||||||
|
$newVersion = "$major.$minor.$patch"
|
||||||
|
|
||||||
|
Write-Host "✨ Nueva versión: $newVersion" -ForegroundColor Green
|
||||||
|
|
||||||
|
# Actualizar package.json
|
||||||
|
$packageJsonContent = Get-Content "package.json" -Raw
|
||||||
|
$packageJsonContent = $packageJsonContent -replace """version"": ""$currentVersion""", """version"": ""$newVersion"""
|
||||||
|
Set-Content "package.json" -Value $packageJsonContent -NoNewline
|
||||||
|
|
||||||
|
Write-Host "✅ package.json actualizado" -ForegroundColor Green
|
||||||
|
|
||||||
|
# Actualizar service-worker.js
|
||||||
|
$swPath = "public\service-worker.js"
|
||||||
|
$swContent = Get-Content $swPath -Raw
|
||||||
|
$swContent = $swContent -replace "ayutec-v$currentVersion", "ayutec-v$newVersion"
|
||||||
|
Set-Content $swPath -Value $swContent -NoNewline
|
||||||
|
|
||||||
|
Write-Host "✅ service-worker.js actualizado" -ForegroundColor Green
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "🎉 Versión actualizada exitosamente a: $newVersion" -ForegroundColor Magenta
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "📝 Recuerda hacer commit de los cambios:" -ForegroundColor Yellow
|
||||||
|
Write-Host " git add package.json public/service-worker.js" -ForegroundColor Gray
|
||||||
|
Write-Host " git commit -m 'chore: bump version to $newVersion'" -ForegroundColor Gray
|
||||||
@@ -1,8 +1,14 @@
|
|||||||
import { defineConfig } from 'vite'
|
import { defineConfig } from 'vite'
|
||||||
import react from '@vitejs/plugin-react'
|
import react from '@vitejs/plugin-react'
|
||||||
|
import path from 'path'
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [react()],
|
plugins: [react()],
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': path.resolve(__dirname, './src'),
|
||||||
|
},
|
||||||
|
},
|
||||||
server: {
|
server: {
|
||||||
host: true,
|
host: true,
|
||||||
port: 5173,
|
port: 5173,
|
||||||
|
|||||||
22
frontend/vite.config.ts
Normal file
22
frontend/vite.config.ts
Normal 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,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
@@ -14,7 +14,7 @@ notepad++.exe $tempFile | Out-Null
|
|||||||
|
|
||||||
# Verificar que el archivo tenga contenido
|
# Verificar que el archivo tenga contenido
|
||||||
if (-not (Test-Path $tempFile) -or (Get-Item $tempFile).Length -eq 0) {
|
if (-not (Test-Path $tempFile) -or (Get-Item $tempFile).Length -eq 0) {
|
||||||
Write-Host "No se ingresó ningún mensaje. Abortando..." -ForegroundColor Red
|
Write-Host "No se ingreso ningun mensaje. Abortando..." -ForegroundColor Red
|
||||||
Remove-Item $tempFile -ErrorAction SilentlyContinue
|
Remove-Item $tempFile -ErrorAction SilentlyContinue
|
||||||
exit
|
exit
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,12 +2,19 @@
|
|||||||
set -e
|
set -e
|
||||||
|
|
||||||
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL
|
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL
|
||||||
|
-- Configurar zona horaria Atlantic/Canary
|
||||||
|
ALTER DATABASE "$POSTGRES_DB" SET timezone TO 'Atlantic/Canary';
|
||||||
|
ALTER ROLE "$POSTGRES_USER" SET timezone TO 'Atlantic/Canary';
|
||||||
|
|
||||||
-- Verificar que la base de datos existe
|
-- Verificar que la base de datos existe
|
||||||
SELECT 'Database is ready!' as status;
|
SELECT 'Database is ready!' as status;
|
||||||
|
|
||||||
-- Crear extensiones si son necesarias
|
-- Crear extensiones si son necesarias
|
||||||
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
|
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
|
||||||
|
|
||||||
|
-- Mostrar zona horaria configurada
|
||||||
|
SHOW timezone;
|
||||||
EOSQL
|
EOSQL
|
||||||
|
|
||||||
echo "Database initialization completed successfully!"
|
echo "Database initialization completed successfully with timezone: Atlantic/Canary"
|
||||||
|
|
||||||
|
|||||||
41
lineas.ps1
Normal file
41
lineas.ps1
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
param(
|
||||||
|
[Parameter(Mandatory=$true)]
|
||||||
|
[string]$Fecha
|
||||||
|
)
|
||||||
|
|
||||||
|
# Convertir fecha dd/mm/aaaa → yyyy-mm-dd
|
||||||
|
try {
|
||||||
|
$dateObj = [datetime]::ParseExact($Fecha, "dd/MM/yyyy", $null)
|
||||||
|
} catch {
|
||||||
|
Write-Host "❌ Formato inválido. Usa dd/mm/aaaa (ej: 05/12/2025)"
|
||||||
|
exit
|
||||||
|
}
|
||||||
|
|
||||||
|
$FechaISO = $dateObj.ToString("yyyy-MM-dd")
|
||||||
|
|
||||||
|
$desde = "$FechaISO 00:00"
|
||||||
|
$hasta = "$FechaISO 23:59"
|
||||||
|
|
||||||
|
$added = 0
|
||||||
|
$removed = 0
|
||||||
|
|
||||||
|
git log --since="$desde" --until="$hasta" --pretty=tformat: --numstat |
|
||||||
|
ForEach-Object {
|
||||||
|
$cols = $_.Split()
|
||||||
|
|
||||||
|
# Saltar líneas vacías
|
||||||
|
if ($cols.Length -lt 2) { return }
|
||||||
|
|
||||||
|
# Si Git muestra "-", ignorar esta línea
|
||||||
|
if ($cols[0] -eq "-" -or $cols[1] -eq "-") { return }
|
||||||
|
|
||||||
|
# Sumar líneas
|
||||||
|
$added += [int]$cols[0]
|
||||||
|
$removed += [int]$cols[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "Fecha ingresada: $Fecha"
|
||||||
|
Write-Host "Fecha usada (ISO): $FechaISO"
|
||||||
|
Write-Host "Líneas añadidas: $added"
|
||||||
|
Write-Host "Líneas eliminadas: $removed"
|
||||||
|
Write-Host "Total neto: " ($added - $removed)
|
||||||
6
migrations/add_chat_history_to_answers.sql
Normal file
6
migrations/add_chat_history_to_answers.sql
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
-- Agregar campo chat_history a la tabla answers
|
||||||
|
-- Fecha: 2025-12-02
|
||||||
|
|
||||||
|
ALTER TABLE answers ADD COLUMN IF NOT EXISTS chat_history JSON;
|
||||||
|
|
||||||
|
COMMENT ON COLUMN answers.chat_history IS 'Historial de conversación con AI Assistant para preguntas tipo ai_assistant';
|
||||||
13
migrations/add_generate_pdf_to_checklists.sql
Normal file
13
migrations/add_generate_pdf_to_checklists.sql
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
-- Agregar campo generate_pdf a la tabla checklists
|
||||||
|
-- Este campo controla si se genera PDF automáticamente al completar una inspección
|
||||||
|
|
||||||
|
ALTER TABLE checklists
|
||||||
|
ADD COLUMN IF NOT EXISTS generate_pdf BOOLEAN DEFAULT TRUE;
|
||||||
|
|
||||||
|
-- Actualizar checklists existentes para que generen PDF por defecto
|
||||||
|
UPDATE checklists
|
||||||
|
SET generate_pdf = TRUE
|
||||||
|
WHERE generate_pdf IS NULL;
|
||||||
|
|
||||||
|
-- Comentario para documentación
|
||||||
|
COMMENT ON COLUMN checklists.generate_pdf IS 'Controla si se genera PDF automáticamente al completar inspección';
|
||||||
25
migrations/add_soft_delete_to_questions.sql
Normal file
25
migrations/add_soft_delete_to_questions.sql
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
-- Migración: Agregar soft delete a preguntas
|
||||||
|
-- Fecha: 2025-11-27
|
||||||
|
-- Descripción: Permite eliminar preguntas sin romper la integridad de respuestas históricas
|
||||||
|
|
||||||
|
-- Agregar columna is_deleted a la tabla questions
|
||||||
|
ALTER TABLE questions ADD COLUMN IF NOT EXISTS is_deleted BOOLEAN DEFAULT FALSE;
|
||||||
|
|
||||||
|
-- Agregar columna updated_at si no existe
|
||||||
|
ALTER TABLE questions ADD COLUMN IF NOT EXISTS updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW();
|
||||||
|
|
||||||
|
-- Crear índice para mejorar queries que filtran por is_deleted
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_questions_is_deleted ON questions(is_deleted);
|
||||||
|
|
||||||
|
-- Crear índice compuesto para mejorar queries de preguntas activas por checklist
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_questions_checklist_active ON questions(checklist_id, is_deleted);
|
||||||
|
|
||||||
|
-- Actualizar preguntas existentes como no eliminadas
|
||||||
|
UPDATE questions SET is_deleted = FALSE WHERE is_deleted IS NULL;
|
||||||
|
|
||||||
|
-- Actualizar updated_at en preguntas existentes
|
||||||
|
UPDATE questions SET updated_at = created_at WHERE updated_at IS NULL;
|
||||||
|
|
||||||
|
-- Comentarios en las columnas
|
||||||
|
COMMENT ON COLUMN questions.is_deleted IS 'Soft delete: marca pregunta como eliminada sin borrarla físicamente, manteniendo integridad de respuestas históricas';
|
||||||
|
COMMENT ON COLUMN questions.updated_at IS 'Timestamp de última actualización de la pregunta';
|
||||||
22
migrations/change_allow_photos_to_photo_requirement.sql
Normal file
22
migrations/change_allow_photos_to_photo_requirement.sql
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
-- Migración: Cambiar allow_photos de Boolean a String con 3 estados
|
||||||
|
-- Fecha: 2025-12-02
|
||||||
|
-- Descripción: Agregar soporte para fotos opcionales/obligatorias/no permitidas
|
||||||
|
|
||||||
|
-- Paso 1: Agregar nueva columna
|
||||||
|
ALTER TABLE questions ADD COLUMN photo_requirement VARCHAR(20) DEFAULT 'optional';
|
||||||
|
|
||||||
|
-- Paso 2: Migrar datos existentes
|
||||||
|
UPDATE questions
|
||||||
|
SET photo_requirement = CASE
|
||||||
|
WHEN allow_photos = TRUE THEN 'optional'
|
||||||
|
WHEN allow_photos = FALSE THEN 'none'
|
||||||
|
ELSE 'optional'
|
||||||
|
END;
|
||||||
|
|
||||||
|
-- Paso 3: Eliminar columna antigua (opcional, comentar si quieres mantener compatibilidad)
|
||||||
|
-- ALTER TABLE questions DROP COLUMN allow_photos;
|
||||||
|
|
||||||
|
-- Nota: Los valores válidos son:
|
||||||
|
-- 'none' = No se permiten fotos
|
||||||
|
-- 'optional' = Fotos opcionales (puede adjuntar o no)
|
||||||
|
-- 'required' = Fotos obligatorias (debe adjuntar al menos 1)
|
||||||
17
migrations/force_timezone_all_sessions.sql
Normal file
17
migrations/force_timezone_all_sessions.sql
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
-- Forzar zona horaria Atlantic/Canary en TODAS las sesiones
|
||||||
|
-- Esto anula cualquier configuración del cliente
|
||||||
|
|
||||||
|
-- 1. Establecer timezone por defecto para la base de datos
|
||||||
|
ALTER DATABASE checklist_db SET timezone TO 'Atlantic/Canary';
|
||||||
|
ALTER DATABASE syntria_db SET timezone TO 'Atlantic/Canary';
|
||||||
|
|
||||||
|
-- 2. Establecer timezone por defecto para todos los usuarios
|
||||||
|
ALTER ROLE checklist_user SET timezone TO 'Atlantic/Canary';
|
||||||
|
ALTER ROLE syntria_user SET timezone TO 'Atlantic/Canary';
|
||||||
|
ALTER ROLE postgres SET timezone TO 'Atlantic/Canary';
|
||||||
|
|
||||||
|
-- 3. Verificar configuración actual
|
||||||
|
SHOW timezone;
|
||||||
|
|
||||||
|
-- 4. Para aplicar los cambios, desconectar y reconectar
|
||||||
|
-- O forzar: SELECT pg_reload_conf();
|
||||||
16
migrations/set_timezone_canary.sql
Normal file
16
migrations/set_timezone_canary.sql
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
-- Migración: Configurar zona horaria de Canarias en la base de datos
|
||||||
|
-- Fecha: 2024-12-08
|
||||||
|
-- Descripción: Establece Atlantic/Canary como zona horaria por defecto
|
||||||
|
|
||||||
|
-- Establecer zona horaria para la sesión actual
|
||||||
|
SET TIME ZONE 'Atlantic/Canary';
|
||||||
|
|
||||||
|
-- Configurar zona horaria por defecto para la base de datos
|
||||||
|
ALTER DATABASE checklist_db SET timezone TO 'Atlantic/Canary';
|
||||||
|
|
||||||
|
-- Nota: Las fechas existentes en la base de datos se mantendrán tal cual están almacenadas
|
||||||
|
-- PostgreSQL almacena las fechas con timezone en UTC internamente y las convierte según la zona horaria configurada
|
||||||
|
-- Si necesitas convertir fechas existentes, ejecuta manualmente según sea necesario
|
||||||
|
|
||||||
|
-- Para verificar la configuración:
|
||||||
|
SHOW timezone;
|
||||||
24
postgres-custom.conf
Normal file
24
postgres-custom.conf
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# Configuración de PostgreSQL para Ayutec
|
||||||
|
# Zona horaria: Atlantic/Canary (Islas Canarias, España)
|
||||||
|
|
||||||
|
# ZONA HORARIA
|
||||||
|
timezone = 'Atlantic/Canary'
|
||||||
|
log_timezone = 'Atlantic/Canary'
|
||||||
|
|
||||||
|
# LOCALE (opcional - para formato de fechas en español)
|
||||||
|
lc_messages = 'es_ES.UTF-8'
|
||||||
|
lc_monetary = 'es_ES.UTF-8'
|
||||||
|
lc_numeric = 'es_ES.UTF-8'
|
||||||
|
lc_time = 'es_ES.UTF-8'
|
||||||
|
|
||||||
|
# CONEXIONES
|
||||||
|
max_connections = 100
|
||||||
|
shared_buffers = 256MB
|
||||||
|
|
||||||
|
# LOGGING
|
||||||
|
logging_collector = on
|
||||||
|
log_directory = 'pg_log'
|
||||||
|
log_filename = 'postgresql-%Y-%m-%d_%H%M%S.log'
|
||||||
|
log_statement = 'mod' # Registrar INSERT, UPDATE, DELETE
|
||||||
|
log_duration = on
|
||||||
|
log_line_prefix = '%t [%p]: [%l-1] user=%u,db=%d,app=%a,client=%h '
|
||||||
Reference in New Issue
Block a user