feat: restructure project for GOLS Control Frontend

- Updated README.md to reflect new project structure and conventions.
- Refactored component imports and paths to align with new layout.
- Removed legacy components (AppConfig.vue, AppTopbar.vue) and created new layout components (MainLayout.vue, Sidebar.vue, TopBar.vue).
- Implemented warehouse module with components for inventory management (WarehouseDashboard.vue, InventoryTable.vue).
- Added composables and services for warehouse logic and API interactions.
- Introduced shared components (KpiCard.vue) for KPI display.
- Enhanced API service for handling HTTP requests.
- Defined TypeScript types for warehouse entities and global application types.
This commit is contained in:
Edgar Mendez Mendoza 2025-11-06 09:30:47 -06:00
parent 47e43ae84e
commit 06c212821a
16 changed files with 975 additions and 29 deletions

118
README.md
View File

@ -1,5 +1,117 @@
# Vue 3 + TypeScript + Vite
# Estructura del Proyecto - GOLS Control Frontend
This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
## 📁 Estructura de Carpetas
Learn more about the recommended Project Setup and IDE Support in the [Vue Docs TypeScript Guide](https://vuejs.org/guide/typescript/overview.html#project-setup).
```
src/
├── App.vue # Componente raíz (cambia entre demos)
├── main.ts # Punto de entrada
├── MainLayout.vue # Layout principal con Sidebar + TopBar
├── ColorDemo.vue # Demo de personalización de colores
├── assets/ # Recursos estáticos
│ └── styles/
│ └── main.css # Estilos globales y variables CSS
├── components/ # Componentes globales
│ ├── layout/ # Componentes de layout
│ │ ├── TopBar.vue # Barra superior
│ │ ├── Sidebar.vue # Menú lateral
│ │ └── AppConfig.vue # Panel de configuración de colores
│ │
│ ├── shared/ # Componentes compartidos
│ │ └── KpiCard.vue # Tarjeta de KPI
│ │
│ ├── ui/ # Componentes UI reutilizables
│ │ └── (vacío por ahora)
│ │
│ └── Holos/ # (Legacy - se puede eliminar)
│ ├── AppTopbar.vue
│ └── AppConfig.vue
├── composables/ # Composables globales
│ └── useLayout.ts # Gestión de tema y colores
├── modules/ # Módulos de negocio
│ └── warehouse/ # Módulo de almacén
│ ├── components/
│ │ ├── WarehouseDashboard.vue # Dashboard principal
│ │ └── InventoryTable.vue # Tabla de inventario
│ │
│ ├── composables/
│ │ └── useWarehouse.ts # Lógica de negocio
│ │
│ ├── services/
│ │ └── warehouseService.ts # API del módulo
│ │
│ └── types/
│ └── warehouse.d.ts # Tipos TypeScript
├── services/ # Servicios globales
│ └── api.ts # Cliente HTTP base
└── types/ # Tipos globales
└── global.d.ts # Definiciones TypeScript globales
```
## 🎯 Convenciones
### Módulos
Cada módulo sigue la estructura:
```
modules/[nombre-modulo]/
├── components/ # Componentes específicos del módulo
├── composables/ # Lógica de negocio del módulo
├── services/ # API calls del módulo
└── types/ # Types específicos del módulo
```
### Componentes
- **Layout**: Componentes de estructura (TopBar, Sidebar, etc.)
- **Shared**: Componentes reutilizables entre módulos
- **UI**: Componentes de interfaz básicos
### Composables
- Prefijo `use` (ej: `useWarehouse`, `useLayout`)
- Encapsulan lógica reutilizable
- Retornan estado reactivo y métodos
### Services
- Manejan comunicaciones HTTP
- Retornan Promises
- Usan el cliente `api.ts` base
## 🚀 Uso
### Ver Demo de Colores
```ts
// En App.vue
const currentView = ref<'color' | 'warehouse'>('color');
```
### Ver Dashboard de Almacén
```ts
// En App.vue
const currentView = ref<'color' | 'warehouse'>('warehouse');
```
## 📦 Crear un Nuevo Módulo
1. Crear estructura en `src/modules/[nombre]/`
2. Definir tipos en `types/[nombre].d.ts`
3. Crear servicio en `services/[nombre]Service.ts`
4. Crear composable en `composables/use[Nombre].ts`
5. Crear componentes en `components/`
## 🎨 Sistema de Colores
- Color primario: **Azul** (fijo)
- Colores de superficie: **5 opciones** (slate, gray, zinc, neutral, stone)
- Modo oscuro: Activable desde TopBar
## 📝 Notas
- Los componentes de PrimeVue se auto-importan
- TypeScript configurado con strict mode
- Tailwind CSS v4 integrado
- Variables CSS personalizadas en `main.css`

8
components.d.ts vendored
View File

@ -11,10 +11,16 @@ export {}
/* prettier-ignore */
declare module 'vue' {
export interface GlobalComponents {
AppConfig: typeof import('./src/components/Holos/AppConfig.vue')['default']
AppConfig: typeof import('./src/components/layout/AppConfig.vue')['default']
AppTopbar: typeof import('./src/components/Holos/AppTopbar.vue')['default']
Button: typeof import('primevue/button')['default']
Column: typeof import('primevue/column')['default']
DataTable: typeof import('primevue/datatable')['default']
HelloWorld: typeof import('./src/components/HelloWorld.vue')['default']
KpiCard: typeof import('./src/components/shared/KpiCard.vue')['default']
Sidebar: typeof import('./src/components/layout/Sidebar.vue')['default']
Tag: typeof import('primevue/tag')['default']
TopBar: typeof import('./src/components/layout/TopBar.vue')['default']
}
export interface GlobalDirectives {
StyleClass: typeof import('primevue/styleclass')['default']

View File

@ -1,9 +1,14 @@
<script setup lang="ts">
import { ref } from 'vue';
import ColorDemo from './ColorDemo.vue';
import HelloWorld from './components/HelloWorld.vue'
import MainLayout from './MainLayout.vue';
// Cambiar entre 'color' y 'warehouse' para ver diferentes demos
const currentView = ref<'color' | 'warehouse'>('warehouse');
</script>
<template>
<ColorDemo />
<ColorDemo v-if="currentView === 'color'" />
<MainLayout v-else />
</template>

View File

@ -1,10 +1,10 @@
<script setup lang="ts">
import AppTopbar from './components/Holos/AppTopbar.vue';
import TopBar from './components/layout/TopBar.vue';
</script>
<template>
<div class="min-h-screen bg-surface-50 dark:bg-surface-950">
<AppTopbar />
<TopBar />
<div class="p-6">
<div class="max-w-7xl mx-auto">

23
src/MainLayout.vue Normal file
View File

@ -0,0 +1,23 @@
<script setup lang="ts">
import TopBar from './components/layout/TopBar.vue';
import Sidebar from './components/layout/Sidebar.vue';
import WarehouseDashboard from './modules/warehouse/components/WarehouseDashboard.vue';
</script>
<template>
<div class="flex min-h-screen bg-surface-50 dark:bg-surface-950">
<!-- Sidebar -->
<Sidebar />
<!-- Main Content -->
<div class="flex-1 flex flex-col">
<!-- TopBar -->
<TopBar />
<!-- Page Content -->
<main class="flex-1 overflow-auto">
<WarehouseDashboard />
</main>
</div>
</div>
</template>

View File

@ -9,26 +9,6 @@ const { surfaces, surface, updateColors } = useLayout();
class="absolute top-16 right-0 w-64 p-4 bg-white dark:bg-surface-900 rounded-md shadow-lg border border-surface-200 dark:border-surface-700 origin-top z-50 hidden"
>
<div class="flex flex-col gap-4">
<!-- Selector de color primario oculto ya que solo hay uno -->
<!-- <div>
<span class="text-sm text-surface-600 dark:text-surface-400 font-semibold">Color Primario</span>
<div class="pt-2 flex gap-2 flex-wrap justify-between">
<button
v-for="pc of primaryColors"
:key="pc.name"
type="button"
:title="pc.name"
:class="[
'border-none w-5 h-5 rounded-full p-0 cursor-pointer focus:outline-none focus:ring-2 focus:ring-offset-2',
{
'ring-2 ring-offset-2 ring-surface-950 dark:ring-surface-0': primary === pc.name,
},
]"
:style="{ backgroundColor: pc.palette['500'] }"
@click="updateColors('primary', pc.name)"
/>
</div>
</div> -->
<div>
<span class="text-sm text-surface-600 dark:text-surface-400 font-semibold">Color de Superficie</span>
<div class="pt-2 flex gap-2 flex-wrap justify-between">

View File

@ -0,0 +1,118 @@
<script setup lang="ts">
import { ref } from 'vue';
interface MenuItem {
label: string;
icon: string;
to?: string;
items?: MenuItem[];
}
const menuItems = ref<MenuItem[]>([
{
label: 'Dashboard',
icon: 'pi pi-home',
to: '/'
},
{
label: 'Almacén',
icon: 'pi pi-warehouse',
items: [
{ label: 'Inventario', icon: 'pi pi-box', to: '/warehouse/inventory' },
{ label: 'Movimientos', icon: 'pi pi-arrow-right-arrow-left', to: '/warehouse/movements' }
]
},
{
label: 'Configuración',
icon: 'pi pi-cog',
to: '/settings'
}
]);
const sidebarVisible = ref(true);
const toggleSidebar = () => {
sidebarVisible.value = !sidebarVisible.value;
};
defineExpose({ toggleSidebar });
</script>
<template>
<aside
:class="[
'bg-surface-0 dark:bg-surface-900 border-r border-surface-200 dark:border-surface-700 transition-all duration-300',
sidebarVisible ? 'w-64' : 'w-20'
]"
>
<div class="flex flex-col h-full">
<!-- Logo / Brand -->
<div class="p-4 border-b border-surface-200 dark:border-surface-700">
<div class="flex items-center gap-3">
<i class="pi pi-box text-2xl text-primary"></i>
<span
v-if="sidebarVisible"
class="text-lg font-semibold text-surface-900 dark:text-surface-0"
>
GOLS Control
</span>
</div>
</div>
<!-- Navigation Menu -->
<nav class="flex-1 overflow-y-auto p-3">
<ul class="space-y-1">
<li v-for="item in menuItems" :key="item.label">
<!-- Item sin subitems -->
<a
v-if="!item.items"
:href="item.to"
class="flex items-center gap-3 px-3 py-2 rounded-lg text-surface-700 dark:text-surface-200 hover:bg-surface-100 dark:hover:bg-surface-800 transition-colors"
>
<i :class="item.icon"></i>
<span v-if="sidebarVisible">{{ item.label }}</span>
</a>
<!-- Item con subitems -->
<div v-else>
<div class="flex items-center gap-3 px-3 py-2 text-surface-700 dark:text-surface-200 font-medium">
<i :class="item.icon"></i>
<span v-if="sidebarVisible">{{ item.label }}</span>
</div>
<ul v-if="sidebarVisible" class="ml-6 mt-1 space-y-1">
<li v-for="subItem in item.items" :key="subItem.label">
<a
:href="subItem.to"
class="flex items-center gap-2 px-3 py-2 rounded-lg text-surface-600 dark:text-surface-300 hover:bg-surface-100 dark:hover:bg-surface-800 transition-colors text-sm"
>
<i :class="subItem.icon" class="text-xs"></i>
<span>{{ subItem.label }}</span>
</a>
</li>
</ul>
</div>
</li>
</ul>
</nav>
<!-- Toggle Button -->
<div class="p-3 border-t border-surface-200 dark:border-surface-700">
<button
@click="toggleSidebar"
class="w-full flex items-center justify-center gap-2 px-3 py-2 rounded-lg text-surface-700 dark:text-surface-200 hover:bg-surface-100 dark:hover:bg-surface-800 transition-colors"
>
<i :class="sidebarVisible ? 'pi pi-angle-left' : 'pi pi-angle-right'"></i>
<span v-if="sidebarVisible">Contraer</span>
</button>
</div>
</div>
</aside>
</template>
<style scoped>
aside {
height: 100vh;
position: sticky;
top: 0;
}
</style>

View File

@ -10,7 +10,7 @@ const { isDarkMode, toggleDarkMode } = useLayout();
<div class="flex items-center justify-between">
<div class="flex items-center gap-3">
<span class="text-xl font-semibold text-surface-900 dark:text-surface-0">
Mi Aplicación
GOLS Control
</span>
</div>

View File

@ -0,0 +1,68 @@
<script setup lang="ts">
interface Props {
title: string;
value: string | number;
icon?: string;
trend?: {
value: number;
isPositive: boolean;
};
color?: 'primary' | 'success' | 'warning' | 'danger' | 'info';
}
const props = withDefaults(defineProps<Props>(), {
color: 'primary'
});
const colorClasses: Record<string, string> = {
primary: 'bg-blue-50 dark:bg-blue-900/20 text-blue-600 dark:text-blue-400',
success: 'bg-green-50 dark:bg-green-900/20 text-green-600 dark:text-green-400',
warning: 'bg-orange-50 dark:bg-orange-900/20 text-orange-600 dark:text-orange-400',
danger: 'bg-red-50 dark:bg-red-900/20 text-red-600 dark:text-red-400',
info: 'bg-cyan-50 dark:bg-cyan-900/20 text-cyan-600 dark:text-cyan-400'
};
</script>
<template>
<div class="bg-surface-0 dark:bg-surface-900 rounded-lg p-6 border border-surface-200 dark:border-surface-700">
<div class="flex items-start justify-between">
<div class="flex-1">
<p class="text-sm text-surface-600 dark:text-surface-400 mb-2">
{{ title }}
</p>
<h3 class="text-3xl font-bold text-surface-900 dark:text-surface-0">
{{ value }}
</h3>
<!-- Trend indicator -->
<div v-if="trend" class="flex items-center gap-1 mt-2">
<i
:class="[
'text-sm',
trend.isPositive ? 'pi pi-arrow-up text-green-500' : 'pi pi-arrow-down text-red-500'
]"
></i>
<span
:class="[
'text-sm font-medium',
trend.isPositive ? 'text-green-600 dark:text-green-400' : 'text-red-600 dark:text-red-400'
]"
>
{{ Math.abs(trend.value) }}%
</span>
</div>
</div>
<!-- Icon -->
<div
v-if="icon"
:class="[
'w-12 h-12 rounded-lg flex items-center justify-center',
colorClasses[color]
]"
>
<i :class="[icon, 'text-xl']"></i>
</div>
</div>
</div>
</template>

