========================================
PROJEKTSTRUKTUR
========================================
(Ermittelt via Git unter Berücksichtigung von .gitignore)
.gitignore
Dockerfile
README.md
copyChat.sh
docker-compose.yml
llm_context.txt
next.config.js
package-lock.json
package.json
src/app/api/attractions/[id]/route.ts
src/app/api/attractions/route.ts
src/app/api/hotels/[id]/attractions/route.ts
src/app/api/hotels/[id]/history/route.ts
src/app/api/hotels/[id]/prices/route.ts
src/app/api/hotels/[id]/ratings/route.ts
src/app/api/hotels/[id]/review-summary/route.ts
src/app/api/hotels/[id]/route.ts
src/app/api/hotels/[id]/scan/route.ts
src/app/api/hotels/route.ts
src/app/api/jobs/trigger-daily-check/route.ts
src/app/api/trips/[id]/members/route.ts
src/app/api/trips/route.ts
src/app/api/webhooks/scraper/route.ts
src/app/layout.tsx
src/app/page.tsx
src/lib/db.ts
tsconfig.json

========================================
DATEIINHALTE
========================================

--- DATEI: .gitignore ---
```
# ==========================================
# 1. Lokale Umgebungs- & Geheimdateien
# ==========================================
.env
.env*.local
.env.development.local
.env.test.local
.env.production.local

# ==========================================
# 2. Abhängigkeiten & Paketmanager
# ==========================================
node_modules/
.pnpm-store/

# ==========================================
# 3. Next.js Build- & Output-Dateien
# ==========================================
.next/
out/
build/
dist/

# ==========================================
# 4. Logs & temporäre Dateien
# ==========================================
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
.pnpm-debug.log*
*.log

# ==========================================
# 5. IDEs & Betriebssystem
# ==========================================
.DS_Store
*.pem
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
.idea/
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

node_modules
.next
.git
.gitignore
llm_context.txt
*.log
Dockerfile
docker-compose.yml

```

--- DATEI: Dockerfile ---
```
FROM node:20-alpine

WORKDIR /app

COPY package*.json ./
RUN npm install

COPY . .
RUN npm run build

EXPOSE 3001
ENV PORT=3001

CMD ["npm", "start"]

```

--- DATEI: README.md ---
```

```

--- DATEI: copyChat.sh ---
```
#!/usr/bin/env bash

OUTPUT_FILE="${1:-llm_context.txt}"
ROOT_DIR="${2:-.}"

# Absolute Pfade auflösen
OUTPUT_PATH="$(cd "$(dirname "$OUTPUT_FILE")" 2>/dev/null && pwd)/$(basename "$OUTPUT_FILE")"

cd "$ROOT_DIR" || exit 1

# Ausgabedatei initialisieren / leeren
> "$OUTPUT_PATH"

echo "========================================" >> "$OUTPUT_PATH"
echo "PROJEKTSTRUKTUR" >> "$OUTPUT_PATH"
echo "========================================" >> "$OUTPUT_PATH"

# Prüfen, ob Git verfügbar und das Verzeichnis ein Git-Repo ist
IS_GIT_REPO=false
if command -v git &> /dev/null && git rev-parse --is-inside-work-tree &> /dev/null; then
    IS_GIT_REPO=true
fi

if [ "$IS_GIT_REPO" = true ]; then
    echo "(Ermittelt via Git unter Berücksichtigung von .gitignore)" >> "$OUTPUT_PATH"
    # Zeigt alle getrackten + ungetrackten Dateien an, ignoriert .gitignore-Treffer
    git ls-files -co --exclude-standard >> "$OUTPUT_PATH"
else
    if command -v tree &> /dev/null; then
        tree -I 'node_modules|.git|venv|.venv|__pycache__|dist|build|.next|target' >> "$OUTPUT_PATH"
    else
        find . -maxdepth 3 -not -path '*/.*' -not -path '*/node_modules*' -not -path '*/venv*' >> "$OUTPUT_PATH"
    fi
fi

echo -e "\n========================================" >> "$OUTPUT_PATH"
echo "DATEIINHALTE" >> "$OUTPUT_PATH"
echo "========================================" >> "$OUTPUT_PATH"

# Dateiliste ermitteln
get_file_list() {
    if [ "$IS_GIT_REPO" = true ]; then
        # -c = cached/tracked, -o = others/untracked, --exclude-standard = .gitignore beachten
        git ls-files -co --exclude-standard
    else
        find . -type f \
            -not -path '*/.git/*' \
            -not -path '*/node_modules/*' \
            -not -path '*/venv/*' \
            -not -path '*/.venv/*' \
            -not -path '*/__pycache__/*' \
            -not -path '*/dist/*' \
            -not -path '*/build/*' \
            -not -path '*/.next/*'
    fi
}

# Dateien verarbeiten
get_file_list | sort | while IFS= read -r file; do
    # Überspringe leere Pfade, die Zieldatei selbst oder typische Binär-Endungen
    [ -z "$file" ] && continue
    [ "$file" = "$OUTPUT_FILE" ] && continue
    [ "$(realpath "$file" 2>/dev/null)" = "$OUTPUT_PATH" ] && continue

    case "$file" in
        *.png|*.jpg|*.jpeg|*.gif|*.ico|*.pdf|*.zip|*.tar.gz|*.lock|package-lock.json|yarn.lock)
            continue
            ;;
    esac

    # Prüfen, ob die Datei existiert und lesbarer Text ist
    if [ -f "$file" ] && (grep -Iq . "$file" 2>/dev/null || [ ! -s "$file" ]); then
        echo -e "\n--- DATEI: $file ---" >> "$OUTPUT_PATH"
        echo '```' >> "$OUTPUT_PATH"
        cat "$file" >> "$OUTPUT_PATH"
        echo -e "\n"'```' >> "$OUTPUT_PATH"
    fi
done

echo "Fertig! Projektkontext liegt in: $OUTPUT_PATH"
```

--- DATEI: docker-compose.yml ---
```
services:
  travel-app:
    build: .
    container_name: travel-app
    restart: always
    ports:
      - "3002:3001"
    env_file:
      - .env
    volumes:
      # Spiegelt deinen lokalen Quellcode live in den Container:
      - ./src:/app/src
      - ./public:/app/public
    command: npm run dev
```

--- DATEI: next.config.js ---
```
/** @type {import('next').NextConfig} */
const nextConfig = {
  experimental: {
    serverComponentsExternalPackages: ['bullmq', 'ioredis'],
  },
};

module.exports = nextConfig;

```

--- DATEI: package.json ---
```
{
  "name": "travel-planner-app",
  "version": "1.0.0",
  "private": true,
  "scripts": {
    "dev": "next dev -p 3001",
    "build": "next build",
    "start": "next start -p 3001"
  },
  "dependencies": {
    "axios": "^1.7.0",
    "bullmq": "^6.0.9",
    "ioredis": "^6.0.0",
    "next": "^14.2.0",
    "pg": "^8.11.5",
    "react": "^18.3.0",
    "react-dom": "^18.3.0"
  },
  "devDependencies": {
    "@types/node": "^20.0.0",
    "@types/pg": "^8.11.0",
    "@types/react": "^18.3.0",
    "autoprefixer": "^10.4.0",
    "postcss": "^8.4.0",
    "tailwindcss": "^3.4.0",
    "typescript": "^5.0.0"
  }
}

```

--- DATEI: src/app/api/attractions/[id]/route.ts ---
```
import { NextRequest, NextResponse } from 'next/server';
import { query } from '@/lib/db';

export const dynamic = 'force-dynamic';

const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));

async function parseGoogleMapsUrl(inputUrl: string) {
  let targetUrl = inputUrl.trim();
  if (targetUrl.includes('goo.gl') || targetUrl.includes('maps.app.goo.gl')) {
    try {
      const res = await fetch(targetUrl, {
        method: 'GET',
        headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' },
        redirect: 'follow',
      });
      targetUrl = res.url || targetUrl;
    } catch (err) {}
  }
  let lat: number | null = null;
  let lon: number | null = null;
  const coordMatch = targetUrl.match(/@(-?\d+\.\d+),(-?\d+\.\d+)/);
  if (coordMatch) {
    lat = parseFloat(coordMatch[1]);
    lon = parseFloat(coordMatch[2]);
  }
  return { lat, lon };
}

async function geocode(address: string) {
  if (!address || address.startsWith('http')) return null;
  const search = async (q: string) => {
    try {
      const url = `https://nominatim.openstreetmap.org/search?q=${encodeURIComponent(q)}&format=json&limit=1`;
      const res = await fetch(url, { headers: { 'User-Agent': 'TravelPlannerApp/1.0' } });
      const data = await res.json();
      if (data && data.length > 0) return { lat: parseFloat(data[0].lat), lon: parseFloat(data[0].lon) };
    } catch (err) {}
    return null;
  };

  let coords = await search(address);
  if (!coords && address.includes(',')) {
    const parts = address.split(',').map(p => p.trim());
    await sleep(1100);
    coords = await search(`${parts[parts.length - 2]}, ${parts[parts.length - 1]}`);
  }
  return coords;
}

async function getDriveInfo(origin: { lat: number; lon: number }, dest: { lat: number; lon: number }) {
  try {
    const url = `http://router.project-osrm.org/route/v1/driving/${origin.lon},${origin.lat};${dest.lon},${dest.lat}?overview=false`;
    const res = await fetch(url);
    const data = await res.json();
    if (data.code === 'Ok' && data.routes?.length > 0) {
      return {
        distanceKm: Math.round((data.routes[0].distance / 1000) * 10) / 10,
        durationMin: Math.round(data.routes[0].duration / 60),
      };
    }
  } catch (err) {}
  return { distanceKm: null, durationMin: null };
}

export async function PUT(req: NextRequest, { params }: { params: { id: string } }) {
  try {
    const { id } = params;
    const { name, address, category_icon, notes, hotel_ids, trip_id } = await req.json();

    let sightCoords: { lat: number; lon: number } | null = null;
    if (address.includes('google.com/maps') || address.includes('goo.gl')) {
      const parsed = await parseGoogleMapsUrl(address);
      if (parsed.lat && parsed.lon) sightCoords = { lat: parsed.lat, lon: parsed.lon };
    }
    if (!sightCoords) {
      sightCoords = await geocode(address || name);
    }

    const res = await query(
      `UPDATE hotel_attractions
       SET name = $1,
           address = $2,
           category_icon = $3,
           notes = $4,
           trip_id = $5,
           lat = COALESCE($6, lat),
           lon = COALESCE($7, lon)
       WHERE id = $8
       RETURNING *`,
      [
        name,
        address,
        category_icon || '🏛️',
        notes || null,
        trip_id || null,
        sightCoords?.lat || null,
        sightCoords?.lon || null,
        id,
      ]
    );

    await query('DELETE FROM hotel_attraction_assignments WHERE attraction_id = $1', [id]);

    const selectedHotelIds: string[] = Array.isArray(hotel_ids) ? hotel_ids : [];
    for (const hId of selectedHotelIds) {
      let distanceKm: number | null = null;
      let durationMin: number | null = null;

      const hotelRes = await query('SELECT address, name FROM hotels WHERE id = $1', [hId]);
      if (hotelRes.rows.length > 0) {
        const hotel = hotelRes.rows[0];
        await sleep(1100);
        let hCoords = await geocode(hotel.address);
        if (!hCoords && hotel.name) {
          await sleep(1100);
          hCoords = await geocode(hotel.name);
        }

        if (hCoords && sightCoords) {
          const drive = await getDriveInfo(hCoords, sightCoords);
          distanceKm = drive.distanceKm;
          durationMin = drive.durationMin;
        }
      }

      await query(
        `INSERT INTO hotel_attraction_assignments (attraction_id, hotel_id, distance_km, duration_minutes)
         VALUES ($1, $2, $3, $4)`,
        [id, hId, distanceKm, durationMin]
      );
    }

    return NextResponse.json({ success: true, attraction: res.rows[0] });
  } catch (e: any) {
    return NextResponse.json({ success: false, error: e.message }, { status: 500 });
  }
}

export async function DELETE(req: NextRequest, { params }: { params: { id: string } }) {
  try {
    await query('DELETE FROM hotel_attractions WHERE id = $1', [params.id]);
    return NextResponse.json({ success: true });
  } catch (e: any) {
    return NextResponse.json({ success: false, error: e.message }, { status: 500 });
  }
}
```

--- DATEI: src/app/api/attractions/route.ts ---
```
import { NextRequest, NextResponse } from 'next/server';
import { query } from '@/lib/db';

export const dynamic = 'force-dynamic';

const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));

async function parseGoogleMapsUrl(inputUrl: string) {
  let targetUrl = inputUrl.trim();

  if (targetUrl.includes('goo.gl') || targetUrl.includes('maps.app.goo.gl')) {
    try {
      const res = await fetch(targetUrl, {
        method: 'GET',
        headers: {
          'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
        },
        redirect: 'follow',
      });
      targetUrl = res.url || targetUrl;
    } catch (err) {
      console.error('Fehler beim Auflösen des Google Maps Links:', err);
    }
  }

  let extractedName: string | null = null;
  let lat: number | null = null;
  let lon: number | null = null;

  const placeMatch = targetUrl.match(/\/place\/([^/@?]+)/);
  if (placeMatch && placeMatch[1]) {
    extractedName = decodeURIComponent(placeMatch[1].replace(/\+/g, ' '));
  }

  const coordMatch = targetUrl.match(/@(-?\d+\.\d+),(-?\d+\.\d+)/);
  if (coordMatch) {
    lat = parseFloat(coordMatch[1]);
    lon = parseFloat(coordMatch[2]);
  } else {
    const dataCoordMatch = targetUrl.match(/!3d(-?\d+\.\d+)!4d(-?\d+\.\d+)/);
    if (dataCoordMatch) {
      lat = parseFloat(dataCoordMatch[1]);
      lon = parseFloat(dataCoordMatch[2]);
    } else {
      const queryCoordMatch = targetUrl.match(/[?&](?:q|query|ll)=(-?\d+\.\d+),(-?\d+\.\d+)/);
      if (queryCoordMatch) {
        lat = parseFloat(queryCoordMatch[1]);
        lon = parseFloat(queryCoordMatch[2]);
      }
    }
  }

  return { name: extractedName, lat, lon, resolvedUrl: targetUrl };
}

// Geocoding mit intelligentem Adress-Fallback
async function geocode(address: string): Promise<{ lat: number; lon: number } | null> {
  if (!address || address.startsWith('http')) return null;

  const search = async (q: string) => {
    try {
      const url = `https://nominatim.openstreetmap.org/search?q=${encodeURIComponent(q)}&format=json&limit=1`;
      const res = await fetch(url, {
        headers: { 'User-Agent': 'TravelPlannerApp/1.0 (Homelab Private Usage)' },
      });
      if (res.status === 429) {
        await sleep(1500);
        return null;
      }
      const data = await res.json();
      if (data && data.length > 0) {
        return { lat: parseFloat(data[0].lat), lon: parseFloat(data[0].lon) };
      }
    } catch (err) {
      console.error(`Geocoding Fehler bei "${q}":`, err);
    }
    return null;
  };

  // 1. Exakter Adresstext
  let coords = await search(address);
  if (coords) return coords;

  // 2. Fallback: Ort und Land aus der Komma-Kette filtern
  const parts = address.split(',').map(p => p.trim());
  if (parts.length > 2) {
    await sleep(1100);
    const cityFallback = `${parts[parts.length - 2]}, ${parts[parts.length - 1]}`;
    coords = await search(cityFallback);
  }

  return coords;
}

// OSRM Routing-Berechnung
async function getDriveInfo(origin: { lat: number; lon: number }, dest: { lat: number; lon: number }) {
  try {
    const url = `http://router.project-osrm.org/route/v1/driving/${origin.lon},${origin.lat};${dest.lon},${dest.lat}?overview=false`;
    const res = await fetch(url);
    const data = await res.json();
    if (data.code === 'Ok' && data.routes && data.routes.length > 0) {
      const distanceKm = Math.round((data.routes[0].distance / 1000) * 10) / 10;
      const durationMin = Math.round(data.routes[0].duration / 60);
      return { distanceKm, durationMin };
    }
  } catch (err) {
    console.error('OSRM Routing Fehler:', err);
  }
  return { distanceKm: null, durationMin: null };
}

