diff --git a/packages/crm-service/Summarize.md b/packages/crm-service/Summarize.md index 1c8b55f..8c6f86e 100644 --- a/packages/crm-service/Summarize.md +++ b/packages/crm-service/Summarize.md @@ -59,8 +59,9 @@ packages/crm-service/ - `docker-compose.crm.yml` im Projekt-Root - Port: 3100 - Netzwerke: insight-web, insight-db, insight-cache -- Traefik-Route: /api/v1/crm/* -- JWT Public Key als Read-Only Volume +- Traefik-Route: `Host(172.20.10.59) && PathPrefix(/api/v1/crm)` mit Priority 100 +- JWT Public Key als Read-Only Volume (.keys/jwt-public.pem) +- Direkte PostgreSQL-Verbindung (PgBouncer unterstuetzt kein search_path fuer Schema-Auswahl) ### Sicherheit @@ -70,10 +71,44 @@ packages/crm-service/ - TenantGuard sichert mandantenbezogenen Zugriff - Globaler ValidationPipe (whitelist + forbidNonWhitelisted) - Strict TypeScript, kein `any` +- 401 bei fehlendem/ungueltigem Token + +### Deployment-Status + +**Erfolgreich deployed auf insight-dev-01 (172.20.10.59) am 2026-03-10** + +- Container: insight-crm (Development-Mode) +- Prisma Migration `20260310163211_init` angewendet +- Alle API-Endpunkte getestet und funktionsfaehig +- Traefik-Routing aktiv: http://172.20.10.59/api/v1/crm/* +- Swagger-Docs: nicht ueber Traefik erreichbar (nur Container-intern) + +### Getestete Endpunkte + +| Test | Ergebnis | +|------|----------| +| GET /contacts (leere Liste) | 200 OK, pagination korrekt | +| POST /contacts (Kontakt erstellen) | 201 Created, UUID generiert | +| GET /contacts/:id | 200 OK, Detail korrekt | +| PATCH /contacts/:id | 200 OK, Update + Tags | +| GET /contacts?search=Muster | 200 OK, Suche funktioniert | +| POST /activities (Notiz) | 201 Created, contactId verknuepft | +| POST /pipelines (mit 4 Stages) | 201 Created, Stages korrekt | +| POST /deals | 201 Created, Pipeline/Stage/Contact verknuepft | +| PATCH /deals/:id (WON) | 200 OK, closedAt automatisch gesetzt | +| GET /deals (Liste) | 200 OK, pagination korrekt | +| GET /contacts ohne Token | 401 Unauthorized | +| Validierung (falsche Felder) | 400 Bad Request, Details korrekt | + +### Bekannte Einschraenkungen + +- PgBouncer kann nicht genutzt werden (search_path nicht kompatibel mit transaction pooling) +- Swagger-Docs nur Container-intern erreichbar (kein Traefik-Route fuer /api/v1/crm/docs) ### Naechste Schritte -1. `npm install` in packages/crm-service/ -2. Prisma Migration: `npx prisma migrate dev --schema=prisma/crm.schema.prisma --name init` -3. Docker Build testen -4. Integration mit laufender Plattform testen +1. DELETE-Endpunkte testen (Kontakte, Deals, Pipelines) +2. Swagger-Docs ueber Traefik erreichbar machen (optional) +3. Integration mit Frontend (CRM-Modul im Admin-Bereich) +4. E2E-Tests schreiben +5. Production-Build testen (multi-stage Dockerfile) diff --git a/packages/frontend/src/components/WeatherWidget.module.css b/packages/frontend/src/components/WeatherWidget.module.css new file mode 100644 index 0000000..78fe711 --- /dev/null +++ b/packages/frontend/src/components/WeatherWidget.module.css @@ -0,0 +1,51 @@ +/* ============================================================ + WeatherWidget - Kompakte Wetter-Anzeige + ============================================================ */ + +.weather { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.375rem 0.75rem; + background: var(--color-bg-card); + border: 1px solid var(--color-border); + border-radius: var(--radius-md); + box-shadow: var(--shadow-sm); + font-size: 0.875rem; + color: var(--color-text); + white-space: nowrap; +} + +.weatherIcon { + font-size: 1.25rem; + line-height: 1; +} + +.weatherTemp { + font-weight: 600; + color: var(--color-text); +} + +.weatherLabel { + color: var(--color-text-secondary); + font-size: 0.8125rem; +} + +.weatherCity { + color: var(--color-text-muted); + font-size: 0.75rem; +} + +.hint { + font-size: 0.8125rem; + color: var(--color-text-muted); + text-decoration: none; + padding: 0.375rem 0.75rem; + border-radius: var(--radius-md); + transition: color 0.15s; +} + +.hint:hover { + color: var(--color-primary); + text-decoration: underline; +} diff --git a/packages/frontend/src/components/WeatherWidget.tsx b/packages/frontend/src/components/WeatherWidget.tsx new file mode 100644 index 0000000..175ed6a --- /dev/null +++ b/packages/frontend/src/components/WeatherWidget.tsx @@ -0,0 +1,45 @@ +import { Link } from 'react-router-dom'; +import { useWeather } from '../hooks/useWeather'; +import styles from './WeatherWidget.module.css'; + +interface WeatherWidgetProps { + city: string | null | undefined; +} + +export function WeatherWidget({ city }: WeatherWidgetProps) { + const { data, isLoading, isError } = useWeather(city); + + // Kein Ort im Profil -> Hinweis-Link + if (!city || city.trim().length < 2) { + return ( + + Ort im Profil hinterlegen + + ); + } + + // Laden + if (isLoading) { + return ( +
+ Wetter wird geladen... +
+ ); + } + + // Fehler -> nichts anzeigen (graceful degradation) + if (isError || !data) { + return null; + } + + return ( +
+ {data.icon} + + {Math.round(data.temperature)}°C + + {data.label} + {data.cityName} +
+ ); +} diff --git a/packages/frontend/src/hooks/useWeather.ts b/packages/frontend/src/hooks/useWeather.ts new file mode 100644 index 0000000..b2bc393 --- /dev/null +++ b/packages/frontend/src/hooks/useWeather.ts @@ -0,0 +1,126 @@ +import { useQuery } from '@tanstack/react-query'; + +// ============================================================ +// Open-Meteo API Types +// ============================================================ + +interface GeocodingResponse { + results?: Array<{ + latitude: number; + longitude: number; + name: string; + country: string; + }>; +} + +interface WeatherResponse { + current: { + temperature_2m: number; + weather_code: number; + is_day: number; + }; +} + +// ============================================================ +// Public Types +// ============================================================ + +export interface WeatherData { + temperature: number; + weatherCode: number; + isDay: boolean; + cityName: string; + icon: string; + label: string; +} + +// ============================================================ +// WMO Weather Code Mapping (deutsch) +// ============================================================ + +export function getWeatherInfo( + code: number, + isDay: boolean, +): { icon: string; label: string } { + if (code === 0) + return { icon: isDay ? '\u2600\uFE0F' : '\uD83C\uDF19', label: 'Klar' }; + if (code <= 3) + return { + icon: isDay ? '\u26C5' : '\uD83C\uDF19', + label: 'Teilweise bew\u00F6lkt', + }; + if (code <= 49) + return { icon: '\u2601\uFE0F', label: 'Bew\u00F6lkt' }; + if (code <= 59) + return { icon: '\uD83C\uDF27\uFE0F', label: 'Nieselregen' }; + if (code <= 69) + return { icon: '\uD83C\uDF27\uFE0F', label: 'Regen' }; + if (code <= 79) + return { icon: '\uD83C\uDF28\uFE0F', label: 'Schnee' }; + if (code <= 84) + return { icon: '\uD83C\uDF27\uFE0F', label: 'Regenschauer' }; + if (code <= 94) + return { icon: '\uD83C\uDF28\uFE0F', label: 'Schneeschauer' }; + if (code <= 99) + return { icon: '\u26C8\uFE0F', label: 'Gewitter' }; + return { icon: '\u2601\uFE0F', label: 'Bew\u00F6lkt' }; +} + +// ============================================================ +// Hook +// ============================================================ + +export function useWeather(city: string | null | undefined) { + // Schritt 1: City -> Koordinaten (Geocoding) + const geocoding = useQuery({ + queryKey: ['geocoding', city], + queryFn: async () => { + const url = `https://geocoding-api.open-meteo.com/v1/search?name=${encodeURIComponent(city!)}&count=1&language=de&format=json`; + const res = await fetch(url); + if (!res.ok) throw new Error('Geocoding fehlgeschlagen'); + return res.json() as Promise; + }, + enabled: !!city && city.trim().length >= 2, + staleTime: 24 * 60 * 60 * 1000, // 24 Stunden + retry: 1, + }); + + const geoResult = geocoding.data?.results?.[0]; + + // Schritt 2: Koordinaten -> Aktuelles Wetter + const weather = useQuery({ + queryKey: ['weather', geoResult?.latitude, geoResult?.longitude], + queryFn: async () => { + const url = `https://api.open-meteo.com/v1/forecast?latitude=${geoResult!.latitude}&longitude=${geoResult!.longitude}¤t=temperature_2m,weather_code,is_day&timezone=auto`; + const res = await fetch(url); + if (!res.ok) throw new Error('Wetter-Abfrage fehlgeschlagen'); + return res.json() as Promise; + }, + enabled: !!geoResult, + staleTime: 15 * 60 * 1000, // 15 Minuten + retry: 1, + }); + + // Ergebnis zusammenbauen + const data: WeatherData | undefined = + weather.data && geoResult + ? (() => { + const { temperature_2m, weather_code, is_day } = weather.data.current; + const info = getWeatherInfo(weather_code, is_day === 1); + return { + temperature: temperature_2m, + weatherCode: weather_code, + isDay: is_day === 1, + cityName: geoResult.name, + icon: info.icon, + label: info.label, + }; + })() + : undefined; + + return { + data, + isLoading: geocoding.isLoading || weather.isLoading, + isError: geocoding.isError || weather.isError, + }; +} diff --git a/packages/frontend/src/shell/DashboardPage.module.css b/packages/frontend/src/shell/DashboardPage.module.css new file mode 100644 index 0000000..869b171 --- /dev/null +++ b/packages/frontend/src/shell/DashboardPage.module.css @@ -0,0 +1,19 @@ +/* ============================================================ + DashboardPage - Header Layout + ============================================================ */ + +.header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 1.5rem; + gap: 1rem; + flex-wrap: wrap; +} + +.title { + font-size: 1.5rem; + font-weight: 600; + margin: 0; + color: var(--color-text); +} diff --git a/packages/frontend/src/shell/DashboardPage.tsx b/packages/frontend/src/shell/DashboardPage.tsx index b6df335..c6de3cc 100644 --- a/packages/frontend/src/shell/DashboardPage.tsx +++ b/packages/frontend/src/shell/DashboardPage.tsx @@ -1,13 +1,19 @@ import { useAuth } from '../auth/AuthContext'; +import { WeatherWidget } from '../components/WeatherWidget'; +import styles from './DashboardPage.module.css'; export function DashboardPage() { const { user } = useAuth(); return (
-

- Dashboard -

+ {/* Header: Titel links, Wetter rechts */} +
+

+ Willkommen, {user?.firstName} {user?.lastName} +

+ +
-

- Willkommen, {user?.firstName}! -

INSIGHT Platform - Sprint 1 Alpha