View File

@ -0,0 +1,130 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { useWarehouse } from '../composables/useWarehouse';
import type { Product } from '../types/warehouse';
const { products, loading, loadProducts } = useWarehouse();
const selectedProducts = ref<Product[]>([]);
const searchQuery = ref('');
onMounted(() => {
loadProducts();
});
const getStockStatus = (product: Product) => {
if (product.quantity === 0) return { label: 'Sin Stock', severity: 'danger' };
if (product.quantity < product.minStock) return { label: 'Stock Bajo', severity: 'warning' };
if (product.quantity > product.maxStock) return { label: 'Sobrestock', severity: 'info' };
return { label: 'Normal', severity: 'success' };
};
const formatCurrency = (value: number) => {
return new Intl.NumberFormat('es-MX', {
style: 'currency',
currency: 'MXN'
}).format(value);
};
const formatDate = (date: Date) => {
return new Intl.DateTimeFormat('es-MX', {
year: 'numeric',
month: 'short',
day: 'numeric'
}).format(new Date(date));
};
</script>
<template>
<div class="p-6">
<!-- Search Bar -->
<div class="mb-4">
<div class="relative">
<i class="pi pi-search absolute left-3 top-1/2 -translate-y-1/2 text-surface-400"></i>
<input
v-model="searchQuery"
type="text"
placeholder="Buscar productos..."
class="w-full pl-10 pr-4 py-2 rounded-lg border border-surface-300 dark:border-surface-600 bg-surface-0 dark:bg-surface-800 text-surface-900 dark:text-surface-0 focus:outline-none focus:ring-2 focus:ring-primary"
/>
</div>
</div>
<!-- DataTable using PrimeVue -->
<DataTable
v-model:selection="selectedProducts"
:value="products"
:loading="loading"
:paginator="true"
:rows="10"
:rowsPerPageOptions="[5, 10, 20, 50]"
:globalFilterFields="['name', 'sku', 'category']"
:globalFilter="searchQuery"
stripedRows
showGridlines
class="text-sm"
>
<Column selectionMode="multiple" headerStyle="width: 3rem"></Column>
<Column field="sku" header="SKU" sortable>
<template #body="{ data }">
<span class="font-mono text-xs">{{ data.sku }}</span>
</template>
</Column>
<Column field="name" header="Producto" sortable>
<template #body="{ data }">
<div>
<div class="font-medium text-surface-900 dark:text-surface-0">{{ data.name }}</div>
<div class="text-xs text-surface-500">{{ data.category }}</div>
</div>
</template>
</Column>
<Column field="location" header="Ubicación" sortable></Column>
<Column field="quantity" header="Cantidad" sortable>
<template #body="{ data }">
<div class="flex items-center gap-2">
<span class="font-semibold">{{ data.quantity }}</span>
<Tag
:value="getStockStatus(data).label"
:severity="getStockStatus(data).severity"
/>
</div>
</template>
</Column>
<Column field="minStock" header="Stock Mín." sortable>
<template #body="{ data }">
<span class="text-surface-600 dark:text-surface-400">{{ data.minStock }}</span>
</template>
</Column>
<Column field="unitPrice" header="Precio Unit." sortable>
<template #body="{ data }">
<span class="font-medium">{{ formatCurrency(data.unitPrice) }}</span>
</template>
</Column>
<Column field="lastUpdated" header="Última Actualización" sortable>
<template #body="{ data }">
<span class="text-xs text-surface-500">{{ formatDate(data.lastUpdated) }}</span>
</template>
</Column>
<Column header="Acciones">
<template #body>
<div class="flex gap-2">
<Button icon="pi pi-pencil" size="small" text rounded />
<Button icon="pi pi-trash" size="small" text rounded severity="danger" />
</div>
</template>
</Column>
</DataTable>
</div>
</template>
<style scoped>
/* Estilos adicionales si es necesario */
</style>

