Improve the frontend

This commit is contained in:
Urtzi Alfaro
2025-10-21 19:50:07 +02:00
parent 05da20357d
commit 8d30172483
105 changed files with 14699 additions and 4630 deletions

View File

@@ -63,6 +63,7 @@ const ListFieldRenderer: React.FC<ListFieldRendererProps> = ({ field, value, onC
const renderItemField = (item: any, itemIndex: number, fieldConfig: any) => {
const fieldValue = item[fieldConfig.name] ?? '';
const isFieldDisabled = fieldConfig.disabled ?? false;
switch (fieldConfig.type) {
case 'select':
@@ -70,10 +71,11 @@ const ListFieldRenderer: React.FC<ListFieldRendererProps> = ({ field, value, onC
<select
value={fieldValue}
onChange={(e) => updateItem(itemIndex, fieldConfig.name, e.target.value)}
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)] text-sm"
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)] text-sm disabled:opacity-50 disabled:cursor-not-allowed"
required={fieldConfig.required}
disabled={isFieldDisabled}
>
<option value="">Seleccionar...</option>
<option value="">{fieldConfig.placeholder || 'Seleccionar...'}</option>
{fieldConfig.options?.map((option: any) => (
<option key={option.value} value={option.value}>{option.label}</option>
))}
@@ -87,11 +89,12 @@ const ListFieldRenderer: React.FC<ListFieldRendererProps> = ({ field, value, onC
type="number"
value={fieldValue}
onChange={(e) => updateItem(itemIndex, fieldConfig.name, parseFloat(e.target.value) || 0)}
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)] text-sm"
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)] text-sm disabled:opacity-50 disabled:cursor-not-allowed"
min="0"
step={fieldConfig.type === 'currency' ? '0.01' : '0.1'}
placeholder={fieldConfig.placeholder}
required={fieldConfig.required}
disabled={isFieldDisabled}
/>
);
@@ -101,14 +104,17 @@ const ListFieldRenderer: React.FC<ListFieldRendererProps> = ({ field, value, onC
type="text"
value={fieldValue}
onChange={(e) => updateItem(itemIndex, fieldConfig.name, e.target.value)}
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)] text-sm"
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)] text-sm disabled:opacity-50 disabled:cursor-not-allowed"
placeholder={fieldConfig.placeholder}
required={fieldConfig.required}
disabled={isFieldDisabled}
/>
);
}
};
const isDisabled = listConfig.disabled ?? false;
return (
<div className="space-y-4">
<div className="flex items-center justify-between">
@@ -116,7 +122,12 @@ const ListFieldRenderer: React.FC<ListFieldRendererProps> = ({ field, value, onC
<button
type="button"
onClick={addItem}
className="flex items-center gap-2 px-3 py-1.5 text-sm bg-[var(--color-primary)] text-white rounded-md hover:bg-[var(--color-primary)]/90 transition-colors"
disabled={isDisabled}
className={`flex items-center gap-2 px-3 py-1.5 text-sm rounded-md transition-colors ${
isDisabled
? 'bg-gray-300 text-gray-500 cursor-not-allowed opacity-50'
: 'bg-[var(--color-primary)] text-white hover:bg-[var(--color-primary)]/90'
}`}
>
<Plus className="w-4 h-4" />
{listConfig.addButtonLabel || t('common:modals.actions.add', 'Agregar')}
@@ -129,7 +140,7 @@ const ListFieldRenderer: React.FC<ListFieldRendererProps> = ({ field, value, onC
<Plus className="w-full h-full" />
</div>
<p>{listConfig.emptyStateText || 'No hay elementos agregados'}</p>
<p className="text-sm">Haz clic en "{listConfig.addButtonLabel || 'Agregar'}" para comenzar</p>
{!isDisabled && <p className="text-sm">Haz clic en "{listConfig.addButtonLabel || 'Agregar'}" para comenzar</p>}
</div>
) : (
<div className="space-y-3 max-h-96 overflow-y-auto">
@@ -204,12 +215,14 @@ export interface AddModalField {
options?: Array<{label: string; value: string | number}>;
defaultValue?: any;
validation?: (value: any) => string | null;
disabled?: boolean;
}>;
addButtonLabel?: string;
removeButtonLabel?: string;
emptyStateText?: string;
showSubtotals?: boolean; // For calculating item totals
subtotalFields?: { quantity: string; price: string }; // Field names for calculation
disabled?: boolean; // Disable adding new items
};
}
@@ -686,8 +699,7 @@ export const AddModal: React.FC<AddModalProps> = ({
disabled={loading}
className="min-w-[80px]"
>
<X className="w-4 h-4 mr-2" />
{t('common:modals.actions.cancel', 'Cancelar')}
{t('common:modals.actions.cancel', 'Cancelar')}
</Button>
<Button
variant="primary"
@@ -698,10 +710,7 @@ export const AddModal: React.FC<AddModalProps> = ({
{loading ? (
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-current"></div>
) : (
<>
<Save className="w-4 h-4 mr-2" />
{t('common:modals.actions.save', 'Guardar')}
</>
t('common:modals.actions.save', 'Guardar')
)}
</Button>
</div>

View File

@@ -54,28 +54,52 @@ const Badge = forwardRef<HTMLSpanElement, BadgeProps>(({
'whitespace-nowrap',
];
// Variant styling using CSS custom properties
const variantStyles: Record<string, React.CSSProperties> = {
default: {},
primary: {
backgroundColor: 'var(--color-primary)',
color: 'white',
borderColor: 'var(--color-primary)',
},
secondary: {
backgroundColor: 'var(--color-secondary)',
color: 'white',
borderColor: 'var(--color-secondary)',
},
success: {
backgroundColor: 'var(--color-success)',
color: 'white',
borderColor: 'var(--color-success)',
},
warning: {
backgroundColor: 'var(--color-warning)',
color: 'white',
borderColor: 'var(--color-warning)',
},
error: {
backgroundColor: 'var(--color-error)',
color: 'white',
borderColor: 'var(--color-error)',
},
info: {
backgroundColor: 'var(--color-info)',
color: 'white',
borderColor: 'var(--color-info)',
},
outline: {},
};
const variantClasses = {
default: [
'bg-bg-tertiary text-text-primary border border-border-primary',
],
primary: [
'bg-color-primary text-text-inverse',
],
secondary: [
'bg-color-secondary text-text-inverse',
],
success: [
'bg-color-success text-text-inverse',
],
warning: [
'bg-color-warning text-text-inverse',
],
error: [
'bg-color-error text-text-inverse',
],
info: [
'bg-color-info text-text-inverse',
'bg-[var(--bg-tertiary)] text-[var(--text-primary)] border border-[var(--border-primary)]',
],
primary: [],
secondary: [],
success: [],
warning: [],
error: [],
info: [],
outline: [
'bg-transparent border border-current',
],
@@ -83,13 +107,13 @@ const Badge = forwardRef<HTMLSpanElement, BadgeProps>(({
const sizeClasses = {
xs: isStandalone ? 'px-1.5 py-0.5 text-xs min-h-4' : 'w-4 h-4 text-xs',
sm: isStandalone ? 'px-2 py-0.5 text-xs min-h-5' : 'w-5 h-5 text-xs',
md: isStandalone ? 'px-2.5 py-1 text-sm min-h-6' : 'w-6 h-6 text-sm',
lg: isStandalone ? 'px-3 py-1.5 text-sm min-h-7' : 'w-7 h-7 text-sm',
sm: isStandalone ? 'px-3 py-1.5 text-sm min-h-6 font-medium' : 'w-5 h-5 text-xs',
md: isStandalone ? 'px-3 py-1.5 text-sm min-h-7 font-semibold' : 'w-6 h-6 text-sm',
lg: isStandalone ? 'px-4 py-2 text-base min-h-8 font-semibold' : 'w-7 h-7 text-sm',
};
const shapeClasses = {
rounded: 'rounded-md',
rounded: 'rounded-lg',
pill: 'rounded-full',
square: 'rounded-none',
};
@@ -171,18 +195,22 @@ const Badge = forwardRef<HTMLSpanElement, BadgeProps>(({
variantClasses[variant],
sizeClasses[size],
shapeClasses[shape],
'border', // Always include border
{
'gap-1': icon || closable,
'pr-1': closable,
'gap-2': icon || closable,
'pr-2': closable,
},
className
);
const customStyle = color ? {
backgroundColor: color,
borderColor: color,
color: getContrastColor(color),
} : undefined;
// Merge custom style with variant style
const customStyle = color
? {
backgroundColor: color,
borderColor: color,
color: getContrastColor(color),
}
: variantStyles[variant] || {};
return (
<span
@@ -192,9 +220,9 @@ const Badge = forwardRef<HTMLSpanElement, BadgeProps>(({
{...props}
>
{icon && (
<span className="flex-shrink-0">{icon}</span>
<span className="flex-shrink-0 flex items-center">{icon}</span>
)}
<span>{text || displayCount || children}</span>
<span className="whitespace-nowrap">{text || displayCount || children}</span>
{closable && onClose && (
<button
type="button"

View File

@@ -38,6 +38,7 @@ export interface StatusCardProps {
onClick: () => void;
priority?: 'primary' | 'secondary' | 'tertiary';
destructive?: boolean;
disabled?: boolean;
}>;
onClick?: () => void;
className?: string;
@@ -292,14 +293,19 @@ export const StatusCard: React.FC<StatusCardProps> = ({
<button
onClick={(e) => {
e.stopPropagation();
primaryActions[0].onClick();
if (!primaryActions[0].disabled) {
primaryActions[0].onClick();
}
}}
disabled={primaryActions[0].disabled}
className={`
flex items-center gap-1 sm:gap-2 px-2 sm:px-3 py-1.5 sm:py-2 text-xs sm:text-sm font-medium rounded-lg
transition-all duration-200 hover:scale-105 active:scale-95 flex-shrink-0 max-w-[120px] sm:max-w-[150px]
${primaryActions[0].destructive
? 'text-red-600 hover:bg-red-50 hover:text-red-700'
: 'text-[var(--color-primary-600)] hover:bg-[var(--color-primary-50)] hover:text-[var(--color-primary-700)]'
${primaryActions[0].disabled
? 'opacity-50 cursor-not-allowed'
: primaryActions[0].destructive
? 'text-red-600 hover:bg-red-50 hover:text-red-700'
: 'text-[var(--color-primary-600)] hover:bg-[var(--color-primary-50)] hover:text-[var(--color-primary-700)]'
}
`}
title={primaryActions[0].label}
@@ -318,14 +324,19 @@ export const StatusCard: React.FC<StatusCardProps> = ({
key={`action-${index}`}
onClick={(e) => {
e.stopPropagation();
action.onClick();
if (!action.disabled) {
action.onClick();
}
}}
disabled={action.disabled}
title={action.label}
className={`
p-1.5 sm:p-2 rounded-lg transition-all duration-200 hover:scale-110 active:scale-95
${action.destructive
? 'text-red-500 hover:bg-red-50 hover:text-red-600'
: 'text-[var(--text-tertiary)] hover:text-[var(--text-primary)] hover:bg-[var(--surface-secondary)]'
${action.disabled
? 'opacity-50 cursor-not-allowed'
: action.destructive
? 'text-red-500 hover:bg-red-50 hover:text-red-600'
: 'text-[var(--text-tertiary)] hover:text-[var(--text-primary)] hover:bg-[var(--surface-secondary)]'
}
`}
>
@@ -339,14 +350,19 @@ export const StatusCard: React.FC<StatusCardProps> = ({
key={`primary-icon-${index}`}
onClick={(e) => {
e.stopPropagation();
action.onClick();
if (!action.disabled) {
action.onClick();
}
}}
disabled={action.disabled}
title={action.label}
className={`
p-1.5 sm:p-2 rounded-lg transition-all duration-200 hover:scale-110 active:scale-95
${action.destructive
? 'text-red-500 hover:bg-red-50 hover:text-red-600'
: 'text-[var(--color-primary-500)] hover:text-[var(--color-primary-600)] hover:bg-[var(--color-primary-50)]'
${action.disabled
? 'opacity-50 cursor-not-allowed'
: action.destructive
? 'text-red-500 hover:bg-red-50 hover:text-red-600'
: 'text-[var(--color-primary-500)] hover:text-[var(--color-primary-600)] hover:bg-[var(--color-primary-50)]'
}
`}
>