// GET: Alle Sehenswürdigkeiten laden
export async function GET(req: NextRequest) {
  try {
    const { searchParams } = new URL(req.url);
    const tripId = searchParams.get('tripId');

    let sql = `
      SELECT 
        a.*,
        COALESCE(
          json_agg(
            json_build_object(
              'hotel_id', haa.hotel_id,
              'distance_km', haa.distance_km,
              'duration_minutes', haa.duration_minutes
            )
          ) FILTER (WHERE haa.id IS NOT NULL),
          '[]'
        ) AS assignments
      FROM hotel_attractions a
      LEFT JOIN hotel_attraction_assignments haa ON a.id = haa.attraction_id
      WHERE 1=1
    `;
    const params: any[] = [];

    if (tripId && tripId !== 'ALL') {
      params.push(tripId);
      sql += ` AND a.trip_id = $${params.length}`;
    }

    sql += ' GROUP BY a.id ORDER BY a.created_at ASC';
    const res = await query(sql, params);
    return NextResponse.json({ success: true, attractions: res.rows });
  } catch (e: any) {
    return NextResponse.json({ success: false, error: e.message, attractions: [] }, { status: 500 });
  }
}

// POST: Neues Ausflugsziel anlegen
export async function POST(req: NextRequest) {
  try {
    const { trip_id, hotel_ids, name, address, category_icon, notes } = await req.json();

    if (!address) {
      return NextResponse.json({ error: 'Google Maps Link oder Adresse erforderlich' }, { status: 400 });
    }

    let finalName = name?.trim() || '';
    let finalAddress = address.trim();
    let sightCoords: { lat: number; lon: number } | null = null;

    if (finalAddress.includes('google.com/maps') || finalAddress.includes('goo.gl')) {
      const parsed = await parseGoogleMapsUrl(finalAddress);
      if (parsed.name && !finalName) finalName = parsed.name;
      if (parsed.lat && parsed.lon) sightCoords = { lat: parsed.lat, lon: parsed.lon };
    }

    if (!sightCoords) {
      sightCoords = await geocode(finalAddress || finalName);
    }
    if (!finalName) finalName = finalAddress;

    // 1. In hotel_attractions anlegen
    const insertRes = await query(
      `INSERT INTO hotel_attractions (trip_id, name, address, category_icon, notes, lat, lon)
       VALUES ($1, $2, $3, $4, $5, $6, $7)
       RETURNING *`,
      [
        trip_id || null,
        finalName,
        finalAddress,
        category_icon || '🏛️',
        notes || null,
        sightCoords?.lat || null,
        sightCoords?.lon || null,
      ]
    );

    const newAttraction = insertRes.rows[0];

    // 2. Für alle ausgewählten Hotels Distanz berechnen (mit Pause gegen 429)
    const selectedHotelIds: string[] = Array.isArray(hotel_ids) ? hotel_ids : [];
    for (const hId of selectedHotelIds) {
      let distanceKm: number | null = null;
      let durationMin: number | null = null;

      const hotelRes = await query('SELECT address, name FROM hotels WHERE id = $1', [hId]);
      if (hotelRes.rows.length > 0) {
        const hotel = hotelRes.rows[0];
        
        await sleep(1100); // Fair-Use Rate-Limit für Nominatim einhalten
        let hCoords = await geocode(hotel.address);
        if (!hCoords && hotel.name) {
          await sleep(1100);
          hCoords = await geocode(hotel.name);
        }

        if (hCoords && sightCoords) {
          const drive = await getDriveInfo(hCoords, sightCoords);
          distanceKm = drive.distanceKm;
          durationMin = drive.durationMin;
        }
      }

      await query(
        `INSERT INTO hotel_attraction_assignments (attraction_id, hotel_id, distance_km, duration_minutes)
         VALUES ($1, $2, $3, $4)
         ON CONFLICT (attraction_id, hotel_id) DO UPDATE 
         SET distance_km = EXCLUDED.distance_km, duration_minutes = EXCLUDED.duration_minutes`,
        [newAttraction.id, hId, distanceKm, durationMin]
      );
    }

    return NextResponse.json({ success: true, attraction: newAttraction });
  } catch (e: any) {
    console.error('Fehler beim Anlegen:', e);
    return NextResponse.json({ success: false, error: e.message }, { status: 500 });
  }
}
```

--- DATEI: src/app/api/hotels/[id]/attractions/route.ts ---
```
import { NextRequest, NextResponse } from 'next/server';
import { query } from '@/lib/db';

export const dynamic = 'force-dynamic';

// Google Maps URL auflösen und Name + Koordinaten extrahieren
async function parseGoogleMapsUrl(inputUrl: string) {
  let targetUrl = inputUrl.trim();

  // Kurzlinks (maps.app.goo.gl oder goo.gl) auflösen
  if (targetUrl.includes('goo.gl') || targetUrl.includes('maps.app.goo.gl')) {
    try {
      const res = await fetch(targetUrl, { method: 'HEAD', redirect: 'follow' });
      targetUrl = res.url || targetUrl;
    } catch (err) {
      console.error('Fehler beim Auflösen des Google Maps Kurzlinks:', err);
    }
  }

  let extractedName: string | null = null;
  let lat: number | null = null;
  let lon: number | null = null;

  // 1. Name extrahieren aus /place/Ortsname/
  const placeMatch = targetUrl.match(/\/place\/([^/@?]+)/);
  if (placeMatch && placeMatch[1]) {
    extractedName = decodeURIComponent(placeMatch[1].replace(/\+/g, ' '));
  }

  // 2. Koordinaten aus @lat,lon extrahieren
  const coordMatch = targetUrl.match(/@(-?\d+\.\d+),(-?\d+\.\d+)/);
  if (coordMatch) {
    lat = parseFloat(coordMatch[1]);
    lon = parseFloat(coordMatch[2]);
  } else {
    // Alternativ-Format: !3dLAT!4dLON
    const dataCoordMatch = targetUrl.match(/!3d(-?\d+\.\d+)!4d(-?\d+\.\d+)/);
    if (dataCoordMatch) {
      lat = parseFloat(dataCoordMatch[1]);
      lon = parseFloat(dataCoordMatch[2]);
    }
  }

  return { name: extractedName, lat, lon, resolvedUrl: targetUrl };
}

// Fallback Nominatim Geocoding
async function geocode(address: string): Promise<{ lat: number; lon: number } | null> {
  try {
    const url = `https://nominatim.openstreetmap.org/search?q=${encodeURIComponent(address)}&format=json&limit=1`;
    const res = await fetch(url, {
      headers: { 'User-Agent': 'TravelPlannerApp/1.0 (Homelab Private Usage)' },
    });
    const data = await res.json();
    if (data && data.length > 0) {
      return { lat: parseFloat(data[0].lat), lon: parseFloat(data[0].lon) };
    }
  } catch (err) {
    console.error('Geocoding Fehler:', err);
  }
  return null;
}

// OSRM Fahrzeit- & Distanzberechnung
async function getDriveInfo(origin: { lat: number; lon: number }, dest: { lat: number; lon: number }) {
  try {
    const url = `http://router.project-osrm.org/route/v1/driving/${origin.lon},${origin.lat};${dest.lon},${dest.lat}?overview=false`;
    const res = await fetch(url);
    const data = await res.json();
    if (data.code === 'Ok' && data.routes && data.routes.length > 0) {
      const distanceKm = Math.round((data.routes[0].distance / 1000) * 10) / 10;
      const durationMin = Math.round(data.routes[0].duration / 60);
      return { distanceKm, durationMin };
    }
  } catch (err) {
    console.error('OSRM Routing Fehler:', err);
  }
  return { distanceKm: null, durationMin: null };
}

// GET: Alle Sehenswürdigkeiten eines Hotels abrufen
export async function GET(req: NextRequest, { params }: { params: { id: string } }) {
  try {
    const res = await query(
      `SELECT * FROM hotel_attractions 
       WHERE hotel_id = $1 
       ORDER BY distance_km ASC NULLS LAST, created_at ASC`,
      [params.id]
    );
    return NextResponse.json({ success: true, attractions: res.rows });
  } catch (e: any) {
    return NextResponse.json({ success: false, error: e.message }, { status: 500 });
  }
}

// POST: Google Maps Link oder Adresse verarbeiten & Route berechnen
export async function POST(req: NextRequest, { params }: { params: { id: string } }) {
  try {
    const hotelId = params.id;
    const { name, input_value, category_icon, notes } = await req.json();

    if (!input_value) {
      return NextResponse.json({ error: 'Google Maps Link oder Adresse ist erforderlich' }, { status: 400 });
    }

    // 1. Hotel-Koordinaten ermitteln
    const hotelRes = await query('SELECT address, name FROM hotels WHERE id = $1', [hotelId]);
    if (hotelRes.rows.length === 0) {
      return NextResponse.json({ error: 'Hotel nicht gefunden' }, { status: 404 });
    }
    const hotel = hotelRes.rows[0];
    const hotelCoords = await geocode(hotel.address || hotel.name);

    let finalName = name?.trim() || '';
    let finalAddress = input_value.trim();
    let sightCoords: { lat: number; lon: number } | null = null;

    // 2. Google Maps URL prüfen & parsen
    if (input_value.includes('google.com/maps') || input_value.includes('goo.gl')) {
      const parsedMaps = await parseGoogleMapsUrl(input_value);
      if (parsedMaps.name && !finalName) {
        finalName = parsedMaps.name;
      }
      if (parsedMaps.lat && parsedMaps.lon) {
        sightCoords = { lat: parsedMaps.lat, lon: parsedMaps.lon };
      }
    }

    // Falls keine Koordinaten aus dem Link gelesen werden konnten: Fallback auf Geocoding
    if (!sightCoords) {
      sightCoords = await geocode(finalAddress || finalName);
    }

    if (!finalName) {
      finalName = finalAddress;
    }

    // 3. Fahrzeit & Distanz via OSRM berechnen
    let distanceKm: number | null = null;
    let durationMin: number | null = null;

    if (hotelCoords && sightCoords) {
      const drive = await getDriveInfo(hotelCoords, sightCoords);
      distanceKm = drive.distanceKm;
      durationMin = drive.durationMin;
    }

    // 4. In Postgres speichern
    const insertRes = await query(
      `INSERT INTO hotel_attractions (hotel_id, name, address, category_icon, notes, distance_km, duration_minutes)
       VALUES ($1, $2, $3, $4, $5, $6, $7)
       RETURNING *`,
      [hotelId, finalName, finalAddress, category_icon || '🏛️', notes || null, distanceKm, durationMin]
    );

    return NextResponse.json({ success: true, attraction: insertRes.rows[0] });
  } catch (e: any) {
    console.error('Fehler beim Anlegen der Sehenswürdigkeit:', e);
    return NextResponse.json({ success: false, error: e.message }, { status: 500 });
  }
}
```

--- DATEI: src/app/api/hotels/[id]/history/route.ts ---
```
import { NextRequest, NextResponse } from 'next/server';
import { query } from '@/lib/db';

export async function GET(req: NextRequest, { params }: { params: { id: string } }) {
  try {
    const { id } = params;
    const res = await query(
      `SELECT * FROM hotel_price_history 
       WHERE hotel_id = $1 
       ORDER BY tracked_at DESC`,
      [id]
    );

    // Alle SQL-NUMERIC Strings in echte JS-Numbers umwandeln
    const parsedHistory = res.rows.map((entry) => ({
      ...entry,
      price_numeric: entry.price_numeric ? parseFloat(entry.price_numeric) : 0,
      genius_price_numeric: entry.genius_price_numeric ? parseFloat(entry.genius_price_numeric) : null,
      price_per_night: entry.price_per_night ? parseFloat(entry.price_per_night) : null,
      price_per_person: entry.price_per_person ? parseFloat(entry.price_per_person) : null,
      nights: entry.nights ? parseInt(entry.nights, 10) : 1,
      persons: entry.persons ? parseInt(entry.persons, 10) : 2,
    }));

    return NextResponse.json({ success: true, history: parsedHistory });
  } catch (e: any) {
    console.error('Error fetching history:', e);
    return NextResponse.json({ success: false, error: e.message }, { status: 500 });
  }
}

```

--- DATEI: src/app/api/hotels/[id]/prices/route.ts ---
```
import { NextRequest, NextResponse } from 'next/server';
import { query } from '@/lib/db';

export const dynamic = 'force-dynamic';

export async function GET(req: NextRequest, { params }: { params: { id: string } }) {
  try {
    const { id } = params;

    const res = await query(
      `SELECT 
        id, 
        price_numeric::float AS price, 
        price_per_night::float AS price_per_night,
        price_per_person::float AS price_per_person,
        nights, 
        persons, 
        tracked_at AS created_at 
       FROM hotel_price_history 
       WHERE hotel_id::text = $1::text 
       ORDER BY tracked_at ASC`,
      [id]
    );

    return NextResponse.json({ success: true, prices: res.rows });
  } catch (error: any) {
    console.error('Fehler beim Laden der Preishistorie:', error.message);
    return NextResponse.json({ success: false, error: error.message, prices: [] }, { status: 500 });
  }
}

```

--- DATEI: src/app/api/hotels/[id]/ratings/route.ts ---
```
import { NextRequest, NextResponse } from 'next/server';
import { query } from '@/lib/db';

export const dynamic = 'force-dynamic';

// POST: Bewertung speichern oder aktualisieren
export async function POST(req: NextRequest, { params }: { params: { id: string } }) {
  try {
    const { member_id, grade, comment } = await req.json();
    if (!member_id || !grade) {
      return NextResponse.json({ error: 'Mitreisender und Note erforderlich' }, { status: 400 });
    }

    const res = await query(
      `INSERT INTO hotel_ratings (hotel_id, member_id, grade, comment, updated_at)
       VALUES ($1, $2, $3, $4, NOW())
       ON CONFLICT (hotel_id, member_id) 
       DO UPDATE SET grade = EXCLUDED.grade, comment = EXCLUDED.comment, updated_at = NOW()
       RETURNING *`,
      [params.id, member_id, grade, comment ? comment.trim() : null]
    );

    return NextResponse.json({ success: true, rating: res.rows[0] });
  } catch (e: any) {
    return NextResponse.json({ success: false, error: e.message }, { status: 500 });
  }
}

```

--- DATEI: src/app/api/hotels/[id]/review-summary/route.ts ---
```
import { NextRequest, NextResponse } from 'next/server';
import { query } from '@/lib/db';

export async function POST(req: NextRequest, { params }: { params: { id: string } }) {
  try {
    const { id } = params;
    const { summary_positive, summary_passable, summary_negative, summary_bathroom, summary_count } = await req.json();

    if (!id) {
      return NextResponse.json({ error: 'Hotel-ID fehlt' }, { status: 400 });
    }

    await query(
      `UPDATE hotels 
       SET review_summary_positive = $1,
           review_summary_passable = $2,
           review_summary_negative = $3,
           review_summary_bathroom = $4,
           review_summary_count = $5,
           reviews_updated_at = NOW()
       WHERE id = $6`,
      [summary_positive, summary_passable, summary_negative, summary_bathroom || null, summary_count || null, id]
    );

    return NextResponse.json({ success: true, message: 'Review-Zusammenfassungen gespeichert.' });
  } catch (error: any) {
    console.error('Fehler beim Speichern der Review-Zusammenfassung:', error);
    return NextResponse.json({ success: false, error: error.message }, { status: 500 });
  }
}

```

--- DATEI: src/app/api/hotels/[id]/route.ts ---
```
import { NextRequest, NextResponse } from 'next/server';
import { query } from '@/lib/db';

// PATCH: Tracking aktivieren / deaktivieren (is_active)
export async function PATCH(req: NextRequest, { params }: { params: { id: string } }) {
  try {
    const { id } = params;
    const { is_active } = await req.json();

    await query('UPDATE hotels SET is_active = $1 WHERE id = $2', [is_active, id]);
    return NextResponse.json({ success: true });
  } catch (e: any) {
    return NextResponse.json({ success: false, error: e.message }, { status: 500 });
  }
}