View File

@ -0,0 +1,74 @@
<script setup lang="ts">
import { onMounted } from 'vue';
import { useWarehouse } from '../composables/useWarehouse';
import KpiCard from '../../../components/shared/KpiCard.vue';
import InventoryTable from './InventoryTable.vue';
const { stats, loadStats, loadProducts } = useWarehouse();
onMounted(async () => {
await loadStats();
await loadProducts();
});
const formatCurrency = (value: number) => {
return new Intl.NumberFormat('es-MX', {
style: 'currency',
currency: 'MXN'
}).format(value);
};
</script>
<template>
<div class="p-6">
<div class="max-w-7xl mx-auto">
<!-- Header -->
<div class="mb-6">
<h1 class="text-3xl font-bold text-surface-900 dark:text-surface-0 mb-2">
Dashboard de Almacén
</h1>
<p class="text-surface-600 dark:text-surface-400">
Visión general del inventario y movimientos
</p>
</div>
<!-- KPIs -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-6">
<KpiCard
title="Total de Productos"
:value="stats?.totalProducts ?? 0"
icon="pi pi-box"
color="primary"
/>
<KpiCard
title="Valor del Inventario"
:value="stats ? formatCurrency(stats.totalValue) : '$0'"
icon="pi pi-dollar"
color="success"
/>
<KpiCard
title="Productos con Stock Bajo"
:value="stats?.lowStockItems ?? 0"
icon="pi pi-exclamation-triangle"
color="warning"
/>
<KpiCard
title="Movimientos Recientes"
:value="stats?.recentMovements ?? 0"
icon="pi pi-arrow-right-arrow-left"
color="info"
/>
</div>
<!-- Inventory Table -->
<div class="bg-surface-0 dark:bg-surface-900 rounded-lg border border-surface-200 dark:border-surface-700">
<div class="p-6 border-b border-surface-200 dark:border-surface-700">
<h2 class="text-xl font-semibold text-surface-900 dark:text-surface-0">
Inventario
</h2>
</div>
<InventoryTable />
</div>
</div>
</div>
</template>

