feat(warehouse): add inventory management features

- Implemented getWarehouseById method in warehouseService to fetch warehouse details by ID.
- Added new types for warehouse inventory management in warehouse.d.ts and warehouse.inventory.d.ts.
- Created WarehouseAddInventory.vue component for handling inventory entries with serial number management.
- Developed inventoryWarehouseServices for adding inventory through API.
- Updated router to include the new inventory management component.
- Added Docker configuration files for production deployment.
- Created Nginx configuration for serving the application.
- Added .dockerignore and .env.production for environment-specific settings.
This commit is contained in:
Edgar Méndez Mendoza 2026-02-13 13:49:41 -06:00
parent 71454dda61
commit d1c203cd0e
31 changed files with 1719 additions and 1149 deletions

50
.dockerignore Normal file
View File

@ -0,0 +1,50 @@
# Dependencies
node_modules
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# Build outputs
dist
dist-ssr
*.local
# Git
.git
.gitignore
# Docker
Dockerfile
docker-compose.yml
.dockerignore
# IDE
.vscode
.idea
*.sw?
*.suo
*.ntvs*
*.njsproj
*.sln
# OS
.DS_Store
Thumbs.db
# Environment
.env
.env.local
.env.*.local
# Documentation
README.md
*.md
# Testing
coverage
.nyc_output
# Logs
logs
*.log

View File

@ -3,6 +3,7 @@ VITE_API_URL=http://localhost:3000/api
# Environment
VITE_APP_ENV=development
APP_PORT=3000
# App Configuration
VITE_APP_NAME=GOLS Control

9
.env.production Normal file
View File

@ -0,0 +1,9 @@
# API Configuration (Production)
VITE_API_URL=https://api.golscontrol.com/api
# Environment
VITE_APP_ENV=production
# App Configuration
VITE_APP_NAME=GOLS Control
VITE_APP_VERSION=1.0.0

6
.gitignore vendored
View File

@ -23,4 +23,10 @@ dist-ssr
*.sln
*.sw?
# Environment files
.env
.env.local
.env.*.local
# Docker
docker-compose.override.yml

33
Dockerfile Normal file
View File

@ -0,0 +1,33 @@
# Stage 1: Build the application
FROM node:22-alpine AS builder
WORKDIR /app
# Copy package files first to leverage Docker cache
COPY package*.json ./
# Install dependencies
RUN npm ci
# Copy the rest of the application code
COPY . .
# Build the application
ARG VITE_API_URL
ENV VITE_API_URL=$VITE_API_URL
RUN npm run build
# Stage 2: Serve the application with Nginx
FROM nginx:alpine AS production
# Copy the built artifacts from the builder stage
COPY --from=builder /app/dist /usr/share/nginx/html
# Copy custom Nginx configuration
COPY nginx.conf /etc/nginx/conf.d/default.conf
# Expose port 80
EXPOSE 80
# Start Nginx
CMD ["nginx", "-g", "daemon off;"]

117
README.md
View File

