mirror of
https://github.com/R0m1k3/LogiFlow.git
synced 2026-10-12 01:37:40 +02:00
6 files changed
+336
-43
No files matched your search
Binary file not shown.
|
After Width: | Height: | Size: 110 KiB |
@@ -1,6 +1,9 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { Cloud, CloudRain, Sun, CloudSnow, CloudSun, ThermometerSun, CloudDrizzle, Wind, Zap, Eye } from "lucide-react";
|
||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { Cloud, CloudRain, Sun, CloudSnow, CloudSun, ThermometerSun, CloudDrizzle, Wind, Zap, Eye, MapPin, Loader2 } from "lucide-react";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import { apiRequest } from "@/lib/queryClient";
|
||||
|
||||
interface WeatherData {
|
||||
id: number;
|
||||
@@ -118,12 +121,95 @@ const WeatherIcon = ({ condition, size = 20 }: { condition: string; size?: numbe
|
||||
};
|
||||
|
||||
export default function WeatherWidget() {
|
||||
const { toast } = useToast();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { data: weather, isLoading, error } = useQuery<WeatherResponse>({
|
||||
queryKey: ['/api/weather/current'],
|
||||
refetchInterval: 30 * 60 * 1000, // Refetch every 30 minutes
|
||||
retry: 1,
|
||||
});
|
||||
|
||||
// Mutation pour la géolocalisation
|
||||
const geoLocationMutation = useMutation({
|
||||
mutationFn: async (coordinates: { latitude: number; longitude: number }) => {
|
||||
const response = await fetch('/api/weather/geolocation', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(coordinates),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json();
|
||||
throw new Error(error.message || 'Erreur de géolocalisation');
|
||||
}
|
||||
|
||||
return response.json();
|
||||
},
|
||||
onSuccess: (data) => {
|
||||
toast({
|
||||
title: "Localisation mise à jour",
|
||||
description: data.message,
|
||||
});
|
||||
|
||||
// Actualiser les données météo
|
||||
queryClient.invalidateQueries({ queryKey: ['/api/weather/current'] });
|
||||
},
|
||||
onError: (error: any) => {
|
||||
toast({
|
||||
title: "Erreur de géolocalisation",
|
||||
description: error.message || "Impossible de déterminer votre localisation",
|
||||
variant: "destructive",
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
// Fonction pour obtenir la géolocalisation
|
||||
const handleGeolocation = () => {
|
||||
if (!navigator.geolocation) {
|
||||
toast({
|
||||
title: "Géolocalisation non supportée",
|
||||
description: "Votre navigateur ne supporte pas la géolocalisation",
|
||||
variant: "destructive",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
navigator.geolocation.getCurrentPosition(
|
||||
(position) => {
|
||||
const { latitude, longitude } = position.coords;
|
||||
geoLocationMutation.mutate({ latitude, longitude });
|
||||
},
|
||||
(error) => {
|
||||
let message = "Erreur de géolocalisation";
|
||||
switch (error.code) {
|
||||
case error.PERMISSION_DENIED:
|
||||
message = "Autorisation de géolocalisation refusée";
|
||||
break;
|
||||
case error.POSITION_UNAVAILABLE:
|
||||
message = "Position non disponible";
|
||||
break;
|
||||
case error.TIMEOUT:
|
||||
message = "Délai d'attente dépassé";
|
||||
break;
|
||||
}
|
||||
|
||||
toast({
|
||||
title: "Erreur de géolocalisation",
|
||||
description: message,
|
||||
variant: "destructive",
|
||||
});
|
||||
},
|
||||
{
|
||||
enableHighAccuracy: true,
|
||||
timeout: 10000,
|
||||
maximumAge: 300000, // 5 minutes
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<Card className="bg-gradient-to-r from-slate-50 to-slate-100 dark:from-slate-800 dark:to-slate-900 border-slate-200 dark:border-slate-700 shadow-sm">
|
||||
@@ -160,9 +246,25 @@ export default function WeatherWidget() {
|
||||
<WeatherIcon condition={weather.currentYear?.condition || ''} size={24} />
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<span className="text-slate-700 dark:text-slate-200 font-semibold text-sm">
|
||||
{location.split(',')[0]}
|
||||
</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-slate-700 dark:text-slate-200 font-semibold text-sm">
|
||||
{location.split(',')[0]}
|
||||
</span>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={handleGeolocation}
|
||||
disabled={geoLocationMutation.isPending}
|
||||
className="h-6 w-6 p-0 hover:bg-slate-200 dark:hover:bg-slate-600"
|
||||
title="Détecter ma localisation"
|
||||
>
|
||||
{geoLocationMutation.isPending ? (
|
||||
<Loader2 className="h-3 w-3 animate-spin" />
|
||||
) : (
|
||||
<MapPin className="h-3 w-3" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
<span className="text-xs text-slate-500 dark:text-slate-400">
|
||||
{translateToFrench(weather.currentYear?.condition || 'Inconnu')}
|
||||
</span>
|
||||
|
||||
@@ -29,10 +29,10 @@ The platform is built with a clear separation of concerns, utilizing a modern fu
|
||||
- **Data Synchronization:** Logic is implemented to ensure data consistency between development (in-memory storage) and production (PostgreSQL) environments, particularly for order status and relationships.
|
||||
|
||||
**Feature Specifications:**
|
||||
- **Weather Integration:** A comprehensive, modern weather interface displays detailed weather conditions sourced directly from the Visual Crossing API with French language support. Features intelligent date comparison using the same day of the week from the previous year for more relevant weather pattern analysis (e.g., Friday August 15, 2025 vs Friday August 16, 2024).
|
||||
- **Weather Integration:** A comprehensive, modern weather interface displays detailed weather conditions sourced directly from the Visual Crossing API with French language support. Features intelligent date comparison using the same day of the week from the previous year for more relevant weather pattern analysis (e.g., Friday August 15, 2025 vs Friday August 16, 2024). Enhanced with automatic geolocation detection allowing users to click a button to auto-detect their city using GPS coordinates and Visual Crossing reverse geocoding API (August 2025).
|
||||
- **Order and Delivery Management:** Full CRUD operations for orders and deliveries, including linking orders to deliveries and managing their statuses.
|
||||
- **Reconciliation:** Functionality for reconciling Bills of Lading (BL) and invoices with immediate UI updates.
|
||||
- **Invoice Verification Cache:** Robust caching system for invoice verification results with automatic expiration and conflict resolution. Enhanced with unique constraint handling and graceful error management for production stability (August 2025).
|
||||
- **Invoice Verification Cache:** Robust caching system for invoice verification results with automatic expiration and conflict resolution. Enhanced with unique constraint handling and graceful error management for production stability (August 2025). Fixed production SSL certificate errors and fetch timeout loops with enhanced error handling, network timeout protection, and error result caching to prevent infinite retry loops (August 2025).
|
||||
- **UI Scrollbar Optimization:** Eliminated redundant vertical scrollbar on the right side of the screen while preserving necessary internal page scrolling. Implemented global CSS rules with `overflow: hidden` on html/body/root and Layout height management to prevent vertical overflow issues in production (August 2025).
|
||||
- **User and Permission Management:** Granular access controls ensuring that users (e.g., employees) have appropriate permissions for tasks like creating customer orders.
|
||||
- **Reporting:** Statistical reporting, including monthly summaries of deliveries based on `deliveredDate`.
|
||||
|
||||
+133
-36
@@ -201,6 +201,19 @@ export class InvoiceVerificationService {
|
||||
tableId
|
||||
);
|
||||
|
||||
// Gérer les erreurs d'API spécifiques
|
||||
if (matchResult.error) {
|
||||
const result = {
|
||||
exists: false,
|
||||
matchType: 'none' as const,
|
||||
errorMessage: matchResult.error
|
||||
};
|
||||
|
||||
// Sauvegarder en cache même les erreurs pour éviter les rappels répétés
|
||||
await this.saveToCache(invoiceReference, groupId, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
if (matchResult.found) {
|
||||
const result = {
|
||||
exists: true,
|
||||
@@ -224,6 +237,19 @@ export class InvoiceVerificationService {
|
||||
tableId
|
||||
);
|
||||
|
||||
// Gérer les erreurs d'API pour la recherche BL
|
||||
if (matchResult.error) {
|
||||
const result = {
|
||||
exists: false,
|
||||
matchType: 'none' as const,
|
||||
errorMessage: matchResult.error
|
||||
};
|
||||
|
||||
// Sauvegarder en cache même les erreurs
|
||||
await this.saveToCache(invoiceReference, groupId, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
if (matchResult.found) {
|
||||
const result = {
|
||||
exists: true,
|
||||
@@ -240,19 +266,35 @@ export class InvoiceVerificationService {
|
||||
}
|
||||
|
||||
// Aucune correspondance trouvée
|
||||
return {
|
||||
const notFoundResult = {
|
||||
exists: false,
|
||||
matchType: 'none',
|
||||
matchType: 'none' as const,
|
||||
errorMessage: 'Aucune correspondance trouvée dans NocoDB'
|
||||
};
|
||||
|
||||
// Sauvegarder même les résultats "non trouvé" pour éviter les rappels répétés
|
||||
await this.saveToCache(invoiceReference, groupId, notFoundResult);
|
||||
return notFoundResult;
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Erreur lors de la vérification NocoDB:', error);
|
||||
return {
|
||||
const errorMessage = `Erreur NocoDB: ${error instanceof Error ? error.message : 'Erreur inconnue'}`;
|
||||
|
||||
// Log condensé en production
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
console.error('❌ Erreur vérification NocoDB:', error instanceof Error ? error.message : 'Erreur inconnue');
|
||||
} else {
|
||||
console.error('❌ Erreur lors de la vérification NocoDB:', error);
|
||||
}
|
||||
|
||||
const errorResult = {
|
||||
exists: false,
|
||||
matchType: 'none',
|
||||
errorMessage: `Erreur NocoDB: ${error instanceof Error ? error.message : 'Erreur inconnue'}`
|
||||
matchType: 'none' as const,
|
||||
errorMessage
|
||||
};
|
||||
|
||||
// Sauvegarder les erreurs en cache pour éviter les rappels répétés
|
||||
await this.saveToCache(invoiceReference, groupId, errorResult);
|
||||
return errorResult;
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
@@ -266,53 +308,108 @@ export class InvoiceVerificationService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Recherche dans NocoDB
|
||||
* Recherche dans NocoDB avec protection contre les erreurs
|
||||
*/
|
||||
private async searchInNocoDB(
|
||||
searchValue: string,
|
||||
columnName: string,
|
||||
config: any,
|
||||
tableId: string
|
||||
): Promise<{ found: boolean; data?: any }> {
|
||||
): Promise<{ found: boolean; data?: any; error?: string }> {
|
||||
try {
|
||||
// Protection contre les appels avec des paramètres invalides
|
||||
if (!searchValue || !columnName || !config || !tableId) {
|
||||
return { found: false, error: 'Paramètres invalides' };
|
||||
}
|
||||
|
||||
// Utiliser directement l'ID de table fourni
|
||||
const searchUrl = `${config.baseUrl}/api/v1/db/data/v1/${config.projectId}/${tableId}`;
|
||||
|
||||
console.log('🔍 Recherche NocoDB:', {
|
||||
url: searchUrl,
|
||||
tableId: tableId,
|
||||
column: columnName,
|
||||
value: searchValue
|
||||
});
|
||||
// Log condensé en production pour éviter le spam
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
console.log('🔍 Recherche NocoDB:', { tableId, column: columnName });
|
||||
} else {
|
||||
console.log('🔍 Recherche NocoDB:', {
|
||||
url: searchUrl,
|
||||
tableId: tableId,
|
||||
column: columnName,
|
||||
value: searchValue
|
||||
});
|
||||
}
|
||||
|
||||
const response = await fetch(`${searchUrl}?where=(${columnName},eq,${encodeURIComponent(searchValue)})`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'xc-token': config.apiToken
|
||||
// Créer un AbortController pour le timeout
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), 10000); // 10 secondes timeout
|
||||
|
||||
try {
|
||||
const response = await fetch(`${searchUrl}?where=(${columnName},eq,${encodeURIComponent(searchValue)})`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'xc-token': config.apiToken
|
||||
},
|
||||
signal: controller.signal
|
||||
});
|
||||
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
if (!response.ok) {
|
||||
const errorMsg = `HTTP ${response.status}: ${response.statusText}`;
|
||||
console.error('❌ Erreur réponse NocoDB:', errorMsg);
|
||||
return { found: false, error: errorMsg };
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
// Log condensé en production
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
console.log('✅ Réponse NocoDB:', result);
|
||||
}
|
||||
|
||||
if (result.list && result.list.length > 0) {
|
||||
return {
|
||||
found: true,
|
||||
data: result.list[0]
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
console.error('❌ Erreur réponse NocoDB:', response.status, response.statusText);
|
||||
return { found: false };
|
||||
|
||||
} catch (fetchError: any) {
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
// Gestion spécifique des erreurs réseau
|
||||
if (fetchError.name === 'AbortError') {
|
||||
const timeoutMsg = 'Timeout - API NocoDB non accessible';
|
||||
console.error('⏱️ Timeout NocoDB:', timeoutMsg);
|
||||
return { found: false, error: timeoutMsg };
|
||||
}
|
||||
|
||||
if (fetchError.code === 'ERR_CERT_VERIFIER_CHANGED' || fetchError.message?.includes('certificate')) {
|
||||
const certMsg = 'Erreur certificat SSL - Vérifiez la configuration NocoDB';
|
||||
console.error('🔒 Erreur certificat NocoDB:', certMsg);
|
||||
return { found: false, error: certMsg };
|
||||
}
|
||||
|
||||
if (fetchError.code === 'ENOTFOUND' || fetchError.code === 'ECONNREFUSED') {
|
||||
const connMsg = 'NocoDB non accessible - Vérifiez la connectivité réseau';
|
||||
console.error('🌐 Erreur connexion NocoDB:', connMsg);
|
||||
return { found: false, error: connMsg };
|
||||
}
|
||||
|
||||
throw fetchError; // Relancer les autres erreurs
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
console.log('✅ Réponse NocoDB:', result);
|
||||
|
||||
if (result.list && result.list.length > 0) {
|
||||
return {
|
||||
found: true,
|
||||
data: result.list[0]
|
||||
};
|
||||
} catch (error: any) {
|
||||
// Log d'erreur condensé en production
|
||||
const errorMsg = error?.message || 'Erreur inconnue';
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
console.error('❌ Erreur NocoDB:', errorMsg);
|
||||
} else {
|
||||
console.error('❌ Erreur appel NocoDB:', error);
|
||||
}
|
||||
|
||||
return { found: false };
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Erreur appel NocoDB:', error);
|
||||
return { found: false };
|
||||
|
||||
return { found: false, error: errorMsg };
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2934,6 +2934,60 @@ RÉSUMÉ DU SCAN
|
||||
}
|
||||
});
|
||||
|
||||
// Route de géolocalisation météo
|
||||
app.post('/api/weather/geolocation', isAuthenticated, async (req: any, res) => {
|
||||
try {
|
||||
const user = await storage.getUserWithGroups(req.user.claims ? req.user.claims.sub : req.user.id);
|
||||
if (!user) {
|
||||
return res.status(404).json({ message: "User not found" });
|
||||
}
|
||||
|
||||
const { latitude, longitude } = req.body;
|
||||
|
||||
if (!latitude || !longitude) {
|
||||
return res.status(400).json({ message: "Latitude et longitude requises" });
|
||||
}
|
||||
|
||||
// Récupérer les paramètres météo actuels pour obtenir la clé API
|
||||
const settings = await storage.getWeatherSettings();
|
||||
if (!settings || !settings.apiKey) {
|
||||
return res.status(500).json({ message: "Configuration météo manquante" });
|
||||
}
|
||||
|
||||
// Convertir les coordonnées en nom de ville
|
||||
const locationData = await weatherService.getCityFromCoordinates(
|
||||
parseFloat(latitude),
|
||||
parseFloat(longitude),
|
||||
settings.apiKey
|
||||
);
|
||||
|
||||
if (!locationData) {
|
||||
return res.status(500).json({ message: "Impossible de déterminer la ville à partir des coordonnées" });
|
||||
}
|
||||
|
||||
// Mettre à jour automatiquement la configuration météo avec la nouvelle localisation
|
||||
await storage.updateWeatherSettings(settings.id, {
|
||||
location: locationData.fullLocation
|
||||
});
|
||||
|
||||
console.log('🌍 Localisation mise à jour automatiquement:', {
|
||||
from: settings.location,
|
||||
to: locationData.fullLocation,
|
||||
coordinates: { latitude, longitude }
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
location: locationData,
|
||||
message: `Localisation mise à jour vers ${locationData.city}, ${locationData.country}`
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error("Erreur géolocalisation météo:", error);
|
||||
res.status(500).json({ message: "Erreur lors de la géolocalisation" });
|
||||
}
|
||||
});
|
||||
|
||||
// Create server instance
|
||||
const httpServer = createServer(app);
|
||||
|
||||
|
||||
@@ -242,6 +242,46 @@ export class WeatherService {
|
||||
return { success: false, message: "Erreur de connexion à l'API météo" };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convertit les coordonnées GPS en nom de ville via l'API Visual Crossing
|
||||
*/
|
||||
async getCityFromCoordinates(latitude: number, longitude: number, apiKey: string): Promise<{ city: string; country: string; fullLocation: string } | null> {
|
||||
try {
|
||||
const url = `${this.baseUrl}/${latitude},${longitude}?unitGroup=metric&include=current&key=${apiKey}&contentType=json`;
|
||||
|
||||
console.log('🌍 Géolocalisation:', { latitude, longitude });
|
||||
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) {
|
||||
console.error(`Geolocation API error: ${response.status} ${response.statusText}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
// L'API Visual Crossing retourne les informations de localisation
|
||||
if (data.resolvedAddress) {
|
||||
const address = data.resolvedAddress;
|
||||
const parts = address.split(',').map((part: string) => part.trim());
|
||||
|
||||
// Extraire la ville et le pays de l'adresse
|
||||
const city = parts[0] || 'Ville inconnue';
|
||||
const country = parts[parts.length - 1] || 'Pays inconnu';
|
||||
|
||||
return {
|
||||
city,
|
||||
country,
|
||||
fullLocation: address
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error("Erreur lors de la géolocalisation:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const weatherService = new WeatherService();
|
||||
Reference in new issue
Block a user