diff --git a/client/src/components/Layout.tsx b/client/src/components/Layout.tsx index acdbd3e..1d60d87 100644 --- a/client/src/components/Layout.tsx +++ b/client/src/components/Layout.tsx @@ -6,6 +6,7 @@ import { LogOut, Store, Menu } from "lucide-react"; import { useAuthUnified } from "@/hooks/useAuthUnified"; import { useIsMobile } from "@/hooks/use-mobile"; import Sidebar from "./Sidebar"; +import WeatherWidget from "./WeatherWidget"; import type { Group } from "@shared/schema"; interface StoreContextType { @@ -91,6 +92,8 @@ export default function Layout({ children }: LayoutProps) { )} + {/* Weather widget */} + {/* Store selector for admin - responsive */} diff --git a/client/src/components/RouterProduction.tsx b/client/src/components/RouterProduction.tsx index d6484b7..d4f27a9 100644 --- a/client/src/components/RouterProduction.tsx +++ b/client/src/components/RouterProduction.tsx @@ -22,6 +22,7 @@ import Utilities from "@/pages/Utilities"; import Tasks from "@/pages/Tasks"; import TasksSimplified from "@/pages/TasksSimplified"; import SavTickets from "@/pages/SavTickets"; +import WeatherSettings from "@/pages/WeatherSettings"; import Layout from "@/components/Layout"; function RouterProduction() { @@ -101,6 +102,7 @@ function RouterProduction() { + diff --git a/client/src/components/WeatherWidget.tsx b/client/src/components/WeatherWidget.tsx new file mode 100644 index 0000000..8fb3d15 --- /dev/null +++ b/client/src/components/WeatherWidget.tsx @@ -0,0 +1,110 @@ +import { useQuery } from "@tanstack/react-query"; +import { Cloud, CloudRain, Sun, CloudSnow, ThermometerSun } from "lucide-react"; +import { Card, CardContent } from "@/components/ui/card"; + +interface WeatherData { + id: number; + date: string; + location: string; + maxTemperature: number; + minTemperature: number; + condition: string; + icon: string; + isCurrentYear: boolean; + createdAt: string; + updatedAt: string; +} + +interface WeatherResponse { + currentYear: WeatherData | null; + previousYear: WeatherData | null; + location: string; +} + +const WeatherIcon = ({ condition, size = 16 }: { condition: string; size?: number }) => { + const iconClass = `h-${Math.floor(size/4)} w-${Math.floor(size/4)}`; + + if (condition.includes('rain') || condition.includes('pluie')) { + return ; + } + if (condition.includes('snow') || condition.includes('neige')) { + return ; + } + if (condition.includes('cloud') || condition.includes('nuage')) { + return ; + } + if (condition.includes('sun') || condition.includes('soleil') || condition.includes('clear')) { + return ; + } + return ; +}; + +export default function WeatherWidget() { + const { data: weather, isLoading, error } = useQuery({ + queryKey: ['/api/weather/current'], + refetchInterval: 30 * 60 * 1000, // Refetch every 30 minutes + retry: 1, + }); + + if (isLoading) { + return ( + + +
+
+ +
+ Chargement météo... +
+
+
+ ); + } + + if (error || !weather) { + return null; // Don't show widget if weather service is not configured + } + + const currentTemp = weather.currentYear?.maxTemperature; + const previousTemp = weather.previousYear?.maxTemperature; + const location = weather.location || "Nancy"; + + return ( + + +
+
+ + + {location.split(',')[0]} + +
+ +
+
+ {currentTemp !== undefined ? `${Math.round(currentTemp)}°` : '--°'} +
+ + {previousTemp !== undefined && ( +
+ (l'an dernier: {Math.round(previousTemp)}°) +
+ )} +
+ + {currentTemp !== undefined && previousTemp !== undefined && ( +
+ {currentTemp > previousTemp ? ( + ↑{Math.round(currentTemp - previousTemp)}° + ) : currentTemp < previousTemp ? ( + ↓{Math.round(previousTemp - currentTemp)}° + ) : ( + = + )} +
+ )} +
+
+
+ ); +} \ No newline at end of file diff --git a/client/src/pages/Utilities.tsx b/client/src/pages/Utilities.tsx index 3b9cc19..f46a5c6 100644 --- a/client/src/pages/Utilities.tsx +++ b/client/src/pages/Utilities.tsx @@ -5,7 +5,8 @@ import { Settings, Database, Server, - Bug + Bug, + Cloud } from "lucide-react"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; @@ -15,6 +16,7 @@ import { AlertTriangle } from "lucide-react"; import BackupManager from "./BackupManager"; import NocoDBConfig from "./NocoDBConfig"; import DatabaseDebug from "./DatabaseDebug"; +import WeatherSettings from "./WeatherSettings"; export default function Utilities() { const { user } = useAuthUnified(); @@ -24,6 +26,7 @@ export default function Utilities() { if (path === '/backup') return 'backups'; if (path === '/nocodb-config') return 'nocodb'; if (path === '/database-debug') return 'debug'; + if (path === '/weather-settings') return 'weather'; return 'backups'; }); @@ -31,9 +34,10 @@ export default function Utilities() { const canManageBackups = hasPermission(user?.role || '', 'backups', 'manage'); const canManageNocoDB = user?.role === 'admin'; const canDebugDatabase = user?.role === 'admin'; + const canManageWeather = user?.role === 'admin'; // Si l'utilisateur n'a aucune permission, afficher le message d'accès refusé - if (!canManageBackups && !canManageNocoDB && !canDebugDatabase) { + if (!canManageBackups && !canManageNocoDB && !canDebugDatabase && !canManageWeather) { return (
@@ -69,7 +73,7 @@ export default function Utilities() { {/* Content avec onglets */}
- + {canManageBackups && ( @@ -88,6 +92,12 @@ export default function Utilities() { Debug Base de Données )} + {canManageWeather && ( + + + Météo + + )} {/* Contenu des onglets */} @@ -114,6 +124,14 @@ export default function Utilities() {
)} + + {canManageWeather && ( + +
+ +
+
+ )}
diff --git a/client/src/pages/WeatherSettings.tsx b/client/src/pages/WeatherSettings.tsx new file mode 100644 index 0000000..290c0aa --- /dev/null +++ b/client/src/pages/WeatherSettings.tsx @@ -0,0 +1,265 @@ +import { useState } from "react"; +import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { Switch } from "@/components/ui/switch"; +import { AlertCircle, CheckCircle, Cloud, Settings } from "lucide-react"; +import { Alert, AlertDescription } from "@/components/ui/alert"; +import { useToast } from "@/hooks/use-toast"; +import { apiRequest } from "@/lib/queryClient"; + +interface WeatherSettings { + id: number; + apiKey: string; + location: string; + isActive: boolean; + createdAt: string; + updatedAt: string; +} + +export default function WeatherSettings() { + const { toast } = useToast(); + const queryClient = useQueryClient(); + const [testResult, setTestResult] = useState<{ success: boolean; message: string } | null>(null); + const [isTesting, setIsTesting] = useState(false); + + // Fetch weather settings + const { data: settings, isLoading } = useQuery({ + queryKey: ['/api/weather/settings'], + }); + + // Update settings mutation + const updateSettingsMutation = useMutation({ + mutationFn: async (data: Partial) => { + const url = settings?.id ? `/api/weather/settings/${settings.id}` : '/api/weather/settings'; + const method = settings?.id ? 'PUT' : 'POST'; + + const response = await fetch(url, { + method, + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(data), + }); + + if (!response.ok) { + throw new Error('Failed to save settings'); + } + + return response.json(); + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['/api/weather/settings'] }); + toast({ + title: "Configuration sauvegardée", + description: "Les paramètres météo ont été mis à jour avec succès.", + }); + }, + onError: (error: any) => { + toast({ + title: "Erreur", + description: error.message || "Impossible de sauvegarder les paramètres.", + variant: "destructive", + }); + }, + }); + + // Test API connection + const testConnection = async (apiKey: string, location: string) => { + setIsTesting(true); + setTestResult(null); + + try { + const response = await fetch('/api/weather/test-connection', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ apiKey, location }), + }); + + if (!response.ok) { + throw new Error('Test connection failed'); + } + + const result = await response.json(); + setTestResult(result); + } catch (error: any) { + setTestResult({ + success: false, + message: error.message || "Erreur lors du test de connexion" + }); + } finally { + setIsTesting(false); + } + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + const formData = new FormData(e.currentTarget); + + const data = { + apiKey: formData.get('apiKey') as string, + location: formData.get('location') as string, + isActive: formData.get('isActive') === 'on', + }; + + updateSettingsMutation.mutate(data); + }; + + if (isLoading) { + return ( +
+
+ +

Configuration Météo

+
+ + +
+
+
+
+
+
+
+
+
+ ); + } + + return ( +
+
+ +

Configuration Météo

+
+ + + + + + Paramètres API Visual Crossing + + + Configurez l'API Visual Crossing pour afficher la météo du jour actuel et de l'année précédente. + + + +
+
+ + +

+ Obtenez votre clé API gratuite sur{" "} + + visualcrossing.com + +

+
+ +
+ + +

+ Ville ou région pour laquelle afficher la météo +

+
+ +
+ + +
+ + {testResult && ( + + {testResult.success ? ( + + ) : ( + + )} + {testResult.message} + + )} + +
+ + + +
+
+
+
+ + + + À propos + + +

+ Le widget météo affiche la température maximale du jour actuel ainsi que + celle du même jour l'année précédente, permettant une comparaison rapide. +

+

+ Les données sont mises en cache pour éviter les appels API répétés et + optimiser les performances. +

+

+ L'affichage météo apparaît en haut à gauche de l'interface, + à côté du sélecteur de magasin. +

+
+
+
+ ); +} \ No newline at end of file diff --git a/server/routes.ts b/server/routes.ts index a310db3..6f1e892 100644 --- a/server/routes.ts +++ b/server/routes.ts @@ -36,12 +36,15 @@ import { insertTaskSchema, insertNocodbConfigSchema, insertSavTicketSchema, - insertSavTicketHistorySchema + insertSavTicketHistorySchema, + insertWeatherDataSchema, + insertWeatherSettingsSchema } from "@shared/schema"; import { hasPermission } from "@shared/permissions"; import { z } from "zod"; import { invoiceVerificationService } from "./invoiceVerification"; import { backupService } from "./backupService"; +import { weatherService } from "./weatherService.js"; export async function registerRoutes(app: Express): Promise { // Health check endpoint for Docker @@ -2753,6 +2756,118 @@ RÉSUMÉ DU SCAN } }); + // Weather routes + app.get('/api/weather/settings', isAuthenticated, requireAdmin, async (req: any, res) => { + try { + const settings = await storage.getWeatherSettings(); + res.json(settings); + } catch (error) { + console.error("Error fetching weather settings:", error); + res.status(500).json({ message: "Failed to fetch weather settings" }); + } + }); + + app.post('/api/weather/settings', isAuthenticated, requireAdmin, async (req: any, res) => { + try { + const data = insertWeatherSettingsSchema.parse(req.body); + const settings = await storage.createWeatherSettings(data); + res.json(settings); + } catch (error) { + console.error("Error creating weather settings:", error); + if (error instanceof z.ZodError) { + return res.status(400).json({ message: "Validation error", errors: error.errors }); + } + res.status(500).json({ message: "Failed to create weather settings" }); + } + }); + + app.put('/api/weather/settings/:id', isAuthenticated, requireAdmin, async (req: any, res) => { + try { + const id = parseInt(req.params.id); + const data = insertWeatherSettingsSchema.partial().parse(req.body); + const settings = await storage.updateWeatherSettings(id, data); + res.json(settings); + } catch (error) { + console.error("Error updating weather settings:", error); + if (error instanceof z.ZodError) { + return res.status(400).json({ message: "Validation error", errors: error.errors }); + } + res.status(500).json({ message: "Failed to update weather settings" }); + } + }); + + app.post('/api/weather/test-connection', isAuthenticated, requireAdmin, async (req: any, res) => { + try { + const { apiKey, location } = req.body; + + if (!apiKey || !location) { + return res.status(400).json({ message: "API key and location are required" }); + } + + const result = await weatherService.testApiConnection(apiKey, location); + res.json(result); + } catch (error) { + console.error("Error testing weather API connection:", error); + res.status(500).json({ message: "Failed to test API connection" }); + } + }); + + app.get('/api/weather/current', isAuthenticated, async (req: any, res) => { + try { + const settings = await storage.getWeatherSettings(); + + if (!settings || !settings.isActive) { + return res.status(404).json({ message: "Weather service not configured or disabled" }); + } + + const today = new Date().toISOString().split('T')[0]; + const previousYearDate = weatherService.getPreviousYearDate(); + + // Check if we already have today's data + let currentYearData = await storage.getWeatherData(today, true); + let previousYearData = await storage.getWeatherData(previousYearDate, false); + + // Fetch current year data if not in cache + if (!currentYearData) { + console.log("🌤️ Fetching current weather data from API"); + const apiData = await weatherService.fetchCurrentWeather(settings); + if (apiData) { + const weatherData = weatherService.convertApiDataToWeatherData(apiData, settings.location, true); + if (weatherData) { + currentYearData = await storage.createWeatherData(weatherData); + } + } + } + + // Fetch previous year data if not in cache + if (!previousYearData) { + console.log("🌤️ Fetching previous year weather data from API"); + const apiData = await weatherService.fetchPreviousYearWeather(settings, previousYearDate); + if (apiData) { + const weatherData = weatherService.convertApiDataToWeatherData(apiData, settings.location, false); + if (weatherData) { + previousYearData = await storage.createWeatherData(weatherData); + } + } + } + + res.json({ + currentYear: currentYearData ? { + ...currentYearData, + icon: weatherService.getWeatherIcon(currentYearData.icon) + } : null, + previousYear: previousYearData ? { + ...previousYearData, + icon: weatherService.getWeatherIcon(previousYearData.icon) + } : null, + location: settings.location + }); + } catch (error) { + console.error("Error fetching weather data:", error); + res.status(500).json({ message: "Failed to fetch weather data" }); + } + }); + // Create server instance const httpServer = createServer(app); diff --git a/server/storage.ts b/server/storage.ts index 2bede21..ddc126e 100644 --- a/server/storage.ts +++ b/server/storage.ts @@ -14,6 +14,8 @@ import { invoiceVerificationCache, savTickets, savTicketHistory, + weatherData, + weatherSettings, type User, type UpsertUser, type Group, @@ -55,6 +57,10 @@ import { type InsertSavTicketHistory, type SavTicketWithRelations, type SavTicketHistoryWithCreator, + type WeatherData, + type InsertWeatherData, + type WeatherSettings, + type InsertWeatherSettings, } from "@shared/schema"; import { db } from "./db"; import { eq, and, inArray, desc, sql, gte, lte, lt, or, isNull, isNotNull, asc } from "drizzle-orm"; @@ -187,6 +193,15 @@ export interface IStorage { resolvedTickets: number; criticalTickets: number; }>; + + // Weather operations + getWeatherSettings(): Promise; + createWeatherSettings(settings: InsertWeatherSettings): Promise; + updateWeatherSettings(id: number, settings: Partial): Promise; + getWeatherData(date: string, isCurrentYear: boolean): Promise; + createWeatherData(data: InsertWeatherData): Promise; + updateWeatherData(id: number, data: Partial): Promise; + deleteOldWeatherData(daysToKeep: number): Promise; } export class DatabaseStorage implements IStorage { @@ -1748,6 +1763,63 @@ export class DatabaseStorage implements IStorage { return stats; } + + // Weather operations + async getWeatherSettings(): Promise { + const [settings] = await db.select().from(weatherSettings).limit(1); + return settings; + } + + async createWeatherSettings(settingsData: InsertWeatherSettings): Promise { + const [settings] = await db.insert(weatherSettings).values(settingsData).returning(); + return settings; + } + + async updateWeatherSettings(id: number, settingsData: Partial): Promise { + const [settings] = await db + .update(weatherSettings) + .set({ ...settingsData, updatedAt: new Date() }) + .where(eq(weatherSettings.id, id)) + .returning(); + return settings; + } + + async getWeatherData(date: string, isCurrentYear: boolean): Promise { + const [data] = await db + .select() + .from(weatherData) + .where(and( + eq(weatherData.date, date), + eq(weatherData.isCurrentYear, isCurrentYear) + )) + .limit(1); + return data; + } + + async createWeatherData(data: InsertWeatherData): Promise { + const [weatherInfo] = await db.insert(weatherData).values(data).returning(); + return weatherInfo; + } + + async updateWeatherData(id: number, data: Partial): Promise { + const [weatherInfo] = await db + .update(weatherData) + .set({ ...data, updatedAt: new Date() }) + .where(eq(weatherData.id, id)) + .returning(); + return weatherInfo; + } + + async deleteOldWeatherData(daysToKeep: number): Promise { + const cutoffDate = new Date(); + cutoffDate.setDate(cutoffDate.getDate() - daysToKeep); + + await db + .delete(weatherData) + .where(lt(weatherData.createdAt, cutoffDate)); + + console.log(`🧹 Deleted weather data older than ${daysToKeep} days`); + } } // MemStorage class for development @@ -3109,6 +3181,66 @@ export class MemStorage implements IStorage { this.idCounters.savTicket = 6; console.log('✅ SAV test data initialized with 5 tickets and comprehensive history'); } + + // Weather operations + async getWeatherSettings(): Promise { + // In memory storage, return a default setting for development + return { + id: 1, + apiKey: "6PSHYLBJB9EEYNZ7NDQ5BPGBZ", // Token fourni par l'utilisateur + location: "Nancy, France", + isActive: true, + createdAt: new Date(), + updatedAt: new Date() + }; + } + + async createWeatherSettings(settings: InsertWeatherSettings): Promise { + // For development, we'll just return the created settings with an ID + return { + id: 1, + ...settings, + createdAt: new Date(), + updatedAt: new Date() + }; + } + + async updateWeatherSettings(id: number, settings: Partial): Promise { + const existing = await this.getWeatherSettings(); + return { + ...existing!, + ...settings, + updatedAt: new Date() + }; + } + + async getWeatherData(date: string, isCurrentYear: boolean): Promise { + // In development, return some mock data for testing + return undefined; // Will trigger API calls + } + + async createWeatherData(data: InsertWeatherData): Promise { + return { + id: 1, + ...data, + createdAt: new Date(), + updatedAt: new Date() + }; + } + + async updateWeatherData(id: number, data: Partial): Promise { + const existing = await this.getWeatherData(data.date || "", data.isCurrentYear || true); + return { + ...existing!, + ...data, + updatedAt: new Date() + }; + } + + async deleteOldWeatherData(daysToKeep: number): Promise { + // In development, this is a no-op + console.log(`🧹 DEV: Would delete weather data older than ${daysToKeep} days`); + } } // Use MemStorage in development, DatabaseStorage in production diff --git a/server/weatherService.ts b/server/weatherService.ts new file mode 100644 index 0000000..bf79112 --- /dev/null +++ b/server/weatherService.ts @@ -0,0 +1,172 @@ +import { WeatherData, WeatherSettings } from "../shared/schema.js"; + +export interface WeatherApiResponse { + days: Array<{ + datetime: string; + tempmax: number; + tempmin: number; + icon: string; + conditions: string; + }>; +} + +export interface WeatherDisplayData { + currentYear: WeatherData | null; + previousYear: WeatherData | null; +} + +export class WeatherService { + private baseUrl = "https://weather.visualcrossing.com/VisualCrossingWebServices/rest/services/timeline"; + + /** + * Récupère les données météo actuelles depuis l'API Visual Crossing + */ + async fetchCurrentWeather(settings: WeatherSettings): Promise { + try { + const url = `${this.baseUrl}/${encodeURIComponent(settings.location)}?unitGroup=metric&include=days&key=${settings.apiKey}&contentType=json`; + + const response = await fetch(url); + if (!response.ok) { + console.error(`Weather API error: ${response.status} ${response.statusText}`); + return null; + } + + const data = await response.json(); + return data; + } catch (error) { + console.error("Erreur lors de la récupération de la météo actuelle:", error); + return null; + } + } + + /** + * Récupère les données météo de l'année précédente pour la même date + */ + async fetchPreviousYearWeather(settings: WeatherSettings, targetDate: string): Promise { + try { + const url = `${this.baseUrl}/${encodeURIComponent(settings.location)}?unitGroup=metric&include=days&key=${settings.apiKey}&contentType=json&forecastBasisDate=${targetDate}`; + + const response = await fetch(url); + if (!response.ok) { + console.error(`Weather API error for previous year: ${response.status} ${response.statusText}`); + return null; + } + + const data = await response.json(); + return data; + } catch (error) { + console.error("Erreur lors de la récupération de la météo de l'année précédente:", error); + return null; + } + } + + /** + * Convertit les données de l'API en format WeatherData + */ + convertApiDataToWeatherData(apiData: WeatherApiResponse, location: string, isCurrentYear: boolean): WeatherData | null { + if (!apiData.days || apiData.days.length === 0) { + return null; + } + + const todayData = apiData.days[0]; + + return { + id: 0, // Sera généré par la base de données + date: todayData.datetime, + location, + tempMax: todayData.tempmax.toString(), + tempMin: todayData.tempmin.toString(), + icon: todayData.icon, + conditions: todayData.conditions, + isCurrentYear, + createdAt: new Date(), + updatedAt: new Date() + }; + } + + /** + * Calcule la date de l'année précédente pour la même date + */ + getPreviousYearDate(date: Date = new Date()): string { + const previousYear = new Date(date); + previousYear.setFullYear(date.getFullYear() - 1); + return previousYear.toISOString().split('T')[0]; // Format YYYY-MM-DD + } + + /** + * Convertit l'icône météo en format d'affichage + */ + getWeatherIcon(iconCode: string): string { + const iconMap: { [key: string]: string } = { + 'clear-day': '☀️', + 'clear-night': '🌙', + 'partly-cloudy-day': '⛅', + 'partly-cloudy-night': '☁️', + 'cloudy': '☁️', + 'rain': '🌧️', + 'snow': '❄️', + 'wind': '💨', + 'fog': '🌫️', + 'thunder': '⛈️', + 'hail': '🌨️' + }; + + return iconMap[iconCode] || '🌤️'; + } + + /** + * Valide les paramètres de configuration météo + */ + validateWeatherSettings(settings: Partial): { isValid: boolean; errors: string[] } { + const errors: string[] = []; + + if (!settings.apiKey || settings.apiKey.trim().length === 0) { + errors.push("La clé API Visual Crossing est requise"); + } + + if (!settings.location || settings.location.trim().length === 0) { + errors.push("La localisation est requise"); + } + + // Vérification basique du format de la clé API Visual Crossing + if (settings.apiKey && settings.apiKey.length < 10) { + errors.push("La clé API semble invalide (trop courte)"); + } + + return { + isValid: errors.length === 0, + errors + }; + } + + /** + * Test de connexion à l'API météo + */ + async testApiConnection(apiKey: string, location: string): Promise<{ success: boolean; message: string }> { + try { + const testUrl = `${this.baseUrl}/${encodeURIComponent(location)}?unitGroup=metric&include=days&key=${apiKey}&contentType=json`; + + const response = await fetch(testUrl); + + if (response.ok) { + const data = await response.json(); + if (data.days && data.days.length > 0) { + return { success: true, message: "Connexion réussie à l'API météo" }; + } else { + return { success: false, message: "Aucune donnée météo trouvée pour cette localisation" }; + } + } else if (response.status === 401) { + return { success: false, message: "Clé API invalide" }; + } else if (response.status === 404) { + return { success: false, message: "Localisation introuvable" }; + } else { + return { success: false, message: `Erreur API: ${response.status} ${response.statusText}` }; + } + } catch (error) { + console.error("Erreur test API météo:", error); + return { success: false, message: "Erreur de connexion à l'API météo" }; + } + } +} + +export const weatherService = new WeatherService(); \ No newline at end of file diff --git a/shared/schema.ts b/shared/schema.ts index 794ce0b..d7e31d5 100644 --- a/shared/schema.ts +++ b/shared/schema.ts @@ -700,3 +700,45 @@ export type SavTicketWithRelations = SavTicket & { export type SavTicketHistoryWithCreator = SavTicketHistory & { creator: User; }; + +// Weather Tables +export const weatherData = pgTable("weather_data", { + id: serial("id").primaryKey(), + date: date("date").notNull(), + location: text("location").notNull(), + tempMax: text("temp_max").notNull(), + tempMin: text("temp_min").notNull(), + icon: text("icon").notNull(), + conditions: text("conditions").notNull(), + isCurrentYear: boolean("is_current_year").notNull().default(true), + createdAt: timestamp("created_at").defaultNow(), + updatedAt: timestamp("updated_at").defaultNow(), +}); + +export const weatherSettings = pgTable("weather_settings", { + id: serial("id").primaryKey(), + apiKey: text("api_key").notNull(), + location: text("location").notNull().default("Paris, France"), + isActive: boolean("is_active").notNull().default(true), + createdAt: timestamp("created_at").defaultNow(), + updatedAt: timestamp("updated_at").defaultNow(), +}); + +// Weather Schemas +export const insertWeatherDataSchema = createInsertSchema(weatherData).omit({ + id: true, + createdAt: true, + updatedAt: true, +}); + +export const insertWeatherSettingsSchema = createInsertSchema(weatherSettings).omit({ + id: true, + createdAt: true, + updatedAt: true, +}); + +// Weather Types +export type WeatherData = typeof weatherData.$inferSelect; +export type InsertWeatherData = z.infer; +export type WeatherSettings = typeof weatherSettings.$inferSelect; +export type InsertWeatherSettings = z.infer;