Route tenant admin PWA via /event-admin
This commit is contained in:
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Http\Controllers;
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Mail\ContactConfirmation;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\Mail;
|
use Illuminate\Support\Facades\Mail;
|
||||||
use Illuminate\Support\Facades\Log;
|
use Illuminate\Support\Facades\Log;
|
||||||
@@ -53,6 +54,8 @@ class MarketingController extends Controller
|
|||||||
->subject('Neue Kontakt-Anfrage');
|
->subject('Neue Kontakt-Anfrage');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Mail::to($request->email)->queue(new ContactConfirmation($request->name));
|
||||||
|
|
||||||
return redirect()->back()->with('success', 'Nachricht gesendet!');
|
return redirect()->back()->with('success', 'Nachricht gesendet!');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,7 +112,7 @@ class MarketingController extends Controller
|
|||||||
'refunded' => false,
|
'refunded' => false,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return redirect('/admin')->with('success', __('marketing.packages.free_assigned'));
|
return redirect('/event-admin')->with('success', __('marketing.packages.free_assigned'));
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($package->type === 'reseller') {
|
if ($package->type === 'reseller') {
|
||||||
@@ -338,13 +341,13 @@ class MarketingController extends Controller
|
|||||||
$request->session()->flash('error', 'Zahlung konnte nicht abgeschlossen werden.');
|
$request->session()->flash('error', 'Zahlung konnte nicht abgeschlossen werden.');
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
Log::error('PayPal success error: ' . $e->getMessage());
|
Log::error('PayPal success error: ' . $e->getMessage());
|
||||||
$request->session()->flash('error', 'Fehler beim Abschließen der Zahlung.');
|
$request->session()->flash('error', 'Fehler beim Abschliessen der Zahlung.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Common logic: Redirect to admin if verified
|
// Common logic: Redirect to admin if verified
|
||||||
if (Auth::check() && Auth::user()->email_verified_at) {
|
if (Auth::check() && Auth::user()->email_verified_at) {
|
||||||
return redirect('/admin')->with('success', __('marketing.success.welcome'));
|
return redirect('/event-admin')->with('success', __('marketing.success.welcome'));
|
||||||
}
|
}
|
||||||
|
|
||||||
return Inertia::render('marketing/Success', compact('packageId'));
|
return Inertia::render('marketing/Success', compact('packageId'));
|
||||||
@@ -434,3 +437,4 @@ class MarketingController extends Controller
|
|||||||
return Inertia::render('marketing/Occasions', ['type' => $type]);
|
return Inertia::render('marketing/Occasions', ['type' => $type]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -523,16 +523,16 @@ class OAuthController extends Controller
|
|||||||
$error = $request->get('error');
|
$error = $request->get('error');
|
||||||
|
|
||||||
if ($error) {
|
if ($error) {
|
||||||
return redirect('/admin')->with('error', 'Stripe connection failed: '.$error);
|
return redirect('/event-admin')->with('error', 'Stripe connection failed: '.$error);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (! $code || ! $state) {
|
if (! $code || ! $state) {
|
||||||
return redirect('/admin')->with('error', 'Invalid callback parameters');
|
return redirect('/event-admin')->with('error', 'Invalid callback parameters');
|
||||||
}
|
}
|
||||||
|
|
||||||
$sessionState = session('stripe_state');
|
$sessionState = session('stripe_state');
|
||||||
if (! hash_equals($state, (string) $sessionState)) {
|
if (! hash_equals($state, (string) $sessionState)) {
|
||||||
return redirect('/admin')->with('error', 'Invalid state parameter');
|
return redirect('/event-admin')->with('error', 'Invalid state parameter');
|
||||||
}
|
}
|
||||||
|
|
||||||
$client = new Client();
|
$client = new Client();
|
||||||
@@ -554,7 +554,7 @@ class OAuthController extends Controller
|
|||||||
$tokenData = json_decode($response->getBody()->getContents(), true);
|
$tokenData = json_decode($response->getBody()->getContents(), true);
|
||||||
|
|
||||||
if (! isset($tokenData['stripe_user_id'])) {
|
if (! isset($tokenData['stripe_user_id'])) {
|
||||||
return redirect('/admin')->with('error', 'Failed to connect Stripe account');
|
return redirect('/event-admin')->with('error', 'Failed to connect Stripe account');
|
||||||
}
|
}
|
||||||
|
|
||||||
$tenant = Tenant::find(session('tenant_id'));
|
$tenant = Tenant::find(session('tenant_id'));
|
||||||
@@ -563,10 +563,11 @@ class OAuthController extends Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
session()->forget(['stripe_state', 'tenant_id']);
|
session()->forget(['stripe_state', 'tenant_id']);
|
||||||
return redirect('/admin')->with('success', 'Stripe account connected successfully');
|
return redirect('/event-admin')->with('success', 'Stripe account connected successfully');
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
Log::error('Stripe OAuth error: '.$e->getMessage());
|
Log::error('Stripe OAuth error: '.$e->getMessage());
|
||||||
return redirect('/admin')->with('error', 'Connection error: '.$e->getMessage());
|
return redirect('/event-admin')->with('error', 'Connection error: '.$e->getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ Diese Dokumentation beschreibt alle API-Endpunkte, die die Tenant Admin App mit
|
|||||||
- **Response**: Neuer Access/Refresh-Token
|
- **Response**: Neuer Access/Refresh-Token
|
||||||
|
|
||||||
- **Token Validation**: `GET /api/v1/tenant/me`
|
- **Token Validation**: `GET /api/v1/tenant/me`
|
||||||
- **Redirect URI**: Standardmaessig `${origin}/admin/auth/callback` (per Vite-Env anpassbar)
|
- **Redirect URI**: Standardmaessig `${origin}/event-admin/auth/callback` (per Vite-Env anpassbar)
|
||||||
- **Headers**: `Authorization: Bearer {access_token}`
|
- **Headers**: `Authorization: Bearer {access_token}`
|
||||||
- **Response**: `{ id, email, tenant_id, role, name }`
|
- **Response**: `{ id, email, tenant_id, role, name }`
|
||||||
|
|
||||||
@@ -272,3 +272,4 @@ curl -H "Authorization: Bearer {token}" \
|
|||||||
|
|
||||||
Für weitere Details siehe die spezifischen Dokumentationsdateien.
|
Für weitere Details siehe die spezifischen Dokumentationsdateien.
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -121,7 +121,7 @@ packages/mobile/ # Shared Native-Config (optional)
|
|||||||
- **Privacy**: Usage Descriptions in Info.plist (z.B. "Kamera für QR-Scans").
|
- **Privacy**: Usage Descriptions in Info.plist (z.B. "Kamera für QR-Scans").
|
||||||
|
|
||||||
- **PWA-Fallback** (Web):
|
- **PWA-Fallback** (Web):
|
||||||
- **manifest.json**: `start_url: '/admin/'`, `display: 'standalone'`.
|
- **manifest.json**: `start_url: '/event-admin/'`, `display: 'standalone'`.
|
||||||
- **Service Worker**: Caching von Assets; Background Sync für Mutations.
|
- **Service Worker**: Caching von Assets; Background Sync für Mutations.
|
||||||
- **Distribution**: Hosting auf `admin.fotospiel.app` mit A2HS-Prompt.
|
- **Distribution**: Hosting auf `admin.fotospiel.app` mit A2HS-Prompt.
|
||||||
|
|
||||||
@@ -152,4 +152,4 @@ packages/mobile/ # Shared Native-Config (optional)
|
|||||||
- **Security**: HTTPS-only; Token-Rotation alle 24h; No Jailbreak-Detection.
|
- **Security**: HTTPS-only; Token-Rotation alle 24h; No Jailbreak-Detection.
|
||||||
- **Performance**: Bundle-Size < 10MB (Web-Assets komprimiert); Lazy-Loading.
|
- **Performance**: Bundle-Size < 10MB (Web-Assets komprimiert); Lazy-Loading.
|
||||||
|
|
||||||
Diese Setup ergänzt die funktionalen Specs und UI-Beschreibungen. Für Repo-Integration siehe ADR-0006.
|
Diese Setup ergänzt die funktionalen Specs und UI-Beschreibungen. Für Repo-Integration siehe ADR-0006.
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { generateCodeChallenge, generateCodeVerifier, generateState } from './pkce';
|
import { generateCodeChallenge, generateCodeVerifier, generateState } from './pkce';
|
||||||
import { decodeStoredTokens } from './utils';
|
import { decodeStoredTokens } from './utils';
|
||||||
|
import { ADMIN_AUTH_CALLBACK_PATH } from '../constants';
|
||||||
|
|
||||||
const TOKEN_STORAGE_KEY = 'tenant_oauth_tokens.v1';
|
const TOKEN_STORAGE_KEY = 'tenant_oauth_tokens.v1';
|
||||||
const CODE_VERIFIER_KEY = 'tenant_oauth_code_verifier';
|
const CODE_VERIFIER_KEY = 'tenant_oauth_code_verifier';
|
||||||
@@ -18,7 +19,7 @@ function getClientId(): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function buildRedirectUri(): string {
|
function buildRedirectUri(): string {
|
||||||
return new URL('/admin/auth/callback', window.location.origin).toString();
|
return new URL(ADMIN_AUTH_CALLBACK_PATH, window.location.origin).toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
export class AuthError extends Error {
|
export class AuthError extends Error {
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { NavLink } from 'react-router-dom';
|
import { NavLink } from 'react-router-dom';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
import { ADMIN_EVENTS_PATH, ADMIN_SETTINGS_PATH } from '../constants';
|
||||||
|
|
||||||
const navItems = [
|
const navItems = [
|
||||||
{ to: '/admin/events', label: 'Events' },
|
{ to: ADMIN_EVENTS_PATH, label: 'Events' },
|
||||||
{ to: '/admin/settings', label: 'Einstellungen' },
|
{ to: ADMIN_SETTINGS_PATH, label: 'Einstellungen' },
|
||||||
];
|
];
|
||||||
|
|
||||||
interface AdminLayoutProps {
|
interface AdminLayoutProps {
|
||||||
@@ -58,3 +59,4 @@ export function AdminLayout({ title, subtitle, actions, children }: AdminLayoutP
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
9
resources/js/admin/constants.ts
Normal file
9
resources/js/admin/constants.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
export const ADMIN_BASE_PATH = '/event-admin';
|
||||||
|
|
||||||
|
export const adminPath = (suffix = ''): string => `${ADMIN_BASE_PATH}${suffix}`;
|
||||||
|
|
||||||
|
export const ADMIN_HOME_PATH = ADMIN_BASE_PATH;
|
||||||
|
export const ADMIN_LOGIN_PATH = adminPath('/login');
|
||||||
|
export const ADMIN_AUTH_CALLBACK_PATH = adminPath('/auth/callback');
|
||||||
|
export const ADMIN_EVENTS_PATH = adminPath('/events');
|
||||||
|
export const ADMIN_SETTINGS_PATH = adminPath('/settings');
|
||||||
@@ -2,6 +2,7 @@ import React from 'react';
|
|||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { useAuth } from '../auth/context';
|
import { useAuth } from '../auth/context';
|
||||||
import { isAuthError } from '../auth/tokens';
|
import { isAuthError } from '../auth/tokens';
|
||||||
|
import { ADMIN_HOME_PATH } from '../constants';
|
||||||
|
|
||||||
export default function AuthCallbackPage() {
|
export default function AuthCallbackPage() {
|
||||||
const { completeLogin } = useAuth();
|
const { completeLogin } = useAuth();
|
||||||
@@ -12,7 +13,7 @@ export default function AuthCallbackPage() {
|
|||||||
const params = new URLSearchParams(window.location.search);
|
const params = new URLSearchParams(window.location.search);
|
||||||
completeLogin(params)
|
completeLogin(params)
|
||||||
.then((redirectTo) => {
|
.then((redirectTo) => {
|
||||||
navigate(redirectTo ?? '/admin', { replace: true });
|
navigate(redirectTo ?? ADMIN_HOME_PATH, { replace: true });
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
console.error('[Auth] Callback processing failed', err);
|
console.error('[Auth] Callback processing failed', err);
|
||||||
@@ -33,3 +34,4 @@ export default function AuthCallbackPage() {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/com
|
|||||||
import { AdminLayout } from '../components/AdminLayout';
|
import { AdminLayout } from '../components/AdminLayout';
|
||||||
import { createInviteLink, getEvent, getEventStats, TenantEvent, EventStats as TenantEventStats, toggleEvent } from '../api';
|
import { createInviteLink, getEvent, getEventStats, TenantEvent, EventStats as TenantEventStats, toggleEvent } from '../api';
|
||||||
import { isAuthError } from '../auth/tokens';
|
import { isAuthError } from '../auth/tokens';
|
||||||
|
import { adminPath } from '../constants';
|
||||||
|
|
||||||
interface State {
|
interface State {
|
||||||
event: TenantEvent | null;
|
event: TenantEvent | null;
|
||||||
@@ -105,7 +106,7 @@ export default function EventDetailPage() {
|
|||||||
<>
|
<>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => navigate('/admin/events')}
|
onClick={() => navigate(adminPath('/events'))}
|
||||||
className="border-pink-200 text-pink-600 hover:bg-pink-50"
|
className="border-pink-200 text-pink-600 hover:bg-pink-50"
|
||||||
>
|
>
|
||||||
<ArrowLeft className="h-4 w-4" /> Zurueck zur Liste
|
<ArrowLeft className="h-4 w-4" /> Zurueck zur Liste
|
||||||
@@ -113,7 +114,7 @@ export default function EventDetailPage() {
|
|||||||
{event && (
|
{event && (
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => navigate(`/admin/events/edit?slug=${encodeURIComponent(event.slug)}`)}
|
onClick={() => navigate(adminPath(`/events/edit?slug=${encodeURIComponent(event.slug)}`))}
|
||||||
className="border-fuchsia-200 text-fuchsia-700 hover:bg-fuchsia-50"
|
className="border-fuchsia-200 text-fuchsia-700 hover:bg-fuchsia-50"
|
||||||
>
|
>
|
||||||
Bearbeiten
|
Bearbeiten
|
||||||
@@ -176,7 +177,7 @@ export default function EventDetailPage() {
|
|||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => navigate(`/admin/events/photos?slug=${encodeURIComponent(event.slug)}`)}
|
onClick={() => navigate(adminPath(`/events/photos?slug=${encodeURIComponent(event.slug)}`))}
|
||||||
className="border-sky-200 text-sky-700 hover:bg-sky-50"
|
className="border-sky-200 text-sky-700 hover:bg-sky-50"
|
||||||
>
|
>
|
||||||
<Camera className="h-4 w-4" /> Fotos moderieren
|
<Camera className="h-4 w-4" /> Fotos moderieren
|
||||||
@@ -280,3 +281,4 @@ function renderName(name: TenantEvent['name']): string {
|
|||||||
}
|
}
|
||||||
return 'Unbenanntes Event';
|
return 'Unbenanntes Event';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, Di
|
|||||||
import { AdminLayout } from '../components/AdminLayout';
|
import { AdminLayout } from '../components/AdminLayout';
|
||||||
import { createEvent, getEvent, updateEvent, getPackages } from '../api';
|
import { createEvent, getEvent, updateEvent, getPackages } from '../api';
|
||||||
import { isAuthError } from '../auth/tokens';
|
import { isAuthError } from '../auth/tokens';
|
||||||
|
import { adminPath } from '../constants';
|
||||||
|
|
||||||
interface EventFormState {
|
interface EventFormState {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -129,10 +130,10 @@ export default function EventFormPage() {
|
|||||||
const targetSlug = originalSlug ?? slugParam!;
|
const targetSlug = originalSlug ?? slugParam!;
|
||||||
const updated = await updateEvent(targetSlug, payload);
|
const updated = await updateEvent(targetSlug, payload);
|
||||||
setOriginalSlug(updated.slug);
|
setOriginalSlug(updated.slug);
|
||||||
navigate(`/admin/events/view?slug=${encodeURIComponent(updated.slug)}`);
|
navigate(adminPath(`/events/view?slug=${encodeURIComponent(updated.slug)}`));
|
||||||
} else {
|
} else {
|
||||||
const { event: created } = await createEvent(payload);
|
const { event: created } = await createEvent(payload);
|
||||||
navigate(`/admin/events/view?slug=${encodeURIComponent(created.slug)}`);
|
navigate(adminPath(`/events/view?slug=${encodeURIComponent(created.slug)}`));
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (!isAuthError(err)) {
|
if (!isAuthError(err)) {
|
||||||
@@ -146,7 +147,7 @@ export default function EventFormPage() {
|
|||||||
const actions = (
|
const actions = (
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => navigate('/admin/events')}
|
onClick={() => navigate(adminPath('/events'))}
|
||||||
className="border-pink-200 text-pink-600 hover:bg-pink-50"
|
className="border-pink-200 text-pink-600 hover:bg-pink-50"
|
||||||
>
|
>
|
||||||
<ArrowLeft className="h-4 w-4" /> Zurueck zur Liste
|
<ArrowLeft className="h-4 w-4" /> Zurueck zur Liste
|
||||||
@@ -213,38 +214,38 @@ export default function EventFormPage() {
|
|||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="package_id">Package</Label>
|
<Label htmlFor="package_id">Package</Label>
|
||||||
<Select value={form.package_id.toString()} onValueChange={(value) => setForm((prev) => ({ ...prev, package_id: parseInt(value) }))}>
|
<Select value={form.package_id.toString()} onValueChange={(value) => setForm((prev) => ({ ...prev, package_id: parseInt(value) }))}>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
<SelectValue placeholder="Wählen Sie ein Package" />
|
<SelectValue placeholder="Waehlen Sie ein Package" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{packagesLoading ? (
|
{packagesLoading ? (
|
||||||
<SelectItem value="">Laden...</SelectItem>
|
<SelectItem value="">Laden...</SelectItem>
|
||||||
) : (
|
) : (
|
||||||
packages?.map((pkg) => (
|
packages?.map((pkg) => (
|
||||||
<SelectItem key={pkg.id} value={pkg.id.toString()}>
|
<SelectItem key={pkg.id} value={pkg.id.toString()}>
|
||||||
{pkg.name} - {pkg.price} € ({pkg.max_photos} Fotos)
|
{pkg.name} - {pkg.price} EUR ({pkg.max_photos} Fotos)
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
<Dialog>
|
<Dialog>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button variant="outline" size="sm">Package-Details</Button>
|
<Button variant="outline" size="sm">Package-Details</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Package auswählen</DialogTitle>
|
<DialogTitle>Package auswaehlen</DialogTitle>
|
||||||
<DialogDescription>Wählen Sie das Package für Ihr Event. Höhere Packages bieten mehr Limits und Features.</DialogDescription>
|
<DialogDescription>Waehlen Sie das Package fuer Ihr Event. Hoehere Packages bieten mehr Limits und Features.</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{packages?.map((pkg) => (
|
{packages?.map((pkg) => (
|
||||||
<div key={pkg.id} className="p-4 border rounded">
|
<div key={pkg.id} className="p-4 border rounded">
|
||||||
<h3 className="font-semibold">{pkg.name}</h3>
|
<h3 className="font-semibold">{pkg.name}</h3>
|
||||||
<p>{pkg.price} €</p>
|
<p>{pkg.price} EUR</p>
|
||||||
<ul className="text-sm">
|
<ul className="text-sm">
|
||||||
<li>Max Fotos: {pkg.max_photos}</li>
|
<li>Max Fotos: {pkg.max_photos}</li>
|
||||||
<li>Max Gäste: {pkg.max_guests}</li>
|
<li>Max Gaeste: {pkg.max_guests}</li>
|
||||||
<li>Galerie: {pkg.gallery_days} Tage</li>
|
<li>Galerie: {pkg.gallery_days} Tage</li>
|
||||||
<li>Features: {Object.keys(pkg.features).filter(k => pkg.features[k]).join(', ')}</li>
|
<li>Features: {Object.keys(pkg.features).filter(k => pkg.features[k]).join(', ')}</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/com
|
|||||||
import { AdminLayout } from '../components/AdminLayout';
|
import { AdminLayout } from '../components/AdminLayout';
|
||||||
import { deletePhoto, featurePhoto, getEventPhotos, TenantPhoto, unfeaturePhoto } from '../api';
|
import { deletePhoto, featurePhoto, getEventPhotos, TenantPhoto, unfeaturePhoto } from '../api';
|
||||||
import { isAuthError } from '../auth/tokens';
|
import { isAuthError } from '../auth/tokens';
|
||||||
|
import { adminPath } from '../constants';
|
||||||
|
|
||||||
export default function EventPhotosPage() {
|
export default function EventPhotosPage() {
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
@@ -81,7 +82,7 @@ export default function EventPhotosPage() {
|
|||||||
<Card className="border-0 bg-white/85 shadow-xl shadow-pink-100/60">
|
<Card className="border-0 bg-white/85 shadow-xl shadow-pink-100/60">
|
||||||
<CardContent className="p-6 text-sm text-slate-600">
|
<CardContent className="p-6 text-sm text-slate-600">
|
||||||
Kein Slug in der URL gefunden. Kehre zur Event-Liste zurueck und waehle dort ein Event aus.
|
Kein Slug in der URL gefunden. Kehre zur Event-Liste zurueck und waehle dort ein Event aus.
|
||||||
<Button className="mt-4" onClick={() => navigate('/admin/events')}>
|
<Button className="mt-4" onClick={() => navigate(adminPath('/events'))}>
|
||||||
Zurueck zur Liste
|
Zurueck zur Liste
|
||||||
</Button>
|
</Button>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
@@ -93,7 +94,7 @@ export default function EventPhotosPage() {
|
|||||||
const actions = (
|
const actions = (
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => navigate(`/admin/events/view-slug=${encodeURIComponent(slug)}`)}
|
onClick={() => navigate(adminPath(`/events/view?slug=${encodeURIComponent(slug)}`))}
|
||||||
className="border-pink-200 text-pink-600 hover:bg-pink-50"
|
className="border-pink-200 text-pink-600 hover:bg-pink-50"
|
||||||
>
|
>
|
||||||
Zurueck zum Event
|
Zurueck zum Event
|
||||||
@@ -197,3 +198,4 @@ function EmptyGallery() {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/com
|
|||||||
import { AdminLayout } from '../components/AdminLayout';
|
import { AdminLayout } from '../components/AdminLayout';
|
||||||
import { getEvents, TenantEvent, getPackages } from '../api';
|
import { getEvents, TenantEvent, getPackages } from '../api';
|
||||||
import { isAuthError } from '../auth/tokens';
|
import { isAuthError } from '../auth/tokens';
|
||||||
|
import { adminPath, ADMIN_SETTINGS_PATH } from '../constants';
|
||||||
|
|
||||||
export default function EventsPage() {
|
export default function EventsPage() {
|
||||||
const [rows, setRows] = React.useState<TenantEvent[]>([]);
|
const [rows, setRows] = React.useState<TenantEvent[]>([]);
|
||||||
@@ -41,11 +42,11 @@ export default function EventsPage() {
|
|||||||
<>
|
<>
|
||||||
<Button
|
<Button
|
||||||
className="bg-gradient-to-r from-pink-500 via-fuchsia-500 to-purple-500 text-white shadow-lg shadow-pink-500/20"
|
className="bg-gradient-to-r from-pink-500 via-fuchsia-500 to-purple-500 text-white shadow-lg shadow-pink-500/20"
|
||||||
onClick={() => navigate('/admin/events/new')}
|
onClick={() => navigate(adminPath('/events/new'))}
|
||||||
>
|
>
|
||||||
<Plus className="h-4 w-4" /> Neues Event
|
<Plus className="h-4 w-4" /> Neues Event
|
||||||
</Button>
|
</Button>
|
||||||
<Link to="/admin/settings">
|
<Link to={ADMIN_SETTINGS_PATH}>
|
||||||
<Button variant="outline" className="border-pink-200 text-pink-600 hover:bg-pink-50">
|
<Button variant="outline" className="border-pink-200 text-pink-600 hover:bg-pink-50">
|
||||||
<Settings className="h-4 w-4" /> Einstellungen
|
<Settings className="h-4 w-4" /> Einstellungen
|
||||||
</Button>
|
</Button>
|
||||||
@@ -111,7 +112,7 @@ export default function EventsPage() {
|
|||||||
{loading ? (
|
{loading ? (
|
||||||
<LoadingState />
|
<LoadingState />
|
||||||
) : rows.length === 0 ? (
|
) : rows.length === 0 ? (
|
||||||
<EmptyState onCreate={() => navigate('/admin/events/new')} />
|
<EmptyState onCreate={() => navigate(adminPath('/events/new'))} />
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{rows.map((event) => (
|
{rows.map((event) => (
|
||||||
@@ -163,15 +164,15 @@ function EventCard({ event }: { event: TenantEvent }) {
|
|||||||
|
|
||||||
<div className="mt-4 flex flex-wrap gap-2">
|
<div className="mt-4 flex flex-wrap gap-2">
|
||||||
<Button asChild variant="outline" className="border-pink-200 text-pink-700 hover:bg-pink-50">
|
<Button asChild variant="outline" className="border-pink-200 text-pink-700 hover:bg-pink-50">
|
||||||
<Link to={`/admin/events/view?slug=${encodeURIComponent(slug)}`}>
|
<Link to={adminPath(`/events/view?slug=${encodeURIComponent(slug)}`)}>
|
||||||
Details <ArrowRight className="ml-1 h-3.5 w-3.5" />
|
Details <ArrowRight className="ml-1 h-3.5 w-3.5" />
|
||||||
</Link>
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
<Button asChild variant="outline" className="border-fuchsia-200 text-fuchsia-700 hover:bg-fuchsia-50">
|
<Button asChild variant="outline" className="border-fuchsia-200 text-fuchsia-700 hover:bg-fuchsia-50">
|
||||||
<Link to={`/admin/events/edit?slug=${encodeURIComponent(slug)}`}>Bearbeiten</Link>
|
<Link to={adminPath(`/events/edit?slug=${encodeURIComponent(slug)}`)}>Bearbeiten</Link>
|
||||||
</Button>
|
</Button>
|
||||||
<Button asChild variant="outline" className="border-sky-200 text-sky-700 hover:bg-sky-50">
|
<Button asChild variant="outline" className="border-sky-200 text-sky-700 hover:bg-sky-50">
|
||||||
<Link to={`/admin/events/photos?slug=${encodeURIComponent(slug)}`}>Fotos moderieren</Link>
|
<Link to={adminPath(`/events/photos?slug=${encodeURIComponent(slug)}`)}>Fotos moderieren</Link>
|
||||||
</Button>
|
</Button>
|
||||||
<Button asChild variant="outline" className="border-amber-200 text-amber-600 hover:bg-amber-50">
|
<Button asChild variant="outline" className="border-amber-200 text-amber-600 hover:bg-amber-50">
|
||||||
<a href={`/e/${slug}`} target="_blank" rel="noreferrer">
|
<a href={`/e/${slug}`} target="_blank" rel="noreferrer">
|
||||||
@@ -238,3 +239,4 @@ function renderName(name: TenantEvent['name']): string {
|
|||||||
}
|
}
|
||||||
return 'Unbenanntes Event';
|
return 'Unbenanntes Event';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { Location, useLocation, useNavigate } from 'react-router-dom';
|
|||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import AppearanceToggleDropdown from '@/components/appearance-dropdown';
|
import AppearanceToggleDropdown from '@/components/appearance-dropdown';
|
||||||
import { useAuth } from '../auth/context';
|
import { useAuth } from '../auth/context';
|
||||||
|
import { ADMIN_HOME_PATH } from '../constants';
|
||||||
|
|
||||||
interface LocationState {
|
interface LocationState {
|
||||||
from?: Location;
|
from?: Location;
|
||||||
@@ -17,7 +18,7 @@ export default function LoginPage() {
|
|||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (status === 'authenticated') {
|
if (status === 'authenticated') {
|
||||||
navigate('/admin', { replace: true });
|
navigate(ADMIN_HOME_PATH, { replace: true });
|
||||||
}
|
}
|
||||||
}, [status, navigate]);
|
}, [status, navigate]);
|
||||||
|
|
||||||
@@ -29,7 +30,7 @@ export default function LoginPage() {
|
|||||||
const hash = from.hash ?? '';
|
const hash = from.hash ?? '';
|
||||||
return `${from.pathname}${search}${hash}`;
|
return `${from.pathname}${search}${hash}`;
|
||||||
}
|
}
|
||||||
return '/admin';
|
return ADMIN_HOME_PATH;
|
||||||
}, [location.state]);
|
}, [location.state]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -41,7 +42,7 @@ export default function LoginPage() {
|
|||||||
<div className="space-y-4 text-sm text-muted-foreground">
|
<div className="space-y-4 text-sm text-muted-foreground">
|
||||||
<p>
|
<p>
|
||||||
Melde dich mit deinem Fotospiel-Account an. Du wirst zur sicheren OAuth-Anmeldung weitergeleitet und danach
|
Melde dich mit deinem Fotospiel-Account an. Du wirst zur sicheren OAuth-Anmeldung weitergeleitet und danach
|
||||||
wieder zur Admin-Oberfl<EFBFBD>che gebracht.
|
wieder zur Admin-Oberflaeche gebracht.
|
||||||
</p>
|
</p>
|
||||||
{oauthError && (
|
{oauthError && (
|
||||||
<div className="rounded border border-red-300 bg-red-50 p-2 text-sm text-red-700">
|
<div className="rounded border border-red-300 bg-red-50 p-2 text-sm text-red-700">
|
||||||
@@ -53,9 +54,10 @@ export default function LoginPage() {
|
|||||||
disabled={status === 'loading'}
|
disabled={status === 'loading'}
|
||||||
onClick={() => login(redirectTarget)}
|
onClick={() => login(redirectTarget)}
|
||||||
>
|
>
|
||||||
{status === 'loading' ? 'Bitte warten <EFBFBD>' : 'Mit Tenant-Account anmelden'}
|
{status === 'loading' ? 'Bitte warten ...' : 'Mit Tenant-Account anmelden'}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,19 +8,20 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/com
|
|||||||
|
|
||||||
import { AdminLayout } from '../components/AdminLayout';
|
import { AdminLayout } from '../components/AdminLayout';
|
||||||
import { useAuth } from '../auth/context';
|
import { useAuth } from '../auth/context';
|
||||||
|
import { ADMIN_EVENTS_PATH, ADMIN_LOGIN_PATH } from '../constants';
|
||||||
|
|
||||||
export default function SettingsPage() {
|
export default function SettingsPage() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { user, logout } = useAuth();
|
const { user, logout } = useAuth();
|
||||||
|
|
||||||
function handleLogout() {
|
function handleLogout() {
|
||||||
logout({ redirect: '/admin/login' });
|
logout({ redirect: ADMIN_LOGIN_PATH });
|
||||||
}
|
}
|
||||||
|
|
||||||
const actions = (
|
const actions = (
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => navigate('/admin/events')}
|
onClick={() => navigate(ADMIN_EVENTS_PATH)}
|
||||||
className="border-pink-200 text-pink-600 hover:bg-pink-50"
|
className="border-pink-200 text-pink-600 hover:bg-pink-50"
|
||||||
>
|
>
|
||||||
Zurueck zur Uebersicht
|
Zurueck zur Uebersicht
|
||||||
@@ -77,3 +78,4 @@ export default function SettingsPage() {
|
|||||||
</AdminLayout>
|
</AdminLayout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import EventPhotosPage from './pages/EventPhotosPage';
|
|||||||
import EventDetailPage from './pages/EventDetailPage';
|
import EventDetailPage from './pages/EventDetailPage';
|
||||||
import AuthCallbackPage from './pages/AuthCallbackPage';
|
import AuthCallbackPage from './pages/AuthCallbackPage';
|
||||||
import { useAuth } from './auth/context';
|
import { useAuth } from './auth/context';
|
||||||
|
import { ADMIN_AUTH_CALLBACK_PATH, ADMIN_BASE_PATH, ADMIN_LOGIN_PATH } from './constants';
|
||||||
|
|
||||||
function RequireAuth() {
|
function RequireAuth() {
|
||||||
const { status } = useAuth();
|
const { status } = useAuth();
|
||||||
@@ -16,23 +17,23 @@ function RequireAuth() {
|
|||||||
if (status === 'loading') {
|
if (status === 'loading') {
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-screen items-center justify-center text-sm text-muted-foreground">
|
<div className="flex min-h-screen items-center justify-center text-sm text-muted-foreground">
|
||||||
Bitte warten <EFBFBD>
|
Bitte warten ...
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (status === 'unauthenticated') {
|
if (status === 'unauthenticated') {
|
||||||
return <Navigate to="/admin/login" state={{ from: location }} replace />;
|
return <Navigate to={ADMIN_LOGIN_PATH} state={{ from: location }} replace />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <Outlet />;
|
return <Outlet />;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const router = createBrowserRouter([
|
export const router = createBrowserRouter([
|
||||||
{ path: '/admin/login', element: <LoginPage /> },
|
{ path: ADMIN_LOGIN_PATH, element: <LoginPage /> },
|
||||||
{ path: '/admin/auth/callback', element: <AuthCallbackPage /> },
|
{ path: ADMIN_AUTH_CALLBACK_PATH, element: <AuthCallbackPage /> },
|
||||||
{
|
{
|
||||||
path: '/admin',
|
path: ADMIN_BASE_PATH,
|
||||||
element: <RequireAuth />,
|
element: <RequireAuth />,
|
||||||
children: [
|
children: [
|
||||||
{ index: true, element: <EventsPage /> },
|
{ index: true, element: <EventsPage /> },
|
||||||
@@ -44,5 +45,7 @@ export const router = createBrowserRouter([
|
|||||||
{ path: 'settings', element: <SettingsPage /> },
|
{ path: 'settings', element: <SettingsPage /> },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{ path: '*', element: <Navigate to="/admin" replace /> },
|
{ path: '*', element: <Navigate to={ADMIN_BASE_PATH} replace /> },
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { usePage, router } from '@inertiajs/react';
|
import { usePage, router } from '@inertiajs/react';
|
||||||
import { Head } from '@inertiajs/react';
|
import { Head } from '@inertiajs/react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import MarketingLayout from '@/layouts/mainWebsite';
|
import MarketingLayout from '@/layouts/mainWebsite';
|
||||||
import { Loader } from 'lucide-react';
|
import { Loader } from 'lucide-react';
|
||||||
import { useLocalizedRoutes } from '@/hooks/useLocalizedRoutes';
|
import { useLocalizedRoutes } from '@/hooks/useLocalizedRoutes';
|
||||||
|
import { ADMIN_HOME_PATH } from '@/admin/constants';
|
||||||
|
|
||||||
const Success: React.FC = () => {
|
const Success: React.FC = () => {
|
||||||
const { auth } = usePage().props as any;
|
const { auth } = usePage().props as any;
|
||||||
@@ -12,7 +13,7 @@ const Success: React.FC = () => {
|
|||||||
const { localizedPath } = useLocalizedRoutes();
|
const { localizedPath } = useLocalizedRoutes();
|
||||||
|
|
||||||
if (auth.user && auth.user.email_verified_at) {
|
if (auth.user && auth.user.email_verified_at) {
|
||||||
router.visit('/admin', { preserveState: false });
|
router.visit(ADMIN_HOME_PATH, { preserveState: false });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-center min-h-screen bg-gray-50">
|
<div className="flex items-center justify-center min-h-screen bg-gray-50">
|
||||||
@@ -79,4 +80,4 @@ const Success: React.FC = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Success;
|
export default Success;
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
@auth
|
@auth
|
||||||
@if(auth()->user()->email_verified_at)
|
@if(auth()->user()->email_verified_at)
|
||||||
<script>
|
<script>
|
||||||
window.location.href = '/admin';
|
window.location.href = '/event-admin';
|
||||||
</script>
|
</script>
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<div class="spinner-border animate-spin inline-block w-8 h-8 border border-2 border-blue-600 border-t-transparent rounded-full" role="status">
|
<div class="spinner-border animate-spin inline-block w-8 h-8 border border-2 border-blue-600 border-t-transparent rounded-full" role="status">
|
||||||
@@ -55,4 +55,4 @@
|
|||||||
</div>
|
</div>
|
||||||
@endauth
|
@endauth
|
||||||
</div>
|
</div>
|
||||||
@endsection
|
@endsection
|
||||||
|
|||||||
@@ -28,11 +28,13 @@ Route::get('/impressum', function () {
|
|||||||
Route::get('/kontakt', function () {
|
Route::get('/kontakt', function () {
|
||||||
return Inertia::render('marketing/Kontakt');
|
return Inertia::render('marketing/Kontakt');
|
||||||
})->name('kontakt');
|
})->name('kontakt');
|
||||||
|
Route::post('/kontakt', [MarketingController::class, 'contact'])->name('kontakt.submit');
|
||||||
Route::get('/blog', [MarketingController::class, 'blogIndex'])->name('blog');
|
Route::get('/blog', [MarketingController::class, 'blogIndex'])->name('blog');
|
||||||
Route::get('/blog/{slug}', [MarketingController::class, 'blogShow'])->name('blog.show');
|
Route::get('/blog/{slug}', [MarketingController::class, 'blogShow'])->name('blog.show');
|
||||||
Route::get('/packages', [MarketingController::class, 'packagesIndex'])->name('packages');
|
Route::get('/packages', [MarketingController::class, 'packagesIndex'])->name('packages');
|
||||||
Route::get('/anlaesse/{type}', [MarketingController::class, 'occasionsType'])->name('anlaesse.type');
|
Route::get('/anlaesse/{type}', [MarketingController::class, 'occasionsType'])->name('anlaesse.type');
|
||||||
Route::get('/success/{packageId?}', [MarketingController::class, 'success'])->name('marketing.success');
|
Route::get('/success/{packageId?}', [MarketingController::class, 'success'])->name('marketing.success');
|
||||||
|
Route::view('/event-admin/{view?}', 'admin')->where('view', '.*')->name('tenant.admin.app');
|
||||||
Route::middleware('auth')->group(function () {
|
Route::middleware('auth')->group(function () {
|
||||||
Route::get('/buy/{packageId}', [MarketingController::class, 'buyPackages'])->name('marketing.buy');
|
Route::get('/buy/{packageId}', [MarketingController::class, 'buyPackages'])->name('marketing.buy');
|
||||||
});
|
});
|
||||||
@@ -44,4 +46,4 @@ Route::post('/checkout/register', [CheckoutController::class, 'register'])->name
|
|||||||
Route::post('/stripe/create-payment-intent', [CheckoutController::class, 'createPaymentIntent'])->name('stripe.create-payment-intent');
|
Route::post('/stripe/create-payment-intent', [CheckoutController::class, 'createPaymentIntent'])->name('stripe.create-payment-intent');
|
||||||
Route::post('/stripe/confirm-payment', [CheckoutController::class, 'confirmPayment'])->name('stripe.confirm-payment');
|
Route::post('/stripe/confirm-payment', [CheckoutController::class, 'confirmPayment'])->name('stripe.confirm-payment');
|
||||||
Route::post('/checkout/track-abandoned', [CheckoutController::class, 'trackAbandonedCheckout'])->name('checkout.track-abandoned');
|
Route::post('/checkout/track-abandoned', [CheckoutController::class, 'trackAbandonedCheckout'])->name('checkout.track-abandoned');
|
||||||
Route::post('/set-locale', [LocaleController::class, 'set'])->name('set-locale');
|
Route::post('/set-locale', [LocaleController::class, 'set'])->name('set-locale');
|
||||||
|
|||||||
@@ -132,7 +132,7 @@ class FullUserFlowTest extends TestCase
|
|||||||
|
|
||||||
// Finaler Redirect zu Success oder Dashboard
|
// Finaler Redirect zu Success oder Dashboard
|
||||||
$successResponse = $this->actingAs($user)->get(route('marketing.success', $paidPackage->id));
|
$successResponse = $this->actingAs($user)->get(route('marketing.success', $paidPackage->id));
|
||||||
$successResponse->assertRedirect('/admin');
|
$successResponse->assertRedirect('/event-admin');
|
||||||
$successResponse->assertStatus(302);
|
$successResponse->assertStatus(302);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -187,4 +187,4 @@ class FullUserFlowTest extends TestCase
|
|||||||
// Nach Korrektur: Erfolgreicher Flow (kurz)
|
// Nach Korrektur: Erfolgreicher Flow (kurz)
|
||||||
// ... (ähnlich wie oben, aber mit Error-Handling)
|
// ... (ähnlich wie oben, aber mit Error-Handling)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,11 +4,11 @@ import { chromium } from 'playwright';
|
|||||||
test.describe('Package Flow in Admin PWA', () => {
|
test.describe('Package Flow in Admin PWA', () => {
|
||||||
test('Create event with package and verify limits', async ({ page }) => {
|
test('Create event with package and verify limits', async ({ page }) => {
|
||||||
// Assume logged in as tenant admin, navigate to events page
|
// Assume logged in as tenant admin, navigate to events page
|
||||||
await page.goto('/admin/events');
|
await page.goto('/event-admin/events');
|
||||||
|
|
||||||
// Click create event button
|
// Click create event button
|
||||||
await page.click('[data-testid="create-event"]');
|
await page.click('[data-testid="create-event"]');
|
||||||
await expect(page).toHaveURL(/\/admin\/events\/create/);
|
await expect(page).toHaveURL(/\/event-admin\/events\/create/);
|
||||||
|
|
||||||
// Fill form
|
// Fill form
|
||||||
await page.fill('[name="name"]', 'Test Package Event');
|
await page.fill('[name="name"]', 'Test Package Event');
|
||||||
@@ -21,19 +21,19 @@ test.describe('Package Flow in Admin PWA', () => {
|
|||||||
|
|
||||||
// Submit
|
// Submit
|
||||||
await page.click('[type="submit"]');
|
await page.click('[type="submit"]');
|
||||||
await expect(page).toHaveURL(/\/admin\/events/);
|
await expect(page).toHaveURL(/\/event-admin\/events/);
|
||||||
|
|
||||||
// Verify event created and package assigned
|
// Verify event created and package assigned
|
||||||
await expect(page.locator('text=Test Package Event')).toBeVisible();
|
await expect(page.locator('text=Test Package Event')).toBeVisible();
|
||||||
await expect(page.locator('text=Starter')).toBeVisible(); // Package name in table
|
await expect(page.locator('text=Starter')).toBeVisible(); // Package name in table
|
||||||
|
|
||||||
// Check dashboard limits
|
// Check dashboard limits
|
||||||
await page.goto('/admin/dashboard');
|
await page.goto('/event-admin/events');
|
||||||
await expect(page.locator('text=Remaining Photos')).toContainText('300'); // Starter limit
|
await expect(page.locator('text=Remaining Photos')).toContainText('300'); // Starter limit
|
||||||
|
|
||||||
// Try to create another event to test reseller limit if applicable
|
// Try to create another event to test reseller limit if applicable
|
||||||
// (Skip for endcustomer; assume tenant has reseller package with limit 1)
|
// (Skip for endcustomer; assume tenant has reseller package with limit 1)
|
||||||
await page.goto('/admin/events');
|
await page.goto('/event-admin/events');
|
||||||
await page.click('[data-testid="create-event"]');
|
await page.click('[data-testid="create-event"]');
|
||||||
await page.fill('[name="name"]', 'Second Event');
|
await page.fill('[name="name"]', 'Second Event');
|
||||||
await page.fill('[name="slug"]', 'second-event');
|
await page.fill('[name="slug"]', 'second-event');
|
||||||
@@ -57,4 +57,4 @@ test.describe('Package Flow in Admin PWA', () => {
|
|||||||
await expect(page.locator('button:disabled')).toBeVisible(); // Upload button disabled
|
await expect(page.locator('button:disabled')).toBeVisible(); // Upload button disabled
|
||||||
await expect(page.locator('text=Upload-Limit erreicht')).toBeVisible();
|
await expect(page.locator('text=Upload-Limit erreicht')).toBeVisible();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user