Update readmes and imporve UI

This commit is contained in:
Urtzi Alfaro
2025-12-19 09:28:36 +01:00
parent a6ae730ef0
commit 71ee2976a2
10 changed files with 1035 additions and 155 deletions

View File

@@ -1,4 +1,4 @@
import React from 'react';
import React, { useState, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import {
Package,
@@ -13,6 +13,8 @@ import {
Sparkles,
FileText,
Factory,
Search,
X,
} from 'lucide-react';
export type ItemType =
@@ -36,6 +38,8 @@ export interface ItemTypeConfig {
badge?: string;
badgeColor?: string;
isHighlighted?: boolean;
category: 'daily' | 'common' | 'setup';
keywords?: string[];
}
export const ITEM_TYPES: ItemTypeConfig[] = [
@@ -47,6 +51,8 @@ export const ITEM_TYPES: ItemTypeConfig[] = [
badge: '⭐ Más Común',
badgeColor: 'bg-gradient-to-r from-amber-100 to-orange-100 text-orange-800 font-semibold',
isHighlighted: true,
category: 'daily',
keywords: ['ventas', 'sales', 'ingresos', 'caja', 'revenue'],
},
{
id: 'inventory',
@@ -55,6 +61,8 @@ export const ITEM_TYPES: ItemTypeConfig[] = [
icon: Package,
badge: 'Configuración',
badgeColor: 'bg-blue-100 text-blue-700',
category: 'setup',
keywords: ['inventario', 'inventory', 'stock', 'ingredientes', 'productos'],
},
{
id: 'supplier',
@@ -63,6 +71,8 @@ export const ITEM_TYPES: ItemTypeConfig[] = [
icon: Building,
badge: 'Configuración',
badgeColor: 'bg-blue-100 text-blue-700',
category: 'setup',
keywords: ['proveedor', 'supplier', 'vendor', 'distribuidor'],
},
{
id: 'recipe',
@@ -71,6 +81,8 @@ export const ITEM_TYPES: ItemTypeConfig[] = [
icon: ChefHat,
badge: 'Común',
badgeColor: 'bg-green-100 text-green-700',
category: 'common',
keywords: ['receta', 'recipe', 'formula', 'producción'],
},
{
id: 'equipment',
@@ -79,6 +91,8 @@ export const ITEM_TYPES: ItemTypeConfig[] = [
icon: Wrench,
badge: 'Configuración',
badgeColor: 'bg-blue-100 text-blue-700',
category: 'setup',
keywords: ['equipo', 'equipment', 'maquinaria', 'horno', 'mixer'],
},
{
id: 'quality-template',
@@ -87,6 +101,8 @@ export const ITEM_TYPES: ItemTypeConfig[] = [
icon: ClipboardCheck,
badge: 'Configuración',
badgeColor: 'bg-blue-100 text-blue-700',
category: 'setup',
keywords: ['calidad', 'quality', 'control', 'estándares', 'inspección'],
},
{
id: 'customer-order',
@@ -95,6 +111,8 @@ export const ITEM_TYPES: ItemTypeConfig[] = [
icon: ShoppingCart,
badge: 'Diario',
badgeColor: 'bg-amber-100 text-amber-700',
category: 'daily',
keywords: ['pedido', 'order', 'cliente', 'customer', 'orden'],
},
{
id: 'customer',
@@ -103,6 +121,8 @@ export const ITEM_TYPES: ItemTypeConfig[] = [
icon: Users,
badge: 'Común',
badgeColor: 'bg-green-100 text-green-700',
category: 'common',
keywords: ['cliente', 'customer', 'comprador', 'contacto'],
},
{
id: 'team-member',
@@ -111,6 +131,8 @@ export const ITEM_TYPES: ItemTypeConfig[] = [
icon: UserPlus,
badge: 'Configuración',
badgeColor: 'bg-blue-100 text-blue-700',
category: 'setup',
keywords: ['empleado', 'employee', 'team', 'staff', 'usuario'],
},
{
id: 'purchase-order',
@@ -119,6 +141,8 @@ export const ITEM_TYPES: ItemTypeConfig[] = [
icon: FileText,
badge: 'Diario',
badgeColor: 'bg-amber-100 text-amber-700',
category: 'daily',
keywords: ['compra', 'purchase', 'orden', 'proveedor', 'abastecimiento'],
},
{
id: 'production-batch',
@@ -127,6 +151,8 @@ export const ITEM_TYPES: ItemTypeConfig[] = [
icon: Factory,
badge: 'Diario',
badgeColor: 'bg-amber-100 text-amber-700',
category: 'daily',
keywords: ['producción', 'production', 'lote', 'batch', 'fabricación'],
},
];
@@ -136,6 +162,36 @@ interface ItemTypeSelectorProps {
export const ItemTypeSelector: React.FC<ItemTypeSelectorProps> = ({ onSelect }) => {
const { t } = useTranslation('wizards');
const [searchQuery, setSearchQuery] = useState('');
const [selectedCategory, setSelectedCategory] = useState<'all' | 'daily' | 'common' | 'setup'>('all');
// Filter items based on search and category
const filteredItems = useMemo(() => {
return ITEM_TYPES.filter(item => {
// Category filter
if (selectedCategory !== 'all' && item.category !== selectedCategory) {
return false;
}
// Search filter
if (searchQuery.trim()) {
const query = searchQuery.toLowerCase();
const matchesTitle = item.title.toLowerCase().includes(query);
const matchesSubtitle = item.subtitle.toLowerCase().includes(query);
const matchesKeywords = item.keywords?.some(keyword => keyword.toLowerCase().includes(query));
return matchesTitle || matchesSubtitle || matchesKeywords;
}
return true;
});
}, [searchQuery, selectedCategory]);
const categoryLabels = {
all: 'Todos',
daily: 'Diario',
common: 'Común',
setup: 'Configuración',
};
return (
<div className="space-y-6">
@@ -154,9 +210,60 @@ export const ItemTypeSelector: React.FC<ItemTypeSelectorProps> = ({ onSelect })
</p>
</div>
{/* Search and Filters */}
<div className="space-y-3">
{/* Search Bar */}
<div className="relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-[var(--text-tertiary)]" />
<input
type="text"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder="Buscar por nombre o categoría..."
className="w-full pl-10 pr-10 py-2.5 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)] placeholder-[var(--text-tertiary)]"
/>
{searchQuery && (
<button
onClick={() => setSearchQuery('')}
className="absolute right-3 top-1/2 -translate-y-1/2 p-1 hover:bg-[var(--bg-secondary)] rounded transition-colors"
>
<X className="w-4 h-4 text-[var(--text-tertiary)]" />
</button>
)}
</div>
{/* Category Filters */}
<div className="flex flex-wrap gap-2">
{(['all', 'daily', 'common', 'setup'] as const).map((category) => (
<button
key={category}
onClick={() => setSelectedCategory(category)}
className={`px-4 py-2 rounded-full text-sm font-medium transition-all ${
selectedCategory === category
? 'bg-[var(--color-primary)] text-white shadow-md'
: 'bg-[var(--bg-secondary)] text-[var(--text-secondary)] hover:bg-[var(--bg-tertiary)]'
}`}
>
{categoryLabels[category]}
</button>
))}
</div>
</div>
{/* Results Count */}
{(searchQuery || selectedCategory !== 'all') && (
<div className="text-sm text-[var(--text-secondary)]">
{filteredItems.length === 0 ? (
<p className="text-center py-4">No se encontraron resultados</p>
) : (
<p>{filteredItems.length} {filteredItems.length === 1 ? 'resultado' : 'resultados'}</p>
)}
</div>
)}
{/* Item Type Grid */}
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3 md:gap-4">
{ITEM_TYPES.map((itemType) => {
{filteredItems.map((itemType) => {
const Icon = itemType.icon;
const isHighlighted = itemType.isHighlighted;

View File

@@ -97,26 +97,40 @@ export const UnifiedAddWizard: React.FC<UnifiedAddWizardProps> = ({
// Handle Purchase Order submission
if (selectedItemType === 'purchase-order') {
// Validate items have positive quantities and prices
if ((finalData.items || []).some((item: any) =>
Number(item.ordered_quantity) < 0.01 || Number(item.unit_price) < 0.01
)) {
throw new Error('Todos los productos deben tener cantidad y precio mayor a 0');
}
const subtotal = (finalData.items || []).reduce(
(sum: number, item: any) => sum + (item.subtotal || 0),
(sum: number, item: any) => sum + (Number(item.ordered_quantity) * Number(item.unit_price)),
0
);
// Convert date string to ISO datetime with timezone (start of day in local timezone)
const deliveryDate = new Date(finalData.required_delivery_date + 'T00:00:00');
if (isNaN(deliveryDate.getTime())) {
throw new Error('Fecha de entrega inválida');
}
const requiredDeliveryDateTime = deliveryDate.toISOString();
await createPurchaseOrderMutation.mutateAsync({
tenantId: currentTenant.id,
data: {
supplier_id: finalData.supplier_id,
required_delivery_date: finalData.required_delivery_date,
required_delivery_date: requiredDeliveryDateTime,
priority: finalData.priority || 'normal',
subtotal: String(subtotal),
tax_amount: String(finalData.tax_amount || 0),
shipping_cost: String(finalData.shipping_cost || 0),
discount_amount: String(finalData.discount_amount || 0),
subtotal: subtotal,
tax_amount: Number(finalData.tax_amount) || 0,
shipping_cost: Number(finalData.shipping_cost) || 0,
discount_amount: Number(finalData.discount_amount) || 0,
notes: finalData.notes || undefined,
items: (finalData.items || []).map((item: any) => ({
inventory_product_id: item.inventory_product_id,
ordered_quantity: item.ordered_quantity,
unit_price: String(item.unit_price),
ordered_quantity: Number(item.ordered_quantity),
unit_price: Number(item.unit_price),
unit_of_measure: item.unit_of_measure,
})),
},
@@ -126,17 +140,37 @@ export const UnifiedAddWizard: React.FC<UnifiedAddWizardProps> = ({
// Handle Production Batch submission
if (selectedItemType === 'production-batch') {
// Validate quantities
if (Number(finalData.planned_quantity) < 0.01) {
throw new Error('La cantidad planificada debe ser mayor a 0');
}
if (Number(finalData.planned_duration_minutes) < 1) {
throw new Error('La duración planificada debe ser mayor a 0');
}
// Convert staff_assigned from string to array
const staffArray = finalData.staff_assigned_string
? finalData.staff_assigned_string.split(',').map((s: string) => s.trim()).filter((s: string) => s.length > 0)
: [];
// Convert datetime-local strings to ISO datetime with timezone
const plannedStartDate = new Date(finalData.planned_start_time);
const plannedEndDate = new Date(finalData.planned_end_time);
if (isNaN(plannedStartDate.getTime()) || isNaN(plannedEndDate.getTime())) {
throw new Error('Fechas de inicio o fin inválidas');
}
if (plannedEndDate <= plannedStartDate) {
throw new Error('La fecha de fin debe ser posterior a la fecha de inicio');
}
const batchData: ProductionBatchCreate = {
product_id: finalData.product_id,
product_name: finalData.product_name,
recipe_id: finalData.recipe_id || undefined,
planned_start_time: finalData.planned_start_time,
planned_end_time: finalData.planned_end_time,
planned_start_time: plannedStartDate.toISOString(),
planned_end_time: plannedEndDate.toISOString(),
planned_quantity: Number(finalData.planned_quantity),
planned_duration_minutes: Number(finalData.planned_duration_minutes),
priority: (finalData.priority || ProductionPriorityEnum.MEDIUM) as ProductionPriorityEnum,

View File

@@ -702,10 +702,10 @@ export const PurchaseOrderWizardSteps = (
return 'Debes agregar al menos un producto';
}
const invalidItems = data.items.some(
(item: any) => !item.inventory_product_id || item.ordered_quantity <= 0 || item.unit_price <= 0
(item: any) => !item.inventory_product_id || item.ordered_quantity < 0.01 || item.unit_price < 0.01
);
if (invalidItems) {
return 'Todos los productos deben tener ingrediente, cantidad y precio válidos';
return 'Todos los productos deben tener ingrediente, cantidad mayor a 0 y precio mayor a 0';
}
return true;
},