airbnb-finder/prisma/schema.prisma
AI d9a203016f feat: massive Airbnb import pipeline overhaul + UI fixes
🔥 Scraper Improvements:
- Add JSON-LD price extraction (regression fix)
- Fix sleeping spotsPerUnit bug (was hardcoded to 2)
- Remove stale CSS selectors, add robust fallbacks
- Add JSON-LD price fallback in extraction pipeline
- Improve sleeping parser regex (lastIndex bug fix)
- Add 15+ new bed type patterns (murphy, day bed, hammock, plurals)
- Smarter deriveSleepingFromBeds() with mixed bed logic

📅 Import Form UX:
- Smart defaults (next weekend dates)
- Auto-calculate nights display
- URL param auto-detection (?check_in=&check_out=&adults=)
- Better visual hierarchy with icons
- Progress steps during import
- Success redirect to listing detail page

🗑️ Delete Button Fix:
- Add router.refresh() after successful delete
- Inline error state instead of alert()
- Admin delete button as proper client component

✏️ Edit/Admin Fixes:
- Fix revalidatePath using slug instead of id
- Fix redirect to detail page after edit
- Add cascade delete logic to admin deleteListing
- Extract delete to proper client component

🎨 UI States for Partial Data:
- Price: 'Preis auf Anfrage' with context hint
- Location: 'Ort nicht erkannt' instead of empty
- Sleeping: placeholder when no data
- Suitability: 3-state (yes/no/unknown)
- Use formatPrice/formatRating utilities

🛏️ Sleeping Data Quality:
- Add sleepingDataQuality to Prisma schema
- Save quality (EXACT/DERIVED/UNKNOWN) to DB
- Display '(geschätzt)' label for derived data

📊 Database:
- Restore corrupted schema.prisma from git
- Add sleepingDataQuality field
- Push schema changes

 TypeScript: Zero errors
 Build: Successful
2026-03-12 08:07:52 +00:00

157 lines
4.9 KiB
Plaintext

// This is your Prisma schema file
// Learn more: https://pris.ly/d/prisma-schema
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "sqlite"
url = env("DATABASE_URL")
}
// ============================================
// MODELS
// ============================================
model Listing {
id String @id @default(cuid())
slug String @unique
airbnbUrl String @unique @map("airbnb_url")
normalizedUrl String @unique @map("normalized_url")
externalId String? @unique @map("external_id")
// Basic Info
title String
locationText String? @map("location_text")
latitude Float?
longitude Float?
// Pricing
nightlyPrice Float? @map("nightly_price")
totalPrice Float? @map("total_price")
currency String? @default("EUR")
priceStatus String? @map("price_status") // EXTRACTED, REQUIRES_TRIP_CONTEXT, UNKNOWN, PARTIAL
// Rating
rating Float?
reviewCount Int? @map("review_count")
// Capacity
guestCount Int? @map("guest_count")
officialGuestCount Int? @map("official_guest_count")
// Sleeping Analysis
maxSleepingPlaces Int? @map("max_sleeping_places")
suitableFor4 Boolean? @map("suitable_for_4")
extraMattressesNeededFor4 Int? @map("extra_mattresses_needed_for_4")
bedTypesSummary String? @map("bed_types_summary")
sleepingDataQuality String? @map("sleeping_data_quality") // EXACT, DERIVED, UNKNOWN
// Room Details
bedrooms Int?
beds Int?
bathrooms Float?
// Description
description String?
hostName String? @map("host_name")
cancellationPolicy String? @map("cancellation_policy")
// Amenities (stored as JSON string)
amenities String? @map("amenities")
// Status & Flags (using String instead of Enum for SQLite)
isFavorite Boolean @default(false) @map("is_favorite")
status String @default("NEW") // NEW, INTERESTING, SHORTLIST, BOOKED, REJECTED
// Images
coverImage String? @map("cover_image")
// Raw Data
rawSourceData String? @map("raw_source_data")
// Timestamps
importedAt DateTime @default(now()) @map("imported_at")
updatedAt DateTime @updatedAt @map("updated_at")
// Relations
images ListingImage[]
notes AdminNote[]
sleepingOptions ListingSleepingOption[]
tags ListingTag[]
// Indexes
@@index([status])
@@index([isFavorite])
@@index([locationText])
@@index([nightlyPrice])
@@index([rating])
@@map("listings")
}
model ListingImage {
id String @id @default(cuid())
listingId String @map("listing_id")
url String
alt String?
sortOrder Int @default(0) @map("sort_order")
isExternal Boolean @default(true) @map("is_external")
createdAt DateTime @default(now()) @map("created_at")
listing Listing @relation(fields: [listingId], references: [id], onDelete: Cascade)
@@index([listingId, sortOrder])
@@map("listing_images")
}
model AdminNote {
id String @id @default(cuid())
listingId String @map("listing_id")
body String
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
listing Listing @relation(fields: [listingId], references: [id], onDelete: Cascade)
@@index([listingId, createdAt])
@@map("admin_notes")
}
model Tag {
id String @id @default(cuid())
name String @unique
slug String @unique
color String? @default("#6366f1")
listings ListingTag[]
@@map("tags")
}
model ListingTag {
listingId String @map("listing_id")
tagId String @map("tag_id")
listing Listing @relation(fields: [listingId], references: [id], onDelete: Cascade)
tag Tag @relation(fields: [tagId], references: [id], onDelete: Cascade)
@@id([listingId, tagId])
@@map("listing_tags")
}
model ListingSleepingOption {
id String @id @default(cuid())
listingId String @map("listing_id")
bedType String @map("bed_type") // DOUBLE, SINGLE, SOFA_BED, etc.
quantity Int @default(1)
spotsPerUnit Int @default(1) @map("spots_per_unit")
quality String @default("FULL") // FULL, AUXILIARY
label String?
notes String?
listing Listing @relation(fields: [listingId], references: [id], onDelete: Cascade)
@@index([listingId])
@@map("listing_sleeping_options")
}