diff --git a/app/Filament/Resources/TicketResource.php b/app/Filament/Resources/TicketResource.php
new file mode 100644
index 0000000..7300124
--- /dev/null
+++ b/app/Filament/Resources/TicketResource.php
@@ -0,0 +1,220 @@
+schema([
+ Select::make('user_id')
+ ->relationship('user', 'name')
+ ->searchable()
+ ->preload()
+ ->required(),
+
+ Select::make('status')
+ ->label('Status')
+ ->options([
+ 'open' => 'Open',
+ 'pending' => 'In Progress',
+ 'closed' => 'Closed',
+ ])
+ ->default('open')
+ ->required(),
+
+ TextInput::make('subject')
+ ->label('Subject')
+ ->required()
+ ->maxLength(255)
+ ->columnSpanFull(),
+
+ TextArea::make('message')
+ ->label('Message')
+ ->required()
+ ->rows(7)
+ ->maxLength(2000)
+ ->columnSpanFull(),
+ ]);
+ }
+
+ public static function table(Table $table): Table
+ {
+ return $table
+ ->defaultSort('created_at', 'desc')
+ ->columns([
+ TextColumn::make('ticket_id')
+ ->label('Ticket ID')
+ ->sortable(),
+ TextColumn::make('user.name'),
+ TextColumn::make('subject')
+ ->limit(50)
+ ->label('Subject'),
+ BadgeColumn::make('status')
+ ->colors([
+ 'success' => fn ($state) => $state === 'open',
+ 'warning' => fn ($state) => $state === 'pending',
+ 'danger' => fn ($state) => $state === 'closed',
+ ])
+ ->sortable(),
+ TextColumn::make('created_at')
+ ->label('Created At')
+ ->dateTime('F d, Y • h:i A')->sortable(),
+ TextColumn::make('last_response_at')
+ ->label('Last Response')
+ ->sortable()
+ ->formatStateUsing(fn ($state) => $state?->diffForHumans()),
+ ])
+ ->filters([
+ SelectFilter::make('status')
+ ->label('Status')
+ ->options([
+ 'open' => 'Open',
+ 'pending' => 'In Progress',
+ 'closed' => 'Closed',
+ ])
+ ->attribute('status'),
+ Filter::make('created_at')
+ ->form([
+ DatePicker::make('created_from')->label('Created From'),
+ DatePicker::make('created_until')->label('Created Until'),
+ ])
+ ->query(function ($query, array $data) {
+ return $query
+ ->when($data['created_from'], fn ($query, $date) => $query->whereDate('created_at', '>=', $date))
+ ->when($data['created_until'], fn ($query, $date) => $query->whereDate('created_at', '<=', $date));
+ }),
+ ])
+// ->actions([
+// Tables\Actions\EditAction::make(),
+// ])
+ ->actions([
+ Action::make('view')
+ ->label('View & Respond')
+ ->icon('heroicon-o-eye')
+ ->form(function (Ticket $ticket): array {
+ return [
+ TextArea::make('response')
+ ->label('Your Response')
+ ->required()
+ ->rows(7)
+ ->placeholder('Type your response here...'),
+
+ Select::make('status')
+ ->label('Ticket Status')
+ ->options([
+ 'open' => 'Open',
+ 'pending' => 'In Progress',
+ 'closed' => 'Closed',
+ ])
+ ->default($ticket->status === 'open' ? 'pending' : $ticket->status)
+ ->required(),
+ ];
+ })
+ ->modalContent(function (Ticket $ticket) {
+ $html = '
';
+
+ // Ticket Subject & Message
+ $html .= '
';
+ $html .= '
Subject: ' . e($ticket->subject) . '
';
+ $html .= '
Message: ' . nl2br(e($ticket->message)) . '
';
+ $html .= '
';
+
+ // Responses Section
+ $html .= '
Previous Responses
';
+
+ foreach ($ticket->responses as $response) {
+ $html .= '
';
+ $html .= '
';
+ $html .= '' . e($response->user->name) . '';
+ $html .= '' . e($response->created_at->diffForHumans()) . '';
+ $html .= '
';
+ $html .= '
' . nl2br(e($response->response)) . '
';
+ $html .= '
';
+ }
+
+ if ($ticket->responses->isEmpty()) {
+ $html .= '
No responses yet.
';
+ }
+
+ $html .= '
';
+
+ return new \Illuminate\Support\HtmlString($html);
+ })
+
+ ->action(function (array $data, Ticket $ticket) {
+ TicketResponse::create([
+ 'ticket_id' => $ticket->id,
+ 'user_id' => auth()->id(),
+ 'response' => $data['response'],
+ ]);
+
+ // Update ticket status and last response time
+ $ticket->update([
+ 'status' => $data['status'],
+ 'last_response_at' => now(),
+ ]);
+
+ // Send success notification
+ Notification::make()
+ ->title('Response sent successfully!')
+ ->success()
+ ->send();
+ })
+ ->modalHeading('View & Respond to Ticket')
+ ->modalSubmitActionLabel('Send Reply'),
+ ])
+ ->bulkActions([
+ Tables\Actions\BulkActionGroup::make([
+ Tables\Actions\DeleteBulkAction::make(),
+ ]),
+ ]);
+ }
+
+ public static function getRelations(): array
+ {
+ return [
+ RelationManagers\ResponsesRelationManager::class,
+ ];
+ }
+
+ public static function getPages(): array
+ {
+ return [
+ 'index' => Pages\ListTickets::route('/'),
+ 'create' => Pages\CreateTicket::route('/create'),
+ 'edit' => Pages\EditTicket::route('/{record}/edit'),
+ ];
+ }
+}
diff --git a/app/Filament/Resources/TicketResource/Pages/CreateTicket.php b/app/Filament/Resources/TicketResource/Pages/CreateTicket.php
new file mode 100644
index 0000000..f3998f5
--- /dev/null
+++ b/app/Filament/Resources/TicketResource/Pages/CreateTicket.php
@@ -0,0 +1,12 @@
+schema([
+ Select::make('user_id')
+ ->relationship('user', 'name')
+ ->searchable()
+ ->preload()
+ ->default(1)
+ ->required()
+ ->columnSpanFull(),
+ Textarea::make('response')
+ ->required()
+ ->rows(7)
+ ->maxLength(255)
+ ->columnSpanFull(),
+ ]);
+ }
+
+ public function table(Table $table): Table
+ {
+ return $table
+ ->recordTitleAttribute('response')
+ ->columns([
+ TextColumn::make('user.name'),
+ TextColumn::make('ip_address'),
+ TextColumn::make('response'),
+ ])
+ ->filters([
+ //
+ ])
+ ->headerActions([
+ Tables\Actions\CreateAction::make(),
+ ])
+ ->actions([
+ Tables\Actions\EditAction::make(),
+ Tables\Actions\DeleteAction::make(),
+ ])
+ ->bulkActions([
+ Tables\Actions\BulkActionGroup::make([
+ Tables\Actions\DeleteBulkAction::make(),
+ ]),
+ ]);
+ }
+}
diff --git a/app/Filament/Widgets/StatsOverview.php b/app/Filament/Widgets/StatsOverview.php
index 7f542eb..930eddc 100644
--- a/app/Filament/Widgets/StatsOverview.php
+++ b/app/Filament/Widgets/StatsOverview.php
@@ -4,6 +4,8 @@ namespace App\Filament\Widgets;
use App\Models\Log;
use App\Models\Meta;
+use App\Models\PremiumEmail;
+use App\Models\Ticket;
use App\Models\User;
use DB;
use Filament\Widgets\StatsOverviewWidget as BaseWidget;
@@ -15,10 +17,14 @@ class StatsOverview extends BaseWidget
{
return [
Stat::make('Total Users', $this->getUser()),
+ Stat::make('Customers', $this->getCustomerCount()),
Stat::make('Paid Users', $this->getUserPaid()),
Stat::make('Logs Count', $this->getLogsCount()),
Stat::make('Total Mailbox', $this->getTotalMailbox()),
Stat::make('Emails Received', $this->getTotalEmailsReceived()),
+ Stat::make('Emails Stored', $this->getStoreEmailsCount()),
+ Stat::make('Open Tickets', $this->getOpenTicketsCount()),
+ Stat::make('Closed Tickets', $this->getClosedTicketsCount()),
];
}
private function getUser(): int
@@ -44,4 +50,22 @@ class StatsOverview extends BaseWidget
{
return Meta::select('value')->where(['key' => 'messages_received'])->first()->value;
}
+
+ private function getCustomerCount(): int
+ {
+ return User::whereNotNull('stripe_id')->count();
+ }
+
+ private function getStoreEmailsCount(): int
+ {
+ return PremiumEmail::all()->count();
+ }
+ private function getOpenTicketsCount(): int
+ {
+ return Ticket::whereIn('status', ['open', 'pending'])->count();
+ }
+ private function getClosedTicketsCount(): int
+ {
+ return Ticket::whereIn('status', ['closed'])->count();
+ }
}
diff --git a/app/Livewire/Dashboard/Support.php b/app/Livewire/Dashboard/Support.php
new file mode 100644
index 0000000..5b17115
--- /dev/null
+++ b/app/Livewire/Dashboard/Support.php
@@ -0,0 +1,149 @@
+validate([
+ 'subject' => 'required|string|max:255',
+ 'message' => 'required|string',
+ ]);
+
+ try {
+ $ticket = Ticket::create([
+ 'user_id' => auth()->id(),
+ 'ticket_id' => strtoupper(Str::random(6)),
+ 'subject' => $this->subject,
+ 'message' => $this->message,
+ 'ip_address' => $this->getClientIp(),
+ 'last_response_at' => now(),
+ ]);
+ $this->dispatch('showAlert', ['type' => 'success', 'message' => 'Your ticket has been created successfully!']);
+ $this->dispatch('closeModal');
+ $this->reset(['subject','message']);
+ $this->tickets = Ticket::with('responses')
+ ->where('user_id', auth()->id())
+ ->get();
+ } catch (\Exception $exception) {
+ $this->dispatch('showAlert', ['type' => 'error', 'message' => 'Something went wrong!']);
+ }
+
+
+ }
+
+ public function reply($ticket_id)
+ {
+ $this->validate([
+ 'response' => 'required|string',
+ ]);
+
+ try {
+
+ if (!is_numeric($ticket_id)) {
+ $this->dispatch('showAlert', ['type' => 'error', 'message' => 'Invalid ticket ID.']);
+ return;
+ }
+
+ $ticket = Ticket::find($ticket_id);
+ if (!$ticket) {
+ $this->dispatch('showAlert', ['type' => 'error', 'message' => 'Ticket not found.']);
+ return;
+ }
+
+ TicketResponse::create([
+ 'ticket_id' => $ticket_id,
+ 'user_id' => auth()->id(),
+ 'response' => $this->response,
+ 'ip_address' => $this->getClientIp(),
+ ]);
+ $ticket->last_response_at = now();
+ $ticket->save();
+
+ $this->dispatch('showAlert', ['type' => 'success', 'message' => 'Reply sent successfully!']);
+
+ $this->reset(['response']);
+ $this->tickets = Ticket::with('responses')
+ ->where('user_id', auth()->id())
+ ->get();
+
+ } catch (\Exception $exception) {
+ session()->flash('error', 'Something went wrong!');
+ }
+
+ }
+
+ public function close($ticket_id)
+ {
+ if (!is_numeric($ticket_id)) {
+ $this->dispatch('showAlert', ['type' => 'error', 'message' => 'Invalid ticket ID.']);
+ return;
+ }
+ $ticket = Ticket::find($ticket_id);
+ if (!$ticket) {
+ $this->dispatch('showAlert', ['type' => 'error', 'message' => 'Ticket not found.']);
+ return;
+ }
+
+ $ticket->status = 'closed';
+ $ticket->save();
+ $this->tickets = Ticket::with('responses')
+ ->where('user_id', auth()->id())
+ ->get();
+ $this->updateTicketCounts();
+ $this->dispatch('showAlert', ['type' => 'error', 'message' => 'This ticket has been closed!']);
+ }
+
+ public function mount()
+ {
+ $this->tickets = Ticket::with('responses')
+ ->where('user_id', auth()->id())
+ ->get();
+ $this->updateTicketCounts();
+ }
+
+ public function updateTicketCounts()
+ {
+ $this->open = $this->tickets->filter(function ($ticket) {
+ return in_array($ticket->status, ['open', 'pending']);
+ })->count();
+
+ $this->closed = $this->tickets->filter(function ($ticket) {
+ return $ticket->status === 'closed';
+ })->count();
+ }
+
+ protected function getClientIp()
+ {
+ // Cloudflare or other proxies may send the original client IP in X-Forwarded-For header
+ $ip = Request::header('X-Forwarded-For');
+
+ // X-Forwarded-For can contain a comma-separated list of IPs, so we need to get the first one
+ if ($ip) {
+ return explode(',', $ip)[0]; // Get the first IP in the list
+ }
+
+ // Fallback to the real IP if no X-Forwarded-For header
+ return Request::ip();
+ }
+
+ public function render()
+ {
+ return view('livewire.dashboard.support')->layout('components.layouts.dashboard');
+ }
+}
diff --git a/app/Models/Ticket.php b/app/Models/Ticket.php
new file mode 100644
index 0000000..e8cf581
--- /dev/null
+++ b/app/Models/Ticket.php
@@ -0,0 +1,57 @@
+belongsTo(User::class);
+ }
+
+ public function responses()
+ {
+ return $this->hasMany(TicketResponse::class);
+ }
+
+ protected $casts = [
+ 'created_at' => 'datetime', // Ensures created_at is a Carbon instance
+ 'updated_at' => 'datetime', // Ensures updated_at is a Carbon instance
+ 'last_response_at' => 'datetime', // Cast last_response_at to Carbon instance
+ ];
+
+ public static function autoClose(): bool
+ {
+ try {
+ $tickets = Ticket::where('status', 'pending')
+ ->where('last_response_at', '<', now()->subDays(3))
+ ->get();
+ if (count($tickets) > 0) {
+ foreach ($tickets as $ticket) {
+ $ticket->status = 'closed';
+ $ticket->save();
+
+ TicketResponse::create([
+ 'ticket_id' => $ticket->id,
+ 'user_id' => 1,
+ 'response' => 'This ticket has been auto-closed due to inactivity.',
+ ]);
+ }
+ }
+ return true;
+ } catch (\Exception $e) {
+ return false;
+ }
+
+ }
+}
diff --git a/app/Models/TicketResponse.php b/app/Models/TicketResponse.php
new file mode 100644
index 0000000..38a0565
--- /dev/null
+++ b/app/Models/TicketResponse.php
@@ -0,0 +1,25 @@
+belongsTo(Ticket::class);
+ }
+
+ public function user()
+ {
+ return $this->belongsTo(User::class);
+ }
+}
diff --git a/app/Models/User.php b/app/Models/User.php
index 92958c1..9537d7b 100644
--- a/app/Models/User.php
+++ b/app/Models/User.php
@@ -6,6 +6,7 @@ namespace App\Models;
use Filament\Models\Contracts\FilamentUser;
use Filament\Panel;
use Illuminate\Database\Eloquent\Factories\HasFactory;
+use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Illuminate\Contracts\Auth\MustVerifyEmail;
@@ -63,4 +64,9 @@ class User extends Authenticatable implements FilamentUser, MustVerifyEmail
{
return str_ends_with($this->email, '@zemail.me') && $this->level === 9 && $this->hasVerifiedEmail();
}
+
+ public function tickets(): HasMany
+ {
+ return $this->hasMany(Ticket::class);
+ }
}
diff --git a/closeTicket.php b/closeTicket.php
new file mode 100644
index 0000000..34f00a9
--- /dev/null
+++ b/closeTicket.php
@@ -0,0 +1,24 @@
+make(Illuminate\Contracts\Console\Kernel::class);
+
+try {
+ // Run the Artisan command 'ping'
+ $exitCode = $kernel->call('closeTicket');
+
+ // Get the output of the command
+ $output = $kernel->output();
+
+ echo "Artisan command 'closeTicket' executed successfully. Exit code: $exitCode\n";
+ echo "Output:\n$output";
+
+} catch (\Exception $e) {
+ echo "Error running Artisan command: " . $e->getMessage();
+}
diff --git a/database/migrations/2025_05_16_072530_create_tickets_table.php b/database/migrations/2025_05_16_072530_create_tickets_table.php
new file mode 100644
index 0000000..3ee371d
--- /dev/null
+++ b/database/migrations/2025_05_16_072530_create_tickets_table.php
@@ -0,0 +1,34 @@
+id();
+ $table->foreignId('user_id')->constrained()->onDelete('cascade');
+ $table->string('ticket_id', 6)->unique();
+ $table->string('subject');
+ $table->text('message');
+ $table->enum('status', ['open', 'closed', 'pending'])->default('open');
+ $table->ipAddress()->nullable();
+ $table->timestamp('last_response_at')->nullable();
+ $table->timestamps();
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::dropIfExists('tickets');
+ }
+};
diff --git a/database/migrations/2025_05_16_072547_create_ticket_responses_table.php b/database/migrations/2025_05_16_072547_create_ticket_responses_table.php
new file mode 100644
index 0000000..12d3000
--- /dev/null
+++ b/database/migrations/2025_05_16_072547_create_ticket_responses_table.php
@@ -0,0 +1,31 @@
+id();
+ $table->foreignId('ticket_id')->constrained()->onDelete('cascade');
+ $table->foreignId('user_id')->constrained()->onDelete('cascade');
+ $table->text('response');
+ $table->ipAddress()->nullable();
+ $table->timestamps();
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::dropIfExists('ticket_responses');
+ }
+};
diff --git a/resources/views/components/layouts/dashboard.blade.php b/resources/views/components/layouts/dashboard.blade.php
index 7735743..ba780de 100644
--- a/resources/views/components/layouts/dashboard.blade.php
+++ b/resources/views/components/layouts/dashboard.blade.php
@@ -40,7 +40,7 @@
//['label' => '10 Minute Mail', 'route' => 'dashboard.10minute'],
['label' => 'Bulk Email Generator', 'route' => 'dashboard.bulk'],
['label' => 'Bulk Gmail Generator', 'route' => 'dashboard.bulkGmail'],
- //['label' => 'Compose Email', 'route' => 'dashboard.compose'],
+ ['label' => 'Support Ticket', 'route' => 'dashboard.support'],
];
$currentRoute = Route::currentRouteName();
diff --git a/resources/views/livewire/dashboard/pricing.blade.php b/resources/views/livewire/dashboard/pricing.blade.php
index 0fc7e5e..64860a6 100644
--- a/resources/views/livewire/dashboard/pricing.blade.php
+++ b/resources/views/livewire/dashboard/pricing.blade.php
@@ -95,7 +95,7 @@
@endif
@if (session()->has('error'))
-
+
{{ session('error') }}
@endif
diff --git a/resources/views/livewire/dashboard/support.blade.php b/resources/views/livewire/dashboard/support.blade.php
new file mode 100644
index 0000000..23be220
--- /dev/null
+++ b/resources/views/livewire/dashboard/support.blade.php
@@ -0,0 +1,199 @@
+@section('title'){{ __('Support Ticket') }}@endsection
+
+
+
+
+
+
+
+
+
+
{{ $open }}
+
Opened Tickets
+
+
+
+
+
+
+
+
+
{{ $closed }}
+
Closed Tickets
+
+
+
+
+
+
+
+
Tickets
+
+ Create Ticket
+
+
+
+
+
+
+
+
+ @if(count($tickets) > 0)
+
+
+
+
+
+
+
+ | ID |
+ Subject |
+ Status |
+ Created Date |
+ |
+
+
+
+
+ @foreach(collect($tickets)->reverse() as $index => $ticket)
+
+ | #{{ $ticket->ticket_id }} |
+ {{ $ticket->subject }} |
+
+ @if($ticket->status == "open")
+ Open
+ @elseif($ticket->status == "pending")
+ In-Progress
+ @else
+ Closed
+ @endif
+
+ |
+ {{ $ticket->created_at->diffForHumans() }} |
+
+
+ |
+
+ @endforeach
+
+
+
+
+
+ @foreach(collect($tickets)->reverse() as $index => $ticket)
+
+
+
+
+
+
+
+
+ {{ auth()->user()->name }}
+ {{ $ticket->created_at->format('F d, Y • h:i A') }}
+
+
Subject: {{ $ticket->subject }}
Message: {{ $ticket->message }}
+
+
+ @if(count($ticket->responses) > 0)
+ @foreach($ticket->responses as $response)
+
+
+ {{ auth()->user()->id === $response->user_id ? auth()->user()->name : 'Support Team' }}
+ {{ $response->created_at->format('F d, Y • h:i A') }}
+
+
{{ $response->response }}
+
+ @endforeach
+ @endif
+
+
+
+
+ @error('response')
+
+ {{ $message }}
+
+ @enderror
+
+ @if (session()->has('success'))
+
+ {{ session('success') }}
+
+ @endif
+ @if (session()->has('error'))
+
+ {{ session('error') }}
+
+ @endif
+
Send Reply
+
+
+
+
+
+
+
+
Ticket Details
+
+
Ticket ID: #{{ $ticket->ticket_id }}
+
Created At: {{ $ticket->created_at->format('F d, Y • h:i A') }}
+
Last Response: {{ $ticket->last_response_at ? $ticket->last_response_at->format('F d, Y • h:i A') : 'No responses yet' }}
+
Status:
+ @if($ticket->status == "open")
+ Open
+ @elseif($ticket->status == "pending")
+ In-Progress
+ @else
+ Closed
+ @endif
+
+
Close Ticket
+
+
+
+
+
+
+ @endforeach
+
+
+ @else
+
+ @endif
+
+
+
+
+
+
+ Create ticket
+ @if (session()->has('success'))
+ {{ session('success') }}
+ @endif
+ @if (session()->has('error'))
+ {{ session('error') }}
+ @endif
+
+
+
+
+
+ Create
+
+
+
+
diff --git a/routes/console.php b/routes/console.php
index 75b3379..335e1c0 100644
--- a/routes/console.php
+++ b/routes/console.php
@@ -1,6 +1,7 @@
comment(Email::cleanMailbox());
-
+});
+
+Artisan::command('closeTicket', function (){
+ $this->comment(Ticket::autoClose());
});
diff --git a/routes/web.php b/routes/web.php
index 3e6156e..eab222e 100644
--- a/routes/web.php
+++ b/routes/web.php
@@ -8,6 +8,7 @@ use App\Livewire\Dashboard\Bulk;
use App\Livewire\Dashboard\BulkGmail;
use App\Livewire\Dashboard\Dashboard;
use App\Livewire\Dashboard\Mailbox\Inbox;
+use App\Livewire\Dashboard\Support;
use App\Livewire\Frontend\Mailbox;
use App\Livewire\Home;
use App\Livewire\ListBlog;
@@ -60,6 +61,7 @@ Route::middleware(['auth', 'verified'])->group(function () {
Route::get('dashboard/bulk-email-generator', Bulk::class)->name('dashboard.bulk');
Route::get('dashboard/bulk-gmail-generator', BulkGmail::class)->name('dashboard.bulkGmail');
Route::get('dashboard/compose-email', Dashboard::class)->name('dashboard.compose');
+ Route::get('dashboard/support', Support::class)->name('dashboard.support');
// Checkout Routes
Route::get('checkout/{plan}', function ($pricing_id) {
@@ -74,6 +76,7 @@ Route::middleware(['auth', 'verified'])->group(function () {
->newSubscription('default', $pricing_id)
->allowPromotionCodes()
->checkout([
+ 'billing_address_collection' => 'required',
'success_url' => route('checkout.success'),
'cancel_url' => route('checkout.cancel'),
]);
diff --git a/zsql/zemailnator.sql b/zsql/zemailnator.sql
index 1c11c28..a222e5c 100644
--- a/zsql/zemailnator.sql
+++ b/zsql/zemailnator.sql
@@ -3,7 +3,7 @@
-- https://www.phpmyadmin.net/
--
-- Host: 127.0.0.1
--- Generation Time: May 16, 2025 at 10:52 PM
+-- Generation Time: May 16, 2025 at 10:53 PM
-- Server version: 10.4.28-MariaDB
-- PHP Version: 8.3.21
@@ -1169,7 +1169,9 @@ INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES
(36, '2025_05_03_200503_add_user_id_to_logs_table', 18),
(39, '2025_05_05_212255_create_premium_emails_table', 19),
(43, '2025_05_16_015550_create_activation_keys_table', 20),
-(44, '2025_05_16_024757_add_shoppy_and_accept_columns_to_plans_table', 21);
+(44, '2025_05_16_024757_add_shoppy_and_accept_columns_to_plans_table', 21),
+(53, '2025_05_16_072530_create_tickets_table', 22),
+(54, '2025_05_16_072547_create_ticket_responses_table', 22);
-- --------------------------------------------------------
@@ -1461,6 +1463,41 @@ INSERT INTO `subscription_items` (`id`, `subscription_id`, `stripe_id`, `stripe_
-- --------------------------------------------------------
+--
+-- Table structure for table `tickets`
+--
+
+CREATE TABLE `tickets` (
+ `id` bigint(20) UNSIGNED NOT NULL,
+ `user_id` bigint(20) UNSIGNED NOT NULL,
+ `ticket_id` varchar(6) NOT NULL,
+ `subject` varchar(255) NOT NULL,
+ `message` text NOT NULL,
+ `status` enum('open','closed','pending') NOT NULL DEFAULT 'open',
+ `ip_address` varchar(45) DEFAULT NULL,
+ `last_response_at` timestamp NULL DEFAULT NULL,
+ `created_at` timestamp NULL DEFAULT NULL,
+ `updated_at` timestamp NULL DEFAULT NULL
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table `ticket_responses`
+--
+
+CREATE TABLE `ticket_responses` (
+ `id` bigint(20) UNSIGNED NOT NULL,
+ `ticket_id` bigint(20) UNSIGNED NOT NULL,
+ `user_id` bigint(20) UNSIGNED NOT NULL,
+ `response` text NOT NULL,
+ `ip_address` varchar(45) DEFAULT NULL,
+ `created_at` timestamp NULL DEFAULT NULL,
+ `updated_at` timestamp NULL DEFAULT NULL
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+
+-- --------------------------------------------------------
+
--
-- Table structure for table `usage_logs`
--
@@ -1679,6 +1716,22 @@ ALTER TABLE `subscription_items`
ADD UNIQUE KEY `subscription_items_stripe_id_unique` (`stripe_id`),
ADD KEY `subscription_items_subscription_id_stripe_price_index` (`subscription_id`,`stripe_price`);
+--
+-- Indexes for table `tickets`
+--
+ALTER TABLE `tickets`
+ ADD PRIMARY KEY (`id`),
+ ADD UNIQUE KEY `tickets_ticket_id_unique` (`ticket_id`),
+ ADD KEY `tickets_user_id_foreign` (`user_id`);
+
+--
+-- Indexes for table `ticket_responses`
+--
+ALTER TABLE `ticket_responses`
+ ADD PRIMARY KEY (`id`),
+ ADD KEY `ticket_responses_ticket_id_foreign` (`ticket_id`),
+ ADD KEY `ticket_responses_user_id_foreign` (`user_id`);
+
--
-- Indexes for table `usage_logs`
--
@@ -1762,7 +1815,7 @@ ALTER TABLE `metas`
-- AUTO_INCREMENT for table `migrations`
--
ALTER TABLE `migrations`
- MODIFY `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=53;
+ MODIFY `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=55;
--
-- AUTO_INCREMENT for table `pages`
@@ -1800,6 +1853,18 @@ ALTER TABLE `subscriptions`
ALTER TABLE `subscription_items`
MODIFY `id` bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=28;
+--
+-- AUTO_INCREMENT for table `tickets`
+--
+ALTER TABLE `tickets`
+ MODIFY `id` bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT;
+
+--
+-- AUTO_INCREMENT for table `ticket_responses`
+--
+ALTER TABLE `ticket_responses`
+ MODIFY `id` bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT;
+
--
-- AUTO_INCREMENT for table `usage_logs`
--
@@ -1840,6 +1905,19 @@ ALTER TABLE `logs`
ALTER TABLE `premium_emails`
ADD CONSTRAINT `premium_emails_user_id_foreign` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE;
+--
+-- Constraints for table `tickets`
+--
+ALTER TABLE `tickets`
+ ADD CONSTRAINT `tickets_user_id_foreign` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE;
+
+--
+-- Constraints for table `ticket_responses`
+--
+ALTER TABLE `ticket_responses`
+ ADD CONSTRAINT `ticket_responses_ticket_id_foreign` FOREIGN KEY (`ticket_id`) REFERENCES `tickets` (`id`) ON DELETE CASCADE,
+ ADD CONSTRAINT `ticket_responses_user_id_foreign` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE;
+
--
-- Constraints for table `usage_logs`
--