From 175a7203f39bb0d5fb1632d8ed4b0bf76be06f42 Mon Sep 17 00:00:00 2001 From: Gitea Date: Sat, 17 May 2025 05:11:29 +0530 Subject: [PATCH] added support ticket and added more stat widgets --- app/Filament/Resources/TicketResource.php | 220 ++++++++++++++++++ .../TicketResource/Pages/CreateTicket.php | 12 + .../TicketResource/Pages/EditTicket.php | 19 ++ .../TicketResource/Pages/ListTickets.php | 19 ++ .../ResponsesRelationManager.php | 65 ++++++ app/Filament/Widgets/StatsOverview.php | 24 ++ app/Livewire/Dashboard/Support.php | 149 ++++++++++++ app/Models/Ticket.php | 57 +++++ app/Models/TicketResponse.php | 25 ++ app/Models/User.php | 6 + closeTicket.php | 24 ++ ...2025_05_16_072530_create_tickets_table.php | 34 +++ ...6_072547_create_ticket_responses_table.php | 31 +++ .../components/layouts/dashboard.blade.php | 2 +- .../livewire/dashboard/pricing.blade.php | 2 +- .../livewire/dashboard/support.blade.php | 199 ++++++++++++++++ routes/console.php | 6 +- routes/web.php | 3 + zsql/zemailnator.sql | 84 ++++++- 19 files changed, 975 insertions(+), 6 deletions(-) create mode 100644 app/Filament/Resources/TicketResource.php create mode 100644 app/Filament/Resources/TicketResource/Pages/CreateTicket.php create mode 100644 app/Filament/Resources/TicketResource/Pages/EditTicket.php create mode 100644 app/Filament/Resources/TicketResource/Pages/ListTickets.php create mode 100644 app/Filament/Resources/TicketResource/RelationManagers/ResponsesRelationManager.php create mode 100644 app/Livewire/Dashboard/Support.php create mode 100644 app/Models/Ticket.php create mode 100644 app/Models/TicketResponse.php create mode 100644 closeTicket.php create mode 100644 database/migrations/2025_05_16_072530_create_tickets_table.php create mode 100644 database/migrations/2025_05_16_072547_create_ticket_responses_table.php create mode 100644 resources/views/livewire/dashboard/support.blade.php 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) +
+
+ +
+ + + + + + + + + + + + + @foreach(collect($tickets)->reverse() as $index => $ticket) + + + + + + + + @endforeach + +
IDSubjectStatusCreated Date
#{{ $ticket->ticket_id }}{{ $ticket->subject }} + @if($ticket->status == "open") + Open + @elseif($ticket->status == "pending") + In-Progress + @else + Closed + @endif + + {{ $ticket->created_at->diffForHumans() }} + +
+
+
+
+ @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 +
+

No Tickets

+
+ @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` --