Update readmes and imporve UI
This commit is contained in:
@@ -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;
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user