Initial commit

This commit is contained in:
Ronald Huynen
2026-03-23 21:37:59 +01:00
commit 2547717edb
2193 changed files with 972171 additions and 0 deletions

View File

@@ -0,0 +1,510 @@
<?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');
}
}