View File

@ -0,0 +1,82 @@
import { ref, computed } from 'vue';
import type { Product, WarehouseStats } from '../types/warehouse';
import { warehouseService } from '../services/warehouseService';
export function useWarehouse() {
const products = ref<Product[]>([]);
const stats = ref<WarehouseStats | null>(null);
const loading = ref(false);
const error = ref<string | null>(null);
// Computed properties
const lowStockProducts = computed(() =>
products.value.filter(p => p.quantity < p.minStock)
);
const totalInventoryValue = computed(() =>
products.value.reduce((sum, p) => sum + (p.quantity * p.unitPrice), 0)
);
// Methods
const loadProducts = async () => {
loading.value = true;
error.value = null;
try {
products.value = await warehouseService.getProducts();
} catch (err) {
error.value = 'Error al cargar productos';
console.error(err);
} finally {
loading.value = false;
}
};
const loadStats = async () => {
loading.value = true;
error.value = null;
try {
stats.value = await warehouseService.getStats();
} catch (err) {
error.value = 'Error al cargar estadísticas';
console.error(err);
} finally {
loading.value = false;
}
};
const updateQuantity = async (productId: string, quantity: number) => {
loading.value = true;
error.value = null;
try {
const updated = await warehouseService.updateProductQuantity(productId, quantity);
if (updated) {
const index = products.value.findIndex(p => p.id === productId);
if (index !== -1) {
products.value[index] = updated;
}
}
} catch (err) {
error.value = 'Error al actualizar cantidad';
console.error(err);
} finally {
loading.value = false;
}
};
return {
// State
products,
stats,
loading,
error,
// Computed
lowStockProducts,
totalInventoryValue,
// Methods
loadProducts,
loadStats,
updateQuantity
};
}