@ -1,115 +1,20 @@
# Estructura del Proyecto - GOLS Control Frontend
## 📁 Estructura de Carpetas
## Docker (Producción)
```
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
```bash
# 1. Configurar
cp .env.production .env
# 2. Levantar
docker-compose up -d
# 3. Verificar en http://localhost
```
## 🎯 Convenciones
Ver [DOCKER.md](DOCKER.md) para más detalles.
### 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
## Notas
- Los componentes de PrimeVue se auto-importan
- TypeScript configurado con strict mode

17
docker-compose.yml Normal file
View File

@ -0,0 +1,17 @@
services:
controls-front:
build:
context: .
dockerfile: Dockerfile
args:
VITE_API_URL: ${VITE_API_URL}
container_name: controls-front-prod
ports:
- "${APP_PORT}:80"
networks:
- controls-network
restart: unless-stopped
networks:
controls-network:
driver: bridge

23
nginx.conf Normal file
View File

@ -0,0 +1,23 @@
server {
listen 80;
server_name 127.0.0.1;
root /usr/share/nginx/html;
index index.html;
location / {
try_files $uri $uri/ /index.html;
}
# Optional: Cache static assets for better performance
location ~* \.(?:ico|css|js|gif|jpe?g|png|woff2?|eot|ttf|svg|otf)$ {
expires 6M;
access_log off;
add_header Cache-Control "public";
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}

View File

@ -10,7 +10,7 @@ interface Props {
color?: 'primary' | 'success' | 'warning' | 'danger' | 'info';
}
const props = withDefaults(defineProps<Props>(), {
withDefaults(defineProps<Props>(), {
color: 'primary'
});

View File

@ -19,7 +19,6 @@ import ProgressSpinner from 'primevue/progressspinner';
import { useUnitOfMeasureStore } from '../stores/unitOfMeasureStore';
import { unitTypesService } from '../services/unitsTypes';
import type { UnitOfMeasure, CreateUnitOfMeasureData } from '../types/unitOfMeasure';
import type { UnitType } from '../types/unitTypes';
const router = useRouter();
const toast = useToast();

View File

@ -15,11 +15,11 @@ export const useSupplierStore = defineStore('supplier', () => {
const response = await supplierServices.getSuppliers(false);
// Si la respuesta es paginada, usar response.data; si es lista, usar response.suppliers o response directamente
if (Array.isArray(response)) {
suppliers.value = response;
suppliers.value = response as Supplier[];
} else if ('suppliers' in response) {
suppliers.value = response.suppliers;
suppliers.value = (response as any).suppliers;
} else if ('data' in response) {
suppliers.value = response.data;
suppliers.value = (response as any).data;
} else {
suppliers.value = [];
}

View File

@ -1,14 +1,15 @@
<script setup lang="ts">
import { ref, computed, watch } from 'vue';
import { productService } from '../../products/services/productService';
import type { Product } from '../../products/types/product';
const visible = defineModel<boolean>('visible');
const emit = defineEmits(['confirm', 'productSelected']);
const search = ref('');
const loading = ref(false);
const products = ref([]);
const selectedProduct = ref(null);
const products = ref<Product[]>([]);
const selectedProduct = ref<Product | null>(null);
const productAttributes = ref<Record<string, string>>({});
const quantity = ref(1);
@ -42,7 +43,7 @@ const filteredProducts = computed(() => products.value);
const totalProducts = computed(() => products.value.length);
const totalFiltered = computed(() => products.value.length);
function selectProduct(product) {
function selectProduct(product: Product) {
selectedProduct.value = product;
// Inicializar atributos dinámicos del producto
const attrs: Record<string, string> = {};

View File

@ -227,52 +227,11 @@ import Column from 'primevue/column';
import Breadcrumb from 'primevue/breadcrumb';
import { useRouter } from 'vue-router';
import { purchaseServices } from '../services/purchaseServices';
import type { PurchaseDetailResponse, PurchaseDetailData, PurchaseItem } from '../types/purchases';
import type { PurchaseDetailResponse, PurchaseDetailData } from '../types/purchases';
import { useRoute } from 'vue-router';
import { PURCHASE_STATUS_MAP } from '../types/purchases.d';
const items = ref([
{
product: 'Advanced Logic Controller v4',
sku: 'ALC-V4-99',
qty: 10,
unitPrice: 450,
tax: 8.5,
lineTotal: 4882.5,
},
{
product: 'Industrial Power Supply 12V',
sku: 'PWR-IND-12',
qty: 25,
unitPrice: 85,
tax: 8.5,
lineTotal: 2305.63,
},
{
product: 'Ethernet Patch Cable Cat6 (2m)',
sku: 'CAB-CAT6-2M',
qty: 100,
unitPrice: 4.5,
tax: 0,
lineTotal: 450,
},
{
product: 'Sensor Mount Bracket A',
sku: 'BRA-SENS-A',
qty: 50,
unitPrice: 12,
tax: 8.5,
lineTotal: 651,
},
]);
const summary = ref({
subtotal: 7625,
tax: 664.13,
shipping: 0,
total: 8289.13,
});
const route = useRoute();
const router = useRouter();
const breadcrumbItems = [
{ label: 'Inicio', route: '/' },
@ -281,8 +240,6 @@ const breadcrumbItems = [
{ label: 'Detalle' }
];
const home = { icon: 'pi pi-home', route: '/' };
const route = useRoute();
const purchase = ref<PurchaseDetailData | null>(null);
const loading = ref(true);

View File

@ -127,6 +127,10 @@ async function convertToPurchase(id: string | number) {
}
});
}
function goToAddInventory(id: string | number) {
router.push({ name: 'WarehouseAddInventory', query: { purchaseId: id } });
}
</script>
<template>
@ -179,6 +183,8 @@ async function convertToPurchase(id: string | number) {
@click="rejectPurchase(data.id)" v-tooltip.top="'Rechazar'" />
<Button v-if="data.status === '2'" icon="pi pi-shopping-cart" severity="success" text rounded
@click="convertToPurchase(data.id)" v-tooltip.top="'Convertir en Compra'" />
<Button v-if="data.status === '3'" icon="pi pi-warehouse" severity="info" text rounded
@click="goToAddInventory(data.id)" v-tooltip.top="'Cargar Inventario'" />
<Button icon="pi pi-eye" severity="info" text rounded
@click="goToDetails(data.id)" v-tooltip.top="'Ver Detalles'" />
</div>

View File

@ -1,5 +1,22 @@
import type { Supplier } from '../../catalog/types/suppliers';
export interface Product {
id: number;
code: string;
sku: string;
name: string;
barcode: string;
description: string;
unit_of_measure_id: number;
suggested_sale_price: number;
attributes: Record<string, any>;
is_active: boolean;
is_serial: boolean;
created_at: string;
updated_at: string;
deleted_at: string | null;
}
export interface Purchase {
id: number;
purchase_number: string;
@ -44,11 +61,12 @@ export interface PurchaseItem {
quantity: number;
unit_price: string;
subtotal: string;
notes: string;
notes: string | null;
attributes: Record<string, any>;
created_at: string;
updated_at: string;
deleted_at: string | null;
product: Product;
}
export interface PurchaseDetailData extends Purchase {

View File

@ -261,7 +261,6 @@
<script setup lang="ts">
import { ref, computed } from 'vue';
import { useRouter } from 'vue-router';
import { useToast } from 'primevue/usetoast';
// PrimeVue Components
@ -274,11 +273,7 @@ import Column from 'primevue/column';
import Dialog from 'primevue/dialog';
import InputText from 'primevue/inputtext';
import Textarea from 'primevue/textarea';
import InputNumber from 'primevue/inputnumber';
import Select from 'primevue/select';
import Tag from 'primevue/tag';
const router = useRouter();
const toast = useToast();
// State
@ -296,7 +291,7 @@ const breadcrumbItems = ref([
]);
// Form Data
const formData = ref({
const formData = ref<{id?: number; name: string; description: string; employeeCount: number; color: string; icon: string}>({
name: '',
description: '',
employeeCount: 0,
@ -304,29 +299,29 @@ const formData = ref({
icon: 'pi pi-building'
});
// Color Options
const colorOptions = [
{ label: 'Azul', value: '#3b82f6' },
{ label: 'Verde', value: '#10b981' },
{ label: 'Púrpura', value: '#8b5cf6' },
{ label: 'Naranja', value: '#f97316' },
{ label: 'Rosa', value: '#ec4899' },
{ label: 'Rojo', value: '#ef4444' },
{ label: 'Amarillo', value: '#f59e0b' },
{ label: 'Índigo', value: '#6366f1' },
];
// Color Options (for future use)
// const colorOptions = [
// { label: 'Azul', value: '#3b82f6' },
// { label: 'Verde', value: '#10b981' },
// { label: 'Púrpura', value: '#8b5cf6' },
// { label: 'Naranja', value: '#f97316' },
// { label: 'Rosa', value: '#ec4899' },
// { label: 'Rojo', value: '#ef4444' },
// { label: 'Amarillo', value: '#f59e0b' },
// { label: 'Índigo', value: '#6366f1' },
// ];
// Icon Options
const iconOptions = [
{ label: 'Edificio', value: 'pi pi-building' },
{ label: 'Usuarios', value: 'pi pi-users' },
{ label: 'Cog', value: 'pi pi-cog' },
{ label: 'Herramientas', value: 'pi pi-wrench' },
{ label: 'Camión', value: 'pi pi-truck' },
{ label: 'Gráfico', value: 'pi pi-chart-line' },
{ label: 'Escudo', value: 'pi pi-shield' },
{ label: 'Estrella', value: 'pi pi-star' },
];
// Icon Options (for future use)
// const iconOptions = [
// { label: 'Edificio', value: 'pi pi-building' },
// { label: 'Usuarios', value: 'pi pi-users' },
// { label: 'Cog', value: 'pi pi-cog' },
// { label: 'Herramientas', value: 'pi pi-wrench' },
// { label: 'Camión', value: 'pi pi-truck' },
// { label: 'Gráfico', value: 'pi pi-chart-line' },
// { label: 'Escudo', value: 'pi pi-shield' },
// { label: 'Estrella', value: 'pi pi-star' },
// ];
// Mock Data - Departments
const departments = ref([
@ -434,7 +429,11 @@ const saveDepartment = () => {
// Create new department
const newDepartment = {
id: Math.max(...departments.value.map(d => d.id)) + 1,
...formData.value
name: formData.value.name,
description: formData.value.description,
employeeCount: formData.value.employeeCount,
color: formData.value.color,
icon: formData.value.icon
};
departments.value.push(newDepartment);
@ -448,7 +447,15 @@ const saveDepartment = () => {
// Update existing department
const index = departments.value.findIndex(d => d.id === formData.value.id);
if (index > -1) {
departments.value[index] = { ...formData.value };
const existingDept = departments.value[index]!;
departments.value[index] = {
id: existingDept.id,
name: formData.value.name,
description: formData.value.description,
employeeCount: formData.value.employeeCount,
color: formData.value.color,
icon: formData.value.icon
};
toast.add({
severity: 'success',
summary: 'Departamento Actualizado',

View File

@ -506,7 +506,15 @@ const savePosition = () => {
} else {
const index = positions.value.findIndex(p => p.id === formData.value.id);
if (index > -1) {
positions.value[index] = { ...positions.value[index], ...formData.value };
const existing = positions.value[index]!;
positions.value[index] = {
id: existing.id,
name: formData.value.name,
department: formData.value.department,
description: formData.value.description,
icon: existing.icon,
iconBg: existing.iconBg
};
toast.add({
severity: 'success',
summary: 'Puesto Actualizado',

View File

@ -259,13 +259,13 @@ const toast = useToast();
const storeData = ref<Store | null>(null);
const loading = ref(false);
const isSaving = ref(false);
const loadingProducts = ref(false);
// const loadingProducts = ref(false);
const loadingTerminals = ref(false);
// Configuration
const sourceType = ref('catalog');
const selectedWarehouse = ref(null);
const productSearchQuery = ref('');
// const productSearchQuery = ref('');
const terminalSearchQuery = ref('');
// Breadcrumb
@ -286,29 +286,30 @@ const warehouseOptions = ref([
{ id: 3, name: 'Almacén Sur' }
]);
const products = ref([
{
id: 1,
sku: 'CAFE-GRN-01',
name: 'Café Grano Entero 1kg',
category: 'Café',
price: 250.00
},
{
id: 2,
sku: 'ACC-TZA-05',
name: 'Taza de Cerámica Blanca',
category: 'Accesorios',
price: 120.00
},
{
id: 3,
sku: 'PST-CHC-12',
name: 'Pastel de Chocolate',
category: 'Repostería',
price: 45.00
}
]);
// Mock products data (for future use)
// const products = ref([
// {
// id: 1,
// sku: 'CAFE-GRN-01',
// name: 'Café Grano Entero 1kg',
// category: 'Café',
// price: 250.00
// },
// {
// id: 2,
// sku: 'ACC-TZA-05',
// name: 'Taza de Cerámica Blanca',
// category: 'Accesorios',
// price: 120.00
// },
// {
// id: 3,
// sku: 'PST-CHC-12',
// name: 'Pastel de Chocolate',
// category: 'Repostería',
// price: 45.00
// }
// ]);
const terminals = ref([
{
@ -382,12 +383,13 @@ const formatDate = (dateString: string | undefined) => {
});
};
const formatCurrency = (amount: number) => {
return new Intl.NumberFormat('es-MX', {
style: 'currency',
currency: 'MXN'
}).format(amount);
};
// Unused utility (for future use)
// const formatCurrency = (amount: number) => {
// return new Intl.NumberFormat('es-MX', {
// style: 'currency',
// currency: 'MXN'
// }).format(amount);
// };
const getTerminalStatusLabel = (status: string) => {
const labels = {
@ -443,23 +445,24 @@ const saveConfiguration = async () => {
}
};
const addProduct = () => {
toast.add({
severity: 'info',
summary: 'Añadir Producto',
detail: 'Funcionalidad próximamente',
life: 3000
});
};
// Unused product methods (for future use)
// const addProduct = () => {
// toast.add({
// severity: 'info',
// summary: 'Añadir Producto',
// detail: 'Funcionalidad próximamente',
// life: 3000
// });
// };
const removeProduct = (product: any) => {
toast.add({
severity: 'warn',
summary: 'Quitar Producto',
detail: `¿Quitar ${product.name}?`,
life: 3000
});
};
// const removeProduct = (product: any) => {
// toast.add({
// severity: 'warn',
// summary: 'Quitar Producto',
// detail: `¿Quitar ${product.name}?`,
// life: 3000
// });
// };
const addTerminal = () => {
toast.add({

View File

@ -83,8 +83,7 @@ import InputSwitch from 'primevue/inputswitch';
import Toast from 'primevue/toast';
import { useToast } from 'primevue/usetoast';
import { permissionsService } from '../services/permissionsService';
import { roleService } from '../services/roleService';
import type { PermissionType, UpdateRolePermissionsData } from '../types/permissions';
import type { PermissionType } from '../types/permissions';
import type { Role } from '../types/role';
const toast = useToast();

View File

@ -174,7 +174,7 @@ import { useConfirm } from 'primevue/useconfirm';
import IconField from 'primevue/iconfield';
import InputIcon from 'primevue/inputicon';
import { roleService } from '../services/roleService';
import type { Role, CreateRoleData, UpdateRoleData } from '../types/role';
import type { Role, CreateRoleData } from '../types/role';
const toast = useToast();
const confirm = useConfirm();

View File

@ -1,12 +1,9 @@
import api from "../../../services/api"
import type {
Role,
RolePagination,
RoleResponse,
CreateRoleData,
UpdateRoleData,
SingleRoleResponse,
ApiError
SingleRoleResponse
} from '../types/role'
export const roleService = {

View File

@ -0,0 +1,620 @@
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import Card from 'primevue/card';
import Button from 'primevue/button';
import InputNumber from 'primevue/inputnumber';
import Dropdown from 'primevue/dropdown';
import InputText from 'primevue/inputtext';
import Badge from 'primevue/badge';
import Toast from 'primevue/toast';
import { useToast } from 'primevue/usetoast';
import { purchaseServices } from '../../purchases/services/purchaseServices';
import type { PurchaseDetailResponse } from '../../purchases/types/purchases';
import { useWarehouseStore } from '../../../stores/warehouseStore';
import { inventoryWarehouseServices } from '../services/inventoryWarehouse.services';
import type { InventoryProductItem, CreateInventoryRequest } from '../types/warehouse.inventory';
interface SerialNumber {
serial: string;
warehouseId: number;
}
interface Product {
id: number;
name: string;
sku: string;
category: string;
quantityOrdered: number;
quantityReceived: number;
warehouseId: number | null;
requiresSerial: boolean;
serialNumbers: SerialNumber[];
purchaseCost: number; // Costo de compra del producto
attributes?: Record<string, any>; // Atributos opcionales del producto
}
const toast = useToast();
const route = useRoute();
const router = useRouter();
const warehouseStore = useWarehouseStore();
// Data from API
const purchaseData = ref<PurchaseDetailResponse | null>(null);
const loading = ref(false);
// Data
const purchaseOrderNumber = ref('ORD-2023-001');
const totalItemsPO = ref(12);
const products = ref<Product[]>([
{
id: 1,
name: 'Cables de Red Cat6 2m',
sku: 'NET-C62M-WH',
category: 'Accesorios Networking',
quantityOrdered: 50,
quantityReceived: 0,
warehouseId: 1,
requiresSerial: false,
serialNumbers: [],
purchaseCost: 0,
},
{
id: 2,
name: 'MacBook Pro M2 14"',
sku: 'LAP-MBP14-M2',
category: 'Equipos de Cómputo',
quantityOrdered: 5,
quantityReceived: 1,
warehouseId: null,
requiresSerial: true,
serialNumbers: [
{ serial: 'SN-LAP-M2-00192', warehouseId: 1 },
],
purchaseCost: 0,
},
{
id: 3,
name: 'Impresora Zebra ZT411',
sku: 'PRN-ZB411-IND',
category: 'Hardware Almacén',
quantityOrdered: 2,
quantityReceived: 0,
warehouseId: null,
requiresSerial: true,
serialNumbers: [],
purchaseCost: 0,
},
]);
const expandedRows = ref<any[]>([]);
const newSerialNumber = ref('');
const newSerialWarehouse = ref<number>(1);
const totalReceived = computed(() => {
return products.value.reduce((sum, p) => sum + p.quantityReceived, 0);
});
const warehouseSummary = computed(() => {
const summary: Record<number, { name: string; count: number }> = {};
warehouseStore.warehouses.forEach(w => {
summary[w.id] = { name: w.name, count: 0 };
});
products.value.forEach(product => {
if (product.requiresSerial) {
product.serialNumbers.forEach(sn => {
const summaryItem = summary[sn.warehouseId];
if (summaryItem) {
summaryItem.count++;
}
});
} else if (product.warehouseId && product.quantityReceived > 0) {
const summaryItem = summary[product.warehouseId];
if (summaryItem) {
summaryItem.count += product.quantityReceived;
}
}
});
return Object.values(summary);
});
const isFormValid = computed(() => {
return products.value.every(product => {
if (product.requiresSerial) {
return product.serialNumbers.length === product.quantityOrdered;
} else {
return product.warehouseId !== null;
}
});
});
function toggleRow(product: Product) {
const index = expandedRows.value.findIndex(p => p.id === product.id);
if (index === -1) {
expandedRows.value.push(product);
} else {
expandedRows.value.splice(index, 1);
}
}
function isRowExpanded(product: Product) {
return expandedRows.value.some(p => p.id === product.id);
}
function addSerialNumber(product: Product) {
if (!newSerialNumber.value) {
toast.add({ severity: 'warn', summary: 'Campo Requerido', detail: 'Por favor ingrese un número de serie', life: 3000 });
return;
}
if (product.serialNumbers.length >= product.quantityOrdered) {
toast.add({ severity: 'warn', summary: 'Límite Alcanzado', detail: `Ya se registraron todos los números de serie para este producto`, life: 3000 });
return;
}
product.serialNumbers.push({
serial: newSerialNumber.value,
warehouseId: newSerialWarehouse.value,
});
product.quantityReceived = product.serialNumbers.length;
newSerialNumber.value = '';
toast.add({ severity: 'success', summary: 'Serie Registrada', detail: 'Número de serie agregado exitosamente', life: 2000 });
}
function removeSerialNumber(product: Product, index: number) {
product.serialNumbers.splice(index, 1);
product.quantityReceived = product.serialNumbers.length;
}
// Unused utility (for future use)
// function getWarehouseName(warehouseId: number): string {
// return warehouseStore.warehouses.find(w => w.id === warehouseId)?.name || '';
// }
async function confirmReceipt() {
if (!isFormValid.value) {
toast.add({ severity: 'error', summary: 'Validación', detail: 'Por favor complete todos los campos requeridos', life: 3000 });
return;
}
const purchaseId = route.query.purchaseId;
if (!purchaseId) {
toast.add({ severity: 'error', summary: 'Error', detail: 'ID de compra no válido', life: 3000 });
return;
}
try {
loading.value = true;
// Transformar productos a formato de API
const inventoryItems: InventoryProductItem[] = [];
products.value.forEach(product => {
if (product.requiresSerial) {
// Para productos con serial, crear un item por cada número de serie
product.serialNumbers.forEach(sn => {
inventoryItems.push({
product_id: product.id,
warehouse_id: sn.warehouseId,
purchase_cost: product.purchaseCost,
quantity: 1,
serial_number: sn.serial,
attributes: product.attributes || undefined,
});
});
} else {
// Para productos estándar, crear un solo item con la cantidad total
if (product.quantityReceived > 0 && product.warehouseId) {
inventoryItems.push({
product_id: product.id,
warehouse_id: product.warehouseId,
purchase_cost: product.purchaseCost,
quantity: product.quantityReceived,
attributes: product.attributes || undefined,
});
}
}
});
const requestData: CreateInventoryRequest = {
products: inventoryItems
};
// Enviar al API
const response = await inventoryWarehouseServices.addInventory(requestData);
// Actualizar estado de la compra a "Ingresada a Inventario" (4)
await purchaseServices.updatePurchaseStatus(Number(purchaseId), '4');
toast.add({
severity: 'success',
summary: 'Inventario Actualizado',
detail: response.message,
life: 4000
});
// Regresar a la vista de compras
setTimeout(() => {
router.back();
}, 1000);
} catch (error: any) {
console.error('Error al agregar inventario:', error);
toast.add({
severity: 'error',
summary: 'Error',
detail: error.response?.data?.message || 'No se pudo agregar el inventario',
life: 4000
});
} finally {
loading.value = false;
}
}
async function fetchPurchaseDetails() {
const purchaseId = route.query.purchaseId;
if (!purchaseId) {
toast.add({
severity: 'error',
summary: 'Error',
detail: 'No se proporcionó un ID de compra válido',
life: 3000
});
router.back();
return;
}
try {
loading.value = true;
const response = await purchaseServices.getPurchaseById(Number(purchaseId));
purchaseData.value = response;
// Actualizar datos de la compra
purchaseOrderNumber.value = response.data.purchase_number;
totalItemsPO.value = response.data.items.length;
// Mapear items de la compra a productos del componente
products.value = response.data.items.map(item => ({
id: item.product_id,
name: item.product.name,
sku: item.product.sku,
category: item.product.description || 'Sin categoría',
quantityOrdered: item.quantity,
quantityReceived: 0,
warehouseId: null,
requiresSerial: item.product.is_serial, // Determina si requiere serial basado en el producto
serialNumbers: [],
purchaseCost: parseFloat(item.subtotal),
attributes: item.product.attributes || null,
}));
} catch (error) {
console.error('Error al cargar los detalles de la compra:', error);
toast.add({
severity: 'error',
summary: 'Error',
detail: 'No se pudieron cargar los detalles de la compra',
life: 3000
});
router.back();
} finally {
loading.value = false;
}
}
onMounted(async () => {
await warehouseStore.fetchWarehouses();
// Establecer el primer almacén activo como predeterminado
if (warehouseStore.activeWarehouses.length > 0) {
newSerialWarehouse.value = warehouseStore.activeWarehouses[0]?.id || 1;
}
await fetchPurchaseDetails();
});
function cancel() {
// Lógica para cancelar y regresar
}
</script>
<template>
<div class="space-y-6">
<Toast />
<!-- Loading State -->
<Card v-if="loading">
<template #content>
<div class="flex items-center justify-center py-8">
<i class="pi pi-spinner pi-spin text-4xl text-primary"></i>
<span class="ml-3 text-lg">Cargando detalles de la compra...</span>
</div>
</template>
</Card>
<template v-else>
<!-- Page Header -->
<div class="flex flex-col md:flex-row md:items-end justify-between gap-4">
<div>
<h2 class="text-3xl font-black text-slate-900 tracking-tight">Registrar Entrada de Mercancía</h2>
<p class="text-slate-500 mt-1 flex items-center gap-2">
<i class="pi pi-receipt text-base"></i>
No. Orden #{{ purchaseOrderNumber }} |
<span class="text-primary font-semibold">Distribución Multi-Almacén</span>
</p>
</div>
<div class="flex items-center gap-3">
<Card class="shadow-sm">
<template #content>
<div class="flex gap-4 px-2">
<div class="text-center">
<p class="text-[10px] text-slate-500 uppercase tracking-wider font-bold">Total Items PO</p>
<p class="text-lg font-black text-slate-900 leading-tight">{{ totalItemsPO }}</p>
</div>
<div class="w-px bg-slate-200 h-8 self-center"></div>
<div class="text-center">
<p class="text-[10px] text-slate-500 uppercase tracking-wider font-bold">Recibidos</p>
<p class="text-lg font-black text-primary leading-tight">{{ totalReceived }}</p>
</div>
</div>
</template>
</Card>
<Button icon="pi pi-eye" label="Ver Detalles" severity="secondary" outlined />
</div>
</div>
<!-- Products Table -->
<Card>
<template #header>
<div class="px-6 py-4 border-b border-slate-200 bg-slate-50/50 flex justify-between items-center">
<h3 class="font-bold text-slate-900">Productos de la Orden</h3>
<span class="text-xs text-slate-500 flex items-center gap-1">
<i class="pi pi-info-circle text-sm"></i>
Seleccione el almacén de destino por cada producto
</span>
</div>
</template>
<template #content>
<div class="overflow-x-auto -m-6">
<table class="w-full border-collapse">
<thead>
<tr class="text-left text-xs font-bold text-slate-500 uppercase tracking-wider bg-slate-50">
<th class="px-6 py-4" style="width: 35%;">Producto</th>
<th class="px-6 py-4" style="width: 12%;">SKU / Ref</th>
<th class="px-6 py-4 text-center" style="width: 8%;">Cant. PO</th>
<th class="px-6 py-4 text-center" style="width: 10%;">Recibida</th>
<th class="px-6 py-4" style="width: 20%;">Almacén de Destino</th>
<th class="px-6 py-4 text-center" style="width: 15%;">Acciones</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-200">
<template v-for="product in products" :key="product.id">
<tr :class="[
'hover:bg-slate-50 transition-colors',
isRowExpanded(product) ? 'bg-primary/[0.02]' : ''
]">
<td class="px-6 py-4" :class="{ 'border-l-4 border-primary': isRowExpanded(product) }">
<div class="flex items-center gap-3">
<div class="size-10 bg-slate-100 rounded-lg flex items-center justify-center"
:class="{ 'bg-primary/10 text-primary': isRowExpanded(product) }">
<i :class="[
product.requiresSerial ? 'pi-desktop' : 'pi-box',
'pi text-xl'
]"></i>
</div>
<div class="flex flex-col">
<span class="text-sm font-semibold text-slate-900">{{ product.name }}</span>
<span class="text-xs text-slate-500">{{ product.category }}</span>
</div>
</div>
</td>
<td class="px-6 py-4 text-sm text-slate-600">{{ product.sku }}</td>
<td class="px-6 py-4 text-center text-sm font-bold text-slate-900">{{ product.quantityOrdered }}</td>
<td class="px-6 py-4 text-center">
<InputNumber v-if="!product.requiresSerial" v-model="product.quantityReceived"
:max="product.quantityOrdered" :min="0"
showButtons
buttonLayout="horizontal"
:step="1"
class="w-full max-w-[120px] mx-auto"
:inputStyle="{ textAlign: 'center', fontWeight: 'bold', fontSize: '0.875rem', width: '60px' }" />
<span v-else class="text-sm font-black text-primary">
{{ product.quantityReceived }} / {{ product.quantityOrdered }}
</span>
</td>
<td class="px-6 py-4">
<Dropdown v-if="!product.requiresSerial" v-model="product.warehouseId"
:options="warehouseStore.activeWarehouses"
optionLabel="name"
optionValue="id"
placeholder="Seleccione Almacén..."
class="w-full"
style="min-width: 200px;" />
<div v-else class="text-xs text-slate-400 italic">
Asignación por número de serie
</div>
</td>
<td class="px-6 py-4 text-center">
<Button v-if="product.requiresSerial"
:label="isRowExpanded(product) ? 'OCULTAR' : 'GESTIONAR SERIES'"
:icon="isRowExpanded(product) ? 'pi pi-chevron-up' : 'pi pi-qrcode'"
:severity="isRowExpanded(product) ? 'secondary' : 'info'"
:outlined="!isRowExpanded(product)"
size="small"
@click="toggleRow(product)" />
<Badge v-else value="Estándar" severity="secondary" />
</td>
</tr>
<!-- Serial Numbers Expansion -->
<tr v-if="isRowExpanded(product)" class="bg-primary/[0.02]">
<td colspan="6" class="px-10 pb-6 pt-2">
<Card class="border border-primary/20 shadow-sm">
<template #header>
<div class="px-5 pt-5 pb-3 border-b border-slate-100">
<div class="flex items-center justify-between mb-2">
<div>
<h4 class="text-sm font-bold text-slate-700">
Entrada de Números de Serie
({{ product.quantityOrdered - product.serialNumbers.length }} Pendientes)
</h4>
<p class="text-[11px] text-slate-500">
Defina el almacén para cada equipo escaneado
</p>
</div>
<Badge severity="info">
<i class="pi pi-qrcode mr-1"></i>
Listo para escanear
</Badge>
</div>
</div>
</template>
<template #content>
<div class="grid grid-cols-1 lg:grid-cols-12 gap-6">
<!-- Serial Input Form -->
<div class="lg:col-span-5 space-y-4">
<div>
<label class="block text-[10px] font-bold text-slate-500 uppercase mb-1">
Número de Serie
</label>
<div class="relative">
<i class="pi pi-qrcode absolute left-3 top-1/2 -translate-y-1/2 text-gray-400"></i>
<InputText v-model="newSerialNumber"
placeholder="Escanear o escribir serie..."
class="w-full pl-10" />
</div>
</div>
<div>
<label class="block text-[10px] font-bold text-slate-500 uppercase mb-1">
Almacén Destino
</label>
<Dropdown v-model="newSerialWarehouse"
:options="warehouseStore.activeWarehouses" optionLabel="name" optionValue="id"
class="w-full" />
</div>
<Button label="Registrar Serie" icon="pi pi-plus"
class="w-full" severity="secondary"
@click="addSerialNumber(product)" />
</div>
<!-- Serial Numbers List -->
<div class="lg:col-span-7">
<Card class="bg-slate-50 border border-slate-200">
<template #content>
<div class="overflow-x-auto -m-6">
<table class="w-full text-xs">
<thead class="bg-slate-100">
<tr>
<th class="px-3 py-2 text-left font-bold text-slate-600">
Serie
</th>
<th class="px-3 py-2 text-left font-bold text-slate-600">
Almacén Destino
</th>
<th class="px-3 py-2"></th>
</tr>
</thead>
<tbody class="divide-y divide-slate-200">
<tr v-for="(sn, index) in product.serialNumbers" :key="index">
<td class="px-3 py-2 font-mono text-slate-900">
{{ sn.serial }}
</td>
<td class="px-3 py-2">
<Dropdown v-model="sn.warehouseId"
:options="warehouseStore.activeWarehouses"
optionLabel="name"
optionValue="id"
class="w-full text-xs" />
</td>
<td class="px-3 py-2 text-right">
<Button icon="pi pi-trash"
severity="danger"
text
rounded
size="small"
@click="removeSerialNumber(product, index)" />
</td>
</tr>
<tr v-if="product.serialNumbers.length === 0">
<td colspan="3" class="px-3 py-4 text-center text-slate-400">
No hay números de serie registrados
</td>
</tr>
</tbody>
</table>
</div>
</template>
</Card>
</div>
</div>
</template>
</Card>
</td>
</tr>
</template>
</tbody>
</table>
</div>
</template>
</Card>
<!-- Summary and Alerts -->
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
<div class="md:col-span-2">
<Card v-if="!isFormValid" class="bg-amber-50 border border-amber-200">
<template #content>
<div class="flex items-start gap-4">
<i class="pi pi-exclamation-triangle text-amber-600 text-xl"></i>
<div>
<p class="text-sm font-bold text-amber-800">Pendiente de Validación y Distribución</p>
<p class="text-sm text-amber-700">
Por favor complete todos los campos requeridos antes de confirmar la recepción.
</p>
</div>
</div>
</template>
</Card>
</div>
<Card class="shadow-sm">
<template #header>
<div class="px-6 pt-4">
<h4 class="text-xs font-bold text-slate-500 uppercase tracking-wider">
Resumen de Destinos
</h4>
</div>
</template>
<template #content>
<div class="space-y-2 -mt-2">
<div v-for="warehouse in warehouseSummary" :key="warehouse.name"
class="flex justify-between text-xs">
<span class="text-slate-600">{{ warehouse.name }}</span>
<span class="font-bold text-slate-900">
{{ warehouse.count }} {{ warehouse.count === 1 ? 'Unidad' : 'Unidades' }}
</span>
</div>
</div>
</template>
</Card>
</div>
<!-- Footer Actions -->
<div class="flex items-center justify-end gap-4 pt-4 border-t border-slate-200">
<Button label="Cancelar" severity="secondary" text @click="cancel" />
<Button label="Confirmar Recepción Multi-Almacén"
icon="pi pi-check"
:disabled="!isFormValid"
@click="confirmReceipt" />
</div>
</template>
</div>
</template>
<style scoped>
/* Estilos adicionales si son necesarios */
</style>

View File

@ -9,17 +9,25 @@
<!-- Page Header -->
<div class="flex flex-wrap items-center justify-between gap-4">
<div class="flex flex-col gap-2">
<h1 class="text-surface-900 dark:text-white text-3xl md:text-4xl font-black leading-tight tracking-tight">
{{ warehouseData?.name || 'North Logistics Center' }}
<h1
class="text-surface-900 dark:text-white text-3xl md:text-4xl font-black leading-tight tracking-tight">
{{ warehouseData?.warehouse.name || 'Cargando...' }}
</h1>
<div class="flex flex-wrap items-center gap-3">
<div class="flex items-center gap-2 text-surface-500 dark:text-surface-400">
<i class="pi pi-map-marker text-sm"></i>
<span class="text-sm">{{ warehouseData?.location || 'Zone A, Building 4' }}</span>
<i class="pi pi-hash text-sm"></i>
<span class="text-sm">{{ warehouseData?.warehouse.code || '-' }}</span>
</div>
<div class="size-1 bg-surface-300 dark:bg-surface-600 rounded-full"></div>
<Tag :value="warehouseData?.isActive ? 'Operacional' : 'Inactivo'"
:severity="warehouseData?.isActive ? 'success' : 'secondary'" />
<div class="flex items-center gap-2 text-surface-500 dark:text-surface-400"
v-if="warehouseData?.warehouse.address">
<i class="pi pi-map-marker text-sm"></i>
<span class="text-sm">{{ warehouseData.warehouse.address }}</span>
</div>
<div class="size-1 bg-surface-300 dark:bg-surface-600 rounded-full"
v-if="warehouseData?.warehouse.address"></div>
<Tag :value="warehouseData?.warehouse.is_active ? 'Operacional' : 'Inactivo'"
:severity="warehouseData?.warehouse.is_active ? 'success' : 'secondary'" />
</div>
</div>
<div class="flex gap-3">
@ -32,54 +40,65 @@
<!-- Stats Overview -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<!-- Total SKUs -->
<Card class="shadow-sm">
<!-- <Card class="shadow-sm">
<template #content>
<div class="space-y-3">
<p class="text-sm font-medium text-surface-500 dark:text-surface-400">Total SKUs</p>
<p class="text-2xl font-bold text-surface-900 dark:text-white">1,240</p>
<div class="flex items-center gap-1 text-xs font-semibold text-green-600 dark:text-green-400">
<i class="pi pi-arrow-up text-xs"></i>
+2.4% vs mes anterior
<p class="text-2xl font-bold text-surface-900 dark:text-white">
{{ warehouseData?.stocks.length || 0 }}
</p>
<div class="flex items-center gap-1 text-xs font-semibold text-blue-600 dark:text-blue-400">
<i class="pi pi-box text-xs"></i>
Productos en stock
</div>
</div>
</template>
</Card>
</Card>
-->
<!-- Low Stock Items -->
<Card class="shadow-sm border-l-4 border-l-red-500">
<!-- <Card class="shadow-sm border-l-4 border-l-red-500">
<template #content>
<div class="space-y-3">
<p class="text-sm font-medium text-surface-500 dark:text-surface-400">Items con Stock Bajo</p>
<p class="text-2xl font-bold text-surface-900 dark:text-white">18</p>
<p class="text-2xl font-bold text-surface-900 dark:text-white">
{{ warehouseData?.stocks.filter(s => s.stock_min && s.stock < s.stock_min).length || 0 }}
</p>
<div class="flex items-center gap-1 text-xs font-semibold text-red-600 dark:text-red-400">
<i class="pi pi-exclamation-triangle text-xs"></i>
Acción requerida
</div>
</div>
</template>
</Card>
</Card> -->
<!-- Movements 24h -->
<Card class="shadow-sm">
<!-- Total Items -->
<!-- <Card class="shadow-sm">
<template #content>
<div class="space-y-3">
<p class="text-sm font-medium text-surface-500 dark:text-surface-400">Movimientos (24h)</p>
<p class="text-2xl font-bold text-surface-900 dark:text-white">142</p>
<p class="text-sm font-medium text-surface-500 dark:text-surface-400">Items Totales</p>
<p class="text-2xl font-bold text-surface-900 dark:text-white">
{{ warehouseData?.items.length || 0 }}
</p>
<div class="flex items-center gap-1 text-xs font-semibold text-blue-600 dark:text-blue-400">
<i class="pi pi-refresh text-xs"></i>
Alta actividad
<i class="pi pi-database text-xs"></i>
Items registrados
</div>
</div>
</template>
</Card>
</Card> -->
<!-- Warehouse Utilization -->
<!-- Total Stock -->
<Card class="shadow-sm">
<template #content>
<div class="space-y-3">
<p class="text-sm font-medium text-surface-500 dark:text-surface-400">Utilización del Almacén</p>
<p class="text-2xl font-bold text-surface-900 dark:text-white">84%</p>
<ProgressBar :value="84" :showValue="false" class="h-2" />
<p class="text-sm font-medium text-surface-500 dark:text-surface-400">Stock Total</p>
<p class="text-2xl font-bold text-surface-900 dark:text-white">
{{warehouseData?.stocks.reduce((sum, s) => sum + s.stock, 0).toLocaleString() || 0}}
</p>
<div class="flex items-center gap-1 text-xs font-semibold text-green-600 dark:text-green-400">
<i class="pi pi-chart-bar text-xs"></i>
Unidades en almacén
</div>
</div>
</template>
</Card>
@ -97,41 +116,22 @@
<span>Stock Actual</span>
</div>
</template>
<!-- Filters -->
<div class="flex flex-wrap items-center justify-end gap-3 mb-4">
<Select
v-model="selectedCategory"
:options="categoryOptions"
optionLabel="label"
optionValue="value"
placeholder="Todas las Categorías"
class="w-full md:w-48"
/>
<Select
v-model="selectedStockLevel"
:options="stockLevelOptions"
optionLabel="label"
optionValue="value"
placeholder="Todos los Niveles"
class="w-full md:w-48"
/>
<Select v-model="selectedCategory" :options="categoryOptions" optionLabel="label"
optionValue="value" placeholder="Todas las Categorías" class="w-full md:w-48" />
<Select v-model="selectedStockLevel" :options="stockLevelOptions" optionLabel="label"
optionValue="value" placeholder="Todos los Niveles" class="w-full md:w-48" />
<Button icon="pi pi-filter" outlined severity="secondary" />
</div>
<!-- Current Stock Table -->
<DataTable
:value="inventoryData"
:paginator="true"
:rows="10"
<DataTable :value="inventoryData" :paginator="true" :rows="10"
:rowsPerPageOptions="[10, 25, 50]"
paginatorTemplate="FirstPageLink PrevPageLink PageLinks NextPageLink LastPageLink RowsPerPageDropdown CurrentPageReport"
currentPageReportTemplate="Mostrando {first} a {last} de {totalRecords} resultados"
:loading="loading"
stripedRows
responsiveLayout="scroll"
class="text-sm"
>
:loading="loading" stripedRows responsiveLayout="scroll" class="text-sm">
<Column field="sku" header="SKU" sortable class="font-mono">
<template #body="slotProps">
<span class="font-semibold">{{ slotProps.data.sku }}</span>
@ -147,7 +147,8 @@
</Column>
<Column field="quantity" header="Cantidad" sortable>
<template #body="slotProps">
<span class="font-bold tabular-nums">{{ slotProps.data.quantity.toLocaleString() }}</span>
<span class="font-bold tabular-nums">{{ slotProps.data.quantity.toLocaleString()
}}</span>
</template>
</Column>
<Column field="unit" header="Unidad" sortable />
@ -161,10 +162,8 @@
</Column>
<Column field="status" header="Estado" sortable>
<template #body="slotProps">
<Tag
:value="slotProps.data.status"
:severity="getStatusSeverity(slotProps.data.status)"
/>
<Tag :value="slotProps.data.status"
:severity="getStatusSeverity(slotProps.data.status)" />
</template>
</Column>
<Column field="lastUpdate" header="Última Actualización" sortable>
@ -175,8 +174,10 @@
<Column header="Acciones" :exportable="false">
<template #body="slotProps">
<div class="flex gap-2">
<Button icon="pi pi-eye" outlined rounded size="small" severity="secondary" @click="viewItem(slotProps.data)" />
<Button icon="pi pi-pencil" outlined rounded size="small" @click="editItem(slotProps.data)" />
<Button icon="pi pi-eye" outlined rounded size="small" severity="secondary"
@click="viewItem(slotProps.data)" />
<Button icon="pi pi-pencil" outlined rounded size="small"
@click="editItem(slotProps.data)" />
</div>
</template>
</Column>
@ -191,27 +192,18 @@
<span>Historial de Movimientos</span>
</div>
</template>
<!-- Movement History Table -->
<DataTable
:value="movementHistory"
:paginator="true"
:rows="10"
<DataTable :value="movementHistory" :paginator="true" :rows="10"
:rowsPerPageOptions="[10, 25, 50]"
paginatorTemplate="FirstPageLink PrevPageLink PageLinks NextPageLink LastPageLink RowsPerPageDropdown CurrentPageReport"
currentPageReportTemplate="Mostrando {first} a {last} de {totalRecords} resultados"
:loading="loading"
stripedRows
responsiveLayout="scroll"
class="text-sm"
>
:loading="loading" stripedRows responsiveLayout="scroll" class="text-sm">
<Column field="date" header="Fecha" sortable />
<Column field="type" header="Tipo" sortable>
<template #body="slotProps">
<Tag
:value="slotProps.data.type"
:severity="getMovementTypeSeverity(slotProps.data.type)"
/>
<Tag :value="slotProps.data.type"
:severity="getMovementTypeSeverity(slotProps.data.type)" />
</template>
</Column>
<Column field="product" header="Producto" sortable />
@ -231,7 +223,7 @@
<!-- Secondary Section: Recent Movements & Insights -->
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
<!-- Quick Activity Log -->
<Card class="lg:col-span-2 shadow-sm">
<!-- <Card class="lg:col-span-2 shadow-sm">
<template #header>
<div class="flex items-center justify-between p-4">
<h3 class="font-bold text-surface-900 dark:text-white">Registro Rápido de Actividad</h3>
@ -240,8 +232,8 @@
</template>
<template #content>
<div class="space-y-4">
<div v-for="activity in recentActivities" :key="activity.id"
class="flex items-center gap-4 py-3 border-b border-surface-200 dark:border-surface-700 last:border-b-0">
<div v-for="activity in recentActivities" :key="activity.id"
class="flex items-center gap-4 py-3 border-b border-surface-200 dark:border-surface-700 last:border-b-0">
<div :class="[
'flex items-center justify-center size-10 rounded-full',
activity.type === 'in' ? 'bg-green-100 dark:bg-green-900/30' : 'bg-red-100 dark:bg-red-900/30'
@ -262,16 +254,18 @@
</div>
</div>
</template>
</Card>
</Card> -->
<!-- Warehouse Health Insights -->
<Card class="shadow-sm bg-primary-50 dark:bg-primary-900/10 border border-primary-200 dark:border-primary-800">
<!-- <Card
class="shadow-sm bg-primary-50 dark:bg-primary-900/10 border border-primary-200 dark:border-primary-800">
<template #content>
<div class="space-y-4">
<div>
<h3 class="font-bold text-surface-900 dark:text-white mb-2">Salud del Almacén</h3>
<p class="text-sm text-surface-600 dark:text-surface-300 leading-relaxed">
North Logistics Center está actualmente al 84% de capacidad. Recomendamos auditar la Zona B
North Logistics Center está actualmente al 84% de capacidad. Recomendamos auditar la
Zona B
para optimización de espacio potencial.
</p>
</div>
@ -291,23 +285,20 @@
</div>
</div>
<Button
label="Generar Reporte Completo"
class="w-full"
outlined
@click="generateReport"
/>
<Button label="Generar Reporte Completo" class="w-full" outlined @click="generateReport" />
</div>
</template>
</Card>
</Card> -->
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { ref, onMounted, computed } from 'vue';
import { useRouter, useRoute } from 'vue-router';
import { useToast } from 'primevue/usetoast';
import { warehouseService } from '../services/warehouseService';
import type { WarehouseDetailData } from '../types/warehouse';
// PrimeVue Components
import Toast from 'primevue/toast';
@ -320,14 +311,13 @@ import TabPanel from 'primevue/tabpanel';
import DataTable from 'primevue/datatable';
import Column from 'primevue/column';
import Select from 'primevue/select';
import ProgressBar from 'primevue/progressbar';
const router = useRouter();
const route = useRoute();
const toast = useToast();
// Reactive State
const warehouseData = ref<any>(null);
const warehouseData = ref<WarehouseDetailData | null>(null);
const loading = ref(false);
const activeTab = ref(0);
const selectedCategory = ref('all');
@ -335,9 +325,9 @@ const selectedStockLevel = ref('all');
// Breadcrumb
const breadcrumbHome = ref({ icon: 'pi pi-home', to: '/' });
const breadcrumbItems = ref([
const breadcrumbItems = computed(() => [
{ label: 'Almacenes', to: '/warehouse' },
{ label: 'North Logistics Center' }
{ label: warehouseData.value?.warehouse.name || 'Cargando...' }
]);
// Filter Options
@ -355,49 +345,8 @@ const stockLevelOptions = [
{ label: 'Sobrestock', value: 'overstock' },
];
// Mock Data - Current Stock
const inventoryData = ref([
{
sku: 'WH-2024-001',
product: 'Laptop Dell XPS 15',
category: 'Electrónica',
quantity: 450,
unit: 'Unidades',
location: 'Rack A-12',
status: 'En Stock',
lastUpdate: 'Hace 2 horas'
},
{
sku: 'WH-2024-002',
product: 'Silla Ergonómica Pro',
category: 'Mobiliario',
quantity: 85,
unit: 'Unidades',
location: 'Zona B-04',
status: 'Stock Bajo',
lastUpdate: 'Hace 4 horas'
},
{
sku: 'WH-2024-003',
product: 'Monitor LG 27" 4K',
category: 'Electrónica',
quantity: 320,
unit: 'Unidades',
location: 'Rack A-15',
status: 'En Stock',
lastUpdate: 'Hace 1 hora'
},
{
sku: 'WH-2024-004',
product: 'Teclado Mecánico RGB',
category: 'Periféricos',
quantity: 12,
unit: 'Unidades',
location: 'Rack C-08',
status: 'Stock Crítico',
lastUpdate: 'Hace 30 min'
},
]);
// Inventory Data from API
const inventoryData = ref<any[]>([]);
// Mock Data - Movement History
const movementHistory = ref([
@ -427,33 +376,33 @@ const movementHistory = ref([
},
]);
// Mock Data - Recent Activities
const recentActivities = ref([
{
id: 1,
type: 'in',
action: 'Entrada de Stock',
product: 'Laptop Dell XPS 15 - 50 unidades',
quantity: '+50',
time: 'Hace 2 horas'
},
{
id: 2,
type: 'out',
action: 'Salida de Stock',
product: 'Monitor LG 27" - 25 unidades',
quantity: '-25',
time: 'Hace 4 horas'
},
{
id: 3,
type: 'in',
action: 'Entrada de Stock',
product: 'Silla Ergonómica - 100 unidades',
quantity: '+100',
time: 'Hace 6 horas'
},
]);
// Mock Data - Recent Activities (unused - template is commented out)
// const recentActivities = ref([
// {
// id: 1,
// type: 'in',
// action: 'Entrada de Stock',
// product: 'Laptop Dell XPS 15 - 50 unidades',
// quantity: '+50',
// time: 'Hace 2 horas'
// },
// {
// id: 2,
// type: 'out',
// action: 'Salida de Stock',
// product: 'Monitor LG 27" - 25 unidades',
// quantity: '-25',
// time: 'Hace 4 horas'
// },
// {
// id: 3,
// type: 'in',
// action: 'Entrada de Stock',
// product: 'Silla Ergonómica - 100 unidades',
// quantity: '+100',
// time: 'Hace 6 horas'
// },
// ]);
// Methods
const getStatusSeverity = (status: string) => {
@ -498,49 +447,99 @@ const editItem = (item: any) => {
});
};
const viewAllMovements = () => {
activeTab.value = 1;
// Unused methods (template sections are commented out)
// const viewAllMovements = () => {
// activeTab.value = 1;
// };
// const generateReport = () => {
// toast.add({
// severity: 'success',
// summary: 'Generando Reporte',
// detail: 'El reporte se está generando...',
// life: 3000
// });
// };
// Helper function to get stock status
const getStockStatus = (stock: number, stockMin: number | null) => {
if (!stockMin) return 'En Stock';
if (stock === 0) return 'Sin Stock';
if (stock < stockMin) return 'Stock Bajo';
if (stock < stockMin * 1.5) return 'Stock Crítico';
return 'En Stock';
};
const generateReport = () => {
toast.add({
severity: 'success',
summary: 'Generando Reporte',
detail: 'El reporte se está generando...',
life: 3000
});
// Helper function to format date
const formatRelativeTime = (dateString: string) => {
const date = new Date(dateString);
const now = new Date();
const diffMs = now.getTime() - date.getTime();
const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
const diffDays = Math.floor(diffHours / 24);
if (diffDays > 0) return `Hace ${diffDays} día${diffDays > 1 ? 's' : ''}`;
if (diffHours > 0) return `Hace ${diffHours} hora${diffHours > 1 ? 's' : ''}`;
return 'Hace menos de 1 hora';
};
// Lifecycle
onMounted(async () => {
const warehouseId = route.params.id;
if (!warehouseId) {
toast.add({
severity: 'error',
summary: 'Error',
detail: 'ID de almacén no válido',
life: 3000
});
router.push({ name: 'Warehouses' });
return;
}
loading.value = true;
try {
// TODO: Cargar datos del almacén desde el API
// const response = await warehouseService.getWarehouseById(warehouseId);
// warehouseData.value = response.data;
// Mock data por ahora
warehouseData.value = {
id: warehouseId,
name: 'North Logistics Center',
location: 'Zone A, Building 4',
isActive: true,
};
const response = await warehouseService.getWarehouseById(Number(warehouseId));
warehouseData.value = response.data;
// Mapear stocks a formato de la tabla
inventoryData.value = response.data.stocks.map(stock => ({
sku: stock.product.sku,
product: stock.product.name,
category: stock.product.description || 'Sin categoría',
quantity: stock.stock,
unit: 'Unidades',
location: `Almacén ${response.data.warehouse.code}`,
status: getStockStatus(stock.stock, stock.stock_min),
lastUpdate: formatRelativeTime(stock.updated_at),
productId: stock.product_id,
stockMin: stock.stock_min,
stockMax: stock.stock_max,
}));
toast.add({
severity: 'success',
summary: 'Datos Cargados',
detail: `Almacén ${response.data.warehouse.name} cargado exitosamente`,
life: 3000
});
} catch (error) {
console.error('Error al cargar los datos del almacén:', error);
toast.add({
severity: 'error',
summary: 'Error',
detail: 'Error al cargar los datos del almacén',
life: 3000
});
router.push({ name: 'Warehouses' });
} finally {
loading.value = false;
}
});
</script>

View File

@ -10,7 +10,6 @@ import InputSwitch from 'primevue/inputswitch';
import Textarea from 'primevue/textarea';
import Dropdown from 'primevue/dropdown';
import Chip from 'primevue/chip';
import Avatar from 'primevue/avatar';
import Toast from 'primevue/toast';
import { warehouseService } from '../services/warehouseService';
import { useClassificationStore } from '../stores/classificationStore';
@ -44,13 +43,16 @@ const isSubmitting = ref(false);
// Categories from store
const availableClassifications = computed(() => {
return classificationStore.activeClassifications.map(cls => ({
// Recursive function to ensure all children arrays are defined
const mapClassification = (cls: any): any => ({
id: cls.id,
name: cls.name,
code: cls.code,
parent_id: cls.parent_id,
children: cls.children || []
}));
children: (cls.children || []).map(mapClassification)
});
return classificationStore.activeClassifications.map(mapClassification);
});
// Get root classifications (categories without parent)
@ -87,7 +89,7 @@ const addSelectedCategory = () => {
// If subcategory is selected, use it instead
if (selectedSubCategory.value) {
const subcat = parent.children?.find((c: any) => c.id === selectedSubCategory.value);
const subcat = parent.children.find((c: any) => c.id === selectedSubCategory.value);
if (subcat) {
categoryToAdd = subcat;
categoryName = `${parent.name} > ${subcat.name}`;
@ -95,7 +97,7 @@ const addSelectedCategory = () => {
}
// Check if already added
if (assignedCategories.value.some(c => c.id === categoryToAdd.id)) {
if (assignedCategories.value.some((c: any) => c.id === categoryToAdd.id)) {
toast.add({
severity: 'warn',
summary: 'Categoría ya agregada',
@ -108,7 +110,9 @@ const addSelectedCategory = () => {
assignedCategories.value.push({
id: categoryToAdd.id,
name: categoryName,
code: categoryToAdd.code
code: categoryToAdd.code,
parent_id: categoryToAdd.parent_id,
children: categoryToAdd.children || []
});
// Reset selections
@ -116,27 +120,27 @@ const addSelectedCategory = () => {
selectedSubCategory.value = null;
};
// Staff
const assignedStaff = ref([
{
id: 1,
name: 'Jane Cooper',
role: 'Gerente de Almacén',
avatar: 'https://i.pravatar.cc/150?img=1'
},
{
id: 2,
name: 'Cody Fisher',
role: 'Operador de Montacargas',
avatar: 'https://i.pravatar.cc/150?img=2'
},
{
id: 3,
name: 'Esther Howard',
role: 'Auxiliar de Recepción',
avatar: 'https://i.pravatar.cc/150?img=3'
}
]);
// Staff (unused, for future use)
// const assignedStaff = ref([
// {
// id: 1,
// name: 'Jane Cooper',
// role: 'Gerente de Almacén',
// avatar: 'https://i.pravatar.cc/150?img=1'
// },
// {
// id: 2,
// name: 'Cody Fisher',
// role: 'Operador de Montacargas',
// avatar: 'https://i.pravatar.cc/150?img=2'
// },
// {
// id: 3,
// name: 'Esther Howard',
// role: 'Auxiliar de Recepción',
// avatar: 'https://i.pravatar.cc/150?img=3'
// }
// ]);
const saveDisabled = ref(false);
@ -144,17 +148,18 @@ const removeCategory = (id: number) => {
assignedCategories.value = assignedCategories.value.filter(cat => cat.id !== id);
};
const addStaff = () => {
console.log('Add staff');
};
// Unused staff methods (for future use)
// const addStaff = () => {
// console.log('Add staff');
// };
const editStaff = (id: number) => {
console.log('Edit staff:', id);
};
// const editStaff = (id: number) => {
// console.log('Edit staff:', id);
// };
const removeStaff = (id: number) => {
assignedStaff.value = assignedStaff.value.filter(staff => staff.id !== id);
};
// const removeStaff = (id: number) => {
// assignedStaff.value = assignedStaff.value.filter(staff => staff.id !== id);
// };
const cancel = () => {
router.push('/warehouse');

View File

@ -1,12 +1,17 @@
<!DOCTYPE html>
<html class="light" lang="en"><head>
<meta charset="utf-8"/>
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
<title>WMS - Batch Add Inventory Items</title>
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<script>
<html class="light" lang="es">
<head>
<meta charset="utf-8" />
<meta content="width=device-width, initial-scale=1.0" name="viewport" />
<title>Multi-Warehouse Goods Receipt Entry | Logistics Pro</title>
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;900&amp;display=swap"
rel="stylesheet" />
<link
href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap"
rel="stylesheet" />
<script id="tailwind-config">
tailwind.config = {
darkMode: "class",
theme: {
@ -17,321 +22,427 @@
"background-dark": "#101922",
},
fontFamily: {
"display": ["Inter", "sans-serif"]
},
borderRadius: {
"DEFAULT": "0.25rem",
"lg": "0.5rem",
"xl": "0.75rem",
"full": "9999px"
"display": ["Inter"]
},
borderRadius: { "DEFAULT": "0.25rem", "lg": "0.5rem", "xl": "0.75rem", "full": "9999px" },
},
},
}
</script>
<style type="text/tailwindcss">
@layer utilities {
.scrollbar-hide::-webkit-scrollbar {
display: none;
}
<style>
body {
font-family: 'Inter', sans-serif;
}
.material-symbols-outlined {
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
}
</style>
</head>
<body class="bg-background-light dark:bg-background-dark font-display text-gray-800 dark:text-gray-200 overflow-hidden">
<div class="flex h-screen w-full">
<aside class="flex w-64 flex-col border-r border-gray-200 dark:border-gray-800 bg-white dark:bg-background-dark">
<div class="flex h-full flex-col justify-between p-4">
<div class="flex flex-col gap-4">
<div class="flex items-center gap-3 p-2">
<div class="bg-center bg-no-repeat aspect-square bg-cover rounded-full size-10" style='background-image: url("https://lh3.googleusercontent.com/aida-public/AB6AXuCztlHhjKvu2qkn2Xi2zFHagNsNToKwcTg3vQr0KtTqBCo13dK1yyz9HzB2uLCiciLyDfnrf7pREvdblPqCcUiN0HqlSbkFwY1dpQLMbJ4hmpVgHVWaLaUCMXju06qyGQSdg2ChGVcbTQIrk-RNI2-hDOFnfrI1PD89RNSsByXGRsdkYWSyEYFOFk7bT4l7aIaasB6cdVxDfNwJdvVx15wb7-qOHZHFTPMbrkkzmjGec-f7iVqTi5U1ykNDclBSezBM97TfXajTwRJE");'></div>
<div class="flex flex-col">
<h1 class="text-gray-900 dark:text-white text-base font-medium leading-normal">Admin User</h1>
<p class="text-gray-500 dark:text-gray-400 text-sm font-normal leading-normal">Warehouse Manager</p>
</div>
</div>
<nav class="flex flex-col gap-2">
<a class="flex items-center gap-3 px-3 py-2 text-gray-700 dark:text-gray-300 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800" href="#">
<span class="material-symbols-outlined">dashboard</span>
<p class="text-sm font-medium leading-normal">Dashboard</p>
</a>
<a class="flex items-center gap-3 px-3 py-2 rounded-lg bg-primary/10 text-primary dark:bg-primary/20" href="#">
<span class="material-symbols-outlined">warehouse</span>
<p class="text-sm font-medium leading-normal">Inventory</p>
</a>
<a class="flex items-center gap-3 px-3 py-2 text-gray-700 dark:text-gray-300 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800" href="#">
<span class="material-symbols-outlined">receipt_long</span>
<p class="text-sm font-medium leading-normal">Orders</p>
</a>
<a class="flex items-center gap-3 px-3 py-2 text-gray-700 dark:text-gray-300 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800" href="#">
<span class="material-symbols-outlined">local_shipping</span>
<p class="text-sm font-medium leading-normal">Suppliers</p>
</a>
<a class="flex items-center gap-3 px-3 py-2 text-gray-700 dark:text-gray-300 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800" href="#">
<span class="material-symbols-outlined">pie_chart</span>
<p class="text-sm font-medium leading-normal">Reports</p>
</a>
</nav>
</div>
<div class="flex flex-col gap-2">
<nav class="flex flex-col gap-1">
<a class="flex items-center gap-3 px-3 py-2 text-gray-700 dark:text-gray-300 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800" href="#">
<span class="material-symbols-outlined">settings</span>
<p class="text-sm font-medium leading-normal">Settings</p>
</a>
<a class="flex items-center gap-3 px-3 py-2 text-gray-700 dark:text-gray-300 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800" href="#">
<span class="material-symbols-outlined">help</span>
<p class="text-sm font-medium leading-normal">Help</p>
</a>
</nav>
<button class="flex min-w-[84px] cursor-pointer items-center justify-center overflow-hidden rounded-lg h-10 px-4 bg-gray-200 dark:bg-gray-800 text-gray-800 dark:text-gray-200 text-sm font-bold leading-normal hover:bg-gray-300 dark:hover:bg-gray-700">
<span class="truncate">Logout</span>
</button>
</div>
</div>
</aside>
<main class="flex-1 flex flex-col h-screen overflow-hidden">
<header class="flex flex-none items-center justify-between whitespace-nowrap border-b border-solid border-gray-200 dark:border-gray-800 px-10 py-3 bg-white dark:bg-background-dark z-10">
<div class="flex items-center gap-4 text-gray-900 dark:text-white">
<div class="size-6 text-primary">
<svg fill="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M21.435 7.182a.75.75 0 0 0-.87-.11L12 10.435 3.435 7.072a.75.75 0 0 0-.87.11.75.75 0 0 0-.11.87l2.122 7.878a.75.75 0 0 0 .869.59l6-1.635a.75.75 0 0 0 .108 0l6 1.635a.75.75 0 0 0 .87-.59l2.12-7.878a.75.75 0 0 0-.11-.87zM12 12.18l-5.693-1.55L12 4.288l5.693 6.342L12 12.18z"></path>
</svg>
</div>
<h2 class="text-lg font-bold leading-tight tracking-[-0.015em]">WMS Dashboard</h2>
</div>
<div class="flex flex-1 justify-end items-center gap-4">
<label class="relative grow max-w-sm">
<span class="material-symbols-outlined absolute left-3 top-1/2 -translate-y-1/2 text-gray-500">search</span>
<input class="form-input w-full rounded-lg border-none bg-gray-100 dark:bg-gray-800 h-10 pl-10 pr-4 text-sm text-gray-900 dark:text-white placeholder:text-gray-500 focus:ring-primary" placeholder="Search items, orders..." value=""/>
</label>
<div class="flex gap-2">
<button class="flex items-center justify-center rounded-lg h-10 w-10 bg-gray-100 dark:bg-gray-800 text-gray-800 dark:text-gray-200 hover:bg-gray-200 dark:hover:bg-gray-700">
<span class="material-symbols-outlined text-xl">notifications</span>
</button>
<button class="flex items-center justify-center rounded-lg h-10 w-10 bg-gray-100 dark:bg-gray-800 text-gray-800 dark:text-gray-200 hover:bg-gray-200 dark:hover:bg-gray-700">
<span class="material-symbols-outlined text-xl">help_outline</span>
</button>
</div>
<div class="bg-center bg-no-repeat aspect-square bg-cover rounded-full size-10" style='background-image: url("https://lh3.googleusercontent.com/aida-public/AB6AXuBNpxncwdjcD3fLZbRbit_NZyuFBZhcpV-Bvdlu6NiZL3kb65hsFRsDkTNHtC0zJxG3HGV1TInl_DCfafj3axNGSwW4-UNj1sZWhiHCYE2aK9hm-FYjrNGiEh0UqKya1EAYMTM5Z4k8qKOWPEPdIaZz9X98tPC5FIn5lbRRusCTuQmgRL-QxK9SdIMA3TflImwA1vyh3zq44j8EkkNTQWf94-e82GDHs5MwHIkK0S-Gg4d950IyRTpoABSa9qXA5yPzoaT9jCGjCodL");'></div>
</div>
</header>
<div class="flex-1 overflow-y-auto p-6 lg:p-10 pb-24 relative">
<div class="mx-auto max-w-7xl">
<div class="flex flex-wrap gap-2 mb-6">
<a class="text-gray-500 dark:text-gray-400 text-sm font-medium leading-normal" href="#">Dashboard</a>
<span class="text-gray-500 dark:text-gray-400 text-sm font-medium leading-normal">/</span>
<a class="text-gray-500 dark:text-gray-400 text-sm font-medium leading-normal" href="#">Inventory</a>
<span class="text-gray-500 dark:text-gray-400 text-sm font-medium leading-normal">/</span>
<span class="text-gray-800 dark:text-gray-200 text-sm font-medium leading-normal">Batch Add Items</span>
</div>
<div class="flex flex-wrap justify-between items-center gap-4 mb-8">
<div class="flex flex-col gap-2">
<h1 class="text-gray-900 dark:text-white text-3xl font-bold tracking-tight">Add Items to Inventory</h1>
<p class="text-gray-500 dark:text-gray-400 text-base font-normal leading-normal">Search and add multiple products to your stock in a single batch.</p>
</div>
</div>
<div class="bg-white dark:bg-gray-900/50 p-6 rounded-xl border border-gray-200 dark:border-gray-800 mb-8 shadow-sm">
<h2 class="text-lg font-bold text-gray-900 dark:text-white mb-4">Select Products from Catalog</h2>
<div class="flex flex-col lg:flex-row gap-4 items-end">
<div class="flex-1 w-full relative">
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Search Catalog</label>
<div class="relative">
<span class="material-symbols-outlined absolute left-3 top-1/2 -translate-y-1/2 text-gray-500">search</span>
<input class="form-input w-full rounded-lg border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 text-gray-900 dark:text-white pl-10 h-11 focus:ring-primary focus:border-primary" placeholder="Type Product Name or SKU..." type="text"/>
</div>
<div class="absolute top-full left-0 w-full mt-2 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-xl shadow-2xl z-20 p-5 grid grid-cols-1 md:grid-cols-12 gap-6">
<div class="md:col-span-4">
<p class="text-xs font-semibold uppercase tracking-wider text-gray-400 mb-2">Selected Product</p>
<div class="flex items-center gap-3">
<div class="w-12 h-12 rounded bg-gray-100 dark:bg-gray-700 flex items-center justify-center">
<span class="material-symbols-outlined text-gray-400">image</span>
</div>
<div>
<p class="font-bold text-gray-900 dark:text-white">Smart Display Panel</p>
<p class="text-xs text-gray-500">SKU: DISP-PNL-SD-001</p>
</div>
</div>
</div>
<div class="md:col-span-2">
<label class="block text-xs font-semibold uppercase tracking-wider text-gray-400 mb-2">Medida</label>
<select class="form-select w-full rounded-lg border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 text-sm focus:ring-primary">
<option value="24">24</option>
<option value="36">36</option>
<option value="64">64</option>
<option value="85">85</option>
</select>
</div>
<div class="md:col-span-2">
<label class="block text-xs font-semibold uppercase tracking-wider text-gray-400 mb-2">Resolución</label>
<select class="form-select w-full rounded-lg border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 text-sm focus:ring-primary">
<option value="1080">1080</option>
<option value="2K">2K</option>
<option value="4K">4K</option>
<option value="8K">8K</option>
</select>
</div>
<div class="md:col-span-2">
<label class="block text-xs font-semibold uppercase tracking-wider text-gray-400 mb-2">Qty</label>
<input class="form-input w-full rounded-lg border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 text-sm focus:ring-primary" type="number" value="10"/>
</div>
<div class="md:col-span-2 flex items-end">
<button class="w-full bg-primary text-white h-11 px-4 rounded-lg font-bold hover:bg-primary/90 flex items-center justify-center gap-1">
<span class="material-symbols-outlined text-lg">add</span> Add
</button>
</div>
</div>
</div>
</div>
</div>
<div class="bg-white dark:bg-gray-900/50 rounded-xl border border-gray-200 dark:border-gray-800 overflow-hidden shadow-sm">
<div class="px-6 py-4 border-b border-gray-200 dark:border-gray-800 flex justify-between items-center">
<h2 class="text-lg font-bold text-gray-900 dark:text-white">Queued Items (4)</h2>
<button class="text-sm font-medium text-red-500 hover:text-red-600">Clear All</button>
</div>
<div class="overflow-x-auto">
<table class="w-full text-left border-collapse">
<thead>
<tr class="bg-gray-50 dark:bg-gray-800/50">
<th class="px-6 py-4 text-xs font-bold text-gray-500 dark:text-gray-400 uppercase tracking-wider">Product Name</th>
<th class="px-6 py-4 text-xs font-bold text-gray-500 dark:text-gray-400 uppercase tracking-wider">SKU</th>
<th class="px-6 py-4 text-xs font-bold text-gray-500 dark:text-gray-400 uppercase tracking-wider">Variant</th>
<th class="px-6 py-4 text-xs font-bold text-gray-500 dark:text-gray-400 uppercase tracking-wider">Quantity</th>
<th class="px-6 py-4 text-xs font-bold text-gray-500 dark:text-gray-400 uppercase tracking-wider">Unit</th>
<th class="px-6 py-4 text-xs font-bold text-gray-500 dark:text-gray-400 uppercase tracking-wider text-right">Action</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-100 dark:divide-gray-800">
<tr class="hover:bg-gray-50/50 dark:hover:bg-gray-800/30 transition-colors">
<td class="px-6 py-4">
<div class="flex items-center gap-3">
<div class="w-8 h-8 rounded bg-gray-100 dark:bg-gray-800 overflow-hidden">
<img alt="product" class="w-full h-full object-cover" src="https://lh3.googleusercontent.com/aida-public/AB6AXuBMbZhSSPYoHEi3akXSqZXm-T6EghACuHJJ5NYeJ8g4XJIKtqXRhCSQ1kFcvo7Txk01ryR-r6RJ61NzPLENWMywvUVnCkQglsizZG0NKPBwRFQjXvtDULkGbGFm6EPocyHGfuzKJ0EoDr601zMBI34mfPCgn9AAaRkTMj2Ize2nCUcanlGn7QJEp6BdNcmf3JFlk-51jPTXj1-mM4Pw6AGIvarhxnZqtEAji6qRF7evVTR_56c48h5if21S3jD-wVhNVp8AltVn8KEH"/>
</div>
<span class="font-medium">Smart Display Panel</span>
</div>
</td>
<td class="px-6 py-4 text-sm text-gray-500">DISP-PNL-SD-001</td>
<td class="px-6 py-4">
<div class="flex flex-wrap gap-1">
<span class="px-2.5 py-1 text-[10px] font-semibold bg-blue-50 dark:bg-blue-900/30 text-blue-600 dark:text-blue-400 rounded-full border border-blue-100 dark:border-blue-800/50 uppercase tracking-tight">Medida: 85</span>
<span class="px-2.5 py-1 text-[10px] font-semibold bg-purple-50 dark:bg-purple-900/30 text-purple-600 dark:text-purple-400 rounded-full border border-purple-100 dark:border-purple-800/50 uppercase tracking-tight">Resolución: 4K</span>
</div>
</td>
<td class="px-6 py-4">
<input class="w-20 form-input h-8 rounded border-gray-300 dark:border-gray-700 bg-transparent text-sm" type="number" value="25"/>
</td>
<td class="px-6 py-4 text-sm">pcs</td>
<td class="px-6 py-4 text-right">
<button class="text-gray-400 hover:text-red-500 transition-colors">
<span class="material-symbols-outlined text-xl">delete</span>
</button>
</td>
</tr>
<tr class="hover:bg-gray-50/50 dark:hover:bg-gray-800/30 transition-colors">
<td class="px-6 py-4">
<div class="flex items-center gap-3">
<div class="w-8 h-8 rounded bg-gray-100 dark:bg-gray-800 overflow-hidden">
<img alt="product" class="w-full h-full object-cover" src="https://lh3.googleusercontent.com/aida-public/AB6AXuBBMBDEGh1exdBX1lU--TOCsUwfsFkYhrl8DxxvxgH-hKLkcuVtuGP_ZhRS5l_YXFWls08Ecr-Ic748cVHqHexFMMzYTPH8YL2s9OISDgsMIwcDOPxitrPUn3RBCD_krFC8SrBXBeNC8ilcFdg_JCwD5BW5gaYjAAEUMFXyM1vd-YT27KyubBk1IKATsMdDkH-NtwAHcrPgNoXsRaAcubBQiebCG1hmxKVW3fAXoDoSqywrxhwZ7cmIuo7BTThJJ1KcczxQh1jYh_w4"/>
</div>
<span class="font-medium">Mechanical Keyboard RGB</span>
</div>
</td>
<td class="px-6 py-4 text-sm text-gray-500">KYBD-RGB-US-02</td>
<td class="px-6 py-4">
<span class="px-2.5 py-1 text-xs font-medium bg-gray-100 dark:bg-gray-800 rounded-full border border-gray-200 dark:border-gray-700">Red Switch</span>
</td>
<td class="px-6 py-4">
<input class="w-20 form-input h-8 rounded border-gray-300 dark:border-gray-700 bg-transparent text-sm" type="number" value="12"/>
</td>
<td class="px-6 py-4 text-sm">pcs</td>
<td class="px-6 py-4 text-right">
<button class="text-gray-400 hover:text-red-500 transition-colors">
<span class="material-symbols-outlined text-xl">delete</span>
</button>
</td>
</tr>
<tr class="hover:bg-gray-50/50 dark:hover:bg-gray-800/30 transition-colors">
<td class="px-6 py-4">
<div class="flex items-center gap-3">
<div class="w-8 h-8 rounded bg-gray-100 dark:bg-gray-800 overflow-hidden">
<img alt="product" class="w-full h-full object-cover" src="https://lh3.googleusercontent.com/aida-public/AB6AXuAsb9nqDON7PeKGYYAU8hmHn7B2l3waP_ewMQTpsgnpJe2vFiY-k-zncE5VEf186tDzlTwVPA6-1SgAFagugq9Anw2WXloUFTNwOrOHgAi-MNhgs0FuxRKF8XMLk6W_u6YLF5xWh3K2q317JXFSUWtURtxCi8pYM4YtHYEyuT2aIrhbAnI2LQ7ja8DvSdvpO6t1IJ6HE9RqlnAFXuLG22igxiekKT2NjRpuK5Zzu_90rBPEjb0KitG9DwiBjzKa702pccHPRqupgXke"/>
</div>
<span class="font-medium">USB-C Charging Cable 2m</span>
</div>
</td>
<td class="px-6 py-4 text-sm text-gray-500">CBL-USBC-2M</td>
<td class="px-6 py-4">
<span class="px-2.5 py-1 text-xs font-medium bg-gray-100 dark:bg-gray-800 rounded-full border border-gray-200 dark:border-gray-700">Braided</span>
</td>
<td class="px-6 py-4">
<input class="w-20 form-input h-8 rounded border-gray-300 dark:border-gray-700 bg-transparent text-sm" type="number" value="100"/>
</td>
<td class="px-6 py-4 text-sm">pcs</td>
<td class="px-6 py-4 text-right">
<button class="text-gray-400 hover:text-red-500 transition-colors">
<span class="material-symbols-outlined text-xl">delete</span>
</button>
</td>
</tr>
<tr class="hover:bg-gray-50/50 dark:hover:bg-gray-800/30 transition-colors">
<td class="px-6 py-4">
<div class="flex items-center gap-3">
<div class="w-8 h-8 rounded bg-gray-100 dark:bg-gray-800 overflow-hidden">
<img alt="product" class="w-full h-full object-cover" src="https://lh3.googleusercontent.com/aida-public/AB6AXuD1PtrsyrvB9KtL5DHjKotvi92lM1NsfdzyEkoW5DAEfSYoPI8MNsCJZDBMtrz6rXnIWApiymAIlp7Yq5P6JTQqpwaNJVw5I4G3KiefVME-D_jR_0iTDtPBxljmlcohkhIrSfK77wV9Wq1KL0yvjCB2UuDewIxBOs0RUl7_RbISQxYaSzVe9GbqnwI5cpj_N_1faxUA89ATStV6GovMDXFA881MUGKd4_ox_CzEG5zzPZYRe9JF7M33YH-rRHdxlpQWtRH9yw4X8sKp"/>
</div>
<span class="font-medium">Smart Display Panel</span>
</div>
</td>
<td class="px-6 py-4 text-sm text-gray-500">DISP-PNL-SD-001</td>
<td class="px-6 py-4">
<div class="flex flex-wrap gap-1">
<span class="px-2.5 py-1 text-[10px] font-semibold bg-blue-50 dark:bg-blue-900/30 text-blue-600 dark:text-blue-400 rounded-full border border-blue-100 dark:border-blue-800/50 uppercase tracking-tight">Medida: 36</span>
<span class="px-2.5 py-1 text-[10px] font-semibold bg-purple-50 dark:bg-purple-900/30 text-purple-600 dark:text-purple-400 rounded-full border border-purple-100 dark:border-purple-800/50 uppercase tracking-tight">Resolución: 1080</span>
</div>
</td>
<td class="px-6 py-4">
<input class="w-20 form-input h-8 rounded border-gray-300 dark:border-gray-700 bg-transparent text-sm" type="number" value="5"/>
</td>
<td class="px-6 py-4 text-sm">pcs</td>
<td class="px-6 py-4 text-right">
<button class="text-gray-400 hover:text-red-500 transition-colors">
<span class="material-symbols-outlined text-xl">delete</span>
</button>
</td>
</tr>
</tbody>
</table>
</div>
<div class="p-6 bg-gray-50/50 dark:bg-gray-800/50 border-t border-gray-200 dark:border-gray-800 text-sm text-gray-500 dark:text-gray-400">
Total items to be added: <span class="font-bold text-gray-900 dark:text-white">142 units</span> across 4 unique products.
</div>
</div>
</div>
</div>
<footer class="fixed bottom-0 right-0 left-64 bg-white dark:bg-background-dark border-t border-gray-200 dark:border-gray-800 p-4 px-10 flex justify-between items-center z-20 shadow-[0_-4px_6px_-1px_rgba(0,0,0,0.1)]">
<div class="flex items-center gap-6">
<div class="flex items-center gap-2">
<span class="text-xs font-medium text-gray-500 uppercase tracking-widest">Storage Location</span>
<select class="form-select text-sm rounded-lg border-gray-300 dark:border-gray-700 bg-gray-50 dark:bg-gray-800 focus:ring-primary h-9">
<option>Main Warehouse (A1)</option>
<option>South Storage (B4)</option>
<option>Cold Storage (C2)</option>
</select>
</div>
</div>
<div class="flex items-center gap-4">
<button class="flex min-w-[100px] cursor-pointer items-center justify-center rounded-lg h-11 px-6 bg-gray-200 dark:bg-gray-800 text-gray-800 dark:text-gray-200 text-sm font-bold leading-normal hover:bg-gray-300 dark:hover:bg-gray-700 transition-all">
Cancel
</button>
<button class="flex min-w-[200px] cursor-pointer items-center justify-center rounded-lg h-11 px-8 bg-primary text-white text-sm font-bold leading-normal tracking-[0.015em] hover:bg-primary/90 shadow-lg shadow-primary/20 transition-all">
Confirm and Save Inventory
</button>
</div>
</footer>
</main>
</div>
</body></html>
<body class="bg-background-light dark:bg-background-dark font-display min-h-screen">
<div class="flex h-screen overflow-hidden">
<aside
class="w-64 bg-white dark:bg-slate-900 border-r border-slate-200 dark:border-slate-800 flex flex-col shrink-0">
<div class="p-6 flex items-center gap-3">
<div class="w-10 h-10 bg-primary rounded-lg flex items-center justify-center text-white">
<span class="material-symbols-outlined">inventory_2</span>
</div>
<div>
<h1 class="text-slate-900 dark:text-white text-base font-bold leading-none">Logistics Pro</h1>
<p class="text-slate-500 dark:text-slate-400 text-xs mt-1">Warehouse Admin</p>
</div>
</div>
<nav class="flex-1 px-4 py-4 space-y-1">
<a class="flex items-center gap-3 px-3 py-2 text-slate-600 dark:text-slate-400 hover:bg-slate-50 dark:hover:bg-slate-800 rounded-lg transition-colors"
href="#">
<span class="material-symbols-outlined">dashboard</span>
<span class="text-sm font-medium">Dashboard</span>
</a>
<a class="flex items-center gap-3 px-3 py-2 text-slate-600 dark:text-slate-400 hover:bg-slate-50 dark:hover:bg-slate-800 rounded-lg transition-colors"
href="#">
<span class="material-symbols-outlined">package_2</span>
<span class="text-sm font-medium">Inventario</span>
</a>
<a class="flex items-center gap-3 px-3 py-2 bg-primary/10 text-primary rounded-lg transition-colors"
href="#">
<span class="material-symbols-outlined"
style="font-variation-settings: 'FILL' 1">shopping_cart</span>
<span class="text-sm font-medium">Órdenes de Compra</span>
</a>
<a class="flex items-center gap-3 px-3 py-2 text-slate-600 dark:text-slate-400 hover:bg-slate-50 dark:hover:bg-slate-800 rounded-lg transition-colors"
href="#">
<span class="material-symbols-outlined">warehouse</span>
<span class="text-sm font-medium">Almacenes</span>
</a>
<a class="flex items-center gap-3 px-3 py-2 text-slate-600 dark:text-slate-400 hover:bg-slate-50 dark:hover:bg-slate-800 rounded-lg transition-colors"
href="#">
<span class="material-symbols-outlined">bar_chart</span>
<span class="text-sm font-medium">Reportes</span>
</a>
</nav>
<div class="p-4 border-t border-slate-200 dark:border-slate-800">
<div class="flex items-center gap-3 px-3 py-2">
<div class="size-8 rounded-full bg-slate-200 dark:bg-slate-700 bg-cover bg-center"
style="background-image: url('https://lh3.googleusercontent.com/aida-public/AB6AXuDa90XPiU0x0vWPCwSkY-b0XPWxuaglsKdsGDuVxfyQKkMYEU5M9ZhbSQkCrmRlRjYEiSLJ6gAeZWIORr6MFvWrYq-WoJFzzEUf18zJjkqmJK9oU270B7r6BRVz-ynoNSS6rUNF4_PE2az4uQgysTtym0Akce2JSv5s077kdSfpUvEYPzeMM4Oi9SSMG9kAIjTiQbCrRVUcE81w8a1TwD-JHuzmXutnjRS3BaPKKcT57SiJhYhoSkL1waAxm7SWR_fWDpTzxbnDlHnl')">
</div>
<div class="flex-1 min-w-0">
<p class="text-sm font-medium text-slate-900 dark:text-white truncate">Carlos Ruiz</p>
<p class="text-xs text-slate-500 truncate">Supervisor</p>
</div>
</div>
</div>
</aside>
<main class="flex-1 flex flex-col min-w-0 overflow-hidden">
<header
class="h-16 bg-white dark:bg-slate-900 border-b border-slate-200 dark:border-slate-800 flex items-center justify-between px-8 shrink-0">
<div class="flex items-center gap-4">
<div class="flex items-center gap-2 text-slate-400 text-sm font-medium">
<a class="hover:text-primary" href="#">Inicio</a>
<span class="material-symbols-outlined text-xs">chevron_right</span>
<a class="hover:text-primary" href="#">Órdenes de Compra</a>
<span class="material-symbols-outlined text-xs">chevron_right</span>
<span class="text-slate-900 dark:text-white">Registrar Entrada (Multialmacén)</span>
</div>
</div>
<div class="flex items-center gap-4">
<div class="relative w-64">
<span
class="material-symbols-outlined absolute left-3 top-1/2 -translate-y-1/2 text-slate-400 text-lg">search</span>
<input
class="w-full pl-10 pr-4 py-1.5 bg-slate-100 dark:bg-slate-800 border-none rounded-lg text-sm focus:ring-2 focus:ring-primary/20"
placeholder="Buscar PO, SKU..." type="text" />
</div>
<button class="p-2 text-slate-400 hover:text-slate-600 dark:hover:text-white">
<span class="material-symbols-outlined">notifications</span>
</button>
<button class="p-2 text-slate-400 hover:text-slate-600 dark:hover:text-white">
<span class="material-symbols-outlined">settings</span>
</button>
</div>
</header>
<div class="flex-1 overflow-y-auto p-8">
<div class="max-w-7xl mx-auto space-y-6">
<div class="flex flex-col md:flex-row md:items-end justify-between gap-4">
<div>
<h2 class="text-3xl font-black text-slate-900 dark:text-white tracking-tight">Registrar
Entrada de Mercancía</h2>
<p class="text-slate-500 dark:text-slate-400 mt-1 flex items-center gap-2">
<span class="material-symbols-outlined text-base">receipt_long</span>
Purchase Order #ORD-2023-001 | <span class="text-primary font-semibold">Distribución
Multi-Almacén</span>
</p>
</div>
<div class="flex items-center gap-3">
<div
class="flex gap-4 bg-white dark:bg-slate-900 px-4 py-2 rounded-lg border border-slate-200 dark:border-slate-800 shadow-sm">
<div class="text-center">
<p class="text-[10px] text-slate-500 uppercase tracking-wider font-bold">Total Items
PO</p>
<p class="text-lg font-black text-slate-900 dark:text-white leading-tight">12</p>
</div>
<div class="w-px bg-slate-200 dark:bg-slate-800 h-8 self-center"></div>
<div class="text-center">
<p class="text-[10px] text-slate-500 uppercase tracking-wider font-bold">Recibidos
</p>
<p class="text-lg font-black text-primary leading-tight">0</p>
</div>
</div>
<button
class="inline-flex items-center gap-2 px-4 py-2 bg-slate-200 dark:bg-slate-800 text-slate-700 dark:text-slate-200 font-bold text-sm rounded-lg hover:bg-slate-300 transition-colors">
<span class="material-symbols-outlined text-lg">visibility</span>
Ver Detalles
</button>
</div>
</div>
<div
class="bg-white dark:bg-slate-900 rounded-xl border border-slate-200 dark:border-slate-800 shadow-sm overflow-hidden">
<div
class="px-6 py-4 border-b border-slate-200 dark:border-slate-800 bg-slate-50/50 dark:bg-slate-800/50 flex justify-between items-center">
<h3 class="font-bold text-slate-900 dark:text-white">Productos de la Orden</h3>
<span class="text-xs text-slate-500 flex items-center gap-1">
<span class="material-symbols-outlined text-sm">info</span>
Seleccione el almacén de destino por cada producto
</span>
</div>
<div class="overflow-x-auto">
<table class="w-full border-collapse">
<thead>
<tr
class="text-left text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-wider bg-slate-50 dark:bg-slate-800/30">
<th class="px-6 py-4">Producto</th>
<th class="px-6 py-4">SKU / Ref</th>
<th class="px-6 py-4 text-center">Cant. PO</th>
<th class="px-6 py-4 text-center">Recibida</th>
<th class="px-6 py-4">Almacén de Destino</th>
<th class="px-6 py-4">Acciones</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-200 dark:divide-slate-800">
<tr class="group hover:bg-slate-50 dark:hover:bg-slate-800/50 transition-colors">
<td class="px-6 py-4">
<div class="flex items-center gap-3">
<div
class="size-10 bg-slate-100 dark:bg-slate-800 rounded-lg flex items-center justify-center text-slate-400">
<span class="material-symbols-outlined">inventory</span>
</div>
<div class="flex flex-col">
<span
class="text-sm font-semibold text-slate-900 dark:text-white">Cables
de Red Cat6 2m</span>
<span class="text-xs text-slate-500">Accesorios Networking</span>
</div>
</div>
</td>
<td class="px-6 py-4 text-sm text-slate-600 dark:text-slate-400">NET-C62M-WH
</td>
<td
class="px-6 py-4 text-center text-sm font-bold text-slate-900 dark:text-white">
50</td>
<td class="px-6 py-4">
<div class="flex justify-center">
<input
class="w-20 text-center px-2 py-1 bg-slate-100 dark:bg-slate-800 border-none rounded-lg text-sm font-bold focus:ring-2 focus:ring-primary"
max="50" min="0" type="number" value="0" />
</div>
</td>
<td class="px-6 py-4">
<select
class="w-full px-3 py-1.5 bg-slate-50 dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-lg text-xs focus:ring-primary focus:border-primary">
<option value="">Seleccione Almacén...</option>
<option selected="" value="1">Almacén Principal (CDMX)</option>
<option value="2">Bodega Regional (MTY)</option>
<option value="3">CEDIS (GDL)</option>
</select>
</td>
<td class="px-6 py-4">
<span
class="inline-flex items-center px-2.5 py-0.5 rounded-full text-[10px] font-bold bg-slate-100 text-slate-600 dark:bg-slate-800 dark:text-slate-400 uppercase">Estándar</span>
</td>
</tr>
<tr class="bg-primary/[0.02] dark:bg-primary/[0.05]">
<td class="px-6 py-4 border-l-4 border-primary">
<div class="flex items-center gap-3">
<div
class="size-10 bg-primary/10 rounded-lg flex items-center justify-center text-primary">
<span class="material-symbols-outlined">laptop_mac</span>
</div>
<div class="flex flex-col">
<span
class="text-sm font-semibold text-slate-900 dark:text-white">MacBook
Pro M2 14"</span>
<span class="text-xs text-slate-500">Equipos de Cómputo</span>
</div>
</div>
</td>
<td class="px-6 py-4 text-sm text-slate-600 dark:text-slate-400">LAP-MBP14-M2
</td>
<td
class="px-6 py-4 text-center text-sm font-bold text-slate-900 dark:text-white">
5</td>
<td class="px-6 py-4 text-center">
<span class="text-sm font-black text-primary">1 / 5</span>
</td>
<td class="px-6 py-4">
<div class="text-xs text-slate-400 italic">Asignación por número de serie
</div>
</td>
<td class="px-6 py-4">
<button
class="inline-flex items-center gap-2 px-3 py-1.5 bg-primary text-white text-xs font-bold rounded-lg hover:bg-primary/90 transition-all shadow-sm">
<span class="material-symbols-outlined text-sm">barcode_scanner</span>
GESTIONAR SERIES
</button>
</td>
</tr>
<tr class="bg-primary/[0.02] dark:bg-primary/[0.05]">
<td class="px-10 pb-6 pt-2" colspan="6">
<div
class="bg-white dark:bg-slate-800 rounded-xl border border-primary/20 p-5 shadow-sm">
<div
class="flex items-center justify-between mb-4 pb-3 border-b border-slate-100 dark:border-slate-700">
<div>
<h4
class="text-sm font-bold text-slate-700 dark:text-slate-200">
Entrada de Números de Serie (4 Pendientes)</h4>
<p class="text-[11px] text-slate-500">Defina el almacén para
cada equipo escaneado</p>
</div>
<div
class="flex items-center gap-2 text-xs text-primary font-bold bg-primary/10 px-3 py-1.5 rounded-full">
<span class="material-symbols-outlined text-sm">barcode</span>
Listo para escanear
</div>
</div>
<div class="grid grid-cols-1 lg:grid-cols-12 gap-6">
<div class="lg:col-span-5 space-y-4">
<div>
<label
class="block text-[10px] font-bold text-slate-500 uppercase mb-1">Número
de Serie</label>
<div class="relative">
<span
class="material-symbols-outlined absolute left-3 top-1/2 -translate-y-1/2 text-slate-400">qr_code_scanner</span>
<input
class="w-full pl-10 pr-4 py-2 bg-slate-50 dark:bg-slate-700 border border-slate-200 dark:border-slate-600 rounded-lg text-sm focus:ring-primary focus:border-primary"
placeholder="Escanear o escribir serie..."
type="text" />
</div>
</div>
<div>
<label
class="block text-[10px] font-bold text-slate-500 uppercase mb-1">Almacén
Destino</label>
<select
class="w-full px-3 py-2 bg-slate-50 dark:bg-slate-700 border border-slate-200 dark:border-slate-600 rounded-lg text-sm focus:ring-primary focus:border-primary">
<option value="1">Almacén Principal (CDMX)</option>
<option value="2">Bodega Regional (MTY)</option>
<option value="3">CEDIS (GDL)</option>
</select>
</div>
<button
class="w-full py-2 bg-slate-900 dark:bg-slate-700 text-white text-xs font-bold rounded-lg hover:bg-black transition-colors">
Registrar Serie
</button>
</div>
<div class="lg:col-span-7">
<div
class="bg-slate-50 dark:bg-slate-900/50 rounded-lg border border-slate-200 dark:border-slate-700 overflow-hidden">
<table class="w-full text-xs">
<thead class="bg-slate-100 dark:bg-slate-800">
<tr>
<th
class="px-3 py-2 text-left font-bold text-slate-500 uppercase">
Nº Serie</th>
<th
class="px-3 py-2 text-left font-bold text-slate-500 uppercase">
Almacén Destino</th>
<th class="px-3 py-2 text-right"></th>
</tr>
</thead>
<tbody
class="divide-y divide-slate-200 dark:divide-slate-700">
<tr>
<td
class="px-3 py-2 font-mono text-slate-900 dark:text-white">
SN-LAP-M2-00192</td>
<td class="px-3 py-2">
<span
class="inline-flex items-center gap-1.5">
<span
class="w-2 h-2 rounded-full bg-emerald-500"></span>
<span
class="text-slate-600 dark:text-slate-300">Almacén
Principal (CDMX)</span>
</span>
</td>
<td class="px-3 py-2 text-right">
<button
class="text-slate-400 hover:text-red-500 transition-colors">
<span
class="material-symbols-outlined text-lg">delete</span>
</button>
</td>
</tr>
<tr>
<td class="px-3 py-4 text-center text-slate-400 italic bg-slate-50/50 dark:bg-slate-800/30"
colspan="3">
Escanee el siguiente equipo para asignar
almacén...
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</td>
</tr>
<tr class="group hover:bg-slate-50 dark:hover:bg-slate-800/50 transition-colors">
<td class="px-6 py-4">
<div class="flex items-center gap-3">
<div
class="size-10 bg-slate-100 dark:bg-slate-800 rounded-lg flex items-center justify-center text-slate-400">
<span class="material-symbols-outlined">print</span>
</div>
<div class="flex flex-col">
<span
class="text-sm font-semibold text-slate-900 dark:text-white">Impresora
Zebra ZT411</span>
<span class="text-xs text-slate-500">Hardware Almacén</span>
</div>
</div>
</td>
<td class="px-6 py-4 text-sm text-slate-600 dark:text-slate-400">PRN-ZB411-IND
</td>
<td
class="px-6 py-4 text-center text-sm font-bold text-slate-900 dark:text-white">
2</td>
<td class="px-6 py-4 text-center text-sm font-bold text-slate-400">0 / 2</td>
<td class="px-6 py-4">
<div class="text-xs text-slate-400 italic">Asignación por serie requerida
</div>
</td>
<td class="px-6 py-4">
<button
class="inline-flex items-center gap-2 px-3 py-1.5 border border-slate-200 dark:border-slate-700 text-slate-600 dark:text-slate-300 text-xs font-bold rounded-lg hover:bg-slate-100 dark:hover:bg-slate-800 transition-colors">
<span class="material-symbols-outlined text-sm">barcode_scanner</span>
GESTIONAR SERIES
</button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
<div
class="md:col-span-2 bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-900/50 p-4 rounded-xl flex items-start gap-4">
<span class="material-symbols-outlined text-amber-600 dark:text-amber-500">warning</span>
<div>
<p class="text-sm font-bold text-amber-800 dark:text-amber-400">Pendiente de Validación
y Distribución</p>
<p class="text-sm text-amber-700 dark:text-amber-500/80">Faltan registrar 6 números de
serie y asignar almacén de destino a 1 item de tipo estándar antes de confirmar.</p>
</div>
</div>
<div
class="bg-white dark:bg-slate-900 p-4 rounded-xl border border-slate-200 dark:border-slate-800 shadow-sm">
<h4 class="text-xs font-bold text-slate-500 uppercase tracking-wider mb-3">Resumen de
Destinos</h4>
<div class="space-y-2">
<div class="flex justify-between text-xs">
<span class="text-slate-600 dark:text-slate-400">Almacén Principal (CDMX)</span>
<span class="font-bold text-slate-900 dark:text-white">1 Unidad</span>
</div>
<div class="flex justify-between text-xs">
<span class="text-slate-600 dark:text-slate-400">Bodega Regional (MTY)</span>
<span class="font-bold text-slate-900 dark:text-white">0 Unidades</span>
</div>
<div class="flex justify-between text-xs">
<span class="text-slate-600 dark:text-slate-400">CEDIS (GDL)</span>
<span class="font-bold text-slate-900 dark:text-white">0 Unidades</span>
</div>
</div>
</div>
</div>
</div>
</div>
<footer
class="h-20 bg-white dark:bg-slate-900 border-t border-slate-200 dark:border-slate-800 flex items-center justify-end px-8 gap-4 shrink-0">
<button
class="px-6 py-2.5 text-sm font-bold text-slate-600 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white transition-colors">
Cancelar
</button>
<button
class="px-8 py-2.5 bg-primary text-white text-sm font-bold rounded-lg shadow-lg shadow-primary/20 hover:bg-primary/90 transition-all opacity-50 cursor-not-allowed">
Confirmar Recepción Multi-Almacén
</button>
</footer>
</main>
</div>
</body>
</html>

View File

@ -1,364 +0,0 @@
<!DOCTYPE html>
<html class="light" lang="en"><head>
<meta charset="utf-8"/>
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
<title>WMS - Batch Add Inventory Items</title>
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<script>
tailwind.config = {
darkMode: "class",
theme: {
extend: {
colors: {
"primary": "#137fec",
"background-light": "#f6f7f8",
"background-dark": "#101922",
},
fontFamily: {
"display": ["Inter", "sans-serif"]
},
borderRadius: {
"DEFAULT": "0.25rem",
"lg": "0.5rem",
"xl": "0.75rem",
"full": "9999px"
},
},
},
}
</script>
<style type="text/tailwindcss">
@layer utilities {
.scrollbar-hide::-webkit-scrollbar {
display: none;
}
}
.material-symbols-outlined {
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
}
</style>
</head>
<body class="bg-background-light dark:bg-background-dark font-display text-gray-800 dark:text-gray-200 overflow-hidden">
<div class="flex h-screen w-full relative">
<aside class="flex w-64 flex-col border-r border-gray-200 dark:border-gray-800 bg-white dark:bg-background-dark">
<div class="flex h-full flex-col justify-between p-4">
<div class="flex flex-col gap-4">
<div class="flex items-center gap-3 p-2">
<div class="bg-center bg-no-repeat aspect-square bg-cover rounded-full size-10" style='background-image: url("https://lh3.googleusercontent.com/aida-public/AB6AXuCztlHhjKvu2qkn2Xi2zFHagNsNToKwcTg3vQr0KtTqBCo13dK1yyz9HzB2uLCiciLyDfnrf7pREvdblPqCcUiN0HqlSbkFwY1dpQLMbJ4hmpVgHVWaLaUCMXju06qyGQSdg2ChGVcbTQIrk-RNI2-hDOFnfrI1PD89RNSsByXGRsdkYWSyEYFOFk7bT4l7aIaasB6cdVxDfNwJdvVx15wb7-qOHZHFTPMbrkkzmjGec-f7iVqTi5U1ykNDclBSezBM97TfXajTwRJE");'></div>
<div class="flex flex-col">
<h1 class="text-gray-900 dark:text-white text-base font-medium leading-normal">Admin User</h1>
<p class="text-gray-500 dark:text-gray-400 text-sm font-normal leading-normal">Warehouse Manager</p>
</div>
</div>
<nav class="flex flex-col gap-2">
<a class="flex items-center gap-3 px-3 py-2 text-gray-700 dark:text-gray-300 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800" href="#">
<span class="material-symbols-outlined">dashboard</span>
<p class="text-sm font-medium leading-normal">Dashboard</p>
</a>
<a class="flex items-center gap-3 px-3 py-2 rounded-lg bg-primary/10 text-primary dark:bg-primary/20" href="#">
<span class="material-symbols-outlined">warehouse</span>
<p class="text-sm font-medium leading-normal">Inventory</p>
</a>
<a class="flex items-center gap-3 px-3 py-2 text-gray-700 dark:text-gray-300 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800" href="#">
<span class="material-symbols-outlined">receipt_long</span>
<p class="text-sm font-medium leading-normal">Orders</p>
</a>
<a class="flex items-center gap-3 px-3 py-2 text-gray-700 dark:text-gray-300 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800" href="#">
<span class="material-symbols-outlined">local_shipping</span>
<p class="text-sm font-medium leading-normal">Suppliers</p>
</a>
<a class="flex items-center gap-3 px-3 py-2 text-gray-700 dark:text-gray-300 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800" href="#">
<span class="material-symbols-outlined">pie_chart</span>
<p class="text-sm font-medium leading-normal">Reports</p>
</a>
</nav>
</div>
<div class="flex flex-col gap-2">
<nav class="flex flex-col gap-1">
<a class="flex items-center gap-3 px-3 py-2 text-gray-700 dark:text-gray-300 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800" href="#">
<span class="material-symbols-outlined">settings</span>
<p class="text-sm font-medium leading-normal">Settings</p>
</a>
<a class="flex items-center gap-3 px-3 py-2 text-gray-700 dark:text-gray-300 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800" href="#">
<span class="material-symbols-outlined">help</span>
<p class="text-sm font-medium leading-normal">Help</p>
</a>
</nav>
<button class="flex min-w-[84px] cursor-pointer items-center justify-center overflow-hidden rounded-lg h-10 px-4 bg-gray-200 dark:bg-gray-800 text-gray-800 dark:text-gray-200 text-sm font-bold leading-normal hover:bg-gray-300 dark:hover:bg-gray-700">
<span class="truncate">Logout</span>
</button>
</div>
</div>
</aside>
<main class="flex-1 flex flex-col h-screen overflow-hidden">
<header class="flex flex-none items-center justify-between whitespace-nowrap border-b border-solid border-gray-200 dark:border-gray-800 px-10 py-3 bg-white dark:bg-background-dark z-10">
<div class="flex items-center gap-4 text-gray-900 dark:text-white">
<div class="size-6 text-primary">
<svg fill="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M21.435 7.182a.75.75 0 0 0-.87-.11L12 10.435 3.435 7.072a.75.75 0 0 0-.87.11.75.75 0 0 0-.11.87l2.122 7.878a.75.75 0 0 0 .869.59l6-1.635a.75.75 0 0 0 .108 0l6 1.635a.75.75 0 0 0 .87-.59l2.12-7.878a.75.75 0 0 0-.11-.87zM12 12.18l-5.693-1.55L12 4.288l5.693 6.342L12 12.18z"></path>
</svg>
</div>
<h2 class="text-lg font-bold leading-tight tracking-[-0.015em]">WMS Dashboard</h2>
</div>
<div class="flex flex-1 justify-end items-center gap-4">
<label class="relative grow max-w-sm">
<span class="material-symbols-outlined absolute left-3 top-1/2 -translate-y-1/2 text-gray-500">search</span>
<input class="form-input w-full rounded-lg border-none bg-gray-100 dark:bg-gray-800 h-10 pl-10 pr-4 text-sm text-gray-900 dark:text-white placeholder:text-gray-500 focus:ring-primary" placeholder="Search items, orders..." value=""/>
</label>
<div class="flex gap-2">
<button class="flex items-center justify-center rounded-lg h-10 w-10 bg-gray-100 dark:bg-gray-800 text-gray-800 dark:text-gray-200 hover:bg-gray-200 dark:hover:bg-gray-700">
<span class="material-symbols-outlined text-xl">notifications</span>
</button>
<button class="flex items-center justify-center rounded-lg h-10 w-10 bg-gray-100 dark:bg-gray-800 text-gray-800 dark:text-gray-200 hover:bg-gray-200 dark:hover:bg-gray-700">
<span class="material-symbols-outlined text-xl">help_outline</span>
</button>
</div>
<div class="bg-center bg-no-repeat aspect-square bg-cover rounded-full size-10" style='background-image: url("https://lh3.googleusercontent.com/aida-public/AB6AXuBNpxncwdjcD3fLZbRbit_NZyuFBZhcpV-Bvdlu6NiZL3kb65hsFRsDkTNHtC0zJxG3HGV1TInl_DCfafj3axNGSwW4-UNj1sZWhiHCYE2aK9hm-FYjrNGiEh0UqKya1EAYMTM5Z4k8qKOWPEPdIaZz9X98tPC5FIn5lbRRusCTuQmgRL-QxK9SdIMA3TflImwA1vyh3zq44j8EkkNTQWf94-e82GDHs5MwHIkK0S-Gg4d950IyRTpoABSa9qXA5yPzoaT9jCGjCodL");'></div>
</div>
</header>
<div class="flex-1 overflow-y-auto p-6 lg:p-10 pb-24">
<div class="mx-auto max-w-7xl">
<div class="flex flex-wrap gap-2 mb-6">
<a class="text-gray-500 dark:text-gray-400 text-sm font-medium leading-normal" href="#">Dashboard</a>
<span class="text-gray-500 dark:text-gray-400 text-sm font-medium leading-normal">/</span>
<a class="text-gray-500 dark:text-gray-400 text-sm font-medium leading-normal" href="#">Inventory</a>
<span class="text-gray-500 dark:text-gray-400 text-sm font-medium leading-normal">/</span>
<span class="text-gray-800 dark:text-gray-200 text-sm font-medium leading-normal">Batch Add Items</span>
</div>
<div class="flex flex-wrap justify-between items-center gap-4 mb-8">
<div class="flex flex-col gap-2">
<h1 class="text-gray-900 dark:text-white text-3xl font-bold tracking-tight">Add Items to Inventory</h1>
<p class="text-gray-500 dark:text-gray-400 text-base font-normal leading-normal">Manage your batch inventory addition list here.</p>
</div>
<button class="flex items-center justify-center gap-2 bg-primary text-white px-6 py-3 rounded-xl font-bold hover:bg-primary/90 transition-all shadow-lg shadow-primary/20">
<span class="material-symbols-outlined">add_circle</span>
Select Product to Add
</button>
</div>
<div class="bg-white dark:bg-gray-900/50 rounded-xl border border-gray-200 dark:border-gray-800 overflow-hidden shadow-sm">
<div class="px-6 py-4 border-b border-gray-200 dark:border-gray-800 flex justify-between items-center">
<h2 class="text-lg font-bold text-gray-900 dark:text-white">Queued Items (4)</h2>
<button class="text-sm font-medium text-red-500 hover:text-red-600">Clear All</button>
</div>
<div class="overflow-x-auto">
<table class="w-full text-left border-collapse">
<thead>
<tr class="bg-gray-50 dark:bg-gray-800/50">
<th class="px-6 py-4 text-xs font-bold text-gray-500 dark:text-gray-400 uppercase tracking-wider">Product Name</th>
<th class="px-6 py-4 text-xs font-bold text-gray-500 dark:text-gray-400 uppercase tracking-wider">SKU</th>
<th class="px-6 py-4 text-xs font-bold text-gray-500 dark:text-gray-400 uppercase tracking-wider">Variant</th>
<th class="px-6 py-4 text-xs font-bold text-gray-500 dark:text-gray-400 uppercase tracking-wider">Quantity</th>
<th class="px-6 py-4 text-xs font-bold text-gray-500 dark:text-gray-400 uppercase tracking-wider">Unit</th>
<th class="px-6 py-4 text-xs font-bold text-gray-500 dark:text-gray-400 uppercase tracking-wider text-right">Action</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-100 dark:divide-gray-800">
<tr class="hover:bg-gray-50/50 dark:hover:bg-gray-800/30 transition-colors">
<td class="px-6 py-4">
<div class="flex items-center gap-3">
<div class="w-8 h-8 rounded bg-gray-100 dark:bg-gray-800 overflow-hidden">
<img alt="product" class="w-full h-full object-cover" src="https://lh3.googleusercontent.com/aida-public/AB6AXuBMbZhSSPYoHEi3akXSqZXm-T6EghACuHJJ5NYeJ8g4XJIKtqXRhCSQ1kFcvo7Txk01ryR-r6RJ61NzPLENWMywvUVnCkQglsizZG0NKPBwRFQjXvtDULkGbGFm6EPocyHGfuzKJ0EoDr601zMBI34mfPCgn9AAaRkTMj2Ize2nCUcanlGn7QJEp6BdNcmf3JFlk-51jPTXj1-mM4Pw6AGIvarhxnZqtEAji6qRF7evVTR_56c48h5if21S3jD-wVhNVp8AltVn8KEH"/>
</div>
<span class="font-medium">Smart Display Panel</span>
</div>
</td>
<td class="px-6 py-4 text-sm text-gray-500">DISP-PNL-SD-001</td>
<td class="px-6 py-4">
<div class="flex flex-wrap gap-1">
<span class="px-2.5 py-1 text-[10px] font-semibold bg-blue-50 dark:bg-blue-900/30 text-blue-600 dark:text-blue-400 rounded-full border border-blue-100 dark:border-blue-800/50 uppercase tracking-tight">Medida: 85</span>
<span class="px-2.5 py-1 text-[10px] font-semibold bg-purple-50 dark:bg-purple-900/30 text-purple-600 dark:text-purple-400 rounded-full border border-purple-100 dark:border-purple-800/50 uppercase tracking-tight">Resolución: 4K</span>
</div>
</td>
<td class="px-6 py-4">
<input class="w-20 form-input h-8 rounded border-gray-300 dark:border-gray-700 bg-transparent text-sm" type="number" value="25"/>
</td>
<td class="px-6 py-4 text-sm">pcs</td>
<td class="px-6 py-4 text-right">
<button class="text-gray-400 hover:text-red-500 transition-colors">
<span class="material-symbols-outlined text-xl">delete</span>
</button>
</td>
</tr>
<tr class="hover:bg-gray-50/50 dark:hover:bg-gray-800/30 transition-colors">
<td class="px-6 py-4">
<div class="flex items-center gap-3">
<div class="w-8 h-8 rounded bg-gray-100 dark:bg-gray-800 overflow-hidden">
<img alt="product" class="w-full h-full object-cover" src="https://lh3.googleusercontent.com/aida-public/AB6AXuBBMBDEGh1exdBX1lU--TOCsUwfsFkYhrl8DxxvxgH-hKLkcuVtuGP_ZhRS5l_YXFWls08Ecr-Ic748cVHqHexFMMzYTPH8YL2s9OISDgsMIwcDOPxitrPUn3RBCD_krFC8SrBXBeNC8ilcFdg_JCwD5BW5gaYjAAEUMFXyM1vd-YT27KyubBk1IKATsMdDkH-NtwAHcrPgNoXsRaAcubBQiebCG1hmxKVW3fAXoDoSqywrxhwZ7cmIuo7BTThJJ1KcczxQh1jYh_w4"/>
</div>
<span class="font-medium">Mechanical Keyboard RGB</span>
</div>
</td>
<td class="px-6 py-4 text-sm text-gray-500">KYBD-RGB-US-02</td>
<td class="px-6 py-4">
<span class="px-2.5 py-1 text-xs font-medium bg-gray-100 dark:bg-gray-800 rounded-full border border-gray-200 dark:border-gray-700">Red Switch</span>
</td>
<td class="px-6 py-4">
<input class="w-20 form-input h-8 rounded border-gray-300 dark:border-gray-700 bg-transparent text-sm" type="number" value="12"/>
</td>
<td class="px-6 py-4 text-sm">pcs</td>
<td class="px-6 py-4 text-right">
<button class="text-gray-400 hover:text-red-500 transition-colors">
<span class="material-symbols-outlined text-xl">delete</span>
</button>
</td>
</tr>
<tr class="hover:bg-gray-50/50 dark:hover:bg-gray-800/30 transition-colors">
<td class="px-6 py-4">
<div class="flex items-center gap-3">
<div class="w-8 h-8 rounded bg-gray-100 dark:bg-gray-800 overflow-hidden">
<img alt="product" class="w-full h-full object-cover" src="https://lh3.googleusercontent.com/aida-public/AB6AXuAsb9nqDON7PeKGYYAU8hmHn7B2l3waP_ewMQTpsgnpJe2vFiY-k-zncE5VEf186tDzlTwVPA6-1SgAFagugq9Anw2WXloUFTNwOrOHgAi-MNhgs0FuxRKF8XMLk6W_u6YLF5xWh3K2q317JXFSUWtURtxCi8pYM4YtHYEyuT2aIrhbAnI2LQ7ja8DvSdvpO6t1IJ6HE9RqlnAFXuLG22igxiekKT2NjRpuK5Zzu_90rBPEjb0KitG9DwiBjzKa702pccHPRqupgXke"/>
</div>
<span class="font-medium">USB-C Charging Cable 2m</span>
</div>
</td>
<td class="px-6 py-4 text-sm text-gray-500">CBL-USBC-2M</td>
<td class="px-6 py-4">
<span class="px-2.5 py-1 text-xs font-medium bg-gray-100 dark:bg-gray-800 rounded-full border border-gray-200 dark:border-gray-700">Braided</span>
</td>
<td class="px-6 py-4">
<input class="w-20 form-input h-8 rounded border-gray-300 dark:border-gray-700 bg-transparent text-sm" type="number" value="100"/>
</td>
<td class="px-6 py-4 text-sm">pcs</td>
<td class="px-6 py-4 text-right">
<button class="text-gray-400 hover:text-red-500 transition-colors">
<span class="material-symbols-outlined text-xl">delete</span>
</button>
</td>
</tr>
<tr class="hover:bg-gray-50/50 dark:hover:bg-gray-800/30 transition-colors">
<td class="px-6 py-4">
<div class="flex items-center gap-3">
<div class="w-8 h-8 rounded bg-gray-100 dark:bg-gray-800 overflow-hidden">
<img alt="product" class="w-full h-full object-cover" src="https://lh3.googleusercontent.com/aida-public/AB6AXuD1PtrsyrvB9KtL5DHjKotvi92lM1NsfdzyEkoW5DAEfSYoPI8MNsCJZDBMtrz6rXnIWApiymAIlp7Yq5P6JTQqpwaNJVw5I4G3KiefVME-D_jR_0iTDtPBxljmlcohkhIrSfK77wV9Wq1KL0yvjCB2UuDewIxBOs0RUl7_RbISQxYaSzVe9GbqnwI5cpj_N_1faxUA89ATStV6GovMDXFA881MUGKd4_ox_CzEG5zzPZYRe9JF7M33YH-rRHdxlpQWtRH9yw4X8sKp"/>
</div>
<span class="font-medium">Smart Display Panel</span>
</div>
</td>
<td class="px-6 py-4 text-sm text-gray-500">DISP-PNL-SD-001</td>
<td class="px-6 py-4">
<div class="flex flex-wrap gap-1">
<span class="px-2.5 py-1 text-[10px] font-semibold bg-blue-50 dark:bg-blue-900/30 text-blue-600 dark:text-blue-400 rounded-full border border-blue-100 dark:border-blue-800/50 uppercase tracking-tight">Medida: 36</span>
<span class="px-2.5 py-1 text-[10px] font-semibold bg-purple-50 dark:bg-purple-900/30 text-purple-600 dark:text-purple-400 rounded-full border border-purple-100 dark:border-purple-800/50 uppercase tracking-tight">Resolución: 1080</span>
</div>
</td>
<td class="px-6 py-4">
<input class="w-20 form-input h-8 rounded border-gray-300 dark:border-gray-700 bg-transparent text-sm" type="number" value="5"/>
</td>
<td class="px-6 py-4 text-sm">pcs</td>
<td class="px-6 py-4 text-right">
<button class="text-gray-400 hover:text-red-500 transition-colors">
<span class="material-symbols-outlined text-xl">delete</span>
</button>
</td>
</tr>
</tbody>
</table>
</div>
<div class="p-6 bg-gray-50/50 dark:bg-gray-800/50 border-t border-gray-200 dark:border-gray-800 text-sm text-gray-500 dark:text-gray-400">
Total items to be added: <span class="font-bold text-gray-900 dark:text-white">142 units</span> across 4 unique products.
</div>
</div>
</div>
</div>
<footer class="fixed bottom-0 right-0 left-64 bg-white dark:bg-background-dark border-t border-gray-200 dark:border-gray-800 p-4 px-10 flex justify-between items-center z-20 shadow-[0_-4px_6px_-1px_rgba(0,0,0,0.1)]">
<div class="flex items-center gap-6">
<div class="flex items-center gap-2">
<span class="text-xs font-medium text-gray-500 uppercase tracking-widest">Storage Location</span>
<select class="form-select text-sm rounded-lg border-gray-300 dark:border-gray-700 bg-gray-50 dark:bg-gray-800 focus:ring-primary h-9">
<option>Main Warehouse (A1)</option>
<option>South Storage (B4)</option>
<option>Cold Storage (C2)</option>
</select>
</div>
</div>
<div class="flex items-center gap-4">
<button class="flex min-w-[100px] cursor-pointer items-center justify-center rounded-lg h-11 px-6 bg-gray-200 dark:bg-gray-800 text-gray-800 dark:text-gray-200 text-sm font-bold leading-normal hover:bg-gray-300 dark:hover:bg-gray-700 transition-all">
Cancel
</button>
<button class="flex min-w-[200px] cursor-pointer items-center justify-center rounded-lg h-11 px-8 bg-primary text-white text-sm font-bold leading-normal tracking-[0.015em] hover:bg-primary/90 shadow-lg shadow-primary/20 transition-all">
Confirm and Save Inventory
</button>
</div>
</footer>
</main>
<div class="fixed inset-0 z-100 flex items-center justify-center">
<div class="absolute inset-0 bg-gray-900/60 backdrop-blur-sm"></div>
<div class="relative bg-white dark:bg-background-dark w-full max-w-2xl mx-4 rounded-2xl shadow-2xl border border-gray-200 dark:border-gray-800 overflow-hidden flex flex-col max-h-[90vh]">
<div class="p-6 border-b border-gray-100 dark:border-gray-800 flex justify-between items-center">
<div>
<h3 class="text-xl font-bold text-gray-900 dark:text-white">Add Product to Batch</h3>
<p class="text-sm text-gray-500 dark:text-gray-400">Search and configure the product variant</p>
</div>
<button class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-200 transition-colors">
<span class="material-symbols-outlined text-2xl">close</span>
</button>
</div>
<div class="p-6 overflow-y-auto space-y-8">
<div class="space-y-3">
<label class="block text-sm font-semibold text-gray-700 dark:text-gray-300">Search Catalog</label>
<div class="relative">
<span class="material-symbols-outlined absolute left-3 top-1/2 -translate-y-1/2 text-gray-500">search</span>
<input class="form-input w-full rounded-xl border-gray-300 dark:border-gray-700 bg-gray-50 dark:bg-gray-800/50 text-gray-900 dark:text-white pl-11 h-12 focus:ring-primary focus:border-primary" placeholder="Type Product Name or SKU..." type="text" value="Smart Display Panel"/>
</div>
</div>
<div class="bg-gray-50 dark:bg-gray-800/30 rounded-xl p-5 border border-gray-100 dark:border-gray-700 space-y-6">
<div class="flex items-center gap-4">
<div class="w-16 h-16 rounded-lg bg-white dark:bg-gray-700 border border-gray-200 dark:border-gray-600 flex items-center justify-center shadow-sm">
<span class="material-symbols-outlined text-3xl text-gray-400">image</span>
</div>
<div>
<p class="text-lg font-bold text-gray-900 dark:text-white">Smart Display Panel</p>
<p class="text-sm text-gray-500 font-medium tracking-tight">SKU: DISP-PNL-SD-001</p>
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div class="space-y-2">
<label class="block text-xs font-bold uppercase tracking-wider text-gray-500 dark:text-gray-400">Medida</label>
<select class="form-select w-full rounded-lg border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 text-sm focus:ring-primary focus:border-primary h-11">
<option value="24">24</option>
<option value="36">36</option>
<option value="64">64</option>
<option selected="" value="85">85</option>
</select>
</div>
<div class="space-y-2">
<label class="block text-xs font-bold uppercase tracking-wider text-gray-500 dark:text-gray-400">Resolución</label>
<select class="form-select w-full rounded-lg border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 text-sm focus:ring-primary focus:border-primary h-11">
<option value="1080">1080</option>
<option value="2K">2K</option>
<option selected="" value="4K">4K</option>
<option value="8K">8K</option>
</select>
</div>
<div class="space-y-2">
<label class="block text-xs font-bold uppercase tracking-wider text-gray-500 dark:text-gray-400">Quantity</label>
<div class="flex items-center gap-2">
<input class="form-input flex-1 rounded-lg border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 text-sm focus:ring-primary focus:border-primary h-11" type="number" value="10"/>
<span class="text-sm font-medium text-gray-500">pcs</span>
</div>
</div>
<div class="flex items-end pb-1">
<div class="bg-blue-50 dark:bg-blue-900/20 text-blue-600 dark:text-blue-400 px-3 py-2 rounded-lg border border-blue-100 dark:border-blue-800/50 w-full flex items-center gap-2">
<span class="material-symbols-outlined text-lg">info</span>
<span class="text-xs font-medium">Current Stock: 420 units</span>
</div>
</div>
</div>
</div>
</div>
<div class="p-6 bg-gray-50 dark:bg-gray-800/50 border-t border-gray-100 dark:border-gray-800 flex justify-end gap-3">
<button class="px-5 py-2.5 rounded-lg text-sm font-bold text-gray-600 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors">
Cancel
</button>
<button class="bg-primary text-white px-8 py-2.5 rounded-lg text-sm font-bold hover:bg-primary/90 transition-all shadow-lg shadow-primary/20 flex items-center gap-2">
<span class="material-symbols-outlined text-lg">add_task</span>
Confirm and Add to List
</button>
</div>
</div>
</div>
</div>
</body></html>

View File

@ -0,0 +1,16 @@
import api from "../../../services/api";
import type { CreateInventoryRequest, CreateInventoryResponse } from "../types/warehouse.inventory";
export const inventoryWarehouseServices = {
async addInventory(data: CreateInventoryRequest): Promise<CreateInventoryResponse> {
try {
const response = await api.post('/api/inventory-warehouses', data);
console.log('📦 Add Inventory response:', response.data);
return response.data;
} catch (error) {
console.error('❌ Error adding inventory:', error);
throw error;
}
}
}

View File

@ -1,5 +1,5 @@
import api from '../../../services/api';
import type { WarehousesResponse, CreateWarehouseData } from '../types/warehouse';
import type { WarehousesResponse, CreateWarehouseData, WarehouseDetailResponse } from '../types/warehouse';
export const warehouseService = {
async getWarehouses() {
@ -22,5 +22,16 @@ export const warehouseService = {
console.error('Error creating warehouse:', error);
throw error;
}
},
async getWarehouseById(warehouseId: number) {
try {
const response = await api.get<WarehouseDetailResponse>(`/api/warehouses/${warehouseId}`);
console.log(`Warehouse with ID ${warehouseId} response:`, response.data);
return response.data;
} catch (error) {
console.error(`Error fetching warehouse with ID ${warehouseId}:`, error);
throw error;
}
}
};

View File

@ -46,3 +46,63 @@ export interface WarehousesResponse {
warehouses: WarehousePagination;
};
}
export interface WarehouseProduct {
id: number;
code: string;
sku: string;
name: string;
barcode: string;
description: string;
unit_of_measure_id: number;
suggested_sale_price: number;
attributes: Record<string, any> | null;
is_active: boolean;
created_at: string;
updated_at: string;
deleted_at: string | null;
}
export interface WarehouseStock {
id: number;
product_id: number;
warehouse_id: number;
stock: number;
stock_min: number | null;
stock_max: number | null;
reorder_point: number | null;
is_active: number;
created_at: string;
updated_at: string;
product: WarehouseProduct;
}
export interface WarehouseInventoryItem {
id: number;
product_id: number;
warehouse_id: number;
quantity: string;
serial_number: string | null;
acquisition_date: string;
warehouse_number: string | null;
attributes: Record<string, any> | null;
purchase_cost: number;
warranty_days: number | null;
warranty_end_date: string | null;
expiration_date: string | null;
created_at: string;
updated_at: string;
deleted_at: string | null;
product: WarehouseProduct;
}
export interface WarehouseDetailData {
warehouse: Warehouse;
stocks: WarehouseStock[];
items: WarehouseInventoryItem[];
}
export interface WarehouseDetailResponse {
status: string;
data: WarehouseDetailData;
}

View File

@ -0,0 +1,74 @@
export interface InventoryProductItem {
product_id: number;
warehouse_id: number;
purchase_cost: number;
quantity?: number;
attributes?: Record<string, any>;
serial_number?: string;
serial_numbers?: string[];
}
export interface CreateInventoryRequest {
products: InventoryProductItem[];
}
export interface InventoryProduct {
id: number;
code: string;
sku: string;
name: string;
barcode: string;
description: string;
unit_of_measure_id: number;
suggested_sale_price: number;
attributes: Record<string, any> | null;
is_active: boolean;
created_at: string;
updated_at: string;
deleted_at: string | null;
}
export interface InventoryWarehouse {
id: number;
code: string;
name: string;
description: string;
address: string | null;
is_active: boolean;
created_at: string;
updated_at: string;
deleted_at: string | null;
}
export interface InventoryItemDetail {
id: number;
product_id: number;
warehouse_id: number;
quantity: string;
serial_number: string | null;
attributes: Record<string, any> | null;
purchase_cost: number;
acquisition_date: string;
created_at: string;
updated_at: string;
product: InventoryProduct;
warehouse: InventoryWarehouse;
}
export interface InventoryGroupResult {
inventory_items: InventoryItemDetail[];
previous_stock: number;
added_quantity: number;
new_stock: number;
type: 'stock' | 'serialized';
}
export interface CreateInventoryResponseMeta {
total_items: number;
}
export interface CreateInventoryResponse {
message: string;
data: InventoryGroupResult[];
meta: CreateInventoryResponseMeta;
}

View File

@ -7,8 +7,6 @@ import MainLayout from '../MainLayout.vue';
import WarehouseIndex from '../modules/warehouse/components/WarehouseIndex.vue';
import WarehouseForm from '../modules/warehouse/components/WarehouseForm.vue';
import WarehouseDetails from '../modules/warehouse/components/WarehouseDetails.vue';
import BatchAddInventory from '../modules/warehouse/components/BatchAddInventory.vue';
import WarehouseClassification from '../modules/warehouse/components/WarehouseClassification.vue';
import UnitOfMeasure from '../modules/catalog/components/UnitOfMeasure.vue';
import ComercialClassification from '../modules/catalog/components/ComercialClassification.vue';
@ -28,6 +26,7 @@ import Suppliers from '../modules/catalog/components/suppliers/Suppliers.vue';
import Purchases from '../modules/purchases/components/Purchases.vue';
import PurchaseDetails from '../modules/purchases/components/PurchaseDetails.vue';
import PurchaseForm from '../modules/purchases/components/PurchaseForm.vue';
import WarehouseAddInventory from '../modules/warehouse/components/WarehouseAddInventory.vue';
const routes: RouteRecordRaw[] = [
{
path: '/login',
@ -100,8 +99,8 @@ const routes: RouteRecordRaw[] = [
},
{
path: 'batch-add',
name: 'BatchAddInventory',
component: BatchAddInventory,
name: 'WarehouseAddInventory',
component: WarehouseAddInventory,
meta: {
title: 'Agregar Items al Inventario',
requiresAuth: true