updated design
This commit is contained in:
@@ -0,0 +1,181 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Modules\Helpdesk\Providers;
|
||||
|
||||
use Illuminate\Support\Facades\Blade;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Livewire\Livewire;
|
||||
use Modules\Helpdesk\Livewire\HelpdeskDashboardWidget;
|
||||
use Modules\Helpdesk\Livewire\KanbanBoard;
|
||||
use Modules\Helpdesk\Livewire\SettingsManager;
|
||||
use Modules\Helpdesk\Livewire\TicketCreate;
|
||||
use Modules\Helpdesk\Livewire\TicketIndex;
|
||||
use Modules\Helpdesk\Livewire\TicketShow;
|
||||
use Modules\Helpdesk\Livewire\TicketTimer;
|
||||
use Modules\Helpdesk\Models\Ticket;
|
||||
use Modules\Helpdesk\Policies\TicketPolicy;
|
||||
use Modules\Helpdesk\Support\ModuleInfo;
|
||||
|
||||
final class HelpdeskServiceProvider extends ServiceProvider
|
||||
{
|
||||
/**
|
||||
* Module name.
|
||||
*/
|
||||
protected string $moduleName = 'Helpdesk';
|
||||
|
||||
/**
|
||||
* Lowercase module name.
|
||||
*/
|
||||
protected string $moduleNameLower = 'helpdesk';
|
||||
|
||||
/**
|
||||
* Register module services.
|
||||
*/
|
||||
public function register(): void
|
||||
{
|
||||
$this->app->register(RouteServiceProvider::class);
|
||||
|
||||
$this->mergeConfigFrom(
|
||||
module_path(ModuleInfo::name(), 'Config/config.php'),
|
||||
ModuleInfo::nameLower(),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Bootstrap module services.
|
||||
*/
|
||||
public function boot(): void
|
||||
{
|
||||
$this->registerTranslations();
|
||||
$this->registerConfig();
|
||||
$this->registerViews();
|
||||
$this->registerHelpers();
|
||||
$this->registerCommands();
|
||||
$this->registerLivewireComponents();
|
||||
$this->registerPolicies();
|
||||
|
||||
$this->loadMigrationsFrom(module_path(ModuleInfo::name(), 'Database/Migrations'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Register Livewire components used by the module.
|
||||
*/
|
||||
protected function registerLivewireComponents(): void
|
||||
{
|
||||
Livewire::component('helpdesk.ticket-index', TicketIndex::class);
|
||||
Livewire::component('helpdesk.ticket-create', TicketCreate::class);
|
||||
Livewire::component('helpdesk.ticket-show', TicketShow::class);
|
||||
Livewire::component('helpdesk.ticket-timer', TicketTimer::class);
|
||||
Livewire::component('helpdesk.dashboard-widget', HelpdeskDashboardWidget::class);
|
||||
Livewire::component('helpdesk.kanban-board', KanbanBoard::class);
|
||||
Livewire::component('helpdesk.settings-manager', SettingsManager::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register policies and gates.
|
||||
*/
|
||||
protected function registerPolicies(): void
|
||||
{
|
||||
Gate::policy(Ticket::class, TicketPolicy::class);
|
||||
|
||||
Gate::define('helpdesk-admin', static fn ($user): bool => method_exists($user, 'isAdmin') && $user->isAdmin());
|
||||
Gate::define('helpdesk-agent', static fn ($user): bool => (method_exists($user, 'isAdmin') && $user->isAdmin()) || (bool) ($user->helpdesk_agent ?? false));
|
||||
}
|
||||
|
||||
/**
|
||||
* Register helpers.
|
||||
*/
|
||||
protected function registerHelpers(): void
|
||||
{
|
||||
foreach (glob(module_path(ModuleInfo::name(), 'Helpers/*.php')) ?: [] as $filename) {
|
||||
require_once $filename;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dynamically register console commands.
|
||||
*/
|
||||
protected function registerCommands(): void
|
||||
{
|
||||
if (! $this->app->runningInConsole()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$commandPath = module_path(ModuleInfo::name(), 'Console');
|
||||
|
||||
if (! is_dir($commandPath)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$commands = collect(File::allFiles($commandPath))
|
||||
->map(fn ($file): string => 'Modules\\'.$this->moduleName.'\\Console\\'.$file->getFilenameWithoutExtension())
|
||||
->filter(fn (string $class): bool => class_exists($class))
|
||||
->values()
|
||||
->all();
|
||||
|
||||
$this->commands($commands);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register config publishing.
|
||||
*/
|
||||
protected function registerConfig(): void
|
||||
{
|
||||
$this->publishes([
|
||||
module_path(ModuleInfo::name(), 'Config/config.php') => config_path(ModuleInfo::nameLower().'.php'),
|
||||
], 'config');
|
||||
}
|
||||
|
||||
/**
|
||||
* Register module views.
|
||||
*/
|
||||
protected function registerViews(): void
|
||||
{
|
||||
$sourcePath = module_path(ModuleInfo::name(), 'Resources/views');
|
||||
$themeRoot = rtrim((string) config('stylist.themes_path', resource_path('themes')), DIRECTORY_SEPARATOR);
|
||||
$backendTheme = (string) config('stylist.default_backend_theme', 'backend');
|
||||
$viewPath = $themeRoot.DIRECTORY_SEPARATOR.$backendTheme
|
||||
.DIRECTORY_SEPARATOR.'views'.DIRECTORY_SEPARATOR.'modules'.DIRECTORY_SEPARATOR.ModuleInfo::nameLower();
|
||||
|
||||
// Module source views remain module-owned. Any application override is
|
||||
// published into the selected backend theme, never resources/views.
|
||||
$this->publishes([
|
||||
$sourcePath => $viewPath,
|
||||
], ['views', ModuleInfo::nameLower().'-module-views']);
|
||||
|
||||
$this->loadViewsFrom($sourcePath, ModuleInfo::nameLower());
|
||||
|
||||
Blade::componentNamespace('Modules\Helpdesk\View\Components', 'helpdesk');
|
||||
Blade::anonymousComponentPath($sourcePath.'/components', 'helpdesk');
|
||||
}
|
||||
|
||||
/**
|
||||
* Register translations.
|
||||
*/
|
||||
protected function registerTranslations(): void
|
||||
{
|
||||
$langPath = resource_path('lang/modules/'.ModuleInfo::nameLower());
|
||||
|
||||
if (is_dir($langPath)) {
|
||||
$this->loadTranslationsFrom($langPath, ModuleInfo::nameLower());
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->loadTranslationsFrom(module_path(ModuleInfo::name(), 'Resources/lang'), ModuleInfo::nameLower());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get provided services.
|
||||
*
|
||||
* @return array<int, string>
|
||||
*/
|
||||
public function provides(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
return [
|
||||
'route_prefix' => env('HELPDESK_ROUTE_PREFIX', 'admin/helpdesk'),
|
||||
'middleware' => ['web', 'auth', 'thredary.admin'],
|
||||
'middleware' => ['web', 'auth'],
|
||||
'disk' => env('HELPDESK_ATTACHMENTS_DISK', 'local'),
|
||||
'max_upload_kb' => (int) env('HELPDESK_MAX_UPLOAD_KB', 10240),
|
||||
'allowed_extensions' => [
|
||||
|
||||
@@ -6,7 +6,6 @@ namespace Modules\Helpdesk\Http\Controllers;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Contracts\View\View;
|
||||
use Illuminate\Support\HtmlString;
|
||||
|
||||
final class HelpdeskController extends Controller
|
||||
{
|
||||
@@ -15,9 +14,6 @@ final class HelpdeskController extends Controller
|
||||
*/
|
||||
public function __invoke(): View
|
||||
{
|
||||
return view('layouts.app', [
|
||||
'title' => __('Helpdesk'),
|
||||
'slot' => new HtmlString(view('helpdesk::index')->render()),
|
||||
]);
|
||||
return view('modules.helpdesk.index');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ final class TicketCommentDeleteController extends Controller
|
||||
{
|
||||
$user = $request->user();
|
||||
|
||||
abort_unless($user !== null && method_exists($user, 'isAdmin') && $user->isAdmin(), 403);
|
||||
abort_unless($user !== null && $user->isHelpdeskAdmin(), 403);
|
||||
abort_unless((int) $comment->ticket_id === (int) $ticket->id, 404);
|
||||
|
||||
DB::transaction(function () use ($comment, $ticket, $user): void {
|
||||
|
||||
@@ -156,7 +156,7 @@ final class TicketCommentStoreController extends Controller
|
||||
return false;
|
||||
}
|
||||
|
||||
return (method_exists($user, 'isAdmin') && $user->isAdmin()) || (bool) ($user->helpdesk_agent ?? false);
|
||||
return $user->isHelpdeskAgent();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -17,9 +17,9 @@ final class HelpdeskDashboardWidget extends Component
|
||||
$user = Auth::user();
|
||||
$base = Ticket::query();
|
||||
|
||||
if ($user->helpdesk_admin ?? false) {
|
||||
if ($user->isHelpdeskAdmin()) {
|
||||
// no filter
|
||||
} elseif ($user->helpdesk_agent ?? false) {
|
||||
} elseif ($user->isHelpdeskAgent()) {
|
||||
$base->where(function ($q) use ($user) {
|
||||
$q->where('agent_id', $user->id)
|
||||
->orWhere('user_id', $user->id);
|
||||
|
||||
@@ -691,7 +691,7 @@ final class TicketShow extends Component
|
||||
{
|
||||
$user = Auth::user();
|
||||
|
||||
return $user !== null && (method_exists($user, 'isAdmin') ? $user->isAdmin() : (bool) ($user->helpdesk_admin ?? false));
|
||||
return $user !== null && $user->isHelpdeskAdmin();
|
||||
}
|
||||
|
||||
private function buildCommentTree(Collection $grouped, ?int $parentId = null): Collection
|
||||
|
||||
@@ -194,11 +194,11 @@ final class Ticket extends Model
|
||||
/** @param Builder<Ticket> $query @return Builder<Ticket> */
|
||||
public function scopeVisibleTo(Builder $query, User $user): Builder
|
||||
{
|
||||
if ($user->isAdmin() || (bool) ($user->helpdesk_admin ?? false)) {
|
||||
if ($user->isHelpdeskAdmin()) {
|
||||
return $query;
|
||||
}
|
||||
|
||||
if ((bool) ($user->helpdesk_agent ?? false)) {
|
||||
if ($user->isHelpdeskAgent()) {
|
||||
return $query->where(static function (Builder $subQuery) use ($user): void {
|
||||
$subQuery->where('agent_id', $user->id)->orWhere('user_id', $user->id);
|
||||
});
|
||||
|
||||
@@ -17,7 +17,7 @@ final class TicketPolicy
|
||||
public function view(User $user, Ticket $ticket): bool
|
||||
{
|
||||
return $this->isPrivileged($user)
|
||||
|| ((bool) ($user->helpdesk_agent ?? false) && ((int) $ticket->agent_id === (int) $user->id || (int) $ticket->user_id === (int) $user->id))
|
||||
|| ($user->isHelpdeskAgent() && ((int) $ticket->agent_id === (int) $user->id || (int) $ticket->user_id === (int) $user->id))
|
||||
|| (int) $ticket->user_id === (int) $user->id;
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ final class TicketPolicy
|
||||
public function update(User $user, Ticket $ticket): bool
|
||||
{
|
||||
return $this->isPrivileged($user)
|
||||
|| ((bool) ($user->helpdesk_agent ?? false) && (int) $ticket->agent_id === (int) $user->id);
|
||||
|| ($user->isHelpdeskAgent() && (int) $ticket->agent_id === (int) $user->id);
|
||||
}
|
||||
|
||||
public function comment(User $user, Ticket $ticket): bool
|
||||
@@ -44,14 +44,6 @@ final class TicketPolicy
|
||||
|
||||
private function isPrivileged(User $user): bool
|
||||
{
|
||||
if ($user->isAdmin() || (bool) ($user->helpdesk_admin ?? false)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (method_exists($user, 'isAdmin') && $user->isAdmin()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
return $user->isHelpdeskAdmin();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ declare(strict_types=1);
|
||||
namespace Modules\Helpdesk\Providers;
|
||||
|
||||
use Illuminate\Support\Facades\Blade;
|
||||
use Illuminate\Support\Facades\Config;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
@@ -82,8 +83,8 @@ final class HelpdeskServiceProvider extends ServiceProvider
|
||||
{
|
||||
Gate::policy(Ticket::class, TicketPolicy::class);
|
||||
|
||||
Gate::define('helpdesk-admin', static fn ($user): bool => method_exists($user, 'isAdmin') && $user->isAdmin());
|
||||
Gate::define('helpdesk-agent', static fn ($user): bool => (method_exists($user, 'isAdmin') && $user->isAdmin()) || (bool) ($user->helpdesk_agent ?? false));
|
||||
Gate::define('helpdesk-admin', static fn ($user): bool => $user->isHelpdeskAdmin());
|
||||
Gate::define('helpdesk-agent', static fn ($user): bool => $user->isHelpdeskAgent());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -135,19 +136,14 @@ final class HelpdeskServiceProvider extends ServiceProvider
|
||||
*/
|
||||
protected function registerViews(): void
|
||||
{
|
||||
$viewPath = resource_path('views/modules/'.ModuleInfo::nameLower());
|
||||
$sourcePath = module_path(ModuleInfo::name(), 'Resources/views');
|
||||
$themeRoot = rtrim((string) config('stylist.themes_path', resource_path('themes')), DIRECTORY_SEPARATOR);
|
||||
$backendTheme = (string) config('stylist.default_backend_theme', 'backend');
|
||||
$viewPath = $themeRoot.DIRECTORY_SEPARATOR.$backendTheme
|
||||
.DIRECTORY_SEPARATOR.'views'.DIRECTORY_SEPARATOR.'modules'.DIRECTORY_SEPARATOR.ModuleInfo::nameLower();
|
||||
|
||||
// Module source views remain module-owned. Any application override is
|
||||
// published into the selected backend theme, never resources/views.
|
||||
$this->publishes([
|
||||
$sourcePath => $viewPath,
|
||||
], ['views', ModuleInfo::nameLower().'-module-views']);
|
||||
|
||||
$this->loadViewsFrom($sourcePath, ModuleInfo::nameLower());
|
||||
$this->loadViewsFrom(array_merge($this->getPublishableViewPaths(), [$sourcePath]), ModuleInfo::nameLower());
|
||||
|
||||
Blade::componentNamespace('Modules\Helpdesk\View\Components', 'helpdesk');
|
||||
Blade::anonymousComponentPath($sourcePath.'/components', 'helpdesk');
|
||||
@@ -169,6 +165,24 @@ final class HelpdeskServiceProvider extends ServiceProvider
|
||||
$this->loadTranslationsFrom(module_path(ModuleInfo::name(), 'Resources/lang'), ModuleInfo::nameLower());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get publishable view override paths.
|
||||
*
|
||||
* @return array<int, string>
|
||||
*/
|
||||
private function getPublishableViewPaths(): array
|
||||
{
|
||||
$paths = [];
|
||||
|
||||
foreach (Config::get('view.paths') as $path) {
|
||||
if (is_dir($path.'/modules/'.ModuleInfo::nameLower())) {
|
||||
$paths[] = $path.'/modules/'.ModuleInfo::nameLower();
|
||||
}
|
||||
}
|
||||
|
||||
return $paths;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get provided services.
|
||||
*
|
||||
|
||||
@@ -70,6 +70,7 @@ final class StylistServiceProvider extends ServiceProvider
|
||||
// replace Laravel's base view locations during their own boot phase; the booted
|
||||
// callback makes Stylist the final authority for ordinary view('...') resolution.
|
||||
$this->registerSelectedThemeOverrides();
|
||||
$this->registerSelectedThemeFluxOverrides();
|
||||
|
||||
$this->app->booted(function (): void {
|
||||
$this->registerSelectedThemeOverrides();
|
||||
@@ -247,10 +248,12 @@ final class StylistServiceProvider extends ServiceProvider
|
||||
/**
|
||||
* Make Flux application overrides theme-owned.
|
||||
*
|
||||
* Flux normally discovers custom components from resources/views/flux. This
|
||||
* application deliberately has no resources/views tree, so expose the selected
|
||||
* theme's views/flux directory through Flux's existing view namespace instead.
|
||||
* Package-provided Flux views remain available as the final fallback.
|
||||
* Flux registers anonymous component paths using the prefix "flux". Laravel
|
||||
* converts that prefix into a hashed view namespace (xxh128 on Laravel 12+).
|
||||
* Registering only a normal "flux" view namespace therefore does not satisfy
|
||||
* Flux::componentExists(). Register each themed views/flux directory through
|
||||
* Blade exactly as Flux itself does, then prepend it to the hashed namespace so
|
||||
* the selected theme wins over package stubs while retaining package fallbacks.
|
||||
*/
|
||||
private function registerSelectedThemeFluxOverrides(): void
|
||||
{
|
||||
@@ -281,7 +284,7 @@ final class StylistServiceProvider extends ServiceProvider
|
||||
|
||||
$fluxPath = $theme->path().DIRECTORY_SEPARATOR.'views'.DIRECTORY_SEPARATOR.'flux';
|
||||
|
||||
if (is_dir($fluxPath)) {
|
||||
if (is_dir($fluxPath) && ! in_array($fluxPath, $themeFluxPaths, true)) {
|
||||
$themeFluxPaths[] = $fluxPath;
|
||||
}
|
||||
}
|
||||
@@ -290,27 +293,25 @@ final class StylistServiceProvider extends ServiceProvider
|
||||
return;
|
||||
}
|
||||
|
||||
// This mirrors FluxServiceProvider::bootComponentPath(). It is essential:
|
||||
// Flux::componentExists() checks the hashed namespace created by
|
||||
// Blade::anonymousComponentPath(..., 'flux'), not a literal flux:: namespace.
|
||||
foreach ($themeFluxPaths as $fluxPath) {
|
||||
Blade::anonymousComponentPath($fluxPath, 'flux');
|
||||
}
|
||||
|
||||
$fluxNamespace = version_compare($this->app->version(), '12.0.0', '>=')
|
||||
? hash('xxh128', 'flux')
|
||||
: md5('flux');
|
||||
|
||||
// anonymousComponentPath() adds locations to the namespace. Prepend the
|
||||
// selected theme paths in reverse order so the first selected theme remains
|
||||
// authoritative while Flux package views continue to work as fallbacks.
|
||||
foreach (array_reverse($themeFluxPaths) as $fluxPath) {
|
||||
View::prependNamespace($fluxNamespace, $fluxPath);
|
||||
}
|
||||
|
||||
$finder = View::getFinder();
|
||||
|
||||
if (! method_exists($finder, 'getHints')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$hints = $finder->getHints();
|
||||
$existingFluxPaths = array_values(array_filter(
|
||||
(array) ($hints['flux'] ?? []),
|
||||
static fn (string $path): bool => is_dir($path),
|
||||
));
|
||||
$paths = array_values(array_unique(array_merge($themeFluxPaths, $existingFluxPaths)));
|
||||
|
||||
if (method_exists($finder, 'replaceNamespace')) {
|
||||
$finder->replaceNamespace('flux', $paths);
|
||||
} else {
|
||||
foreach (array_reverse($themeFluxPaths) as $fluxPath) {
|
||||
View::prependNamespace('flux', $fluxPath);
|
||||
}
|
||||
}
|
||||
|
||||
if (method_exists($finder, 'flush')) {
|
||||
$finder->flush();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,457 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Stylist\Providers;
|
||||
|
||||
use Illuminate\Support\Facades\Blade;
|
||||
use Illuminate\Support\Facades\View;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Modules\Stylist\Console\CreateThemeCommand;
|
||||
use Modules\Stylist\Console\FindDuplicateViewsCommand;
|
||||
use Modules\Stylist\Console\InspectThemesCommand;
|
||||
use Modules\Stylist\Console\PublishAssetsCommand;
|
||||
use Modules\Stylist\Console\ThemeDoctorCommand;
|
||||
use Modules\Stylist\Console\ValidateThemeCommand;
|
||||
use Modules\Stylist\Html\ThemeHtmlBuilder;
|
||||
use Modules\Stylist\Html\ThemeSeoBuilder;
|
||||
use Modules\Stylist\Support\ModuleInfo;
|
||||
use Modules\Stylist\Theme\Loader;
|
||||
use Modules\Stylist\Theme\Stylist;
|
||||
use Modules\Stylist\Theme\Theme;
|
||||
|
||||
final class StylistServiceProvider extends ServiceProvider
|
||||
{
|
||||
protected string $moduleName = 'Stylist';
|
||||
|
||||
protected string $moduleNameLower = 'stylist';
|
||||
|
||||
public function register(): void
|
||||
{
|
||||
$this->app->register(RouteServiceProvider::class);
|
||||
|
||||
$this->mergeConfigFrom(module_path(ModuleInfo::name(), 'Config/config.php'), ModuleInfo::nameLower());
|
||||
|
||||
$this->app->singleton('stylist', fn ($app) => $app->make(Stylist::class));
|
||||
$this->app->singleton('stylist.html', fn ($app) => $app->make(ThemeHtmlBuilder::class));
|
||||
$this->app->singleton('stylist.seo', fn ($app) => $app->make(ThemeSeoBuilder::class));
|
||||
}
|
||||
|
||||
public function boot(): void
|
||||
{
|
||||
$this->registerViews();
|
||||
$this->registerThemeViews();
|
||||
$this->registerTranslations();
|
||||
$this->registerPublishing();
|
||||
$this->registerHelpers();
|
||||
$this->registerBlade();
|
||||
|
||||
if ($this->app->runningInConsole()) {
|
||||
$this->commands([
|
||||
PublishAssetsCommand::class,
|
||||
ValidateThemeCommand::class,
|
||||
InspectThemesCommand::class,
|
||||
FindDuplicateViewsCommand::class,
|
||||
ThemeDoctorCommand::class,
|
||||
CreateThemeCommand::class,
|
||||
]);
|
||||
}
|
||||
|
||||
$this->loadMigrationsFrom(module_path(ModuleInfo::name(), 'Database/Migrations'));
|
||||
|
||||
View::composer('*', function ($view): void {
|
||||
if (! $this->app->bound(Stylist::class)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$view->with('activeTheme', $this->app->make(Stylist::class)->current());
|
||||
});
|
||||
|
||||
// Register both selected presentation trees immediately, then register them again
|
||||
// after every service provider has finished booting. Some package/module providers
|
||||
// replace Laravel's base view locations during their own boot phase; the booted
|
||||
// callback makes Stylist the final authority for ordinary view('...') resolution.
|
||||
$this->registerSelectedThemeOverrides();
|
||||
$this->registerSelectedThemeFluxOverrides();
|
||||
|
||||
$this->app->booted(function (): void {
|
||||
$this->registerSelectedThemeOverrides();
|
||||
$this->registerSelectedThemeFluxOverrides();
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
private function registerViews(): void
|
||||
{
|
||||
$sourcePath = module_path(ModuleInfo::name(), 'Resources/views');
|
||||
|
||||
// Module-owned package views stay inside the module. Application overrides
|
||||
// belong to resources/themes/{theme}/views/modules/stylist, which Stylist
|
||||
// registers as the authoritative themed namespace. Never publish back into
|
||||
// resources/views; the standard baseline intentionally has no legacy view tree.
|
||||
$this->loadViewsFrom($sourcePath, ModuleInfo::nameLower());
|
||||
}
|
||||
|
||||
private function registerThemeViews(): void
|
||||
{
|
||||
foreach ($this->themeRoots() as $root) {
|
||||
if (! is_dir($root)) {
|
||||
logger()->warning('Stylist theme root does not exist', [
|
||||
'root' => $root,
|
||||
]);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$themeDirectories = glob($root.'/*', GLOB_ONLYDIR) ?: [];
|
||||
|
||||
foreach ($themeDirectories as $themeDirectory) {
|
||||
$themeViewsPath = $themeDirectory.'/views';
|
||||
|
||||
if (! is_dir($themeViewsPath)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$themeKey = strtolower(basename($themeDirectory));
|
||||
|
||||
// Themes always live outside the module, normally in resources/themes.
|
||||
// Register the canonical namespace and the legacy namespace as an alias
|
||||
// to the same application-owned directory. The alias keeps existing
|
||||
// routes and Blade templates working without reintroducing module themes.
|
||||
$namespaces = [
|
||||
"stylist-theme-{$themeKey}",
|
||||
"stylist-module-theme-{$themeKey}",
|
||||
];
|
||||
|
||||
$componentsPath = $themeViewsPath.'/components';
|
||||
|
||||
// The common theme is the application-wide owner of layouts,
|
||||
// authentication, settings and shared components. Livewire 4
|
||||
// resolves its default full-page layout through layouts::app,
|
||||
// so point that compatibility namespace at the common theme.
|
||||
if ($themeKey === 'common') {
|
||||
$layoutsPath = $componentsPath.'/layouts';
|
||||
|
||||
if (is_dir($layoutsPath)) {
|
||||
View::addNamespace('layouts', $layoutsPath);
|
||||
Blade::anonymousComponentPath($layoutsPath, 'layouts');
|
||||
}
|
||||
|
||||
View::addNamespace('common', $themeViewsPath);
|
||||
|
||||
if (is_dir($componentsPath)) {
|
||||
Blade::anonymousComponentPath($componentsPath, 'common');
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($namespaces as $namespace) {
|
||||
View::addNamespace($namespace, $themeViewsPath);
|
||||
|
||||
if (is_dir($componentsPath)) {
|
||||
Blade::anonymousComponentPath($componentsPath, $namespace);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function registerSelectedThemeOverrides(): void
|
||||
{
|
||||
$stylist = $this->app->make(Stylist::class);
|
||||
|
||||
$current = $stylist->current();
|
||||
$frontend = $stylist->frontend();
|
||||
$backend = $stylist->backend();
|
||||
|
||||
// The active request context must win ordinary view resolution. Keep the
|
||||
// opposite presentation tree available as a compatibility fallback, but
|
||||
// never let frontend views shadow backend views on backend requests (or
|
||||
// vice versa) simply because of provider registration order.
|
||||
$orderedThemes = [$current, $frontend, $backend];
|
||||
$chains = [];
|
||||
$seenThemes = [];
|
||||
|
||||
foreach ($orderedThemes as $theme) {
|
||||
if (isset($seenThemes[$theme->key()])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$seenThemes[$theme->key()] = true;
|
||||
$chains[] = $this->themeChain($theme);
|
||||
}
|
||||
|
||||
$keys = [];
|
||||
foreach ($chains as $chain) {
|
||||
foreach ($chain as $key) {
|
||||
if (! in_array($key, $keys, true)) {
|
||||
$keys[] = $key;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (! in_array('common', $keys, true)) {
|
||||
$keys[] = 'common';
|
||||
}
|
||||
|
||||
// Laravel's starter kits resolve authenticated layouts through the
|
||||
// x-layouts:: namespace. Once those views live inside the backend
|
||||
// theme, Stylist must make that selected theme authoritative for the
|
||||
// namespace as well as for ordinary view('...') lookups.
|
||||
$this->registerLayoutCompatibilityNamespace($stylist->backend());
|
||||
|
||||
$finder = View::getFinder();
|
||||
$themePaths = [];
|
||||
|
||||
foreach ($keys as $key) {
|
||||
try {
|
||||
$theme = $this->app->make(Loader::class)->find($key);
|
||||
} catch (\Throwable) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$viewsPath = $theme->path().DIRECTORY_SEPARATOR.'views';
|
||||
if (is_dir($viewsPath)) {
|
||||
$themePaths[] = $viewsPath;
|
||||
}
|
||||
|
||||
$this->registerThemeModuleNamespaces($key);
|
||||
}
|
||||
|
||||
// FileViewFinder is the source of truth for ordinary view('...') resolution.
|
||||
// Set the complete ordered path list explicitly instead of relying on provider
|
||||
// boot order and incremental prependLocation() calls. Theme paths stay first;
|
||||
// Laravel's normal view locations remain as the final fallback.
|
||||
if (method_exists($finder, 'getPaths') && method_exists($finder, 'setPaths')) {
|
||||
// The application intentionally has no resources/views tree. Laravel's
|
||||
// default FileViewFinder still starts with that location, and view:cache
|
||||
// asks Symfony Finder to scan every registered path. Remove missing legacy
|
||||
// locations so theme-only applications can be optimized on a clean release.
|
||||
$existingPaths = array_values(array_filter(
|
||||
$finder->getPaths(),
|
||||
static fn (string $path): bool => is_dir($path),
|
||||
));
|
||||
$paths = array_values(array_unique(array_merge($themePaths, $existingPaths)));
|
||||
$finder->setPaths($paths);
|
||||
|
||||
if (method_exists($finder, 'flush')) {
|
||||
$finder->flush();
|
||||
}
|
||||
} else {
|
||||
foreach (array_reverse($themePaths) as $viewsPath) {
|
||||
if (method_exists($finder, 'prependLocation')) {
|
||||
$finder->prependLocation($viewsPath);
|
||||
} else {
|
||||
View::addLocation($viewsPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Make Flux application overrides theme-owned.
|
||||
*
|
||||
* Flux registers anonymous component paths using the prefix "flux". Laravel
|
||||
* converts that prefix into a hashed view namespace (xxh128 on Laravel 12+).
|
||||
* Registering only a normal "flux" view namespace therefore does not satisfy
|
||||
* Flux::componentExists(). Register each themed views/flux directory through
|
||||
* Blade exactly as Flux itself does, then prepend it to the hashed namespace so
|
||||
* the selected theme wins over package stubs while retaining package fallbacks.
|
||||
*/
|
||||
private function registerSelectedThemeFluxOverrides(): void
|
||||
{
|
||||
$stylist = $this->app->make(Stylist::class);
|
||||
$orderedThemes = [$stylist->current(), $stylist->frontend(), $stylist->backend()];
|
||||
$keys = [];
|
||||
|
||||
foreach ($orderedThemes as $theme) {
|
||||
foreach ($this->themeChain($theme) as $key) {
|
||||
if (! in_array($key, $keys, true)) {
|
||||
$keys[] = $key;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (! in_array('common', $keys, true)) {
|
||||
$keys[] = 'common';
|
||||
}
|
||||
|
||||
$themeFluxPaths = [];
|
||||
|
||||
foreach ($keys as $key) {
|
||||
try {
|
||||
$theme = $this->app->make(Loader::class)->find($key);
|
||||
} catch (\Throwable) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$fluxPath = $theme->path().DIRECTORY_SEPARATOR.'views'.DIRECTORY_SEPARATOR.'flux';
|
||||
|
||||
if (is_dir($fluxPath) && ! in_array($fluxPath, $themeFluxPaths, true)) {
|
||||
$themeFluxPaths[] = $fluxPath;
|
||||
}
|
||||
}
|
||||
|
||||
if ($themeFluxPaths === []) {
|
||||
return;
|
||||
}
|
||||
|
||||
// This mirrors FluxServiceProvider::bootComponentPath(). It is essential:
|
||||
// Flux::componentExists() checks the hashed namespace created by
|
||||
// Blade::anonymousComponentPath(..., 'flux'), not a literal flux:: namespace.
|
||||
foreach ($themeFluxPaths as $fluxPath) {
|
||||
Blade::anonymousComponentPath($fluxPath, 'flux');
|
||||
}
|
||||
|
||||
$fluxNamespace = version_compare($this->app->version(), '12.0.0', '>=')
|
||||
? hash('xxh128', 'flux')
|
||||
: md5('flux');
|
||||
|
||||
// anonymousComponentPath() adds locations to the namespace. Prepend the
|
||||
// selected theme paths in reverse order so the first selected theme remains
|
||||
// authoritative while Flux package views continue to work as fallbacks.
|
||||
foreach (array_reverse($themeFluxPaths) as $fluxPath) {
|
||||
View::prependNamespace($fluxNamespace, $fluxPath);
|
||||
}
|
||||
|
||||
$finder = View::getFinder();
|
||||
if (method_exists($finder, 'flush')) {
|
||||
$finder->flush();
|
||||
}
|
||||
}
|
||||
|
||||
/** @return array<int, string> */
|
||||
private function themeChain(Theme $theme): array
|
||||
{
|
||||
$loader = $this->app->make(Loader::class);
|
||||
$chain = [];
|
||||
$seen = [];
|
||||
|
||||
while (! isset($seen[$theme->key()])) {
|
||||
$seen[$theme->key()] = true;
|
||||
$chain[] = $theme->key();
|
||||
|
||||
$parent = $theme->parent();
|
||||
if (! $parent) {
|
||||
break;
|
||||
}
|
||||
|
||||
try {
|
||||
$theme = $loader->find($parent);
|
||||
} catch (\Throwable) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $chain;
|
||||
}
|
||||
|
||||
private function prependThemeViewTree(string $themeKey): void
|
||||
{
|
||||
try {
|
||||
$theme = $this->app->make(Loader::class)->find($themeKey);
|
||||
} catch (\Throwable) {
|
||||
return;
|
||||
}
|
||||
|
||||
$viewsPath = $theme->path().DIRECTORY_SEPARATOR.'views';
|
||||
if (! is_dir($viewsPath)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$finder = View::getFinder();
|
||||
if (method_exists($finder, 'prependLocation')) {
|
||||
$finder->prependLocation($viewsPath);
|
||||
} else {
|
||||
View::addLocation($viewsPath);
|
||||
}
|
||||
|
||||
$this->registerThemeModuleNamespaces($themeKey);
|
||||
}
|
||||
|
||||
private function registerLayoutCompatibilityNamespace(Theme $theme): void
|
||||
{
|
||||
$candidates = [
|
||||
$theme->path().DIRECTORY_SEPARATOR.'views'.DIRECTORY_SEPARATOR.'layouts',
|
||||
$theme->path().DIRECTORY_SEPARATOR.'views'.DIRECTORY_SEPARATOR.'components'.DIRECTORY_SEPARATOR.'layouts',
|
||||
];
|
||||
|
||||
foreach ($candidates as $layoutsPath) {
|
||||
if (! is_dir($layoutsPath)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
View::prependNamespace('layouts', $layoutsPath);
|
||||
Blade::anonymousComponentPath($layoutsPath, 'layouts');
|
||||
}
|
||||
}
|
||||
|
||||
private function registerThemeModuleNamespaces(string $themeKey): void
|
||||
{
|
||||
try {
|
||||
$theme = $this->app->make(Loader::class)->find($themeKey);
|
||||
} catch (\Throwable) {
|
||||
return;
|
||||
}
|
||||
|
||||
$modulesPath = $theme->path().DIRECTORY_SEPARATOR.'views'.DIRECTORY_SEPARATOR.'modules';
|
||||
if (! is_dir($modulesPath)) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (glob($modulesPath.'/*', GLOB_ONLYDIR) ?: [] as $moduleViewsPath) {
|
||||
$namespace = strtolower(basename($moduleViewsPath));
|
||||
View::prependNamespace($namespace, $moduleViewsPath);
|
||||
|
||||
$componentsPath = $moduleViewsPath.DIRECTORY_SEPARATOR.'components';
|
||||
if (is_dir($componentsPath)) {
|
||||
Blade::anonymousComponentPath($componentsPath, $namespace);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function normalisePath(string $path): string
|
||||
{
|
||||
return rtrim(
|
||||
str_replace('\\', '/', realpath($path) ?: $path),
|
||||
'/'
|
||||
);
|
||||
}
|
||||
|
||||
/** @return array<int, string> */
|
||||
private function themeRoots(): array
|
||||
{
|
||||
return array_values(array_unique(array_filter([
|
||||
(string) config('stylist.themes_path'),
|
||||
])));
|
||||
}
|
||||
|
||||
private function registerTranslations(): void
|
||||
{
|
||||
$langPath = resource_path('lang/modules/'.ModuleInfo::nameLower());
|
||||
$sourcePath = module_path(ModuleInfo::name(), 'Resources/lang');
|
||||
|
||||
$this->loadTranslationsFrom(is_dir($langPath) ? $langPath : $sourcePath, ModuleInfo::nameLower());
|
||||
}
|
||||
|
||||
private function registerPublishing(): void
|
||||
{
|
||||
$this->publishes([
|
||||
module_path(ModuleInfo::name(), 'Config/config.php') => config_path('stylist.php'),
|
||||
], ['config', 'stylist-config']);
|
||||
|
||||
}
|
||||
|
||||
private function registerHelpers(): void
|
||||
{
|
||||
foreach (glob(module_path(ModuleInfo::name(), 'Helpers/*.php')) ?: [] as $filename) {
|
||||
require_once $filename;
|
||||
}
|
||||
}
|
||||
|
||||
private function registerBlade(): void
|
||||
{
|
||||
Blade::directive('themeVariables', fn () => '<?php echo stylist_html()->variables(); ?>');
|
||||
Blade::directive('themeStylesheet', fn ($expression) => '<?php echo stylist_html()->stylesheet('.($expression ?: 'null').'); ?>');
|
||||
Blade::directive('themeScript', fn ($expression) => '<?php echo stylist_html()->script('.($expression ?: 'null').'); ?>');
|
||||
Blade::directive('themeSwitcher', fn () => '<?php echo stylist_html()->switcher(); ?>');
|
||||
Blade::directive('themeSeo', fn ($expression) => '<?php echo app(\Modules\Stylist\Html\ThemeSeoBuilder::class)->render('.($expression ?: '[]').'); ?>');
|
||||
}
|
||||
}
|
||||
+23
-1
@@ -14,6 +14,7 @@ use Illuminate\Support\Str;
|
||||
use Laravel\Fortify\Contracts\PasskeyUser;
|
||||
use Laravel\Fortify\PasskeyAuthenticatable;
|
||||
use Laravel\Fortify\TwoFactorAuthenticatable;
|
||||
use Modules\Helpdesk\Concerns\HasHelpdeskTickets;
|
||||
|
||||
/**
|
||||
* @property int $id
|
||||
@@ -33,7 +34,7 @@ use Laravel\Fortify\TwoFactorAuthenticatable;
|
||||
class User extends Authenticatable implements PasskeyUser
|
||||
{
|
||||
/** @use HasFactory<UserFactory> */
|
||||
use HasFactory, Notifiable, PasskeyAuthenticatable, TwoFactorAuthenticatable;
|
||||
use HasFactory, HasHelpdeskTickets, Notifiable, PasskeyAuthenticatable, TwoFactorAuthenticatable;
|
||||
|
||||
/**
|
||||
* Get the attributes that should be cast.
|
||||
@@ -45,9 +46,30 @@ class User extends Authenticatable implements PasskeyUser
|
||||
return [
|
||||
'email_verified_at' => 'datetime',
|
||||
'password' => 'hashed',
|
||||
'helpdesk_agent' => 'boolean',
|
||||
'helpdesk_admin' => 'boolean',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Backwards-compatible application-level admin check.
|
||||
*
|
||||
* Helpdesk currently owns the admin role, so this maps to the
|
||||
* Helpdesk admin permission flag.
|
||||
*/
|
||||
public function isAdmin(): bool
|
||||
{
|
||||
return $this->isHelpdeskAdmin();
|
||||
}
|
||||
|
||||
/**
|
||||
* Backwards-compatible application-level agent check.
|
||||
*/
|
||||
public function isAgent(): bool
|
||||
{
|
||||
return $this->isHelpdeskAgent();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the user's initials
|
||||
*/
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
@import 'tailwindcss';
|
||||
@import '../../../../../vendor/livewire/flux/dist/flux.css';
|
||||
@import './helpdesk.css';
|
||||
|
||||
@source '../../views';
|
||||
@source '../../views/modules/helpdesk/**/*.blade.php';
|
||||
@source '../../../../../Modules/Helpdesk/Livewire/**/*.php';
|
||||
@source '../../../**/*.blade.php';
|
||||
@source '../../../../../vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php';
|
||||
@source '../../../../../vendor/livewire/flux-pro/stubs/**/*.blade.php';
|
||||
|
||||
@@ -0,0 +1,974 @@
|
||||
.hd-helpdesk-surface .hd-btn,
|
||||
.hd-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: .5rem;
|
||||
border-radius: .85rem;
|
||||
padding: .62rem 1.05rem;
|
||||
font-size: .875rem;
|
||||
line-height: 1.25rem;
|
||||
font-weight: 800;
|
||||
color: #ffffff !important;
|
||||
text-decoration: none !important;
|
||||
border: 1px solid rgba(255,255,255,.14);
|
||||
box-shadow: 0 12px 28px rgba(0,0,0,.22);
|
||||
transition: background-color .15s ease, transform .15s ease, box-shadow .15s ease;
|
||||
}
|
||||
.hd-helpdesk-surface .hd-btn:hover,
|
||||
.hd-btn:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 16px 34px rgba(0,0,0,.30);
|
||||
}
|
||||
.hd-helpdesk-surface .hd-btn-sm,
|
||||
.hd-btn-sm {
|
||||
border-radius: .65rem;
|
||||
padding: .42rem .78rem;
|
||||
font-size: .8125rem;
|
||||
}
|
||||
.hd-helpdesk-surface .hd-btn-blue,
|
||||
.hd-btn-blue { background: #2563eb !important; }
|
||||
.hd-helpdesk-surface .hd-btn-blue:hover,
|
||||
.hd-btn-blue:hover { background: #1d4ed8 !important; }
|
||||
.hd-helpdesk-surface .hd-btn-green,
|
||||
.hd-btn-green { background: #16a34a !important; }
|
||||
.hd-helpdesk-surface .hd-btn-green:hover,
|
||||
.hd-btn-green:hover { background: #15803d !important; }
|
||||
.hd-helpdesk-surface .hd-btn-amber,
|
||||
.hd-btn-amber { background: #f59e0b !important; }
|
||||
.hd-helpdesk-surface .hd-btn-amber:hover,
|
||||
.hd-btn-amber:hover { background: #d97706 !important; }
|
||||
.hd-helpdesk-surface .hd-btn-red,
|
||||
.hd-btn-red { background: #dc2626 !important; }
|
||||
.hd-helpdesk-surface .hd-btn-red:hover,
|
||||
.hd-btn-red:hover { background: #b91c1c !important; }
|
||||
.hd-helpdesk-surface .hd-btn-violet,
|
||||
.hd-btn-violet { background: #7c3aed !important; }
|
||||
.hd-helpdesk-surface .hd-btn-violet:hover,
|
||||
.hd-btn-violet:hover { background: #6d28d9 !important; }
|
||||
|
||||
.hd-helpdesk-surface button[type="submit"],
|
||||
.hd-helpdesk-surface button[data-flux-button][type="submit"] {
|
||||
background: #16a34a !important;
|
||||
color: #ffffff !important;
|
||||
border-color: rgba(255,255,255,.14) !important;
|
||||
}
|
||||
.hd-helpdesk-surface button[wire\:click^="reset"],
|
||||
.hd-helpdesk-surface button[wire\:target^="reset"] {
|
||||
background: #f59e0b !important;
|
||||
color: #ffffff !important;
|
||||
border-color: rgba(255,255,255,.14) !important;
|
||||
}
|
||||
.hd-helpdesk-surface button[wire\:click^="edit"],
|
||||
.hd-helpdesk-surface button[wire\:target^="edit"] {
|
||||
background: #2563eb !important;
|
||||
color: #ffffff !important;
|
||||
border-color: rgba(255,255,255,.14) !important;
|
||||
}
|
||||
.hd-helpdesk-surface button[wire\:click^="delete"],
|
||||
.hd-helpdesk-surface button[wire\:target^="delete"] {
|
||||
background: #dc2626 !important;
|
||||
color: #ffffff !important;
|
||||
border-color: rgba(255,255,255,.14) !important;
|
||||
}
|
||||
|
||||
.hd-select {
|
||||
width: 100%;
|
||||
min-height: 3.35rem;
|
||||
border-radius: 1rem;
|
||||
border: 1px solid #3f3f46 !important;
|
||||
background: #09090b !important;
|
||||
color: #f4f4f5 !important;
|
||||
padding: .78rem 3rem .78rem 1rem;
|
||||
font-weight: 800;
|
||||
outline: none;
|
||||
box-shadow: inset 0 1px 0 rgba(255,255,255,.05), 0 12px 26px rgba(0,0,0,.24);
|
||||
}
|
||||
.hd-select:focus {
|
||||
border-color: #60a5fa !important;
|
||||
box-shadow: 0 0 0 3px rgba(96,165,250,.38), inset 0 1px 0 rgba(255,255,255,.05);
|
||||
}
|
||||
|
||||
.helpdesk-ticket-form,
|
||||
.helpdesk-ticket-form * {
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
.helpdesk-ticket-form input,
|
||||
.helpdesk-ticket-form textarea,
|
||||
.helpdesk-ticket-form select,
|
||||
.helpdesk-ticket-form .select2-container--default .select2-selection--single,
|
||||
.helpdesk-ticket-form .select2-container--default .select2-selection--multiple,
|
||||
.helpdesk-ticket-form .select2-container--default .select2-selection__rendered,
|
||||
.helpdesk-ticket-form .select2-container--default .select2-selection__choice {
|
||||
background: #09090b !important;
|
||||
background-color: #09090b !important;
|
||||
color: #f4f4f5 !important;
|
||||
border-color: #3f3f46 !important;
|
||||
caret-color: #f4f4f5 !important;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
.helpdesk-ticket-form input:disabled,
|
||||
.helpdesk-ticket-form textarea:disabled,
|
||||
.helpdesk-ticket-form select:disabled,
|
||||
.helpdesk-ticket-form .select2-container--disabled .select2-selection--single,
|
||||
.helpdesk-ticket-form .select2-container--disabled .select2-selection--multiple,
|
||||
.helpdesk-ticket-form [disabled],
|
||||
.helpdesk-ticket-form [aria-disabled="true"] {
|
||||
background: #09090b !important;
|
||||
background-color: #09090b !important;
|
||||
color: #a1a1aa !important;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
.helpdesk-ticket-form input:-webkit-autofill,
|
||||
.helpdesk-ticket-form input:-webkit-autofill:hover,
|
||||
.helpdesk-ticket-form input:-webkit-autofill:focus,
|
||||
.helpdesk-ticket-form textarea:-webkit-autofill,
|
||||
.helpdesk-ticket-form textarea:-webkit-autofill:hover,
|
||||
.helpdesk-ticket-form textarea:-webkit-autofill:focus,
|
||||
.helpdesk-ticket-form select:-webkit-autofill,
|
||||
.helpdesk-ticket-form select:-webkit-autofill:hover,
|
||||
.helpdesk-ticket-form select:-webkit-autofill:focus {
|
||||
-webkit-text-fill-color: #f4f4f5 !important;
|
||||
box-shadow: 0 0 0 1000px #09090b inset !important;
|
||||
transition: background-color 9999s ease-in-out 0s;
|
||||
}
|
||||
|
||||
.hd-helpdesk-surface .hd-btn,
|
||||
.hd-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: .5rem;
|
||||
border-radius: .85rem;
|
||||
padding: .62rem 1.05rem;
|
||||
font-size: .875rem;
|
||||
line-height: 1.25rem;
|
||||
font-weight: 800;
|
||||
color: #fff !important;
|
||||
text-decoration: none !important;
|
||||
border: 1px solid rgba(255,255,255,.14);
|
||||
box-shadow: 0 12px 28px rgba(0,0,0,.22);
|
||||
transition: background-color .15s ease, transform .15s ease, box-shadow .15s ease;
|
||||
}
|
||||
|
||||
.hd-helpdesk-surface .hd-btn:hover,
|
||||
.hd-btn:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 16px 34px rgba(0,0,0,.30);
|
||||
}
|
||||
|
||||
.hd-btn-sm {
|
||||
border-radius: .65rem;
|
||||
padding: .42rem .78rem;
|
||||
font-size: .8125rem;
|
||||
}
|
||||
|
||||
.hd-btn-blue { background: #2563eb !important; }
|
||||
.hd-btn-blue:hover { background: #1d4ed8 !important; }
|
||||
|
||||
.hd-btn-green { background: #16a34a !important; }
|
||||
.hd-btn-green:hover { background: #15803d !important; }
|
||||
|
||||
.hd-btn-amber { background: #f59e0b !important; }
|
||||
.hd-btn-amber:hover { background: #d97706 !important; }
|
||||
|
||||
.hd-btn-red { background: #dc2626 !important; }
|
||||
.hd-btn-red:hover { background: #b91c1c !important; }
|
||||
|
||||
.hd-btn-violet { background: #7c3aed !important; }
|
||||
.hd-btn-violet:hover { background: #6d28d9 !important; }
|
||||
|
||||
.hd-helpdesk-surface button[type="submit"],
|
||||
.hd-helpdesk-surface button[data-flux-button][type="submit"] {
|
||||
background: #16a34a !important;
|
||||
color: #fff !important;
|
||||
border-color: rgba(255,255,255,.14) !important;
|
||||
}
|
||||
|
||||
.hd-helpdesk-surface button[wire\:click^="reset"],
|
||||
.hd-helpdesk-surface button[wire\:target^="reset"] {
|
||||
background: #f59e0b !important;
|
||||
color: #fff !important;
|
||||
border-color: rgba(255,255,255,.14) !important;
|
||||
}
|
||||
|
||||
.hd-helpdesk-surface button[wire\:click^="edit"],
|
||||
.hd-helpdesk-surface button[wire\:target^="edit"] {
|
||||
background: #2563eb !important;
|
||||
color: #fff !important;
|
||||
border-color: rgba(255,255,255,.14) !important;
|
||||
}
|
||||
|
||||
.hd-helpdesk-surface button[wire\:click^="delete"],
|
||||
.hd-helpdesk-surface button[wire\:target^="delete"] {
|
||||
background: #dc2626 !important;
|
||||
color: #fff !important;
|
||||
border-color: rgba(255,255,255,.14) !important;
|
||||
}
|
||||
|
||||
.hd-select {
|
||||
width: 100%;
|
||||
min-height: 3.35rem;
|
||||
border-radius: 1rem;
|
||||
border: 1px solid #3f3f46 !important;
|
||||
background: #09090b !important;
|
||||
color: #f4f4f5 !important;
|
||||
padding: .78rem 3rem .78rem 1rem;
|
||||
font-weight: 800;
|
||||
outline: none;
|
||||
box-shadow: inset 0 1px 0 rgba(255,255,255,.05), 0 12px 26px rgba(0,0,0,.24);
|
||||
}
|
||||
|
||||
.hd-select:focus {
|
||||
border-color: #60a5fa !important;
|
||||
box-shadow:
|
||||
0 0 0 3px rgba(96,165,250,.38),
|
||||
inset 0 1px 0 rgba(255,255,255,.05);
|
||||
}
|
||||
|
||||
.helpdesk-required-star {
|
||||
color: #ef4444 !important;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.helpdesk-ticket-form,
|
||||
.helpdesk-ticket-form * {
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
.helpdesk-ticket-form input,
|
||||
.helpdesk-ticket-form textarea,
|
||||
.helpdesk-ticket-form select,
|
||||
.helpdesk-ticket-form .select2-container--default .select2-selection--single,
|
||||
.helpdesk-ticket-form .select2-container--default .select2-selection--multiple,
|
||||
.helpdesk-ticket-form .select2-container--default .select2-selection__rendered,
|
||||
.helpdesk-ticket-form .select2-container--default .select2-selection__choice {
|
||||
background: #09090b !important;
|
||||
color: #f4f4f5 !important;
|
||||
border-color: #3f3f46 !important;
|
||||
caret-color: #f4f4f5 !important;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
.helpdesk-ticket-form input:disabled,
|
||||
.helpdesk-ticket-form textarea:disabled,
|
||||
.helpdesk-ticket-form select:disabled,
|
||||
.helpdesk-ticket-form .select2-container--disabled .select2-selection--single,
|
||||
.helpdesk-ticket-form .select2-container--disabled .select2-selection--multiple {
|
||||
background: #09090b !important;
|
||||
color: #a1a1aa !important;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
.helpdesk-ticket-form input:-webkit-autofill,
|
||||
.helpdesk-ticket-form input:-webkit-autofill:hover,
|
||||
.helpdesk-ticket-form input:-webkit-autofill:focus,
|
||||
.helpdesk-ticket-form textarea:-webkit-autofill,
|
||||
.helpdesk-ticket-form textarea:-webkit-autofill:hover,
|
||||
.helpdesk-ticket-form textarea:-webkit-autofill:focus,
|
||||
.helpdesk-ticket-form select:-webkit-autofill,
|
||||
.helpdesk-ticket-form select:-webkit-autofill:hover,
|
||||
.helpdesk-ticket-form select:-webkit-autofill:focus {
|
||||
-webkit-text-fill-color: #f4f4f5 !important;
|
||||
box-shadow: 0 0 0 1000px #09090b inset !important;
|
||||
transition: background-color 9999s ease-in-out 0s;
|
||||
}
|
||||
|
||||
.select2-container--default .select2-selection--single {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.select2-container--default .select2-selection--single .select2-selection__clear {
|
||||
position: absolute !important;
|
||||
top: 50% !important;
|
||||
right: 2.25rem !important;
|
||||
transform: translateY(-50%);
|
||||
color: #f87171 !important;
|
||||
font-weight: 800 !important;
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
/* Select2 base */
|
||||
.helpdesk-ticket-form .select2-container {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
.helpdesk-ticket-form .select2-container--default .select2-selection--single,
|
||||
.helpdesk-ticket-form .select2-container--default .select2-selection--multiple {
|
||||
min-height: 3.35rem;
|
||||
border-radius: 1rem;
|
||||
border: 1px solid #d4d4d8 !important;
|
||||
background: #ffffff !important;
|
||||
color: #18181b !important;
|
||||
box-shadow: 0 10px 22px rgba(24, 24, 27, .08);
|
||||
}
|
||||
|
||||
.helpdesk-ticket-form .select2-container--default .select2-selection--single .select2-selection__rendered {
|
||||
color: #18181b !important;
|
||||
line-height: 3.25rem;
|
||||
padding-left: 1rem;
|
||||
padding-right: 2.75rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.helpdesk-ticket-form .select2-container--default .select2-selection--multiple {
|
||||
align-items: center;
|
||||
padding: .4rem .65rem;
|
||||
}
|
||||
|
||||
.helpdesk-ticket-form .select2-container--default .select2-selection--multiple .select2-selection__choice {
|
||||
border: 1px solid #2563eb !important;
|
||||
border-radius: 9999px !important;
|
||||
background: #dbeafe !important;
|
||||
color: #1e3a8a !important;
|
||||
font-weight: 700;
|
||||
padding: .2rem .55rem;
|
||||
}
|
||||
|
||||
.helpdesk-ticket-form .select2-container--default .select2-selection--multiple .select2-selection__choice__remove {
|
||||
color: #dc2626 !important;
|
||||
margin-right: .35rem;
|
||||
}
|
||||
|
||||
.select2-dropdown {
|
||||
border: 1px solid #d4d4d8 !important;
|
||||
border-radius: .9rem !important;
|
||||
background: #ffffff !important;
|
||||
color: #18181b !important;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 18px 42px rgba(24, 24, 27, .18);
|
||||
}
|
||||
|
||||
.select2-search__field {
|
||||
border: 1px solid #d4d4d8 !important;
|
||||
border-radius: .7rem !important;
|
||||
background: #ffffff !important;
|
||||
color: #18181b !important;
|
||||
}
|
||||
|
||||
.select2-container--default .select2-results__option {
|
||||
color: #18181b !important;
|
||||
}
|
||||
|
||||
.select2-container--default .select2-results__option--highlighted[aria-selected] {
|
||||
background: #2563eb !important;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
.select2-container--default .select2-results__option[aria-selected="true"] {
|
||||
background: #e0f2fe !important;
|
||||
color: #075985 !important;
|
||||
}
|
||||
|
||||
/* Dark mode */
|
||||
.dark .helpdesk-ticket-form .select2-container--default .select2-selection--single,
|
||||
.dark .helpdesk-ticket-form .select2-container--default .select2-selection--multiple {
|
||||
border-color: #3f3f46 !important;
|
||||
background: #09090b !important;
|
||||
color: #f4f4f5 !important;
|
||||
box-shadow: inset 0 1px 0 rgba(255,255,255,.05), 0 12px 26px rgba(0,0,0,.24);
|
||||
}
|
||||
|
||||
.dark .helpdesk-ticket-form .select2-container--default .select2-selection--single .select2-selection__rendered {
|
||||
color: #f4f4f5 !important;
|
||||
}
|
||||
|
||||
.dark .helpdesk-ticket-form .select2-container--default .select2-selection--multiple .select2-selection__choice {
|
||||
border-color: #60a5fa !important;
|
||||
background: #1d4ed8 !important;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
.dark .select2-dropdown {
|
||||
border-color: #3f3f46 !important;
|
||||
background: #18181b !important;
|
||||
color: #f4f4f5 !important;
|
||||
box-shadow: 0 18px 42px rgba(0,0,0,.55);
|
||||
}
|
||||
|
||||
.dark .select2-search__field {
|
||||
border-color: #3f3f46 !important;
|
||||
background: #09090b !important;
|
||||
color: #f4f4f5 !important;
|
||||
}
|
||||
|
||||
.dark .select2-container--default .select2-results__option {
|
||||
color: #f4f4f5 !important;
|
||||
}
|
||||
|
||||
.dark .select2-container--default .select2-results__option--highlighted[aria-selected] {
|
||||
background: #2563eb !important;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
.dark .select2-container--default .select2-results__option[aria-selected="true"] {
|
||||
background: #27272a !important;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
.select2-container--default .select2-results__option {
|
||||
background: rgb(9 9 11);
|
||||
color: rgb(244 244 245);
|
||||
}
|
||||
|
||||
.select2-container--default .select2-results__option--highlighted[aria-selected],
|
||||
.select2-container--default .select2-results__option--highlighted.select2-results__option--selectable {
|
||||
background: rgb(37 99 235) !important;
|
||||
color: rgb(255 255 255) !important;
|
||||
}
|
||||
|
||||
.select2-container--default .select2-results__option[aria-selected="true"],
|
||||
.select2-container--default .select2-results__option--selected {
|
||||
background: rgb(30 64 175) !important;
|
||||
color: rgb(255 255 255) !important;
|
||||
}
|
||||
|
||||
.select2-dropdown {
|
||||
border-color: rgb(63 63 70) !important;
|
||||
background: rgb(9 9 11) !important;
|
||||
}
|
||||
|
||||
.select2-container--default .select2-selection--single {
|
||||
border: 1px solid rgb(63 63 70) !important;
|
||||
background: rgb(9 9 11) !important;
|
||||
}
|
||||
|
||||
.select2-container--default .select2-selection--single .select2-selection__rendered {
|
||||
color: rgb(244 244 245) !important;
|
||||
}
|
||||
|
||||
.select2-container--default .select2-selection--single .select2-selection__placeholder {
|
||||
color: rgb(113 113 122) !important;
|
||||
}
|
||||
|
||||
.select2-container--default .select2-selection--single .select2-selection__arrow b {
|
||||
border-color: rgb(161 161 170) transparent transparent transparent !important;
|
||||
}
|
||||
|
||||
.select2-container--default.select2-container--open .select2-selection--single {
|
||||
border-color: rgb(59 130 246) !important;
|
||||
box-shadow: 0 0 0 2px rgb(59 130 246 / 0.35);
|
||||
}
|
||||
|
||||
.select2-container--default .select2-selection--multiple {
|
||||
border: 1px solid rgb(63 63 70) !important;
|
||||
background: rgb(9 9 11) !important;
|
||||
}
|
||||
|
||||
.select2-container--default .select2-selection--multiple .select2-selection__choice {
|
||||
border: 1px solid rgb(59 130 246 / 0.45) !important;
|
||||
background: rgb(30 64 175 / 0.35) !important;
|
||||
color: rgb(219 234 254) !important;
|
||||
}
|
||||
|
||||
.select2-container--default .select2-selection--multiple .select2-selection__choice__remove {
|
||||
color: rgb(191 219 254) !important;
|
||||
}
|
||||
|
||||
.select2-container--default .select2-selection--single,
|
||||
.select2-container--default .select2-selection--multiple {
|
||||
border-radius: 0.75rem !important;
|
||||
}
|
||||
|
||||
.select2-dropdown {
|
||||
overflow: hidden;
|
||||
border-radius: 0.75rem !important;
|
||||
}
|
||||
|
||||
.select2-container--open .select2-dropdown--below {
|
||||
margin-top: 0.35rem;
|
||||
border-radius: 0.75rem !important;
|
||||
}
|
||||
|
||||
.select2-container--open .select2-dropdown--above {
|
||||
margin-bottom: 0.35rem;
|
||||
border-radius: 0.75rem !important;
|
||||
}
|
||||
|
||||
.select2-container--default .select2-selection--single {
|
||||
position: relative;
|
||||
padding-right: 3.5rem;
|
||||
}
|
||||
|
||||
.select2-container--default .select2-selection--single .select2-selection__rendered {
|
||||
padding-right: 3.5rem;
|
||||
}
|
||||
|
||||
.select2-container--default .select2-selection--single .select2-selection__clear {
|
||||
position: absolute;
|
||||
right: 2rem;
|
||||
top: 50%;
|
||||
z-index: 3;
|
||||
margin: 0;
|
||||
transform: translateY(-50%);
|
||||
color: rgb(161 161 170);
|
||||
font-size: 1rem;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.select2-container--default .select2-selection--single .select2-selection__clear:hover {
|
||||
color: rgb(248 113 113);
|
||||
}
|
||||
|
||||
.select2-container--default .select2-selection--single .select2-selection__arrow {
|
||||
right: 0.75rem;
|
||||
}
|
||||
|
||||
.select2-container--default .select2-selection--single {
|
||||
position: relative !important;
|
||||
}
|
||||
|
||||
.select2-container--default .select2-selection--single .select2-selection__rendered {
|
||||
padding-left: 0.75rem !important;
|
||||
padding-right: 4.25rem !important;
|
||||
}
|
||||
|
||||
.select2-container--default .select2-selection--single .select2-selection__clear {
|
||||
position: absolute !important;
|
||||
right: 2.35rem !important;
|
||||
top: 50% !important;
|
||||
float: none !important;
|
||||
margin: 0 !important;
|
||||
transform: translateY(-50%) !important;
|
||||
}
|
||||
|
||||
.select2-container--default .select2-selection--single .select2-selection__clear span {
|
||||
color: rgb(161 161 170) !important;
|
||||
font-size: 1.1rem !important;
|
||||
line-height: 1 !important;
|
||||
}
|
||||
|
||||
.select2-container--default .select2-selection--single .select2-selection__arrow {
|
||||
right: 0.75rem !important;
|
||||
}
|
||||
|
||||
.select2-container--default .select2-selection--single {
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
position: relative !important;
|
||||
}
|
||||
|
||||
.select2-container--default .select2-selection--single .select2-selection__rendered {
|
||||
flex: 1 1 auto !important;
|
||||
width: 100% !important;
|
||||
padding-right: 4.5rem !important;
|
||||
}
|
||||
|
||||
.select2-container--default .select2-selection--single .select2-selection__clear {
|
||||
position: absolute !important;
|
||||
right: 2.25rem !important;
|
||||
top: 50% !important;
|
||||
z-index: 20 !important;
|
||||
display: inline-flex !important;
|
||||
align-items: center !important;
|
||||
justify-content: center !important;
|
||||
width: 1.25rem !important;
|
||||
height: 1.25rem !important;
|
||||
float: none !important;
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
transform: translateY(-50%) !important;
|
||||
color: rgb(161 161 170) !important;
|
||||
cursor: pointer !important;
|
||||
}
|
||||
|
||||
.select2-container--default .select2-selection--single .select2-selection__clear:hover {
|
||||
color: rgb(248 113 113) !important;
|
||||
}
|
||||
|
||||
.select2-container--default .select2-selection--single .select2-selection__arrow {
|
||||
position: absolute !important;
|
||||
right: 0.75rem !important;
|
||||
top: 50% !important;
|
||||
transform: translateY(-50%) !important;
|
||||
}
|
||||
|
||||
/* Helpdesk Flatpickr date picker */
|
||||
.flatpickr-calendar {
|
||||
border: 1px solid rgb(63 63 70);
|
||||
background: rgb(24 24 27);
|
||||
color: rgb(244 244 245);
|
||||
box-shadow: 0 20px 45px rgb(0 0 0 / 0.35);
|
||||
}
|
||||
|
||||
.flatpickr-calendar.arrowTop::before,
|
||||
.flatpickr-calendar.arrowTop::after,
|
||||
.flatpickr-calendar.arrowBottom::before,
|
||||
.flatpickr-calendar.arrowBottom::after {
|
||||
border-bottom-color: rgb(24 24 27);
|
||||
border-top-color: rgb(24 24 27);
|
||||
}
|
||||
|
||||
.flatpickr-months,
|
||||
.flatpickr-months .flatpickr-month,
|
||||
.flatpickr-current-month .flatpickr-monthDropdown-months,
|
||||
.flatpickr-current-month input.cur-year {
|
||||
background: rgb(24 24 27);
|
||||
color: rgb(244 244 245);
|
||||
}
|
||||
|
||||
.flatpickr-weekday,
|
||||
span.flatpickr-weekday {
|
||||
color: rgb(161 161 170);
|
||||
}
|
||||
|
||||
.flatpickr-day {
|
||||
color: rgb(228 228 231);
|
||||
}
|
||||
|
||||
.flatpickr-day:hover,
|
||||
.flatpickr-day:focus {
|
||||
border-color: rgb(59 130 246);
|
||||
background: rgb(39 39 42);
|
||||
color: rgb(255 255 255);
|
||||
}
|
||||
|
||||
.flatpickr-day.selected,
|
||||
.flatpickr-day.startRange,
|
||||
.flatpickr-day.endRange {
|
||||
border-color: rgb(37 99 235);
|
||||
background: rgb(37 99 235);
|
||||
color: rgb(255 255 255);
|
||||
}
|
||||
|
||||
.flatpickr-day.today {
|
||||
border-color: rgb(34 197 94);
|
||||
}
|
||||
|
||||
.flatpickr-day.flatpickr-disabled,
|
||||
.flatpickr-day.prevMonthDay,
|
||||
.flatpickr-day.nextMonthDay {
|
||||
color: rgb(113 113 122);
|
||||
}
|
||||
.hd-helpdesk-surface .hd-btn,
|
||||
.hd-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: .5rem;
|
||||
border-radius: .85rem;
|
||||
padding: .62rem 1.05rem;
|
||||
font-size: .875rem;
|
||||
line-height: 1.25rem;
|
||||
font-weight: 800;
|
||||
font-size: .95rem;
|
||||
color: #ffffff !important;
|
||||
text-decoration: none !important;
|
||||
border: 1px solid rgba(255,255,255,.14);
|
||||
box-shadow: 0 12px 28px rgba(0,0,0,.22);
|
||||
transition: background-color .15s ease, transform .15s ease, box-shadow .15s ease;
|
||||
}
|
||||
.hd-helpdesk-surface .hd-btn:hover,
|
||||
.hd-btn:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 16px 34px rgba(0,0,0,.30);
|
||||
}
|
||||
.hd-helpdesk-surface .hd-btn-sm,
|
||||
.hd-btn-sm {
|
||||
border-radius: .65rem;
|
||||
padding: .42rem .78rem;
|
||||
font-size: .8125rem;
|
||||
}
|
||||
.hd-helpdesk-surface .hd-btn-blue,
|
||||
.hd-btn-blue { background: #2563eb !important; }
|
||||
.hd-helpdesk-surface .hd-btn-blue:hover,
|
||||
.hd-btn-blue:hover { background: #1d4ed8 !important; }
|
||||
.hd-helpdesk-surface .hd-btn-green,
|
||||
.hd-btn-green { background: #16a34a !important; }
|
||||
.hd-helpdesk-surface .hd-btn-green:hover,
|
||||
.hd-btn-green:hover { background: #15803d !important; }
|
||||
.hd-helpdesk-surface .hd-btn-amber,
|
||||
.hd-btn-amber { background: #f59e0b !important; }
|
||||
.hd-helpdesk-surface .hd-btn-amber:hover,
|
||||
.hd-btn-amber:hover { background: #d97706 !important; }
|
||||
.hd-helpdesk-surface .hd-btn-red,
|
||||
.hd-btn-red { background: #dc2626 !important; }
|
||||
.hd-helpdesk-surface .hd-btn-red:hover,
|
||||
.hd-btn-red:hover { background: #b91c1c !important; }
|
||||
.hd-helpdesk-surface .hd-btn-violet,
|
||||
.hd-btn-violet { background: #7c3aed !important; }
|
||||
.hd-helpdesk-surface .hd-btn-violet:hover,
|
||||
.hd-btn-violet:hover { background: #6d28d9 !important; }
|
||||
|
||||
.hd-helpdesk-surface button[type="submit"],
|
||||
.hd-helpdesk-surface button[data-flux-button][type="submit"] {
|
||||
background: #16a34a !important;
|
||||
color: #ffffff !important;
|
||||
border-color: rgba(255,255,255,.14) !important;
|
||||
}
|
||||
.hd-helpdesk-surface button[wire\:click^="reset"],
|
||||
.hd-helpdesk-surface button[wire\:target^="reset"] {
|
||||
background: #f59e0b !important;
|
||||
color: #ffffff !important;
|
||||
border-color: rgba(255,255,255,.14) !important;
|
||||
}
|
||||
.hd-helpdesk-surface button[wire\:click^="edit"],
|
||||
.hd-helpdesk-surface button[wire\:target^="edit"] {
|
||||
background: #2563eb !important;
|
||||
color: #ffffff !important;
|
||||
border-color: rgba(255,255,255,.14) !important;
|
||||
}
|
||||
.hd-helpdesk-surface button[wire\:click^="delete"],
|
||||
.hd-helpdesk-surface button[wire\:target^="delete"] {
|
||||
background: #dc2626 !important;
|
||||
color: #ffffff !important;
|
||||
border-color: rgba(255,255,255,.14) !important;
|
||||
}
|
||||
|
||||
.hd-select-wrap {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
max-width: 18rem;
|
||||
}
|
||||
.hd-select-wrap::after {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: 1rem;
|
||||
width: .55rem;
|
||||
height: .55rem;
|
||||
content: "";
|
||||
pointer-events: none;
|
||||
border-right: 2px solid #93c5fd;
|
||||
border-bottom: 2px solid #93c5fd;
|
||||
transform: translateY(-65%) rotate(45deg);
|
||||
}
|
||||
.hd-select {
|
||||
width: 100%;
|
||||
min-height: 2.75rem;
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
border-radius: .9rem;
|
||||
border: 1px solid rgba(96, 165, 250, .38) !important;
|
||||
background: linear-gradient(135deg, #030712 0%, #111827 54%, #0f172a 100%) !important;
|
||||
color: #f8fafc !important;
|
||||
padding: .62rem 2.45rem .62rem .9rem;
|
||||
font-weight: 800;
|
||||
outline: none;
|
||||
box-shadow: inset 0 1px 0 rgba(255,255,255,.08), 0 14px 30px rgba(2,6,23,.38);
|
||||
}
|
||||
.hd-select option {
|
||||
background: #020617 !important;
|
||||
color: #f8fafc !important;
|
||||
}
|
||||
.hd-select:focus {
|
||||
border-color: #60a5fa !important;
|
||||
box-shadow: 0 0 0 3px rgba(96,165,250,.38), inset 0 1px 0 rgba(255,255,255,.08), 0 14px 30px rgba(2,6,23,.38);
|
||||
}
|
||||
.hd-ticket-board-select-wrap {
|
||||
position: relative;
|
||||
max-width: 12.5rem;
|
||||
}
|
||||
.hd-ticket-board-select-wrap::after {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: .7rem;
|
||||
width: .45rem;
|
||||
height: .45rem;
|
||||
content: "";
|
||||
pointer-events: none;
|
||||
border-right: 2px solid #86efac;
|
||||
border-bottom: 2px solid #86efac;
|
||||
transform: translateY(-65%) rotate(45deg);
|
||||
}
|
||||
.hd-ticket-board-select {
|
||||
width: 100%;
|
||||
min-height: 2.35rem;
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
border-radius: .85rem;
|
||||
border: 1px solid rgba(34,197,94,.35) !important;
|
||||
background: linear-gradient(135deg, #052e16 0%, #064e3b 100%) !important;
|
||||
color: #ecfdf5 !important;
|
||||
padding: .48rem 2rem .48rem .75rem;
|
||||
font-size: .75rem;
|
||||
font-weight: 800;
|
||||
outline: none;
|
||||
box-shadow: inset 0 1px 0 rgba(255,255,255,.08), 0 10px 22px rgba(0,0,0,.26);
|
||||
}
|
||||
.hd-ticket-board-select option {
|
||||
background: #052e16 !important;
|
||||
color: #ecfdf5 !important;
|
||||
}
|
||||
.hd-ticket-board-select:focus {
|
||||
border-color: #86efac !important;
|
||||
box-shadow: 0 0 0 3px rgba(34,197,94,.28), inset 0 1px 0 rgba(255,255,255,.08);
|
||||
}
|
||||
|
||||
.helpdesk-ticket-form,
|
||||
.helpdesk-ticket-form * {
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
.helpdesk-ticket-form input,
|
||||
.helpdesk-ticket-form textarea,
|
||||
.helpdesk-ticket-form select,
|
||||
.helpdesk-ticket-form .select2-container--default .select2-selection--single,
|
||||
.helpdesk-ticket-form .select2-container--default .select2-selection--multiple,
|
||||
.helpdesk-ticket-form .select2-container--default .select2-selection__rendered,
|
||||
.helpdesk-ticket-form .select2-container--default .select2-selection__choice {
|
||||
background: #09090b !important;
|
||||
background-color: #09090b !important;
|
||||
color: #f4f4f5 !important;
|
||||
border-color: #3f3f46 !important;
|
||||
caret-color: #f4f4f5 !important;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
.helpdesk-ticket-form input:disabled,
|
||||
.helpdesk-ticket-form textarea:disabled,
|
||||
.helpdesk-ticket-form select:disabled,
|
||||
.helpdesk-ticket-form .select2-container--disabled .select2-selection--single,
|
||||
.helpdesk-ticket-form .select2-container--disabled .select2-selection--multiple,
|
||||
.helpdesk-ticket-form [disabled],
|
||||
.helpdesk-ticket-form [aria-disabled="true"] {
|
||||
background: #09090b !important;
|
||||
background-color: #09090b !important;
|
||||
color: #a1a1aa !important;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
.helpdesk-ticket-form input:-webkit-autofill,
|
||||
.helpdesk-ticket-form input:-webkit-autofill:hover,
|
||||
.helpdesk-ticket-form input:-webkit-autofill:focus,
|
||||
.helpdesk-ticket-form textarea:-webkit-autofill,
|
||||
.helpdesk-ticket-form textarea:-webkit-autofill:hover,
|
||||
.helpdesk-ticket-form textarea:-webkit-autofill:focus,
|
||||
.helpdesk-ticket-form select:-webkit-autofill,
|
||||
.helpdesk-ticket-form select:-webkit-autofill:hover,
|
||||
.helpdesk-ticket-form select:-webkit-autofill:focus {
|
||||
-webkit-text-fill-color: #f4f4f5 !important;
|
||||
box-shadow: 0 0 0 1000px #09090b inset !important;
|
||||
transition: background-color 9999s ease-in-out 0s;
|
||||
}
|
||||
|
||||
.hd-helpdesk-surface .hd-btn,
|
||||
.hd-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: .5rem;
|
||||
border-radius: .85rem;
|
||||
padding: .62rem 1.05rem;
|
||||
font-size: .875rem;
|
||||
line-height: 1.25rem;
|
||||
font-weight: 800;
|
||||
color: #ffffff !important;
|
||||
text-decoration: none !important;
|
||||
border: 1px solid rgba(255,255,255,.14);
|
||||
box-shadow: 0 12px 28px rgba(0,0,0,.22);
|
||||
transition: background-color .15s ease, transform .15s ease, box-shadow .15s ease;
|
||||
}
|
||||
.hd-helpdesk-surface .hd-btn:hover,
|
||||
.hd-btn:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 16px 34px rgba(0,0,0,.30);
|
||||
}
|
||||
.hd-helpdesk-surface .hd-btn-sm,
|
||||
.hd-btn-sm {
|
||||
border-radius: .65rem;
|
||||
padding: .42rem .78rem;
|
||||
font-size: .8125rem;
|
||||
}
|
||||
.hd-helpdesk-surface .hd-btn-blue,
|
||||
.hd-btn-blue { background: #2563eb !important; }
|
||||
.hd-helpdesk-surface .hd-btn-blue:hover,
|
||||
.hd-btn-blue:hover { background: #1d4ed8 !important; }
|
||||
.hd-helpdesk-surface .hd-btn-green,
|
||||
.hd-btn-green { background: #16a34a !important; }
|
||||
.hd-helpdesk-surface .hd-btn-green:hover,
|
||||
.hd-btn-green:hover { background: #15803d !important; }
|
||||
.hd-helpdesk-surface .hd-btn-amber,
|
||||
.hd-btn-amber { background: #f59e0b !important; }
|
||||
.hd-helpdesk-surface .hd-btn-amber:hover,
|
||||
.hd-btn-amber:hover { background: #d97706 !important; }
|
||||
.hd-helpdesk-surface .hd-btn-red,
|
||||
.hd-btn-red { background: #dc2626 !important; }
|
||||
.hd-helpdesk-surface .hd-btn-red:hover,
|
||||
.hd-btn-red:hover { background: #b91c1c !important; }
|
||||
.hd-helpdesk-surface .hd-btn-violet,
|
||||
.hd-btn-violet { background: #7c3aed !important; }
|
||||
.hd-helpdesk-surface .hd-btn-violet:hover,
|
||||
.hd-btn-violet:hover { background: #6d28d9 !important; }
|
||||
|
||||
.hd-helpdesk-surface button[type="submit"],
|
||||
.hd-helpdesk-surface button[data-flux-button][type="submit"] {
|
||||
background: #16a34a !important;
|
||||
color: #ffffff !important;
|
||||
border-color: rgba(255,255,255,.14) !important;
|
||||
}
|
||||
.hd-helpdesk-surface button[wire\:click^="reset"],
|
||||
.hd-helpdesk-surface button[wire\:target^="reset"] {
|
||||
background: #f59e0b !important;
|
||||
color: #ffffff !important;
|
||||
border-color: rgba(255,255,255,.14) !important;
|
||||
}
|
||||
.hd-helpdesk-surface button[wire\:click^="edit"],
|
||||
.hd-helpdesk-surface button[wire\:target^="edit"] {
|
||||
background: #2563eb !important;
|
||||
color: #ffffff !important;
|
||||
border-color: rgba(255,255,255,.14) !important;
|
||||
}
|
||||
.hd-helpdesk-surface button[wire\:click^="delete"],
|
||||
.hd-helpdesk-surface button[wire\:target^="delete"] {
|
||||
background: #dc2626 !important;
|
||||
color: #ffffff !important;
|
||||
border-color: rgba(255,255,255,.14) !important;
|
||||
}
|
||||
|
||||
.hd-select {
|
||||
width: 100%;
|
||||
min-height: 3.35rem;
|
||||
border-radius: 1rem;
|
||||
border: 1px solid #3f3f46 !important;
|
||||
background: #09090b !important;
|
||||
color: #f4f4f5 !important;
|
||||
padding: .78rem 3rem .78rem 1rem;
|
||||
font-weight: 800;
|
||||
outline: none;
|
||||
box-shadow: inset 0 1px 0 rgba(255,255,255,.05), 0 12px 26px rgba(0,0,0,.24);
|
||||
}
|
||||
.hd-select:focus {
|
||||
border-color: #60a5fa !important;
|
||||
box-shadow: 0 0 0 3px rgba(96,165,250,.38), inset 0 1px 0 rgba(255,255,255,.05);
|
||||
}
|
||||
|
||||
.helpdesk-ticket-form,
|
||||
.helpdesk-ticket-form * {
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
.helpdesk-ticket-form input,
|
||||
.helpdesk-ticket-form textarea,
|
||||
.helpdesk-ticket-form select,
|
||||
.helpdesk-ticket-form .select2-container--default .select2-selection--single,
|
||||
.helpdesk-ticket-form .select2-container--default .select2-selection--multiple,
|
||||
.helpdesk-ticket-form .select2-container--default .select2-selection__rendered,
|
||||
.helpdesk-ticket-form .select2-container--default .select2-selection__choice {
|
||||
background: #09090b !important;
|
||||
background-color: #09090b !important;
|
||||
color: #f4f4f5 !important;
|
||||
border-color: #3f3f46 !important;
|
||||
caret-color: #f4f4f5 !important;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
.helpdesk-ticket-form input:disabled,
|
||||
.helpdesk-ticket-form textarea:disabled,
|
||||
.helpdesk-ticket-form select:disabled,
|
||||
.helpdesk-ticket-form .select2-container--disabled .select2-selection--single,
|
||||
.helpdesk-ticket-form .select2-container--disabled .select2-selection--multiple,
|
||||
.helpdesk-ticket-form [disabled],
|
||||
.helpdesk-ticket-form [aria-disabled="true"] {
|
||||
background: #09090b !important;
|
||||
background-color: #09090b !important;
|
||||
color: #a1a1aa !important;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
.helpdesk-ticket-form input:-webkit-autofill,
|
||||
.helpdesk-ticket-form input:-webkit-autofill:hover,
|
||||
.helpdesk-ticket-form input:-webkit-autofill:focus,
|
||||
.helpdesk-ticket-form textarea:-webkit-autofill,
|
||||
.helpdesk-ticket-form textarea:-webkit-autofill:hover,
|
||||
.helpdesk-ticket-form textarea:-webkit-autofill:focus,
|
||||
.helpdesk-ticket-form select:-webkit-autofill,
|
||||
.helpdesk-ticket-form select:-webkit-autofill:hover,
|
||||
.helpdesk-ticket-form select:-webkit-autofill:focus {
|
||||
-webkit-text-fill-color: #f4f4f5 !important;
|
||||
box-shadow: 0 0 0 1000px #09090b inset !important;
|
||||
transition: background-color 9999s ease-in-out 0s;
|
||||
}
|
||||
@@ -1,18 +1,3 @@
|
||||
<x-layouts::app :title="__('Dashboard')">
|
||||
<div class="flex h-full w-full flex-1 flex-col gap-4 rounded-xl">
|
||||
<div class="grid auto-rows-min gap-4 md:grid-cols-3">
|
||||
<div class="relative aspect-video overflow-hidden rounded-xl border border-neutral-200 dark:border-neutral-700">
|
||||
<x-placeholder-pattern class="absolute inset-0 size-full stroke-gray-900/20 dark:stroke-neutral-100/20" />
|
||||
</div>
|
||||
<div class="relative aspect-video overflow-hidden rounded-xl border border-neutral-200 dark:border-neutral-700">
|
||||
<x-placeholder-pattern class="absolute inset-0 size-full stroke-gray-900/20 dark:stroke-neutral-100/20" />
|
||||
</div>
|
||||
<div class="relative aspect-video overflow-hidden rounded-xl border border-neutral-200 dark:border-neutral-700">
|
||||
<x-placeholder-pattern class="absolute inset-0 size-full stroke-gray-900/20 dark:stroke-neutral-100/20" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="relative h-full flex-1 overflow-hidden rounded-xl border border-neutral-200 dark:border-neutral-700">
|
||||
<x-placeholder-pattern class="absolute inset-0 size-full stroke-gray-900/20 dark:stroke-neutral-100/20" />
|
||||
</div>
|
||||
</div>
|
||||
<x-layouts::app :title="__('Helpdesk Dashboard')">
|
||||
<livewire:helpdesk.dashboard-widget />
|
||||
</x-layouts::app>
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
<x-layouts::app :title="__('Helpdesk')">
|
||||
<livewire:helpdesk.dashboard-widget />
|
||||
</x-layouts::app>
|
||||
@@ -0,0 +1,124 @@
|
||||
<div class="flex w-full flex-1 flex-col gap-6">
|
||||
<div class="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
|
||||
<div>
|
||||
<flux:heading size="xl">{{ __('Helpdesk') }}</flux:heading>
|
||||
<flux:text class="mt-1">{{ __('Support operations, workload and recent activity.') }}</flux:text>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<flux:button :href="route('helpdesk.tickets.index')" variant="ghost" icon="list-bullet" wire:navigate>
|
||||
{{ __('Tickets') }}
|
||||
</flux:button>
|
||||
<flux:button :href="route('helpdesk.kanban')" variant="ghost" icon="view-columns" wire:navigate>
|
||||
{{ __('Kanban') }}
|
||||
</flux:button>
|
||||
<flux:button :href="route('helpdesk.tickets.create')" variant="primary" icon="plus" wire:navigate>
|
||||
{{ __('New ticket') }}
|
||||
</flux:button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-4 sm:grid-cols-2 xl:grid-cols-4">
|
||||
<div class="rounded-xl border border-zinc-200 bg-white p-5 shadow-sm dark:border-zinc-700 dark:bg-zinc-900">
|
||||
<flux:text>{{ __('Total tickets') }}</flux:text>
|
||||
<div class="mt-2 text-3xl font-semibold tracking-tight text-zinc-950 dark:text-white">{{ $totalCount }}</div>
|
||||
<flux:text class="mt-2">{{ __('All tickets visible to you') }}</flux:text>
|
||||
</div>
|
||||
|
||||
<div class="rounded-xl border border-zinc-200 bg-white p-5 shadow-sm dark:border-zinc-700 dark:bg-zinc-900">
|
||||
<flux:text>{{ __('Open') }}</flux:text>
|
||||
<div class="mt-2 text-3xl font-semibold tracking-tight text-zinc-950 dark:text-white">{{ $openCount }}</div>
|
||||
<flux:text class="mt-2">{{ $openPercent }}% {{ __('of total') }}</flux:text>
|
||||
</div>
|
||||
|
||||
<div class="rounded-xl border border-zinc-200 bg-white p-5 shadow-sm dark:border-zinc-700 dark:bg-zinc-900">
|
||||
<flux:text>{{ __('Closed') }}</flux:text>
|
||||
<div class="mt-2 text-3xl font-semibold tracking-tight text-zinc-950 dark:text-white">{{ $closedCount }}</div>
|
||||
<flux:text class="mt-2">{{ $closedPercent }}% {{ __('resolved') }}</flux:text>
|
||||
</div>
|
||||
|
||||
<div class="rounded-xl border border-zinc-200 bg-white p-5 shadow-sm dark:border-zinc-700 dark:bg-zinc-900">
|
||||
<flux:text>{{ __('High priority') }}</flux:text>
|
||||
<div class="mt-2 text-3xl font-semibold tracking-tight text-zinc-950 dark:text-white">{{ $criticalCount }}</div>
|
||||
<flux:text class="mt-2">{{ __('Critical, urgent or high') }}</flux:text>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-6 xl:grid-cols-3">
|
||||
<section class="xl:col-span-2 rounded-xl border border-zinc-200 bg-white shadow-sm dark:border-zinc-700 dark:bg-zinc-900">
|
||||
<div class="flex items-center justify-between border-b border-zinc-200 px-5 py-4 dark:border-zinc-700">
|
||||
<div>
|
||||
<flux:heading size="lg">{{ __('Recent tickets') }}</flux:heading>
|
||||
<flux:text>{{ __('Latest support activity') }}</flux:text>
|
||||
</div>
|
||||
<flux:button :href="route('helpdesk.tickets.index')" size="sm" variant="ghost" wire:navigate>
|
||||
{{ __('View all') }}
|
||||
</flux:button>
|
||||
</div>
|
||||
|
||||
<div class="divide-y divide-zinc-200 dark:divide-zinc-700">
|
||||
@forelse ($recentTickets as $ticket)
|
||||
<a
|
||||
href="{{ route('helpdesk.tickets.show', $ticket) }}"
|
||||
wire:navigate
|
||||
class="flex items-start justify-between gap-4 px-5 py-4 transition hover:bg-zinc-50 dark:hover:bg-zinc-800/60"
|
||||
>
|
||||
<div class="min-w-0">
|
||||
<div class="truncate font-medium text-zinc-950 dark:text-white">
|
||||
{{ $ticket->reference ?? $ticket->id }} · {{ $ticket->subject ?? __('Untitled ticket') }}
|
||||
</div>
|
||||
<div class="mt-1 flex flex-wrap gap-x-3 gap-y-1 text-sm text-zinc-500 dark:text-zinc-400">
|
||||
@if ($ticket->status)
|
||||
<span>{{ $ticket->status->name }}</span>
|
||||
@endif
|
||||
@if ($ticket->priority)
|
||||
<span>{{ $ticket->priority->name }}</span>
|
||||
@endif
|
||||
@if ($ticket->last_activity_at)
|
||||
<span>{{ $ticket->last_activity_at->diffForHumans() }}</span>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
<flux:icon.chevron-right class="mt-1 size-4 shrink-0 text-zinc-400" />
|
||||
</a>
|
||||
@empty
|
||||
<div class="px-5 py-10 text-center">
|
||||
<flux:text>{{ __('No tickets yet.') }}</flux:text>
|
||||
</div>
|
||||
@endforelse
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="rounded-xl border border-zinc-200 bg-white shadow-sm dark:border-zinc-700 dark:bg-zinc-900">
|
||||
<div class="border-b border-zinc-200 px-5 py-4 dark:border-zinc-700">
|
||||
<flux:heading size="lg">{{ __('Recent Git activity') }}</flux:heading>
|
||||
<flux:text>{{ __('Commits linked to Helpdesk') }}</flux:text>
|
||||
</div>
|
||||
|
||||
<div class="divide-y divide-zinc-200 dark:divide-zinc-700">
|
||||
@forelse ($recentCommits as $commit)
|
||||
<div class="px-5 py-4">
|
||||
<div class="truncate font-medium text-zinc-950 dark:text-white">
|
||||
{{ $commit->repository?->name ?? __('Repository') }}
|
||||
</div>
|
||||
<div class="mt-1 line-clamp-2 text-sm text-zinc-600 dark:text-zinc-300">
|
||||
{{ $commit->message ?? __('Commit') }}
|
||||
</div>
|
||||
<div class="mt-2 flex flex-wrap gap-x-3 gap-y-1 text-xs text-zinc-500 dark:text-zinc-400">
|
||||
@if ($commit->sha)
|
||||
<span>{{ substr($commit->sha, 0, 8) }}</span>
|
||||
@endif
|
||||
@if ($commit->committed_at)
|
||||
<span>{{ $commit->committed_at->diffForHumans() }}</span>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@empty
|
||||
<div class="px-5 py-10 text-center">
|
||||
<flux:text>{{ __('No Git activity yet.') }}</flux:text>
|
||||
</div>
|
||||
@endforelse
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
Reference in New Issue
Block a user