Add frontend pages imporvements

This commit is contained in:
Urtzi Alfaro
2025-09-21 22:56:55 +02:00
parent f08667150d
commit ecfc6a1997
14 changed files with 1538 additions and 1093 deletions

View File

@@ -23,6 +23,7 @@ export const AddTeamMemberModal: React.FC<AddTeamMemberModalProps> = ({
}) => {
const [formData, setFormData] = useState({
userId: '',
userEmail: '', // Add email field for manual input
role: TENANT_ROLES.MEMBER
});
@@ -33,7 +34,7 @@ export const AddTeamMemberModal: React.FC<AddTeamMemberModalProps> = ({
// Map field positions to form data fields
const fieldMappings = [
// Basic Information section
['userId', 'role']
['userId', 'userEmail', 'role']
];
const fieldName = fieldMappings[sectionIndex]?.[fieldIndex] as keyof typeof formData;
@@ -46,9 +47,9 @@ export const AddTeamMemberModal: React.FC<AddTeamMemberModalProps> = ({
};
const handleSave = async () => {
// Validation
if (!formData.userId) {
alert('Por favor selecciona un usuario');
// Validation - need either userId OR userEmail
if (!formData.userId && !formData.userEmail) {
alert('Por favor selecciona un usuario o ingresa un email');
return;
}
@@ -61,7 +62,7 @@ export const AddTeamMemberModal: React.FC<AddTeamMemberModalProps> = ({
try {
if (onAddMember) {
await onAddMember({
userId: formData.userId,
userId: formData.userId || formData.userEmail, // Use email as userId if no userId selected
role: formData.role
});
}
@@ -69,6 +70,7 @@ export const AddTeamMemberModal: React.FC<AddTeamMemberModalProps> = ({
// Reset form
setFormData({
userId: '',
userEmail: '',
role: TENANT_ROLES.MEMBER
});
@@ -85,6 +87,7 @@ export const AddTeamMemberModal: React.FC<AddTeamMemberModalProps> = ({
// Reset form to initial values
setFormData({
userId: '',
userEmail: '',
role: TENANT_ROLES.MEMBER
});
onClose();
@@ -104,10 +107,12 @@ export const AddTeamMemberModal: React.FC<AddTeamMemberModalProps> = ({
{ label: 'Observador - Solo lectura', value: TENANT_ROLES.VIEWER }
];
const userOptions = availableUsers.map(user => ({
label: `${user.full_name} (${user.email})`,
value: user.id
}));
const userOptions = availableUsers.length > 0
? availableUsers.map(user => ({
label: `${user.full_name} (${user.email})`,
value: user.id
}))
: [];
const getRoleDescription = (role: string) => {
switch (role) {
@@ -127,14 +132,22 @@ export const AddTeamMemberModal: React.FC<AddTeamMemberModalProps> = ({
title: 'Información del Miembro',
icon: Users,
fields: [
{
...(userOptions.length > 0 ? [{
label: 'Usuario',
value: formData.userId,
type: 'select' as const,
editable: true,
required: true,
required: !formData.userEmail, // Only required if email not provided
options: userOptions,
placeholder: 'Seleccionar usuario...'
}] : []),
{
label: userOptions.length > 0 ? 'O Email del Usuario' : 'Email del Usuario',
value: formData.userEmail,
type: 'email' as const,
editable: true,
required: !formData.userId, // Only required if user not selected
placeholder: 'usuario@ejemplo.com'
},
{
label: 'Rol',

View File

@@ -9,16 +9,14 @@ import { Avatar } from '../../ui';
import { Badge } from '../../ui';
import { Modal } from '../../ui';
import { TenantSwitcher } from '../../ui/TenantSwitcher';
import { ThemeToggle } from '../../ui/ThemeToggle';
import { NotificationPanel } from '../../ui/NotificationPanel/NotificationPanel';
import {
Menu,
Search,
Bell,
Sun,
Moon,
Computer,
Settings,
User,
import {
Menu,
Search,
Bell,
Settings,
User,
LogOut,
X
} from 'lucide-react';
@@ -116,7 +114,6 @@ export const Header = forwardRef<HeaderRef, HeaderProps>(({
const [isUserMenuOpen, setIsUserMenuOpen] = useState(false);
const [isSearchFocused, setIsSearchFocused] = useState(false);
const [searchValue, setSearchValue] = useState('');
const [isThemeMenuOpen, setIsThemeMenuOpen] = useState(false);
const [isNotificationPanelOpen, setIsNotificationPanelOpen] = useState(false);
const searchInputRef = React.useRef<HTMLInputElement>(null);
@@ -179,7 +176,6 @@ export const Header = forwardRef<HeaderRef, HeaderProps>(({
// Escape to close menus
if (e.key === 'Escape') {
setIsUserMenuOpen(false);
setIsThemeMenuOpen(false);
setIsNotificationPanelOpen(false);
if (isSearchFocused) {
searchInputRef.current?.blur();
@@ -198,9 +194,6 @@ export const Header = forwardRef<HeaderRef, HeaderProps>(({
if (!target.closest('[data-user-menu]')) {
setIsUserMenuOpen(false);
}
if (!target.closest('[data-theme-menu]')) {
setIsThemeMenuOpen(false);
}
if (!target.closest('[data-notification-panel]')) {
setIsNotificationPanelOpen(false);
}
@@ -210,13 +203,6 @@ export const Header = forwardRef<HeaderRef, HeaderProps>(({
return () => document.removeEventListener('click', handleClickOutside);
}, []);
const themeIcons = {
light: Sun,
dark: Moon,
auto: Computer,
};
const ThemeIcon = themeIcons[theme] || Sun;
return (
<header
@@ -352,45 +338,7 @@ export const Header = forwardRef<HeaderRef, HeaderProps>(({
{/* Theme toggle */}
{showThemeToggle && (
<div className="relative" data-theme-menu>
<Button
variant="ghost"
size="sm"
onClick={() => setIsThemeMenuOpen(!isThemeMenuOpen)}
className="w-10 h-10 p-0 flex items-center justify-center"
aria-label={`Tema actual: ${theme}`}
aria-expanded={isThemeMenuOpen}
aria-haspopup="true"
>
<ThemeIcon className="h-5 w-5" />
</Button>
{isThemeMenuOpen && (
<div className="absolute right-0 top-full mt-2 w-48 bg-[var(--bg-primary)] border border-[var(--border-primary)] rounded-lg shadow-lg py-1 z-[var(--z-dropdown)]">
{[
{ key: 'light' as const, label: 'Claro', icon: Sun },
{ key: 'dark' as const, label: 'Oscuro', icon: Moon },
{ key: 'auto' as const, label: 'Sistema', icon: Computer },
].map(({ key, label, icon: Icon }) => (
<button
key={key}
onClick={() => {
setTheme(key);
setIsThemeMenuOpen(false);
}}
className={clsx(
'w-full px-4 py-2 text-left text-sm flex items-center gap-3',
'hover:bg-[var(--bg-secondary)] transition-colors',
theme === key && 'bg-[var(--bg-secondary)] text-[var(--color-primary)]'
)}
>
<Icon className="h-4 w-4" />
{label}
</button>
))}
</div>
)}
</div>
<ThemeToggle variant="button" size="md" />
)}
{/* Notifications */}

View File

@@ -1,8 +1,9 @@
import React, { useState, useRef, useEffect } from 'react';
import { createPortal } from 'react-dom';
import { useNavigate } from 'react-router-dom';
import { useTenant } from '../../stores/tenant.store';
import { useToast } from '../../hooks/ui/useToast';
import { ChevronDown, Building2, Check, AlertCircle } from 'lucide-react';
import { ChevronDown, Building2, Check, AlertCircle, Plus } from 'lucide-react';
interface TenantSwitcherProps {
className?: string;
@@ -13,6 +14,7 @@ export const TenantSwitcher: React.FC<TenantSwitcherProps> = ({
className = '',
showLabel = true,
}) => {
const navigate = useNavigate();
const [isOpen, setIsOpen] = useState(false);
const [dropdownPosition, setDropdownPosition] = useState<{
top: number;
@@ -23,7 +25,7 @@ export const TenantSwitcher: React.FC<TenantSwitcherProps> = ({
}>({ top: 0, left: 0, width: 288, isMobile: false });
const dropdownRef = useRef<HTMLDivElement>(null);
const buttonRef = useRef<HTMLButtonElement>(null);
const {
currentTenant,
availableTenants,
@@ -33,7 +35,7 @@ export const TenantSwitcher: React.FC<TenantSwitcherProps> = ({
loadUserTenants,
clearError,
} = useTenant();
const { success: showSuccessToast, error: showErrorToast } = useToast();
// Load tenants on mount
@@ -170,6 +172,12 @@ export const TenantSwitcher: React.FC<TenantSwitcherProps> = ({
loadUserTenants();
};
// Handle creating new tenant
const handleCreateNewTenant = () => {
setIsOpen(false);
navigate('/app/onboarding');
};
// Don't render if no tenants available
if (!availableTenants || availableTenants.length === 0) {
return null;
@@ -229,11 +237,8 @@ export const TenantSwitcher: React.FC<TenantSwitcherProps> = ({
{/* Header */}
<div className={`border-b border-border-primary ${dropdownPosition.isMobile ? 'px-4 py-3' : 'px-3 py-2'}`}>
<h3 className={`font-semibold text-text-primary ${dropdownPosition.isMobile ? 'text-base' : 'text-sm'}`}>
Switch Organization
Organizations
</h3>
<p className={`text-text-secondary ${dropdownPosition.isMobile ? 'text-sm mt-1' : 'text-xs'}`}>
Select the organization you want to work with
</p>
</div>
{/* Error State */}
@@ -267,21 +272,25 @@ export const TenantSwitcher: React.FC<TenantSwitcherProps> = ({
>
<div className="flex items-center justify-between">
<div className="flex-1 min-w-0">
<div className="flex items-center space-x-2">
<div className="w-8 h-8 bg-color-primary/10 rounded-full flex items-center justify-center flex-shrink-0">
<Building2 className="w-4 h-4 text-color-primary" />
<div className="flex items-center space-x-3">
<div className={`w-8 h-8 rounded-full flex items-center justify-center flex-shrink-0 ${
tenant.id === currentTenant?.id
? 'bg-color-primary text-white'
: 'bg-color-primary/10 text-color-primary'
}`}>
<Building2 className="w-4 h-4" />
</div>
<div className="min-w-0 flex-1">
<p className="text-sm font-medium text-text-primary truncate">
{tenant.name}
</p>
<p className="text-xs text-text-secondary truncate">
{tenant.business_type} {tenant.city}
{tenant.city}
</p>
</div>
</div>
</div>
{tenant.id === currentTenant?.id && (
<Check className="w-4 h-4 text-color-success flex-shrink-0 ml-2" />
)}
@@ -294,14 +303,17 @@ export const TenantSwitcher: React.FC<TenantSwitcherProps> = ({
<div className={`border-t border-border-primary bg-bg-secondary rounded-b-lg ${
dropdownPosition.isMobile ? 'px-4 py-3' : 'px-3 py-2'
}`}>
<p className={`text-text-secondary ${dropdownPosition.isMobile ? 'text-sm' : 'text-xs'}`}>
Need to add a new organization?{' '}
<button className={`text-color-primary hover:text-color-primary-dark underline ${
dropdownPosition.isMobile ? 'active:text-color-primary-dark' : ''
}`}>
Contact Support
</button>
</p>
<button
onClick={handleCreateNewTenant}
className={`w-full flex items-center justify-center gap-2 ${
dropdownPosition.isMobile ? 'px-4 py-3' : 'px-3 py-2'
} text-color-primary hover:text-color-primary-dark hover:bg-bg-tertiary rounded-lg transition-colors focus:outline-none focus:ring-2 focus:ring-color-primary focus:ring-opacity-20`}
>
<Plus className="w-4 h-4" />
<span className={`font-medium ${dropdownPosition.isMobile ? 'text-sm' : 'text-xs'}`}>
Add New Organization
</span>
</button>
</div>
</div>,
document.body

View File

@@ -2,7 +2,7 @@ import React, { useState } from 'react';
import { clsx } from 'clsx';
import { useTheme } from '../../../contexts/ThemeContext';
import { Button } from '../Button';
import { Sun, Moon, Computer } from 'lucide-react';
import { Sun, Moon } from 'lucide-react';
export interface ThemeToggleProps {
className?: string;
@@ -29,7 +29,7 @@ export interface ThemeToggleProps {
*
* Features:
* - Multiple display variants (button, dropdown, switch)
* - Support for light/dark/system themes
* - Support for light/dark themes
* - Configurable size and labels
* - Accessible keyboard navigation
* - Click outside to close dropdown
@@ -47,10 +47,11 @@ export const ThemeToggle: React.FC<ThemeToggleProps> = ({
const themes = [
{ key: 'light' as const, label: 'Claro', icon: Sun },
{ key: 'dark' as const, label: 'Oscuro', icon: Moon },
{ key: 'auto' as const, label: 'Sistema', icon: Computer },
];
const currentTheme = themes.find(t => t.key === theme) || themes[0];
// If theme is 'auto', use the resolved theme for display
const displayTheme = theme === 'auto' ? resolvedTheme : theme;
const currentTheme = themes.find(t => t.key === displayTheme) || themes[0];
const CurrentIcon = currentTheme.icon;
// Size mappings
@@ -93,20 +94,18 @@ export const ThemeToggle: React.FC<ThemeToggleProps> = ({
return () => document.removeEventListener('click', handleClickOutside);
}, [isDropdownOpen]);
// Cycle through themes for button variant
// Toggle between light and dark for button variant
const handleButtonToggle = () => {
const currentIndex = themes.findIndex(t => t.key === theme);
const nextIndex = (currentIndex + 1) % themes.length;
setTheme(themes[nextIndex].key);
setTheme(resolvedTheme === 'dark' ? 'light' : 'dark');
};
// Handle theme selection
const handleThemeSelect = (themeKey: 'light' | 'dark' | 'auto') => {
const handleThemeSelect = (themeKey: 'light' | 'dark') => {
setTheme(themeKey);
setIsDropdownOpen(false);
};
// Button variant - cycles through themes
// Button variant - toggles between light and dark
if (variant === 'button') {
return (
<Button
@@ -209,14 +208,14 @@ export const ThemeToggle: React.FC<ThemeToggleProps> = ({
'w-full px-4 py-2 text-left text-sm flex items-center gap-3',
'hover:bg-[var(--bg-secondary)] transition-colors',
'focus:bg-[var(--bg-secondary)] focus:outline-none',
theme === key && 'bg-[var(--bg-secondary)] text-[var(--color-primary)]'
displayTheme === key && 'bg-[var(--bg-secondary)] text-[var(--color-primary)]'
)}
role="menuitem"
aria-label={`Cambiar a tema ${label.toLowerCase()}`}
>
<Icon className="w-4 h-4" />
{label}
{theme === key && (
{displayTheme === key && (
<div className="ml-auto w-2 h-2 bg-[var(--color-primary)] rounded-full" />
)}
</button>