========================================
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]/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/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]/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.nights AS latest_nights,
        ph.persons AS latest_persons
      FROM hotels h
      LEFT JOIN trips t ON h.trip_id = t.id
      LEFT JOIN LATERAL (
        SELECT 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
      ORDER BY h.checkin_date ASC NULLS LAST, h.id ASC
    `);

    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/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);

    // 1. In hotel_price_history eintragen mit echten Spalten
    if (numericPrice !== null && numericPrice > 0) {
      try {
        await query(
          `INSERT INTO hotel_price_history (hotel_id, price_numeric, price_raw, currency, nights, persons, tracked_at)
           VALUES ($1::uuid, $2, $3, 'EUR', $4, $5, NOW())`,
          [hotelId, numericPrice, 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';

interface Trip {
  id: string;
  name: string;
}

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_nights?: number | null;
  latest_persons?: number | null;
  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() {
  const [trips, setTrips] = useState<Trip[]>([]);
  const [activeTripId, setActiveTripId] = useState<string>('ALL');
  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);

  // Ausflugsziele: Zentral & Mehrfachzuordnung
  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);

  useEffect(() => {
    loadData();
  }, []);

  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 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);
    }
  };

  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 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 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, name: 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);
    }
  };

  // Ausflug Modal öffnen (mit Checkboxen für Mehrfachauswahl)
  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);
    }
  };

  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;

  return (
    <div style={{ maxWidth: '1050px', margin: '0 auto', padding: '2rem 1rem', fontFamily: 'system-ui, sans-serif', color: '#1f2937' }}>
      
      <header style={{ marginBottom: '2rem', textAlign: 'center' }}>
        <h1 style={{ fontSize: '2.2rem', marginBottom: '0.5rem', fontWeight: '800' }}>📍 Reise-Ablauf & Timeline</h1>
        <p style={{ color: '#6b7280', margin: 0 }}>Routenplanung, Sehenswürdigkeiten, Fahrzeiten & KI-Reviews</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.7rem 1.3rem', borderRadius: '8px', fontWeight: '600', cursor: 'pointer' }}
          >
            ➕ Neues Hotel hinzufügen
          </button>
          <button
            onClick={() => openSightModal()}
            style={{ background: '#2563eb', color: '#fff', border: 'none', padding: '0.7rem 1.3rem', borderRadius: '8px', fontWeight: '600', cursor: 'pointer' }}
          >
            🏛️ Ausflug anlegen
          </button>
          <button
            onClick={() => setShowManageSightsModal(true)}
            style={{ background: '#f1f5f9', color: '#334155', border: '1px solid #cbd5e1', padding: '0.7rem 1.3rem', 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.7rem 1.3rem', borderRadius: '8px', fontWeight: '700', cursor: 'pointer', display: 'flex', alignItems: 'center', gap: '0.4rem' }}
          >
            📦 Ausgemusterte Hotels
            {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.7rem 1.3rem', borderRadius: '8px', fontWeight: '600', cursor: 'pointer', opacity: scanningAllPrices ? 0.7 : 1 }}
          >
            {scanningAllPrices ? '🔄 Scanne...' : '📊 Preise aktualisieren'}
          </button>
        </div>
      </header>

      {/* TABS */}
      <div style={{ display: 'flex', gap: '0.5rem', borderBottom: '2px solid #e5e7eb', marginBottom: '2rem', overflowX: 'auto', paddingBottom: '0.5rem' }}>
        <button
          onClick={() => setActiveTripId('ALL')}
          style={{
            padding: '0.6rem 1.2rem',
            borderRadius: '8px',
            border: 'none',
            background: activeTripId === 'ALL' ? '#2563eb' : '#f3f4f6',
            color: activeTripId === 'ALL' ? '#ffffff' : '#4b5563',
            fontWeight: 'bold',
            cursor: 'pointer',
            whiteSpace: 'nowrap'
          }}
        >
          🌐 Alle aktiven 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' : '#f3f4f6',
              color: String(activeTripId).toLowerCase() === String(trip.id).toLowerCase() ? '#ffffff' : '#4b5563',
              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>

      {/* MODAL: AUSFLÜGE ZENTRAL 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 flexibel 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', overflow: 'hidden' }}>
                        <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', display: 'flex', gap: '0.4rem', alignItems: 'center', flexWrap: 'wrap' }}>
                          {assignedCount > 0 ? (
                            <span style={{ background: '#dbeafe', color: '#1e40af', padding: '0.15rem 0.5rem', borderRadius: '6px', fontWeight: '600' }}>
                              🏨 Zugewiesen zu {assignedCount} {assignedCount === 1 ? 'Hotel' : 'Hotels'}
                            </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 (optional bei Maps-Link)
                </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>

              {/* MEHRFACHAUSWAHL VON HOTELS */}
              <div>
                <label style={{ display: 'block', fontSize: '0.82rem', fontWeight: '700', marginBottom: '0.3rem' }}>
                  🏨 Diesen Hotels zuordnen (Distanz wird für jedes Hotel 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.length === 0 ? (
                    <span style={{ fontSize: '0.8rem', color: '#9ca3af' }}>Keine Hotels angelegt.</span>
                  ) : (
                    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. Öffnungszeiten, Tickets vorab" 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>
      )}

      {/* 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 aktuell nicht Teil deiner 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>
      )}

      {/* PREISHISTORIE MODAL */}
      {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>
      )}

      {/* TIMELINE MIT CLUSTERING */}
      {loading ? (
        <p style={{ textAlign: 'center', color: '#6b7280' }}>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' }}>
            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;

            return (
              <div key={stop.clusterKey} style={{ position: 'relative', marginBottom: '3rem' }}>
                <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.8rem', 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 Vergleich
                      </span>
                    )}
                  </div>
                </div>

                {/* Hotel Grid */}
                <div style={{
                  display: 'grid',
                  gridTemplateColumns: isCluster ? 'repeat(auto-fit, minmax(340px, 1fr))' : '1fr',
                  gap: '1.2rem',
                  background: isCluster ? '#fdfcff' : 'transparent',
                  padding: isCluster ? '1.2rem' : '0',
                  borderRadius: '16px',
                  border: isCluster ? '2px dashed #c4b5fd' : 'none'
                }}>
                  {stop.hotels.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 optionColors = ['#2563eb', '#7c3aed', '#059669', '#d97706'];
                    const currentAccent = optionColors[hIdx % optionColors.length];
                    
                    // Filtert Ausflüge, die DIESEM Hotel zugeordnet sind
                    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 #e5e7eb',
                          borderTop: isCluster ? `4px solid ${currentAccent}` : '1px solid #e5e7eb',
                          borderRadius: '12px',
                          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 Header */}
                          <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '0.6rem' }}>
                            {isCluster ? (
                              <span style={{ fontSize: '0.75rem', fontWeight: '800', textTransform: 'uppercase', background: '#f3f4f6', color: currentAccent, padding: '0.2rem 0.5rem', borderRadius: '4px', border: '1px solid #e5e7eb' }}>
                                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, hotel.name)}
                              title="Dieses Hotel aus der Timeline nehmen"
                              style={{ background: 'transparent', border: '1px solid #cbd5e1', color: '#64748b', padding: '0.3rem 0.6rem', borderRadius: '6px', fontSize: '0.78rem', fontWeight: '600', cursor: 'pointer' }}
                            >
                              👁️‍🗨️ Nicht betrachten
                            </button>
                          </div>

                          {/* Hotel Body */}
                          <div style={{ display: 'flex', gap: '1rem', flexWrap: 'wrap', marginBottom: '0.8rem' }}>
                            {hotel.image_url && <img src={hotel.image_url} alt={hotel.name} style={{ width: '110px', height: '85px', objectFit: 'cover', borderRadius: '8px', flexShrink: 0 }} />}
                            <div style={{ flex: 1, minWidth: '180px' }}>
                              <h3 style={{ margin: '0 0 0.3rem 0', fontSize: '1.1rem', color: '#1f2937', lineHeight: 1.3 }}>{hotel.name}</h3>
                              <p style={{ margin: '0 0 0.4rem 0', fontSize: '0.8rem', color: '#6b7280' }}>📍 {hotel.address || 'Keine Adresse hinterlegt'}</p>
                            </div>
                          </div>

                          {/* Preis-Box */}
                          {totalPrice !== null ? (
                            <div style={{ display: 'flex', gap: '0.5rem', flexWrap: 'wrap', marginBottom: '0.8rem', background: '#ecfdf5', padding: '0.5rem 0.7rem', borderRadius: '8px', border: '1px solid #a7f3d0', alignItems: 'center' }}>
                              <span style={{ fontWeight: '800', color: '#047857', fontSize: '0.92rem' }}>
                                💰 Gesamt: {totalPrice.toLocaleString('de-DE')} €
                              </span>
                              <span style={{ color: '#059669', fontSize: '0.8rem', fontWeight: '600' }}>
                                | 🌙 {pricePerNight?.toLocaleString('de-DE')} € / N.
                              </span>
                              <span style={{ color: '#059669', fontSize: '0.8rem' }}>
                                | 👤 {pricePerPersonPerNight?.toLocaleString('de-DE')} € / P.
                              </span>
                            </div>
                          ) : (
                            <div style={{ marginBottom: '0.8rem', fontSize: '0.82rem', color: '#6b7280', fontStyle: 'italic' }}>
                              💰 Noch kein Preis erfasst
                            </div>
                          )}

                          {/* Meta Tags */}
                          <div style={{ display: 'flex', gap: '0.8rem', fontSize: '0.8rem', marginBottom: '0.8rem', color: '#374151', flexWrap: 'wrap' }}>
                            <span>⭐ <strong>{hotel.rating || 'N/A'}</strong>/10</span>
                            <span>💬 {hotel.review_count || 0} Bew.</span>
                            {hotel.room_size_m2 && <span>📐 {hotel.room_size_m2} m²</span>}
                          </div>

                          {/* SEHENSWÜRDIGKEITEN & FAHRZEITEN FÜR DIESES HOTEL */}
                          <div style={{ marginTop: '0.8rem', background: '#f8fafc', border: '1px solid #e2e8f0', borderRadius: '10px', padding: '0.8rem' }}>
                            <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '0.6rem' }}>
                              <div style={{ fontWeight: '700', fontSize: '0.85rem', color: '#334155', display: 'flex', alignItems: 'center', gap: '0.4rem' }}>
                                🏛️ Ausflugsziele ({hotelSights.length})
                              </div>
                              <button
                                onClick={() => openSightModal(undefined, hotel.id)}
                                style={{ background: '#eff6ff', color: '#2563eb', border: '1px dashed #bfdbfe', padding: '0.25rem 0.6rem', borderRadius: '6px', fontSize: '0.75rem', fontWeight: '700', cursor: 'pointer' }}
                              >
                                ➕ Ausflug zuordnen
                              </button>
                            </div>

                            <div style={{ display: 'flex', flexDirection: 'column', gap: '0.4rem' }}>
                              {hotelSights.length === 0 ? (
                                <p style={{ margin: 0, fontSize: '0.75rem', color: '#94a3b8', fontStyle: 'italic' }}>Keine Ausflugsziele zugeordnet.</p>
                              ) : (
                                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.5rem 0.7rem', display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: '0.6rem', flexWrap: 'wrap' }}>
                                      <div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', flex: 1, minWidth: '160px', overflow: 'hidden' }}>
                                        <span style={{ fontSize: '1.1rem', flexShrink: 0 }}>{sight.category_icon || '🏛️'}</span>
                                        <div style={{ overflow: 'hidden' }}>
                                          <div style={{ fontWeight: '700', fontSize: '0.82rem', color: '#0f172a', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
                                            {sight.name}
                                          </div>
                                          {sight.notes && (
                                            <div style={{ fontSize: '0.72rem', color: '#64748b', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
                                              {sight.notes}
                                            </div>
                                          )}
                                        </div>
                                      </div>

                                      {assignment?.distance_km !== null && assignment?.distance_km !== undefined ? (
                                        <div style={{ background: '#ecfdf5', border: '1px solid #a7f3d0', color: '#047857', padding: '0.2rem 0.5rem', borderRadius: '6px', fontSize: '0.78rem', fontWeight: '700', whiteSpace: 'nowrap' }}>
                                          🚗 {assignment.distance_km} km • {assignment.duration_minutes} Min.
                                        </div>
                                      ) : (
                                        <span style={{ fontSize: '0.72rem', color: '#94a3b8' }}>📍 Route n.v.</span>
                                      )}

                                      <div style={{ display: 'flex', gap: '0.3rem', alignItems: 'center' }}>
                                        <a
                                          href={sight.address.startsWith('http') ? sight.address : `https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(sight.name + ' ' + sight.address)}`}
                                          target="_blank"
                                          rel="noopener noreferrer"
                                          style={{ color: '#2563eb', background: '#eff6ff', border: '1px solid #bfdbfe', padding: '0.2rem 0.4rem', borderRadius: '4px', fontSize: '0.72rem', textDecoration: 'none', fontWeight: '600' }}
                                        >
                                          Maps ↗
                                        </a>
                                        <button
                                          onClick={() => openSightModal(sight)}
                                          title="Bearbeiten"
                                          style={{ background: '#f1f5f9', border: '1px solid #cbd5e1', cursor: 'pointer', fontSize: '0.75rem', padding: '0.2rem 0.4rem', borderRadius: '4px' }}
                                        >
                                          ✏️
                                        </button>
                                        <button
                                          onClick={() => deleteSight(sight.id)}
                                          title="Löschen"
                                          style={{ background: 'none', border: 'none', color: '#94a3b8', cursor: 'pointer', fontSize: '0.8rem', padding: '0.1rem 0.3rem' }}
                                        >
                                          ✕
                                        </button>
                                      </div>
                                    </div>
                                  );
                                })
                              )}
                            </div>
                          </div>
                        </div>

                        {/* Action Buttons */}
                        <div style={{ display: 'flex', gap: '0.5rem', flexWrap: 'wrap', marginTop: '0.6rem', paddingTop: '0.6rem', borderTop: '1px solid #f3f4f6' }}>
                          <button
                            onClick={() => openPriceHistoryModal(hotel)}
                            style={{ background: '#0284c7', color: '#ffffff', border: 'none', padding: '0.4rem 0.7rem', borderRadius: '6px', cursor: 'pointer', fontWeight: '600', fontSize: '0.78rem' }}
                          >
                            📈 Preishistorie
                          </button>
                          <button
                            onClick={() => scanSingleHotel(hotel.id)}
                            disabled={scanningHotelId === hotel.id || !hotel.is_active}
                            style={{ background: '#8b5cf6', color: '#ffffff', border: 'none', padding: '0.4rem 0.7rem', borderRadius: '6px', cursor: 'pointer', fontWeight: '600', fontSize: '0.78rem', 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', alignSelf: 'center', marginLeft: 'auto', fontWeight: '500' }}>
                            🔗 Booking ↗
                          </a>
                        </div>
                      </div>
                    );
                  })}
                </div>
              </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"]
}

```
