settings updates, fixed
This commit is contained in:
@@ -1,181 +0,0 @@
|
||||
<?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 [];
|
||||
}
|
||||
}
|
||||
@@ -73,7 +73,7 @@ final class KanbanBoard extends Component
|
||||
'last_activity_at' => now(),
|
||||
])->save();
|
||||
|
||||
$freshTicket = $ticket->fresh(['user:id,name,email', 'owner:id,name,email', 'agent:id,name,email', 'status', 'priority', 'category', 'project', 'team', 'product', 'tags', 'checklistItems', 'board']) ?? $ticket;
|
||||
$freshTicket = $ticket->fresh(['user:id,name,email', 'agent:id,name,email', 'status', 'priority', 'category', 'project', 'team', 'product', 'tags', 'checklistItems', 'board']) ?? $ticket;
|
||||
|
||||
$workflow->dispatch(
|
||||
$freshTicket,
|
||||
@@ -121,7 +121,7 @@ final class KanbanBoard extends Component
|
||||
|
||||
$this->board_id = $board->id;
|
||||
|
||||
$fresh = $ticket->fresh(['user:id,name,email', 'owner:id,name,email', 'agent:id,name,email', 'status', 'priority', 'category', 'project', 'team', 'board']) ?? $ticket;
|
||||
$fresh = $ticket->fresh(['user:id,name,email', 'agent:id,name,email', 'status', 'priority', 'category', 'project', 'team', 'board']) ?? $ticket;
|
||||
$workflow->dispatch($fresh, 'ticket.updated', Auth::user());
|
||||
|
||||
$message = 'Ticket '.$fresh->reference.' moved from '.$oldBoardName.' to '.$board->name.'.';
|
||||
@@ -206,7 +206,7 @@ final class KanbanBoard extends Component
|
||||
}
|
||||
|
||||
return Ticket::query()
|
||||
->with(['user:id,name,email', 'owner:id,name,email', 'agent:id,name,email', 'status', 'priority', 'category', 'project', 'team', 'product', 'tags', 'checklistItems', 'board'])
|
||||
->with(['user:id,name,email', 'agent:id,name,email', 'status', 'priority', 'category', 'project', 'team', 'product', 'tags', 'checklistItems', 'board'])
|
||||
->visibleTo(Auth::user())
|
||||
->where('board_id', $board->id)
|
||||
->latest('last_activity_at')
|
||||
|
||||
@@ -27,7 +27,7 @@ final class Ticket extends Model
|
||||
|
||||
protected $table = 'helpdesk_tickets';
|
||||
|
||||
protected $with = ['user', 'owner', 'agent'];
|
||||
protected $with = ['user'];
|
||||
|
||||
/** @var list<string> */
|
||||
protected $fillable = [
|
||||
@@ -62,17 +62,24 @@ final class Ticket extends Model
|
||||
return $this->belongsTo(User::class, 'user_id');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Compatibility alias for the requester.
|
||||
*
|
||||
* This is deliberately an accessor, not another BelongsTo relation.
|
||||
* `owner()` and `user()` previously pointed to the same users.user_id
|
||||
* relation and caused duplicate User relationship queries.
|
||||
*/
|
||||
public function getOwnerAttribute(): ?User
|
||||
{
|
||||
return $this->user;
|
||||
}
|
||||
|
||||
public function tickets(): Ticket|HasMany
|
||||
{
|
||||
return $this->hasMany(Ticket::class, 'ticket_id');
|
||||
}
|
||||
|
||||
/** @return BelongsTo<User, Ticket> */
|
||||
public function owner(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'user_id');
|
||||
}
|
||||
|
||||
/** @return BelongsTo<User, Ticket> */
|
||||
public function agent(): BelongsTo
|
||||
{
|
||||
|
||||
@@ -69,7 +69,7 @@ final class WorkflowNotificationService
|
||||
*/
|
||||
private function hydrateTicketUsers(Ticket $ticket): void
|
||||
{
|
||||
if ($ticket->relationLoaded('owner') && $ticket->relationLoaded('agent')) {
|
||||
if ($ticket->relationLoaded('user') && $ticket->relationLoaded('agent')) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -81,7 +81,6 @@ final class WorkflowNotificationService
|
||||
|
||||
if ($userIds->isEmpty()) {
|
||||
$ticket->setRelation('user', null);
|
||||
$ticket->setRelation('owner', null);
|
||||
$ticket->setRelation('agent', null);
|
||||
|
||||
return;
|
||||
@@ -96,7 +95,6 @@ final class WorkflowNotificationService
|
||||
$agent = $ticket->agent_id === null ? null : $users->get((int) $ticket->agent_id);
|
||||
|
||||
$ticket->setRelation('user', $owner);
|
||||
$ticket->setRelation('owner', $owner);
|
||||
$ticket->setRelation('agent', $agent);
|
||||
}
|
||||
|
||||
@@ -105,7 +103,7 @@ final class WorkflowNotificationService
|
||||
return match ($role) {
|
||||
'agent' => $ticket->agent,
|
||||
'actor' => $actor,
|
||||
default => $ticket->owner,
|
||||
default => $ticket->user,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -119,7 +117,7 @@ final class WorkflowNotificationService
|
||||
'{{ ticket.category }}' => (string) ($ticket->category?->name ?? ''),
|
||||
'{{ ticket.project }}' => (string) ($ticket->project?->title ?? ''),
|
||||
'{{ ticket.team }}' => (string) ($ticket->team?->name ?? ''),
|
||||
'{{ requester.name }}' => (string) ($ticket->owner?->name ?? ''),
|
||||
'{{ requester.name }}' => (string) ($ticket->user?->name ?? ''),
|
||||
'{{ agent.name }}' => (string) ($ticket->agent?->name ?? ''),
|
||||
'{{ actor.name }}' => (string) ($actor?->name ?? ''),
|
||||
'{{ ticket.url }}' => route('helpdesk.tickets.show', ['ticket' => $ticket->getKey()]),
|
||||
|
||||
@@ -1,457 +0,0 @@
|
||||
<?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 ?: '[]').'); ?>');
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,6 @@
|
||||
class="bg-zinc-50 text-left text-xs font-semibold uppercase tracking-wide text-zinc-500 dark:bg-zinc-800 dark:text-zinc-300">
|
||||
<tr>
|
||||
<th class="px-3 py-2.5">Project</th>
|
||||
<th class="px-3 py-2.5">Company</th>
|
||||
<th class="px-3 py-2.5">Team</th>
|
||||
<th class="px-3 py-2.5">Colour</th>
|
||||
<th class="px-3 py-2.5">Default</th>
|
||||
@@ -16,7 +15,6 @@
|
||||
@forelse ($items as $item)
|
||||
<tr>
|
||||
<td class="px-3 py-2.5 font-medium text-zinc-900 dark:text-zinc-100">{{ $item->title }}</td>
|
||||
<td class="px-3 py-2.5 text-zinc-500">{{ $item->company?->name ?? 'Global' }}</td>
|
||||
<td class="px-3 py-2.5 text-zinc-500">{{ $item->team?->name ?? 'No team' }}</td>
|
||||
<td class="px-3 py-2.5">
|
||||
<span class="inline-flex items-center gap-2 rounded-full border border-zinc-200 bg-white px-2.5 py-1 text-xs font-semibold text-zinc-700 shadow-sm dark:border-zinc-700 dark:bg-zinc-950 dark:text-zinc-200">
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
class="bg-zinc-50 text-left text-xs font-semibold uppercase tracking-wide text-zinc-500 dark:bg-zinc-800 dark:text-zinc-300">
|
||||
<tr>
|
||||
<th class="px-3 py-2.5">Team</th>
|
||||
<th class="px-3 py-2.5">Company</th>
|
||||
<th class="px-3 py-2.5">Colour</th>
|
||||
<th class="px-3 py-2.5">Default</th>
|
||||
<th class="px-3 py-2.5">Tickets</th>
|
||||
@@ -15,7 +14,6 @@
|
||||
@forelse ($items as $item)
|
||||
<tr>
|
||||
<td class="px-3 py-2.5 font-medium text-zinc-900 dark:text-zinc-100">{{ $item->name }}</td>
|
||||
<td class="px-3 py-2.5 text-zinc-500">{{ $item->company?->name ?? 'Global' }}</td>
|
||||
<td class="px-3 py-2.5">
|
||||
<span class="inline-flex items-center gap-2 rounded-full border border-zinc-200 bg-white px-2.5 py-1 text-xs font-semibold text-zinc-700 shadow-sm dark:border-zinc-700 dark:bg-zinc-950 dark:text-zinc-200">
|
||||
<span class="h-3 w-3 rounded-full" style="background-color: {{ \Modules\Helpdesk\Support\Color::css($item->color) }}"></span>
|
||||
|
||||
@@ -6,19 +6,11 @@
|
||||
|
||||
<body class="min-h-screen bg-white dark:bg-zinc-800">
|
||||
@php
|
||||
/*
|
||||
* Match the Core WMS support navigation strategy:
|
||||
* detect routes rather than hiding navigation behind role checks.
|
||||
*/
|
||||
$helpdeskEnabled = Route::has('helpdesk.index') || Route::has('helpdesk.tickets.index');
|
||||
|
||||
$helpdeskSettingsRoute = Route::has('helpdesk.settings')
|
||||
? 'helpdesk.settings'
|
||||
: (Route::has('helpdesk.settings.index')
|
||||
? 'helpdesk.settings.index'
|
||||
: null);
|
||||
|
||||
$helpdeskActive = request()->routeIs('helpdesk.*');
|
||||
@endphp
|
||||
|
||||
<flux:sidebar
|
||||
@@ -48,63 +40,40 @@
|
||||
</flux:sidebar.item>
|
||||
</flux:sidebar.group>
|
||||
|
||||
@if ($helpdeskEnabled)
|
||||
<div class="hd-support-group">
|
||||
{{-- Core WMS uses Support as the selected parent group. --}}
|
||||
@if (Route::has('helpdesk.index'))
|
||||
<flux:sidebar.item
|
||||
icon="lifebuoy"
|
||||
:href="route('helpdesk.index')"
|
||||
class="hd-support-parent {{ $helpdeskActive ? 'is-section-active' : '' }}"
|
||||
wire:navigate
|
||||
>
|
||||
{{ __('Support') }}
|
||||
</flux:sidebar.item>
|
||||
@else
|
||||
<div class="hd-support-parent is-section-active">
|
||||
<span class="hd-support-parent-label">{{ __('Support') }}</span>
|
||||
</div>
|
||||
@endif
|
||||
<flux:sidebar.group :heading="__('Helpdesk')" class="grid">
|
||||
@if (Route::has('helpdesk.tickets.index'))
|
||||
<flux:sidebar.item
|
||||
icon="inbox"
|
||||
:href="route('helpdesk.tickets.index')"
|
||||
:current="request()->routeIs('helpdesk.tickets.*')"
|
||||
wire:navigate
|
||||
>
|
||||
{{ __('Tickets') }}
|
||||
</flux:sidebar.item>
|
||||
@endif
|
||||
|
||||
<div class="hd-support-children">
|
||||
@if (Route::has('helpdesk.tickets.index'))
|
||||
<flux:sidebar.item
|
||||
icon="inbox"
|
||||
:href="route('helpdesk.tickets.index')"
|
||||
:current="request()->routeIs('helpdesk.tickets.*')"
|
||||
class="hd-support-child"
|
||||
wire:navigate
|
||||
>
|
||||
{{ __('Tickets') }}
|
||||
</flux:sidebar.item>
|
||||
@endif
|
||||
@if (Route::has('helpdesk.kanban'))
|
||||
<flux:sidebar.item
|
||||
icon="squares-2x2"
|
||||
:href="route('helpdesk.kanban')"
|
||||
:current="request()->routeIs('helpdesk.kanban')"
|
||||
wire:navigate
|
||||
>
|
||||
{{ __('Kanban') }}
|
||||
</flux:sidebar.item>
|
||||
@endif
|
||||
|
||||
@if (Route::has('helpdesk.kanban'))
|
||||
<flux:sidebar.item
|
||||
icon="squares-2x2"
|
||||
:href="route('helpdesk.kanban')"
|
||||
:current="request()->routeIs('helpdesk.kanban')"
|
||||
class="hd-support-child"
|
||||
wire:navigate
|
||||
>
|
||||
{{ __('Kanban') }}
|
||||
</flux:sidebar.item>
|
||||
@endif
|
||||
|
||||
@if ($helpdeskSettingsRoute)
|
||||
<flux:sidebar.item
|
||||
icon="cog"
|
||||
:href="route($helpdeskSettingsRoute)"
|
||||
:current="request()->routeIs('helpdesk.settings') || request()->routeIs('helpdesk.settings.index')"
|
||||
class="hd-support-child"
|
||||
wire:navigate
|
||||
>
|
||||
{{ __('Settings') }}
|
||||
</flux:sidebar.item>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
@if ($helpdeskSettingsRoute)
|
||||
<flux:sidebar.item
|
||||
icon="cog"
|
||||
:href="route($helpdeskSettingsRoute)"
|
||||
:current="request()->routeIs('helpdesk.settings') || request()->routeIs('helpdesk.settings.index')"
|
||||
wire:navigate
|
||||
>
|
||||
{{ __('Settings') }}
|
||||
</flux:sidebar.item>
|
||||
@endif
|
||||
</flux:sidebar.group>
|
||||
</flux:sidebar.nav>
|
||||
|
||||
<flux:spacer />
|
||||
@@ -156,16 +125,6 @@
|
||||
<flux:menu.separator />
|
||||
|
||||
<flux:menu.radio.group>
|
||||
@if (Route::has('helpdesk.index'))
|
||||
<flux:menu.item
|
||||
:href="route('helpdesk.index')"
|
||||
icon="lifebuoy"
|
||||
wire:navigate
|
||||
>
|
||||
{{ __('Support') }}
|
||||
</flux:menu.item>
|
||||
@endif
|
||||
|
||||
@if ($helpdeskSettingsRoute)
|
||||
<flux:menu.item
|
||||
:href="route($helpdeskSettingsRoute)"
|
||||
|
||||
+4
-28
@@ -173,16 +173,10 @@
|
||||
|
||||
@if ($section === 'categories')
|
||||
<x-helpdesk.settings-section title="Categories"
|
||||
description="Group tickets by support area, optionally per company.">
|
||||
description="Group tickets by support area.">
|
||||
<x-slot:form>
|
||||
<form wire:submit.prevent="saveCategory" class="grid gap-3 xl:grid-cols-12">
|
||||
<div class="xl:col-span-3">
|
||||
<flux:select wire:model="categoryForm.company_id" label="Company">
|
||||
<option value="">Global</option>
|
||||
@foreach ($companies as $company)
|
||||
<option value="{{ $company->id }}">{{ $company->name }}</option>
|
||||
@endforeach
|
||||
</flux:select>
|
||||
</div>
|
||||
<div class="xl:col-span-3">
|
||||
<flux:input wire:model="categoryForm.name" label="Name" />
|
||||
@@ -215,16 +209,10 @@
|
||||
|
||||
@if ($section === 'teams')
|
||||
<x-helpdesk.settings-section title="Teams"
|
||||
description="Create support teams under each company and assign projects to them.">
|
||||
description="Create support teams and assign projects to them.">
|
||||
<x-slot:form>
|
||||
<form wire:submit.prevent="saveTeam" class="grid gap-3 xl:grid-cols-12">
|
||||
<div class="xl:col-span-3">
|
||||
<flux:select wire:model="teamForm.company_id" label="Company">
|
||||
<option value="">Global</option>
|
||||
@foreach ($companies as $company)
|
||||
<option value="{{ $company->id }}">{{ $company->name }}</option>
|
||||
@endforeach
|
||||
</flux:select>
|
||||
</div>
|
||||
<div class="xl:col-span-3">
|
||||
<flux:input wire:model="teamForm.name" label="Name" />
|
||||
@@ -257,16 +245,10 @@
|
||||
|
||||
@if ($section === 'projects')
|
||||
<x-helpdesk.settings-section title="Projects"
|
||||
description="Create company projects and optionally assign them to support teams.">
|
||||
description="Create projects and optionally assign them to support teams.">
|
||||
<x-slot:form>
|
||||
<form wire:submit.prevent="saveProject" class="grid gap-3 xl:grid-cols-12">
|
||||
<div class="xl:col-span-2">
|
||||
<flux:select wire:model="projectForm.company_id" label="Company">
|
||||
<option value="">Global</option>
|
||||
@foreach ($companies as $company)
|
||||
<option value="{{ $company->id }}">{{ $company->name }}</option>
|
||||
@endforeach
|
||||
</flux:select>
|
||||
</div>
|
||||
<div class="xl:col-span-2">
|
||||
<flux:select wire:model="projectForm.team_id" label="Team">
|
||||
@@ -420,12 +402,6 @@
|
||||
description="Configure repositories that can be linked to projects and tickets. Commits can then be stored against tickets for traceability.">
|
||||
<x-slot:form>
|
||||
<form wire:submit.prevent="saveRepository" class="grid gap-3 md:grid-cols-6">
|
||||
<flux:select wire:model="repositoryForm.company_id" label="Company">
|
||||
<option value="">Global</option>
|
||||
@foreach ($companies as $company)
|
||||
<option value="{{ $company->id }}">{{ $company->name }}</option>
|
||||
@endforeach
|
||||
</flux:select>
|
||||
<flux:select wire:model="repositoryForm.project_id" label="Project">
|
||||
<option value="">No project</option>
|
||||
@foreach ($projects as $project)
|
||||
@@ -439,7 +415,7 @@
|
||||
<flux:input wire:model="repositoryForm.webhook_secret" label="Webhook secret" />
|
||||
<div class="md:col-span-full">
|
||||
<flux:input wire:model="repositoryForm.remote_url" label="Remote URL"
|
||||
placeholder="git@github.com:company/repo.git" />
|
||||
placeholder="git@github.com:organisation/repo.git" />
|
||||
</div>
|
||||
<div class="md:col-span-full flex gap-2">
|
||||
<flux:button size="sm" type="submit" variant="primary"
|
||||
|
||||
@@ -73,30 +73,6 @@
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
@if ($companies->isNotEmpty())
|
||||
<div class="xl:col-span-2">
|
||||
<label class="block text-sm font-semibold text-zinc-200" for="company_id">
|
||||
Company <span class="helpdesk-required-star">*</span>
|
||||
</label>
|
||||
|
||||
<div class="mt-2" wire:ignore>
|
||||
<select id="company_id" dusk="ticket-company" data-helpdesk-select2 wire:model.defer="company_id" name="company_id"
|
||||
data-placeholder="Select option" data-wire-model="company_id"
|
||||
class="block w-full rounded-xl border-zinc-700 bg-zinc-950 text-zinc-100 shadow-sm focus:border-blue-500 focus:ring-blue-500">
|
||||
<option value="">Select company</option>
|
||||
@foreach ($companies as $company)
|
||||
<option value="{{ $company->id }}" @selected((string) $company_id === (string) $company->id)>
|
||||
{{ $company->name }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
|
||||
@error('company_id')
|
||||
<p class="mt-2 text-sm text-red-500">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="xl:col-span-2">
|
||||
<label class="block text-sm font-semibold text-zinc-200" for="board_id">
|
||||
Board <span class="helpdesk-required-star">*</span>
|
||||
|
||||
@@ -59,30 +59,6 @@
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
@if ($companies->isNotEmpty())
|
||||
<div class="xl:col-span-2">
|
||||
<label class="block text-sm font-semibold text-zinc-200" for="company_id">
|
||||
Company <span class="helpdesk-required-star">*</span>
|
||||
</label>
|
||||
|
||||
<div class="mt-2" wire:ignore>
|
||||
<select id="company_id" data-helpdesk-select2 data-placeholder="Select option"
|
||||
data-wire-model="company_id"
|
||||
class="block w-full rounded-xl border-zinc-700 bg-zinc-950 text-zinc-100 shadow-sm focus:border-blue-500 focus:ring-blue-500">
|
||||
<option value="">Select company</option>
|
||||
@foreach ($companies as $company)
|
||||
<option value="{{ $company->id }}" @selected((string) $company_id === (string) $company->id)>
|
||||
{{ $company->name }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
|
||||
@error('company_id')
|
||||
<p class="mt-2 text-sm text-red-500">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="xl:col-span-2">
|
||||
<label class="block text-sm font-semibold text-zinc-200" for="board_id">Board</label>
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<p class="text-xs font-semibold uppercase tracking-[0.28em] text-blue-200">Ticket management</p>
|
||||
<h1 class="mt-2 text-3xl font-semibold tracking-tight">Active tickets</h1>
|
||||
<p class="mt-2 max-w-3xl text-sm text-blue-100/90">Search, triage and manage support requests across
|
||||
companies, projects, teams, priorities and statuses.</p>
|
||||
projects, teams, priorities and statuses.</p>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<a href="{{ route('helpdesk.index') }}" wire:navigate class="hd-btn hd-btn-blue">
|
||||
|
||||
@@ -9,11 +9,11 @@
|
||||
use Illuminate\Support\Str;
|
||||
use Modules\Helpdesk\Models\TicketTimeLog;
|
||||
$currentUser = auth()->user();
|
||||
$currentCompany = $currentUser?->company;
|
||||
$canSeeInternalNotes =
|
||||
(bool) ($currentCompany?->is_system_company ?? false) ||
|
||||
Str::lower((string) ($currentCompany?->slug ?? '')) === 'mpba-limited' ||
|
||||
Str::contains(Str::lower((string) ($currentCompany?->name ?? '')), 'mpba');
|
||||
(bool) ($currentUser?->helpdesk_admin ?? false) ||
|
||||
(bool) ($currentUser?->helpdesk_agent ?? false) ||
|
||||
(method_exists($currentUser, 'isHelpdeskAdmin') && $currentUser->isHelpdeskAdmin()) ||
|
||||
(method_exists($currentUser, 'isHelpdeskAgent') && $currentUser->isHelpdeskAgent());
|
||||
$visibleCommentCount = $ticket->comments->count();
|
||||
$ticketTimeLogs = TicketTimeLog::query()
|
||||
->with('user')
|
||||
@@ -1285,10 +1285,6 @@
|
||||
<dt class="text-zinc-500">Email</dt>
|
||||
<dd class="truncate">{{ $ticket->owner?->email ?? 'None' }}</dd>
|
||||
</div>
|
||||
<div class="flex justify-between gap-3">
|
||||
<dt class="text-zinc-500">Company</dt>
|
||||
<dd>{{ $ticket->owner?->company?->name ?? 'Current company' }}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
|
||||
@if ($ticket->tags->isNotEmpty())
|
||||
|
||||
Reference in New Issue
Block a user