View File

@ -0,0 +1,152 @@
import type { Product, WarehouseMovement, WarehouseStats } from '../types/warehouse';
// Simulación de datos de ejemplo
const mockProducts: Product[] = [
{
id: '1',
name: 'Laptop Dell XPS 15',
sku: 'LAP-001',
category: 'Electrónica',
quantity: 45,
minStock: 10,
maxStock: 100,
unitPrice: 1299.99,
location: 'A-1-01',
lastUpdated: new Date('2025-11-01')
},
{
id: '2',
name: 'Mouse Logitech MX Master',
sku: 'MOU-001',
category: 'Accesorios',
quantity: 8,
minStock: 15,
maxStock: 50,
unitPrice: 99.99,
location: 'B-2-03',
lastUpdated: new Date('2025-11-03')
},
{
id: '3',
name: 'Teclado Mecánico RGB',
sku: 'KEY-001',
category: 'Accesorios',
quantity: 120,
minStock: 20,
maxStock: 150,
unitPrice: 149.99,
location: 'B-2-05',
lastUpdated: new Date('2025-11-04')
}
];
const mockMovements: WarehouseMovement[] = [
{
id: '1',
productId: '1',
productName: 'Laptop Dell XPS 15',
type: 'in',
quantity: 20,
reason: 'Compra a proveedor',
date: new Date('2025-11-01'),
user: 'Admin'
},
{
id: '2',
productId: '2',
productName: 'Mouse Logitech MX Master',
type: 'out',
quantity: 5,
reason: 'Venta',
date: new Date('2025-11-03'),
user: 'Admin'
}
];
/**
* Servicio para gestionar operaciones del almacén
*/
export const warehouseService = {
/**
* Obtiene todos los productos del inventario
*/
async getProducts(): Promise<Product[]> {
// Simular llamada API
return new Promise((resolve) => {
setTimeout(() => resolve(mockProducts), 500);
});
},
/**
* Obtiene un producto por ID
*/
async getProductById(id: string): Promise<Product | null> {
return new Promise((resolve) => {
setTimeout(() => {
const product = mockProducts.find(p => p.id === id);
resolve(product || null);
}, 300);
});
},
/**
* Obtiene los movimientos del almacén
*/
async getMovements(): Promise<WarehouseMovement[]> {
return new Promise((resolve) => {
setTimeout(() => resolve(mockMovements), 500);
});
},
/**
* Obtiene las estadísticas del almacén
*/
async getStats(): Promise<WarehouseStats> {
return new Promise((resolve) => {
setTimeout(() => {
const stats: WarehouseStats = {
totalProducts: mockProducts.reduce((sum, p) => sum + p.quantity, 0),
totalValue: mockProducts.reduce((sum, p) => sum + (p.quantity * p.unitPrice), 0),
lowStockItems: mockProducts.filter(p => p.quantity < p.minStock).length,
recentMovements: mockMovements.length
};
resolve(stats);
}, 300);
});
},
/**
* Crea un nuevo movimiento de inventario
*/
async createMovement(movement: Omit<WarehouseMovement, 'id' | 'date'>): Promise<WarehouseMovement> {
return new Promise((resolve) => {
setTimeout(() => {
const newMovement: WarehouseMovement = {
...movement,
id: String(mockMovements.length + 1),
date: new Date()
};
mockMovements.push(newMovement);
resolve(newMovement);
}, 500);
});
},
/**
* Actualiza la cantidad de un producto
*/
async updateProductQuantity(productId: string, quantity: number): Promise<Product | null> {
return new Promise((resolve) => {
setTimeout(() => {
const product = mockProducts.find(p => p.id === productId);
if (product) {
product.quantity = quantity;
product.lastUpdated = new Date();
resolve(product);
} else {
resolve(null);
}
}, 500);
});
}
};

