Add new frontend - fix 9

This commit is contained in:
Urtzi Alfaro
2025-07-22 17:01:12 +02:00
parent 5959eb6e15
commit 06cbe3f4e8
16 changed files with 2048 additions and 166 deletions

View File

@@ -9,6 +9,7 @@ import {
ArrowPathIcon,
ScaleIcon, // For accuracy
CalendarDaysIcon, // For last training date
CurrencyEuroIcon
} from '@heroicons/react/24/outline';
import { useAuth } from '../../contexts/AuthContext';
import { useTrainingProgress } from '../../api/hooks/useTrainingProgress'; // Path corrected
@@ -17,14 +18,17 @@ import { ForecastChart } from '../../components/charts/ForecastChart';
import { SalesUploader } from '../../components/data/SalesUploader';
import { NotificationToast } from '../../components/common/NotificationToast';
import { ErrorBoundary } from '../../components/common/ErrorBoundary';
import { defaultProducts } from '../../components/common/ProductSelector';
import {
dataApi,
forecastingApi,
ApiResponse,
ForecastRecord,
SalesRecord,
TrainingRequest,
} from '../../api/services/api'; // Consolidated API services and types
TrainingJobProgress
} from '@/api/services';
import api from '@/api/services';
// Dashboard specific types
interface DashboardStats {
@@ -141,7 +145,7 @@ const DashboardPage: React.FC = () => {
setLoadingData(true);
try {
// Fetch Dashboard Stats
const statsResponse: ApiResponse<DashboardStats> = await dataApi.getDashboardStats();
const statsResponse: ApiResponse<DashboardStats> = await api.data.dataApi.getDashboardStats();
if (statsResponse.data) {
setStats(statsResponse.data);
} else if (statsResponse.message) {
@@ -149,7 +153,7 @@ const DashboardPage: React.FC = () => {
}
// Fetch initial forecasts (e.g., for a default product or the first available product)
const forecastResponse: ApiResponse<ForecastRecord[]> = await forecastingApi.getForecast({
const forecastResponse: ApiResponse<ForecastRecord[]> = await api.forecasting.getForecast({
forecast_days: 7, // Example: 7 days forecast
product_name: user?.tenant_id ? 'pan' : undefined, // Default to 'pan' or first product
});
@@ -177,7 +181,7 @@ const DashboardPage: React.FC = () => {
const handleSalesUpload = async (file: File) => {
try {
addNotification('info', 'Subiendo archivo', 'Cargando historial de ventas...');
const response = await dataApi.uploadSalesHistory(file);
const response = await api.data.dataApi.uploadSalesHistory(file);
addNotification('success', 'Subida Completa', 'Historial de ventas cargado exitosamente.');
// After upload, trigger a new training (assuming this is the flow)
@@ -186,9 +190,9 @@ const DashboardPage: React.FC = () => {
// You might want to specify products if the uploader supports it,
// or let the backend determine based on the uploaded data.
};
const trainingTask: TrainingTask = await trainingApi.startTraining(trainingRequest);
setActiveJobId(trainingTask.job_id);
addNotification('info', 'Entrenamiento iniciado', `Un nuevo entrenamiento ha comenzado (ID: ${trainingTask.job_id}).`);
const trainingTask: TrainingJobProgress = await api.training.trainingApi.startTraining(trainingRequest);
setActiveJobId(trainingTask.id);
addNotification('info', 'Entrenamiento iniciado', `Un nuevo entrenamiento ha comenzado (ID: ${trainingTask.id}).`);
// No need to fetch dashboard data here, as useEffect for isTrainingComplete will handle it
} catch (error: any) {
console.error('Error uploading sales or starting training:', error);
@@ -199,7 +203,7 @@ const DashboardPage: React.FC = () => {
const handleForecastProductChange = async (productName: string) => {
setLoadingData(true);
try {
const forecastResponse: ApiResponse<ForecastRecord[]> = await forecastingApi.getForecast({
const forecastResponse: ApiResponse<ForecastRecord[]> = await api.forecasting.forecastingApi.getForecast({
forecast_days: 7,
product_name: productName,
});
@@ -280,7 +284,7 @@ const DashboardPage: React.FC = () => {
<StatsCard
title="Ingresos Totales"
value={stats?.totalRevenue}
icon={CurrencyEuroIcon} {/* Assuming CurrencyEuroIcon from heroicons */}
icon={CurrencyEuroIcon}
format="currency"
loading={loadingData}
/>

View File

@@ -1,5 +1,5 @@
// frontend/src/pages/onboarding.tsx - ORIGINAL DESIGN WITH AUTH FIXES ONLY
import React, { useState, useEffect, useCallback } from 'react';
import React, { useState, useRef, useEffect, useCallback } from 'react';
import { useRouter } from 'next/router';
import Head from 'next/head';
import {
@@ -14,7 +14,6 @@ import {
import { SalesUploader } from '../components/data/SalesUploader';
import { TrainingProgressCard } from '../components/training/TrainingProgressCard';
import { useAuth } from '../contexts/AuthContext';
import { RegisterData } from '../api/services/authService';
import { dataApi, TrainingRequest, TrainingTask } from '../api/services/api';
import { NotificationToast } from '../components/common/NotificationToast';
import { Product, defaultProducts } from '../components/common/ProductSelector';
@@ -76,6 +75,76 @@ const OnboardingPage: React.FC = () => {
const [errors, setErrors] = useState<Partial<OnboardingFormData>>({});
const addressInputRef = useRef<HTMLInputElement>(null); // Ref for the address input
let autocompleteTimeout: NodeJS.Timeout | null = null; // For debouncing API calls
const handleAddressInputChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
const query = e.target.value;
setFormData(prevData => ({ ...prevData, address: query })); // Update address immediately
if (autocompleteTimeout) {
clearTimeout(autocompleteTimeout);
}
if (query.length < 3) { // Only search if at least 3 characters are typed
return;
}
autocompleteTimeout = setTimeout(async () => {
try {
// Construct the Nominatim API URL
// Make sure NOMINATIM_PORT matches your .env file, default is 8080
const gatewayNominatimApiUrl = `/api/v1/nominatim/search`; // Relative path if frontend serves from gateway's domain/port
const params = new URLSearchParams({
q: query,
format: 'json',
addressdetails: '1', // Request detailed address components
limit: '5', // Number of results to return
'accept-language': 'es', // Request results in Spanish
countrycodes: 'es' // Restrict search to Spain
});
const response = await fetch(`${gatewayNominatimApiUrl}?${params.toString()}`);
const data = await response.json();
// Process Nominatim results and update form data
if (data && data.length > 0) {
// Take the first result or let the user choose from suggestions if you implement a dropdown
const place = data[0]; // For simplicity, take the first result
let address = '';
let city = '';
let postal_code = '';
// Nominatim's 'address' object contains components
if (place.address) {
const addr = place.address;
// Reconstruct the address in a common format
const street = addr.road || '';
const houseNumber = addr.house_number || '';
address = `${street} ${houseNumber}`.trim();
city = addr.city || addr.town || addr.village || '';
postal_code = addr.postcode || '';
}
setFormData(prevData => ({
...prevData,
address: address || query, // Use parsed address or fall back to user input
city: city || prevData.city,
postal_code: postal_code || prevData.postal_code,
}));
}
} catch (error) {
console.error('Error fetching Nominatim suggestions:', error);
// Optionally show an error notification
// showNotification('error', 'Error de Autocompletado', 'No se pudieron cargar las sugerencias de dirección.');
}
}, 500); // Debounce time: 500ms
}, []); // Re-create if dependencies change, none for now
useEffect(() => {
// If user is already authenticated and on onboarding, redirect to dashboard
if (user && currentStep === 1) {
@@ -106,7 +175,7 @@ const OnboardingPage: React.FC = () => {
setLoading(true);
try {
const registerData: RegisterData = {
const registerData: dataApi.auth.RegisterData = {
full_name: formData.full_name,
email: formData.email,
password: formData.password,
@@ -287,9 +356,11 @@ const OnboardingPage: React.FC = () => {
<input
type="text"
id="address"
ref={addressInputRef}
className="mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:ring-pania-blue focus:border-pania-blue sm:text-sm"
value={formData.address}
onChange={(e) => setFormData({ ...formData, address: e.target.value })}
// Use the new handler for changes to trigger autocomplete
onChange={handleAddressInputChange}
required
/>
{errors.address && <p className="mt-1 text-sm text-red-600">{errors.address}</p>}