// DELETE: Hotel entfernen
export async function DELETE(req: NextRequest, { params }: { params: { id: string } }) {
  try {
    const { id } = params;
    await query('DELETE FROM hotels WHERE id = $1', [id]);
    return NextResponse.json({ success: true });
  } catch (e: any) {
    return NextResponse.json({ success: false, error: e.message }, { status: 500 });
  }
}

```

--- DATEI: src/app/api/hotels/[id]/scan/route.ts ---
```
import { NextRequest, NextResponse } from 'next/server';
import { query } from '@/lib/db';

export const runtime = 'nodejs';
export const dynamic = 'force-dynamic';

export async function POST(
  req: NextRequest,
  { params }: { params: { id: string } }
) {
  try {
    const { id } = params;

    const bullmq = typeof window === 'undefined' ? eval('require')('bullmq') : null;
    if (!bullmq || !bullmq.Queue) {
      throw new Error('BullMQ Queue Klasse konnte nicht geladen werden.');
    }

    const { Queue } = bullmq;

    const connection = {
      host: process.env.REDIS_HOST || '172.17.0.1',
      port: parseInt(process.env.REDIS_PORT || '6379', 10),
      connectTimeout: 5000,
      maxRetriesPerRequest: 3,
    };

    const scrapeQueue = new Queue('scrape-jobs', { connection });

    // Nur dieses eine Hotel laden
    const res = await query('SELECT id, booking_url, name FROM hotels WHERE id = $1', [id]);
    if (res.rows.length === 0) {
      await scrapeQueue.close();
      return NextResponse.json({ error: 'Hotel nicht gefunden' }, { status: 404 });
    }

    const hotel = res.rows[0];

    const n8nBaseUrl = process.env.APP_WEBHOOK_URL || 'https://n8n.prinz-pilaw.de/webhook-test/scrape-reviews';
    const n8nWebhookUrl = n8nBaseUrl.includes('?')
      ? `${n8nBaseUrl}&hotelId=${hotel.id}`
      : `${n8nBaseUrl}?hotelId=${hotel.id}`;

    const appWebhookUrl = `http://172.17.0.1:3002/api/webhooks/scraper?hotelId=${hotel.id}`;

    // Nur DIESEN einzelnen Job in die Queue schreiben
    await scrapeQueue.add('scrape-hotel', {
      hotelId: hotel.id,
      url: hotel.booking_url,
      appWebhookUrl,
      webhookUrl: n8nWebhookUrl,
    });

    await scrapeQueue.close();

    return NextResponse.json({
      success: true,
      message: `Scrape-Job für "${hotel.name}" erfolgreich gestartet.`,
    });
  } catch (error: any) {
    console.error('Fehler beim Starten des Einzel-Scrapes:', error);
    return NextResponse.json({ success: false, error: error.message }, { status: 500 });
  }
}

```

--- DATEI: src/app/api/hotels/route.ts ---
```
import { NextRequest, NextResponse } from 'next/server';
import { query } from '@/lib/db';

export const dynamic = 'force-dynamic';

export async function GET() {
  try {
      const res = await query(`
SELECT 
        h.*, 
        t.name AS trip_name,
        ph.price_numeric::float AS latest_price,
        ph.price_per_night::float AS latest_price_per_night,
        ph.price_per_person::float AS latest_price_per_person,
        ph.genius_price_numeric::float AS latest_genius_price,
        ph.nights AS latest_nights,
        ph.persons AS latest_persons,
        bp.best_price,
        hr.avg_grade,
        hr.total_votes,
        hr.ratings_json
      FROM hotels h
      LEFT JOIN trips t ON h.trip_id = t.id
      LEFT JOIN LATERAL (
        SELECT price_numeric, genius_price_numeric, price_per_night, price_per_person, nights, persons
        FROM hotel_price_history 
        WHERE hotel_id::text = h.id::text 
        ORDER BY tracked_at DESC NULLS LAST, id DESC
        LIMIT 1
      ) ph ON true
      LEFT JOIN LATERAL (
        SELECT MIN(price_numeric)::float AS best_price
        FROM hotel_price_history
        WHERE hotel_id::text = h.id::text AND price_numeric > 0
      ) bp ON true
      LEFT JOIN LATERAL (
        SELECT 
          ROUND(AVG(hr_sub.grade), 1)::float AS avg_grade,
          COUNT(hr_sub.id)::int AS total_votes,
          COALESCE(
            json_agg(
              json_build_object(
                'member_id', hr_sub.member_id,
                'name', tm.name,
                'color', tm.color,
                'grade', hr_sub.grade,
                'comment', hr_sub.comment
              )
              ORDER BY hr_sub.updated_at DESC
            ),
            '[]'
          ) AS ratings_json
        FROM hotel_ratings hr_sub
        JOIN trip_members tm ON hr_sub.member_id = tm.id
        WHERE hr_sub.hotel_id = h.id
      ) hr ON true
      `);
      return NextResponse.json({ success: true, hotels: res.rows });
  } catch (error: any) {
    console.error('Fehler in GET /api/hotels:', error.message);
    try {
      const fallback = await query(`SELECT * FROM hotels ORDER BY id ASC`);
      return NextResponse.json({ success: true, hotels: fallback.rows });
    } catch (fallbackErr: any) {
      return NextResponse.json({ success: false, error: error.message, hotels: [] }, { status: 500 });
    }
  }
}

export async function POST(req: NextRequest) {
  try {
    const { name, booking_url, trip_id } = await req.json();

    if (!booking_url) {
      return NextResponse.json({ error: 'Booking.com URL erforderlich' }, { status: 400 });
    }

    let checkin_date: string | null = null;
    let checkout_date: string | null = null;

    try {
      const parsedUrl = new URL(booking_url);
      checkin_date = parsedUrl.searchParams.get('checkin');
      checkout_date = parsedUrl.searchParams.get('checkout');
    } catch (e) {}

    const res = await query(
      `INSERT INTO hotels (name, booking_url, checkin_date, checkout_date, trip_id, is_active)
       VALUES ($1, $2, $3, $4, $5, true)
       RETURNING *`,
      [name || 'Neues Hotel', booking_url, checkin_date, checkout_date, trip_id || null]
    );

    return NextResponse.json({ success: true, hotel: res.rows[0] });
  } catch (error: any) {
    console.error('Fehler in POST /api/hotels:', error.message);
    return NextResponse.json({ success: false, error: error.message }, { status: 500 });
  }

  
}


```

--- DATEI: src/app/api/jobs/trigger-daily-check/route.ts ---
```
import { NextResponse } from 'next/server';
import { query } from '@/lib/db';

export const runtime = 'nodejs';
export const dynamic = 'force-dynamic';

export async function POST() {
  try {
    const bullmq = typeof window === 'undefined' ? eval('require')('bullmq') : null;
    if (!bullmq || !bullmq.Queue) {
      throw new Error('BullMQ Queue Klasse konnte nicht geladen werden.');
    }

    const { Queue } = bullmq;

    const connection = {
      host: process.env.REDIS_HOST || '172.17.0.1',
      port: parseInt(process.env.REDIS_PORT || '6379', 10),
      connectTimeout: 5000,
      maxRetriesPerRequest: 3,
    };

    const scrapeQueue = new Queue('scrape-jobs', { connection });

    const res = await query('SELECT id, booking_url FROM hotels WHERE is_active = true');
    const activeHotels = res.rows;

    if (activeHotels.length === 0) {
      await scrapeQueue.close();
      return NextResponse.json({
        success: true,
        message: 'Keine aktiven Hotels zum Scrapen gefunden.',
      });
    }

    // Für ALLE aktiven Hotels nur den Preis-Scan anwerfen (OHNE n8n / OHNE LLM)
    for (const hotel of activeHotels) {
      const appWebhookUrl = `http://172.17.0.1:3002/api/webhooks/scraper?hotelId=${hotel.id}`;

      await scrapeQueue.add('scrape-hotel', {
        hotelId: hotel.id,
        url: hotel.booking_url,
        appWebhookUrl, // Speichert den Preis in Postgres
        webhookUrl: null, // KEIN n8n -> KOSTENLOS!
      });
    }

    await scrapeQueue.close();

    return NextResponse.json({
      success: true,
      message: `Preis-Scan für ${activeHotels.length} Hotels gestartet.`,
    });
  } catch (error: any) {
    console.error('Fehler beim Starten des Preis-Scans:', error);
    return NextResponse.json({ success: false, error: error.message }, { status: 500 });
  }
}

```

--- DATEI: src/app/api/trips/[id]/members/route.ts ---
```
import { NextRequest, NextResponse } from 'next/server';
import { query } from '@/lib/db';

export const dynamic = 'force-dynamic';

// GET: Alle Mitreisenden eines Trips laden
export async function GET(req: NextRequest, { params }: { params: { id: string } }) {
  try {
    const res = await query(
      `SELECT * FROM trip_members WHERE trip_id = $1 ORDER BY created_at ASC`,
      [params.id]
    );
    return NextResponse.json({ success: true, members: res.rows });
  } catch (e: any) {
    return NextResponse.json({ success: false, error: e.message }, { status: 500 });
  }
}

// POST: Neuen Mitreisenden hinzufügen
export async function POST(req: NextRequest, { params }: { params: { id: string } }) {
  try {
    const { name, color } = await req.json();
    if (!name) {
      return NextResponse.json({ error: 'Name ist erforderlich' }, { status: 400 });
    }

    const defaultColors = ['#2563eb', '#db2777', '#059669', '#d97706', '#7c3aed', '#0891b2'];
    const randomColor = color || defaultColors[Math.floor(Math.random() * defaultColors.length)];

    const res = await query(
      `INSERT INTO trip_members (trip_id, name, color)
       VALUES ($1, $2, $3)
       RETURNING *`,
      [params.id, name.trim(), randomColor]
    );

    return NextResponse.json({ success: true, member: res.rows[0] });
  } catch (e: any) {
    return NextResponse.json({ success: false, error: e.message }, { status: 500 });
  }
}

// DELETE: Mitreisenden entfernen
export async function DELETE(req: NextRequest) {
  try {
    const { searchParams } = new URL(req.url);
    const memberId = searchParams.get('memberId');
    if (!memberId) return NextResponse.json({ error: 'memberId fehlt' }, { status: 400 });

    await query(`DELETE FROM trip_members WHERE id = $1`, [memberId]);
    return NextResponse.json({ success: true });
  } catch (e: any) {
    return NextResponse.json({ success: false, error: e.message }, { status: 500 });
  }
}

```

--- DATEI: src/app/api/trips/route.ts ---
```
import { NextRequest, NextResponse } from 'next/server';
import { query } from '@/lib/db';

export const dynamic = 'force-dynamic';

export async function GET() {
  try {
    const res = await query(`SELECT * FROM trips ORDER BY id ASC`);
    return NextResponse.json({ success: true, trips: res.rows });
  } catch (error: any) {
    return NextResponse.json({ success: false, error: error.message, trips: [] }, { status: 500 });
  }
}

export async function POST(req: NextRequest) {
  try {
    const { name } = await req.json();
    if (!name) {
      return NextResponse.json({ error: 'Name der Reise erforderlich' }, { status: 400 });
    }

    const res = await query(
      `INSERT INTO trips (name) VALUES ($1) RETURNING *`,
      [name]
    );

    return NextResponse.json({ success: true, trip: res.rows[0] });
  } catch (error: any) {
    return NextResponse.json({ success: false, error: error.message }, { status: 500 });
  }
}

```

--- DATEI: src/app/api/webhooks/scraper/route.ts ---
```
import { NextRequest, NextResponse } from 'next/server';
import { query } from '@/lib/db';

function parseGermanPrice(priceStr: any): number | null {
  if (!priceStr) return null;
  if (typeof priceStr === 'number') return Math.round(priceStr);

  let cleaned = String(priceStr).replace(/[^0-9.,]/g, '').trim();
  if (!cleaned) return null;

  if (cleaned.includes('.') && !cleaned.includes(',')) {
    cleaned = cleaned.replace(/\./g, '');
  } else if (cleaned.includes('.') && cleaned.includes(',')) {
    cleaned = cleaned.replace(/\./g, '').replace(',', '.');
  } else if (cleaned.includes(',')) {
    cleaned = cleaned.replace(',', '.');
  }

  const parsed = parseFloat(cleaned);
  return isNaN(parsed) ? null : Math.round(parsed);
}

export async function POST(req: NextRequest) {
  try {
    const body = await req.json().catch(() => ({}));
    const { searchParams } = new URL(req.url);
    const hotelId = searchParams.get('hotelId') || body.hotelId;

    if (!hotelId) {
      return NextResponse.json({ success: false, error: 'hotelId fehlt' }, { status: 400 });
    }

    const numericPrice = parseGermanPrice(body.price);

// Prüfen, ob das Genius-Badge vom Scraper erkannt wurde (z.B. body.hasGenius oder body.isGenius)
const hasGeniusBadge = Boolean(body.hasGenius || body.isGenius || body.genius);

// Wenn Genius vorhanden ist: 10% abziehen und runden
const geniusPriceNumeric = (numericPrice !== null && hasGeniusBadge)
  ? Math.round(numericPrice * 0.9)
  : null;

if (numericPrice !== null && numericPrice > 0) {
  try {
    await query(
      `INSERT INTO hotel_price_history (hotel_id, price_numeric, genius_price_numeric, price_raw, currency, nights, persons, tracked_at)
       VALUES ($1::uuid, $2, $3, $4, 'EUR', $5, $6, NOW())`,
      [hotelId, numericPrice, geniusPriceNumeric, String(body.price || ''), body.nights || 1, body.persons || 2]
    );
  } catch (err: any) {
    console.error('DB Insert Error (hotel_price_history):', err.message);
  }
}

    // 2. Stammdaten aktualisieren
    try {
      await query(
        `UPDATE hotels
         SET name = COALESCE($1, name),
             rating = COALESCE($2, rating),
             review_count = COALESCE($3, review_count),
             address = COALESCE($4, address),
             image_url = COALESCE($5, image_url),
             room_size_m2 = COALESCE($6, room_size_m2),
             updated_at = NOW()
         WHERE id = $7::uuid`,
        [
          body.hotelName || null,
          body.rating || null,
          body.reviewCount || null,
          body.address || null,
          body.image || null,
          body.roomSizeM2 || null,
          hotelId
        ]
      );
    } catch (err: any) {
      console.error('DB Update Error (hotels):', err.message);
    }

    return NextResponse.json({ success: true, hotelId, parsedPrice: numericPrice });
  } catch (error: any) {
    console.error('Allgemeiner Fehler im Webhook:', error.message);
    return NextResponse.json({ success: false, error: error.message }, { status: 200 });
  }
}

```

--- DATEI: src/app/layout.tsx ---
```
import type { Metadata } from 'next';

export const metadata: Metadata = {
  title: 'Reiseplaner App',
  description: 'Hotelpreise und Reisen tracken',
};

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="de">
      <body style={{ margin: 0, padding: 0, backgroundColor: '#fafafa' }}>
        {children}
      </body>
    </html>
  );
}

```

--- DATEI: src/app/page.tsx ---
```
'use client';

import { useState, useEffect } from 'react';

// ==========================================
// TYPEN & INTERFACES
// ==========================================
interface Trip {
  id: string;
  name: string;
}

interface TripMember {
  id: string;
  trip_id: string;
  name: string;
  color: string;
  created_at?: string;
}

interface RatingEntry {
  member_id: string;
  name: string;
  color: string;
  grade: number;
  comment?: string | null;
}

interface PriceEntry {
  id: string;
  price: number;
  price_per_night?: number | null;
  price_per_person?: number | null;
  nights: number;
  persons: number;
  created_at: string;
}

interface AttractionAssignment {
  hotel_id: string;
  distance_km?: number | null;
  duration_minutes?: number | null;
}

interface Attraction {
  id: string;
  trip_id?: string | null;
  name: string;
  address: string;
  category_icon: string;
  notes?: string | null;
  assignments?: AttractionAssignment[];
}

