Files
hms-licht-ton/frontend/pages/mietanfrage.vue
T

509 lines
22 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="max-w-5xl mx-auto px-4 py-8">
<!-- Breadcrumb -->
<nav class="flex items-center gap-2 text-sm text-text-muted mb-6" aria-label="Breadcrumb">
<NuxtLink to="/" class="hover:text-accent transition-colors duration-fast">Home</NuxtLink>
<span class="text-secondary">/</span>
<NuxtLink to="/warenkorb" class="hover:text-accent transition-colors duration-fast">Warenkorb</NuxtLink>
<span class="text-secondary">/</span>
<span class="text-text font-medium">Mietanfrage</span>
</nav>
<h1 class="text-2xl font-bold text-text mb-6" data-testid="mietanfrage-title">Mietanfrage</h1>
<!-- Empty Cart Redirect -->
<div v-if="isEmpty" class="flex flex-col items-center justify-center py-16 text-center" data-testid="mietanfrage-empty-cart">
<div class="w-20 h-20 mb-4 flex items-center justify-center rounded-full bg-surface">
<svg class="w-10 h-10 text-secondary" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" d="M2.25 3h1.386c.51 0 .955.343 1.087.835l.383 1.437M7.5 14.25a3 3 0 00-3 3h15.75m-12.75-3h11.218c1.121-2.3 2.1-4.684 2.924-7.138a60.114 60.114 0 00-16.536-1.84M7.5 14.25L5.106 5.272M6 20.25a.75.75 0 11-1.5 0 .75.75 0 011.5 0zm12.75 0a.75.75 0 11-1.5 0 .75.75 0 011.5 0z" />
</svg>
</div>
<h2 class="text-lg font-bold text-text mb-2">Ihr Warenkorb ist leer</h2>
<p class="text-text-muted text-sm mb-6 max-w-sm">Bitte fügen Sie zuerst Artikel zum Warenkorb hinzu, bevor Sie eine Mietanfrage stellen.</p>
<NuxtLink to="/mietkatalog" class="btn-primary" data-testid="mietanfrage-empty-cta">
Zum Mietkatalog
</NuxtLink>
</div>
<!-- Success State -->
<div v-else-if="submitState === 'success'" class="flex flex-col items-center justify-center py-16 text-center" data-testid="mietanfrage-success">
<div class="w-20 h-20 mb-4 flex items-center justify-center rounded-full bg-success-bg">
<svg class="w-10 h-10 text-success" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
<h2 class="text-lg font-bold text-text mb-2">Vielen Dank für Ihre Anfrage!</h2>
<p class="text-text-muted text-sm mb-2">Ihre Mietanfrage wurde erfolgreich übermittelt.</p>
<p class="text-text-muted text-sm mb-4">
Ihre Referenznummer:
<span class="text-accent font-bold text-base" data-testid="mietanfrage-reference-number">{{ referenceNumber }}</span>
</p>
<p class="text-text-muted text-xs mb-6 max-w-md">Wir werden uns in Kürze mit Ihnen in Verbindung setzen, um die Verfügbarkeit zu prüfen und Ihnen ein Angebot zu erstellen.</p>
<NuxtLink to="/" class="btn-primary" data-testid="mietanfrage-success-home">
Zurück zur Startseite
</NuxtLink>
</div>
<!-- Form State -->
<div v-else class="grid grid-cols-1 lg:grid-cols-3 gap-8">
<!-- Cart Overview (Read-Only) -->
<div class="lg:col-span-1">
<div class="panel rounded-lg p-4" data-testid="mietanfrage-cart-overview">
<h2 class="text-base font-bold text-text mb-4">Übersicht</h2>
<div class="space-y-3">
<div
v-for="item in items"
:key="item.equipment_id"
class="flex items-center gap-3"
data-testid="mietanfrage-cart-item"
>
<div class="w-10 h-10 rounded-md overflow-hidden bg-surface shrink-0">
<img
v-if="item.image_url"
:src="item.image_url"
:alt="item.name"
class="w-full h-full object-cover"
/>
<div v-else class="w-full h-full flex items-center justify-center">
<svg class="w-5 h-5 text-secondary" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" d="M2.25 15.75l5.159-5.159a2.25 2.25 0 013.182 0l5.159 5.159m-1.5-1.5l1.409-1.409a2.25 2.25 0 013.182 0l2.909 2.909M3.75 3.75h16.5a1.5 1.5 0 011.5 1.5v12a1.5 1.5 0 01-1.5 1.5H3.75a1.5 1.5 0 01-1.5-1.5V5.25a1.5 1.5 0 011.5-1.5z" />
</svg>
</div>
</div>
<div class="flex-grow min-w-0">
<p class="text-sm font-medium text-text line-clamp-1">{{ item.name }}</p>
<p class="text-xs text-text-muted">Menge: {{ item.quantity }}</p>
</div>
</div>
</div>
<div class="mt-4 pt-4 border-t border-border-default">
<p class="text-sm text-text-muted">
Insgesamt <span class="text-text font-bold">{{ totalCount }}</span> Artikel
</p>
</div>
</div>
</div>
<!-- Form Section -->
<div class="lg:col-span-2">
<form class="space-y-8" @submit.prevent="handleSubmit" data-testid="mietanfrage-form">
<!-- Event Details -->
<fieldset class="panel rounded-lg p-6">
<legend class="text-base font-bold text-text px-2">Veranstaltungsdetails</legend>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4 mt-4">
<!-- Event Name -->
<div class="sm:col-span-2">
<label for="event_name" class="block text-sm font-medium text-text-muted mb-1">
Veranstaltung / Event <span class="text-accent">*</span>
</label>
<input
id="event_name"
v-model="form.event_name"
type="text"
class="w-full bg-surface border border-border-default rounded-md px-3 py-2.5 text-text placeholder:text-secondary focus:border-accent focus:outline-none transition-colors duration-fast"
:class="{ 'border-error': errors.event_name }"
data-testid="form-event-name"
placeholder="z.B. Firmenfeier, Hochzeit, Konzert"
@blur="validateField('event_name')"
/>
<p v-if="errors.event_name" class="text-xs text-error mt-1" data-testid="error-event-name">{{ errors.event_name }}</p>
</div>
<!-- Date Start -->
<div>
<label for="date_start" class="block text-sm font-medium text-text-muted mb-1">
Von <span class="text-accent">*</span>
</label>
<input
id="date_start"
v-model="form.date_start"
type="date"
class="w-full bg-surface border border-border-default rounded-md px-3 py-2.5 text-text focus:border-accent focus:outline-none transition-colors duration-fast"
:class="{ 'border-error': errors.date_start }"
data-testid="form-date-start"
@blur="validateField('date_start')"
/>
<p v-if="errors.date_start" class="text-xs text-error mt-1" data-testid="error-date-start">{{ errors.date_start }}</p>
</div>
<!-- Date End -->
<div>
<label for="date_end" class="block text-sm font-medium text-text-muted mb-1">
Bis <span class="text-accent">*</span>
</label>
<input
id="date_end"
v-model="form.date_end"
type="date"
class="w-full bg-surface border border-border-default rounded-md px-3 py-2.5 text-text focus:border-accent focus:outline-none transition-colors duration-fast"
:class="{ 'border-error': errors.date_end }"
data-testid="form-date-end"
@blur="validateField('date_end')"
/>
<p v-if="errors.date_end" class="text-xs text-error mt-1" data-testid="error-date-end">{{ errors.date_end }}</p>
</div>
<!-- Location -->
<div class="sm:col-span-2">
<label for="location" class="block text-sm font-medium text-text-muted mb-1">
Veranstaltungsort <span class="text-accent">*</span>
</label>
<input
id="location"
v-model="form.location"
type="text"
class="w-full bg-surface border border-border-default rounded-md px-3 py-2.5 text-text placeholder:text-secondary focus:border-accent focus:outline-none transition-colors duration-fast"
:class="{ 'border-error': errors.location }"
data-testid="form-location"
placeholder="z.B. Leipheim, Bayern"
@blur="validateField('location')"
/>
<p v-if="errors.location" class="text-xs text-error mt-1" data-testid="error-location">{{ errors.location }}</p>
</div>
<!-- Person Count -->
<div class="sm:col-span-2">
<label for="person_count" class="block text-sm font-medium text-text-muted mb-1">
Personenanzahl <span class="text-accent">*</span>
</label>
<input
id="person_count"
v-model.number="form.person_count"
type="number"
min="1"
class="w-full bg-surface border border-border-default rounded-md px-3 py-2.5 text-text placeholder:text-secondary focus:border-accent focus:outline-none transition-colors duration-fast"
:class="{ 'border-error': errors.person_count }"
data-testid="form-person-count"
placeholder="z.B. 100"
@blur="validateField('person_count')"
/>
<p v-if="errors.person_count" class="text-xs text-error mt-1" data-testid="error-person-count">{{ errors.person_count }}</p>
</div>
</div>
</fieldset>
<!-- Contact Details -->
<fieldset class="panel rounded-lg p-6">
<legend class="text-base font-bold text-text px-2">Kontaktdaten</legend>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4 mt-4">
<!-- Contact Name -->
<div>
<label for="contact_name" class="block text-sm font-medium text-text-muted mb-1">
Name <span class="text-accent">*</span>
</label>
<input
id="contact_name"
v-model="form.contact_name"
type="text"
class="w-full bg-surface border border-border-default rounded-md px-3 py-2.5 text-text placeholder:text-secondary focus:border-accent focus:outline-none transition-colors duration-fast"
:class="{ 'border-error': errors.contact_name }"
data-testid="form-contact-name"
placeholder="Vor- und Nachname"
@blur="validateField('contact_name')"
/>
<p v-if="errors.contact_name" class="text-xs text-error mt-1" data-testid="error-contact-name">{{ errors.contact_name }}</p>
</div>
<!-- Contact Company -->
<div>
<label for="contact_company" class="block text-sm font-medium text-text-muted mb-1">
Firma
</label>
<input
id="contact_company"
v-model="form.contact_company"
type="text"
class="w-full bg-surface border border-border-default rounded-md px-3 py-2.5 text-text placeholder:text-secondary focus:border-accent focus:outline-none transition-colors duration-fast"
data-testid="form-contact-company"
placeholder="Firmenname (optional)"
/>
</div>
<!-- Contact Email -->
<div>
<label for="contact_email" class="block text-sm font-medium text-text-muted mb-1">
E-Mail <span class="text-accent">*</span>
</label>
<input
id="contact_email"
v-model="form.contact_email"
type="email"
class="w-full bg-surface border border-border-default rounded-md px-3 py-2.5 text-text placeholder:text-secondary focus:border-accent focus:outline-none transition-colors duration-fast"
:class="{ 'border-error': errors.contact_email }"
data-testid="form-contact-email"
placeholder="ihre@email.de"
@blur="validateField('contact_email')"
/>
<p v-if="errors.contact_email" class="text-xs text-error mt-1" data-testid="error-contact-email">{{ errors.contact_email }}</p>
</div>
<!-- Contact Phone -->
<div>
<label for="contact_phone" class="block text-sm font-medium text-text-muted mb-1">
Telefon
</label>
<input
id="contact_phone"
v-model="form.contact_phone"
type="tel"
class="w-full bg-surface border border-border-default rounded-md px-3 py-2.5 text-text placeholder:text-secondary focus:border-accent focus:outline-none transition-colors duration-fast"
data-testid="form-contact-phone"
placeholder="+49 ..."
/>
</div>
<!-- Contact Street -->
<div class="sm:col-span-2">
<label for="contact_street" class="block text-sm font-medium text-text-muted mb-1">
Straße & Hausnummer
</label>
<input
id="contact_street"
v-model="form.contact_street"
type="text"
class="w-full bg-surface border border-border-default rounded-md px-3 py-2.5 text-text placeholder:text-secondary focus:border-accent focus:outline-none transition-colors duration-fast"
data-testid="form-contact-street"
placeholder="Musterstraße 1"
/>
</div>
<!-- Contact Postal Code -->
<div>
<label for="contact_postalcode" class="block text-sm font-medium text-text-muted mb-1">
PLZ
</label>
<input
id="contact_postalcode"
v-model="form.contact_postalcode"
type="text"
class="w-full bg-surface border border-border-default rounded-md px-3 py-2.5 text-text placeholder:text-secondary focus:border-accent focus:outline-none transition-colors duration-fast"
data-testid="form-contact-postalcode"
placeholder="89340"
/>
</div>
<!-- Contact City -->
<div>
<label for="contact_city" class="block text-sm font-medium text-text-muted mb-1">
Ort
</label>
<input
id="contact_city"
v-model="form.contact_city"
type="text"
class="w-full bg-surface border border-border-default rounded-md px-3 py-2.5 text-text placeholder:text-secondary focus:border-accent focus:outline-none transition-colors duration-fast"
data-testid="form-contact-city"
placeholder="Leipheim"
/>
</div>
</div>
</fieldset>
<!-- Message -->
<fieldset class="panel rounded-lg p-6">
<legend class="text-base font-bold text-text px-2">Anmerkungen</legend>
<div class="mt-4">
<label for="message" class="block text-sm font-medium text-text-muted mb-1">
Nachricht
</label>
<textarea
id="message"
v-model="form.message"
rows="4"
class="w-full bg-surface border border-border-default rounded-md px-3 py-2.5 text-text placeholder:text-secondary focus:border-accent focus:outline-none transition-colors duration-fast resize-y"
data-testid="form-message"
placeholder="Zusätzliche Informationen zu Ihrer Anfrage..."
/>
</div>
</fieldset>
<!-- Error Banner -->
<div
v-if="submitState === 'error'"
class="bg-error-bg border border-error/30 rounded-md p-4"
data-testid="mietanfrage-error"
role="alert"
>
<div class="flex items-start gap-3">
<svg class="w-5 h-5 text-error shrink-0 mt-0.5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v4m0 4h.01M4.93 19h14.14c1.54 0 2.5-1.67 1.73-3L13.73 4c-.77-1.33-2.69-1.33-3.46 0L3.2 16c-.77 1.33.19 3 1.73 3z" />
</svg>
<div>
<p class="text-sm font-medium text-error">Fehler beim Senden der Anfrage</p>
<p class="text-xs text-text-muted mt-1">{{ errorMessage }}</p>
</div>
</div>
</div>
<!-- Submit Button -->
<div class="flex items-center justify-end gap-4">
<NuxtLink to="/warenkorb" class="btn-secondary" data-testid="mietanfrage-back">
Zurück
</NuxtLink>
<button
type="submit"
class="btn-primary"
:disabled="submitState === 'submitting'"
data-testid="mietanfrage-submit"
>
<span v-if="submitState === 'submitting'" class="flex items-center gap-2">
<svg class="w-4 h-4 animate-spin" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0l3.181 3.183a8.25 8.25 0 0013.803-3.7M4.031 9.865a8.25 8.25 0 0113.803-3.7l3.181 3.182m0-4.991v4.99" />
</svg>
Wird gesendet...
</span>
<span v-else>Anfrage senden</span>
</button>
</div>
</form>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { useCart } from "~/composables/useCart";
import type { RentalRequestPayload, RentalRequestResponse } from "~/stores/cart";
const { items, totalCount, isEmpty, apiItems, clearCart } = useCart();
const api = useApi();
const submitState = ref<"form" | "submitting" | "success" | "error">("form");
const referenceNumber = ref("");
const errorMessage = ref("");
const form = reactive({
event_name: "",
date_start: "",
date_end: "",
location: "",
person_count: 0 as number,
contact_name: "",
contact_company: "",
contact_email: "",
contact_phone: "",
contact_street: "",
contact_postalcode: "",
contact_city: "",
message: "",
});
const errors = reactive<Record<string, string>>({});
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
function validateField(field: string): void {
switch (field) {
case "event_name":
errors.event_name = form.event_name.trim()
? ""
: "Bitte geben Sie den Namen der Veranstaltung ein";
break;
case "date_start":
errors.date_start = form.date_start
? ""
: "Bitte wählen Sie ein Startdatum";
break;
case "date_end":
if (!form.date_end) {
errors.date_end = "Bitte wählen Sie ein Enddatum";
} else if (form.date_start && form.date_end < form.date_start) {
errors.date_end = "Das Enddatum muss nach dem Startdatum liegen";
} else {
errors.date_end = "";
}
break;
case "location":
errors.location = form.location.trim()
? ""
: "Bitte geben Sie den Veranstaltungsort ein";
break;
case "person_count":
errors.person_count =
form.person_count && form.person_count > 0
? ""
: "Bitte geben Sie eine gültige Personenanzahl ein";
break;
case "contact_name":
errors.contact_name = form.contact_name.trim()
? ""
: "Bitte geben Sie Ihren Namen ein";
break;
case "contact_email":
if (!form.contact_email.trim()) {
errors.contact_email = "Bitte geben Sie Ihre E-Mail-Adresse ein";
} else if (!emailRegex.test(form.contact_email)) {
errors.contact_email = "Bitte geben Sie eine gültige E-Mail-Adresse ein";
} else {
errors.contact_email = "";
}
break;
}
}
function validateAll(): boolean {
const requiredFields = [
"event_name",
"date_start",
"date_end",
"location",
"person_count",
"contact_name",
"contact_email",
];
for (const field of requiredFields) {
validateField(field);
}
return Object.values(errors).every((e) => e === "" || e === undefined);
}
async function handleSubmit(): Promise<void> {
if (!validateAll()) {
return;
}
submitState.value = "submitting";
errorMessage.value = "";
const payload: RentalRequestPayload = {
event_name: form.event_name.trim(),
date_start: form.date_start,
date_end: form.date_end,
location: form.location.trim(),
person_count: form.person_count,
contact_name: form.contact_name.trim(),
contact_company: form.contact_company.trim(),
contact_email: form.contact_email.trim(),
contact_phone: form.contact_phone.trim(),
contact_street: form.contact_street.trim(),
contact_postalcode: form.contact_postalcode.trim(),
contact_city: form.contact_city.trim(),
message: form.message.trim(),
items: apiItems.value,
};
try {
const response = await api.post<RentalRequestPayload, RentalRequestResponse>(
"/api/rental-requests",
payload,
);
referenceNumber.value = response.reference_number;
submitState.value = "success";
clearCart();
} catch (err: unknown) {
submitState.value = "error";
if (err instanceof Error) {
errorMessage.value = err.message;
} else {
errorMessage.value =
"Ein unerwarteter Fehler ist aufgetreten. Bitte versuchen Sie es später erneut.";
}
}
}
useHead({
title: "Mietanfrage HMS Licht & Ton",
meta: [{ name: "robots", content: "noindex, nofollow, noarchive, nosnippet" }],
});
</script>