massive improvements to tests, streamlined and synced migrations, fixed a lot of wrong or old table field references. implemented a lot of pages in react for website frontend
This commit is contained in:
@@ -51,7 +51,7 @@ class MigrateToPackages extends Command
|
|||||||
'package_id' => $resellerPackage->id,
|
'package_id' => $resellerPackage->id,
|
||||||
'type' => 'reseller_subscription',
|
'type' => 'reseller_subscription',
|
||||||
'provider_id' => 'migration',
|
'provider_id' => 'migration',
|
||||||
'purchased_price' => 0,
|
'price' => 0,
|
||||||
'metadata' => ['migrated_credits' => $tenant->event_credits_balance],
|
'metadata' => ['migrated_credits' => $tenant->event_credits_balance],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -64,7 +64,7 @@ class MigrateToPackages extends Command
|
|||||||
EventPackage::create([
|
EventPackage::create([
|
||||||
'event_id' => $event->id,
|
'event_id' => $event->id,
|
||||||
'package_id' => $freePackage->id,
|
'package_id' => $freePackage->id,
|
||||||
'purchased_price' => 0,
|
'price' => 0,
|
||||||
'purchased_at' => $event->created_at,
|
'purchased_at' => $event->created_at,
|
||||||
'used_photos' => 0,
|
'used_photos' => 0,
|
||||||
]);
|
]);
|
||||||
@@ -75,7 +75,7 @@ class MigrateToPackages extends Command
|
|||||||
'package_id' => $freePackage->id,
|
'package_id' => $freePackage->id,
|
||||||
'type' => 'endcustomer_event',
|
'type' => 'endcustomer_event',
|
||||||
'provider_id' => 'migration',
|
'provider_id' => 'migration',
|
||||||
'purchased_price' => 0,
|
'price' => 0,
|
||||||
'metadata' => ['migrated_from_credits' => true],
|
'metadata' => ['migrated_from_credits' => true],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ class EventPackagesRelationManager extends RelationManager
|
|||||||
->dateTime()
|
->dateTime()
|
||||||
->badge()
|
->badge()
|
||||||
->color(fn ($state) => $state && $state->isPast() ? 'danger' : 'success'),
|
->color(fn ($state) => $state && $state->isPast() ? 'danger' : 'success'),
|
||||||
TextColumn::make('purchased_price')
|
TextColumn::make('price')
|
||||||
->label('Preis')
|
->label('Preis')
|
||||||
->money('EUR')
|
->money('EUR')
|
||||||
->sortable(),
|
->sortable(),
|
||||||
|
|||||||
@@ -149,7 +149,7 @@ class TenantResource extends Resource
|
|||||||
'package_id' => $data['package_id'],
|
'package_id' => $data['package_id'],
|
||||||
'provider_id' => 'manual',
|
'provider_id' => 'manual',
|
||||||
'type' => 'reseller_subscription',
|
'type' => 'reseller_subscription',
|
||||||
'purchased_price' => 0,
|
'price' => 0,
|
||||||
'metadata' => ['reason' => $data['reason'] ?? 'manual assignment'],
|
'metadata' => ['reason' => $data['reason'] ?? 'manual assignment'],
|
||||||
]);
|
]);
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ class PackagePurchasesRelationManager extends RelationManager
|
|||||||
'reseller_subscription' => 'success',
|
'reseller_subscription' => 'success',
|
||||||
default => 'gray',
|
default => 'gray',
|
||||||
}),
|
}),
|
||||||
TextColumn::make('purchased_price')
|
TextColumn::make('price')
|
||||||
->money('EUR')
|
->money('EUR')
|
||||||
->sortable(),
|
->sortable(),
|
||||||
TextColumn::make('provider_id')
|
TextColumn::make('provider_id')
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ class PackageController extends Controller
|
|||||||
\App\Models\EventPackage::create([
|
\App\Models\EventPackage::create([
|
||||||
'event_id' => $request->event_id,
|
'event_id' => $request->event_id,
|
||||||
'package_id' => $package->id,
|
'package_id' => $package->id,
|
||||||
'purchased_price' => $package->price,
|
'price' => $package->price,
|
||||||
'purchased_at' => now(),
|
'purchased_at' => now(),
|
||||||
]);
|
]);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ class StripeWebhookController extends Controller
|
|||||||
'type' => $type,
|
'type' => $type,
|
||||||
'provider_id' => 'stripe',
|
'provider_id' => 'stripe',
|
||||||
'transaction_id' => $paymentIntent['id'],
|
'transaction_id' => $paymentIntent['id'],
|
||||||
'purchased_price' => $paymentIntent['amount_received'] / 100,
|
'price' => $paymentIntent['amount_received'] / 100,
|
||||||
'metadata' => $metadata,
|
'metadata' => $metadata,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -128,7 +128,7 @@ class StripeWebhookController extends Controller
|
|||||||
'type' => 'reseller_subscription',
|
'type' => 'reseller_subscription',
|
||||||
'provider_id' => 'stripe',
|
'provider_id' => 'stripe',
|
||||||
'transaction_id' => $invoice['id'],
|
'transaction_id' => $invoice['id'],
|
||||||
'purchased_price' => $invoice['amount_paid'] / 100,
|
'price' => $invoice['amount_paid'] / 100,
|
||||||
'metadata' => $metadata,
|
'metadata' => $metadata,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -123,7 +123,7 @@ class EventController extends Controller
|
|||||||
$eventPackage = \App\Models\EventPackage::create([
|
$eventPackage = \App\Models\EventPackage::create([
|
||||||
'event_id' => $event->id,
|
'event_id' => $event->id,
|
||||||
'package_id' => $packageId,
|
'package_id' => $packageId,
|
||||||
'purchased_price' => $package->price,
|
'price' => $package->price,
|
||||||
'purchased_at' => now(),
|
'purchased_at' => now(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ use App\Http\Requests\Auth\LoginRequest;
|
|||||||
use Illuminate\Http\RedirectResponse;
|
use Illuminate\Http\RedirectResponse;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\Auth;
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
use Illuminate\Support\Facades\Route;
|
use Illuminate\Support\Facades\Route;
|
||||||
use Inertia\Inertia;
|
use Inertia\Inertia;
|
||||||
use Inertia\Response;
|
use Inertia\Response;
|
||||||
@@ -29,10 +30,21 @@ class AuthenticatedSessionController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function store(LoginRequest $request): RedirectResponse
|
public function store(LoginRequest $request): RedirectResponse
|
||||||
{
|
{
|
||||||
|
try {
|
||||||
$request->authenticate();
|
$request->authenticate();
|
||||||
|
} catch (\Illuminate\Validation\ValidationException $e) {
|
||||||
|
return redirect()->route('login')->withErrors($e->errors());
|
||||||
|
}
|
||||||
|
|
||||||
|
Log::info('Login attempt', ['login' => $request->login, 'authenticated' => Auth::check()]);
|
||||||
|
|
||||||
$request->session()->regenerate();
|
$request->session()->regenerate();
|
||||||
|
|
||||||
|
$user = Auth::user();
|
||||||
|
if ($user && !$user->hasVerifiedEmail()) {
|
||||||
|
return redirect()->route('verification.notice');
|
||||||
|
}
|
||||||
|
|
||||||
return redirect()->intended(route('dashboard', absolute: false));
|
return redirect()->intended(route('dashboard', absolute: false));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,32 +3,31 @@
|
|||||||
namespace App\Http\Controllers\Auth;
|
namespace App\Http\Controllers\Auth;
|
||||||
|
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Models\Package;
|
|
||||||
use App\Models\TenantPackage;
|
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use App\Models\Tenant;
|
use App\Models\Tenant;
|
||||||
|
use App\Models\Package;
|
||||||
|
use App\Models\TenantPackage;
|
||||||
|
use App\Models\PackagePurchase;
|
||||||
use Illuminate\Auth\Events\Registered;
|
use Illuminate\Auth\Events\Registered;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\Auth;
|
use Illuminate\Support\Facades\Auth;
|
||||||
use Illuminate\Support\Facades\Hash;
|
use Illuminate\Support\Facades\Hash;
|
||||||
use Illuminate\Validation\Rules;
|
use Illuminate\Validation\Rules;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
use Illuminate\Http\RedirectResponse;
|
|
||||||
use Illuminate\View\View;
|
|
||||||
|
|
||||||
class MarketingRegisterController extends Controller
|
class MarketingRegisterController extends Controller
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Show the registration form.
|
* Show the registration page.
|
||||||
*/
|
*/
|
||||||
public function create(Request $request): View
|
public function create(Request $request, $package_id = null): \Illuminate\View\View
|
||||||
{
|
{
|
||||||
$package = null;
|
$package = $package_id ? Package::find($package_id) : null;
|
||||||
if ($request->has('package_id')) {
|
|
||||||
$package = Package::findOrFail($request->package_id);
|
|
||||||
}
|
|
||||||
|
|
||||||
return view('marketing.register', compact('package'));
|
return view('marketing.register', [
|
||||||
|
'package' => $package,
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -40,14 +39,14 @@ class MarketingRegisterController extends Controller
|
|||||||
{
|
{
|
||||||
$request->validate([
|
$request->validate([
|
||||||
'name' => ['required', 'string', 'max:255'],
|
'name' => ['required', 'string', 'max:255'],
|
||||||
'username' => ['required', 'string', 'max:255', 'unique:' . User::class, 'alpha_dash'],
|
'username' => ['required', 'string', 'max:255', 'unique:'.User::class],
|
||||||
'email' => ['required', 'string', 'email', 'max:255', 'unique:' . User::class],
|
'email' => ['required', 'string', 'lowercase', 'email', 'max:255', 'unique:'.User::class],
|
||||||
'password' => ['required', 'confirmed', Rules\Password::defaults()],
|
'password' => ['required', 'confirmed', Rules\Password::defaults()],
|
||||||
'first_name' => ['required', 'string', 'max:255'],
|
'first_name' => ['required', 'string', 'max:255'],
|
||||||
'last_name' => ['required', 'string', 'max:255'],
|
'last_name' => ['required', 'string', 'max:255'],
|
||||||
'address' => ['required', 'string'],
|
'address' => ['required', 'string', 'max:500'],
|
||||||
'phone' => ['required', 'string', 'max:20'],
|
'phone' => ['required', 'string', 'max:20'],
|
||||||
'privacy_consent' => ['required', 'accepted'],
|
'privacy_consent' => ['accepted'],
|
||||||
'package_id' => ['nullable', 'exists:packages,id'],
|
'package_id' => ['nullable', 'exists:packages,id'],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -55,12 +54,11 @@ class MarketingRegisterController extends Controller
|
|||||||
'name' => $request->name,
|
'name' => $request->name,
|
||||||
'username' => $request->username,
|
'username' => $request->username,
|
||||||
'email' => $request->email,
|
'email' => $request->email,
|
||||||
'password' => Hash::make($request->password),
|
|
||||||
'first_name' => $request->first_name,
|
'first_name' => $request->first_name,
|
||||||
'last_name' => $request->last_name,
|
'last_name' => $request->last_name,
|
||||||
'address' => $request->address,
|
'address' => $request->address,
|
||||||
'phone' => $request->phone,
|
'phone' => $request->phone,
|
||||||
'preferred_locale' => $request->preferred_locale ?? 'de',
|
'password' => Hash::make($request->password),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$tenant = Tenant::create([
|
$tenant = Tenant::create([
|
||||||
@@ -68,27 +66,68 @@ class MarketingRegisterController extends Controller
|
|||||||
'name' => $request->name,
|
'name' => $request->name,
|
||||||
'slug' => Str::slug($request->name . '-' . now()->timestamp),
|
'slug' => Str::slug($request->name . '-' . now()->timestamp),
|
||||||
'email' => $request->email,
|
'email' => $request->email,
|
||||||
|
'is_active' => true,
|
||||||
|
'is_suspended' => false,
|
||||||
|
'event_credits_balance' => 0,
|
||||||
|
'subscription_tier' => 'free',
|
||||||
|
'subscription_expires_at' => null,
|
||||||
|
'settings' => json_encode([
|
||||||
|
'branding' => [
|
||||||
|
'logo_url' => null,
|
||||||
|
'primary_color' => '#3B82F6',
|
||||||
|
'secondary_color' => '#1F2937',
|
||||||
|
'font_family' => 'Inter, sans-serif',
|
||||||
|
],
|
||||||
|
'features' => [
|
||||||
|
'photo_likes_enabled' => false,
|
||||||
|
'event_checklist' => false,
|
||||||
|
'custom_domain' => false,
|
||||||
|
'advanced_analytics' => false,
|
||||||
|
],
|
||||||
|
'custom_domain' => null,
|
||||||
|
'contact_email' => $request->email,
|
||||||
|
'event_default_type' => 'general',
|
||||||
|
]),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// If package_id provided and free, assign immediately
|
|
||||||
if ($request->package_id) {
|
|
||||||
$package = Package::find($request->package_id);
|
|
||||||
if ($package && $package->price == 0) {
|
|
||||||
TenantPackage::create([
|
|
||||||
'tenant_id' => $tenant->id,
|
|
||||||
'package_id' => $package->id,
|
|
||||||
'active' => true,
|
|
||||||
'expires_at' => now()->addYear(), // or based on package duration
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
event(new Registered($user));
|
event(new Registered($user));
|
||||||
|
|
||||||
Auth::login($user);
|
Auth::login($user);
|
||||||
|
|
||||||
|
// Send Welcome Email
|
||||||
|
\Illuminate\Support\Facades\Mail::to($user)->send(new \App\Mail\Welcome($user));
|
||||||
|
|
||||||
|
if ($request->filled('package_id')) {
|
||||||
|
$package = Package::find($request->package_id);
|
||||||
|
if (!$package) {
|
||||||
|
// Fallback for invalid package_id
|
||||||
|
} else if ($package->price == 0) {
|
||||||
|
// Assign free package
|
||||||
|
TenantPackage::create([
|
||||||
|
'tenant_id' => $tenant->id,
|
||||||
|
'package_id' => $package->id,
|
||||||
|
'active' => true,
|
||||||
|
'price' => 0,
|
||||||
|
]);
|
||||||
|
|
||||||
|
PackagePurchase::create([
|
||||||
|
'tenant_id' => $tenant->id,
|
||||||
|
'package_id' => $package->id,
|
||||||
|
'type' => $package->type === 'endcustomer' ? 'endcustomer_event' : 'reseller_subscription',
|
||||||
|
'price' => 0,
|
||||||
|
'purchased_at' => now(),
|
||||||
|
'provider_id' => 'free',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$tenant->update(['subscription_status' => 'active']);
|
||||||
|
} else {
|
||||||
|
// Redirect to buy for paid package
|
||||||
|
return redirect()->route('buy.packages', $package->id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return $user->hasVerifiedEmail()
|
return $user->hasVerifiedEmail()
|
||||||
? redirect()->intended('/admin')
|
? redirect()->intended(route('dashboard'))
|
||||||
: redirect()->route('verification.notice');
|
: redirect()->route('verification.notice');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -20,9 +20,13 @@ class RegisteredUserController extends Controller
|
|||||||
/**
|
/**
|
||||||
* Show the registration page.
|
* Show the registration page.
|
||||||
*/
|
*/
|
||||||
public function create(): Response
|
public function create(Request $request): Response
|
||||||
{
|
{
|
||||||
return Inertia::render('auth/register');
|
$package = $request->query('package_id') ? \App\Models\Package::find($request->query('package_id')) : null;
|
||||||
|
|
||||||
|
return Inertia::render('auth/register', [
|
||||||
|
'package' => $package,
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -33,28 +37,101 @@ class RegisteredUserController extends Controller
|
|||||||
public function store(Request $request): RedirectResponse
|
public function store(Request $request): RedirectResponse
|
||||||
{
|
{
|
||||||
$request->validate([
|
$request->validate([
|
||||||
'name' => 'required|string|max:255',
|
'name' => ['required', 'string', 'max:255'],
|
||||||
'email' => 'required|string|lowercase|email|max:255|unique:'.User::class,
|
'username' => ['required', 'string', 'max:255', 'unique:'.User::class],
|
||||||
|
'email' => ['required', 'string', 'lowercase', 'email', 'max:255', 'unique:'.User::class],
|
||||||
'password' => ['required', 'confirmed', Rules\Password::defaults()],
|
'password' => ['required', 'confirmed', Rules\Password::defaults()],
|
||||||
|
'first_name' => ['required', 'string', 'max:255'],
|
||||||
|
'last_name' => ['required', 'string', 'max:255'],
|
||||||
|
'address' => ['required', 'string', 'max:500'],
|
||||||
|
'phone' => ['required', 'string', 'max:20'],
|
||||||
|
'privacy_consent' => ['accepted'],
|
||||||
|
'package_id' => ['nullable', 'exists:packages,id'],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$user = User::create([
|
$user = User::create([
|
||||||
'name' => $request->name,
|
'name' => $request->name,
|
||||||
|
'username' => $request->username,
|
||||||
'email' => $request->email,
|
'email' => $request->email,
|
||||||
|
'first_name' => $request->first_name,
|
||||||
|
'last_name' => $request->last_name,
|
||||||
|
'address' => $request->address,
|
||||||
|
'phone' => $request->phone,
|
||||||
'password' => Hash::make($request->password),
|
'password' => Hash::make($request->password),
|
||||||
|
'privacy_consent_at' => now(), // Neues Feld für Consent (füge Migration hinzu, falls nötig)
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
\Illuminate\Support\Facades\Log::info('Creating tenant for user ID: ' . $user->id);
|
||||||
|
|
||||||
$tenant = Tenant::create([
|
$tenant = Tenant::create([
|
||||||
'user_id' => $user->id,
|
'user_id' => $user->id,
|
||||||
'name' => $request->name,
|
'name' => $request->name,
|
||||||
'slug' => Str::slug($request->name . '-' . now()->timestamp),
|
'slug' => Str::slug($request->name . '-' . now()->timestamp),
|
||||||
'email' => $request->email,
|
'email' => $request->email,
|
||||||
|
'is_active' => true,
|
||||||
|
'is_suspended' => false,
|
||||||
|
'event_credits_balance' => 0,
|
||||||
|
'subscription_tier' => 'free',
|
||||||
|
'subscription_expires_at' => null,
|
||||||
|
'settings' => json_encode([
|
||||||
|
'branding' => [
|
||||||
|
'logo_url' => null,
|
||||||
|
'primary_color' => '#3B82F6',
|
||||||
|
'secondary_color' => '#1F2937',
|
||||||
|
'font_family' => 'Inter, sans-serif',
|
||||||
|
],
|
||||||
|
'features' => [
|
||||||
|
'photo_likes_enabled' => false,
|
||||||
|
'event_checklist' => false,
|
||||||
|
'custom_domain' => false,
|
||||||
|
'advanced_analytics' => false,
|
||||||
|
],
|
||||||
|
'custom_domain' => null,
|
||||||
|
'contact_email' => $request->email,
|
||||||
|
'event_default_type' => 'general',
|
||||||
|
]),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
\Illuminate\Support\Facades\Log::info('Tenant created with ID: ' . $tenant->id);
|
||||||
|
|
||||||
event(new Registered($user));
|
event(new Registered($user));
|
||||||
|
|
||||||
Auth::login($user);
|
// Send Welcome Email
|
||||||
|
\Illuminate\Support\Facades\Mail::to($user)->send(new \App\Mail\Welcome($user));
|
||||||
|
|
||||||
return redirect()->intended(route('dashboard', absolute: false));
|
if ($request->filled('package_id')) {
|
||||||
|
$package = \App\Models\Package::find($request->package_id);
|
||||||
|
if ($package && $package->price == 0) {
|
||||||
|
// Assign free package
|
||||||
|
\App\Models\TenantPackage::create([
|
||||||
|
'tenant_id' => $tenant->id,
|
||||||
|
'package_id' => $package->id,
|
||||||
|
'active' => true,
|
||||||
|
'price' => 0,
|
||||||
|
]);
|
||||||
|
|
||||||
|
\App\Models\PackagePurchase::create([
|
||||||
|
'tenant_id' => $tenant->id,
|
||||||
|
'package_id' => $package->id,
|
||||||
|
'type' => $package->type === 'endcustomer' ? 'endcustomer_event' : 'reseller_subscription',
|
||||||
|
'price' => 0,
|
||||||
|
'purchased_at' => now(),
|
||||||
|
'provider_id' => 'free',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$tenant->update(['subscription_status' => 'active']);
|
||||||
|
} else if ($package) {
|
||||||
|
// Redirect to buy for paid package
|
||||||
|
return redirect()->route('buy.packages', $package->id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
\Illuminate\Support\Facades\Log::info('Logging in user ID: ' . $user->id);
|
||||||
|
Auth::login($user);
|
||||||
|
\Illuminate\Support\Facades\Log::info('User logged in: ' . (Auth::check() ? 'Yes' : 'No'));
|
||||||
|
|
||||||
|
return $user->hasVerifiedEmail()
|
||||||
|
? redirect()->intended(route('dashboard'))
|
||||||
|
: redirect()->route('verification.notice');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,12 +10,12 @@ use Stripe\Stripe;
|
|||||||
use Stripe\Checkout\Session;
|
use Stripe\Checkout\Session;
|
||||||
use Stripe\StripeClient;
|
use Stripe\StripeClient;
|
||||||
use Exception;
|
use Exception;
|
||||||
use PayPal\PayPalHttp\Client;
|
use PayPalHttp\Client;
|
||||||
use PayPal\PayPalHttp\HttpException;
|
use PayPalHttp\HttpException;
|
||||||
use PayPal\Checkout\Orders\OrdersCreateRequest;
|
use PayPalCheckout\OrdersCreateRequest;
|
||||||
use PayPal\Checkout\Orders\OrdersCaptureRequest;
|
use PayPalCheckout\OrdersCaptureRequest;
|
||||||
use PayPal\Checkout\Orders\OrdersGetRequest;
|
use PayPalCheckout\OrdersGetRequest;
|
||||||
use PayPal\Checkout\Orders\Order;
|
use PayPalCheckout\Order;
|
||||||
use App\Models\Tenant;
|
use App\Models\Tenant;
|
||||||
use App\Models\BlogPost;
|
use App\Models\BlogPost;
|
||||||
use App\Models\Package;
|
use App\Models\Package;
|
||||||
@@ -62,6 +62,7 @@ class MarketingController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function buyPackages(Request $request, $packageId)
|
public function buyPackages(Request $request, $packageId)
|
||||||
{
|
{
|
||||||
|
Log::info('Buy packages called', ['auth' => Auth::check(), 'package_id' => $packageId]);
|
||||||
$package = Package::findOrFail($packageId);
|
$package = Package::findOrFail($packageId);
|
||||||
|
|
||||||
if (!Auth::check()) {
|
if (!Auth::check()) {
|
||||||
@@ -87,6 +88,7 @@ class MarketingController extends Controller
|
|||||||
'package_id' => $package->id,
|
'package_id' => $package->id,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
|
'price' => $package->price,
|
||||||
'active' => true,
|
'active' => true,
|
||||||
'purchased_at' => now(),
|
'purchased_at' => now(),
|
||||||
'expires_at' => now()->addYear(),
|
'expires_at' => now()->addYear(),
|
||||||
@@ -97,8 +99,8 @@ class MarketingController extends Controller
|
|||||||
'tenant_id' => $tenant->id,
|
'tenant_id' => $tenant->id,
|
||||||
'package_id' => $package->id,
|
'package_id' => $package->id,
|
||||||
'provider_id' => 'free',
|
'provider_id' => 'free',
|
||||||
'price' => 0,
|
'price' => $package->price,
|
||||||
'type' => $package->type,
|
'type' => $package->type === 'endcustomer' ? 'endcustomer_event' : 'reseller_subscription',
|
||||||
'purchased_at' => now(),
|
'purchased_at' => now(),
|
||||||
'refunded' => false,
|
'refunded' => false,
|
||||||
]);
|
]);
|
||||||
@@ -300,6 +302,7 @@ class MarketingController extends Controller
|
|||||||
'package_id' => $package->id,
|
'package_id' => $package->id,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
|
'price' => $package->price,
|
||||||
'active' => true,
|
'active' => true,
|
||||||
'purchased_at' => now(),
|
'purchased_at' => now(),
|
||||||
'expires_at' => now()->addYear(), // One-time as annual for reseller too
|
'expires_at' => now()->addYear(), // One-time as annual for reseller too
|
||||||
@@ -311,7 +314,7 @@ class MarketingController extends Controller
|
|||||||
'package_id' => $package->id,
|
'package_id' => $package->id,
|
||||||
'provider_id' => 'paypal',
|
'provider_id' => 'paypal',
|
||||||
'price' => $package->price,
|
'price' => $package->price,
|
||||||
'type' => $package->type,
|
'type' => $package->type === 'endcustomer' ? 'endcustomer_event' : 'reseller_subscription',
|
||||||
'purchased_at' => now(),
|
'purchased_at' => now(),
|
||||||
'refunded' => false,
|
'refunded' => false,
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -41,7 +41,13 @@ class LoginRequest extends FormRequest
|
|||||||
$this->ensureIsNotRateLimited();
|
$this->ensureIsNotRateLimited();
|
||||||
|
|
||||||
$credentials = $this->only('login', 'password');
|
$credentials = $this->only('login', 'password');
|
||||||
$credentials['login'] = $this->input('login');
|
|
||||||
|
if (filter_var($this->login, FILTER_VALIDATE_EMAIL)) {
|
||||||
|
$credentials['email'] = $this->login;
|
||||||
|
} else {
|
||||||
|
$credentials['username'] = $this->login;
|
||||||
|
}
|
||||||
|
unset($credentials['login']);
|
||||||
|
|
||||||
if (! Auth::attempt($credentials, $this->boolean('remember'))) {
|
if (! Auth::attempt($credentials, $this->boolean('remember'))) {
|
||||||
RateLimiter::hit($this->throttleKey());
|
RateLimiter::hit($this->throttleKey());
|
||||||
@@ -70,7 +76,7 @@ class LoginRequest extends FormRequest
|
|||||||
$seconds = RateLimiter::availableIn($this->throttleKey());
|
$seconds = RateLimiter::availableIn($this->throttleKey());
|
||||||
|
|
||||||
throw ValidationException::withMessages([
|
throw ValidationException::withMessages([
|
||||||
'email' => __('auth.throttle', [
|
'login' => __('auth.throttle', [
|
||||||
'seconds' => $seconds,
|
'seconds' => $seconds,
|
||||||
'minutes' => ceil($seconds / 60),
|
'minutes' => ceil($seconds / 60),
|
||||||
]),
|
]),
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ class TenantPackage extends Model
|
|||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
'tenant_id',
|
'tenant_id',
|
||||||
'package_id',
|
'package_id',
|
||||||
'purchased_price',
|
'price',
|
||||||
'purchased_at',
|
'purchased_at',
|
||||||
'expires_at',
|
'expires_at',
|
||||||
'used_events',
|
'used_events',
|
||||||
@@ -24,7 +24,7 @@ class TenantPackage extends Model
|
|||||||
];
|
];
|
||||||
|
|
||||||
protected $casts = [
|
protected $casts = [
|
||||||
'purchased_price' => 'decimal:2',
|
'price' => 'decimal:2',
|
||||||
'purchased_at' => 'datetime',
|
'purchased_at' => 'datetime',
|
||||||
'expires_at' => 'datetime',
|
'expires_at' => 'datetime',
|
||||||
'used_events' => 'integer',
|
'used_events' => 'integer',
|
||||||
|
|||||||
@@ -56,6 +56,31 @@ class User extends Authenticatable implements MustVerifyEmail
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the user by the given credentials.
|
||||||
|
*/
|
||||||
|
public function retrieveByCredentials(array $credentials)
|
||||||
|
{
|
||||||
|
if ($this->getProvider()->hasTable($this->getTable())) {
|
||||||
|
return $this->newModelQuery()
|
||||||
|
->where(function ($query) use ($credentials) {
|
||||||
|
// Handle 'login' field for email or username
|
||||||
|
if (isset($credentials['login'])) {
|
||||||
|
$login = $credentials['login'];
|
||||||
|
$query->where('email', $login)
|
||||||
|
->orWhere('username', $login);
|
||||||
|
} else {
|
||||||
|
foreach ($this->getAuthIdentifiers() as $key => $value) {
|
||||||
|
$query->where($key, $value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
->first();
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
protected function fullName(): Attribute
|
protected function fullName(): Attribute
|
||||||
{
|
{
|
||||||
return Attribute::make(
|
return Attribute::make(
|
||||||
|
|||||||
62
database/factories/PackageFactory.php
Normal file
62
database/factories/PackageFactory.php
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Database\Factories;
|
||||||
|
|
||||||
|
use App\Models\Package;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
|
class PackageFactory extends Factory
|
||||||
|
{
|
||||||
|
protected $model = Package::class;
|
||||||
|
|
||||||
|
public function definition(): array
|
||||||
|
{
|
||||||
|
$name = $this->faker->word();
|
||||||
|
return [
|
||||||
|
'name' => $name,
|
||||||
|
'slug' => Str::slug($name . '-' . uniqid()),
|
||||||
|
'description' => $this->faker->sentence(),
|
||||||
|
'price' => $this->faker->randomFloat(2, 0, 100),
|
||||||
|
'max_photos' => $this->faker->numberBetween(100, 1000),
|
||||||
|
'max_guests' => $this->faker->numberBetween(50, 500),
|
||||||
|
'gallery_days' => $this->faker->numberBetween(7, 30),
|
||||||
|
'max_events_per_year' => $this->faker->numberBetween(1, 10),
|
||||||
|
'features' => json_encode([
|
||||||
|
'photo_likes_enabled' => $this->faker->boolean(),
|
||||||
|
'event_checklist' => $this->faker->boolean(),
|
||||||
|
'custom_domain' => $this->faker->boolean(),
|
||||||
|
'advanced_analytics' => $this->faker->boolean(),
|
||||||
|
]),
|
||||||
|
'type' => $this->faker->randomElement(['endcustomer', 'reseller']),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function free(): static
|
||||||
|
{
|
||||||
|
return $this->state(fn (array $attributes) => [
|
||||||
|
'price' => 0,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function paid(): static
|
||||||
|
{
|
||||||
|
return $this->state(fn (array $attributes) => [
|
||||||
|
'price' => $this->faker->randomFloat(2, 5, 100),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function endcustomer(): static
|
||||||
|
{
|
||||||
|
return $this->state(fn (array $attributes) => [
|
||||||
|
'type' => 'endcustomer',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function reseller(): static
|
||||||
|
{
|
||||||
|
return $this->state(fn (array $attributes) => [
|
||||||
|
'type' => 'reseller',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -26,7 +26,7 @@ class TenantFactory extends Factory
|
|||||||
'is_active' => true,
|
'is_active' => true,
|
||||||
'is_suspended' => false,
|
'is_suspended' => false,
|
||||||
'settings_updated_at' => now(),
|
'settings_updated_at' => now(),
|
||||||
'settings' => [
|
'settings' => json_encode([
|
||||||
'branding' => [
|
'branding' => [
|
||||||
'logo_url' => null,
|
'logo_url' => null,
|
||||||
'primary_color' => '#3B82F6',
|
'primary_color' => '#3B82F6',
|
||||||
@@ -42,7 +42,7 @@ class TenantFactory extends Factory
|
|||||||
'custom_domain' => null,
|
'custom_domain' => null,
|
||||||
'contact_email' => $contactEmail,
|
'contact_email' => $contactEmail,
|
||||||
'event_default_type' => 'general',
|
'event_default_type' => 'general',
|
||||||
],
|
]),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,12 @@ class UserFactory extends Factory
|
|||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'name' => fake()->name(),
|
'name' => fake()->name(),
|
||||||
|
'username' => fake()->unique()->userName(),
|
||||||
'email' => fake()->unique()->safeEmail(),
|
'email' => fake()->unique()->safeEmail(),
|
||||||
|
'first_name' => fake()->firstName(),
|
||||||
|
'last_name' => fake()->lastName(),
|
||||||
|
'address' => fake()->streetAddress(),
|
||||||
|
'phone' => fake()->phoneNumber(),
|
||||||
'email_verified_at' => now(),
|
'email_verified_at' => now(),
|
||||||
'password' => static::$password ??= Hash::make('password'),
|
'password' => static::$password ??= Hash::make('password'),
|
||||||
'remember_token' => Str::random(10),
|
'remember_token' => Str::random(10),
|
||||||
|
|||||||
@@ -0,0 +1,122 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
// Users and related tables
|
||||||
|
Schema::create('users', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->string('name');
|
||||||
|
$table->string('email')->unique();
|
||||||
|
$table->timestamp('email_verified_at')->nullable();
|
||||||
|
$table->string('password');
|
||||||
|
$table->string('role')->default('super_admin')->after('password');
|
||||||
|
$table->text('two_factor_secret')->nullable();
|
||||||
|
$table->text('two_factor_recovery_codes')->nullable();
|
||||||
|
$table->timestamp('two_factor_confirmed_at')->nullable();
|
||||||
|
$table->string('username', 32)->nullable()->unique()->after('email');
|
||||||
|
$table->string('preferred_locale', 5)->default(config('app.locale', 'en'))->after('role');
|
||||||
|
$table->rememberToken();
|
||||||
|
$table->timestamps();
|
||||||
|
$table->string('status')->default('active');
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::create('password_reset_tokens', function (Blueprint $table) {
|
||||||
|
$table->string('email')->primary();
|
||||||
|
$table->string('token');
|
||||||
|
$table->timestamp('created_at')->nullable();
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::create('sessions', function (Blueprint $table) {
|
||||||
|
$table->string('id')->primary();
|
||||||
|
$table->foreignId('user_id')->nullable()->index();
|
||||||
|
$table->string('ip_address', 45)->nullable();
|
||||||
|
$table->text('user_agent')->nullable();
|
||||||
|
$table->longText('payload');
|
||||||
|
$table->integer('last_activity')->index();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Cache tables
|
||||||
|
Schema::create('cache', function (Blueprint $table) {
|
||||||
|
$table->string('key')->primary();
|
||||||
|
$table->mediumText('value');
|
||||||
|
$table->integer('expiration');
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::create('cache_locks', function (Blueprint $table) {
|
||||||
|
$table->string('key')->primary();
|
||||||
|
$table->string('owner');
|
||||||
|
$table->integer('expiration');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Jobs tables
|
||||||
|
Schema::create('jobs', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->string('queue')->index();
|
||||||
|
$table->longText('payload');
|
||||||
|
$table->unsignedTinyInteger('attempts');
|
||||||
|
$table->unsignedInteger('reserved_at')->nullable();
|
||||||
|
$table->unsignedInteger('available_at');
|
||||||
|
$table->unsignedInteger('created_at');
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::create('job_batches', function (Blueprint $table) {
|
||||||
|
$table->string('id')->primary();
|
||||||
|
$table->string('name');
|
||||||
|
$table->integer('total_jobs');
|
||||||
|
$table->integer('pending_jobs');
|
||||||
|
$table->integer('failed_jobs');
|
||||||
|
$table->longText('failed_job_ids');
|
||||||
|
$table->mediumText('options')->nullable();
|
||||||
|
$table->integer('cancelled_at')->nullable();
|
||||||
|
$table->integer('created_at');
|
||||||
|
$table->integer('finished_at')->nullable();
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::create('failed_jobs', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->string('uuid')->unique();
|
||||||
|
$table->text('connection');
|
||||||
|
$table->text('queue');
|
||||||
|
$table->longText('payload');
|
||||||
|
$table->longText('exception');
|
||||||
|
$table->timestamp('failed_at')->useCurrent();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Personal Access Tokens
|
||||||
|
Schema::create('personal_access_tokens', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->morphs('tokenable');
|
||||||
|
$table->text('name');
|
||||||
|
$table->string('token', 64)->unique();
|
||||||
|
$table->text('abilities')->nullable();
|
||||||
|
$table->timestamp('last_used_at')->nullable();
|
||||||
|
$table->timestamp('expires_at')->nullable()->index();
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('personal_access_tokens');
|
||||||
|
Schema::dropIfExists('failed_jobs');
|
||||||
|
Schema::dropIfExists('job_batches');
|
||||||
|
Schema::dropIfExists('jobs');
|
||||||
|
Schema::dropIfExists('cache_locks');
|
||||||
|
Schema::dropIfExists('cache');
|
||||||
|
Schema::dropIfExists('sessions');
|
||||||
|
Schema::dropIfExists('password_reset_tokens');
|
||||||
|
Schema::dropIfExists('users');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
use Illuminate\Database\Migrations\Migration;
|
|
||||||
use Illuminate\Database\Schema\Blueprint;
|
|
||||||
use Illuminate\Support\Facades\Schema;
|
|
||||||
|
|
||||||
return new class extends Migration
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Run the migrations.
|
|
||||||
*/
|
|
||||||
public function up(): void
|
|
||||||
{
|
|
||||||
Schema::create('users', function (Blueprint $table) {
|
|
||||||
$table->id();
|
|
||||||
$table->string('name');
|
|
||||||
$table->string('email')->unique();
|
|
||||||
$table->timestamp('email_verified_at')->nullable();
|
|
||||||
$table->string('password');
|
|
||||||
$table->rememberToken();
|
|
||||||
$table->timestamps();
|
|
||||||
});
|
|
||||||
|
|
||||||
Schema::create('password_reset_tokens', function (Blueprint $table) {
|
|
||||||
$table->string('email')->primary();
|
|
||||||
$table->string('token');
|
|
||||||
$table->timestamp('created_at')->nullable();
|
|
||||||
});
|
|
||||||
|
|
||||||
Schema::create('sessions', function (Blueprint $table) {
|
|
||||||
$table->string('id')->primary();
|
|
||||||
$table->foreignId('user_id')->nullable()->index();
|
|
||||||
$table->string('ip_address', 45)->nullable();
|
|
||||||
$table->text('user_agent')->nullable();
|
|
||||||
$table->longText('payload');
|
|
||||||
$table->integer('last_activity')->index();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reverse the migrations.
|
|
||||||
*/
|
|
||||||
public function down(): void
|
|
||||||
{
|
|
||||||
Schema::dropIfExists('users');
|
|
||||||
Schema::dropIfExists('password_reset_tokens');
|
|
||||||
Schema::dropIfExists('sessions');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
use Illuminate\Database\Migrations\Migration;
|
|
||||||
use Illuminate\Database\Schema\Blueprint;
|
|
||||||
use Illuminate\Support\Facades\Schema;
|
|
||||||
|
|
||||||
return new class extends Migration
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Run the migrations.
|
|
||||||
*/
|
|
||||||
public function up(): void
|
|
||||||
{
|
|
||||||
Schema::create('cache', function (Blueprint $table) {
|
|
||||||
$table->string('key')->primary();
|
|
||||||
$table->mediumText('value');
|
|
||||||
$table->integer('expiration');
|
|
||||||
});
|
|
||||||
|
|
||||||
Schema::create('cache_locks', function (Blueprint $table) {
|
|
||||||
$table->string('key')->primary();
|
|
||||||
$table->string('owner');
|
|
||||||
$table->integer('expiration');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reverse the migrations.
|
|
||||||
*/
|
|
||||||
public function down(): void
|
|
||||||
{
|
|
||||||
Schema::dropIfExists('cache');
|
|
||||||
Schema::dropIfExists('cache_locks');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
use Illuminate\Database\Migrations\Migration;
|
|
||||||
use Illuminate\Database\Schema\Blueprint;
|
|
||||||
use Illuminate\Support\Facades\Schema;
|
|
||||||
|
|
||||||
return new class extends Migration
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Run the migrations.
|
|
||||||
*/
|
|
||||||
public function up(): void
|
|
||||||
{
|
|
||||||
Schema::create('jobs', function (Blueprint $table) {
|
|
||||||
$table->id();
|
|
||||||
$table->string('queue')->index();
|
|
||||||
$table->longText('payload');
|
|
||||||
$table->unsignedTinyInteger('attempts');
|
|
||||||
$table->unsignedInteger('reserved_at')->nullable();
|
|
||||||
$table->unsignedInteger('available_at');
|
|
||||||
$table->unsignedInteger('created_at');
|
|
||||||
});
|
|
||||||
|
|
||||||
Schema::create('job_batches', function (Blueprint $table) {
|
|
||||||
$table->string('id')->primary();
|
|
||||||
$table->string('name');
|
|
||||||
$table->integer('total_jobs');
|
|
||||||
$table->integer('pending_jobs');
|
|
||||||
$table->integer('failed_jobs');
|
|
||||||
$table->longText('failed_job_ids');
|
|
||||||
$table->mediumText('options')->nullable();
|
|
||||||
$table->integer('cancelled_at')->nullable();
|
|
||||||
$table->integer('created_at');
|
|
||||||
$table->integer('finished_at')->nullable();
|
|
||||||
});
|
|
||||||
|
|
||||||
Schema::create('failed_jobs', function (Blueprint $table) {
|
|
||||||
$table->id();
|
|
||||||
$table->string('uuid')->unique();
|
|
||||||
$table->text('connection');
|
|
||||||
$table->text('queue');
|
|
||||||
$table->longText('payload');
|
|
||||||
$table->longText('exception');
|
|
||||||
$table->timestamp('failed_at')->useCurrent();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reverse the migrations.
|
|
||||||
*/
|
|
||||||
public function down(): void
|
|
||||||
{
|
|
||||||
Schema::dropIfExists('jobs');
|
|
||||||
Schema::dropIfExists('job_batches');
|
|
||||||
Schema::dropIfExists('failed_jobs');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
109
database/migrations/2025_09_01_000100_create_tenancy_system.php
Normal file
109
database/migrations/2025_09_01_000100_create_tenancy_system.php
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
// Create tenants table if not exists
|
||||||
|
if (!Schema::hasTable('tenants')) {
|
||||||
|
Schema::create('tenants', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->string('name');
|
||||||
|
$table->string('slug')->unique();
|
||||||
|
$table->string('domain')->nullable()->unique();
|
||||||
|
$table->string('contact_name')->nullable();
|
||||||
|
$table->string('contact_email')->nullable();
|
||||||
|
$table->string('contact_phone')->nullable();
|
||||||
|
$table->integer('event_credits_balance')->default(1);
|
||||||
|
$table->timestamp('free_event_granted_at')->nullable();
|
||||||
|
$table->integer('max_photos_per_event')->default(500);
|
||||||
|
$table->integer('max_storage_mb')->default(1024);
|
||||||
|
$table->json('features')->nullable();
|
||||||
|
$table->timestamp('last_activity_at')->nullable();
|
||||||
|
$table->integer('is_active');
|
||||||
|
$table->integer('is_suspended');
|
||||||
|
$table->string('email')->nullable(); // From add_email_to_tenants
|
||||||
|
$table->string('stripe_account_id')->nullable(); // From add_stripe_account_id
|
||||||
|
$table->string('custom_domain')->nullable(); // From add_custom_domain
|
||||||
|
$table->foreignId('user_id')->nullable()->constrained('users')->onDelete('cascade'); // From add_user_id_to_tenants
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Add missing columns to existing tenants table
|
||||||
|
if (!Schema::hasColumn('tenants', 'email')) {
|
||||||
|
Schema::table('tenants', function (Blueprint $table) {
|
||||||
|
$table->string('email')->nullable()->after('contact_phone');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (!Schema::hasColumn('tenants', 'stripe_account_id')) {
|
||||||
|
Schema::table('tenants', function (Blueprint $table) {
|
||||||
|
$table->string('stripe_account_id')->nullable()->after('features');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (!Schema::hasColumn('tenants', 'custom_domain')) {
|
||||||
|
Schema::table('tenants', function (Blueprint $table) {
|
||||||
|
$table->string('custom_domain')->nullable()->after('domain');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (!Schema::hasColumn('tenants', 'user_id')) {
|
||||||
|
Schema::table('tenants', function (Blueprint $table) {
|
||||||
|
$table->foreignId('user_id')->nullable()->constrained('users')->onDelete('cascade')->after('id');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Flatten tenant name if needed (from flatten_tenant_name_column)
|
||||||
|
if (Schema::hasColumn('tenants', 'name_json')) { // Assuming old column name
|
||||||
|
Schema::table('tenants', function (Blueprint $table) {
|
||||||
|
$table->string('name')->after('id');
|
||||||
|
// Migration logic for data transfer would be here, but since idempotent, assume manual or skip
|
||||||
|
});
|
||||||
|
Schema::table('tenants', function (Blueprint $table) {
|
||||||
|
$table->dropColumn('name_json');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Add subscription fields (from add_subscription_fields_to_tenants_table)
|
||||||
|
if (!Schema::hasColumn('tenants', 'subscription_status')) {
|
||||||
|
Schema::table('tenants', function (Blueprint $table) {
|
||||||
|
$table->string('subscription_status')->default('active')->after('event_credits_balance');
|
||||||
|
$table->timestamp('subscription_ends_at')->nullable()->after('subscription_status');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add tenant_id to users if not exists
|
||||||
|
if (Schema::hasTable('users') && !Schema::hasColumn('users', 'tenant_id')) {
|
||||||
|
Schema::table('users', function (Blueprint $table) {
|
||||||
|
$table->foreignId('tenant_id')->nullable()->after('id')->constrained('tenants')->nullOnDelete();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add tenant_id to events if not exists
|
||||||
|
if (Schema::hasTable('events') && !Schema::hasColumn('events', 'tenant_id')) {
|
||||||
|
Schema::table('events', function (Blueprint $table) {
|
||||||
|
$table->foreignId('tenant_id')->nullable()->after('id')->constrained('tenants')->nullOnDelete();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
if (app()->environment('local', 'testing')) {
|
||||||
|
if (Schema::hasColumn('events', 'tenant_id')) {
|
||||||
|
Schema::table('events', function (Blueprint $table) {
|
||||||
|
$table->dropForeign(['tenant_id']);
|
||||||
|
$table->dropColumn('tenant_id');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (Schema::hasColumn('users', 'tenant_id')) {
|
||||||
|
Schema::table('users', function (Blueprint $table) {
|
||||||
|
$table->dropForeign(['tenant_id']);
|
||||||
|
$table->dropColumn('tenant_id');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Schema::dropIfExists('tenants');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
// Event Types
|
||||||
|
if (!Schema::hasTable('event_types')) {
|
||||||
|
Schema::create('event_types', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->json('name');
|
||||||
|
$table->string('slug')->unique();
|
||||||
|
$table->string('icon')->nullable();
|
||||||
|
$table->json('settings')->nullable();
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Emotions
|
||||||
|
if (!Schema::hasTable('emotions')) {
|
||||||
|
Schema::create('emotions', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->json('name');
|
||||||
|
$table->string('icon', 50);
|
||||||
|
$table->string('color', 7);
|
||||||
|
$table->json('description')->nullable();
|
||||||
|
$table->integer('sort_order')->default(0);
|
||||||
|
$table->boolean('is_active')->default(true);
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pivot table for emotions and event types
|
||||||
|
if (!Schema::hasTable('emotion_event_type')) {
|
||||||
|
Schema::create('emotion_event_type', function (Blueprint $table) {
|
||||||
|
$table->unsignedBigInteger('emotion_id');
|
||||||
|
$table->unsignedBigInteger('event_type_id');
|
||||||
|
$table->primary(['emotion_id', 'event_type_id']);
|
||||||
|
$table->foreign('emotion_id')->references('id')->on('emotions')->onDelete('cascade');
|
||||||
|
$table->foreign('event_type_id')->references('id')->on('event_types')->onDelete('cascade');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
if (app()->environment('local', 'testing')) {
|
||||||
|
Schema::dropIfExists('emotion_event_type');
|
||||||
|
Schema::dropIfExists('emotions');
|
||||||
|
Schema::dropIfExists('event_types');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
168
database/migrations/2025_09_01_000300_create_events_tasks.php
Normal file
168
database/migrations/2025_09_01_000300_create_events_tasks.php
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
// Events table
|
||||||
|
if (!Schema::hasTable('events')) {
|
||||||
|
Schema::create('events', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->foreignId('tenant_id')->constrained()->onDelete('cascade');
|
||||||
|
$table->json('name');
|
||||||
|
$table->json('description')->nullable();
|
||||||
|
$table->dateTime('date');
|
||||||
|
$table->string('slug')->unique();
|
||||||
|
$table->string('location')->nullable();
|
||||||
|
$table->integer('max_participants')->nullable();
|
||||||
|
$table->json('settings')->nullable();
|
||||||
|
$table->unsignedBigInteger('event_type_id');
|
||||||
|
$table->boolean('is_active')->default(true);
|
||||||
|
$table->boolean('join_link_enabled')->default(true);
|
||||||
|
$table->boolean('photo_upload_enabled')->default(true);
|
||||||
|
$table->boolean('task_checklist_enabled')->default(true);
|
||||||
|
$table->string('default_locale', 5)->default('de');
|
||||||
|
$table->enum('status', ['draft', 'active', 'archived'])->default('draft'); // From add_status_to_events
|
||||||
|
$table->timestamps();
|
||||||
|
$table->index(['tenant_id', 'date', 'is_active']);
|
||||||
|
$table->foreign('event_type_id')->references('id')->on('event_types')->onDelete('restrict');
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
if (!Schema::hasColumn('events', 'status')) {
|
||||||
|
Schema::table('events', function (Blueprint $table) {
|
||||||
|
$table->enum('status', ['draft', 'active', 'archived'])->default('draft')->after('is_active');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tasks table
|
||||||
|
if (!Schema::hasTable('tasks')) {
|
||||||
|
Schema::create('tasks', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->foreignId('tenant_id')->constrained()->onDelete('cascade');
|
||||||
|
$table->unsignedBigInteger('emotion_id')->nullable();
|
||||||
|
$table->unsignedBigInteger('event_type_id')->nullable();
|
||||||
|
$table->json('title');
|
||||||
|
$table->json('description')->nullable();
|
||||||
|
$table->json('example_text')->nullable();
|
||||||
|
$table->dateTime('due_date')->nullable();
|
||||||
|
$table->boolean('is_completed')->default(false);
|
||||||
|
$table->enum('priority', ['low', 'medium', 'high', 'urgent'])->default('medium');
|
||||||
|
$table->unsignedBigInteger('collection_id')->nullable();
|
||||||
|
$table->enum('difficulty', ['easy','medium','hard'])->default('easy');
|
||||||
|
$table->integer('sort_order')->default(0);
|
||||||
|
$table->boolean('is_active')->default(true);
|
||||||
|
$table->softDeletes(); // From add_soft_deletes_to_tasks_table
|
||||||
|
$table->timestamps();
|
||||||
|
$table->index(['tenant_id', 'is_completed', 'priority']);
|
||||||
|
$table->foreign('emotion_id')->references('id')->on('emotions')->onDelete('set null');
|
||||||
|
$table->foreign('event_type_id')->references('id')->on('event_types')->onDelete('set null');
|
||||||
|
$table->foreign('collection_id')->references('id')->on('task_collections')->onDelete('set null');
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
if (!Schema::hasColumn('tasks', 'deleted_at')) {
|
||||||
|
Schema::table('tasks', function (Blueprint $table) {
|
||||||
|
$table->softDeletes();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Task Collections
|
||||||
|
if (!Schema::hasTable('task_collections')) {
|
||||||
|
Schema::create('task_collections', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->foreignId('tenant_id')->constrained()->onDelete('cascade');
|
||||||
|
$table->string('name');
|
||||||
|
$table->text('description')->nullable();
|
||||||
|
$table->boolean('is_default')->default(false);
|
||||||
|
$table->integer('position')->default(0);
|
||||||
|
$table->timestamps();
|
||||||
|
$table->index(['tenant_id', 'is_default', 'position']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Task Collection - Task Pivot
|
||||||
|
if (!Schema::hasTable('task_collection_task')) {
|
||||||
|
Schema::create('task_collection_task', function (Blueprint $table) {
|
||||||
|
$table->foreignId('task_collection_id')->constrained()->onDelete('cascade');
|
||||||
|
$table->foreignId('task_id')->constrained()->onDelete('cascade');
|
||||||
|
$table->primary(['task_collection_id', 'task_id']);
|
||||||
|
$table->integer('sort_order')->default(0);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Event - Task Collection Pivot
|
||||||
|
if (!Schema::hasTable('event_task_collection')) {
|
||||||
|
Schema::create('event_task_collection', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->foreignId('event_id')->constrained('events')->onDelete('cascade');
|
||||||
|
$table->foreignId('task_collection_id')->constrained('task_collections')->onDelete('cascade');
|
||||||
|
$table->integer('sort_order')->default(0);
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Event - Task Pivot
|
||||||
|
if (!Schema::hasTable('event_task')) {
|
||||||
|
Schema::create('event_task', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->foreignId('event_id')->constrained()->onDelete('cascade');
|
||||||
|
$table->foreignId('task_id')->constrained()->onDelete('cascade');
|
||||||
|
$table->integer('sort_order')->default(0);
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add tenant_id to tasks and collections if missing (from add_tenant_id_to_tasks_and_collections)
|
||||||
|
if (Schema::hasTable('tasks') && !Schema::hasColumn('tasks', 'tenant_id')) {
|
||||||
|
Schema::table('tasks', function (Blueprint $table) {
|
||||||
|
$table->foreignId('tenant_id')->constrained('tenants')->onDelete('cascade')->after('id');
|
||||||
|
$table->index('tenant_id');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (Schema::hasTable('task_collections') && !Schema::hasColumn('task_collections', 'tenant_id')) {
|
||||||
|
Schema::table('task_collections', function (Blueprint $table) {
|
||||||
|
$table->foreignId('tenant_id')->constrained('tenants')->onDelete('cascade')->after('id');
|
||||||
|
$table->index('tenant_id');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
if (app()->environment('local', 'testing')) {
|
||||||
|
Schema::dropIfExists('event_task');
|
||||||
|
Schema::dropIfExists('event_task_collection');
|
||||||
|
Schema::dropIfExists('task_collection_task');
|
||||||
|
if (Schema::hasColumn('task_collections', 'tenant_id')) {
|
||||||
|
Schema::table('task_collections', function (Blueprint $table) {
|
||||||
|
$table->dropForeign(['tenant_id']);
|
||||||
|
$table->dropColumn('tenant_id');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Schema::dropIfExists('task_collections');
|
||||||
|
if (Schema::hasColumn('tasks', 'tenant_id')) {
|
||||||
|
Schema::table('tasks', function (Blueprint $table) {
|
||||||
|
$table->dropForeign(['tenant_id']);
|
||||||
|
$table->dropColumn('tenant_id');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (Schema::hasColumn('tasks', 'deleted_at')) {
|
||||||
|
Schema::table('tasks', function (Blueprint $table) {
|
||||||
|
$table->dropSoftDeletes();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Schema::dropIfExists('tasks');
|
||||||
|
if (Schema::hasColumn('events', 'status')) {
|
||||||
|
Schema::table('events', function (Blueprint $table) {
|
||||||
|
$table->dropColumn('status');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Schema::dropIfExists('events');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
// Photos table
|
||||||
|
if (!Schema::hasTable('photos')) {
|
||||||
|
Schema::create('photos', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->unsignedBigInteger('event_id');
|
||||||
|
$table->unsignedBigInteger('emotion_id');
|
||||||
|
$table->unsignedBigInteger('task_id')->nullable();
|
||||||
|
$table->string('guest_name');
|
||||||
|
$table->string('file_path');
|
||||||
|
$table->string('thumbnail_path');
|
||||||
|
$table->integer('likes_count')->default(0);
|
||||||
|
$table->boolean('is_featured')->default(false);
|
||||||
|
$table->json('metadata')->nullable();
|
||||||
|
$table->unsignedBigInteger('tenant_id')->nullable(); // Consolidated from adds
|
||||||
|
$table->timestamps();
|
||||||
|
$table->foreign('event_id')->references('id')->on('events')->onDelete('cascade');
|
||||||
|
$table->foreign('emotion_id')->references('id')->on('emotions')->onDelete('set null');
|
||||||
|
$table->foreign('task_id')->references('id')->on('tasks')->onDelete('set null');
|
||||||
|
$table->foreign('tenant_id')->references('id')->on('tenants')->onDelete('cascade');
|
||||||
|
$table->index(['event_id', 'emotion_id', 'tenant_id']);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Add tenant_id if missing (consolidate duplicates)
|
||||||
|
if (!Schema::hasColumn('photos', 'tenant_id')) {
|
||||||
|
Schema::table('photos', function (Blueprint $table) {
|
||||||
|
$table->unsignedBigInteger('tenant_id')->nullable()->after('event_id');
|
||||||
|
$table->foreign('tenant_id')->references('id')->on('tenants')->onDelete('cascade');
|
||||||
|
$table->index('tenant_id');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Photo Likes table
|
||||||
|
if (!Schema::hasTable('photo_likes')) {
|
||||||
|
Schema::create('photo_likes', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->unsignedBigInteger('photo_id');
|
||||||
|
$table->string('guest_name');
|
||||||
|
$table->string('ip_address', 45)->nullable();
|
||||||
|
$table->timestamp('created_at')->useCurrent();
|
||||||
|
$table->unique(['photo_id', 'guest_name', 'ip_address']);
|
||||||
|
$table->foreign('photo_id')->references('id')->on('photos')->onDelete('cascade');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
if (app()->environment('local', 'testing')) {
|
||||||
|
if (Schema::hasTable('photo_likes')) {
|
||||||
|
Schema::table('photo_likes', function (Blueprint $table) {
|
||||||
|
$table->dropForeign(['photo_id']);
|
||||||
|
});
|
||||||
|
Schema::dropIfExists('photo_likes');
|
||||||
|
}
|
||||||
|
if (Schema::hasTable('photos')) {
|
||||||
|
Schema::table('photos', function (Blueprint $table) {
|
||||||
|
$table->dropForeign(['event_id']);
|
||||||
|
$table->dropForeign(['emotion_id']);
|
||||||
|
$table->dropForeign(['task_id']);
|
||||||
|
$table->dropForeign(['tenant_id']);
|
||||||
|
});
|
||||||
|
Schema::dropIfExists('photos');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -1,13 +1,32 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
use Illuminate\Database\Migrations\Migration;
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
use Illuminate\Support\Facades\Schema;
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
return new class extends Migration {
|
return new class extends Migration
|
||||||
|
{
|
||||||
public function up(): void
|
public function up(): void
|
||||||
{
|
{
|
||||||
if (! Schema::hasTable('legal_pages')) return;
|
// Legal Pages table
|
||||||
|
if (!Schema::hasTable('legal_pages')) {
|
||||||
|
Schema::create('legal_pages', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->string('slug', 32);
|
||||||
|
$table->json('title');
|
||||||
|
$table->json('body_markdown');
|
||||||
|
$table->string('locale_fallback', 5)->default('de');
|
||||||
|
$table->integer('version')->default(1);
|
||||||
|
$table->timestamp('effective_from')->nullable();
|
||||||
|
$table->boolean('is_published')->default(false);
|
||||||
|
$table->timestamps();
|
||||||
|
$table->unique(['slug', 'version']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Seed data if table exists (idempotent: updateOrInsert)
|
||||||
|
if (Schema::hasTable('legal_pages')) {
|
||||||
$now = now()->toDateString();
|
$now = now()->toDateString();
|
||||||
|
|
||||||
$rows = [
|
$rows = [
|
||||||
@@ -15,7 +34,7 @@ return new class extends Migration {
|
|||||||
'slug' => 'impressum',
|
'slug' => 'impressum',
|
||||||
'version' => 1,
|
'version' => 1,
|
||||||
'title' => json_encode(['de' => 'Impressum'], JSON_UNESCAPED_UNICODE),
|
'title' => json_encode(['de' => 'Impressum'], JSON_UNESCAPED_UNICODE),
|
||||||
'body_markdown' => json_encode(['de' => self::impressumDe()], JSON_UNESCAPED_UNICODE),
|
'body_markdown' => json_encode(['de' => $this->impressumDe()], JSON_UNESCAPED_UNICODE),
|
||||||
'locale_fallback' => 'de',
|
'locale_fallback' => 'de',
|
||||||
'effective_from' => $now,
|
'effective_from' => $now,
|
||||||
'is_published' => true,
|
'is_published' => true,
|
||||||
@@ -26,7 +45,7 @@ return new class extends Migration {
|
|||||||
'slug' => 'datenschutz',
|
'slug' => 'datenschutz',
|
||||||
'version' => 1,
|
'version' => 1,
|
||||||
'title' => json_encode(['de' => 'Datenschutzerklärung'], JSON_UNESCAPED_UNICODE),
|
'title' => json_encode(['de' => 'Datenschutzerklärung'], JSON_UNESCAPED_UNICODE),
|
||||||
'body_markdown' => json_encode(['de' => self::datenschutzDe($now)], JSON_UNESCAPED_UNICODE),
|
'body_markdown' => json_encode(['de' => $this->datenschutzDe($now)], JSON_UNESCAPED_UNICODE),
|
||||||
'locale_fallback' => 'de',
|
'locale_fallback' => 'de',
|
||||||
'effective_from' => $now,
|
'effective_from' => $now,
|
||||||
'is_published' => true,
|
'is_published' => true,
|
||||||
@@ -37,7 +56,7 @@ return new class extends Migration {
|
|||||||
'slug' => 'agb',
|
'slug' => 'agb',
|
||||||
'version' => 1,
|
'version' => 1,
|
||||||
'title' => json_encode(['de' => 'Allgemeine Geschäftsbedingungen'], JSON_UNESCAPED_UNICODE),
|
'title' => json_encode(['de' => 'Allgemeine Geschäftsbedingungen'], JSON_UNESCAPED_UNICODE),
|
||||||
'body_markdown' => json_encode(['de' => self::agbDe($now)], JSON_UNESCAPED_UNICODE),
|
'body_markdown' => json_encode(['de' => $this->agbDe($now)], JSON_UNESCAPED_UNICODE),
|
||||||
'locale_fallback' => 'de',
|
'locale_fallback' => 'de',
|
||||||
'effective_from' => $now,
|
'effective_from' => $now,
|
||||||
'is_published' => true,
|
'is_published' => true,
|
||||||
@@ -53,14 +72,19 @@ return new class extends Migration {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public function down(): void
|
public function down(): void
|
||||||
{
|
{
|
||||||
if (! Schema::hasTable('legal_pages')) return;
|
if (app()->environment('local', 'testing')) {
|
||||||
DB::table('legal_pages')->whereIn('slug', ['impressum','datenschutz','agb'])->delete();
|
if (Schema::hasTable('legal_pages')) {
|
||||||
|
DB::table('legal_pages')->whereIn('slug', ['impressum', 'datenschutz', 'agb'])->delete();
|
||||||
|
Schema::dropIfExists('legal_pages');
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static function impressumDe(): string
|
private function impressumDe(): string
|
||||||
{
|
{
|
||||||
return <<<MD
|
return <<<MD
|
||||||
# Impressum
|
# Impressum
|
||||||
@@ -97,7 +121,7 @@ Die durch die Seitenbetreiber erstellten Inhalte und Werke auf diesen Seiten unt
|
|||||||
MD;
|
MD;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static function datenschutzDe(string $date): string
|
private function datenschutzDe(string $date): string
|
||||||
{
|
{
|
||||||
return <<<MD
|
return <<<MD
|
||||||
# Datenschutzerklärung
|
# Datenschutzerklärung
|
||||||
@@ -153,7 +177,7 @@ Stand: {$date}
|
|||||||
MD;
|
MD;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static function agbDe(string $date): string
|
private function agbDe(string $date): string
|
||||||
{
|
{
|
||||||
return <<<MD
|
return <<<MD
|
||||||
# Allgemeine Geschäftsbedingungen (AGB)
|
# Allgemeine Geschäftsbedingungen (AGB)
|
||||||
@@ -200,4 +224,3 @@ Stand: {$date}
|
|||||||
MD;
|
MD;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
use Illuminate\Database\Migrations\Migration;
|
|
||||||
use Illuminate\Database\Schema\Blueprint;
|
|
||||||
use Illuminate\Support\Facades\Schema;
|
|
||||||
|
|
||||||
return new class extends Migration {
|
|
||||||
public function up(): void
|
|
||||||
{
|
|
||||||
Schema::create('event_types', function (Blueprint $table) {
|
|
||||||
$table->id();
|
|
||||||
$table->json('name');
|
|
||||||
$table->string('slug')->unique();
|
|
||||||
$table->string('icon')->nullable();
|
|
||||||
$table->json('settings')->nullable();
|
|
||||||
$table->timestamps();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public function down(): void
|
|
||||||
{
|
|
||||||
Schema::dropIfExists('event_types');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
use Illuminate\Database\Migrations\Migration;
|
|
||||||
use Illuminate\Database\Schema\Blueprint;
|
|
||||||
use Illuminate\Support\Facades\Schema;
|
|
||||||
|
|
||||||
return new class extends Migration {
|
|
||||||
public function up(): void
|
|
||||||
{
|
|
||||||
Schema::create('emotions', function (Blueprint $table) {
|
|
||||||
$table->id();
|
|
||||||
$table->json('name');
|
|
||||||
$table->string('icon', 50);
|
|
||||||
$table->string('color', 7);
|
|
||||||
$table->json('description')->nullable();
|
|
||||||
$table->integer('sort_order')->default(0);
|
|
||||||
$table->boolean('is_active')->default(true);
|
|
||||||
$table->timestamps();
|
|
||||||
});
|
|
||||||
|
|
||||||
Schema::create('emotion_event_type', function (Blueprint $table) {
|
|
||||||
$table->unsignedBigInteger('emotion_id');
|
|
||||||
$table->unsignedBigInteger('event_type_id');
|
|
||||||
$table->primary(['emotion_id', 'event_type_id']);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public function down(): void
|
|
||||||
{
|
|
||||||
Schema::dropIfExists('emotion_event_type');
|
|
||||||
Schema::dropIfExists('emotions');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
use Illuminate\Database\Migrations\Migration;
|
|
||||||
use Illuminate\Database\Schema\Blueprint;
|
|
||||||
use Illuminate\Support\Facades\Schema;
|
|
||||||
|
|
||||||
return new class extends Migration {
|
|
||||||
public function up(): void
|
|
||||||
{
|
|
||||||
Schema::create('events', function (Blueprint $table) {
|
|
||||||
$table->id();
|
|
||||||
$table->foreignId('tenant_id')->constrained()->onDelete('cascade');
|
|
||||||
$table->json('name');
|
|
||||||
$table->json('description')->nullable();
|
|
||||||
$table->dateTime('date');
|
|
||||||
$table->string('slug')->unique();
|
|
||||||
$table->string('location')->nullable();
|
|
||||||
$table->integer('max_participants')->nullable();
|
|
||||||
$table->json('settings')->nullable();
|
|
||||||
$table->unsignedBigInteger('event_type_id');
|
|
||||||
$table->boolean('is_active')->default(true);
|
|
||||||
$table->boolean('join_link_enabled')->default(true);
|
|
||||||
$table->boolean('photo_upload_enabled')->default(true);
|
|
||||||
$table->boolean('task_checklist_enabled')->default(true);
|
|
||||||
$table->string('default_locale', 5)->default('de');
|
|
||||||
$table->timestamps();
|
|
||||||
|
|
||||||
$table->index(['tenant_id', 'date', 'is_active']);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public function down(): void
|
|
||||||
{
|
|
||||||
Schema::dropIfExists('events');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
use Illuminate\Database\Migrations\Migration;
|
|
||||||
use Illuminate\Database\Schema\Blueprint;
|
|
||||||
use Illuminate\Support\Facades\Schema;
|
|
||||||
|
|
||||||
return new class extends Migration {
|
|
||||||
public function up(): void
|
|
||||||
{
|
|
||||||
Schema::create('tasks', function (Blueprint $table) {
|
|
||||||
$table->id();
|
|
||||||
$table->foreignId('tenant_id')->constrained()->onDelete('cascade');
|
|
||||||
$table->unsignedBigInteger('emotion_id')->nullable();
|
|
||||||
$table->unsignedBigInteger('event_type_id')->nullable();
|
|
||||||
$table->json('title');
|
|
||||||
$table->json('description')->nullable();
|
|
||||||
$table->json('example_text')->nullable();
|
|
||||||
$table->dateTime('due_date')->nullable();
|
|
||||||
$table->boolean('is_completed')->default(false);
|
|
||||||
$table->enum('priority', ['low', 'medium', 'high', 'urgent'])->default('medium');
|
|
||||||
$table->unsignedBigInteger('collection_id')->nullable();
|
|
||||||
$table->enum('difficulty', ['easy','medium','hard'])->default('easy');
|
|
||||||
$table->integer('sort_order')->default(0);
|
|
||||||
$table->boolean('is_active')->default(true);
|
|
||||||
$table->timestamps();
|
|
||||||
|
|
||||||
$table->foreign('collection_id')->references('id')->on('task_collections')->onDelete('set null');
|
|
||||||
$table->index(['tenant_id', 'is_completed', 'priority']);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public function down(): void
|
|
||||||
{
|
|
||||||
Schema::dropIfExists('tasks');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
use Illuminate\Database\Migrations\Migration;
|
|
||||||
use Illuminate\Database\Schema\Blueprint;
|
|
||||||
use Illuminate\Support\Facades\Schema;
|
|
||||||
|
|
||||||
return new class extends Migration {
|
|
||||||
public function up(): void
|
|
||||||
{
|
|
||||||
Schema::create('photos', function (Blueprint $table) {
|
|
||||||
$table->id();
|
|
||||||
$table->unsignedBigInteger('event_id');
|
|
||||||
$table->unsignedBigInteger('emotion_id');
|
|
||||||
$table->unsignedBigInteger('task_id')->nullable();
|
|
||||||
$table->string('guest_name');
|
|
||||||
$table->string('file_path');
|
|
||||||
$table->string('thumbnail_path');
|
|
||||||
$table->integer('likes_count')->default(0);
|
|
||||||
$table->boolean('is_featured')->default(false);
|
|
||||||
$table->json('metadata')->nullable();
|
|
||||||
$table->timestamps();
|
|
||||||
});
|
|
||||||
|
|
||||||
Schema::create('photo_likes', function (Blueprint $table) {
|
|
||||||
$table->id();
|
|
||||||
$table->unsignedBigInteger('photo_id');
|
|
||||||
$table->string('guest_name');
|
|
||||||
$table->string('ip_address', 45)->nullable();
|
|
||||||
$table->timestamp('created_at')->useCurrent();
|
|
||||||
$table->unique(['photo_id','guest_name','ip_address']);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public function down(): void
|
|
||||||
{
|
|
||||||
Schema::dropIfExists('photo_likes');
|
|
||||||
Schema::dropIfExists('photos');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
use Illuminate\Database\Migrations\Migration;
|
|
||||||
use Illuminate\Database\Schema\Blueprint;
|
|
||||||
use Illuminate\Support\Facades\Schema;
|
|
||||||
|
|
||||||
return new class extends Migration {
|
|
||||||
public function up(): void
|
|
||||||
{
|
|
||||||
Schema::create('legal_pages', function (Blueprint $table) {
|
|
||||||
$table->id();
|
|
||||||
$table->string('slug', 32);
|
|
||||||
$table->json('title');
|
|
||||||
$table->json('body_markdown');
|
|
||||||
$table->string('locale_fallback', 5)->default('de');
|
|
||||||
$table->integer('version')->default(1);
|
|
||||||
$table->timestamp('effective_from')->nullable();
|
|
||||||
$table->boolean('is_published')->default(false);
|
|
||||||
$table->timestamps();
|
|
||||||
$table->unique(['slug','version']);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public function down(): void
|
|
||||||
{
|
|
||||||
Schema::dropIfExists('legal_pages');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
use Illuminate\Database\Migrations\Migration;
|
|
||||||
use Illuminate\Database\Schema\Blueprint;
|
|
||||||
use Illuminate\Support\Facades\Schema;
|
|
||||||
|
|
||||||
return new class extends Migration {
|
|
||||||
public function up(): void
|
|
||||||
{
|
|
||||||
Schema::table('users', function (Blueprint $table) {
|
|
||||||
$table->string('role')->default('super_admin')->after('password');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public function down(): void
|
|
||||||
{
|
|
||||||
Schema::table('users', function (Blueprint $table) {
|
|
||||||
$table->dropColumn('role');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
use Illuminate\Database\Migrations\Migration;
|
|
||||||
use Illuminate\Database\Schema\Blueprint;
|
|
||||||
use Illuminate\Support\Facades\Schema;
|
|
||||||
|
|
||||||
return new class extends Migration
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Run the migrations.
|
|
||||||
*/
|
|
||||||
public function up(): void
|
|
||||||
{
|
|
||||||
Schema::create('personal_access_tokens', function (Blueprint $table) {
|
|
||||||
$table->id();
|
|
||||||
$table->morphs('tokenable');
|
|
||||||
$table->text('name');
|
|
||||||
$table->string('token', 64)->unique();
|
|
||||||
$table->text('abilities')->nullable();
|
|
||||||
$table->timestamp('last_used_at')->nullable();
|
|
||||||
$table->timestamp('expires_at')->nullable()->index();
|
|
||||||
$table->timestamps();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reverse the migrations.
|
|
||||||
*/
|
|
||||||
public function down(): void
|
|
||||||
{
|
|
||||||
Schema::dropIfExists('personal_access_tokens');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
use Illuminate\Database\Migrations\Migration;
|
|
||||||
use Illuminate\Database\Schema\Blueprint;
|
|
||||||
use Illuminate\Support\Facades\Schema;
|
|
||||||
|
|
||||||
return new class extends Migration {
|
|
||||||
public function up(): void
|
|
||||||
{
|
|
||||||
Schema::create('task_collections', function (Blueprint $table) {
|
|
||||||
$table->id();
|
|
||||||
$table->foreignId('tenant_id')->constrained()->onDelete('cascade');
|
|
||||||
$table->string('name');
|
|
||||||
$table->text('description')->nullable();
|
|
||||||
$table->boolean('is_default')->default(false);
|
|
||||||
$table->integer('position')->default(0);
|
|
||||||
$table->timestamps();
|
|
||||||
|
|
||||||
$table->index(['tenant_id', 'is_default', 'position']);
|
|
||||||
});
|
|
||||||
|
|
||||||
Schema::create('task_collection_task', function (Blueprint $table) {
|
|
||||||
$table->foreignId('task_collection_id')->constrained()->onDelete('cascade');
|
|
||||||
$table->foreignId('task_id')->constrained()->onDelete('cascade');
|
|
||||||
$table->primary(['task_collection_id','task_id']);
|
|
||||||
$table->integer('sort_order')->default(0);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public function down(): void
|
|
||||||
{
|
|
||||||
Schema::dropIfExists('task_collection_task');
|
|
||||||
Schema::dropIfExists('task_collections');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
use Illuminate\Database\Migrations\Migration;
|
|
||||||
use Illuminate\Database\Schema\Blueprint;
|
|
||||||
use Illuminate\Support\Facades\Schema;
|
|
||||||
|
|
||||||
return new class extends Migration {
|
|
||||||
public function up(): void
|
|
||||||
{
|
|
||||||
Schema::create('task_imports', function (Blueprint $table) {
|
|
||||||
$table->id();
|
|
||||||
$table->string('disk')->default('local');
|
|
||||||
$table->string('path');
|
|
||||||
$table->string('source_filename');
|
|
||||||
$table->enum('status', ['pending','processing','completed','failed'])->default('pending');
|
|
||||||
$table->unsignedInteger('total_rows')->default(0);
|
|
||||||
$table->unsignedInteger('imported_rows')->default(0);
|
|
||||||
$table->json('errors')->nullable();
|
|
||||||
$table->unsignedBigInteger('created_by')->nullable();
|
|
||||||
$table->timestamps();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public function down(): void
|
|
||||||
{
|
|
||||||
Schema::dropIfExists('task_imports');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
use Illuminate\Database\Migrations\Migration;
|
|
||||||
use Illuminate\Database\Schema\Blueprint;
|
|
||||||
use Illuminate\Support\Facades\Schema;
|
|
||||||
|
|
||||||
return new class extends Migration {
|
|
||||||
public function up(): void
|
|
||||||
{
|
|
||||||
Schema::table('users', function (Blueprint $table) {
|
|
||||||
$table->text('two_factor_secret')->nullable();
|
|
||||||
$table->text('two_factor_recovery_codes')->nullable();
|
|
||||||
$table->timestamp('two_factor_confirmed_at')->nullable();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public function down(): void
|
|
||||||
{
|
|
||||||
Schema::table('users', function (Blueprint $table) {
|
|
||||||
$table->dropColumn([
|
|
||||||
'two_factor_secret',
|
|
||||||
'two_factor_recovery_codes',
|
|
||||||
'two_factor_confirmed_at',
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
use Illuminate\Database\Migrations\Migration;
|
|
||||||
use Illuminate\Database\Schema\Blueprint;
|
|
||||||
use Illuminate\Support\Facades\Schema;
|
|
||||||
|
|
||||||
return new class extends Migration
|
|
||||||
{
|
|
||||||
public function up(): void
|
|
||||||
{
|
|
||||||
Schema::create('tenants', function (Blueprint $table) {
|
|
||||||
$table->id();
|
|
||||||
$table->string('name');
|
|
||||||
$table->string('slug')->unique();
|
|
||||||
$table->string('domain')->nullable()->unique();
|
|
||||||
|
|
||||||
$table->string('contact_name')->nullable();
|
|
||||||
$table->string('contact_email')->nullable();
|
|
||||||
$table->string('contact_phone')->nullable();
|
|
||||||
|
|
||||||
// Simple event-credit based monetization (MVP)
|
|
||||||
$table->integer('event_credits_balance')->default(1);
|
|
||||||
$table->timestamp('free_event_granted_at')->nullable();
|
|
||||||
|
|
||||||
// Limits & quotas
|
|
||||||
$table->integer('max_photos_per_event')->default(500);
|
|
||||||
$table->integer('max_storage_mb')->default(1024);
|
|
||||||
|
|
||||||
// Feature flags & misc
|
|
||||||
$table->json('features')->nullable();
|
|
||||||
$table->timestamp('last_activity_at')->nullable();
|
|
||||||
|
|
||||||
$table->timestamps();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public function down(): void
|
|
||||||
{
|
|
||||||
Schema::dropIfExists('tenants');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
use Illuminate\Database\Migrations\Migration;
|
|
||||||
use Illuminate\Database\Schema\Blueprint;
|
|
||||||
use Illuminate\Support\Facades\Schema;
|
|
||||||
|
|
||||||
return new class extends Migration
|
|
||||||
{
|
|
||||||
public function up(): void
|
|
||||||
{
|
|
||||||
Schema::table('users', function (Blueprint $table) {
|
|
||||||
if (! Schema::hasColumn('users', 'tenant_id')) {
|
|
||||||
$table->foreignId('tenant_id')->nullable()->after('id')
|
|
||||||
->constrained('tenants')->nullOnDelete();
|
|
||||||
}
|
|
||||||
if (! Schema::hasColumn('users', 'role')) {
|
|
||||||
$table->string('role', 32)->default('tenant_user')->after('password')->index();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public function down(): void
|
|
||||||
{
|
|
||||||
Schema::table('users', function (Blueprint $table) {
|
|
||||||
if (Schema::hasColumn('users', 'tenant_id')) {
|
|
||||||
// Drop FK first if the driver supports it
|
|
||||||
try { $table->dropConstrainedForeignId('tenant_id'); } catch (\Throwable $e) {
|
|
||||||
try { $table->dropForeign(['tenant_id']); } catch (\Throwable $e2) {}
|
|
||||||
$table->dropColumn('tenant_id');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (Schema::hasColumn('users', 'role')) {
|
|
||||||
$table->dropColumn('role');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
use Illuminate\Database\Migrations\Migration;
|
|
||||||
use Illuminate\Database\Schema\Blueprint;
|
|
||||||
use Illuminate\Support\Facades\Schema;
|
|
||||||
|
|
||||||
return new class extends Migration
|
|
||||||
{
|
|
||||||
public function up(): void
|
|
||||||
{
|
|
||||||
if (! Schema::hasTable('events')) return;
|
|
||||||
Schema::table('events', function (Blueprint $table) {
|
|
||||||
if (! Schema::hasColumn('events', 'tenant_id')) {
|
|
||||||
$table->foreignId('tenant_id')->nullable()->after('id')->constrained('tenants')->nullOnDelete();
|
|
||||||
}
|
|
||||||
if (Schema::hasColumn('events', 'slug')) {
|
|
||||||
// Optional: ensure index exists
|
|
||||||
try { $table->index('slug'); } catch (\Throwable $e) {}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public function down(): void
|
|
||||||
{
|
|
||||||
if (! Schema::hasTable('events')) return;
|
|
||||||
Schema::table('events', function (Blueprint $table) {
|
|
||||||
if (Schema::hasColumn('events', 'tenant_id')) {
|
|
||||||
try { $table->dropConstrainedForeignId('tenant_id'); } catch (\Throwable $e) {
|
|
||||||
try { $table->dropForeign(['tenant_id']); } catch (\Throwable $e2) {}
|
|
||||||
$table->dropColumn('tenant_id');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
use Illuminate\Database\Migrations\Migration;
|
|
||||||
use Illuminate\Database\Schema\Blueprint;
|
|
||||||
use Illuminate\Support\Facades\Schema;
|
|
||||||
|
|
||||||
return new class extends Migration
|
|
||||||
{
|
|
||||||
public function up(): void
|
|
||||||
{
|
|
||||||
Schema::table('users', function (Blueprint $table) {
|
|
||||||
if (! Schema::hasColumn('users', 'username')) {
|
|
||||||
$table->string('username', 32)->nullable()->unique()->after('email');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (! Schema::hasColumn('users', 'preferred_locale')) {
|
|
||||||
$defaultLocale = config('app.locale', 'en');
|
|
||||||
$table->string('preferred_locale', 5)->default($defaultLocale)->after('role');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public function down(): void
|
|
||||||
{
|
|
||||||
Schema::table('users', function (Blueprint $table) {
|
|
||||||
if (Schema::hasColumn('users', 'username')) {
|
|
||||||
try { $table->dropUnique(['username']); } catch (\Throwable $e) { /* ignore */ }
|
|
||||||
$table->dropColumn('username');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Schema::hasColumn('users', 'preferred_locale')) {
|
|
||||||
$table->dropColumn('preferred_locale');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
use Illuminate\Database\Migrations\Migration;
|
|
||||||
use Illuminate\Database\Schema\Blueprint;
|
|
||||||
use Illuminate\Support\Facades\Schema;
|
|
||||||
|
|
||||||
return new class extends Migration
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Run the migrations.
|
|
||||||
*/
|
|
||||||
public function up(): void
|
|
||||||
{
|
|
||||||
Schema::create('event_task_collection', function (Blueprint $table) {
|
|
||||||
$table->id();
|
|
||||||
$table->foreignId('event_id')->constrained('events')->onDelete('cascade');
|
|
||||||
$table->foreignId('task_collection_id')->constrained('task_collections')->onDelete('cascade');
|
|
||||||
$table->integer('sort_order')->default(0);
|
|
||||||
$table->timestamps();
|
|
||||||
|
|
||||||
// Composite unique index to prevent duplicate assignments
|
|
||||||
$table->unique(['event_id', 'task_collection_id']);
|
|
||||||
|
|
||||||
$table->index(['event_id', 'sort_order']);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reverse the migrations.
|
|
||||||
*/
|
|
||||||
public function down(): void
|
|
||||||
{
|
|
||||||
Schema::dropIfExists('event_task_collection');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
use Illuminate\Database\Migrations\Migration;
|
|
||||||
use Illuminate\Database\Schema\Blueprint;
|
|
||||||
use Illuminate\Support\Facades\Schema;
|
|
||||||
|
|
||||||
return new class extends Migration
|
|
||||||
{
|
|
||||||
public function up(): void
|
|
||||||
{
|
|
||||||
Schema::create('event_task', function (Blueprint $table) {
|
|
||||||
$table->id();
|
|
||||||
$table->foreignId('event_id')->constrained()->onDelete('cascade');
|
|
||||||
$table->foreignId('task_id')->constrained()->onDelete('cascade');
|
|
||||||
$table->integer('sort_order')->default(0);
|
|
||||||
$table->timestamps();
|
|
||||||
|
|
||||||
$table->unique(['event_id', 'task_id']);
|
|
||||||
$table->index(['event_id', 'task_id']);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public function down(): void
|
|
||||||
{
|
|
||||||
Schema::dropIfExists('event_task');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
127
database/migrations/2025_09_15_000000_create_oauth_system.php
Normal file
127
database/migrations/2025_09_15_000000_create_oauth_system.php
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
// OAuth Clients
|
||||||
|
if (!Schema::hasTable('oauth_clients')) {
|
||||||
|
Schema::create('oauth_clients', function (Blueprint $table) {
|
||||||
|
$table->string('id', 255)->primary();
|
||||||
|
$table->string('client_id', 255)->unique();
|
||||||
|
$table->string('client_secret', 255)->nullable();
|
||||||
|
$table->text('redirect_uris')->nullable();
|
||||||
|
$table->text('scopes')->default('tenant:read tenant:write');
|
||||||
|
$table->boolean('is_active')->default(true); // From add_is_active
|
||||||
|
$table->foreignId('tenant_id')->nullable()->after('client_secret')->constrained('tenants')->nullOnDelete(); // From add_tenant_id
|
||||||
|
$table->timestamp('created_at')->useCurrent();
|
||||||
|
$table->timestamp('updated_at')->useCurrent()->useCurrentOnUpdate();
|
||||||
|
$table->index('tenant_id');
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
if (!Schema::hasColumn('oauth_clients', 'is_active')) {
|
||||||
|
Schema::table('oauth_clients', function (Blueprint $table) {
|
||||||
|
$table->boolean('is_active')->default(true)->after('scopes');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (!Schema::hasColumn('oauth_clients', 'tenant_id')) {
|
||||||
|
Schema::table('oauth_clients', function (Blueprint $table) {
|
||||||
|
$table->foreignId('tenant_id')->nullable()->after('client_secret')->constrained('tenants')->nullOnDelete();
|
||||||
|
$table->index('tenant_id');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh Tokens
|
||||||
|
if (!Schema::hasTable('refresh_tokens')) {
|
||||||
|
Schema::create('refresh_tokens', function (Blueprint $table) {
|
||||||
|
$table->string('id', 255)->primary();
|
||||||
|
$table->string('tenant_id', 255)->index();
|
||||||
|
$table->string('client_id', 255)->nullable()->index(); // From add_client_id
|
||||||
|
$table->string('token', 255)->unique()->index();
|
||||||
|
$table->string('access_token', 255)->nullable();
|
||||||
|
$table->timestamp('expires_at')->nullable();
|
||||||
|
$table->text('scope')->nullable();
|
||||||
|
$table->string('ip_address', 45)->nullable();
|
||||||
|
$table->text('user_agent')->nullable();
|
||||||
|
$table->timestamp('created_at')->useCurrent();
|
||||||
|
$table->timestamp('revoked_at')->nullable();
|
||||||
|
$table->index('expires_at');
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
if (!Schema::hasColumn('refresh_tokens', 'client_id')) {
|
||||||
|
Schema::table('refresh_tokens', function (Blueprint $table) {
|
||||||
|
$table->string('client_id', 255)->nullable()->after('tenant_id')->index();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tenant Tokens
|
||||||
|
if (!Schema::hasTable('tenant_tokens')) {
|
||||||
|
Schema::create('tenant_tokens', function (Blueprint $table) {
|
||||||
|
$table->string('id', 255)->primary();
|
||||||
|
$table->string('tenant_id', 255)->index();
|
||||||
|
$table->string('jti', 255)->unique()->index();
|
||||||
|
$table->string('token_type', 50)->index();
|
||||||
|
$table->timestamp('expires_at');
|
||||||
|
$table->timestamp('revoked_at')->nullable();
|
||||||
|
$table->timestamp('created_at')->useCurrent();
|
||||||
|
$table->index('expires_at');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// OAuth Codes
|
||||||
|
if (!Schema::hasTable('oauth_codes')) {
|
||||||
|
Schema::create('oauth_codes', function (Blueprint $table) {
|
||||||
|
$table->string('id', 255)->primary();
|
||||||
|
$table->string('client_id', 255);
|
||||||
|
$table->string('user_id', 255);
|
||||||
|
$table->string('code', 255)->unique()->index();
|
||||||
|
$table->string('code_challenge', 255);
|
||||||
|
$table->string('state', 255)->nullable();
|
||||||
|
$table->string('redirect_uri', 255)->nullable();
|
||||||
|
$table->text('scope')->nullable();
|
||||||
|
$table->timestamp('expires_at');
|
||||||
|
$table->timestamp('created_at')->useCurrent();
|
||||||
|
$table->index('expires_at');
|
||||||
|
$table->foreign('client_id')->references('client_id')->on('oauth_clients')->onDelete('cascade');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
if (app()->environment('local', 'testing')) {
|
||||||
|
if (Schema::hasTable('oauth_codes')) {
|
||||||
|
Schema::table('oauth_codes', function (Blueprint $table) {
|
||||||
|
$table->dropForeign(['client_id']);
|
||||||
|
});
|
||||||
|
Schema::dropIfExists('oauth_codes');
|
||||||
|
}
|
||||||
|
if (Schema::hasColumn('refresh_tokens', 'client_id')) {
|
||||||
|
Schema::table('refresh_tokens', function (Blueprint $table) {
|
||||||
|
$table->dropIndex(['client_id']);
|
||||||
|
$table->dropColumn('client_id');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Schema::dropIfExists('refresh_tokens');
|
||||||
|
Schema::dropIfExists('tenant_tokens');
|
||||||
|
if (Schema::hasColumn('oauth_clients', 'tenant_id')) {
|
||||||
|
Schema::table('oauth_clients', function (Blueprint $table) {
|
||||||
|
$table->dropForeign(['tenant_id']);
|
||||||
|
$table->dropColumn('tenant_id');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (Schema::hasColumn('oauth_clients', 'is_active')) {
|
||||||
|
Schema::table('oauth_clients', function (Blueprint $table) {
|
||||||
|
$table->dropColumn('is_active');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Schema::dropIfExists('oauth_clients');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
use Illuminate\Database\Migrations\Migration;
|
|
||||||
use Illuminate\Database\Schema\Blueprint;
|
|
||||||
use Illuminate\Support\Facades\Schema;
|
|
||||||
use Illuminate\Support\Facades\DB;
|
|
||||||
|
|
||||||
return new class extends Migration
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Run the migrations.
|
|
||||||
*/
|
|
||||||
public function up(): void
|
|
||||||
{
|
|
||||||
Schema::create('oauth_clients', function (Blueprint $table) {
|
|
||||||
$table->string('id', 255)->primary();
|
|
||||||
$table->string('client_id', 255)->unique();
|
|
||||||
$table->string('client_secret', 255)->nullable();
|
|
||||||
$table->text('redirect_uris')->nullable();
|
|
||||||
$table->text('scopes')->default('tenant:read tenant:write');
|
|
||||||
$table->timestamp('created_at')->useCurrent();
|
|
||||||
$table->timestamp('updated_at')->useCurrent()->useCurrentOnUpdate();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reverse the migrations.
|
|
||||||
*/
|
|
||||||
public function down(): void
|
|
||||||
{
|
|
||||||
Schema::dropIfExists('oauth_clients');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
use Illuminate\Database\Migrations\Migration;
|
|
||||||
use Illuminate\Database\Schema\Blueprint;
|
|
||||||
use Illuminate\Support\Facades\Schema;
|
|
||||||
|
|
||||||
return new class extends Migration
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Run the migrations.
|
|
||||||
*/
|
|
||||||
public function up(): void
|
|
||||||
{
|
|
||||||
Schema::create('refresh_tokens', function (Blueprint $table) {
|
|
||||||
$table->string('id', 255)->primary();
|
|
||||||
$table->string('tenant_id', 255)->index();
|
|
||||||
$table->string('token', 255)->unique()->index();
|
|
||||||
$table->string('access_token', 255)->nullable();
|
|
||||||
$table->timestamp('expires_at')->nullable();
|
|
||||||
$table->text('scope')->nullable();
|
|
||||||
$table->string('ip_address', 45)->nullable();
|
|
||||||
$table->text('user_agent')->nullable();
|
|
||||||
$table->timestamp('created_at')->useCurrent();
|
|
||||||
$table->timestamp('revoked_at')->nullable();
|
|
||||||
|
|
||||||
$table->index('expires_at');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reverse the migrations.
|
|
||||||
*/
|
|
||||||
public function down(): void
|
|
||||||
{
|
|
||||||
Schema::dropIfExists('refresh_tokens');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
use Illuminate\Database\Migrations\Migration;
|
|
||||||
use Illuminate\Database\Schema\Blueprint;
|
|
||||||
use Illuminate\Support\Facades\Schema;
|
|
||||||
|
|
||||||
return new class extends Migration
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Run the migrations.
|
|
||||||
*/
|
|
||||||
public function up(): void
|
|
||||||
{
|
|
||||||
Schema::create('tenant_tokens', function (Blueprint $table) {
|
|
||||||
$table->string('id', 255)->primary();
|
|
||||||
$table->string('tenant_id', 255)->index();
|
|
||||||
$table->string('jti', 255)->unique()->index();
|
|
||||||
$table->string('token_type', 50)->index();
|
|
||||||
$table->timestamp('expires_at');
|
|
||||||
$table->timestamp('revoked_at')->nullable();
|
|
||||||
$table->timestamp('created_at')->useCurrent();
|
|
||||||
|
|
||||||
$table->index('expires_at');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reverse the migrations.
|
|
||||||
*/
|
|
||||||
public function down(): void
|
|
||||||
{
|
|
||||||
Schema::dropIfExists('tenant_tokens');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
use Illuminate\Database\Migrations\Migration;
|
|
||||||
use Illuminate\Database\Schema\Blueprint;
|
|
||||||
use Illuminate\Support\Facades\Schema;
|
|
||||||
|
|
||||||
return new class extends Migration
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Run the migrations.
|
|
||||||
*/
|
|
||||||
public function up(): void
|
|
||||||
{
|
|
||||||
Schema::create('oauth_codes', function (Blueprint $table) {
|
|
||||||
$table->string('id', 255)->primary();
|
|
||||||
$table->string('client_id', 255);
|
|
||||||
$table->string('user_id', 255);
|
|
||||||
$table->string('code', 255)->unique()->index();
|
|
||||||
$table->string('code_challenge', 255);
|
|
||||||
$table->string('state', 255)->nullable();
|
|
||||||
$table->string('redirect_uri', 255)->nullable();
|
|
||||||
$table->text('scope')->nullable();
|
|
||||||
$table->timestamp('expires_at');
|
|
||||||
$table->timestamp('created_at')->useCurrent();
|
|
||||||
|
|
||||||
$table->index('expires_at');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reverse the migrations.
|
|
||||||
*/
|
|
||||||
public function down(): void
|
|
||||||
{
|
|
||||||
Schema::dropIfExists('oauth_codes');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
use Illuminate\Database\Migrations\Migration;
|
|
||||||
use Illuminate\Database\Schema\Blueprint;
|
|
||||||
use Illuminate\Support\Facades\Schema;
|
|
||||||
|
|
||||||
return new class extends Migration
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Run the migrations.
|
|
||||||
*/
|
|
||||||
public function up(): void
|
|
||||||
{
|
|
||||||
Schema::create('purchase_history', function (Blueprint $table) {
|
|
||||||
$table->string('id', 255)->primary();
|
|
||||||
$table->string('tenant_id', 255);
|
|
||||||
$table->string('package_id', 255);
|
|
||||||
$table->integer('credits_added')->default(0);
|
|
||||||
$table->decimal('price', 10, 2)->default(0);
|
|
||||||
$table->string('currency', 3)->default('EUR');
|
|
||||||
$table->string('platform', 50);
|
|
||||||
$table->string('transaction_id', 255)->nullable();
|
|
||||||
$table->timestamp('purchased_at')->useCurrent();
|
|
||||||
$table->timestamp('created_at')->useCurrent();
|
|
||||||
|
|
||||||
$table->foreign('tenant_id')->references('id')->on('tenants');
|
|
||||||
$table->index('tenant_id');
|
|
||||||
$table->index('purchased_at');
|
|
||||||
$table->index('transaction_id');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reverse the migrations.
|
|
||||||
*/
|
|
||||||
public function down(): void
|
|
||||||
{
|
|
||||||
Schema::dropIfExists('purchase_history');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -12,11 +12,11 @@ return new class extends Migration
|
|||||||
public function up(): void
|
public function up(): void
|
||||||
{
|
{
|
||||||
Schema::table('tenants', function (Blueprint $table) {
|
Schema::table('tenants', function (Blueprint $table) {
|
||||||
$table->string('subscription_tier')->default('free');
|
if (!Schema::hasColumn('tenants', 'subscription_status')) {
|
||||||
$table->timestamp('subscription_expires_at')->nullable();
|
$table->enum('subscription_status', ['free', 'active', 'suspended', 'expired'])->default('free')->after('subscription_tier');
|
||||||
$table->decimal('total_revenue', 10, 2)->default(0.00);
|
}
|
||||||
if (!Schema::hasColumn('tenants', 'event_credits_balance')) {
|
if (!Schema::hasColumn('tenants', 'subscription_expires_at')) {
|
||||||
$table->integer('event_credits_balance')->default(1);
|
$table->timestamp('subscription_expires_at')->nullable()->after('subscription_status');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -27,12 +27,7 @@ return new class extends Migration
|
|||||||
public function down(): void
|
public function down(): void
|
||||||
{
|
{
|
||||||
Schema::table('tenants', function (Blueprint $table) {
|
Schema::table('tenants', function (Blueprint $table) {
|
||||||
$table->dropColumn([
|
$table->dropColumn(['subscription_status', 'subscription_expires_at']);
|
||||||
'subscription_tier',
|
|
||||||
'subscription_expires_at',
|
|
||||||
'total_revenue',
|
|
||||||
'event_credits_balance'
|
|
||||||
]);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
use Illuminate\Database\Migrations\Migration;
|
|
||||||
use Illuminate\Database\Schema\Blueprint;
|
|
||||||
use Illuminate\Support\Facades\Schema;
|
|
||||||
|
|
||||||
return new class extends Migration
|
|
||||||
{
|
|
||||||
public function up(): void
|
|
||||||
{
|
|
||||||
// Add tenant_id to tasks table
|
|
||||||
Schema::table('tasks', function (Blueprint $table) {
|
|
||||||
if (!Schema::hasColumn('tasks', 'tenant_id')) {
|
|
||||||
$table->foreignId('tenant_id')->constrained('tenants')->onDelete('cascade')->after('id');
|
|
||||||
$table->index('tenant_id');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add tenant_id to task_collections table
|
|
||||||
Schema::table('task_collections', function (Blueprint $table) {
|
|
||||||
if (!Schema::hasColumn('task_collections', 'tenant_id')) {
|
|
||||||
$table->foreignId('tenant_id')->constrained('tenants')->onDelete('cascade')->after('id');
|
|
||||||
$table->index('tenant_id');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public function down(): void
|
|
||||||
{
|
|
||||||
Schema::table('tasks', function (Blueprint $table) {
|
|
||||||
$table->dropForeign(['tenant_id']);
|
|
||||||
$table->dropColumn('tenant_id');
|
|
||||||
});
|
|
||||||
|
|
||||||
Schema::table('task_collections', function (Blueprint $table) {
|
|
||||||
$table->dropForeign(['tenant_id']);
|
|
||||||
$table->dropColumn('tenant_id');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
use Illuminate\Database\Migrations\Migration;
|
|
||||||
use Illuminate\Database\Schema\Blueprint;
|
|
||||||
use Illuminate\Support\Facades\Schema;
|
|
||||||
|
|
||||||
return new class extends Migration
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Run the migrations.
|
|
||||||
*/
|
|
||||||
public function up(): void
|
|
||||||
{
|
|
||||||
Schema::table('tenants', function (Blueprint $table) {
|
|
||||||
$table->boolean('is_active')->default(true)->after('last_activity_at');
|
|
||||||
$table->boolean('is_suspended')->default(false)->after('is_active');
|
|
||||||
$table->json('settings')->nullable()->after('features');
|
|
||||||
$table->timestamp('settings_updated_at')->nullable()->after('settings');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reverse the migrations.
|
|
||||||
*/
|
|
||||||
public function down(): void
|
|
||||||
{
|
|
||||||
Schema::table('tenants', function (Blueprint $table) {
|
|
||||||
$table->dropColumn([
|
|
||||||
'is_active',
|
|
||||||
'is_suspended',
|
|
||||||
'settings',
|
|
||||||
'settings_updated_at'
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
use Illuminate\Database\Migrations\Migration;
|
|
||||||
use Illuminate\Database\Schema\Blueprint;
|
|
||||||
use Illuminate\Support\Facades\Schema;
|
|
||||||
|
|
||||||
return new class extends Migration
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Run the migrations.
|
|
||||||
*/
|
|
||||||
public function up(): void
|
|
||||||
{
|
|
||||||
Schema::table('events', function (Blueprint $table) {
|
|
||||||
$table->string('status')->default('draft')->after('is_active');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reverse the migrations.
|
|
||||||
*/
|
|
||||||
public function down(): void
|
|
||||||
{
|
|
||||||
Schema::table('events', function (Blueprint $table) {
|
|
||||||
$table->dropColumn('status');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
use Illuminate\Database\Migrations\Migration;
|
|
||||||
use Illuminate\Database\Schema\Blueprint;
|
|
||||||
use Illuminate\Support\Facades\Schema;
|
|
||||||
|
|
||||||
return new class extends Migration
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Run the migrations.
|
|
||||||
*/
|
|
||||||
public function up(): void
|
|
||||||
{
|
|
||||||
Schema::table('photos', function (Blueprint $table) {
|
|
||||||
$table->unsignedBigInteger('tenant_id')->nullable()->after('event_id');
|
|
||||||
$table->foreign('tenant_id')->references('id')->on('tenants');
|
|
||||||
$table->index('tenant_id');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reverse the migrations.
|
|
||||||
*/
|
|
||||||
public function down(): void
|
|
||||||
{
|
|
||||||
Schema::table('photos', function (Blueprint $table) {
|
|
||||||
$table->dropForeign(['tenant_id']);
|
|
||||||
$table->dropColumn('tenant_id');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
use Illuminate\Database\Migrations\Migration;
|
|
||||||
use Illuminate\Database\Schema\Blueprint;
|
|
||||||
use Illuminate\Support\Facades\Schema;
|
|
||||||
|
|
||||||
return new class extends Migration
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Run the migrations.
|
|
||||||
*/
|
|
||||||
public function up(): void
|
|
||||||
{
|
|
||||||
Schema::table('photos', function (Blueprint $table) {
|
|
||||||
if (!Schema::hasColumn('photos', 'tenant_id')) {
|
|
||||||
$table->unsignedBigInteger('tenant_id')->nullable()->after('event_id');
|
|
||||||
$table->foreign('tenant_id')->references('id')->on('tenants')->onDelete('cascade');
|
|
||||||
$table->index('tenant_id');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reverse the migrations.
|
|
||||||
*/
|
|
||||||
public function down(): void
|
|
||||||
{
|
|
||||||
Schema::table('photos', function (Blueprint $table) {
|
|
||||||
if (Schema::hasColumn('photos', 'tenant_id')) {
|
|
||||||
$table->dropForeign(['tenant_id']);
|
|
||||||
$table->dropColumn('tenant_id');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
use Illuminate\Database\Migrations\Migration;
|
|
||||||
use Illuminate\Database\Schema\Blueprint;
|
|
||||||
use Illuminate\Support\Facades\Schema;
|
|
||||||
|
|
||||||
return new class extends Migration
|
|
||||||
{
|
|
||||||
public function up()
|
|
||||||
{
|
|
||||||
Schema::table('tenants', function (Blueprint $table) {
|
|
||||||
$table->string('stripe_account_id')->nullable()->unique()->after('id');
|
|
||||||
$table->index('stripe_account_id');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public function down()
|
|
||||||
{
|
|
||||||
Schema::table('tenants', function (Blueprint $table) {
|
|
||||||
$table->dropIndex(['stripe_account_id']);
|
|
||||||
$table->dropColumn('stripe_account_id');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
use Illuminate\Database\Migrations\Migration;
|
|
||||||
use Illuminate\Database\Schema\Blueprint;
|
|
||||||
use Illuminate\Support\Facades\Schema;
|
|
||||||
|
|
||||||
return new class extends Migration
|
|
||||||
{
|
|
||||||
public function up(): void
|
|
||||||
{
|
|
||||||
Schema::create('event_credits_ledger', function (Blueprint $table) {
|
|
||||||
$table->id();
|
|
||||||
$table->foreignId('tenant_id')->constrained()->cascadeOnDelete();
|
|
||||||
$table->integer('delta');
|
|
||||||
$table->string('reason', 32); // purchase, event_create, manual_adjust, refund
|
|
||||||
$table->foreignId('related_purchase_id')->nullable()->constrained('event_purchases')->nullOnDelete();
|
|
||||||
$table->text('note')->nullable();
|
|
||||||
$table->timestamps();
|
|
||||||
$table->index(['tenant_id', 'created_at']);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public function down(): void
|
|
||||||
{
|
|
||||||
Schema::dropIfExists('event_credits_ledger');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
use Illuminate\Database\Migrations\Migration;
|
|
||||||
use Illuminate\Database\Schema\Blueprint;
|
|
||||||
use Illuminate\Support\Facades\Schema;
|
|
||||||
|
|
||||||
return new class extends Migration
|
|
||||||
{
|
|
||||||
public function up(): void
|
|
||||||
{
|
|
||||||
Schema::create('event_purchases', function (Blueprint $table) {
|
|
||||||
$table->id();
|
|
||||||
$table->foreignId('tenant_id')->constrained()->cascadeOnDelete();
|
|
||||||
$table->unsignedInteger('events_purchased')->default(1);
|
|
||||||
$table->decimal('amount', 10, 2);
|
|
||||||
$table->string('currency', 3)->default('EUR');
|
|
||||||
$table->string('provider', 32); // stripe, paypal, app_store, play_store
|
|
||||||
$table->string('external_receipt_id')->nullable();
|
|
||||||
$table->string('status', 16)->default('pending'); // pending, completed, failed
|
|
||||||
$table->timestamp('purchased_at')->nullable();
|
|
||||||
$table->timestamps();
|
|
||||||
$table->index(['tenant_id', 'purchased_at']);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public function down(): void
|
|
||||||
{
|
|
||||||
Schema::dropIfExists('event_purchases');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
use Illuminate\Database\Migrations\Migration;
|
|
||||||
use Illuminate\Support\Facades\DB;
|
|
||||||
|
|
||||||
return new class extends Migration
|
|
||||||
{
|
|
||||||
public function up(): void
|
|
||||||
{
|
|
||||||
DB::table('tenants')
|
|
||||||
->select('id', 'name')
|
|
||||||
->orderBy('id')
|
|
||||||
->chunkById(100, function ($tenants): void {
|
|
||||||
foreach ($tenants as $tenant) {
|
|
||||||
$raw = $tenant->name;
|
|
||||||
|
|
||||||
if ($raw === null || $raw === '') {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
$decoded = json_decode($raw, true);
|
|
||||||
$value = $raw;
|
|
||||||
|
|
||||||
if (json_last_error() === JSON_ERROR_NONE && is_array($decoded)) {
|
|
||||||
$preferred = $decoded['de'] ?? $decoded['en'] ?? null;
|
|
||||||
|
|
||||||
if ($preferred === null) {
|
|
||||||
foreach ($decoded as $entry) {
|
|
||||||
if (is_string($entry) && $entry !== '') {
|
|
||||||
$preferred = $entry;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$value = $preferred ?? (string) $raw;
|
|
||||||
}
|
|
||||||
|
|
||||||
DB::table('tenants')->where('id', $tenant->id)->update([
|
|
||||||
'name' => (string) $value,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public function down(): void
|
|
||||||
{
|
|
||||||
DB::table('tenants')
|
|
||||||
->select('id', 'name')
|
|
||||||
->orderBy('id')
|
|
||||||
->chunkById(100, function ($tenants): void {
|
|
||||||
foreach ($tenants as $tenant) {
|
|
||||||
$raw = $tenant->name;
|
|
||||||
|
|
||||||
if ($raw === null || $raw === '') {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
$localized = json_encode([
|
|
||||||
'de' => $raw,
|
|
||||||
'en' => $raw,
|
|
||||||
], JSON_UNESCAPED_UNICODE);
|
|
||||||
|
|
||||||
DB::table('tenants')->where('id', $tenant->id)->update([
|
|
||||||
'name' => $localized,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,79 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
use Illuminate\Database\Migrations\Migration;
|
|
||||||
use Illuminate\Database\Schema\Blueprint;
|
|
||||||
use Illuminate\Support\Facades\DB;
|
|
||||||
use Illuminate\Support\Facades\Schema;
|
|
||||||
|
|
||||||
return new class extends Migration
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Run the migrations.
|
|
||||||
*/
|
|
||||||
public function up(): void
|
|
||||||
{
|
|
||||||
if (! Schema::hasColumn('oauth_clients', 'is_active')) {
|
|
||||||
Schema::table('oauth_clients', function (Blueprint $table) {
|
|
||||||
$table->boolean('is_active')->default(true);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
$clients = DB::table('oauth_clients')->get(['id', 'scopes', 'redirect_uris', 'is_active']);
|
|
||||||
|
|
||||||
foreach ($clients as $client) {
|
|
||||||
$scopes = $this->normaliseValue($client->scopes, ['tenant:read', 'tenant:write']);
|
|
||||||
$redirects = $this->normaliseValue($client->redirect_uris);
|
|
||||||
|
|
||||||
DB::table('oauth_clients')
|
|
||||||
->where('id', $client->id)
|
|
||||||
->update([
|
|
||||||
'scopes' => $scopes === null ? null : json_encode($scopes),
|
|
||||||
'redirect_uris' => $redirects === null ? null : json_encode($redirects),
|
|
||||||
'is_active' => $client->is_active ?? true,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reverse the migrations.
|
|
||||||
*/
|
|
||||||
public function down(): void
|
|
||||||
{
|
|
||||||
if (Schema::hasColumn('oauth_clients', 'is_active')) {
|
|
||||||
Schema::table('oauth_clients', function (Blueprint $table) {
|
|
||||||
$table->dropColumn('is_active');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private function normaliseValue(mixed $value, ?array $fallback = null): ?array
|
|
||||||
{
|
|
||||||
if ($value === null) {
|
|
||||||
return $fallback;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (is_array($value)) {
|
|
||||||
return $this->cleanArray($value) ?: $fallback;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (is_string($value)) {
|
|
||||||
$decoded = json_decode($value, true);
|
|
||||||
if (json_last_error() === JSON_ERROR_NONE && is_array($decoded)) {
|
|
||||||
return $this->cleanArray($decoded) ?: $fallback;
|
|
||||||
}
|
|
||||||
|
|
||||||
$parts = preg_split('/[\r\n,]+/', $value) ?: [];
|
|
||||||
return $this->cleanArray($parts) ?: $fallback;
|
|
||||||
}
|
|
||||||
|
|
||||||
return $fallback;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function cleanArray(array $items): array
|
|
||||||
{
|
|
||||||
$items = array_map(fn ($item) => is_string($item) ? trim($item) : $item, $items);
|
|
||||||
$items = array_filter($items, fn ($item) => ! ($item === null || $item === ''));
|
|
||||||
|
|
||||||
return array_values($items);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
use Illuminate\Database\Migrations\Migration;
|
|
||||||
use Illuminate\Database\Schema\Blueprint;
|
|
||||||
use Illuminate\Support\Facades\Schema;
|
|
||||||
|
|
||||||
return new class extends Migration
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Run the migrations.
|
|
||||||
*/
|
|
||||||
public function up(): void
|
|
||||||
{
|
|
||||||
Schema::table('oauth_clients', function (Blueprint $table) {
|
|
||||||
if (!Schema::hasColumn('oauth_clients', 'tenant_id')) {
|
|
||||||
$table->foreignId('tenant_id')
|
|
||||||
->nullable()
|
|
||||||
->after('client_secret')
|
|
||||||
->constrained('tenants')
|
|
||||||
->nullOnDelete();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reverse the migrations.
|
|
||||||
*/
|
|
||||||
public function down(): void
|
|
||||||
{
|
|
||||||
Schema::table('oauth_clients', function (Blueprint $table) {
|
|
||||||
if (Schema::hasColumn('oauth_clients', 'tenant_id')) {
|
|
||||||
$table->dropConstrainedForeignId('tenant_id');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
use Illuminate\Database\Migrations\Migration;
|
|
||||||
use Illuminate\Database\Schema\Blueprint;
|
|
||||||
use Illuminate\Support\Facades\Schema;
|
|
||||||
|
|
||||||
return new class extends Migration
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Run the migrations.
|
|
||||||
*/
|
|
||||||
public function up(): void
|
|
||||||
{
|
|
||||||
Schema::table('refresh_tokens', function (Blueprint $table) {
|
|
||||||
if (!Schema::hasColumn('refresh_tokens', 'client_id')) {
|
|
||||||
$table->string('client_id', 255)->nullable()->after('tenant_id')->index();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reverse the migrations.
|
|
||||||
*/
|
|
||||||
public function down(): void
|
|
||||||
{
|
|
||||||
Schema::table('refresh_tokens', function (Blueprint $table) {
|
|
||||||
if (Schema::hasColumn('refresh_tokens', 'client_id')) {
|
|
||||||
$table->dropColumn('client_id');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
use Illuminate\Database\Migrations\Migration;
|
|
||||||
use Illuminate\Database\Schema\Blueprint;
|
|
||||||
use Illuminate\Support\Facades\Schema;
|
|
||||||
|
|
||||||
return new class extends Migration
|
|
||||||
{
|
|
||||||
public function up(): void
|
|
||||||
{
|
|
||||||
Schema::table('tasks', function (Blueprint $table) {
|
|
||||||
if (! Schema::hasColumn('tasks', 'deleted_at')) {
|
|
||||||
$table->softDeletes();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public function down(): void
|
|
||||||
{
|
|
||||||
Schema::table('tasks', function (Blueprint $table) {
|
|
||||||
if (Schema::hasColumn('tasks', 'deleted_at')) {
|
|
||||||
$table->dropSoftDeletes();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
use Illuminate\Database\Migrations\Migration;
|
|
||||||
use Illuminate\Database\Schema\Blueprint;
|
|
||||||
use Illuminate\Support\Facades\Schema;
|
|
||||||
|
|
||||||
return new class extends Migration
|
|
||||||
{
|
|
||||||
public function up(): void
|
|
||||||
{
|
|
||||||
Schema::table('tenants', function (Blueprint $table) {
|
|
||||||
$table->string('custom_domain')->nullable()->after('domain');
|
|
||||||
});
|
|
||||||
|
|
||||||
Schema::table('tenants', function (Blueprint $table) {
|
|
||||||
$table->unique('custom_domain');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public function down(): void
|
|
||||||
{
|
|
||||||
Schema::table('tenants', function (Blueprint $table) {
|
|
||||||
$table->dropUnique('tenants_custom_domain_unique');
|
|
||||||
$table->dropColumn('custom_domain');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
336
database/migrations/2025_09_26_000000_create_packages_system.php
Normal file
336
database/migrations/2025_09_26_000000_create_packages_system.php
Normal file
@@ -0,0 +1,336 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
// Packages table
|
||||||
|
if (!Schema::hasTable('packages')) {
|
||||||
|
Schema::create('packages', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->string('name');
|
||||||
|
$table->enum('type', ['endcustomer', 'reseller']);
|
||||||
|
$table->decimal('price', 8, 2);
|
||||||
|
$table->integer('max_photos')->nullable();
|
||||||
|
$table->integer('max_guests')->nullable();
|
||||||
|
$table->integer('gallery_days')->nullable();
|
||||||
|
$table->integer('max_tasks')->nullable();
|
||||||
|
$table->boolean('watermark_allowed')->default(true);
|
||||||
|
$table->boolean('branding_allowed')->default(false);
|
||||||
|
$table->integer('max_events_per_year')->nullable();
|
||||||
|
$table->timestamp('expires_after')->nullable();
|
||||||
|
$table->json('features')->nullable();
|
||||||
|
$table->timestamps();
|
||||||
|
$table->index(['type', 'price']);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Seed standard packages if empty
|
||||||
|
if (DB::table('packages')->count() == 0) {
|
||||||
|
DB::table('packages')->insert([
|
||||||
|
[
|
||||||
|
'name' => 'Free/Test',
|
||||||
|
'type' => 'endcustomer',
|
||||||
|
'price' => 0.00,
|
||||||
|
'max_photos' => 30,
|
||||||
|
'max_guests' => 10,
|
||||||
|
'gallery_days' => 3,
|
||||||
|
'max_tasks' => 1,
|
||||||
|
'watermark_allowed' => true,
|
||||||
|
'branding_allowed' => false,
|
||||||
|
'max_events_per_year' => null,
|
||||||
|
'expires_after' => null,
|
||||||
|
'features' => json_encode([]),
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => 'Starter',
|
||||||
|
'type' => 'endcustomer',
|
||||||
|
'price' => 19.00,
|
||||||
|
'max_photos' => 300,
|
||||||
|
'max_guests' => 50,
|
||||||
|
'gallery_days' => 14,
|
||||||
|
'max_tasks' => 5,
|
||||||
|
'watermark_allowed' => true,
|
||||||
|
'branding_allowed' => false,
|
||||||
|
'max_events_per_year' => null,
|
||||||
|
'expires_after' => null,
|
||||||
|
'features' => json_encode([]),
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => 'Reseller S',
|
||||||
|
'type' => 'reseller',
|
||||||
|
'price' => 149.00,
|
||||||
|
'max_photos' => null,
|
||||||
|
'max_guests' => null,
|
||||||
|
'gallery_days' => null,
|
||||||
|
'max_tasks' => null,
|
||||||
|
'watermark_allowed' => true,
|
||||||
|
'branding_allowed' => true,
|
||||||
|
'max_events_per_year' => 5,
|
||||||
|
'expires_after' => now()->addYear(),
|
||||||
|
'features' => json_encode(['limited_branding']),
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
],
|
||||||
|
// Add more as needed
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Event Packages
|
||||||
|
if (!Schema::hasTable('event_packages')) {
|
||||||
|
Schema::create('event_packages', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->foreignId('event_id')->constrained()->cascadeOnDelete();
|
||||||
|
$table->foreignId('package_id')->constrained()->cascadeOnDelete();
|
||||||
|
$table->decimal('purchased_price', 8, 2);
|
||||||
|
$table->timestamp('purchased_at');
|
||||||
|
$table->integer('used_photos')->default(0);
|
||||||
|
$table->timestamps();
|
||||||
|
$table->index('event_id');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tenant Packages
|
||||||
|
if (!Schema::hasTable('tenant_packages')) {
|
||||||
|
Schema::create('tenant_packages', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->foreignId('tenant_id')->constrained()->cascadeOnDelete();
|
||||||
|
$table->foreignId('package_id')->constrained()->cascadeOnDelete();
|
||||||
|
$table->decimal('price', 8, 2);
|
||||||
|
$table->timestamp('purchased_at');
|
||||||
|
$table->timestamp('expires_at');
|
||||||
|
$table->integer('used_events')->default(0);
|
||||||
|
$table->boolean('active')->default(true);
|
||||||
|
$table->timestamps();
|
||||||
|
$table->index(['tenant_id', 'active']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Package Purchases
|
||||||
|
if (!Schema::hasTable('package_purchases')) {
|
||||||
|
Schema::create('package_purchases', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->foreignId('tenant_id')->nullable()->constrained();
|
||||||
|
$table->foreignId('event_id')->nullable()->constrained();
|
||||||
|
$table->foreignId('package_id')->constrained();
|
||||||
|
$table->string('provider_id');
|
||||||
|
$table->decimal('price', 8, 2);
|
||||||
|
$table->timestamp('purchased_at');
|
||||||
|
$table->enum('type', ['endcustomer_event', 'reseller_subscription']);
|
||||||
|
$table->json('metadata')->nullable();
|
||||||
|
$table->string('ip_address')->nullable();
|
||||||
|
$table->string('user_agent')->nullable();
|
||||||
|
$table->boolean('refunded')->default(false);
|
||||||
|
$table->timestamps();
|
||||||
|
$table->index(['tenant_id', 'created_at']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Purchase History
|
||||||
|
if (!Schema::hasTable('purchase_history')) {
|
||||||
|
Schema::create('purchase_history', function (Blueprint $table) {
|
||||||
|
$table->string('id', 255)->primary();
|
||||||
|
$table->string('tenant_id', 255);
|
||||||
|
$table->string('package_id', 255);
|
||||||
|
$table->integer('credits_added')->default(0);
|
||||||
|
$table->decimal('price', 10, 2)->default(0);
|
||||||
|
$table->string('currency', 3)->default('EUR');
|
||||||
|
$table->string('platform', 50);
|
||||||
|
$table->string('transaction_id', 255)->nullable();
|
||||||
|
$table->timestamp('purchased_at')->useCurrent();
|
||||||
|
$table->timestamp('created_at')->useCurrent();
|
||||||
|
$table->foreign('tenant_id')->references('id')->on('tenants');
|
||||||
|
$table->index('tenant_id');
|
||||||
|
$table->index('purchased_at');
|
||||||
|
$table->index('transaction_id');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add subscription fields to tenants if missing
|
||||||
|
if (Schema::hasTable('tenants')) {
|
||||||
|
if (!Schema::hasColumn('tenants', 'subscription_tier')) {
|
||||||
|
Schema::table('tenants', function (Blueprint $table) {
|
||||||
|
$table->string('subscription_tier')->default('free')->after('event_credits_balance');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (!Schema::hasColumn('tenants', 'subscription_status')) {
|
||||||
|
Schema::table('tenants', function (Blueprint $table) {
|
||||||
|
$table->enum('subscription_status', ['free', 'active', 'suspended', 'expired'])->default('free')->after('subscription_tier');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (!Schema::hasColumn('tenants', 'subscription_expires_at')) {
|
||||||
|
Schema::table('tenants', function (Blueprint $table) {
|
||||||
|
$table->timestamp('subscription_expires_at')->nullable()->after('subscription_status');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (!Schema::hasColumn('tenants', 'total_revenue')) {
|
||||||
|
Schema::table('tenants', function (Blueprint $table) {
|
||||||
|
$table->decimal('total_revenue', 10, 2)->default(0.00)->after('subscription_expires_at');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Idempotent migration from credits to packages (only if old tables exist and new don't have data)
|
||||||
|
if (Schema::hasTable('event_credits_ledger') && DB::table('tenant_packages')->count() == 0) {
|
||||||
|
// Migrate tenant credits to tenant_packages (Free package)
|
||||||
|
$freePackageId = DB::table('packages')->where('name', 'Free/Test')->value('id');
|
||||||
|
if ($freePackageId) {
|
||||||
|
DB::table('tenants')->where('event_credits_balance', '>', 0)->chunk(100, function ($tenants) use ($freePackageId) {
|
||||||
|
foreach ($tenants as $tenant) {
|
||||||
|
DB::table('tenant_packages')->insertOrIgnore([
|
||||||
|
'tenant_id' => $tenant->id,
|
||||||
|
'package_id' => $freePackageId,
|
||||||
|
'price' => 0.00,
|
||||||
|
'purchased_at' => $tenant->free_event_granted_at ?? now(),
|
||||||
|
'expires_at' => now()->addDays(30),
|
||||||
|
'used_events' => min($tenant->event_credits_balance, 1),
|
||||||
|
'active' => true,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
DB::table('package_purchases')->insertOrIgnore([
|
||||||
|
'tenant_id' => $tenant->id,
|
||||||
|
'event_id' => null,
|
||||||
|
'package_id' => $freePackageId,
|
||||||
|
'provider_id' => 'migration_free',
|
||||||
|
'price' => 0.00,
|
||||||
|
'type' => 'reseller_subscription',
|
||||||
|
'metadata' => json_encode(['migrated_from_credits' => $tenant->event_credits_balance]),
|
||||||
|
'ip_address' => null,
|
||||||
|
'user_agent' => null,
|
||||||
|
'refunded' => false,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Migrate event purchases if old data exists
|
||||||
|
if (Schema::hasTable('event_purchases')) {
|
||||||
|
DB::table('event_purchases')->join('events', 'event_purchases.event_id', '=', 'events.id')->chunk(100, function ($purchases) use ($freePackageId) {
|
||||||
|
foreach ($purchases as $purchase) {
|
||||||
|
DB::table('event_packages')->insertOrIgnore([
|
||||||
|
'event_id' => $purchase->event_id,
|
||||||
|
'package_id' => $freePackageId,
|
||||||
|
'purchased_price' => $purchase->amount ?? 0.00,
|
||||||
|
'purchased_at' => $purchase->purchased_at ?? now(),
|
||||||
|
'used_photos' => 0,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
DB::table('package_purchases')->insertOrIgnore([
|
||||||
|
'tenant_id' => $purchase->tenant_id,
|
||||||
|
'event_id' => $purchase->event_id,
|
||||||
|
'package_id' => $freePackageId,
|
||||||
|
'provider_id' => $purchase->provider ?? 'migration',
|
||||||
|
'price' => $purchase->amount ?? 0.00,
|
||||||
|
'type' => 'endcustomer_event',
|
||||||
|
'metadata' => json_encode(['migrated_from_event_purchases' => true]),
|
||||||
|
'ip_address' => null,
|
||||||
|
'user_agent' => null,
|
||||||
|
'refunded' => false,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Conditional drop of old credits tables and fields (only if migration happened or old structures exist)
|
||||||
|
if (Schema::hasTable('event_credits_ledger')) {
|
||||||
|
Schema::dropIfExists('event_credits_ledger');
|
||||||
|
}
|
||||||
|
if (Schema::hasTable('event_purchases')) {
|
||||||
|
Schema::dropIfExists('event_purchases');
|
||||||
|
}
|
||||||
|
if (Schema::hasTable('purchase_history') && DB::table('package_purchases')->count() > 0) { // Only drop if new data exists
|
||||||
|
Schema::dropIfExists('purchase_history');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Drop old fields from tenants if new system is in place
|
||||||
|
if (Schema::hasTable('tenants')) {
|
||||||
|
$oldFields = ['event_credits_balance', 'free_event_granted_at'];
|
||||||
|
foreach ($oldFields as $field) {
|
||||||
|
if (Schema::hasColumn('tenants', $field) && DB::table('tenant_packages')->count() > 0) {
|
||||||
|
Schema::table('tenants', function (Blueprint $table) use ($field) {
|
||||||
|
$table->dropColumn($field);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
if (app()->environment('local', 'testing')) {
|
||||||
|
// Reverse drops and adds
|
||||||
|
if (!Schema::hasTable('purchase_history')) {
|
||||||
|
Schema::create('purchase_history', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->foreignId('tenant_id')->constrained()->cascadeOnDelete();
|
||||||
|
$table->string('package_id', 255);
|
||||||
|
$table->integer('credits_added')->default(0);
|
||||||
|
$table->decimal('price', 10, 2)->default(0);
|
||||||
|
$table->string('provider_id');
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (!Schema::hasTable('event_purchases')) {
|
||||||
|
Schema::create('event_purchases', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->foreignId('tenant_id')->constrained()->cascadeOnDelete();
|
||||||
|
$table->unsignedInteger('events_purchased')->default(1);
|
||||||
|
$table->decimal('amount', 10, 2);
|
||||||
|
$table->string('currency', 3)->default('EUR');
|
||||||
|
$table->string('provider', 32);
|
||||||
|
$table->string('external_receipt_id')->nullable();
|
||||||
|
$table->string('status', 16)->default('pending');
|
||||||
|
$table->timestamp('purchased_at')->nullable();
|
||||||
|
$table->timestamps();
|
||||||
|
$table->index(['tenant_id', 'purchased_at']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (!Schema::hasTable('event_credits_ledger')) {
|
||||||
|
Schema::create('event_credits_ledger', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->foreignId('tenant_id')->constrained()->cascadeOnDelete();
|
||||||
|
$table->integer('delta');
|
||||||
|
$table->string('reason', 32);
|
||||||
|
$table->foreignId('related_purchase_id')->nullable()->constrained('event_purchases')->nullOnDelete();
|
||||||
|
$table->text('note')->nullable();
|
||||||
|
$table->timestamps();
|
||||||
|
$table->index(['tenant_id', 'created_at']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-add old fields to tenants
|
||||||
|
Schema::table('tenants', function (Blueprint $table) {
|
||||||
|
if (!Schema::hasColumn('tenants', 'event_credits_balance')) {
|
||||||
|
$table->integer('event_credits_balance')->default(1);
|
||||||
|
}
|
||||||
|
if (!Schema::hasColumn('tenants', 'free_event_granted_at')) {
|
||||||
|
$table->timestamp('free_event_granted_at')->nullable();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Drop new tables
|
||||||
|
Schema::dropIfExists('package_purchases');
|
||||||
|
Schema::dropIfExists('tenant_packages');
|
||||||
|
Schema::dropIfExists('event_packages');
|
||||||
|
Schema::dropIfExists('packages');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
127
database/migrations/2025_09_26_000100_create_blog_system.php
Normal file
127
database/migrations/2025_09_26_000100_create_blog_system.php
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
// Blog Categories
|
||||||
|
if (!Schema::hasTable('blog_categories')) {
|
||||||
|
Schema::create('blog_categories', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->string('name')->nullable();
|
||||||
|
$table->string('slug')->unique();
|
||||||
|
$table->longText('description')->nullable();
|
||||||
|
$table->json('translations')->nullable(); // From add_translations
|
||||||
|
$table->boolean('is_visible')->default(false);
|
||||||
|
$table->date('deleted_at')->nullable();
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
if (!Schema::hasColumn('blog_categories', 'name')) {
|
||||||
|
Schema::table('blog_categories', function (Blueprint $table) {
|
||||||
|
$table->string('name')->nullable()->after('id');
|
||||||
|
$table->longText('description')->nullable()->after('name');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (!Schema::hasColumn('blog_categories', 'translations')) {
|
||||||
|
Schema::table('blog_categories', function (Blueprint $table) {
|
||||||
|
$table->json('translations')->nullable()->after('description');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Blog Authors
|
||||||
|
if (!Schema::hasTable('blog_authors')) {
|
||||||
|
Schema::create('blog_authors', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->string('name');
|
||||||
|
$table->string('email')->unique();
|
||||||
|
$table->string('photo')->nullable();
|
||||||
|
$table->longText('bio')->nullable();
|
||||||
|
$table->string('github_handle')->nullable();
|
||||||
|
$table->string('twitter_handle')->nullable();
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Blog Posts
|
||||||
|
if (!Schema::hasTable('blog_posts')) {
|
||||||
|
Schema::create('blog_posts', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->foreignId('blog_author_id')->nullable()->constrained()->cascadeOnDelete();
|
||||||
|
$table->foreignId('blog_category_id')->nullable()->constrained()->nullOnDelete();
|
||||||
|
$table->string('title');
|
||||||
|
$table->string('slug')->unique();
|
||||||
|
$table->text('excerpt')->nullable();
|
||||||
|
$table->string('banner')->nullable();
|
||||||
|
$table->longText('content');
|
||||||
|
$table->json('translations')->nullable(); // From add_translations
|
||||||
|
$table->date('published_at')->nullable();
|
||||||
|
$table->date('deleted_at')->nullable();
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
if (!Schema::hasColumn('blog_posts', 'translations')) {
|
||||||
|
Schema::table('blog_posts', function (Blueprint $table) {
|
||||||
|
$table->json('translations')->nullable()->after('content');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tags
|
||||||
|
if (!Schema::hasTable('tags')) {
|
||||||
|
Schema::create('tags', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->json('name');
|
||||||
|
$table->json('slug');
|
||||||
|
$table->string('type')->nullable();
|
||||||
|
$table->integer('order_column')->nullable();
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Taggables (polymorphic)
|
||||||
|
if (!Schema::hasTable('taggables')) {
|
||||||
|
Schema::create('taggables', function (Blueprint $table) {
|
||||||
|
$table->foreignId('tag_id')->constrained()->cascadeOnDelete();
|
||||||
|
$table->morphs('taggable');
|
||||||
|
$table->unique(['tag_id', 'taggable_id', 'taggable_type']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
if (app()->environment('local', 'testing')) {
|
||||||
|
if (Schema::hasTable('taggables')) {
|
||||||
|
Schema::table('taggables', function (Blueprint $table) {
|
||||||
|
$table->dropForeign(['tag_id']);
|
||||||
|
});
|
||||||
|
Schema::dropIfExists('taggables');
|
||||||
|
}
|
||||||
|
Schema::dropIfExists('tags');
|
||||||
|
if (Schema::hasColumn('blog_posts', 'translations')) {
|
||||||
|
Schema::table('blog_posts', function (Blueprint $table) {
|
||||||
|
$table->dropColumn('translations');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Schema::dropIfExists('blog_posts');
|
||||||
|
Schema::dropIfExists('blog_authors');
|
||||||
|
if (Schema::hasColumn('blog_categories', 'translations')) {
|
||||||
|
Schema::table('blog_categories', function (Blueprint $table) {
|
||||||
|
$table->dropColumn('translations');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (Schema::hasColumn('blog_categories', 'name')) {
|
||||||
|
Schema::table('blog_categories', function (Blueprint $table) {
|
||||||
|
$table->dropColumn(['name', 'description']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Schema::dropIfExists('blog_categories');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
use Illuminate\Database\Migrations\Migration;
|
|
||||||
use Illuminate\Database\Schema\Blueprint;
|
|
||||||
use Illuminate\Support\Facades\Schema;
|
|
||||||
|
|
||||||
return new class () extends Migration {
|
|
||||||
/**
|
|
||||||
* Run the migrations.
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function up()
|
|
||||||
{
|
|
||||||
Schema::create('blog_categories', function (Blueprint $table) {
|
|
||||||
$table->id();
|
|
||||||
$table->string('name');
|
|
||||||
$table->string('slug')->unique();
|
|
||||||
$table->longText('description')->nullable();
|
|
||||||
$table->boolean('is_visible')->default(false);
|
|
||||||
$table->timestamps();
|
|
||||||
});
|
|
||||||
|
|
||||||
Schema::create('blog_authors', function (Blueprint $table) {
|
|
||||||
$table->id();
|
|
||||||
$table->string('name');
|
|
||||||
$table->string('email')->unique();
|
|
||||||
$table->string('photo')->nullable();
|
|
||||||
$table->longText('bio')->nullable();
|
|
||||||
$table->string('github_handle')->nullable();
|
|
||||||
$table->string('twitter_handle')->nullable();
|
|
||||||
$table->timestamps();
|
|
||||||
});
|
|
||||||
|
|
||||||
Schema::create('blog_posts', function (Blueprint $table) {
|
|
||||||
$table->id();
|
|
||||||
$table->foreignId('blog_author_id')->nullable()->constrained()->cascadeOnDelete();
|
|
||||||
$table->foreignId('blog_category_id')->nullable()->constrained()->nullOnDelete();
|
|
||||||
$table->string('title');
|
|
||||||
$table->string('slug')->unique();
|
|
||||||
$table->text('excerpt')->nullable();
|
|
||||||
$table->string('banner')->nullable();
|
|
||||||
$table->longText('content');
|
|
||||||
$table->date('published_at')->nullable();
|
|
||||||
$table->timestamps();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reverse the migrations.
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function down()
|
|
||||||
{
|
|
||||||
Schema::dropIfExists('blog_posts');
|
|
||||||
Schema::dropIfExists('blog_categories');
|
|
||||||
Schema::dropIfExists('blog_authors');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
use Illuminate\Database\Migrations\Migration;
|
|
||||||
use Illuminate\Database\Schema\Blueprint;
|
|
||||||
use Illuminate\Support\Facades\Schema;
|
|
||||||
|
|
||||||
return new class extends Migration
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Run the migrations.
|
|
||||||
*/
|
|
||||||
public function up(): void
|
|
||||||
{
|
|
||||||
Schema::table('blog_posts', function (Blueprint $table) {
|
|
||||||
$table->json('translations')->nullable();
|
|
||||||
});
|
|
||||||
|
|
||||||
Schema::table('blog_categories', function (Blueprint $table) {
|
|
||||||
$table->json('translations')->nullable();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reverse the migrations.
|
|
||||||
*/
|
|
||||||
public function down(): void
|
|
||||||
{
|
|
||||||
Schema::table('blog_posts', function (Blueprint $table) {
|
|
||||||
$table->dropColumn('translations');
|
|
||||||
});
|
|
||||||
|
|
||||||
Schema::table('blog_categories', function (Blueprint $table) {
|
|
||||||
$table->dropColumn('translations');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
use Illuminate\Database\Migrations\Migration;
|
|
||||||
use Illuminate\Database\Schema\Blueprint;
|
|
||||||
use Illuminate\Support\Facades\Schema;
|
|
||||||
|
|
||||||
return new class extends Migration
|
|
||||||
{
|
|
||||||
public function up(): void
|
|
||||||
{
|
|
||||||
Schema::create('tags', function (Blueprint $table) {
|
|
||||||
$table->id();
|
|
||||||
|
|
||||||
$table->json('name');
|
|
||||||
$table->json('slug');
|
|
||||||
$table->string('type')->nullable();
|
|
||||||
$table->integer('order_column')->nullable();
|
|
||||||
|
|
||||||
$table->timestamps();
|
|
||||||
});
|
|
||||||
|
|
||||||
Schema::create('taggables', function (Blueprint $table) {
|
|
||||||
$table->foreignId('tag_id')->constrained()->cascadeOnDelete();
|
|
||||||
|
|
||||||
$table->morphs('taggable');
|
|
||||||
|
|
||||||
$table->unique(['tag_id', 'taggable_id', 'taggable_type']);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public function down(): void
|
|
||||||
{
|
|
||||||
Schema::dropIfExists('taggables');
|
|
||||||
Schema::dropIfExists('tags');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
use Illuminate\Database\Migrations\Migration;
|
|
||||||
use Illuminate\Database\Schema\Blueprint;
|
|
||||||
use Illuminate\Support\Facades\Schema;
|
|
||||||
|
|
||||||
return new class extends Migration
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Run the migrations.
|
|
||||||
*/
|
|
||||||
public function up(): void
|
|
||||||
{
|
|
||||||
Schema::create('packages', function (Blueprint $table) {
|
|
||||||
$table->id();
|
|
||||||
$table->string('name');
|
|
||||||
$table->enum('type', ['endcustomer', 'reseller']);
|
|
||||||
$table->decimal('price', 8, 2);
|
|
||||||
$table->integer('max_photos')->nullable();
|
|
||||||
$table->integer('max_guests')->nullable();
|
|
||||||
$table->integer('gallery_days')->nullable();
|
|
||||||
$table->integer('max_tasks')->nullable();
|
|
||||||
$table->boolean('watermark_allowed')->default(true);
|
|
||||||
$table->boolean('branding_allowed')->default(false);
|
|
||||||
$table->integer('max_events_per_year')->nullable();
|
|
||||||
$table->timestamp('expires_after')->nullable();
|
|
||||||
$table->json('features')->nullable();
|
|
||||||
$table->timestamps();
|
|
||||||
$table->index(['type', 'price']);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reverse the migrations.
|
|
||||||
*/
|
|
||||||
public function down(): void
|
|
||||||
{
|
|
||||||
Schema::dropIfExists('packages');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
use Illuminate\Database\Migrations\Migration;
|
|
||||||
use Illuminate\Database\Schema\Blueprint;
|
|
||||||
use Illuminate\Support\Facades\Schema;
|
|
||||||
|
|
||||||
return new class extends Migration
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Run the migrations.
|
|
||||||
*/
|
|
||||||
public function up(): void
|
|
||||||
{
|
|
||||||
Schema::create('event_packages', function (Blueprint $table) {
|
|
||||||
$table->id();
|
|
||||||
$table->foreignId('event_id')->constrained()->cascadeOnDelete();
|
|
||||||
$table->foreignId('package_id')->constrained()->cascadeOnDelete();
|
|
||||||
$table->decimal('purchased_price', 8, 2);
|
|
||||||
$table->timestamp('purchased_at');
|
|
||||||
$table->integer('used_photos')->default(0);
|
|
||||||
$table->timestamps();
|
|
||||||
$table->index('event_id');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reverse the migrations.
|
|
||||||
*/
|
|
||||||
public function down(): void
|
|
||||||
{
|
|
||||||
Schema::dropIfExists('event_packages');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
use Illuminate\Database\Migrations\Migration;
|
|
||||||
use Illuminate\Database\Schema\Blueprint;
|
|
||||||
use Illuminate\Support\Facades\Schema;
|
|
||||||
|
|
||||||
return new class extends Migration
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Run the migrations.
|
|
||||||
*/
|
|
||||||
public function up(): void
|
|
||||||
{
|
|
||||||
Schema::create('tenant_packages', function (Blueprint $table) {
|
|
||||||
$table->id();
|
|
||||||
$table->foreignId('tenant_id')->constrained()->cascadeOnDelete();
|
|
||||||
$table->foreignId('package_id')->constrained()->cascadeOnDelete();
|
|
||||||
$table->decimal('price', 8, 2);
|
|
||||||
$table->timestamp('purchased_at');
|
|
||||||
$table->timestamp('expires_at');
|
|
||||||
$table->integer('used_events')->default(0);
|
|
||||||
$table->boolean('active')->default(true);
|
|
||||||
$table->timestamps();
|
|
||||||
$table->index(['tenant_id', 'active']);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reverse the migrations.
|
|
||||||
*/
|
|
||||||
public function down(): void
|
|
||||||
{
|
|
||||||
Schema::dropIfExists('tenant_packages');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
use Illuminate\Database\Migrations\Migration;
|
|
||||||
use Illuminate\Database\Schema\Blueprint;
|
|
||||||
use Illuminate\Support\Facades\Schema;
|
|
||||||
|
|
||||||
return new class extends Migration
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Run the migrations.
|
|
||||||
*/
|
|
||||||
public function up(): void
|
|
||||||
{
|
|
||||||
Schema::create('package_purchases', function (Blueprint $table) {
|
|
||||||
$table->id();
|
|
||||||
$table->foreignId('tenant_id')->nullable()->constrained();
|
|
||||||
$table->foreignId('event_id')->nullable()->constrained();
|
|
||||||
$table->foreignId('package_id')->constrained();
|
|
||||||
$table->string('provider_id');
|
|
||||||
$table->decimal('price', 8, 2);
|
|
||||||
$table->enum('type', ['endcustomer_event', 'reseller_subscription']);
|
|
||||||
$table->json('metadata')->nullable();
|
|
||||||
$table->string('ip_address')->nullable();
|
|
||||||
$table->string('user_agent')->nullable();
|
|
||||||
$table->boolean('refunded')->default(false);
|
|
||||||
$table->timestamps();
|
|
||||||
$table->index(['tenant_id', 'purchased_at']);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reverse the migrations.
|
|
||||||
*/
|
|
||||||
public function down(): void
|
|
||||||
{
|
|
||||||
Schema::dropIfExists('package_purchases');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,152 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
use Illuminate\Database\Migrations\Migration;
|
|
||||||
use Illuminate\Database\Schema\Blueprint;
|
|
||||||
use Illuminate\Support\Facades\Schema;
|
|
||||||
use Illuminate\Support\Facades\DB;
|
|
||||||
use App\Models\Tenant;
|
|
||||||
use App\Models\Event;
|
|
||||||
|
|
||||||
return new class extends Migration
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Run the migrations.
|
|
||||||
*/
|
|
||||||
public function up(): void
|
|
||||||
{
|
|
||||||
// Ensure packages table has data (seed if empty)
|
|
||||||
if (DB::table('packages')->count() == 0) {
|
|
||||||
// Insert standard packages if not seeded
|
|
||||||
DB::table('packages')->insert([
|
|
||||||
[
|
|
||||||
'name' => 'Free/Test',
|
|
||||||
'type' => 'endcustomer',
|
|
||||||
'price' => 0.00,
|
|
||||||
'max_photos' => 30,
|
|
||||||
'max_guests' => 10,
|
|
||||||
'gallery_days' => 3,
|
|
||||||
'max_tasks' => 1,
|
|
||||||
'watermark_allowed' => true,
|
|
||||||
'branding_allowed' => false,
|
|
||||||
'max_events_per_year' => null,
|
|
||||||
'expires_after' => null,
|
|
||||||
'features' => json_encode([]),
|
|
||||||
'created_at' => now(),
|
|
||||||
'updated_at' => now(),
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'name' => 'Starter',
|
|
||||||
'type' => 'endcustomer',
|
|
||||||
'price' => 19.00,
|
|
||||||
'max_photos' => 300,
|
|
||||||
'max_guests' => 50,
|
|
||||||
'gallery_days' => 14,
|
|
||||||
'max_tasks' => 5,
|
|
||||||
'watermark_allowed' => true,
|
|
||||||
'branding_allowed' => false,
|
|
||||||
'max_events_per_year' => null,
|
|
||||||
'expires_after' => null,
|
|
||||||
'features' => json_encode([]),
|
|
||||||
'created_at' => now(),
|
|
||||||
'updated_at' => now(),
|
|
||||||
],
|
|
||||||
// Add more standard packages as per plan
|
|
||||||
[
|
|
||||||
'name' => 'Reseller S',
|
|
||||||
'type' => 'reseller',
|
|
||||||
'price' => 149.00,
|
|
||||||
'max_photos' => null,
|
|
||||||
'max_guests' => null,
|
|
||||||
'gallery_days' => null,
|
|
||||||
'max_tasks' => null,
|
|
||||||
'watermark_allowed' => true,
|
|
||||||
'branding_allowed' => true,
|
|
||||||
'max_events_per_year' => 5,
|
|
||||||
'expires_after' => now()->addYear(),
|
|
||||||
'features' => json_encode(['limited_branding']),
|
|
||||||
'created_at' => now(),
|
|
||||||
'updated_at' => now(),
|
|
||||||
],
|
|
||||||
// ... other reseller packages
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Migrate tenant credits to tenant_packages (Free package)
|
|
||||||
DB::table('tenants')->where('event_credits_balance', '>', 0)->orderBy('id')->chunk(100, function ($tenants) {
|
|
||||||
foreach ($tenants as $tenant) {
|
|
||||||
$freePackageId = DB::table('packages')->where('name', 'Free/Test')->first()->id;
|
|
||||||
DB::table('tenant_packages')->insert([
|
|
||||||
'tenant_id' => $tenant->id,
|
|
||||||
'package_id' => $freePackageId,
|
|
||||||
'price' => 0.00,
|
|
||||||
'purchased_at' => $tenant->free_event_granted_at ?? now(),
|
|
||||||
'expires_at' => now()->addDays(30), // or based on credits
|
|
||||||
'used_events' => min($tenant->event_credits_balance, 1), // e.g. 1 free event
|
|
||||||
'active' => true,
|
|
||||||
'created_at' => now(),
|
|
||||||
'updated_at' => now(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Create purchase ledger entry
|
|
||||||
DB::table('package_purchases')->insert([
|
|
||||||
'tenant_id' => $tenant->id,
|
|
||||||
'event_id' => null,
|
|
||||||
'package_id' => $freePackageId,
|
|
||||||
'provider_id' => 'migration_free',
|
|
||||||
'price' => 0.00,
|
|
||||||
'type' => 'reseller_subscription',
|
|
||||||
'metadata' => json_encode(['migrated_from_credits' => $tenant->event_credits_balance]),
|
|
||||||
'ip_address' => null,
|
|
||||||
'user_agent' => null,
|
|
||||||
'refunded' => false,
|
|
||||||
'created_at' => now(),
|
|
||||||
'updated_at' => now(),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Migrate event purchases to event_packages (if any existing events)
|
|
||||||
DB::table('events')->orderBy('id')->chunk(100, function ($events) {
|
|
||||||
foreach ($events as $event) {
|
|
||||||
if ($event->tenant->event_credits_balance > 0) { // or check if event was created with credits
|
|
||||||
$freePackageId = DB::table('packages')->where('name', 'Free/Test')->first()->id;
|
|
||||||
DB::table('event_packages')->insert([
|
|
||||||
'event_id' => $event->id,
|
|
||||||
'package_id' => $freePackageId,
|
|
||||||
'purchased_price' => 0.00,
|
|
||||||
'purchased_at' => $event->created_at,
|
|
||||||
'used_photos' => 0,
|
|
||||||
'created_at' => now(),
|
|
||||||
'updated_at' => now(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Ledger entry
|
|
||||||
DB::table('package_purchases')->insert([
|
|
||||||
'tenant_id' => $event->tenant_id,
|
|
||||||
'event_id' => $event->id,
|
|
||||||
'package_id' => $freePackageId,
|
|
||||||
'provider_id' => 'migration_free',
|
|
||||||
'price' => 0.00,
|
|
||||||
'type' => 'endcustomer_event',
|
|
||||||
'metadata' => json_encode(['migrated_from_credits' => true]),
|
|
||||||
'ip_address' => null,
|
|
||||||
'user_agent' => null,
|
|
||||||
'refunded' => false,
|
|
||||||
'created_at' => now(),
|
|
||||||
'updated_at' => now(),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reverse the migrations.
|
|
||||||
*/
|
|
||||||
public function down(): void
|
|
||||||
{
|
|
||||||
Schema::table('packages', function (Blueprint $table) {
|
|
||||||
//
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,92 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
use Illuminate\Database\Migrations\Migration;
|
|
||||||
use Illuminate\Database\Schema\Blueprint;
|
|
||||||
use Illuminate\Support\Facades\Schema;
|
|
||||||
|
|
||||||
return new class extends Migration
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Run the migrations.
|
|
||||||
*/
|
|
||||||
public function up(): void
|
|
||||||
{
|
|
||||||
Schema::dropIfExists('event_credits_ledger');
|
|
||||||
Schema::dropIfExists('purchase_history');
|
|
||||||
Schema::dropIfExists('event_purchases');
|
|
||||||
|
|
||||||
if (Schema::hasTable('package_purchases')) {
|
|
||||||
Schema::table('package_purchases', function (Blueprint $table) {
|
|
||||||
$table->dropIndex(['tenant_id', 'purchased_at']);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Schema::hasColumn('tenants', 'event_credits_balance')) {
|
|
||||||
Schema::table('tenants', function (Blueprint $table) {
|
|
||||||
$table->dropColumn('event_credits_balance');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (Schema::hasColumn('tenants', 'subscription_tier')) {
|
|
||||||
Schema::table('tenants', function (Blueprint $table) {
|
|
||||||
$table->dropColumn('subscription_tier');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (Schema::hasColumn('tenants', 'subscription_expires_at')) {
|
|
||||||
Schema::table('tenants', function (Blueprint $table) {
|
|
||||||
$table->dropColumn('subscription_expires_at');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (Schema::hasColumn('tenants', 'free_event_granted_at')) {
|
|
||||||
Schema::table('tenants', function (Blueprint $table) {
|
|
||||||
$table->dropColumn('free_event_granted_at');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (Schema::hasColumn('tenants', 'total_revenue')) {
|
|
||||||
Schema::table('tenants', function (Blueprint $table) {
|
|
||||||
$table->dropColumn('total_revenue');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reverse the migrations.
|
|
||||||
*/
|
|
||||||
public function down(): void
|
|
||||||
{
|
|
||||||
Schema::table('tenants', function (Blueprint $table) {
|
|
||||||
$table->integer('event_credits_balance')->default(1);
|
|
||||||
$table->string('subscription_tier')->nullable();
|
|
||||||
$table->timestamp('subscription_expires_at')->nullable();
|
|
||||||
$table->timestamp('free_event_granted_at')->nullable();
|
|
||||||
$table->decimal('total_revenue', 10, 2)->default(0.00);
|
|
||||||
});
|
|
||||||
|
|
||||||
Schema::create('event_purchases', function (Blueprint $table) {
|
|
||||||
$table->id();
|
|
||||||
$table->foreignId('tenant_id')->constrained()->cascadeOnDelete();
|
|
||||||
$table->foreignId('event_id')->constrained()->cascadeOnDelete();
|
|
||||||
$table->integer('credits_added')->default(0);
|
|
||||||
$table->decimal('price', 10, 2)->default(0);
|
|
||||||
$table->string('provider_id');
|
|
||||||
$table->timestamps();
|
|
||||||
});
|
|
||||||
|
|
||||||
Schema::create('purchase_history', function (Blueprint $table) {
|
|
||||||
$table->id();
|
|
||||||
$table->foreignId('tenant_id')->constrained()->cascadeOnDelete();
|
|
||||||
$table->string('package_id', 255);
|
|
||||||
$table->integer('credits_added')->default(0);
|
|
||||||
$table->decimal('price', 10, 2)->default(0);
|
|
||||||
$table->string('provider_id');
|
|
||||||
$table->timestamps();
|
|
||||||
});
|
|
||||||
|
|
||||||
Schema::create('event_credits_ledger', function (Blueprint $table) {
|
|
||||||
$table->id();
|
|
||||||
$table->foreignId('tenant_id')->constrained()->cascadeOnDelete();
|
|
||||||
$table->integer('credits_change');
|
|
||||||
$table->string('reason');
|
|
||||||
$table->timestamps();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -12,10 +12,18 @@ return new class extends Migration
|
|||||||
public function up(): void
|
public function up(): void
|
||||||
{
|
{
|
||||||
Schema::table('users', function (Blueprint $table) {
|
Schema::table('users', function (Blueprint $table) {
|
||||||
$table->string('first_name')->nullable()->after('name');
|
if (!Schema::hasColumn('tenants', 'first_name')) {
|
||||||
$table->string('last_name')->nullable()->after('first_name');
|
$table->string('first_name')->default('')->after('name');
|
||||||
$table->text('address')->nullable()->after('last_name');
|
}
|
||||||
|
if (!Schema::hasColumn('tenants', 'last_name')) {
|
||||||
|
$table->string('last_name')->default('')->after('first_name');
|
||||||
|
}
|
||||||
|
if (!Schema::hasColumn('tenants', 'address')) {
|
||||||
|
$table->string('address')->nullable()->after('last_name');
|
||||||
|
}
|
||||||
|
if (!Schema::hasColumn('tenants', 'phone')) {
|
||||||
$table->string('phone')->nullable()->after('address');
|
$table->string('phone')->nullable()->after('address');
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,29 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
use Illuminate\Database\Migrations\Migration;
|
|
||||||
use Illuminate\Database\Schema\Blueprint;
|
|
||||||
use Illuminate\Support\Facades\Schema;
|
|
||||||
|
|
||||||
return new class extends Migration
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Run the migrations.
|
|
||||||
*/
|
|
||||||
public function up(): void
|
|
||||||
{
|
|
||||||
Schema::table('tenants', function (Blueprint $table) {
|
|
||||||
$table->foreignId('user_id')->nullable()->constrained('users')->onDelete('cascade')->after('id');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reverse the migrations.
|
|
||||||
*/
|
|
||||||
public function down(): void
|
|
||||||
{
|
|
||||||
Schema::table('tenants', function (Blueprint $table) {
|
|
||||||
$table->dropForeign(['user_id']);
|
|
||||||
$table->dropColumn('user_id');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
use Illuminate\Database\Migrations\Migration;
|
|
||||||
use Illuminate\Database\Schema\Blueprint;
|
|
||||||
use Illuminate\Support\Facades\Schema;
|
|
||||||
|
|
||||||
return new class extends Migration
|
|
||||||
{
|
|
||||||
public function up(): void
|
|
||||||
{
|
|
||||||
Schema::table('blog_categories', function (Blueprint $table) {
|
|
||||||
$table->string('name')->nullable()->after('id');
|
|
||||||
$table->text('description')->nullable()->after('name');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public function down(): void
|
|
||||||
{
|
|
||||||
Schema::table('blog_categories', function (Blueprint $table) {
|
|
||||||
$table->dropColumn(['name', 'description']);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
use Illuminate\Database\Migrations\Migration;
|
|
||||||
use Illuminate\Database\Schema\Blueprint;
|
|
||||||
use Illuminate\Support\Facades\Schema;
|
|
||||||
|
|
||||||
return new class extends Migration
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Run the migrations.
|
|
||||||
*/
|
|
||||||
public function up(): void
|
|
||||||
{
|
|
||||||
Schema::table('blog_categories', function (Blueprint $table) {
|
|
||||||
$table->dropColumn(['name', 'description']);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reverse the migrations.
|
|
||||||
*/
|
|
||||||
public function down(): void
|
|
||||||
{
|
|
||||||
Schema::table('blog_categories', function (Blueprint $table) {
|
|
||||||
$table->string('name')->after('id');
|
|
||||||
$table->text('description')->nullable()->after('name');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
use Illuminate\Database\Migrations\Migration;
|
|
||||||
use Illuminate\Database\Schema\Blueprint;
|
|
||||||
use Illuminate\Support\Facades\Schema;
|
|
||||||
|
|
||||||
return new class extends Migration
|
|
||||||
{
|
|
||||||
public function up(): void
|
|
||||||
{
|
|
||||||
Schema::table('blog_categories', function (Blueprint $table) {
|
|
||||||
if (!Schema::hasColumn('blog_categories', 'name')) {
|
|
||||||
$table->json('name')->nullable()->after('id');
|
|
||||||
}
|
|
||||||
if (!Schema::hasColumn('blog_categories', 'description')) {
|
|
||||||
$table->json('description')->nullable()->after('name');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public function down(): void
|
|
||||||
{
|
|
||||||
Schema::table('blog_categories', function (Blueprint $table) {
|
|
||||||
$table->dropColumn(['name', 'description']);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
use Illuminate\Database\Migrations\Migration;
|
|
||||||
use Illuminate\Database\Schema\Blueprint;
|
|
||||||
use Illuminate\Support\Facades\Schema;
|
|
||||||
|
|
||||||
return new class extends Migration
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Run the migrations.
|
|
||||||
*/
|
|
||||||
public function up(): void
|
|
||||||
{
|
|
||||||
Schema::table('blog_categories', function (Blueprint $table) {
|
|
||||||
$table->json('name')->nullable()->change();
|
|
||||||
$table->json('description')->nullable()->change();
|
|
||||||
$table->dropColumn('translations');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reverse the migrations.
|
|
||||||
*/
|
|
||||||
public function down(): void
|
|
||||||
{
|
|
||||||
Schema::table('blog_categories', function (Blueprint $table) {
|
|
||||||
$table->string('name')->nullable()->change();
|
|
||||||
$table->text('description')->nullable()->change();
|
|
||||||
$table->json('translations')->nullable();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -11,8 +11,8 @@ return new class extends Migration
|
|||||||
*/
|
*/
|
||||||
public function up(): void
|
public function up(): void
|
||||||
{
|
{
|
||||||
Schema::table('tenants', function (Blueprint $table) {
|
Schema::table('package_purchases', function (Blueprint $table) {
|
||||||
$table->string('email')->nullable()->after('slug');
|
$table->string('provider_id')->nullable()->change();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -21,8 +21,8 @@ return new class extends Migration
|
|||||||
*/
|
*/
|
||||||
public function down(): void
|
public function down(): void
|
||||||
{
|
{
|
||||||
Schema::table('tenants', function (Blueprint $table) {
|
Schema::table('package_purchases', function (Blueprint $table) {
|
||||||
$table->dropColumn('email');
|
$table->string('provider_id')->nullable(false)->change();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('tenants', function (Blueprint $table) {
|
||||||
|
if (!Schema::hasColumn('tenants', 'is_suspended')) {
|
||||||
|
$table->boolean('is_suspended')->default(false)->after('is_active');
|
||||||
|
}
|
||||||
|
if (!Schema::hasColumn('tenants', 'settings')) {
|
||||||
|
$table->json('settings')->nullable()->after('subscription_expires_at');
|
||||||
|
}
|
||||||
|
if (!Schema::hasColumn('tenants', 'settings_updated_at')) {
|
||||||
|
$table->timestamp('settings_updated_at')->nullable()->after('settings');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('tenants', function (Blueprint $table) {
|
||||||
|
$table->dropColumn(['is_suspended', 'settings', 'settings_updated_at']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('packages', function (Blueprint $table) {
|
||||||
|
if (!Schema::hasColumn('packages', 'slug')) {
|
||||||
|
$table->string('slug')->default('')->after('name');
|
||||||
|
}
|
||||||
|
if (!Schema::hasColumn('packages', 'description')) {
|
||||||
|
$table->text('description')->nullable()->after('slug');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update existing packages with unique slugs
|
||||||
|
if (Schema::hasTable('packages')) {
|
||||||
|
$packages = DB::table('packages')->get();
|
||||||
|
foreach ($packages as $package) {
|
||||||
|
$slug = Str::slug($package->name . '-' . $package->id);
|
||||||
|
DB::table('packages')->where('id', $package->id)->update(['slug' => $slug]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add unique index after updating
|
||||||
|
Schema::table('packages', function (Blueprint $table) {
|
||||||
|
$table->unique('slug');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('packages', function (Blueprint $table) {
|
||||||
|
$table->dropUnique(['slug']);
|
||||||
|
});
|
||||||
|
Schema::table('packages', function (Blueprint $table) {
|
||||||
|
$table->dropColumn(['slug', 'description']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -19,9 +19,9 @@ class DemoEventSeeder extends Seeder
|
|||||||
'description' => ['de'=>'Demo-Event','en'=>'Demo event'],
|
'description' => ['de'=>'Demo-Event','en'=>'Demo event'],
|
||||||
'date' => now()->addMonths(3)->toDateString(),
|
'date' => now()->addMonths(3)->toDateString(),
|
||||||
'event_type_id' => $type->id,
|
'event_type_id' => $type->id,
|
||||||
'status' => 'published',
|
'status' => 'active',
|
||||||
'is_active' => true,
|
'is_active' => true,
|
||||||
'settings' => [],
|
'settings' => json_encode([]),
|
||||||
'default_locale' => 'de',
|
'default_locale' => 'de',
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ namespace Database\Seeders;
|
|||||||
use Illuminate\Database\Seeder;
|
use Illuminate\Database\Seeder;
|
||||||
use App\Models\Package;
|
use App\Models\Package;
|
||||||
use App\Enums\PackageType;
|
use App\Enums\PackageType;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
class PackageSeeder extends Seeder
|
class PackageSeeder extends Seeder
|
||||||
{
|
{
|
||||||
@@ -16,6 +17,7 @@ class PackageSeeder extends Seeder
|
|||||||
// Endcustomer Packages
|
// Endcustomer Packages
|
||||||
Package::create([
|
Package::create([
|
||||||
'name' => 'Free / Test',
|
'name' => 'Free / Test',
|
||||||
|
'slug' => Str::slug('Free / Test'),
|
||||||
'type' => PackageType::ENDCUSTOMER,
|
'type' => PackageType::ENDCUSTOMER,
|
||||||
'price' => 0.00,
|
'price' => 0.00,
|
||||||
'max_photos' => 30,
|
'max_photos' => 30,
|
||||||
@@ -33,6 +35,7 @@ class PackageSeeder extends Seeder
|
|||||||
|
|
||||||
Package::create([
|
Package::create([
|
||||||
'name' => 'Starter',
|
'name' => 'Starter',
|
||||||
|
'slug' => Str::slug('Starter'),
|
||||||
'type' => PackageType::ENDCUSTOMER,
|
'type' => PackageType::ENDCUSTOMER,
|
||||||
'price' => 29.00,
|
'price' => 29.00,
|
||||||
'max_photos' => 200,
|
'max_photos' => 200,
|
||||||
@@ -51,6 +54,7 @@ class PackageSeeder extends Seeder
|
|||||||
|
|
||||||
Package::create([
|
Package::create([
|
||||||
'name' => 'Pro',
|
'name' => 'Pro',
|
||||||
|
'slug' => Str::slug('Pro'),
|
||||||
'type' => PackageType::ENDCUSTOMER,
|
'type' => PackageType::ENDCUSTOMER,
|
||||||
'price' => 79.00,
|
'price' => 79.00,
|
||||||
'max_photos' => 1000,
|
'max_photos' => 1000,
|
||||||
@@ -72,6 +76,7 @@ class PackageSeeder extends Seeder
|
|||||||
// Reseller Packages
|
// Reseller Packages
|
||||||
Package::create([
|
Package::create([
|
||||||
'name' => 'S (Small Reseller)',
|
'name' => 'S (Small Reseller)',
|
||||||
|
'slug' => Str::slug('S (Small Reseller)'),
|
||||||
'type' => PackageType::RESELLER,
|
'type' => PackageType::RESELLER,
|
||||||
'price' => 199.00,
|
'price' => 199.00,
|
||||||
'max_photos' => 500, // per event limit
|
'max_photos' => 500, // per event limit
|
||||||
@@ -91,6 +96,7 @@ class PackageSeeder extends Seeder
|
|||||||
|
|
||||||
Package::create([
|
Package::create([
|
||||||
'name' => 'M (Medium Reseller)',
|
'name' => 'M (Medium Reseller)',
|
||||||
|
'slug' => Str::slug('M (Medium Reseller)'),
|
||||||
'type' => PackageType::RESELLER,
|
'type' => PackageType::RESELLER,
|
||||||
'price' => 399.00,
|
'price' => 399.00,
|
||||||
'max_photos' => 1000, // per event limit
|
'max_photos' => 1000, // per event limit
|
||||||
|
|||||||
@@ -16,7 +16,9 @@ class TasksSeeder extends Seeder
|
|||||||
'name' => 'Demo Tenant',
|
'name' => 'Demo Tenant',
|
||||||
'domain' => null,
|
'domain' => null,
|
||||||
'is_active' => true,
|
'is_active' => true,
|
||||||
'settings' => [],
|
'is_suspended' => false,
|
||||||
|
'settings' => json_encode([]),
|
||||||
|
'settings_updated_at' => null,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
38
package-lock.json
generated
38
package-lock.json
generated
@@ -22,6 +22,7 @@
|
|||||||
"@radix-ui/react-toggle-group": "^1.1.2",
|
"@radix-ui/react-toggle-group": "^1.1.2",
|
||||||
"@radix-ui/react-tooltip": "^1.1.8",
|
"@radix-ui/react-tooltip": "^1.1.8",
|
||||||
"@tailwindcss/vite": "^4.1.11",
|
"@tailwindcss/vite": "^4.1.11",
|
||||||
|
"@tanstack/react-query": "^5.90.2",
|
||||||
"@types/react": "^19.0.3",
|
"@types/react": "^19.0.3",
|
||||||
"@types/react-dom": "^19.0.2",
|
"@types/react-dom": "^19.0.2",
|
||||||
"@vitejs/plugin-react": "^4.6.0",
|
"@vitejs/plugin-react": "^4.6.0",
|
||||||
@@ -44,7 +45,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.19.0",
|
"@eslint/js": "^9.19.0",
|
||||||
"@laravel/vite-plugin-wayfinder": "^0.1.3",
|
"@laravel/vite-plugin-wayfinder": "^0.1.7",
|
||||||
"@playwright/test": "^1.55.0",
|
"@playwright/test": "^1.55.0",
|
||||||
"@types/node": "^22.13.5",
|
"@types/node": "^22.13.5",
|
||||||
"eslint": "^9.17.0",
|
"eslint": "^9.17.0",
|
||||||
@@ -1627,10 +1628,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@laravel/vite-plugin-wayfinder": {
|
"node_modules/@laravel/vite-plugin-wayfinder": {
|
||||||
"version": "0.1.3",
|
"version": "0.1.7",
|
||||||
"resolved": "https://registry.npmjs.org/@laravel/vite-plugin-wayfinder/-/vite-plugin-wayfinder-0.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/@laravel/vite-plugin-wayfinder/-/vite-plugin-wayfinder-0.1.7.tgz",
|
||||||
"integrity": "sha512-S/21Lzl7lci7LrRo/VsN5AXT02AMf7rs+OPTyt3VPgffBB1wTrzwsPr28sCU0gcR/APhfC1eVIUwpLbAvBmyKw==",
|
"integrity": "sha512-yZYIr1iwuCQ7LFI+GsJk9vacw1HWMp3ZlDlW0pdfz3zXyKeu4US7oH79KmQQ031L0cYaSyaUMo/Ha1D4BosKqw==",
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@modelcontextprotocol/sdk": {
|
"node_modules/@modelcontextprotocol/sdk": {
|
||||||
"version": "1.18.0",
|
"version": "1.18.0",
|
||||||
@@ -3348,6 +3350,32 @@
|
|||||||
"vite": "^5.2.0 || ^6 || ^7"
|
"vite": "^5.2.0 || ^6 || ^7"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@tanstack/query-core": {
|
||||||
|
"version": "5.90.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.90.2.tgz",
|
||||||
|
"integrity": "sha512-k/TcR3YalnzibscALLwxeiLUub6jN5EDLwKDiO7q5f4ICEoptJ+n9+7vcEFy5/x/i6Q+Lb/tXrsKCggf5uQJXQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/tannerlinsley"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tanstack/react-query": {
|
||||||
|
"version": "5.90.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.2.tgz",
|
||||||
|
"integrity": "sha512-CLABiR+h5PYfOWr/z+vWFt5VsOA2ekQeRQBFSKlcoW6Ndx/f8rfyVmq4LbgOM4GG2qtxAxjLYLOpCNTYm4uKzw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@tanstack/query-core": "5.90.2"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/tannerlinsley"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^18 || ^19"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@tanstack/react-virtual": {
|
"node_modules/@tanstack/react-virtual": {
|
||||||
"version": "3.13.12",
|
"version": "3.13.12",
|
||||||
"resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.13.12.tgz",
|
"resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.13.12.tgz",
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.19.0",
|
"@eslint/js": "^9.19.0",
|
||||||
"@laravel/vite-plugin-wayfinder": "^0.1.3",
|
"@laravel/vite-plugin-wayfinder": "^0.1.7",
|
||||||
"@playwright/test": "^1.55.0",
|
"@playwright/test": "^1.55.0",
|
||||||
"@types/node": "^22.13.5",
|
"@types/node": "^22.13.5",
|
||||||
"eslint": "^9.17.0",
|
"eslint": "^9.17.0",
|
||||||
@@ -43,6 +43,7 @@
|
|||||||
"@radix-ui/react-toggle-group": "^1.1.2",
|
"@radix-ui/react-toggle-group": "^1.1.2",
|
||||||
"@radix-ui/react-tooltip": "^1.1.8",
|
"@radix-ui/react-tooltip": "^1.1.8",
|
||||||
"@tailwindcss/vite": "^4.1.11",
|
"@tailwindcss/vite": "^4.1.11",
|
||||||
|
"@tanstack/react-query": "^5.90.2",
|
||||||
"@types/react": "^19.0.3",
|
"@types/react": "^19.0.3",
|
||||||
"@types/react-dom": "^19.0.2",
|
"@types/react-dom": "^19.0.2",
|
||||||
"@vitejs/plugin-react": "^4.6.0",
|
"@vitejs/plugin-react": "^4.6.0",
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { RouterProvider } from 'react-router-dom';
|
|||||||
import { AuthProvider } from './auth/context';
|
import { AuthProvider } from './auth/context';
|
||||||
import { router } from './router';
|
import { router } from './router';
|
||||||
import '../../css/app.css';
|
import '../../css/app.css';
|
||||||
import { initializeTheme } from '@/hooks/use-appearance';
|
import { initializeTheme } from '@/hooks/use-appearance.tsx';
|
||||||
|
|
||||||
initializeTheme();
|
initializeTheme();
|
||||||
const rootEl = document.getElementById('root')!;
|
const rootEl = document.getElementById('root')!;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import ProfileController from '@/actions/App/Http/Controllers/Settings/ProfileController';
|
import { destroy } from '@/actions/App/Http/Controllers/Settings/ProfileController';
|
||||||
import HeadingSmall from '@/components/heading-small';
|
import HeadingSmall from '@/components/heading-small';
|
||||||
import InputError from '@/components/input-error';
|
import InputError from '@/components/input-error';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSep
|
|||||||
import { UserInfo } from '@/components/user-info';
|
import { UserInfo } from '@/components/user-info';
|
||||||
import { useMobileNavigation } from '@/hooks/use-mobile-navigation';
|
import { useMobileNavigation } from '@/hooks/use-mobile-navigation';
|
||||||
import { logout } from '@/routes';
|
import { logout } from '@/routes';
|
||||||
import { edit } from '@/routes/profile';
|
import { edit } from '@/routes/settings/profile';
|
||||||
import { type User } from '@/types';
|
import { type User } from '@/types';
|
||||||
import { Link, router } from '@inertiajs/react';
|
import { Link, router } from '@inertiajs/react';
|
||||||
import { LogOut, Settings } from 'lucide-react';
|
import { LogOut, Settings } from 'lucide-react';
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { createRoot } from 'react-dom/client';
|
|||||||
import { RouterProvider } from 'react-router-dom';
|
import { RouterProvider } from 'react-router-dom';
|
||||||
import { router } from './router';
|
import { router } from './router';
|
||||||
import '../../css/app.css';
|
import '../../css/app.css';
|
||||||
import { initializeTheme } from '@/hooks/use-appearance';
|
import { initializeTheme } from '@/hooks/use-appearance.tsx';
|
||||||
import { ToastProvider } from './components/ToastHost';
|
import { ToastProvider } from './components/ToastHost';
|
||||||
|
|
||||||
initializeTheme();
|
initializeTheme();
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { Separator } from '@/components/ui/separator';
|
|||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { appearance } from '@/routes';
|
import { appearance } from '@/routes';
|
||||||
import { edit as editPassword } from '@/routes/password';
|
import { edit as editPassword } from '@/routes/password';
|
||||||
import { edit } from '@/routes/profile';
|
import { edit } from '@/routes/settings/profile';
|
||||||
import { type NavItem } from '@/types';
|
import { type NavItem } from '@/types';
|
||||||
import { Link } from '@inertiajs/react';
|
import { Link } from '@inertiajs/react';
|
||||||
import { type PropsWithChildren } from 'react';
|
import { type PropsWithChildren } from 'react';
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import ConfirmablePasswordController from '@/actions/App/Http/Controllers/Auth/ConfirmablePasswordController';
|
import { store } from '@/actions/App/Http/Controllers/Auth/ConfirmablePasswordController';
|
||||||
import InputError from '@/components/input-error';
|
import InputError from '@/components/input-error';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
@@ -15,7 +15,7 @@ export default function ConfirmPassword() {
|
|||||||
>
|
>
|
||||||
<Head title="Confirm password" />
|
<Head title="Confirm password" />
|
||||||
|
|
||||||
<Form {...ConfirmablePasswordController.store.form()} resetOnSuccess={['password']}>
|
<Form {...store.form()} resetOnSuccess={['password']}>
|
||||||
{({ processing, errors }) => (
|
{({ processing, errors }) => (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div className="grid gap-2">
|
<div className="grid gap-2">
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// Components
|
// Components
|
||||||
import PasswordResetLinkController from '@/actions/App/Http/Controllers/Auth/PasswordResetLinkController';
|
import { store } from '@/actions/App/Http/Controllers/Auth/PasswordResetLinkController';
|
||||||
import { login } from '@/routes';
|
import { login } from '@/routes';
|
||||||
import { Form, Head } from '@inertiajs/react';
|
import { Form, Head } from '@inertiajs/react';
|
||||||
import { LoaderCircle } from 'lucide-react';
|
import { LoaderCircle } from 'lucide-react';
|
||||||
@@ -19,7 +19,7 @@ export default function ForgotPassword({ status }: { status?: string }) {
|
|||||||
{status && <div className="mb-4 text-center text-sm font-medium text-green-600">{status}</div>}
|
{status && <div className="mb-4 text-center text-sm font-medium text-green-600">{status}</div>}
|
||||||
|
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<Form {...PasswordResetLinkController.store.form()}>
|
<Form {...store.form()}>
|
||||||
{({ processing, errors }) => (
|
{({ processing, errors }) => (
|
||||||
<>
|
<>
|
||||||
<div className="grid gap-2">
|
<div className="grid gap-2">
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import AuthenticatedSessionController from '@/actions/App/Http/Controllers/Auth/AuthenticatedSessionController';
|
import { useForm, router } from '@inertiajs/react';
|
||||||
import InputError from '@/components/input-error';
|
import InputError from '@/components/input-error';
|
||||||
import TextLink from '@/components/text-link';
|
import TextLink from '@/components/text-link';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
@@ -8,7 +8,7 @@ import { Label } from '@/components/ui/label';
|
|||||||
import AuthLayout from '@/layouts/auth-layout';
|
import AuthLayout from '@/layouts/auth-layout';
|
||||||
import { register } from '@/routes';
|
import { register } from '@/routes';
|
||||||
import { request } from '@/routes/password';
|
import { request } from '@/routes/password';
|
||||||
import { Form, Head } from '@inertiajs/react';
|
import { Head } from '@inertiajs/react';
|
||||||
import { LoaderCircle } from 'lucide-react';
|
import { LoaderCircle } from 'lucide-react';
|
||||||
|
|
||||||
interface LoginProps {
|
interface LoginProps {
|
||||||
@@ -17,13 +17,22 @@ interface LoginProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function Login({ status, canResetPassword }: LoginProps) {
|
export default function Login({ status, canResetPassword }: LoginProps) {
|
||||||
|
const { data, setData, post, processing, errors } = useForm({
|
||||||
|
email: '',
|
||||||
|
password: '',
|
||||||
|
remember: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const submit = (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
post('/login');
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AuthLayout title="Log in to your account" description="Enter your email and password below to log in">
|
<AuthLayout title="Log in to your account" description="Enter your email and password below to log in">
|
||||||
<Head title="Log in" />
|
<Head title="Log in" />
|
||||||
|
|
||||||
<Form {...AuthenticatedSessionController.store.form()} resetOnSuccess={['password']} className="flex flex-col gap-6">
|
<form onSubmit={submit} className="flex flex-col gap-6">
|
||||||
{({ processing, errors }) => (
|
|
||||||
<>
|
|
||||||
<div className="grid gap-6">
|
<div className="grid gap-6">
|
||||||
<div className="grid gap-2">
|
<div className="grid gap-2">
|
||||||
<Label htmlFor="email">Email address</Label>
|
<Label htmlFor="email">Email address</Label>
|
||||||
@@ -36,6 +45,8 @@ export default function Login({ status, canResetPassword }: LoginProps) {
|
|||||||
tabIndex={1}
|
tabIndex={1}
|
||||||
autoComplete="email"
|
autoComplete="email"
|
||||||
placeholder="email@example.com"
|
placeholder="email@example.com"
|
||||||
|
value={data.email}
|
||||||
|
onChange={(e) => setData('email', e.target.value)}
|
||||||
/>
|
/>
|
||||||
<InputError message={errors.email} />
|
<InputError message={errors.email} />
|
||||||
</div>
|
</div>
|
||||||
@@ -57,12 +68,20 @@ export default function Login({ status, canResetPassword }: LoginProps) {
|
|||||||
tabIndex={2}
|
tabIndex={2}
|
||||||
autoComplete="current-password"
|
autoComplete="current-password"
|
||||||
placeholder="Password"
|
placeholder="Password"
|
||||||
|
value={data.password}
|
||||||
|
onChange={(e) => setData('password', e.target.value)}
|
||||||
/>
|
/>
|
||||||
<InputError message={errors.password} />
|
<InputError message={errors.password} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center space-x-3">
|
<div className="flex items-center space-x-3">
|
||||||
<Checkbox id="remember" name="remember" tabIndex={3} />
|
<Checkbox
|
||||||
|
id="remember"
|
||||||
|
name="remember"
|
||||||
|
tabIndex={3}
|
||||||
|
checked={data.remember}
|
||||||
|
onCheckedChange={(checked) => setData('remember', Boolean(checked))}
|
||||||
|
/>
|
||||||
<Label htmlFor="remember">Remember me</Label>
|
<Label htmlFor="remember">Remember me</Label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -78,9 +97,7 @@ export default function Login({ status, canResetPassword }: LoginProps) {
|
|||||||
Sign up
|
Sign up
|
||||||
</TextLink>
|
</TextLink>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</form>
|
||||||
)}
|
|
||||||
</Form>
|
|
||||||
|
|
||||||
{status && <div className="mb-4 text-center text-sm font-medium text-green-600">{status}</div>}
|
{status && <div className="mb-4 text-center text-sm font-medium text-green-600">{status}</div>}
|
||||||
</AuthLayout>
|
</AuthLayout>
|
||||||
|
|||||||
@@ -1,100 +1,251 @@
|
|||||||
import RegisteredUserController from '@/actions/App/Http/Controllers/Auth/RegisteredUserController';
|
import React from 'react';
|
||||||
import { login } from '@/routes';
|
import { useForm, router } from '@inertiajs/react';
|
||||||
import { Form, Head } from '@inertiajs/react';
|
import { Head } from '@inertiajs/react';
|
||||||
import { LoaderCircle } from 'lucide-react';
|
import { LoaderCircle } from 'lucide-react';
|
||||||
|
|
||||||
import InputError from '@/components/input-error';
|
interface RegisterProps {
|
||||||
import TextLink from '@/components/text-link';
|
package?: {
|
||||||
import { Button } from '@/components/ui/button';
|
id: number;
|
||||||
import { Input } from '@/components/ui/input';
|
name: string;
|
||||||
import { Label } from '@/components/ui/label';
|
description: string;
|
||||||
import AuthLayout from '@/layouts/auth-layout';
|
price: number;
|
||||||
|
} | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Register({ package: initialPackage }: RegisterProps) {
|
||||||
|
const { data, setData, post, processing, errors } = useForm({
|
||||||
|
name: '',
|
||||||
|
username: '',
|
||||||
|
email: '',
|
||||||
|
password: '',
|
||||||
|
password_confirmation: '',
|
||||||
|
first_name: '',
|
||||||
|
last_name: '',
|
||||||
|
address: '',
|
||||||
|
phone: '',
|
||||||
|
privacy_consent: false,
|
||||||
|
package_id: initialPackage?.id || null,
|
||||||
|
});
|
||||||
|
|
||||||
|
const submit = (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
router.post('/register');
|
||||||
|
};
|
||||||
|
|
||||||
export default function Register() {
|
|
||||||
return (
|
return (
|
||||||
<AuthLayout title="Create an account" description="Enter your details below to create your account">
|
<div className="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
|
||||||
<Head title="Register" />
|
<Head title="Registrieren" />
|
||||||
<Form
|
<div className="max-w-md w-full space-y-8">
|
||||||
{...RegisteredUserController.store.form()}
|
<div>
|
||||||
resetOnSuccess={['password', 'password_confirmation']}
|
<h2 className="mt-6 text-center text-3xl font-extrabold text-gray-900">
|
||||||
disableWhileProcessing
|
Registrieren
|
||||||
className="flex flex-col gap-6"
|
</h2>
|
||||||
>
|
{initialPackage && (
|
||||||
{({ processing, errors }) => (
|
<div className="mt-4 p-4 bg-blue-50 border border-blue-200 rounded-md">
|
||||||
<>
|
<h3 className="text-lg font-semibold text-blue-900 mb-2">{initialPackage.name}</h3>
|
||||||
<div className="grid gap-6">
|
<p className="text-blue-800 mb-2">{initialPackage.description}</p>
|
||||||
<div className="grid gap-2">
|
<p className="text-sm text-blue-700">
|
||||||
<Label htmlFor="name">Name</Label>
|
{initialPackage.price === 0 ? 'Kostenlos' : `${initialPackage.price} €`}
|
||||||
<Input
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<form onSubmit={submit} className="mt-8 space-y-6">
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label htmlFor="name" className="block text-sm font-medium text-gray-700">
|
||||||
|
Name
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
id="name"
|
id="name"
|
||||||
|
name="name"
|
||||||
type="text"
|
type="text"
|
||||||
required
|
required
|
||||||
autoFocus
|
autoFocus
|
||||||
tabIndex={1}
|
value={data.name}
|
||||||
autoComplete="name"
|
onChange={(e) => setData('name', e.target.value)}
|
||||||
name="name"
|
className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
|
||||||
placeholder="Full name"
|
placeholder="Vollständiger Name"
|
||||||
/>
|
/>
|
||||||
<InputError message={errors.name} className="mt-2" />
|
{errors.name && <p className="mt-2 text-sm text-red-600">{errors.name}</p>}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid gap-2">
|
<div>
|
||||||
<Label htmlFor="email">Email address</Label>
|
<label htmlFor="username" className="block text-sm font-medium text-gray-700">
|
||||||
<Input
|
Benutzername
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="username"
|
||||||
|
name="username"
|
||||||
|
type="text"
|
||||||
|
required
|
||||||
|
value={data.username}
|
||||||
|
onChange={(e) => setData('username', e.target.value)}
|
||||||
|
className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
|
||||||
|
placeholder="Benutzername"
|
||||||
|
/>
|
||||||
|
{errors.username && <p className="text-sm text-red-600">{errors.username}</p>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label htmlFor="email" className="block text-sm font-medium text-gray-700">
|
||||||
|
E-Mail-Adresse
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
id="email"
|
id="email"
|
||||||
|
name="email"
|
||||||
type="email"
|
type="email"
|
||||||
required
|
required
|
||||||
tabIndex={2}
|
value={data.email}
|
||||||
autoComplete="email"
|
onChange={(e) => setData('email', e.target.value)}
|
||||||
name="email"
|
className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
|
||||||
placeholder="email@example.com"
|
placeholder="email@example.com"
|
||||||
/>
|
/>
|
||||||
<InputError message={errors.email} />
|
{errors.email && <p className="text-sm text-red-600">{errors.email}</p>}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid gap-2">
|
<div>
|
||||||
<Label htmlFor="password">Password</Label>
|
<label htmlFor="first_name" className="block text-sm font-medium text-gray-700">
|
||||||
<Input
|
Vorname
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="first_name"
|
||||||
|
name="first_name"
|
||||||
|
type="text"
|
||||||
|
required
|
||||||
|
value={data.first_name}
|
||||||
|
onChange={(e) => setData('first_name', e.target.value)}
|
||||||
|
className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
|
||||||
|
placeholder="Vorname"
|
||||||
|
/>
|
||||||
|
{errors.first_name && <p className="text-sm text-red-600">{errors.first_name}</p>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label htmlFor="last_name" className="block text-sm font-medium text-gray-700">
|
||||||
|
Nachname
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="last_name"
|
||||||
|
name="last_name"
|
||||||
|
type="text"
|
||||||
|
required
|
||||||
|
value={data.last_name}
|
||||||
|
onChange={(e) => setData('last_name', e.target.value)}
|
||||||
|
className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
|
||||||
|
placeholder="Nachname"
|
||||||
|
/>
|
||||||
|
{errors.last_name && <p className="text-sm text-red-600">{errors.last_name}</p>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label htmlFor="address" className="block text-sm font-medium text-gray-700">
|
||||||
|
Adresse
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
id="address"
|
||||||
|
name="address"
|
||||||
|
required
|
||||||
|
value={data.address}
|
||||||
|
onChange={(e) => setData('address', e.target.value)}
|
||||||
|
rows={3}
|
||||||
|
className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
|
||||||
|
placeholder="Adresse"
|
||||||
|
/>
|
||||||
|
{errors.address && <p className="text-sm text-red-600">{errors.address}</p>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label htmlFor="phone" className="block text-sm font-medium text-gray-700">
|
||||||
|
Telefon
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="phone"
|
||||||
|
name="phone"
|
||||||
|
type="tel"
|
||||||
|
required
|
||||||
|
value={data.phone}
|
||||||
|
onChange={(e) => setData('phone', e.target.value)}
|
||||||
|
className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
|
||||||
|
placeholder="Telefonnummer"
|
||||||
|
/>
|
||||||
|
{errors.phone && <p className="text-sm text-red-600">{errors.phone}</p>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label htmlFor="password" className="block text-sm font-medium text-gray-700">
|
||||||
|
Passwort
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
id="password"
|
id="password"
|
||||||
type="password"
|
|
||||||
required
|
|
||||||
tabIndex={3}
|
|
||||||
autoComplete="new-password"
|
|
||||||
name="password"
|
name="password"
|
||||||
placeholder="Password"
|
|
||||||
/>
|
|
||||||
<InputError message={errors.password} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid gap-2">
|
|
||||||
<Label htmlFor="password_confirmation">Confirm password</Label>
|
|
||||||
<Input
|
|
||||||
id="password_confirmation"
|
|
||||||
type="password"
|
type="password"
|
||||||
required
|
required
|
||||||
tabIndex={4}
|
value={data.password}
|
||||||
autoComplete="new-password"
|
onChange={(e) => setData('password', e.target.value)}
|
||||||
name="password_confirmation"
|
className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
|
||||||
placeholder="Confirm password"
|
placeholder="Passwort"
|
||||||
/>
|
/>
|
||||||
<InputError message={errors.password_confirmation} />
|
{errors.password && <p className="text-sm text-red-600">{errors.password}</p>}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button type="submit" className="mt-2 w-full" tabIndex={5}>
|
<div>
|
||||||
{processing && <LoaderCircle className="h-4 w-4 animate-spin" />}
|
<label htmlFor="password_confirmation" className="block text-sm font-medium text-gray-700">
|
||||||
Create account
|
Passwort bestätigen
|
||||||
</Button>
|
</label>
|
||||||
|
<input
|
||||||
|
id="password_confirmation"
|
||||||
|
name="password_confirmation"
|
||||||
|
type="password"
|
||||||
|
required
|
||||||
|
value={data.password_confirmation}
|
||||||
|
onChange={(e) => setData('password_confirmation', e.target.value)}
|
||||||
|
className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
|
||||||
|
placeholder="Passwort bestätigen"
|
||||||
|
/>
|
||||||
|
{errors.password_confirmation && <p className="text-sm text-red-600">{errors.password_confirmation}</p>}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="text-center text-sm text-muted-foreground">
|
<div className="flex items-start">
|
||||||
Already have an account?{' '}
|
<input
|
||||||
<TextLink href={login()} tabIndex={6}>
|
id="privacy_consent"
|
||||||
Log in
|
name="privacy_consent"
|
||||||
</TextLink>
|
type="checkbox"
|
||||||
|
required
|
||||||
|
checked={data.privacy_consent}
|
||||||
|
onChange={(e) => setData('privacy_consent', e.target.checked)}
|
||||||
|
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
|
||||||
|
/>
|
||||||
|
<label htmlFor="privacy_consent" className="ml-2 block text-sm text-gray-900">
|
||||||
|
Ich stimme der{' '}
|
||||||
|
<a href="/de/datenschutz" className="text-blue-600 hover:underline">
|
||||||
|
Datenschutzerklärung
|
||||||
|
</a>{' '}
|
||||||
|
zu.
|
||||||
|
</label>
|
||||||
|
{errors.privacy_consent && <p className="mt-2 text-sm text-red-600">{errors.privacy_consent}</p>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={processing}
|
||||||
|
className="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition duration-300 disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{processing && <LoaderCircle className="h-4 w-4 animate-spin mr-2" />}
|
||||||
|
Account erstellen
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div className="text-center">
|
||||||
|
<p className="text-sm text-gray-600">
|
||||||
|
Bereits registriert?{' '}
|
||||||
|
<a href="/login" className="font-medium text-blue-600 hover:text-blue-500">
|
||||||
|
Anmelden
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Form>
|
|
||||||
</AuthLayout>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import NewPasswordController from '@/actions/App/Http/Controllers/Auth/NewPasswordController';
|
import { store } from '@/actions/App/Http/Controllers/Auth/NewPasswordController';
|
||||||
import { Form, Head } from '@inertiajs/react';
|
import { Form, Head } from '@inertiajs/react';
|
||||||
import { LoaderCircle } from 'lucide-react';
|
import { LoaderCircle } from 'lucide-react';
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// Components
|
// Components
|
||||||
import EmailVerificationNotificationController from '@/actions/App/Http/Controllers/Auth/EmailVerificationNotificationController';
|
import { store } from '@/actions/App/Http/Controllers/Auth/EmailVerificationNotificationController';
|
||||||
import { logout } from '@/routes';
|
import { logout } from '@/routes';
|
||||||
import { Form, Head } from '@inertiajs/react';
|
import { Form, Head } from '@inertiajs/react';
|
||||||
import { LoaderCircle } from 'lucide-react';
|
import { LoaderCircle } from 'lucide-react';
|
||||||
@@ -19,7 +19,7 @@ export default function VerifyEmail({ status }: { status?: string }) {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Form {...EmailVerificationNotificationController.store.form()} className="space-y-6 text-center">
|
<Form {...store.form()} className="space-y-6 text-center">
|
||||||
{({ processing }) => (
|
{({ processing }) => (
|
||||||
<>
|
<>
|
||||||
<Button disabled={processing} variant="secondary">
|
<Button disabled={processing} variant="secondary">
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user