interface Hotel {
  id: string;
  name: string;
  booking_url: string;
  is_active: boolean;
  trip_id?: string | null;
  trip_name?: string | null;
  latest_price?: number | null;
  latest_price_per_night?: number | null;
  latest_price_per_person?: number | null;
  latest_genius_price?: number | null;
  latest_nights?: number | null;
  latest_persons?: number | null;
  best_price?: number | null;
  best_price_per_night?: number | null;
  avg_grade?: number | null;
  total_votes?: number | null;
  ratings_json?: RatingEntry[];
  rating?: number | null;
  review_count?: number | null;
  image_url?: string | null;
  address?: string | null;
  room_size_m2?: number | null;
  review_summary_positive?: string | null;
  review_summary_passable?: string | null;
  review_summary_negative?: string | null;
  review_summary_bathroom?: string | null;
  review_summary_count?: number | null;
  reviews_updated_at?: string | null;
  checkin_date?: string | null;
  checkout_date?: string | null;
}

interface HotelCluster {
  clusterKey: string;
  checkin_date?: string | null;
  checkout_date?: string | null;
  hotels: Hotel[];
}

export default function TravelDashboard() {
  // ==========================================
  // STATES
  // ==========================================
  const [trips, setTrips] = useState<Trip[]>([]);
  const [activeTripId, setActiveTripId] = useState<string>('ALL');
  const [tripMembers, setTripMembers] = useState<TripMember[]>([]);
  const [hotels, setHotels] = useState<Hotel[]>([]);
  const [attractions, setAttractions] = useState<Attraction[]>([]);
  const [loading, setLoading] = useState(true);
  const [scanningAllPrices, setScanningAllPrices] = useState(false);
  const [scanningHotelId, setScanningHotelId] = useState<string | null>(null);

  // Preishistorie Modal
  const [selectedPriceHotel, setSelectedPriceHotel] = useState<Hotel | null>(null);
  const [priceHistory, setPriceHistory] = useState<PriceEntry[]>([]);
  const [loadingPrices, setLoadingPrices] = useState(false);

  // Reise & Hotel Modals
  const [showAddTripModal, setShowAddTripModal] = useState(false);
  const [newTripName, setNewTripName] = useState('');
  const [showAddHotelModal, setShowAddHotelModal] = useState(false);
  const [newBookingUrl, setNewBookingUrl] = useState('');
  const [newHotelName, setNewHotelName] = useState('');
  const [isSubmitting, setIsSubmitting] = useState(false);
  const [showParkedModal, setShowParkedModal] = useState(false);

  // Mitreisende Modal
  const [showMembersModal, setShowMembersModal] = useState(false);
  const [newMemberName, setNewMemberName] = useState('');
  const [isAddingMember, setIsAddingMember] = useState(false);

  // Hotel-Bewertungs-Modal (Schulnoten 1 bis 6)
  const [ratingHotel, setRatingHotel] = useState<Hotel | null>(null);
  const [selectedMemberId, setSelectedMemberId] = useState<string>('');
  const [selectedGrade, setSelectedGrade] = useState<number | null>(null);
  const [ratingComment, setRatingComment] = useState('');
  const [isSubmittingRating, setIsSubmittingRating] = useState(false);

  // Ausflugsziele Modals
  const [showManageSightsModal, setShowManageSightsModal] = useState(false);
  const [showSightFormModal, setShowSightFormModal] = useState(false);
  const [editingSight, setEditingSight] = useState<Attraction | null>(null);
  const [sightName, setSightName] = useState('');
  const [sightAddress, setSightAddress] = useState('');
  const [sightIcon, setSightIcon] = useState('🏛️');
  const [sightNotes, setSightNotes] = useState('');
  const [selectedHotelIds, setSelectedHotelIds] = useState<string[]>([]);
  const [isSubmittingSight, setIsSubmittingSight] = useState(false);

  // ==========================================
  // EFFEKTE & DATEN LADEN
  // ==========================================
  useEffect(() => {
    loadData();
  }, []);

  useEffect(() => {
    if (activeTripId !== 'ALL') {
      loadMembers(activeTripId);
    } else {
      setTripMembers([]);
    }
  }, [activeTripId, trips]);

  const formatDateGerman = (dateStr?: string | null) => {
    if (!dateStr) return '';
    const date = new Date(dateStr);
    return isNaN(date.getTime())
      ? dateStr
      : date.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' });
  };

  const calculateNights = (checkin?: string | null, checkout?: string | null) => {
    if (!checkin || !checkout) return null;
    const start = new Date(checkin).getTime();
    const end = new Date(checkout).getTime();
    if (isNaN(start) || isNaN(end)) return null;
    const diffDays = Math.round((end - start) / (1000 * 60 * 60 * 24));
    return diffDays > 0 ? diffDays : null;
  };

  const loadData = async () => {
    setLoading(true);
    try {
      try {
        const resTrips = await fetch('/api/trips');
        if (resTrips.ok) {
          const dataTrips = await resTrips.json();
          setTrips(dataTrips.trips || []);
        }
      } catch (e) { }

      try {
        const resHotels = await fetch('/api/hotels');
        if (resHotels.ok) {
          const dataHotels = await resHotels.json();
          setHotels(dataHotels.hotels || []);
        }
      } catch (e) { }

      try {
        const resAttractions = await fetch('/api/attractions');
        if (resAttractions.ok) {
          const dataAttractions = await resAttractions.json();
          setAttractions(dataAttractions.attractions || []);
        }
      } catch (e) { }
    } finally {
      setLoading(false);
    }
  };

  const loadMembers = async (tripId: string) => {
    try {
      const res = await fetch(`/api/trips/${tripId}/members`);
      if (res.ok) {
        const data = await res.json();
        setTripMembers(data.members || []);
      }
    } catch (e) {
      console.error('Fehler beim Laden der Mitreisenden:', e);
    }
  };

  const refreshAttractions = async () => {
    try {
      const res = await fetch('/api/attractions');
      const data = await res.json();
      if (data.success) {
        setAttractions(data.attractions || []);
      }
    } catch (e) {
      console.error('Fehler beim Aktualisieren der Ausflugsziele:', e);
    }
  };

  // ==========================================
  // HANDLERS: REISEN & MITREISENDE
  // ==========================================
  const handleAddTrip = async (e: React.FormEvent) => {
    e.preventDefault();
    if (!newTripName.trim()) return;
    try {
      const res = await fetch('/api/trips', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ name: newTripName.trim() })
      });
      const data = await res.json();
      if (data.success) {
        setTrips([...trips, data.trip]);
        setActiveTripId(String(data.trip.id));
        setNewTripName('');
        setShowAddTripModal(false);
      }
    } catch (e: any) {
      alert('Fehler: ' + e.message);
    }
  };

  const handleAddMember = async (e: React.FormEvent) => {
    e.preventDefault();
    if (!newMemberName.trim() || activeTripId === 'ALL') return;
    setIsAddingMember(true);
    try {
      const res = await fetch(`/api/trips/${activeTripId}/members`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ name: newMemberName.trim() })
      });
      const data = await res.json();
      if (data.success) {
        setTripMembers([...tripMembers, data.member]);
        setNewMemberName('');
        loadData();
      }
    } catch (err: any) {
      alert('Fehler beim Hinzufügen: ' + err.message);
    } finally {
      setIsAddingMember(false);
    }
  };

  const handleDeleteMember = async (memberId: string) => {
    if (!confirm('Mitreisenden wirklich entfernen? Alle zugehörigen Noten werden gelöscht.')) return;
    try {
      const res = await fetch(`/api/trips/${activeTripId}/members?memberId=${memberId}`, {
        method: 'DELETE'
      });
      if (res.ok) {
        setTripMembers(tripMembers.filter(m => m.id !== memberId));
        loadData();
      }
    } catch (err: any) {
      console.error('Fehler beim Löschen:', err);
    }
  };

  // ==========================================
  // HANDLERS: BEWERTUNG (1 bis 6)
  // ==========================================
  const openRatingModal = (hotel: Hotel) => {
    setRatingHotel(hotel);
    setSelectedMemberId(tripMembers[0]?.id || '');
    setSelectedGrade(null);
    setRatingComment('');

    if (tripMembers.length > 0 && hotel.ratings_json) {
      const existing = hotel.ratings_json.find(r => r.member_id === tripMembers[0].id);
      if (existing) {
        setSelectedGrade(existing.grade);
        setRatingComment(existing.comment || '');
      }
    }
  };

  const handleSelectMemberForRating = (mId: string) => {
    setSelectedMemberId(mId);
    if (ratingHotel?.ratings_json) {
      const existing = ratingHotel.ratings_json.find(r => r.member_id === mId);
      if (existing) {
        setSelectedGrade(existing.grade);
        setRatingComment(existing.comment || '');
      } else {
        setSelectedGrade(null);
        setRatingComment('');
      }
    }
  };

  const handleSaveRating = async (e: React.FormEvent) => {
    e.preventDefault();
    if (!ratingHotel || !selectedMemberId || !selectedGrade) {
      return alert('Bitte wähle Mitreisenden und Note aus!');
    }
    setIsSubmittingRating(true);
    try {
      const res = await fetch(`/api/hotels/${ratingHotel.id}/ratings`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          member_id: selectedMemberId,
          grade: selectedGrade,
          comment: ratingComment
        })
      });
      const data = await res.json();
      if (data.success) {
        await loadData();
        setRatingHotel(null);
      } else {
        alert('Fehler: ' + data.error);
      }
    } catch (err: any) {
      alert('Fehler beim Speichern: ' + err.message);
    } finally {
      setIsSubmittingRating(false);
    }
  };

  // ==========================================
  // HANDLERS: HOTELS, PREISE, AUSFLÜGE
  // ==========================================
  const openPriceHistoryModal = async (hotel: Hotel) => {
    setSelectedPriceHotel(hotel);
    setLoadingPrices(true);
    setPriceHistory([]);
    try {
      const res = await fetch(`/api/hotels/${hotel.id}/prices`);
      const data = await res.json();
      if (data.success) setPriceHistory(data.prices || []);
    } catch (e) {
      console.error('Fehler beim Laden der Preise:', e);
    } finally {
      setLoadingPrices(false);
    }
  };

  const handleAddHotel = async (e: React.FormEvent) => {
    e.preventDefault();
    if (!newBookingUrl) return alert('Bitte URL eingeben');
    setIsSubmitting(true);
    try {
      const targetTripId = activeTripId === 'ALL' ? (trips[0]?.id || null) : activeTripId;
      const res = await fetch('/api/hotels', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          name: newHotelName || 'Neues Hotel',
          booking_url: newBookingUrl,
          trip_id: targetTripId
        })
      });
      const data = await res.json();
      if (res.ok || data.success) {
        setNewBookingUrl('');
        setNewHotelName('');
        setShowAddHotelModal(false);
        loadData();
      }
    } catch (err: any) {
      alert('Fehler: ' + err.message);
    } finally {
      setIsSubmitting(false);
    }
  };

  const parkHotel = async (id: string) => {
    try {
      await fetch(`/api/hotels/${id}`, {
        method: 'PATCH',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ is_active: false })
      });
      setHotels(hotels.map(h => (h.id === id ? { ...h, is_active: false } : h)));
    } catch (e) {
      console.error('Fehler beim Ausmustern:', e);
    }
  };

  const restoreHotel = async (id: string) => {
    try {
      await fetch(`/api/hotels/${id}`, {
        method: 'PATCH',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ is_active: true })
      });
      setHotels(hotels.map(h => (h.id === id ? { ...h, is_active: true } : h)));
    } catch (e) {
      console.error('Fehler beim Wiederaufnehmen:', e);
    }
  };

  const openSightModal = (sight?: Attraction, preselectedHotelId?: string) => {
    if (sight) {
      setEditingSight(sight);
      setSightName(sight.name);
      setSightAddress(sight.address);
      setSightIcon(sight.category_icon || '🏛️');
      setSightNotes(sight.notes || '');
      setSelectedHotelIds((sight.assignments || []).map(a => a.hotel_id));
    } else {
      setEditingSight(null);
      setSightName('');
      setSightAddress('');
      setSightIcon('🏛️');
      setSightNotes('');
      setSelectedHotelIds(preselectedHotelId ? [preselectedHotelId] : []);
    }
    setShowSightFormModal(true);
  };

  const toggleHotelSelection = (hotelId: string) => {
    setSelectedHotelIds(prev =>
      prev.includes(hotelId) ? prev.filter(id => id !== hotelId) : [...prev, hotelId]
    );
  };

  const handleSaveSight = async (e: React.FormEvent) => {
    e.preventDefault();
    if (!sightAddress) return;
    setIsSubmittingSight(true);
    try {
      const targetTripId = activeTripId === 'ALL' ? (trips[0]?.id || null) : activeTripId;
      const payload = {
        name: sightName,
        address: sightAddress,
        category_icon: sightIcon,
        notes: sightNotes,
        hotel_ids: selectedHotelIds,
        trip_id: targetTripId
      };
      if (editingSight) {
        const res = await fetch(`/api/attractions/${editingSight.id}`, {
          method: 'PUT',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify(payload)
        });
        const data = await res.json();
        if (!data.success) throw new Error(data.error);
      } else {
        const res = await fetch('/api/attractions', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify(payload)
        });
        const data = await res.json();
        if (!data.success) throw new Error(data.error);
      }
      await refreshAttractions();
      setShowSightFormModal(false);
    } catch (err: any) {
      alert('Fehler beim Speichern: ' + err.message);
    } finally {
      setIsSubmittingSight(false);
    }
  };

  const deleteSight = async (sightId: string) => {
    if (!confirm('Sehenswürdigkeit wirklich löschen?')) return;
    try {
      await fetch(`/api/attractions/${sightId}`, { method: 'DELETE' });
      await refreshAttractions();
    } catch (e) {
      console.error('Fehler beim Löschen:', e);
    }
  };

  const triggerBatchPriceScan = async () => {
    setScanningAllPrices(true);
    try {
      const res = await fetch('/api/jobs/trigger-daily-check', { method: 'POST' });
      const data = await res.json();
      alert(data.success ? '📊 Preis-Scan gestartet!' : 'Fehler: ' + data.error);
      setTimeout(loadData, 3000);
    } finally {
      setScanningAllPrices(false);
    }
  };

  const scanSingleHotel = async (hotelId: string) => {
    setScanningHotelId(hotelId);
    try {
      const res = await fetch(`/api/hotels/${hotelId}/scan`, { method: 'POST' });
      const data = await res.json();
      alert(data.message || data.error);
    } finally {
      setScanningHotelId(null);
    }
  };

  // ==========================================
  // FILTER & CLUSTERING MIT AUTOMATISCHEM RANKING
  // ==========================================
  const currentHotels = hotels
    .filter(h => h.is_active)
    .filter(h => {
      if (activeTripId === 'ALL') return true;
      if (!h.trip_id) return true;
      return String(h.trip_id).toLowerCase() === String(activeTripId).toLowerCase();
    })
    .sort((a, b) => {
      if (!a.checkin_date && !b.checkin_date) return 0;
      if (!a.checkin_date) return 1;
      if (!b.checkin_date) return -1;
      return new Date(a.checkin_date).getTime() - new Date(b.checkin_date).getTime();
    });

  const parkedHotels = hotels.filter(h => !h.is_active);
  const currentAttractions = attractions.filter(a => {
    if (activeTripId === 'ALL') return true;
    return String(a.trip_id).toLowerCase() === String(activeTripId).toLowerCase();
  });

  const clusteredStops = currentHotels.reduce<HotelCluster[]>((acc, hotel) => {
    const checkin = hotel.checkin_date ? hotel.checkin_date.split('T')[0] : null;
    const checkout = hotel.checkout_date ? hotel.checkout_date.split('T')[0] : null;
    if (!checkin) {
      acc.push({ clusterKey: `single_${hotel.id}`, checkin_date: null, checkout_date: null, hotels: [hotel] });
      return acc;
    }
    const key = `${checkin}_${checkout || 'NO_CHECKOUT'}`;
    const existingCluster = acc.find(c => c.clusterKey === key);
    if (existingCluster) {
      existingCluster.hotels.push(hotel);
    } else {
      acc.push({ clusterKey: key, checkin_date: checkin, checkout_date: checkout, hotels: [hotel] });
    }
    return acc;
  }, []);

  const pricesList = priceHistory.map(p => Number(p.price));
  const minPrice = pricesList.length > 0 ? Math.min(...pricesList) : 0;
  const maxPrice = pricesList.length > 0 ? Math.max(...pricesList) : 0;
  const currentPrice = pricesList.length > 0 ? pricesList[pricesList.length - 1] : 0;

  const activeTripObj = trips.find(t => String(t.id) === String(activeTripId));

  return (
    <div style={{ maxWidth: '1100px', margin: '0 auto', padding: '2rem 1rem', fontFamily: 'system-ui, -apple-system, sans-serif', color: '#1f2937' }}>

      {/* HEADER */}
      <header style={{ marginBottom: '2rem', textAlign: 'center' }}>
        <h1 style={{ fontSize: '2.2rem', marginBottom: '0.5rem', fontWeight: '800', color: '#0f172a' }}>
          📍 Reise-Ablauf & Freunde-Ranking
        </h1>
        <p style={{ color: '#64748b', margin: 0, fontSize: '0.95rem' }}>
          Hotelvergleich nach Schulnoten (1–6), Bestpreise, Routen & Fahrzeiten
        </p>

        <div style={{ display: 'flex', gap: '0.8rem', justifyContent: 'center', marginTop: '1.5rem', flexWrap: 'wrap' }}>
          <button
            onClick={() => setShowAddHotelModal(true)}
            style={{ background: '#10b981', color: '#fff', border: 'none', padding: '0.65rem 1.2rem', borderRadius: '8px', fontWeight: '700', cursor: 'pointer' }}
          >
            ➕ Neues Hotel hinzufügen
          </button>
          <button
            onClick={() => openSightModal()}
            style={{ background: '#2563eb', color: '#fff', border: 'none', padding: '0.65rem 1.2rem', borderRadius: '8px', fontWeight: '700', cursor: 'pointer' }}
          >
            🏛️ Ausflug anlegen
          </button>
          <button
            onClick={() => setShowManageSightsModal(true)}
            style={{ background: '#f1f5f9', color: '#334155', border: '1px solid #cbd5e1', padding: '0.65rem 1.2rem', borderRadius: '8px', fontWeight: '700', cursor: 'pointer', display: 'flex', alignItems: 'center', gap: '0.4rem' }}
          >
            📋 Alle Ausflüge ({currentAttractions.length})
          </button>
          <button
            onClick={() => setShowParkedModal(true)}
            style={{ background: '#f1f5f9', color: '#334155', border: '1px solid #cbd5e1', padding: '0.65rem 1.2rem', borderRadius: '8px', fontWeight: '700', cursor: 'pointer', display: 'flex', alignItems: 'center', gap: '0.4rem' }}
          >
            📦 Ausgemustert
            {parkedHotels.length > 0 && (
              <span style={{ background: '#dc2626', color: '#fff', fontSize: '0.75rem', padding: '0.15rem 0.5rem', borderRadius: '9999px', fontWeight: '800' }}>
                {parkedHotels.length}
              </span>
            )}
          </button>
          <button
            onClick={triggerBatchPriceScan}
            disabled={scanningAllPrices}
            style={{ background: '#3b82f6', color: '#fff', border: 'none', padding: '0.65rem 1.2rem', borderRadius: '8px', fontWeight: '700', cursor: 'pointer', opacity: scanningAllPrices ? 0.7 : 1 }}
          >
            {scanningAllPrices ? '🔄 Scanne...' : '📊 Preise aktualisieren'}
          </button>
        </div>
      </header>

      {/* REISE TABS */}
      <div style={{ display: 'flex', gap: '0.5rem', borderBottom: '2px solid #e2e8f0', marginBottom: '1.5rem', overflowX: 'auto', paddingBottom: '0.5rem' }}>
        <button
          onClick={() => setActiveTripId('ALL')}
          style={{
            padding: '0.6rem 1.2rem',
            borderRadius: '8px',
            border: 'none',
            background: activeTripId === 'ALL' ? '#2563eb' : '#f1f5f9',
            color: activeTripId === 'ALL' ? '#ffffff' : '#475569',
            fontWeight: 'bold',
            cursor: 'pointer',
            whiteSpace: 'nowrap'
          }}
        >
          🌐 Alle Hotels ({currentHotels.length})
        </button>
        {trips.map(trip => (
          <button
            key={trip.id}
            onClick={() => setActiveTripId(String(trip.id))}
            style={{
              padding: '0.6rem 1.2rem',
              borderRadius: '8px',
              border: 'none',
              background: String(activeTripId).toLowerCase() === String(trip.id).toLowerCase() ? '#2563eb' : '#f1f5f9',
              color: String(activeTripId).toLowerCase() === String(trip.id).toLowerCase() ? '#ffffff' : '#475569',
              fontWeight: 'bold',
              cursor: 'pointer',
              whiteSpace: 'nowrap'
            }}
          >
            🌍 {trip.name}
          </button>
        ))}
        <button
          onClick={() => setShowAddTripModal(true)}
          style={{ padding: '0.6rem 1rem', borderRadius: '8px', border: '1px dashed #2563eb', background: '#eff6ff', color: '#2563eb', fontWeight: 'bold', cursor: 'pointer' }}
        >
          ➕ Neue Reise
        </button>
      </div>

      {/* MITREISENDE SECTION */}
      {activeTripId !== 'ALL' && activeTripObj && (
        <div style={{
          background: '#ffffff',
          border: '1px solid #e2e8f0',
          borderRadius: '12px',
          padding: '0.85rem 1.2rem',
          marginBottom: '2rem',
          display: 'flex',
          justifyContent: 'space-between',
          alignItems: 'center',
          flexWrap: 'wrap',
          gap: '1rem',
          boxShadow: '0 2px 4px rgba(0,0,0,0.02)'
        }}>
          <div style={{ display: 'flex', alignItems: 'center', gap: '0.6rem', flexWrap: 'wrap' }}>
            <span style={{ fontWeight: '800', fontSize: '0.95rem', color: '#1e293b' }}>
              👥 Mitreisende ({tripMembers.length}):
            </span>
            {tripMembers.length === 0 ? (
              <span style={{ fontSize: '0.82rem', color: '#94a3b8', fontStyle: 'italic' }}>
                Noch keine Freunde hinterlegt
              </span>
            ) : (
              <div style={{ display: 'flex', gap: '0.4rem', flexWrap: 'wrap' }}>
                {tripMembers.map(m => (
                  <span
                    key={m.id}
                    style={{
                      background: '#f8fafc',
                      border: '1px solid #cbd5e1',
                      padding: '0.2rem 0.6rem',
                      borderRadius: '9999px',
                      fontSize: '0.8rem',
                      fontWeight: '700',
                      color: '#334155',
                      display: 'inline-flex',
                      alignItems: 'center',
                      gap: '0.35rem'
                    }}
                  >
                    <span style={{ width: '16px', height: '16px', borderRadius: '50%', background: m.color || '#2563eb', color: '#fff', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', fontSize: '0.65rem', fontWeight: '800' }}>
                      {m.name[0]}
                    </span>
                    {m.name}
                  </span>
                ))}
              </div>
            )}
          </div>

          <button
            onClick={() => setShowMembersModal(true)}
            style={{
              background: '#eff6ff',
              color: '#2563eb',
              border: '1px dashed #93c5fd',
              padding: '0.4rem 0.8rem',
              borderRadius: '6px',
              fontWeight: '700',
              fontSize: '0.8rem',
              cursor: 'pointer'
            }}
          >
            ⚙️ Mitreisende verwalten
          </button>
        </div>
      )}

      {/* TIMELINE MIT AUTOMATISCHEM RANKING */}
      {loading ? (
        <p style={{ textAlign: 'center', color: '#64748b', padding: '3rem 0' }}>Lade Daten...</p>
      ) : clusteredStops.length === 0 ? (
        <div style={{ textAlign: 'center', padding: '3rem', background: '#f9fafb', borderRadius: '12px', border: '1px dashed #d1d5db' }}>
          <p style={{ color: '#6b7280', marginBottom: '1rem' }}>Keine aktiven Hotels für diese Auswahl gefunden.</p>
          <button onClick={() => setShowAddHotelModal(true)} style={{ background: '#10b981', color: '#fff', border: 'none', padding: '0.6rem 1.2rem', borderRadius: '6px', cursor: 'pointer', fontWeight: 'bold' }}>
            Erstes Hotel hinzufügen
          </button>
        </div>
      ) : (
        <div style={{ position: 'relative', paddingLeft: '35px', borderLeft: '4px solid #3b82f6', marginLeft: '15px' }}>
          {clusteredStops.map((stop, stopIndex) => {
            const isCluster = stop.hotels.length > 1;
            const nights = calculateNights(stop.checkin_date, stop.checkout_date) || stop.hotels[0]?.latest_nights || 1;

            const sortedHotels = [...stop.hotels].sort((a, b) => {
              const gradeA = a.avg_grade !== null && a.avg_grade !== undefined ? Number(a.avg_grade) : 99;
              const gradeB = b.avg_grade !== null && b.avg_grade !== undefined ? Number(b.avg_grade) : 99;
              if (gradeA !== gradeB) return gradeA - gradeB;
              return (a.latest_price || 99999) - (b.latest_price || 99999);
            });

            return (
              <div key={stop.clusterKey} style={{ position: 'relative', marginBottom: '3.5rem' }}>
                <div style={{
                  position: 'absolute',
                  left: '-47px',
                  top: '2px',
                  width: '20px',
                  height: '20px',
                  borderRadius: '50%',
                  background: isCluster ? '#7c3aed' : '#3b82f6',
                  border: '4px solid #ffffff',
                  boxShadow: `0 0 0 2px ${isCluster ? '#7c3aed' : '#3b82f6'}`
                }} />

                {/* Stop Header */}
                <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '0.9rem', flexWrap: 'wrap', gap: '0.5rem' }}>
                  <div style={{ fontSize: '0.95rem', fontWeight: 'bold', display: 'flex', alignItems: 'center', gap: '0.6rem', flexWrap: 'wrap' }}>
                    <span style={{ color: isCluster ? '#6d28d9' : '#2563eb' }}>📍 Stop #{stopIndex + 1}</span>
                    {stop.checkin_date ? (
                      <span style={{ background: isCluster ? '#faf5ff' : '#dbeafe', color: isCluster ? '#6b21a8' : '#1e40af', padding: '0.25rem 0.7rem', borderRadius: '9999px', fontSize: '0.82rem', border: isCluster ? '1px solid #e9d5ff' : 'none', fontWeight: '600' }}>
                        🗓️ {formatDateGerman(stop.checkin_date)} – {formatDateGerman(stop.checkout_date) || '?'} ({nights} {nights === 1 ? 'Nacht' : 'Nächte'})
                      </span>
                    ) : (
                      <span style={{ color: '#9ca3af', fontSize: '0.8rem', fontStyle: 'italic' }}>(Warte auf Scan für Datum)</span>
                    )}
                    {isCluster && (
                      <span style={{ background: '#f5f3ff', color: '#7c3aed', border: '1px solid #ddd6fe', padding: '0.25rem 0.7rem', borderRadius: '9999px', fontSize: '0.8rem', fontWeight: '700' }}>
                        ⚖️ {stop.hotels.length} Optionen im Freunde-Ranking
                      </span>
                    )}
                  </div>
                </div>

                {/* Hotel Grid */}
                <div style={{
                  display: 'grid',
                  gridTemplateColumns: isCluster ? 'repeat(auto-fit, minmax(340px, 1fr))' : '1fr',
                  gap: '1.3rem',
                  background: isCluster ? '#fdfcff' : 'transparent',
                  padding: isCluster ? '1.2rem' : '0',
                  borderRadius: '16px',
                  border: isCluster ? '2px dashed #c4b5fd' : 'none'
                }}>
                  {sortedHotels.map((hotel, hIdx) => {
                    const hotelNights = calculateNights(hotel.checkin_date, hotel.checkout_date) || hotel.latest_nights || nights || 1;
                    const persons = hotel.latest_persons || 2;
                    const totalPrice = hotel.latest_price !== null && hotel.latest_price !== undefined ? Number(hotel.latest_price) : null;
                    const pricePerNight = hotel.latest_price_per_night
                      ? Math.round(Number(hotel.latest_price_per_night))
                      : (totalPrice ? Math.round(totalPrice / hotelNights) : null);
                    const pricePerPersonPerNight = hotel.latest_price_per_person
                      ? Math.round(Number(hotel.latest_price_per_person))
                      : (totalPrice ? Math.round(totalPrice / (hotelNights * persons)) : null);

                    const hasGrade = hotel.avg_grade !== null && hotel.avg_grade !== undefined;
                    const rankBadgeClass = hIdx === 0 && hasGrade ? '🥇 Rang #1 · Favorit' : hIdx === 1 && hasGrade ? '🥈 Rang #2' : `🥉 Rang #${hIdx + 1}`;
                    const rankTopBorder = hIdx === 0 && hasGrade ? '#f59e0b' : hIdx === 1 && hasGrade ? '#94a3b8' : '#cbd5e1';

                    let gradeColor = '#15803d';
                    let gradeBg = '#f0fdf4';
                    let gradeBorder = '#bbf7d0';
                    let gradeDesc = 'Sehr gut';

                    if (hasGrade) {
                      const g = Number(hotel.avg_grade);
                      if (g > 1.5 && g <= 2.5) { gradeColor = '#059669'; gradeBg = '#ecfdf5'; gradeBorder = '#a7f3d0'; gradeDesc = 'Gut'; }
                      else if (g > 2.5 && g <= 3.5) { gradeColor = '#d97706'; gradeBg = '#fffbeb'; gradeBorder = '#fde68a'; gradeDesc = 'Befriedigend'; }
                      else if (g > 3.5 && g <= 4.5) { gradeColor = '#ea580c'; gradeBg = '#fff7ed'; gradeBorder = '#fed7aa'; gradeDesc = 'Ausreichend'; }
                      else if (g > 4.5) { gradeColor = '#dc2626'; gradeBg = '#fef2f2'; gradeBorder = '#fecaca'; gradeDesc = 'Mangelhaft'; }
                    }

                    const hotelSights = attractions.filter(a =>
                      (a.assignments || []).some(asgn => asgn.hotel_id === hotel.id)
                    );

                    return (
                      <div
                        key={hotel.id}
                        style={{
                          background: '#ffffff',
                          border: '1px solid #e2e8f0',
                          borderTop: isCluster ? `4px solid ${rankTopBorder}` : '1px solid #e2e8f0',
                          borderRadius: '14px',
                          padding: '1.2rem',
                          boxShadow: '0 4px 6px -1px rgba(0,0,0,0.05)',
                          opacity: hotel.is_active ? 1 : 0.5,
                          display: 'flex',
                          flexDirection: 'column',
                          justifyContent: 'space-between'
                        }}
                      >
                        <div>
                          {/* Option / Ranking Header */}
                          <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '0.65rem' }}>
                            {isCluster ? (
                              <span style={{
                                fontSize: '0.78rem',
                                fontWeight: '800',
                                textTransform: 'uppercase',
                                background: hIdx === 0 && hasGrade ? '#fef3c7' : '#f1f5f9',
                                color: hIdx === 0 && hasGrade ? '#92400e' : '#475569',
                                padding: '0.2rem 0.55rem',
                                borderRadius: '6px',
                                border: hIdx === 0 && hasGrade ? '1px solid #fde68a' : '1px solid #e2e8f0'
                              }}>
                                {hasGrade ? rankBadgeClass : `Option ${String.fromCharCode(65 + hIdx)}`}
                              </span>
                            ) : (
                              <div>{hotel.trip_name && <span style={{ background: '#f3f4f6', color: '#4b5563', padding: '0.2rem 0.5rem', borderRadius: '6px', fontSize: '0.75rem' }}>{hotel.trip_name}</span>}</div>
                            )}
                            <button
                              onClick={() => parkHotel(hotel.id)}
                              title="Dieses Hotel aus der Timeline nehmen"
                              style={{ background: 'transparent', border: '1px solid #cbd5e1', color: '#64748b', padding: '0.25rem 0.55rem', borderRadius: '6px', fontSize: '0.75rem', fontWeight: '600', cursor: 'pointer' }}
                            >
                              👁️‍🗨️ Nicht betrachten
                            </button>
                          </div>

                          {/* Hotel Main Body */}
                          <div style={{ display: 'flex', gap: '0.9rem', marginBottom: '0.8rem' }}>
                            {hotel.image_url && (
                              <img src={hotel.image_url} alt={hotel.name} style={{ width: '100px', height: '80px', objectFit: 'cover', borderRadius: '8px', flexShrink: 0 }} />
                            )}
                            <div style={{ flex: 1, minWidth: '160px' }}>
                              <h3 style={{ margin: '0 0 0.25rem 0', fontSize: '1.05rem', color: '#0f172a', lineHeight: 1.3 }}>{hotel.name}</h3>
                              <p style={{ margin: '0 0 0.3rem 0', fontSize: '0.78rem', color: '#64748b' }}>📍 {hotel.address || 'Keine Adresse'}</p>
                              <div style={{ fontSize: '0.75rem', color: '#475569' }}>
                                ⭐ <strong>{hotel.rating || 'N/A'}</strong>/10 · {hotel.review_count || 0} Bew.
                              </div>
                            </div>
                          </div>

                          {/* NOTENBOX: FREUNDE BEWERTUNG */}
                          <div style={{
                            background: gradeBg,
                            border: `1px solid ${gradeBorder}`,
                            borderLeft: `5px solid ${gradeColor}`,
                            borderRadius: '10px',
                            padding: '0.65rem 0.85rem',
                            marginBottom: '0.75rem',
                            display: 'flex',
                            justifyContent: 'space-between',
                            alignItems: 'center'
                          }}>
                            <div style={{ display: 'flex', alignItems: 'center', gap: '0.6rem' }}>
                              <div style={{ background: gradeColor, color: '#fff', fontSize: '1.2rem', fontWeight: '900', padding: '0.2rem 0.5rem', borderRadius: '6px', lineHeight: 1 }}>
                                {hasGrade ? Number(hotel.avg_grade).toFixed(1) : '–'}
                              </div>
                              <div style={{ display: 'flex', flexDirection: 'column' }}>
                                <span style={{ fontSize: '0.8rem', fontWeight: '800', color: '#0f172a' }}>
                                  {hasGrade ? `${gradeDesc} (⌀ Note)` : 'Noch keine Note'}
                                </span>
                                <span style={{ fontSize: '0.72rem', color: '#64748b' }}>
                                  {hotel.total_votes || 0} von {tripMembers.length || 0} Stimmen
                                </span>
                              </div>
                            </div>

                            <button
                              onClick={() => openRatingModal(hotel)}
                              style={{ background: '#2563eb', color: '#fff', border: 'none', padding: '0.35rem 0.75rem', borderRadius: '6px', fontSize: '0.78rem', fontWeight: '700', cursor: 'pointer' }}
                            >
                              ✍️ Bewerten
                            </button>
                          </div>

                          {/* MITREISENDE ABSTIMMUNGS-STATUS */}
                          {tripMembers.length > 0 && (
                            <div style={{ background: '#f8fafc', border: '1px solid #e2e8f0', borderRadius: '8px', padding: '0.5rem 0.7rem', marginBottom: '0.75rem' }}>
                              <div style={{ fontSize: '0.7rem', fontWeight: '700', color: '#64748b', marginBottom: '0.3rem', display: 'flex', justifyContent: 'space-between' }}>
                                <span>STIMMEN DER FREUNDE:</span>
                                <span>{hotel.ratings_json?.length || 0}/{tripMembers.length} abgegeben</span>
                              </div>
                              <div style={{ display: 'flex', flexWrap: 'wrap', gap: '0.3rem' }}>
                                {tripMembers.map(m => {
                                  const vote = hotel.ratings_json?.find(r => r.member_id === m.id);
                                  return (
                                    <span
                                      key={m.id}
                                      style={{
                                        fontSize: '0.72rem',
                                        padding: '0.1rem 0.4rem',
                                        borderRadius: '4px',
                                        fontWeight: '600',
                                        background: vote ? '#ecfdf5' : '#f1f5f9',
                                        color: vote ? '#065f46' : '#94a3b8',
                                        border: vote ? '1px solid #a7f3d0' : '1px dashed #cbd5e1'
                                      }}
                                    >
                                      {vote ? `✓ ${m.name}: ${Number(vote.grade).toFixed(1)}` : `⏳ ${m.name}`}
                                    </span>
                                  );
                                })}
                              </div>
                            </div>
                          )}

                          {/* MINI-KOMMENTARE VORSCHAU */}
                          {hotel.ratings_json && hotel.ratings_json.filter(r => r.comment).length > 0 && (
                            <div style={{ background: '#fafafa', borderTop: '1px dashed #e2e8f0', paddingTop: '0.4rem', marginBottom: '0.75rem', fontSize: '0.75rem' }}>
                              {hotel.ratings_json.filter(r => r.comment).slice(0, 2).map((r, cIdx) => (
                                <div key={cIdx} style={{ display: 'flex', gap: '0.35rem', marginBottom: '0.2rem', alignItems: 'baseline' }}>
                                  <span style={{ fontWeight: '700', color: '#334155' }}>{r.name}:</span>
                                  <span style={{ color: '#64748b', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
                                    „{r.comment}“
                                  </span>
                                </div>
                              ))}
                            </div>
                          )}

                          {/* PREIS-BOX MIT BESTPREIS-VERGLEICH & GENIUS */}
                          {totalPrice !== null ? (
                            <div style={{ background: '#f8fafc', border: '1px solid #e2e8f0', borderRadius: '10px', padding: '0.6rem 0.8rem', marginBottom: '0.8rem', display: 'flex', flexDirection: 'column', gap: '0.35rem' }}>

                              {hotel.latest_genius_price ? (
                                <>
                                  {/* Durchgestrichener Normalpreis bei aktivem Genius */}
                                  <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', fontSize: '0.76rem', color: '#64748b' }}>
                                    <span>Regulär: <span style={{ textDecoration: 'line-through' }}>{totalPrice.toLocaleString('de-DE')} €</span></span>
                                    <span style={{ background: '#16a34a', color: '#fff', fontSize: '0.68rem', fontWeight: '800', padding: '0.1rem 0.35rem', borderRadius: '4px' }}>-10% GENIUS</span>
                                  </div>

                                  {/* Aktive Genius-Zeile mit Tiefstpreis-Badge */}
                                  <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexWrap: 'wrap', gap: '0.3rem' }}>
                                    <div style={{ display: 'flex', alignItems: 'center', gap: '0.35rem', flexWrap: 'wrap' }}>
                                      <span style={{ fontSize: '0.78rem', fontWeight: '700', color: '#003580' }}>Genius:</span>
                                      <span style={{ fontWeight: '900', color: '#003580', fontSize: '0.95rem' }}>
                                        💙 {Number(hotel.latest_genius_price).toLocaleString('de-DE')} €
                                      </span>
                                      <span style={{ color: '#0052b4', fontSize: '0.76rem' }}>
                                        (🌙 {Math.round(Number(hotel.latest_genius_price) / hotelNights).toLocaleString('de-DE')} € / N.)
                                      </span>
                                    </div>

                                    {hotel.best_price && Number(hotel.latest_genius_price) > Number(hotel.best_price) ? (
                                      <span style={{ background: '#fee2e2', color: '#991b1b', border: '1px solid #fecaca', fontSize: '0.7rem', padding: '0.1rem 0.35rem', borderRadius: '4px', fontWeight: '700' }}>
                                        ▲ +{(Number(hotel.latest_genius_price) - Number(hotel.best_price)).toLocaleString('de-DE')} €
                                      </span>
                                    ) : (
                                      <span style={{ background: '#ecfdf5', color: '#047857', border: '1px solid #a7f3d0', fontSize: '0.7rem', padding: '0.1rem 0.35rem', borderRadius: '4px', fontWeight: '800' }}>
                                        🔥 Tiefstpreis
                                      </span>
                                    )}
                                  </div>
                                </>
                              ) : (
                                /* Standard-Preiszeile mit Tiefstpreis-Badge */
                                <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexWrap: 'wrap', gap: '0.3rem' }}>
                                  <div style={{ display: 'flex', alignItems: 'center', gap: '0.35rem', flexWrap: 'wrap' }}>
                                    <span style={{ fontSize: '0.78rem', fontWeight: '700', color: '#475569' }}>Aktuell:</span>
                                    <span style={{ fontWeight: '800', color: '#0f172a', fontSize: '0.92rem' }}>
                                      💰 {totalPrice.toLocaleString('de-DE')} €
                                    </span>
                                    <span style={{ color: '#64748b', fontSize: '0.76rem' }}>
                                      (🌙 {pricePerNight?.toLocaleString('de-DE')} € / N. · 👤 {pricePerPersonPerNight?.toLocaleString('de-DE')} € / P.)
                                    </span>
                                  </div>

                                  {hotel.best_price && totalPrice > Number(hotel.best_price) ? (
                                    <span style={{ background: '#fee2e2', color: '#991b1b', border: '1px solid #fecaca', fontSize: '0.7rem', padding: '0.1rem 0.35rem', borderRadius: '4px', fontWeight: '700' }}>
                                      ▲ +{(totalPrice - Number(hotel.best_price)).toLocaleString('de-DE')} €
                                    </span>
                                  ) : (
                                    <span style={{ background: '#ecfdf5', color: '#047857', border: '1px solid #a7f3d0', fontSize: '0.7rem', padding: '0.1rem 0.35rem', borderRadius: '4px', fontWeight: '800' }}>
                                      🔥 Tiefstpreis
                                    </span>
                                  )}
                                </div>
                              )}

                              {/* Allzeit-Bestpreis Zeile */}
                              {hotel.best_price && (
                                <div style={{ display: 'flex', alignItems: 'center', gap: '0.4rem', borderTop: '1px dashed #e2e8f0', paddingTop: '0.3rem', fontSize: '0.75rem' }}>
                                  <span style={{ fontWeight: '700', color: '#059669' }}>🏆 Bestpreis:</span>
                                  <span style={{ fontWeight: '800', color: '#047857' }}>
                                    {Number(hotel.best_price).toLocaleString('de-DE')} €
                                  </span>
                                  {hotel.best_price_per_night && (
                                    <span style={{ color: '#64748b', fontSize: '0.72rem' }}>
                                      (🌙 {Math.round(Number(hotel.best_price_per_night)).toLocaleString('de-DE')} € / N.)
                                    </span>
                                  )}
                                </div>
                              )}
                            </div>
                          ) : (
                            <div style={{ marginBottom: '0.8rem', fontSize: '0.8rem', color: '#6b7280', fontStyle: 'italic' }}>
                              💰 Noch kein Preis erfasst
                            </div>
                          )}

                          {/* SEHENSWÜRDIGKEITEN & FAHRZEITEN */}
                          <div style={{ background: '#f8fafc', border: '1px solid #e2e8f0', borderRadius: '8px', padding: '0.7rem' }}>
                            <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '0.4rem' }}>
                              <span style={{ fontWeight: '700', fontSize: '0.8rem', color: '#334155' }}>
                                🏛️ Ausflugsziele ({hotelSights.length})
                              </span>
                              <button
                                onClick={() => openSightModal(undefined, hotel.id)}
                                style={{ background: '#eff6ff', color: '#2563eb', border: '1px dashed #bfdbfe', padding: '0.2rem 0.5rem', borderRadius: '4px', fontSize: '0.72rem', fontWeight: '700', cursor: 'pointer' }}
                              >
                                ➕ Zuordnen
                              </button>
                            </div>

                            <div style={{ display: 'flex', flexDirection: 'column', gap: '0.3rem' }}>
                              {hotelSights.length === 0 ? (
                                <span style={{ fontSize: '0.74rem', color: '#94a3b8', fontStyle: 'italic' }}>Keine Ausflugsziele zugeordnet.</span>
                              ) : (
                                hotelSights.map(sight => {
                                  const assignment = (sight.assignments || []).find(a => a.hotel_id === hotel.id);
                                  return (
                                    <div key={sight.id} style={{ background: '#ffffff', border: '1px solid #e2e8f0', borderRadius: '6px', padding: '0.4rem 0.6rem', display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: '0.4rem' }}>
                                      <div style={{ display: 'flex', alignItems: 'center', gap: '0.4rem', overflow: 'hidden' }}>
                                        <span>{sight.category_icon || '🏛️'}</span>
                                        <span style={{ fontWeight: '700', fontSize: '0.78rem', color: '#0f172a', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
                                          {sight.name}
                                        </span>
                                      </div>
                                      {assignment?.distance_km !== null && assignment?.distance_km !== undefined ? (
                                        <span style={{ background: '#ecfdf5', border: '1px solid #a7f3d0', color: '#047857', padding: '0.15rem 0.4rem', borderRadius: '4px', fontSize: '0.72rem', fontWeight: '700', whiteSpace: 'nowrap' }}>
                                          🚗 {assignment.distance_km} km · {assignment.duration_minutes} Min.
                                        </span>
                                      ) : null}
                                    </div>
                                  );
                                })
                              )}
                            </div>
                          </div>
                        </div>

                        {/* Card Actions Footer */}
                        <div style={{ display: 'flex', gap: '0.5rem', flexWrap: 'wrap', marginTop: '0.8rem', paddingTop: '0.6rem', borderTop: '1px solid #f1f5f9', alignItems: 'center' }}>
                          <button
                            onClick={() => openRatingModal(hotel)}
                            style={{ background: '#f1f5f9', border: '1px solid #cbd5e1', padding: '0.35rem 0.65rem', borderRadius: '6px', cursor: 'pointer', fontWeight: '700', fontSize: '0.75rem', color: '#334155' }}
                          >
                            💬 Stimmen ({hotel.ratings_json?.length || 0})
                          </button>
                          <button
                            onClick={() => openPriceHistoryModal(hotel)}
                            style={{ background: '#0284c7', color: '#ffffff', border: 'none', padding: '0.35rem 0.65rem', borderRadius: '6px', cursor: 'pointer', fontWeight: '600', fontSize: '0.75rem' }}
                          >
                            📈 Preishistorie
                          </button>
                          <button
                            onClick={() => scanSingleHotel(hotel.id)}
                            disabled={scanningHotelId === hotel.id || !hotel.is_active}
                            style={{ background: '#8b5cf6', color: '#ffffff', border: 'none', padding: '0.35rem 0.65rem', borderRadius: '6px', cursor: 'pointer', fontWeight: '600', fontSize: '0.75rem', opacity: (!hotel.is_active || scanningHotelId === hotel.id) ? 0.6 : 1 }}
                          >
                            {scanningHotelId === hotel.id ? '🔄 Scanne...' : '🤖 KI-Reviews'}
                          </button>
                          <a href={hotel.booking_url} target="_blank" rel="noopener noreferrer" style={{ color: '#2563eb', textDecoration: 'none', fontSize: '0.78rem', marginLeft: 'auto', fontWeight: '700' }}>
                            Booking ↗
                          </a>
                        </div>
                      </div>
                    );
                  })}
                </div>
              </div>
            );
          })}
        </div>
      )}

      {/* MODAL 1: STIMMABGABE */}
      {ratingHotel && (
        <div style={{ position: 'fixed', top: 0, left: 0, right: 0, bottom: 0, background: 'rgba(15, 23, 42, 0.6)', backdropFilter: 'blur(3px)', display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 1000, padding: '1rem' }}>
          <div style={{ background: '#fff', borderRadius: '16px', maxWidth: '560px', width: '100%', maxHeight: '90vh', display: 'flex', flexDirection: 'column', boxShadow: '0 25px 50px -12px rgba(0,0,0,0.25)', overflow: 'hidden' }}>
            <div style={{ padding: '1.2rem 1.5rem', borderBottom: '1px solid #e2e8f0', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
              <div>
                <h3 style={{ margin: 0, fontSize: '1.15rem', fontWeight: '800', color: '#0f172a' }}>Hotel bewerten</h3>
                <p style={{ margin: 0, fontSize: '0.8rem', color: '#64748b' }}>{ratingHotel.name}</p>
              </div>
              <button onClick={() => setRatingHotel(null)} style={{ background: '#f1f5f9', border: 'none', borderRadius: '50%', width: '32px', height: '32px', cursor: 'pointer', fontWeight: 'bold' }}>✕</button>
            </div>

            <form onSubmit={handleSaveRating} style={{ padding: '1.3rem 1.5rem', overflowY: 'auto', display: 'flex', flexDirection: 'column', gap: '1.1rem' }}>
              <div>
                <label style={{ display: 'block', fontSize: '0.82rem', fontWeight: '800', color: '#1e293b', marginBottom: '0.4rem' }}>
                  1. Wer bewertet? *
                </label>
                {tripMembers.length === 0 ? (
                  <p style={{ margin: 0, fontSize: '0.8rem', color: '#dc2626' }}>
                    Bitte lege zuerst Mitreisende in der Reise-Leiste an!
                  </p>
                ) : (
                  <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(110px, 1fr))', gap: '0.4rem' }}>
                    {tripMembers.map(m => {
                      const isSelected = selectedMemberId === m.id;
                      const hasVoted = ratingHotel.ratings_json?.some(r => r.member_id === m.id);
                      return (
                        <div
                          key={m.id}
                          onClick={() => handleSelectMemberForRating(m.id)}
                          style={{
                            border: isSelected ? '2px solid #2563eb' : '1px solid #e2e8f0',
                            background: isSelected ? '#eff6ff' : '#f8fafc',
                            borderRadius: '8px',
                            padding: '0.45rem',
                            cursor: 'pointer',
                            display: 'flex',
                            alignItems: 'center',
                            gap: '0.4rem'
                          }}
                        >
                          <span style={{ width: '18px', height: '18px', borderRadius: '50%', background: m.color || '#2563eb', color: '#fff', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', fontSize: '0.65rem', fontWeight: '800' }}>
                            {m.name[0]}
                          </span>
                          <div>
                            <div style={{ fontSize: '0.8rem', fontWeight: isSelected ? '800' : '600', color: isSelected ? '#1e40af' : '#1e293b' }}>
                              {m.name}
                            </div>
                            <div style={{ fontSize: '0.65rem', color: hasVoted ? '#059669' : '#94a3b8' }}>
                              {hasVoted ? 'Bereits bewertet' : 'Offen'}
                            </div>
                          </div>
                        </div>
                      );
                    })}
                  </div>
                )}
              </div>

              <div>
                <label style={{ display: 'block', fontSize: '0.82rem', fontWeight: '800', color: '#1e293b', marginBottom: '0.3rem' }}>
                  2. Schulnote (1 = Sehr gut bis 6 = Ungenügend) *
                </label>
                <div style={{ display: 'grid', gridTemplateColumns: 'repeat(6, 1fr)', gap: '0.4rem' }}>
                  {[1, 2, 3, 4, 5, 6].map(gradeNum => {
                    const isSelected = selectedGrade === gradeNum;
                    return (
                      <button
                        type="button"
                        key={gradeNum}
                        onClick={() => setSelectedGrade(gradeNum)}
                        style={{
                          padding: '0.6rem 0.2rem',
                          borderRadius: '8px',
                          fontWeight: '800',
                          fontSize: '1.1rem',
                          cursor: 'pointer',
                          border: isSelected ? '2px solid #2563eb' : '1px solid #cbd5e1',
                          background: isSelected ? '#2563eb' : '#f8fafc',
                          color: isSelected ? '#ffffff' : '#1e293b'
                        }}
                      >
                        {gradeNum}
                      </button>
                    );
                  })}
                </div>
              </div>

              <div>
                <label style={{ display: 'block', fontSize: '0.82rem', fontWeight: '800', color: '#1e293b', marginBottom: '0.3rem' }}>
                  3. Notiz / Begründung (optional)
                </label>
                <input
                  type="text"
                  placeholder="z. B. Strandnähe top, schöner Balkon..."
                  value={ratingComment}
                  onChange={e => setRatingComment(e.target.value)}
                  style={{ width: '100%', padding: '0.6rem', borderRadius: '6px', border: '1px solid #d1d5db', fontSize: '0.85rem' }}
                />
              </div>

              <div style={{ display: 'flex', gap: '0.6rem', justifyContent: 'flex-end', marginTop: '0.4rem' }}>
                <button type="button" onClick={() => setRatingHotel(null)} style={{ padding: '0.55rem 1rem', background: '#e5e7eb', border: 'none', borderRadius: '6px', cursor: 'pointer', fontSize: '0.85rem' }}>
                  Abbrechen
                </button>
                <button type="submit" disabled={isSubmittingRating} style={{ padding: '0.55rem 1.1rem', background: '#2563eb', color: '#fff', border: 'none', borderRadius: '6px', cursor: 'pointer', fontWeight: '700', fontSize: '0.85rem' }}>
                  {isSubmittingRating ? 'Speichere...' : '💾 Note speichern'}
                </button>
              </div>

              <div style={{ borderTop: '1px solid #e2e8f0', paddingTop: '0.8rem', marginTop: '0.4rem' }}>
                <h4 style={{ margin: '0 0 0.5rem 0', fontSize: '0.85rem', fontWeight: '800', color: '#1e293b' }}>
                  Bisherige Bewertungen ({ratingHotel.ratings_json?.length || 0}):
                </h4>
                <div style={{ display: 'flex', flexDirection: 'column', gap: '0.4rem' }}>
                  {(!ratingHotel.ratings_json || ratingHotel.ratings_json.length === 0) ? (
                    <span style={{ fontSize: '0.78rem', color: '#94a3b8', fontStyle: 'italic' }}>Noch keine Stimmen hinterlegt.</span>
                  ) : (
                    ratingHotel.ratings_json.map((r, idx) => (
                      <div key={idx} style={{ background: '#f8fafc', border: '1px solid #e2e8f0', padding: '0.45rem 0.7rem', borderRadius: '6px', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
                        <div>
                          <span style={{ fontWeight: '700', fontSize: '0.8rem', color: '#0f172a' }}>👤 {r.name}: </span>
                          <span style={{ fontSize: '0.78rem', color: '#475569' }}>{r.comment || '(Kein Kommentar)'}</span>
                        </div>
                        <span style={{ background: '#ecfdf5', color: '#047857', border: '1px solid #a7f3d0', padding: '0.1rem 0.4rem', borderRadius: '4px', fontWeight: '800', fontSize: '0.75rem' }}>
                          Note {Number(r.grade).toFixed(1)}
                        </span>
                      </div>
                    ))
                  )}
                </div>
              </div>
            </form>
          </div>
        </div>
      )}

      {/* MODAL 2: MITREISENDE VERWALTEN */}
      {showMembersModal && (
        <div style={{ position: 'fixed', top: 0, left: 0, right: 0, bottom: 0, background: 'rgba(15, 23, 42, 0.6)', backdropFilter: 'blur(3px)', display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 1000, padding: '1rem' }}>
          <div style={{ background: '#fff', borderRadius: '16px', maxWidth: '450px', width: '100%', maxHeight: '85vh', display: 'flex', flexDirection: 'column', boxShadow: '0 25px 50px -12px rgba(0,0,0,0.25)' }}>
            <div style={{ padding: '1.2rem 1.5rem', borderBottom: '1px solid #e2e8f0', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
              <div>
                <h3 style={{ margin: 0, fontSize: '1.2rem', fontWeight: '800' }}>👥 Mitreisende verwalten</h3>
                <p style={{ margin: 0, fontSize: '0.8rem', color: '#64748b' }}>Für: {activeTripObj?.name}</p>
              </div>
              <button onClick={() => setShowMembersModal(false)} style={{ background: '#f1f5f9', border: 'none', borderRadius: '50%', width: '32px', height: '32px', cursor: 'pointer', fontWeight: 'bold' }}>✕</button>
            </div>

            <div style={{ padding: '1.3rem 1.5rem', overflowY: 'auto', display: 'flex', flexDirection: 'column', gap: '1rem' }}>
              <form onSubmit={handleAddMember} style={{ display: 'flex', gap: '0.5rem' }}>
                <input
                  type="text"
                  required
                  placeholder="Name (z. B. Alex)..."
                  value={newMemberName}
                  onChange={e => setNewMemberName(e.target.value)}
                  style={{ flex: 1, padding: '0.55rem', borderRadius: '6px', border: '1px solid #cbd5e1', fontSize: '0.85rem' }}
                />
                <button
                  type="submit"
                  disabled={isAddingMember}
                  style={{ background: '#10b981', color: '#fff', border: 'none', padding: '0.55rem 1rem', borderRadius: '6px', fontWeight: '700', fontSize: '0.85rem', cursor: 'pointer' }}
                >
                  ➕ Hinzufügen
                </button>
              </form>

              <div style={{ fontSize: '0.8rem', fontWeight: '700', color: '#475569' }}>Aktuelle Mitreisende:</div>

              <div style={{ display: 'flex', flexDirection: 'column', gap: '0.4rem' }}>
                {tripMembers.length === 0 ? (
                  <p style={{ margin: 0, fontSize: '0.8rem', color: '#94a3b8', fontStyle: 'italic', textAlign: 'center', padding: '1rem 0' }}>
                    Noch keine Mitreisenden angelegt.
                  </p>
                ) : (
                  tripMembers.map(m => (
                    <div key={m.id} style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', background: '#f8fafc', border: '1px solid #e2e8f0', padding: '0.5rem 0.8rem', borderRadius: '6px' }}>
                      <div style={{ display: 'flex', alignItems: 'center', gap: '0.4rem' }}>
                        <span style={{ width: '18px', height: '18px', borderRadius: '50%', background: m.color || '#2563eb', color: '#fff', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', fontSize: '0.65rem', fontWeight: '800' }}>
                          {m.name[0]}
                        </span>
                        <span style={{ fontWeight: '700', fontSize: '0.85rem', color: '#1e293b' }}>{m.name}</span>
                      </div>
                      <button
                        onClick={() => handleDeleteMember(m.id)}
                        style={{ background: 'none', border: 'none', color: '#ef4444', fontWeight: '700', cursor: 'pointer', fontSize: '0.8rem' }}
                      >
                        🗑️
                      </button>
                    </div>
                  ))
                )}
              </div>
            </div>

            <div style={{ padding: '0.8rem 1.5rem', borderTop: '1px solid #e2e8f0', display: 'flex', justifyContent: 'flex-end' }}>
              <button onClick={() => setShowMembersModal(false)} style={{ padding: '0.5rem 1rem', background: '#2563eb', color: '#fff', border: 'none', borderRadius: '6px', fontWeight: '700', cursor: 'pointer', fontSize: '0.85rem' }}>
                Fertig
              </button>
            </div>
          </div>
        </div>
      )}

      {/* MODAL 3: PREISHISTORIE */}
      {selectedPriceHotel && (
        <div style={{ position: 'fixed', top: 0, left: 0, right: 0, bottom: 0, background: 'rgba(0,0,0,0.6)', display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 1000, padding: '1rem' }}>
          <div style={{ background: '#fff', padding: '1.8rem', borderRadius: '12px', maxWidth: '650px', width: '100%', boxShadow: '0 20px 25px -5px rgba(0,0,0,0.1)' }}>
            <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '1rem' }}>
              <div>
                <h3 style={{ margin: 0, fontSize: '1.3rem' }}>📈 Preishistorie: {selectedPriceHotel.name}</h3>
                <p style={{ margin: 0, color: '#6b7280', fontSize: '0.85rem' }}>Entwicklung aller erfassten Preis-Scans</p>
              </div>
              <button onClick={() => setSelectedPriceHotel(null)} style={{ background: '#f3f4f6', border: 'none', borderRadius: '50%', width: '32px', height: '32px', cursor: 'pointer', fontWeight: 'bold' }}>✕</button>
            </div>
            {loadingPrices ? (
              <p style={{ textAlign: 'center', padding: '2rem', color: '#6b7280' }}>Lade Preishistorie...</p>
            ) : priceHistory.length === 0 ? (
              <p style={{ textAlign: 'center', padding: '2rem', color: '#6b7280' }}>Noch keine Preishistorie vorhanden.</p>
            ) : (
              <div>
                <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: '1rem', marginBottom: '1.5rem', textAlign: 'center' }}>
                  <div style={{ background: '#f0fdf4', padding: '0.8rem', borderRadius: '8px', border: '1px solid #bbf7d0' }}>
                    <small style={{ color: '#166534', fontWeight: 'bold' }}>Tiefstpreis</small>
                    <div style={{ fontSize: '1.3rem', fontWeight: 'bold', color: '#15803d' }}>{minPrice} €</div>
                  </div>
                  <div style={{ background: '#eff6ff', padding: '0.8rem', borderRadius: '8px', border: '1px solid #bfdbfe' }}>
                    <small style={{ color: '#1e40af', fontWeight: 'bold' }}>Aktuellster Preis</small>
                    <div style={{ fontSize: '1.3rem', fontWeight: 'bold', color: '#1d4ed8' }}>{currentPrice} €</div>
                  </div>
                  <div style={{ background: '#fef2f2', padding: '0.8rem', borderRadius: '8px', border: '1px solid #fecaca' }}>
                    <small style={{ color: '#991b1b', fontWeight: 'bold' }}>Höchstpreis</small>
                    <div style={{ fontSize: '1.3rem', fontWeight: 'bold', color: '#b91c1c' }}>{maxPrice} €</div>
                  </div>
                </div>
                {priceHistory.length > 1 && (
                  <div style={{ background: '#fafafa', padding: '1rem', borderRadius: '8px', border: '1px solid #f3f4f6', marginBottom: '1.5rem' }}>
                    <svg viewBox="0 0 500 150" style={{ width: '100%', height: '160px', overflow: 'visible' }}>
                      <line x1="0" y1="20" x2="500" y2="20" stroke="#e5e7eb" strokeDasharray="4" />
                      <line x1="0" y1="130" x2="500" y2="130" stroke="#e5e7eb" strokeDasharray="4" />
                      {(() => {
                        const range = maxPrice - minPrice || 1;
                        const points = priceHistory.map((p, idx) => {
                          const x = (idx / (priceHistory.length - 1)) * 500;
                          const y = 130 - ((p.price - minPrice) / range) * 110;
                          return `${x},${y}`;
                        }).join(' ');
                        return (
                          <>
                            <polyline fill="none" stroke="#2563eb" strokeWidth="3" points={points} />
                            {priceHistory.map((p, idx) => {
                              const x = (idx / (priceHistory.length - 1)) * 500;
                              const y = 130 - ((p.price - minPrice) / range) * 110;
                              return (
                                <g key={p.id}>
                                  <circle cx={x} cy={y} r="5" fill="#2563eb" stroke="#ffffff" strokeWidth="2" />
                                  <text x={x} y={y - 10} fontSize="10" textAnchor="middle" fill="#374151" fontWeight="bold">{p.price}€</text>
                                </g>
                              );
                            })}
                          </>
                        );
                      })()}
                    </svg>
                  </div>
                )}
              </div>
            )}
          </div>
        </div>
      )}

      {/* MODAL: NEUE REISE */}
      {showAddTripModal && (
        <div style={{ position: 'fixed', top: 0, left: 0, right: 0, bottom: 0, background: 'rgba(0,0,0,0.5)', display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 1000, padding: '1rem' }}>
          <div style={{ background: '#fff', padding: '2rem', borderRadius: '12px', maxWidth: '400px', width: '100%' }}>
            <h3 style={{ marginTop: 0 }}>🌍 Neue Reise anlegen</h3>
            <form onSubmit={handleAddTrip}>
              <input
                type="text"
                required
                placeholder="z. B. Indien 2026"
                value={newTripName}
                onChange={e => setNewTripName(e.target.value)}
                style={{ width: '100%', padding: '0.7rem', borderRadius: '6px', border: '1px solid #d1d5db', marginBottom: '1rem' }}
              />
              <div style={{ display: 'flex', gap: '0.5rem', justifyContent: 'flex-end' }}>
                <button type="button" onClick={() => setShowAddTripModal(false)} style={{ padding: '0.6rem 1rem', background: '#e5e7eb', border: 'none', borderRadius: '6px', cursor: 'pointer' }}>Abbrechen</button>
                <button type="submit" style={{ padding: '0.6rem 1rem', background: '#2563eb', color: '#fff', border: 'none', borderRadius: '6px', cursor: 'pointer', fontWeight: 'bold' }}>Anlegen</button>
              </div>
            </form>
          </div>
        </div>
      )}

      {/* MODAL: HOTEL HINZUFÜGEN */}
      {showAddHotelModal && (
        <div style={{ position: 'fixed', top: 0, left: 0, right: 0, bottom: 0, background: 'rgba(0,0,0,0.5)', display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 1000, padding: '1rem' }}>
          <div style={{ background: '#fff', padding: '2rem', borderRadius: '12px', maxWidth: '500px', width: '100%' }}>
            <h2 style={{ marginTop: 0, marginBottom: '0.5rem' }}>➕ Neues Hotel hinzufügen</h2>
            <form onSubmit={handleAddHotel} style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
              <div>
                <label style={{ display: 'block', fontSize: '0.85rem', fontWeight: 'bold', marginBottom: '0.3rem' }}>Hotel Name (optional)</label>
                <input type="text" placeholder="z. B. Abad Fort" value={newHotelName} onChange={e => setNewHotelName(e.target.value)} style={{ width: '100%', padding: '0.6rem', borderRadius: '6px', border: '1px solid #d1d5db' }} />
              </div>
              <div>
                <label style={{ display: 'block', fontSize: '0.85rem', fontWeight: 'bold', marginBottom: '0.3rem' }}>Booking.com URL *</label>
                <input type="url" required placeholder="https://www.booking.com/hotel/..." value={newBookingUrl} onChange={e => setNewBookingUrl(e.target.value)} style={{ width: '100%', padding: '0.6rem', borderRadius: '6px', border: '1px solid #d1d5db' }} />
              </div>
              <div style={{ display: 'flex', gap: '0.8rem', justifyContent: 'flex-end', marginTop: '1rem' }}>
                <button type="button" onClick={() => setShowAddHotelModal(false)} style={{ padding: '0.6rem 1.2rem', background: '#e5e7eb', border: 'none', borderRadius: '6px', cursor: 'pointer' }}>Abbrechen</button>
                <button type="submit" disabled={isSubmitting} style={{ padding: '0.6rem 1.2rem', background: '#10b981', color: '#fff', border: 'none', borderRadius: '6px', cursor: 'pointer', fontWeight: 'bold' }}>
                  {isSubmitting ? 'Speichere...' : 'Speichern'}
                </button>
              </div>
            </form>
          </div>
        </div>
      )}

      {/* MODAL: AUSGEMUSTERTE HOTELS */}
      {showParkedModal && (
        <div style={{ position: 'fixed', top: 0, left: 0, right: 0, bottom: 0, background: 'rgba(15, 23, 42, 0.6)', backdropFilter: 'blur(3px)', display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 1000, padding: '1rem' }}>
          <div style={{ background: '#fff', borderRadius: '16px', maxWidth: '700px', width: '100%', maxHeight: '85vh', display: 'flex', flexDirection: 'column', boxShadow: '0 25px 50px -12px rgba(0,0,0,0.25)' }}>
            <div style={{ padding: '1.2rem 1.5rem', borderBottom: '1px solid #e2e8f0', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
              <div>
                <h3 style={{ margin: 0, fontSize: '1.3rem', fontWeight: '800' }}>📦 Ausgemusterte Hotels</h3>
                <p style={{ margin: 0, fontSize: '0.85rem', color: '#64748b' }}>Diese Hotels sind nicht in der Timeline, bleiben aber gespeichert.</p>
              </div>
              <button onClick={() => setShowParkedModal(false)} style={{ background: '#f1f5f9', border: 'none', borderRadius: '50%', width: '32px', height: '32px', cursor: 'pointer', fontWeight: 'bold' }}>✕</button>
            </div>
            <div style={{ padding: '1.5rem', overflowY: 'auto' }}>
              {parkedHotels.length === 0 ? (
                <p style={{ textAlign: 'center', color: '#64748b', padding: '2rem 0' }}>🎉 Aktuell sind keine Hotels ausgemustert.</p>
              ) : (
                <div style={{ display: 'flex', flexDirection: 'column', gap: '0.8rem' }}>
                  {parkedHotels.map(h => (
                    <div key={h.id} style={{ background: '#f8fafc', border: '1px solid #e2e8f0', borderRadius: '10px', padding: '1rem', display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: '1rem', flexWrap: 'wrap' }}>
                      <div style={{ flex: 1, minWidth: '220px' }}>
                        <div style={{ fontWeight: '700', fontSize: '1rem', color: '#0f172a' }}>{h.name}</div>
                        <div style={{ fontSize: '0.82rem', color: '#64748b', marginTop: '0.2rem' }}>
                          {h.checkin_date ? `🗓️ ${formatDateGerman(h.checkin_date)} – ${formatDateGerman(h.checkout_date)}` : 'Kein Datum'}
                          {h.latest_price ? ` • 💰 Gesamt: ${h.latest_price.toLocaleString('de-DE')} €` : ''}
                        </div>
                      </div>
                      <button onClick={() => restoreHotel(h.id)} style={{ background: '#2563eb', color: '#fff', border: 'none', padding: '0.5rem 1rem', borderRadius: '6px', fontSize: '0.85rem', fontWeight: '700', cursor: 'pointer' }}>
                        ➕ Wieder in Trip aufnehmen
                      </button>
                    </div>
                  ))}
                </div>
              )}
            </div>
          </div>
        </div>
      )}

      {/* MODAL: AUSFLÜGE VERWALTEN */}
      {showManageSightsModal && (
        <div style={{ position: 'fixed', top: 0, left: 0, right: 0, bottom: 0, background: 'rgba(15, 23, 42, 0.6)', backdropFilter: 'blur(3px)', display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 1000, padding: '1rem' }}>
          <div style={{ background: '#fff', borderRadius: '16px', maxWidth: '750px', width: '100%', maxHeight: '85vh', display: 'flex', flexDirection: 'column', boxShadow: '0 25px 50px -12px rgba(0,0,0,0.25)' }}>
            <div style={{ padding: '1.2rem 1.5rem', borderBottom: '1px solid #e2e8f0', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
              <div>
                <h3 style={{ margin: 0, fontSize: '1.3rem', fontWeight: '800' }}>🏛️ Zentrale Ausflugsziele</h3>
                <p style={{ margin: 0, fontSize: '0.85rem', color: '#64748b' }}>Sehenswürdigkeiten bearbeiten und mehreren Hotels zuweisen</p>
              </div>
              <button onClick={() => setShowManageSightsModal(false)} style={{ background: '#f1f5f9', border: 'none', borderRadius: '50%', width: '32px', height: '32px', cursor: 'pointer', fontWeight: 'bold' }}>✕</button>
            </div>
            <div style={{ padding: '1.5rem', overflowY: 'auto', display: 'flex', flexDirection: 'column', gap: '0.8rem' }}>
              {currentAttractions.length === 0 ? (
                <p style={{ textAlign: 'center', color: '#64748b', padding: '2rem 0' }}>Noch keine Ausflugsziele für diesen Trip hinterlegt.</p>
              ) : (
                currentAttractions.map(sight => {
                  const assignedCount = (sight.assignments || []).length;
                  return (
                    <div key={sight.id} style={{ background: '#f8fafc', border: '1px solid #e2e8f0', borderRadius: '10px', padding: '0.9rem 1.1rem', display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: '1rem', flexWrap: 'wrap' }}>
                      <div style={{ flex: 1, minWidth: '240px' }}>
                        <div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
                          <span style={{ fontSize: '1.2rem' }}>{sight.category_icon || '🏛️'}</span>
                          <span style={{ fontWeight: '700', fontSize: '0.95rem', color: '#0f172a' }}>{sight.name}</span>
                        </div>
                        <div style={{ fontSize: '0.8rem', color: '#64748b', marginTop: '0.3rem' }}>
                          {assignedCount > 0 ? (
                            <span style={{ background: '#dbeafe', color: '#1e40af', padding: '0.15rem 0.5rem', borderRadius: '6px', fontWeight: '600' }}>
                              🏨 {assignedCount} {assignedCount === 1 ? 'Hotel' : 'Hotels'} zugeordnet
                            </span>
                          ) : (
                            <span style={{ background: '#f1f5f9', color: '#64748b', padding: '0.15rem 0.5rem', borderRadius: '6px' }}>Keinem Hotel zugewiesen</span>
                          )}
                        </div>
                      </div>
                      <div style={{ display: 'flex', gap: '0.4rem' }}>
                        <button onClick={() => { setShowManageSightsModal(false); openSightModal(sight); }} style={{ background: '#f1f5f9', border: '1px solid #cbd5e1', padding: '0.4rem 0.7rem', borderRadius: '6px', fontSize: '0.8rem', fontWeight: '600', cursor: 'pointer' }}>
                          ✏️ Bearbeiten
                        </button>
                        <button onClick={() => deleteSight(sight.id)} style={{ background: '#fee2e2', color: '#b91c1c', border: 'none', padding: '0.4rem 0.7rem', borderRadius: '6px', fontSize: '0.8rem', fontWeight: '600', cursor: 'pointer' }}>
                          🗑️
                        </button>
                      </div>
                    </div>
                  );
                })
              )}
            </div>
            <div style={{ padding: '1rem 1.5rem', borderTop: '1px solid #e2e8f0', display: 'flex', justifyContent: 'flex-end' }}>
              <button onClick={() => { setShowManageSightsModal(false); openSightModal(); }} style={{ background: '#2563eb', color: '#fff', border: 'none', padding: '0.6rem 1.2rem', borderRadius: '6px', fontWeight: '700', cursor: 'pointer' }}>
                ➕ Neues Ausflugsziel
              </button>
            </div>
          </div>
        </div>
      )}

      {/* MODAL: AUSFLUG ANLEGEN / BEARBEITEN */}
      {showSightFormModal && (
        <div style={{ position: 'fixed', top: 0, left: 0, right: 0, bottom: 0, background: 'rgba(15, 23, 42, 0.6)', backdropFilter: 'blur(3px)', display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 1001, padding: '1rem' }}>
          <div style={{ background: '#fff', borderRadius: '16px', maxWidth: '520px', width: '100%', maxHeight: '90vh', display: 'flex', flexDirection: 'column', boxShadow: '0 25px 50px -12px rgba(0,0,0,0.25)', overflow: 'hidden' }}>
            <div style={{ padding: '1.2rem 1.5rem', borderBottom: '1px solid #e2e8f0', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
              <h3 style={{ margin: 0, fontSize: '1.2rem', fontWeight: '800' }}>
                {editingSight ? '✏️ Ausflugsziel bearbeiten' : '🏛️ Neues Ausflugsziel'}
              </h3>
              <button onClick={() => setShowSightFormModal(false)} style={{ background: '#f1f5f9', border: 'none', borderRadius: '50%', width: '30px', height: '30px', cursor: 'pointer', fontWeight: 'bold' }}>✕</button>
            </div>
            <form onSubmit={handleSaveSight} style={{ padding: '1.5rem', overflowY: 'auto', display: 'flex', flexDirection: 'column', gap: '0.9rem' }}>
              <div>
                <label style={{ display: 'block', fontSize: '0.82rem', fontWeight: '700', marginBottom: '0.2rem' }}>🔗 Google Maps Link oder Adresse *</label>
                <input type="text" required placeholder="https://maps.app.goo.gl/... oder Adresse" value={sightAddress} onChange={e => setSightAddress(e.target.value)} style={{ width: '100%', padding: '0.6rem', borderRadius: '6px', border: '1px solid #d1d5db' }} />
              </div>
              <div>
                <label style={{ display: 'block', fontSize: '0.82rem', fontWeight: '700', marginBottom: '0.2rem' }}>Name der Sehenswürdigkeit</label>
                <input type="text" placeholder="z. B. Chinesische Fischernetze" value={sightName} onChange={e => setSightName(e.target.value)} style={{ width: '100%', padding: '0.6rem', borderRadius: '6px', border: '1px solid #d1d5db' }} />
              </div>
              <div>
                <label style={{ display: 'block', fontSize: '0.82rem', fontWeight: '700', marginBottom: '0.3rem' }}>🏨 Diesen Hotels zuordnen (Distanz wird berechnet):</label>
                <div style={{ maxHeight: '140px', overflowY: 'auto', border: '1px solid #d1d5db', borderRadius: '6px', padding: '0.5rem', background: '#fafafa', display: 'flex', flexDirection: 'column', gap: '0.3rem' }}>
                  {hotels.map(h => {
                    const isChecked = selectedHotelIds.includes(h.id);
                    return (
                      <label key={h.id} style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', fontSize: '0.82rem', cursor: 'pointer', padding: '0.2rem 0.3rem', borderRadius: '4px', background: isChecked ? '#eff6ff' : 'transparent' }}>
                        <input type="checkbox" checked={isChecked} onChange={() => toggleHotelSelection(h.id)} />
                        <span style={{ fontWeight: isChecked ? '700' : 'normal', color: isChecked ? '#1e40af' : '#374151' }}>
                          {h.name} {h.checkin_date ? `(${formatDateGerman(h.checkin_date)})` : ''}
                        </span>
                      </label>
                    );
                  })}
                </div>
              </div>
              <div>
                <label style={{ display: 'block', fontSize: '0.82rem', fontWeight: '700', marginBottom: '0.2rem' }}>Kategorie</label>
                <select value={sightIcon} onChange={e => setSightIcon(e.target.value)} style={{ width: '100%', padding: '0.6rem', borderRadius: '6px', border: '1px solid #d1d5db' }}>
                  <option value="🏛️">🏛️ Kultur & Bauwerk</option>
                  <option value="🌿">🌿 Natur & Park</option>
                  <option value="🏖️">🏖️ Strand & Meer</option>
                  <option value="🐘">🐘 Safari & Tiere</option>
                  <option value="🍛">🍛 Food & Markt</option>
                  <option value="🧗">🧗 Abenteuer & Wandern</option>
                </select>
              </div>
              <div>
                <label style={{ display: 'block', fontSize: '0.82rem', fontWeight: '700', marginBottom: '0.2rem' }}>Notizen / Tipps (optional)</label>
                <input type="text" placeholder="z. B. Vorab Tickets kaufen" value={sightNotes} onChange={e => setSightNotes(e.target.value)} style={{ width: '100%', padding: '0.6rem', borderRadius: '6px', border: '1px solid #d1d5db' }} />
              </div>
              <div style={{ display: 'flex', gap: '0.6rem', justifyContent: 'flex-end', marginTop: '0.5rem' }}>
                <button type="button" onClick={() => setShowSightFormModal(false)} style={{ padding: '0.55rem 1rem', background: '#e5e7eb', border: 'none', borderRadius: '6px', cursor: 'pointer', fontSize: '0.85rem' }}>Abbrechen</button>
                <button type="submit" disabled={isSubmittingSight} style={{ padding: '0.55rem 1.1rem', background: '#2563eb', color: '#fff', border: 'none', borderRadius: '6px', cursor: 'pointer', fontWeight: '700', fontSize: '0.85rem' }}>
                  {isSubmittingSight ? 'Berechne Routen...' : 'Speichern & Berechnen'}
                </button>
              </div>
            </form>
          </div>
        </div>
      )}

    </div>
  );
}
```

--- DATEI: src/lib/db.ts ---
```
import { Pool } from 'pg';

const pool = new Pool({
  connectionString: process.env.DATABASE_URL,
});

export const query = (text: string, params?: any[]) => pool.query(text, params);

```

--- DATEI: tsconfig.json ---
```
{
  "compilerOptions": {
    "target": "es2017",
    "lib": ["dom", "dom.iterable", "esnext"],
    "allowJs": true,
    "skipLibCheck": true,
    "strict": true,
    "noEmit": true,
    "esModuleInterop": true,
    "module": "esnext",
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "jsx": "preserve",
    "incremental": true,
    "plugins": [
      {
        "name": "next"
      }
    ],
    "paths": {
      "@/*": ["./src/*"]
    }
  },
  "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
  "exclude": ["node_modules"]
}

```
