Tickets is a comprehensive support ticket management system for Filament applications. It provides a full-featured ticketing system with chat widget, email authentication, activity tracking, and extensive customization options.
The package is using orchestral/testbench for testing against a Laravel app.
composer serve will start the application as http://127.0.0.1:8000.
When running composer serve the latest Filament assets are automatically published.
If you want to work on JS or CSS files, you can run:
npm install
npm run devThis will start Vite, put the application in Dev Mode, remove existing Filament assets and serve the assets directly from the dist folder. It will also enable BrowserSync to reload the browser on changes.
Make sure you restart npm run dev if you restart composer serve or after you published the assets.
To test storage features make sure to add a S3 compatible bucket to testbench.yaml env config. It defaults to a local minio install.
// In your Panel Service Provider
use App\Models\User;
use Padmission\Tickets\AssignmentStrategies\AssignRandomUser;
use Padmission\Tickets\TicketPlugin;
public function panel(Panel $panel): Panel
{
return $panel
->id('admin')
->plugin(
TicketPlugin::make()
->allSupportersQuery(fn () => User::role('support'))
->assignmentStrategy(new AssignRandomUser())
->registerResources()
->showChatWidget()
);
}// Support Panel - Where tickets are managed
public function panel(Panel $panel): Panel
{
return $panel
->id('support')
->plugin(
TicketPlugin::make()
->allSupportersQuery(fn () => User::role(['support', 'admin']))
->initialAssignmentSupportersQuery(fn () => User::role('tier-1'))
->assignmentStrategy(new AssignUserWithLeastTickets())
->registerResources()
);
}
// Customer Panel - Where tickets are created
public function panel(Panel $panel): Panel
{
return $panel
->id('customer')
->plugin(
TicketPlugin::make()
->targetPanel('support')
->showChatWidget()
);
}- 🎫 Full Ticket Management - Create, view, assign, and close tickets
- đź’¬ Embedded Chat Widget - Real-time support chat for your users
- đź“§ Email Authentication - Allow non-authenticated users to submit tickets via email verification
- 👥 Multi-Tenancy Support - Built-in support for multi-tenant applications
- 📊 Analytics Widgets - Track open tickets, response times, and burndown charts
- 🔄 Turn Management - Track whose turn it is to respond (User or Supporter)
- 📝 Activity Tracking - Comprehensive logging of all ticket changes
- đź”” Flexible Notifications - Multiple notification strategies
- 📎 File Attachments - Support for file uploads via Spatie Media Library
- 🎯 Smart Assignment - Automatic ticket assignment with flexible strategies
- 🏢 Multi-Panel Support - Route tickets from multiple panels to a central location
- PHP: 8.3 or higher
- Laravel: 11.0 or higher
- Filament: 3.0 or higher
For distribution we use Satis Padmission, a private Composer repository. During the purchasing process, Lemon Squeezy will provide you with a license key that you'll need for installation.
Add the private repository to your composer.json file:
{
"repositories": [
{
"type": "composer",
"url": "https://satis.padmission.com"
}
]
}Step 1: Install the package via Composer:
composer require padmission/tickets
When prompted, provide your authentication details:
- Username: Your email address (e.g., myname@example.com)
- Password: Your license key (e.g., 9f3a2e1d-5b7c-4f86-a9d0-3e1c2b4a5f8e)
Step 2: Run the migrations to set up the database tables:
php artisan migrateStep 3: Publish the assets
php artisan filament:assets
Step 4: Publish the assets
Add the following import to your custom theme:
@import '../../../../vendor/padmission/tickets/resources/css/tickets.css';Step 5: Configure the plugin in your Filament panel:
use App\Models\User;
use Padmission\Tickets\TicketPlugin;
public function panel(Panel $panel): Panel
{
return $panel
// ... other panel configuration
->plugin(
TicketPlugin::make()
->allSupportersQuery(fn () => User::role(['support', 'admin']))
->registerResources()
);
}Important: The
allSupportersQuery()is required when registering resources. It defines all users who can support tickets in this panel.
Step 6: Set up your User model:
Add the HasTickets trait to your User model.
<?php
namespace App\Models;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Padmission\Tickets\Models\Concerns\User\HasTickets;
use Padmission\Tickets\Models\Contracts\HasTicketDisplayName;
class User extends Authenticatable implements HasTicketDisplayName
{
use HasTickets;
// ... your existing code ...
/**
* Get the display name for ticket activities and notifications
*/
public function getNameForTickets(): string
{
return $this->name ?? $this->email ?? "User {$this->id}";
}
}The HasTickets trait provides:
assignedTickets()- Relationship to tickets assigned to this usersubmittedTickets()- Relationship to tickets submitted by this user
To customize the package settings, publish the configuration file:
php artisan vendor:publish --tag="padmission-tickets-config"This will create a config/padmission-tickets.php file where you can configure:
- Model bindings
- Multi-tenancy settings
- Escalation levels
- Attachment storage settings
The package comes with a set of Filament resources to manage tickets. To enable ticket management in a panel:
use App\Models\User;
use Padmission\Tickets\TicketPlugin;
TicketPlugin::make()
->allSupportersQuery(fn () => User::role(['support', 'admin']))
->registerResources()Important: When registering resources, you must define
allSupportersQuery(). This query determines which users can be assigned tickets through the UI.
This registers the following resources:
- TicketResource - Main ticket management with assignment controls
- StatusResource - Manage ticket statuses
- DispositionResource - Manage ticket dispositions
- PriorityResource - Manage ticket priorities
For each resource you can easily overwrite its label, navigation group, sort, and navigation icon:
use Padmission\Tickets\Filament\Resources\Tickets\TicketResource;
class YourServiceProvider {
public function boot() {
TicketResource::configure(
modelLabel: 'Your Label',
pluralModelLabel: fn () => __('your.model'),
navigationGroup: 'New Group',
navigationIcon: 'heroicon-o-tag',
navigationSort: 10,
);
}
}This package comes with multiple Filament widgets that can be added to your dashboard:
- OpenTicketsWidget - Shows count of open tickets
- OpenSupporterTickets - Shows tickets assigned to supporters
- TicketCloseTimeWidget - Displays average ticket close times
- TicketBurndownChartWidget - Visualizes ticket closure trends
Widgets are registered automatically when using ->registerResources(). You can disable this by using ->registerResources(shouldRegisterWidgets: false).
By default, it will show the TicketStatsWidget on the ListTickets page.
We define a basic policy, but you can swap it anytime with your implementation:
use Filament\Facades\Filament;
use Padmission\Tickets\Models\Ticket;
use Padmission\Tickets\TicketPlugin;
use Illuminate\Support\Facades\Gate;
// Define your policy, which extends from `TicketPolicy`
Gate::policy(
Ticket::class,
YourTicketPolicy::class
);The TicketPolicy will affect Tickets, but also Statuses, Priorities, and Dispositions. If you want specific rules for the latter ones, you can define a Policy for those.
The package allows you to define custom dispositions for tickets. Dispositions are used to categorize tickets when they are closed. You can configure dispositions within each panel using the DispositionResource.
Users can create tickets via a chat widget. The widget provides a modern, real-time chat interface with:
- Rich text editor with formatting options (bold, links, lists)
- Auto-response messages after first user message
- Turn management - Shows whose turn it is to respond
- File attachments (when configured)
- Keyboard shortcuts for power users
To enable the widget in a panel, use the ->showChatWidget() method:
use Filament\Support\Colors\Color;
use Padmission\Tickets\ChatWidgetConfig;
use Padmission\Tickets\TicketPlugin;
TicketPlugin::make()
->showChatWidget(config: ChatWidgetConfig::make()
->introMessage('Welcome to our support system! How can we help you today?')
->autoResponse('Thanks for your message! A support agent will be with you shortly.')
->placeholder('Type your message here...')
->primaryColor(Color::Cyan)
);If you want to render the chat widget outside a Filament panel add the Blade component at the end of your body tag:
<x-padmission-tickets::chat-widget />Make sure the CSRF token is included in your HTML head section:
<meta name="csrf-token" content="{{ csrf_token() }}">The package supports email-based authentication for non-authenticated users, allowing them to submit and track tickets without creating an account. This is particularly useful for password reset requests or public support systems.
To enable email authentication:
use Padmission\Tickets\ChatWidgetConfig;
use Padmission\Tickets\TicketPlugin;
TicketPlugin::make()
->showChatWidget(config: ChatWidgetConfig::make()
->allowEmailAuthentication(
allow: true,
allowGuests: true,
otpExpiresAfterMinutes: 10
)
);How it works:
- User enters their email address
- System sends a 6-digit OTP (One-Time Password) to their email
- User enters the OTP to verify their identity
- User can then submit tickets and view their ticket history
Features:
- Rate limiting on OTP requests (1 per minute)
- Rate limiting on OTP verification attempts (5 per minute)
- Configurable OTP expiration time
- Session-based authentication for verified users
You can add a button to the chat widget that opens your documentation in a new tab using ->documentationUrl().
use Filament\Support\Colors\Color;
use Padmission\Tickets\ChatWidgetConfig;
use Padmission\Tickets\TicketPlugin;
TicketPlugin::make()
->showChatWidget(
config: ChatWidgetConfig::make()->documentationUrl(url('/docs'))
);If you want to allow users to upload files you can use the ->allowFileUploads() method on the ChatWidgetConfig:
use Filament\Support\Colors\Color;
use Padmission\Tickets\ChatWidgetConfig;
use Padmission\Tickets\TicketPlugin;
TicketPlugin::make()
->showChatWidget(config: ChatWidgetConfig::make()
->allowFileUploads()
);The package includes built-in support for multi-tenant applications. Enable it in your configuration:
// config/padmission-tickets.php
return [
'tenancy' => [
'enabled' => true,
'tenancy_model' => App\Models\Tenant::class,
],
];The package automatically handles:
- Foreign key constraints based on your tenant model
- UUID/ULID support for tenant IDs
- Tenant isolation for all ticket operations
The package automatically tracks whose "turn" it is to respond to a ticket:
- User Turn - Waiting for user response
- Supporter Turn - Waiting for support agent response
This helps support teams prioritize tickets that need attention. Turn changes are automatically logged in the activity history.
The package provides automatic ticket assignment with flexible configuration options.
All Supporters Query (Required for Resources)
Define all users who can support tickets in a panel:
TicketPlugin::make()
->allSupportersQuery(fn () => User::role(['support', 'senior-support', 'admin']))
->registerResources()Initial Assignment Query (Optional)
Define a subset of users for automatic assignment. If not specified, falls back to allSupportersQuery():
TicketPlugin::make()
->allSupportersQuery(fn () => User::role(['support', 'senior-support', 'admin']))
->initialAssignmentSupportersQuery(fn () => User::role('support'))
->assignmentStrategy(new AssignUserWithLeastTickets())DoNotAssign (Default)
Leaves tickets unassigned:
TicketPlugin::make()
->assignmentStrategy(new DoNotAssign())AssignUserWithLeastTickets
Assigns to the user with fewest open tickets:
use Padmission\Tickets\AssignmentStrategies\AssignUserWithLeastTickets;
TicketPlugin::make()
->allSupportersQuery(fn () => User::role('support'))
->assignmentStrategy(new AssignUserWithLeastTickets())AssignRandomUser
Randomly assigns to an eligible user:
use Padmission\Tickets\AssignmentStrategies\AssignRandomUser;
TicketPlugin::make()
->allSupportersQuery(fn () => User::role('support'))
->assignmentStrategy(new AssignRandomUser())AssignDefaultUser
Assigns to a specific user:
use Padmission\Tickets\AssignmentStrategies\AssignDefaultUser;
// Using user ID
TicketPlugin::make()
->allSupportersQuery(fn () => User::role('support'))
->assignmentStrategy(new AssignDefaultUser(userId: 1))
// Using callback
TicketPlugin::make()
->allSupportersQuery(fn () => User::role('support'))
->assignmentStrategy(new AssignDefaultUser(
fn() => User::role('lead-support')->first()->id
))Route tickets from multiple panels to a central support panel:
// Main support panel - manages all tickets
public function panel(Panel $panel): Panel
{
return $panel
->id('support')
->plugin(
TicketPlugin::make()
->allSupportersQuery(fn () => User::role(['support', 'admin']))
->initialAssignmentSupportersQuery(fn () => User::role('tier-1-support'))
->assignmentStrategy(new AssignUserWithLeastTickets())
->registerResources()
);
}
// Customer panel - creates tickets but routes to support
public function panel(Panel $panel): Panel
{
return $panel
->id('customer')
->plugin(
TicketPlugin::make()
->targetPanel('support') // Route tickets to support panel
->initialAssignmentSupportersQuery(fn () => User::role('customer-support'))
->showChatWidget()
->registerResources(false) // Don't show management UI here
);
}
// Enterprise panel - creates tickets with different assignment
public function panel(Panel $panel): Panel
{
return $panel
->id('enterprise')
->plugin(
TicketPlugin::make()
->targetPanel('support')
->initialAssignmentSupportersQuery(fn () => User::role('enterprise-support'))
->showChatWidget()
->registerResources(false)
);
}When tickets are created from different panels, the system tracks the source:
- The
panelcolumn stores where the ticket is managed - The
source_panelcolumn stores where the ticket was created - Source panel is automatically shown in the UI when multiple panels have the chat widget
Extend PanelAwareAssignmentStrategy for automatic query handling:
namespace App\AssignmentStrategies;
use Padmission\Tickets\AssignmentStrategies\PanelAwareAssignmentStrategy;
use Padmission\Tickets\Models\Ticket;
class AssignByWorkload extends PanelAwareAssignmentStrategy
{
public function assign(Ticket $ticket): void
{
// Automatically uses initialAssignmentSupportersQuery or allSupportersQuery
$user = $this->getEligibleUsersQuery($ticket)
->withCount([
'assignedTickets as today_count' => fn ($q) =>
$q->whereDate('created_at', today())
])
->orderBy('today_count')
->first();
if ($user) {
$ticket->assignee_id = $user->id;
// Do NOT call save() - handled automatically
}
}
}Department-Based Assignment
TicketPlugin::make()
->allSupportersQuery(fn () => User::whereHas('department'))
->initialAssignmentSupportersQuery(fn () => User::query()
->whereHas('department', fn ($q) => $q->where('name', 'Support'))
)
->assignmentStrategy(new AssignUserWithLeastTickets())Time-Based Assignment
TicketPlugin::make()
->allSupportersQuery(fn () => User::role('support'))
->initialAssignmentSupportersQuery(fn () => User::role('support')
->whereHas('workSchedule', fn ($q) =>
$q->where('day', now()->dayOfWeek)
->whereTime('start_time', '<=', now())
->whereTime('end_time', '>=', now())
)
)
->assignmentStrategy(new AssignRandomUser())Role-Based with Spatie Permissions
TicketPlugin::make()
->allSupportersQuery(fn () => User::permission('manage tickets'))
->initialAssignmentSupportersQuery(fn () => User::role('support'))
->assignmentStrategy(new AssignUserWithLeastTickets())The package supports granular control over who receives notifications based on event type and actor role. You can configure different notification rules for each Filament panel using a fluent API.
use Padmission\Tickets\ConfigurationManagers\NotificationConfiguration;
use Padmission\Tickets\Enums\NotificationRecipient;
use Padmission\Tickets\Enums\NotificationTrigger;
use Padmission\Tickets\Events\TicketCreatedEvent;
use Padmission\Tickets\Events\TicketActivityEvent;
use Padmission\Tickets\Events\TicketAssignedEvent;
use Padmission\Tickets\Events\TicketClosedEvent;
use Padmission\Tickets\TicketPlugin;
$panel->plugin(
TicketPlugin::make()
->notificationConfiguration(
NotificationConfiguration::make()
->on(
TicketCreatedEvent::class,
fn (NotificationTrigger $trigger) => match ($trigger) {
NotificationTrigger::User => NotificationRecipient::User,
NotificationTrigger::Supporter => NotificationRecipient::Both,
}
)
->on(
TicketActivityEvent::class,
fn (NotificationTrigger $trigger) => match ($trigger) {
NotificationTrigger::User => NotificationRecipient::Supporter,
NotificationTrigger::Supporter => NotificationRecipient::User,
}
)
->on(
TicketAssignedEvent::class,
fn (NotificationTrigger $trigger) => match ($trigger) {
NotificationTrigger::Supporter => NotificationRecipient::Supporter,
default => NotificationRecipient::None,
}
)
->on(
TicketClosedEvent::class,
fn (NotificationTrigger $trigger) => match ($trigger) {
NotificationTrigger::User => NotificationRecipient::Supporter,
NotificationTrigger::Supporter => NotificationRecipient::User,
}
)
)
);The notification system uses two key enums:
NotificationTrigger - Who triggered the event:
NotificationTrigger::User- The ticket submitter performed the actionNotificationTrigger::Supporter- Someone with ticket management permissions performed the action
NotificationRecipient - Who should be notified:
NotificationRecipient::User- Notify the ticket submitter onlyNotificationRecipient::Supporter- Notify the assigned supporter onlyNotificationRecipient::Both- Notify both user and supporterNotificationRecipient::None- Don't send any notifications
For each event type, you define a closure that receives the trigger type and returns who should be notified. This allows for flexible notification rules based on who initiated the action.
The package provides sensible defaults if no configuration is provided:
Ticket Created
- User-triggered: Notifies user only
- Supporter-triggered: Notifies both user and supporter
Ticket Assigned
- Supporter-triggered: Notifies supporter only
- User-triggered: No notifications (users cannot assign tickets)
Ticket Activity (messages, comments)
- User-triggered: Notifies supporter only
- Supporter-triggered: Notifies user only
Ticket Closed
- User-triggered: Notifies supporter only
- Supporter-triggered: Notifies user only
Since configuration is set at the panel level, you can have different rules for different panels:
use Padmission\Tickets\ConfigurationManagers\NotificationConfiguration;
use Padmission\Tickets\Enums\NotificationRecipient;
use Padmission\Tickets\Enums\NotificationTrigger;
use Padmission\Tickets\Events\TicketCreatedEvent;
use Padmission\Tickets\Events\TicketActivityEvent;
// Admin Panel - Notify all parties for everything
$adminPanel->plugin(
TicketPlugin::make()
->notificationConfiguration(
NotificationConfiguration::make()
->on(
TicketCreatedEvent::class,
fn (NotificationTrigger $trigger) => NotificationRecipient::Both
)
->on(
TicketActivityEvent::class,
fn (NotificationTrigger $trigger) => NotificationRecipient::Both
)
)
);
// Customer Panel - More restrictive notifications
$customerPanel->plugin(
TicketPlugin::make()
->notificationConfiguration(
NotificationConfiguration::make()
->on(
TicketCreatedEvent::class,
fn (NotificationTrigger $trigger) => match ($trigger) {
NotificationTrigger::User => NotificationRecipient::User,
NotificationTrigger::Supporter => NotificationRecipient::None,
}
)
->on(
TicketActivityEvent::class,
fn (NotificationTrigger $trigger) => match ($trigger) {
NotificationTrigger::User => NotificationRecipient::None,
NotificationTrigger::Supporter => NotificationRecipient::User,
}
)
)
);You can also implement complex notification logic based on your business requirements:
->on(
TicketCreatedEvent::class,
function (NotificationTrigger $trigger) {
// Custom logic based on time of day, user type, etc.
if ($trigger === NotificationTrigger::User) {
// During business hours, notify both
if (now()->hour >= 9 && now()->hour < 17) {
return NotificationRecipient::Both;
}
// Outside business hours, just notify user
return NotificationRecipient::User;
}
return NotificationRecipient::Both;
}
)All ticket changes are automatically tracked in the activity log:
- Message - Regular ticket messages
- Internal Message - Internal notes not visible to end users
- Opened - Ticket creation
- Priority Changed - Priority modifications
- Status Changed - Status updates
- Turn Changed - Turn ownership changes
- Closed - Ticket closure with disposition
Activities include:
- User who made the change
- Timestamp
- Previous and new values (where applicable)
- Soft delete support for audit trails
The package supports file attachments via Spatie Media Library. Configure the storage disk in your configuration:
// config/padmission-tickets.php
'attachments' => [
'storage' => env('MEDIA_DISK', 's3'),
],Files can be attached to ticket messages through the chat widget or API.
You can extend the package models with your own:
// config/padmission-tickets.php
'models' => [
Padmission\Tickets\Models\Ticket::class => App\Models\Ticket::class,
Padmission\Tickets\Models\TicketActivity::class => App\Models\TicketActivity::class,
// ... other models
],Your custom models should extend the package models to ensure compatibility.
To use custom models with this package, you need to:
- Create your custom model class by extending the base model
- Update the configuration to map the base class to your custom class
<?php
namespace App\Models;
use Padmission\Tickets\Models\Ticket as BaseTicket;
class CustomTicket extends BaseTicket
{
// Your custom functionality
// The observers are automatically inherited from the base class
public function someCustomMethod()
{
// Your custom logic here
}
}Then update your config/padmission-tickets.php file:
'models' => [
// ... other models
\Padmission\Tickets\Models\Ticket::class => \App\Models\CustomTicket::class,
],For other models, follow the same pattern:
// Custom TicketActivity
namespace App\Models;
use Padmission\Tickets\Models\TicketActivity as BaseTicketActivity;
class CustomTicketActivity extends BaseTicketActivity
{
// Your custom functionality
}
// Custom TicketStatus
namespace App\Models;
use Padmission\Tickets\Models\TicketStatus as BaseTicketStatus;
class CustomTicketStatus extends BaseTicketStatus
{
// Your custom functionality
}Then update the config:
'models' => [
// ... other models
\Padmission\Tickets\Models\TicketActivity::class => \App\Models\CustomTicketActivity::class,
\Padmission\Tickets\Models\TicketStatus::class => \App\Models\CustomTicketStatus::class,
],To use custom job classes, you need to:
- Create your custom job class by extending the base job
- Update the configuration to map the base class to your custom class
To add custom properties like tenantId to the notification job:
<?php
namespace App\Jobs;
use Illuminate\Contracts\Auth\Authenticatable;
use Padmission\Tickets\Jobs\NotificationJob;
use Padmission\Tickets\Models\Ticket;
class CustomNotificationJob extends NotificationJob
{
public ?int $tenantId = null;
/**
* Override to add custom initialization
*/
protected function initializeJob(Authenticatable $user, Ticket $model): void
{
// Add your custom logic here
$this->tenantId = $user->tenant_id ?? null;
// Set custom queue, delay, etc.
$this->onQueue('tenant-' . $this->tenantId);
}
/**
* Override unique ID generation to include tenant
*/
protected function buildUniqueId(): string
{
return parent::buildUniqueId() . '-tenant-' . $this->tenantId;
}
/**
* Override notification sending for tenant-specific logic
*/
protected function sendNotification(Authenticatable $user, Ticket $record, string $notificationClass): void
{
// Add tenant-specific notification logic
if ($this->shouldSendNotification($user, $record)) {
parent::sendNotification($user, $record, $notificationClass);
}
}
/**
* Custom logic to determine if notification should be sent
*/
protected function shouldSendNotification(Authenticatable $user, Ticket $record): bool
{
// Add your business logic here
return $user->tenant_id === $record->tenant_id;
}
/**
* Override error handling
*/
protected function handleException(\Exception $e): void
{
// Log errors with tenant context
\Log::error('Notification job failed', [
'tenant_id' => $this->tenantId,
'ticket_id' => $this->getTicketKey(),
'user_id' => $this->getUserId(),
'error' => $e->getMessage(),
]);
}
}Configure the package to use your custom job in your config/padmission-tickets.php:
'jobs' => [
\Padmission\Tickets\Jobs\NotificationJob::class => \App\Jobs\CustomNotificationJob::class,
],This happens when registering resources without defining who can support tickets:
TicketPlugin::make()
->allSupportersQuery(fn () => User::role('support'))
->registerResources()Debug your queries to see what users are being returned:
// Test all supporters query
$query = app()->call(TicketPlugin::get()->getAllSupportersQuery());
dd($query->count(), $query->pluck('name', 'id'));
// Test initial assignment query
$query = app()->call(TicketPlugin::get()->getInitialAssignmentSupportersQuery());
dd($query->count(), $query->pluck('name', 'id'));Ensure your target panel ID matches exactly:
// Panel ID must match exactly (case-sensitive)
->targetPanel('support') // Not 'Support' or 'SUPPORT'- Ensure the User model has the
HasTicketstrait - Check that your queries return users
- Verify the assignment strategy is configured
- Check Laravel logs for exceptions
For additional support:
- Contact support at hello@padmission.com
The Tickets package is a private, paid package. All rights reserved. Unauthorized distribution, modification, or use is strictly prohibited.