511 lines
19 KiB
PHP
511 lines
19 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Livewire\Calls;
|
|
|
|
use App\Helpers\StringHelper;
|
|
use App\Jobs\SendEmailNewTag;
|
|
use App\Models\Category;
|
|
use App\Models\Language;
|
|
use App\Models\Tag;
|
|
use Illuminate\Support\Facades\Auth;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Livewire\Component;
|
|
|
|
class CallSkillInput extends Component
|
|
{
|
|
// Tagify state
|
|
public string $tagsArray = '[]';
|
|
public array $suggestions = [];
|
|
|
|
// New tag creation modal
|
|
public bool $modalVisible = false;
|
|
public array $newTag = ['name' => ''];
|
|
public ?int $newTagCategory = null;
|
|
public array $categoryOptions = [];
|
|
public string $categoryColor = 'gray';
|
|
|
|
// Language detection
|
|
public bool $sessionLanguageOk = false;
|
|
public bool $sessionLanguageIgnored = false;
|
|
public bool $transLanguageOk = false;
|
|
public bool $transLanguageIgnored = false;
|
|
|
|
// Translation support
|
|
public bool $translationPossible = true;
|
|
public bool $translationAllowed = true;
|
|
public bool $translationVisible = false;
|
|
public array $translationLanguages = [];
|
|
public $selectTranslationLanguage = null;
|
|
public array $translationOptions = [];
|
|
public $selectTagTranslation = null;
|
|
public array $inputTagTranslation = [];
|
|
public bool $inputDisabled = true;
|
|
public $translateRadioButton = null;
|
|
|
|
protected $langDetector = null;
|
|
|
|
protected function rules(): array
|
|
{
|
|
return $this->createTagValidationRules();
|
|
}
|
|
|
|
public ?int $initialTagId = null;
|
|
|
|
public function mount(?int $initialTagId = null): void
|
|
{
|
|
$this->initialTagId = $initialTagId;
|
|
|
|
if ($initialTagId) {
|
|
$tag = \App\Models\Tag::find($initialTagId);
|
|
if ($tag) {
|
|
$color = $tag->contexts->first()?->category?->relatedColor ?? 'gray';
|
|
$tagDisplayName = $tag->translation?->name ?? $tag->name;
|
|
$this->tagsArray = json_encode([[
|
|
'value' => $tagDisplayName,
|
|
'tag_id' => $tag->tag_id,
|
|
'title' => $tagDisplayName,
|
|
'style' => '--tag-bg:' . tailwindColorToHex($color . '-400') . '; --tag-text-color:#000; --tag-hover:' . tailwindColorToHex($color . '-200'),
|
|
]]);
|
|
}
|
|
}
|
|
|
|
$this->suggestions = $this->getSuggestions();
|
|
$this->checkTranslationAllowed();
|
|
$this->checkTranslationPossible();
|
|
}
|
|
|
|
protected function getSuggestions(): array
|
|
{
|
|
return DB::table('taggable_tags as tt')
|
|
->join('taggable_locales as tl', 'tt.tag_id', '=', 'tl.taggable_tag_id')
|
|
->join('taggable_locale_context as tlc', 'tt.tag_id', '=', 'tlc.tag_id')
|
|
->join('taggable_contexts as tc', 'tlc.context_id', '=', 'tc.id')
|
|
->join('categories as c', 'tc.category_id', '=', 'c.id')
|
|
->join('categories as croot', DB::raw('COALESCE(c.parent_id, c.id)'), '=', 'croot.id')
|
|
->where('tl.locale', app()->getLocale())
|
|
->select('tt.tag_id', 'tt.name', 'croot.color')
|
|
->distinct()
|
|
->orderBy('tt.name')
|
|
->get()
|
|
->map(function ($t) {
|
|
$color = $t->color ?? 'gray';
|
|
return [
|
|
'value' => $t->name,
|
|
'tag_id' => $t->tag_id,
|
|
'style' => '--tag-bg:' . tailwindColorToHex($color . '-400') . '; --tag-text-color:#000; --tag-hover:' . tailwindColorToHex($color . '-200'),
|
|
'title' => $t->name,
|
|
];
|
|
})
|
|
->values()
|
|
->toArray();
|
|
}
|
|
|
|
/**
|
|
* Called from Alpine when the user selects a known tag from the whitelist.
|
|
* Notifies the parent Create component.
|
|
*/
|
|
public function notifyTagSelected(int $tagId): void
|
|
{
|
|
$this->dispatch('callTagSelected', tagId: $tagId);
|
|
}
|
|
|
|
/**
|
|
* Called from Alpine when the tag is removed (input cleared).
|
|
*/
|
|
public function notifyTagCleared(): void
|
|
{
|
|
$this->dispatch('callTagCleared');
|
|
}
|
|
|
|
/**
|
|
* Called from Alpine when the user types an unknown tag name and confirms it.
|
|
*/
|
|
public function openNewTagModal(string $name): void
|
|
{
|
|
$this->newTag['name'] = $name;
|
|
$this->categoryOptions = $this->loadCategoryOptions();
|
|
$this->modalVisible = true;
|
|
$this->checkSessionLanguage();
|
|
}
|
|
|
|
protected function loadCategoryOptions(): array
|
|
{
|
|
return Category::where('type', Tag::class)
|
|
->get()
|
|
->map(function ($category) {
|
|
return [
|
|
'category_id' => $category->id,
|
|
'name' => ucfirst($category->translation->name ?? ''),
|
|
'description' => $category->relatedPathExSelfTranslation ?? '',
|
|
'color' => $category->relatedColor ?? 'gray',
|
|
];
|
|
})
|
|
->sortBy('name')
|
|
->values()
|
|
->toArray();
|
|
}
|
|
|
|
public function checkTranslationAllowed(): void
|
|
{
|
|
$allowTranslations = timebank_config('tags.allow_tag_transations_for_non_admins', false);
|
|
$profileType = getActiveProfileType();
|
|
|
|
if (!$allowTranslations) {
|
|
$this->translationAllowed = ($profileType === 'admin');
|
|
} else {
|
|
$this->translationAllowed = true;
|
|
}
|
|
}
|
|
|
|
public function checkTranslationPossible(): void
|
|
{
|
|
$profile = getActiveProfile();
|
|
if (!$profile || !method_exists($profile, 'languages')) {
|
|
$this->translationPossible = false;
|
|
return;
|
|
}
|
|
$countNonBaseLanguages = $profile->languages()->where('lang_code', '!=', timebank_config('base_language'))->count();
|
|
if ($countNonBaseLanguages === 0 && app()->getLocale() === timebank_config('base_language')) {
|
|
$this->translationPossible = false;
|
|
}
|
|
}
|
|
|
|
protected function getLanguageDetector()
|
|
{
|
|
if (!$this->langDetector) {
|
|
$this->langDetector = new \Text_LanguageDetect();
|
|
$this->langDetector->setNameMode(2);
|
|
}
|
|
return $this->langDetector;
|
|
}
|
|
|
|
public function checkSessionLanguage(): void
|
|
{
|
|
$this->getLanguageDetector();
|
|
$detectedLanguage = $this->langDetector->detectSimple($this->newTag['name'] ?? '');
|
|
if ($detectedLanguage === session('locale')) {
|
|
$this->sessionLanguageOk = true;
|
|
$this->sessionLanguageIgnored = false;
|
|
} else {
|
|
$this->sessionLanguageOk = false;
|
|
}
|
|
$this->validateOnly('newTag.name');
|
|
}
|
|
|
|
public function checkTransLanguage(): void
|
|
{
|
|
$this->getLanguageDetector();
|
|
$detectedLanguage = $this->langDetector->detectSimple($this->inputTagTranslation['name'] ?? '');
|
|
if ($detectedLanguage === $this->selectTranslationLanguage) {
|
|
$this->transLanguageOk = true;
|
|
$this->transLanguageIgnored = false;
|
|
} else {
|
|
$this->transLanguageOk = false;
|
|
}
|
|
$this->validateOnly('inputTagTranslation.name');
|
|
}
|
|
|
|
public function updatedNewTagCategory(): void
|
|
{
|
|
$this->categoryColor = collect($this->categoryOptions)
|
|
->firstWhere('category_id', $this->newTagCategory)['color'] ?? 'gray';
|
|
$this->selectTagTranslation = null;
|
|
$this->translationOptions = $this->getTranslationOptions($this->selectTranslationLanguage);
|
|
$this->resetErrorBag('inputTagTranslationCategory');
|
|
}
|
|
|
|
public function updatedSessionLanguageIgnored(): void
|
|
{
|
|
if (!$this->sessionLanguageIgnored) {
|
|
$this->checkSessionLanguage();
|
|
}
|
|
$this->validateOnly('newTag.name');
|
|
}
|
|
|
|
public function updatedTransLanguageIgnored(): void
|
|
{
|
|
if (!$this->transLanguageIgnored) {
|
|
$this->checkTransLanguage();
|
|
} else {
|
|
$this->resetErrorBag('inputTagTranslation.name');
|
|
}
|
|
}
|
|
|
|
public function updatedSelectTranslationLanguage(): void
|
|
{
|
|
$this->selectTagTranslation = null;
|
|
$this->translationOptions = $this->getTranslationOptions($this->selectTranslationLanguage);
|
|
}
|
|
|
|
public function updatedTranslationVisible(): void
|
|
{
|
|
if ($this->translationVisible && $this->translationAllowed) {
|
|
$this->updatedNewTagCategory();
|
|
|
|
$profile = getActiveProfile();
|
|
if (!$profile || !method_exists($profile, 'languages')) {
|
|
return;
|
|
}
|
|
|
|
\App\Helpers\ProfileAuthorizationHelper::authorize($profile);
|
|
|
|
$this->translationLanguages = $profile
|
|
->languages()
|
|
->wherePivot('competence', 1)
|
|
->where('lang_code', '!=', app()->getLocale())
|
|
->get()
|
|
->map(function ($language) {
|
|
$language->name = trans($language->name);
|
|
return $language;
|
|
})
|
|
->toArray();
|
|
|
|
if (!collect($this->translationLanguages)->contains('lang_code', 'en')) {
|
|
$transLanguage = Language::where('lang_code', timebank_config('base_language'))->first();
|
|
if ($transLanguage) {
|
|
$transLanguage->name = trans($transLanguage->name);
|
|
$this->translationLanguages = collect($this->translationLanguages)->push($transLanguage)->toArray();
|
|
}
|
|
if (app()->getLocale() != timebank_config('base_language')) {
|
|
$this->selectTranslationLanguage = timebank_config('base_language');
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public function updatedTranslateRadioButton(): void
|
|
{
|
|
if ($this->translateRadioButton === 'select') {
|
|
$this->inputDisabled = true;
|
|
$this->dispatch('disableInput');
|
|
} elseif ($this->translateRadioButton === 'input') {
|
|
$this->inputDisabled = false;
|
|
}
|
|
$this->resetErrorBag('selectTagTranslation');
|
|
$this->resetErrorBag('inputTagTranslation.name');
|
|
$this->resetErrorBag('newTagCategory');
|
|
}
|
|
|
|
public function updatedSelectTagTranslation(): void
|
|
{
|
|
if ($this->selectTagTranslation) {
|
|
$this->categoryColor = Tag::find($this->selectTagTranslation)->categories->first()->relatedColor ?? 'gray';
|
|
$this->translateRadioButton = 'select';
|
|
$this->dispatch('disableInput');
|
|
}
|
|
}
|
|
|
|
public function updatedInputTagTranslation(): void
|
|
{
|
|
$this->translateRadioButton = 'input';
|
|
$this->inputDisabled = false;
|
|
$this->checkTransLanguage();
|
|
}
|
|
|
|
public function getTranslationOptions(?string $locale): array
|
|
{
|
|
if (!$locale) {
|
|
return [];
|
|
}
|
|
|
|
$appLocale = app()->getLocale();
|
|
|
|
$contextIdsInAppLocale = DB::table('taggable_locale_context')
|
|
->whereIn('tag_id', function ($query) use ($appLocale) {
|
|
$query->select('taggable_tag_id')
|
|
->from('taggable_locales')
|
|
->where('locale', $appLocale);
|
|
})
|
|
->pluck('context_id');
|
|
|
|
$tags = Tag::with(['locale', 'contexts.category'])
|
|
->whereHas('locale', function ($query) use ($locale) {
|
|
$query->where('locale', $locale);
|
|
})
|
|
->whereNotIn('tag_id', function ($subquery) use ($contextIdsInAppLocale) {
|
|
$subquery->select('tag_id')
|
|
->from('taggable_locale_context')
|
|
->whereIn('context_id', $contextIdsInAppLocale);
|
|
})
|
|
->get();
|
|
|
|
return $tags->map(function ($tag) use ($locale) {
|
|
$category = optional($tag->contexts->first())->category;
|
|
$description = optional(optional($category)->translation)->name ?? '';
|
|
return [
|
|
'tag_id' => $tag->tag_id,
|
|
'name' => $locale == 'de' ? $tag->name : StringHelper::DutchTitleCase($tag->normalized),
|
|
'description' => $description,
|
|
];
|
|
})->sortBy('name')->values()->toArray();
|
|
}
|
|
|
|
protected function createTagValidationRules(): array
|
|
{
|
|
return [
|
|
'newTag.name' => array_merge(
|
|
timebank_config('tags.name_rule'),
|
|
timebank_config('tags.exists_in_current_locale_rule', []),
|
|
[
|
|
'sometimes',
|
|
function ($attribute, $value, $fail) {
|
|
if (!$this->sessionLanguageOk && !$this->sessionLanguageIgnored) {
|
|
$locale = app()->getLocale();
|
|
$localeName = \Locale::getDisplayName($locale, $locale);
|
|
$fail(__('Is this :locale? Please confirm here below', ['locale' => $localeName]));
|
|
}
|
|
},
|
|
]
|
|
),
|
|
'newTagCategory' => function () {
|
|
if ($this->translationVisible && $this->translateRadioButton == 'input') {
|
|
return 'required|int';
|
|
}
|
|
if (!$this->translationVisible) {
|
|
return 'required|int';
|
|
}
|
|
return 'nullable';
|
|
},
|
|
'selectTagTranslation' => ($this->translationVisible && $this->translateRadioButton == 'select')
|
|
? 'required|int'
|
|
: 'nullable',
|
|
'inputTagTranslation.name' => ($this->translationVisible && $this->translateRadioButton === 'input')
|
|
? array_merge(
|
|
timebank_config('tags.name_rule'),
|
|
timebank_config('tags.exists_in_current_locale_rule', []),
|
|
[
|
|
'sometimes',
|
|
function ($attribute, $value, $fail) {
|
|
if (!$this->transLanguageOk && !$this->transLanguageIgnored) {
|
|
$baseLocale = $this->selectTranslationLanguage;
|
|
$locale = \Locale::getDisplayName($baseLocale, $baseLocale);
|
|
$fail(__('Is this :locale? Please confirm here below', ['locale' => $locale]));
|
|
}
|
|
},
|
|
function ($attribute, $value, $fail) {
|
|
$existsInTransLationLanguage = DB::table('taggable_tags')
|
|
->join('taggable_locales', 'taggable_tags.tag_id', '=', 'taggable_locales.taggable_tag_id')
|
|
->where('taggable_locales.locale', $this->selectTranslationLanguage)
|
|
->where(function ($query) use ($value) {
|
|
$query->where('taggable_tags.name', $value)
|
|
->orWhere('taggable_tags.normalized', $value);
|
|
})
|
|
->exists();
|
|
if ($existsInTransLationLanguage) {
|
|
$fail(__('This tag already exists.'));
|
|
}
|
|
},
|
|
]
|
|
)
|
|
: 'nullable',
|
|
];
|
|
}
|
|
|
|
public function createTag(): void
|
|
{
|
|
$this->validate($this->createTagValidationRules());
|
|
$this->resetErrorBag();
|
|
|
|
$name = app()->getLocale() == 'de'
|
|
? trim($this->newTag['name'])
|
|
: StringHelper::DutchTitleCase(trim($this->newTag['name']));
|
|
$normalized = call_user_func(config('taggable.normalizer'), $name);
|
|
|
|
$existing = Tag::whereHas('locale', fn ($q) => $q->where('locale', app()->getLocale()))
|
|
->where('name', $name)
|
|
->first();
|
|
|
|
if ($existing) {
|
|
$tag = $existing;
|
|
} else {
|
|
$tag = Tag::create(['name' => $name, 'normalized' => $normalized]);
|
|
$tag->locale()->create(['locale' => app()->getLocale()]);
|
|
}
|
|
|
|
$context = [
|
|
'category_id' => $this->newTagCategory,
|
|
'updated_by_user' => Auth::guard('web')->id(),
|
|
];
|
|
|
|
if ($this->translationVisible) {
|
|
if ($this->translateRadioButton === 'select') {
|
|
$tagContext = Tag::find($this->selectTagTranslation)->contexts()->first();
|
|
$tag->contexts()->attach($tagContext->id);
|
|
} elseif ($this->translateRadioButton === 'input') {
|
|
$tagContext = $tag->contexts()->create($context);
|
|
|
|
$this->inputTagTranslation['name'] = $this->selectTranslationLanguage == 'de'
|
|
? $this->inputTagTranslation['name']
|
|
: StringHelper::DutchTitleCase($this->inputTagTranslation['name']);
|
|
|
|
$nameTranslation = $this->inputTagTranslation['name'];
|
|
$normalizedTranslation = call_user_func(config('taggable.normalizer'), $nameTranslation);
|
|
|
|
$tagTranslation = Tag::create([
|
|
'name' => $nameTranslation,
|
|
'normalized' => $normalizedTranslation,
|
|
]);
|
|
$tagTranslation->locale()->create(['locale' => $this->selectTranslationLanguage]);
|
|
$tagTranslation->contexts()->attach($tagContext->id);
|
|
}
|
|
} else {
|
|
if (!$tag->contexts()->where('category_id', $this->newTagCategory)->exists()) {
|
|
$tag->contexts()->create($context);
|
|
}
|
|
}
|
|
|
|
$color = collect($this->categoryOptions)->firstWhere('category_id', $this->newTagCategory)['color'] ?? 'gray';
|
|
$this->tagsArray = json_encode([[
|
|
'value' => $tag->translation?->name ?? $tag->name,
|
|
'tag_id' => $tag->tag_id,
|
|
'title' => $name,
|
|
'style' => '--tag-bg:' . tailwindColorToHex($color . '-400') . '; --tag-text-color:#000; --tag-hover:' . tailwindColorToHex($color . '-200'),
|
|
]]);
|
|
|
|
$this->modalVisible = false;
|
|
$this->newTag = ['name' => ''];
|
|
$this->newTagCategory = null;
|
|
$this->categoryColor = 'gray';
|
|
$this->translationVisible = false;
|
|
$this->translateRadioButton = null;
|
|
$this->selectTagTranslation = null;
|
|
$this->inputTagTranslation = [];
|
|
$this->sessionLanguageOk = false;
|
|
$this->sessionLanguageIgnored = false;
|
|
$this->transLanguageOk = false;
|
|
$this->transLanguageIgnored = false;
|
|
$this->resetErrorBag(['newTag.name', 'newTagCategory']);
|
|
|
|
// Reload Tagify badge in this component's input
|
|
$this->dispatch('callTagifyReload', tagsArray: $this->tagsArray);
|
|
|
|
// Notify parent (Create or Edit) of the selected tag
|
|
$this->dispatch('callTagSelected', tagId: $tag->tag_id);
|
|
|
|
SendEmailNewTag::dispatch($tag->tag_id);
|
|
}
|
|
|
|
public function cancelCreateTag(): void
|
|
{
|
|
$this->resetErrorBag();
|
|
$this->newTag = ['name' => ''];
|
|
$this->newTagCategory = null;
|
|
$this->categoryColor = 'gray';
|
|
$this->modalVisible = false;
|
|
$this->translationVisible = false;
|
|
$this->translateRadioButton = null;
|
|
$this->selectTagTranslation = null;
|
|
$this->inputTagTranslation = [];
|
|
$this->sessionLanguageOk = false;
|
|
$this->sessionLanguageIgnored = false;
|
|
$this->transLanguageOk = false;
|
|
$this->transLanguageIgnored = false;
|
|
$this->dispatch('removeLastCallTag');
|
|
}
|
|
|
|
public function render()
|
|
{
|
|
return view('livewire.calls.call-skill-input');
|
|
}
|
|
}
|