View File

@ -0,0 +1,37 @@
// Types para el módulo Warehouse
export interface Product {
id: string;
name: string;
sku: string;
category: string;
quantity: number;
minStock: number;
maxStock: number;
unitPrice: number;
location: string;
lastUpdated: Date;
}
export interface WarehouseMovement {
id: string;
productId: string;
productName: string;
type: 'in' | 'out' | 'adjustment';
quantity: number;
reason: string;
date: Date;
user: string;
}
export interface WarehouseStats {
totalProducts: number;
totalValue: number;
lowStockItems: number;
recentMovements: number;
}
export interface InventoryFilter {
search?: string;
category?: string;
lowStock?: boolean;
}

120
src/services/api.ts Normal file
View File

@ -0,0 +1,120 @@
// Servicio API base para todas las peticiones HTTP
const API_BASE_URL = import.meta.env.VITE_API_URL || 'http://localhost:3000/api';
interface RequestOptions extends RequestInit {
params?: Record<string, string>;
}
class ApiService {
private baseUrl: string;
constructor(baseUrl: string) {
this.baseUrl = baseUrl;
}
/**
* Construye la URL con query params
*/
private buildUrl(endpoint: string, params?: Record<string, string>): string {
const url = new URL(`${this.baseUrl}${endpoint}`);
if (params) {
Object.entries(params).forEach(([key, value]) => {
url.searchParams.append(key, value);
});
}
return url.toString();
}
/**
* Maneja la respuesta de la API
*/
private async handleResponse<T>(response: Response): Promise<T> {
if (!response.ok) {
const error = await response.json().catch(() => ({ message: 'Error desconocido' }));
throw new Error(error.message || `HTTP Error: ${response.status}`);
}
return response.json();
}
/**
* GET request
*/
async get<T>(endpoint: string, options?: RequestOptions): Promise<T> {
const url = this.buildUrl(endpoint, options?.params);
const response = await fetch(url, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
...options?.headers,
},
});
return this.handleResponse<T>(response);
}
/**
* POST request
*/
async post<T>(endpoint: string, data?: any, options?: RequestOptions): Promise<T> {
const url = this.buildUrl(endpoint, options?.params);
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...options?.headers,
},
body: JSON.stringify(data),
});
return this.handleResponse<T>(response);
}
/**
* PUT request
*/
async put<T>(endpoint: string, data?: any, options?: RequestOptions): Promise<T> {
const url = this.buildUrl(endpoint, options?.params);
const response = await fetch(url, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
...options?.headers,
},
body: JSON.stringify(data),
});
return this.handleResponse<T>(response);
}
/**
* DELETE request
*/
async delete<T>(endpoint: string, options?: RequestOptions): Promise<T> {
const url = this.buildUrl(endpoint, options?.params);
const response = await fetch(url, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
...options?.headers,
},
});
return this.handleResponse<T>(response);
}
/**
* PATCH request
*/
async patch<T>(endpoint: string, data?: any, options?: RequestOptions): Promise<T> {
const url = this.buildUrl(endpoint, options?.params);
const response = await fetch(url, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
...options?.headers,
},
body: JSON.stringify(data),
});
return this.handleResponse<T>(response);
}
}
// Exportar instancia singleton
export const api = new ApiService(API_BASE_URL);

39
src/types/global.d.ts vendored Normal file
View File

@ -0,0 +1,39 @@
// Tipos globales de la aplicación
export interface ApiResponse<T> {
data: T;
message?: string;
success: boolean;
}
export interface PaginatedResponse<T> {
data: T[];
total: number;
page: number;
pageSize: number;
}
export interface User {
id: string;
name: string;
email: string;
role: string;
}
export interface TableColumn {
field: string;
header: string;
sortable?: boolean;
filterable?: boolean;
}
export type SortOrder = 'asc' | 'desc';
export interface SortOptions {
field: string;
order: SortOrder;
}
export interface FilterOptions {
[key: string]: any;
}