Add role-based filtering and imporve code
This commit is contained in:
@@ -1,7 +1,10 @@
|
||||
import React, { useState, useCallback, useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Button } from '../../../ui/Button';
|
||||
import { useCurrentTenant } from '../../../../stores/tenant.store';
|
||||
import { useCreateTrainingJob, useTrainingWebSocket, useTrainingJobStatus } from '../../../../api/hooks/training';
|
||||
import { Info } from 'lucide-react';
|
||||
|
||||
interface MLTrainingStepProps {
|
||||
onNext: () => void;
|
||||
@@ -22,14 +25,33 @@ interface TrainingProgress {
|
||||
export const MLTrainingStep: React.FC<MLTrainingStepProps> = ({
|
||||
onComplete
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const [trainingProgress, setTrainingProgress] = useState<TrainingProgress | null>(null);
|
||||
const [isTraining, setIsTraining] = useState(false);
|
||||
const [error, setError] = useState<string>('');
|
||||
const [jobId, setJobId] = useState<string | null>(null);
|
||||
const [trainingStartTime, setTrainingStartTime] = useState<number | null>(null);
|
||||
const [showSkipOption, setShowSkipOption] = useState(false);
|
||||
|
||||
const currentTenant = useCurrentTenant();
|
||||
const createTrainingJob = useCreateTrainingJob();
|
||||
|
||||
// Check if training has been running for more than 2 minutes
|
||||
useEffect(() => {
|
||||
if (trainingStartTime && isTraining && !showSkipOption) {
|
||||
const checkTimer = setInterval(() => {
|
||||
const elapsedTime = (Date.now() - trainingStartTime) / 1000; // in seconds
|
||||
if (elapsedTime > 120) { // 2 minutes
|
||||
setShowSkipOption(true);
|
||||
clearInterval(checkTimer);
|
||||
}
|
||||
}, 5000); // Check every 5 seconds
|
||||
|
||||
return () => clearInterval(checkTimer);
|
||||
}
|
||||
}, [trainingStartTime, isTraining, showSkipOption]);
|
||||
|
||||
// Memoized WebSocket callbacks to prevent reconnections
|
||||
const handleProgress = useCallback((data: any) => {
|
||||
setTrainingProgress({
|
||||
@@ -37,7 +59,7 @@ export const MLTrainingStep: React.FC<MLTrainingStepProps> = ({
|
||||
progress: data.data?.progress || 0,
|
||||
message: data.data?.message || 'Entrenando modelo...',
|
||||
currentStep: data.data?.current_step,
|
||||
estimatedTimeRemaining: data.data?.estimated_time_remaining
|
||||
estimatedTimeRemaining: data.data?.estimated_time_remaining_seconds || data.data?.estimated_time_remaining
|
||||
});
|
||||
}, []);
|
||||
|
||||
@@ -177,7 +199,8 @@ export const MLTrainingStep: React.FC<MLTrainingStepProps> = ({
|
||||
});
|
||||
|
||||
setJobId(response.job_id);
|
||||
|
||||
setTrainingStartTime(Date.now()); // Track when training started
|
||||
|
||||
setTrainingProgress({
|
||||
stage: 'queued',
|
||||
progress: 10,
|
||||
@@ -190,6 +213,12 @@ export const MLTrainingStep: React.FC<MLTrainingStepProps> = ({
|
||||
}
|
||||
};
|
||||
|
||||
const handleSkipToDashboard = () => {
|
||||
// Navigate to dashboard while training continues in background
|
||||
console.log('🚀 User chose to skip to dashboard while training continues');
|
||||
navigate('/app/dashboard');
|
||||
};
|
||||
|
||||
const formatTime = (seconds?: number) => {
|
||||
if (!seconds) return '';
|
||||
|
||||
@@ -273,7 +302,7 @@ export const MLTrainingStep: React.FC<MLTrainingStepProps> = ({
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between text-xs text-[var(--text-tertiary)]">
|
||||
<span>{trainingProgress.currentStep || 'Procesando...'}</span>
|
||||
<span>{trainingProgress.currentStep || t('onboarding:steps.ml_training.progress.data_preparation', 'Procesando...')}</span>
|
||||
<div className="flex items-center gap-2">
|
||||
{jobId && (
|
||||
<span className={`text-xs ${isConnected ? 'text-green-500' : 'text-red-500'}`}>
|
||||
@@ -281,7 +310,7 @@ export const MLTrainingStep: React.FC<MLTrainingStepProps> = ({
|
||||
</span>
|
||||
)}
|
||||
{trainingProgress.estimatedTimeRemaining && (
|
||||
<span>Tiempo estimado: {formatTime(trainingProgress.estimatedTimeRemaining)}</span>
|
||||
<span>{t('onboarding:steps.ml_training.estimated_time_remaining', 'Tiempo restante estimado: {{time}}', { time: formatTime(trainingProgress.estimatedTimeRemaining) })}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -293,6 +322,35 @@ export const MLTrainingStep: React.FC<MLTrainingStepProps> = ({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Skip to Dashboard Option - Show after 2 minutes */}
|
||||
{showSkipOption && isTraining && trainingProgress?.stage !== 'completed' && (
|
||||
<div className="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-4">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="flex-shrink-0 mt-0.5">
|
||||
<Info className="w-5 h-5 text-blue-600 dark:text-blue-400" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h4 className="font-medium text-blue-900 dark:text-blue-100 mb-1">
|
||||
{t('onboarding:steps.ml_training.skip_to_dashboard.title', '¿Toma demasiado tiempo?')}
|
||||
</h4>
|
||||
<p className="text-sm text-blue-800 dark:text-blue-200 mb-3">
|
||||
{t('onboarding:steps.ml_training.skip_to_dashboard.info', 'El entrenamiento está tardando más de lo esperado. No te preocupes, puedes explorar tu dashboard mientras el modelo termina de entrenarse en segundo plano.')}
|
||||
</p>
|
||||
<Button
|
||||
onClick={handleSkipToDashboard}
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
>
|
||||
{t('onboarding:steps.ml_training.skip_to_dashboard.button', 'Ir al Dashboard')}
|
||||
</Button>
|
||||
<p className="text-xs text-blue-700 dark:text-blue-300 mt-2">
|
||||
{t('onboarding:steps.ml_training.skip_to_dashboard.training_continues', 'El entrenamiento continúa en segundo plano')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Training Info */}
|
||||
<div className="bg-[var(--bg-secondary)] rounded-lg p-4">
|
||||
<h4 className="font-medium mb-2">¿Qué sucede durante el entrenamiento?</h4>
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import React, { useState } from 'react';
|
||||
import React, { useState, useCallback, useEffect } from 'react';
|
||||
import { Button } from '../../../ui/Button';
|
||||
import { Input } from '../../../ui/Input';
|
||||
import { useRegisterBakery } from '../../../../api/hooks/tenant';
|
||||
import { BakeryRegistration } from '../../../../api/types/tenant';
|
||||
import { nominatimService, NominatimResult } from '../../../../api/services/nominatim';
|
||||
import { debounce } from 'lodash';
|
||||
|
||||
interface RegisterTenantStepProps {
|
||||
onNext: () => void;
|
||||
@@ -27,14 +29,51 @@ export const RegisterTenantStep: React.FC<RegisterTenantStepProps> = ({
|
||||
});
|
||||
|
||||
const [errors, setErrors] = useState<Record<string, string>>({});
|
||||
const [addressSuggestions, setAddressSuggestions] = useState<NominatimResult[]>([]);
|
||||
const [showSuggestions, setShowSuggestions] = useState(false);
|
||||
const [isSearching, setIsSearching] = useState(false);
|
||||
const registerBakery = useRegisterBakery();
|
||||
|
||||
// Debounced address search
|
||||
const searchAddress = useCallback(
|
||||
debounce(async (query: string) => {
|
||||
if (query.length < 3) {
|
||||
setAddressSuggestions([]);
|
||||
return;
|
||||
}
|
||||
|
||||
setIsSearching(true);
|
||||
try {
|
||||
const results = await nominatimService.searchAddress(query);
|
||||
setAddressSuggestions(results);
|
||||
setShowSuggestions(true);
|
||||
} catch (error) {
|
||||
console.error('Address search failed:', error);
|
||||
} finally {
|
||||
setIsSearching(false);
|
||||
}
|
||||
}, 500),
|
||||
[]
|
||||
);
|
||||
|
||||
// Cleanup debounce on unmount
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
searchAddress.cancel();
|
||||
};
|
||||
}, [searchAddress]);
|
||||
|
||||
const handleInputChange = (field: keyof BakeryRegistration, value: string) => {
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
[field]: value
|
||||
}));
|
||||
|
||||
|
||||
// Trigger address search when address field changes
|
||||
if (field === 'address') {
|
||||
searchAddress(value);
|
||||
}
|
||||
|
||||
if (errors[field]) {
|
||||
setErrors(prev => ({
|
||||
...prev,
|
||||
@@ -43,6 +82,20 @@ export const RegisterTenantStep: React.FC<RegisterTenantStepProps> = ({
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddressSelect = (result: NominatimResult) => {
|
||||
const parsed = nominatimService.parseAddress(result);
|
||||
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
address: parsed.street,
|
||||
city: parsed.city,
|
||||
postal_code: parsed.postalCode,
|
||||
}));
|
||||
|
||||
setShowSuggestions(false);
|
||||
setAddressSuggestions([]);
|
||||
};
|
||||
|
||||
const validateForm = () => {
|
||||
const newErrors: Record<string, string> = {};
|
||||
|
||||
@@ -121,15 +174,43 @@ export const RegisterTenantStep: React.FC<RegisterTenantStepProps> = ({
|
||||
isRequired
|
||||
/>
|
||||
|
||||
<div className="md:col-span-2">
|
||||
<div className="md:col-span-2 relative">
|
||||
<Input
|
||||
label="Dirección"
|
||||
placeholder="Calle Principal 123, Ciudad, Provincia"
|
||||
placeholder="Calle Principal 123, Madrid"
|
||||
value={formData.address}
|
||||
onChange={(e) => handleInputChange('address', e.target.value)}
|
||||
onFocus={() => {
|
||||
if (addressSuggestions.length > 0) {
|
||||
setShowSuggestions(true);
|
||||
}
|
||||
}}
|
||||
onBlur={() => {
|
||||
setTimeout(() => setShowSuggestions(false), 200);
|
||||
}}
|
||||
error={errors.address}
|
||||
isRequired
|
||||
/>
|
||||
{isSearching && (
|
||||
<div className="absolute right-3 top-10 text-gray-400">
|
||||
Buscando...
|
||||
</div>
|
||||
)}
|
||||
{showSuggestions && addressSuggestions.length > 0 && (
|
||||
<div className="absolute z-10 w-full mt-1 bg-white border border-gray-300 rounded-lg shadow-lg max-h-60 overflow-y-auto">
|
||||
{addressSuggestions.map((result) => (
|
||||
<div
|
||||
key={result.place_id}
|
||||
className="px-4 py-3 hover:bg-gray-100 cursor-pointer border-b border-gray-100 last:border-b-0"
|
||||
onClick={() => handleAddressSelect(result)}
|
||||
>
|
||||
<div className="text-sm font-medium text-gray-900">
|
||||
{nominatimService.formatAddress(result)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Input
|
||||
|
||||
Reference in New Issue
Block a user