Add Language Management Module: Created Language resource with CRUD pages, schemas, and models to support the universal translation system. Implemented localization for English and Turkish, ensuring consistent user experience. Added necessary policies and seeder for initial language data, enhancing the overall language management functionality.
This commit is contained in:
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Resources\Languages;
|
||||
|
||||
use App\Filament\Admin\Resources\Languages\Pages\CreateLanguage;
|
||||
use App\Filament\Admin\Resources\Languages\Pages\EditLanguage;
|
||||
use App\Filament\Admin\Resources\Languages\Pages\ListLanguages;
|
||||
use App\Models\Language;
|
||||
use BackedEnum;
|
||||
use UnitEnum;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Schemas\Schema;
|
||||
use Filament\Support\Icons\Heroicon;
|
||||
use Filament\Tables\Table;
|
||||
|
||||
class LanguageResource extends Resource
|
||||
{
|
||||
protected static ?string $model = Language::class;
|
||||
|
||||
protected static string|BackedEnum|null $navigationIcon = Heroicon::OutlinedGlobeAlt;
|
||||
|
||||
protected static UnitEnum|string|null $navigationGroup = 'System';
|
||||
|
||||
protected static ?int $navigationSort = 100;
|
||||
|
||||
public static function getNavigationLabel(): string
|
||||
{
|
||||
return __('language.navigation_label');
|
||||
}
|
||||
|
||||
public static function getModelLabel(): string
|
||||
{
|
||||
return __('language.model_label');
|
||||
}
|
||||
|
||||
public static function getPluralModelLabel(): string
|
||||
{
|
||||
return __('language.plural_model_label');
|
||||
}
|
||||
|
||||
public static function form(Schema $schema): Schema
|
||||
{
|
||||
return $schema
|
||||
->schema(\App\Filament\Admin\Resources\Languages\Schemas\LanguageForm::schema());
|
||||
}
|
||||
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
return \App\Filament\Admin\Resources\Languages\Tables\LanguagesTable::configure($table);
|
||||
}
|
||||
|
||||
public static function getRelations(): array
|
||||
{
|
||||
return [
|
||||
//
|
||||
];
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => ListLanguages::route('/'),
|
||||
'create' => CreateLanguage::route('/create'),
|
||||
'edit' => EditLanguage::route('/{record}/edit'),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Resources\Languages\Pages;
|
||||
|
||||
use App\Filament\Admin\Resources\Languages\LanguageResource;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Resources\Pages\CreateRecord;
|
||||
|
||||
class CreateLanguage extends CreateRecord
|
||||
{
|
||||
protected static string $resource = LanguageResource::class;
|
||||
|
||||
public function getTitle(): string
|
||||
{
|
||||
return __('language.create');
|
||||
}
|
||||
|
||||
protected function getCreatedNotification(): ?Notification
|
||||
{
|
||||
return Notification::make()
|
||||
->success()
|
||||
->title(__('language.created_successfully'))
|
||||
->body(__('language.created_successfully_body'));
|
||||
}
|
||||
|
||||
protected function getRedirectUrl(): string
|
||||
{
|
||||
return $this->getResource()::getUrl('index');
|
||||
}
|
||||
|
||||
protected function mutateFormDataBeforeCreate(array $data): array
|
||||
{
|
||||
$data['created_by'] = auth()->id();
|
||||
$data['updated_by'] = auth()->id();
|
||||
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Resources\Languages\Pages;
|
||||
|
||||
use App\Filament\Admin\Resources\Languages\LanguageResource;
|
||||
use Filament\Actions\DeleteAction;
|
||||
use Filament\Actions\ForceDeleteAction;
|
||||
use Filament\Actions\RestoreAction;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
|
||||
class EditLanguage extends EditRecord
|
||||
{
|
||||
protected static string $resource = LanguageResource::class;
|
||||
|
||||
public function getTitle(): string
|
||||
{
|
||||
return __('language.edit');
|
||||
}
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
DeleteAction::make()
|
||||
->label(__('language.delete')),
|
||||
RestoreAction::make()
|
||||
->label(__('language.restore')),
|
||||
ForceDeleteAction::make()
|
||||
->label(__('language.force_delete')),
|
||||
];
|
||||
}
|
||||
|
||||
protected function getSavedNotification(): ?Notification
|
||||
{
|
||||
return Notification::make()
|
||||
->success()
|
||||
->title(__('language.updated_successfully'))
|
||||
->body(__('language.updated_successfully_body'));
|
||||
}
|
||||
|
||||
protected function getRedirectUrl(): string
|
||||
{
|
||||
return $this->getResource()::getUrl('index');
|
||||
}
|
||||
|
||||
protected function mutateFormDataBeforeSave(array $data): array
|
||||
{
|
||||
$data['updated_by'] = auth()->id();
|
||||
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Resources\Languages\Pages;
|
||||
|
||||
use App\Filament\Admin\Resources\Languages\LanguageResource;
|
||||
use Filament\Actions\CreateAction;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
|
||||
class ListLanguages extends ListRecords
|
||||
{
|
||||
protected static string $resource = LanguageResource::class;
|
||||
|
||||
public function getTitle(): string
|
||||
{
|
||||
return __('language.title');
|
||||
}
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
CreateAction::make()
|
||||
->label(__('language.create')),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Resources\Languages\Schemas;
|
||||
|
||||
use Filament\Forms\Components\Checkbox;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Schemas\Components\Grid;
|
||||
use Filament\Schemas\Components\Section;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class LanguageForm
|
||||
{
|
||||
public static function schema(): array
|
||||
{
|
||||
return [
|
||||
Section::make(__('language.basic_information'))
|
||||
->schema([
|
||||
Grid::make(2)->schema([
|
||||
TextInput::make('code')
|
||||
->label(__('language.code'))
|
||||
->required()
|
||||
->unique(ignoreRecord: true)
|
||||
->maxLength(10)
|
||||
->placeholder('tr, en, de')
|
||||
->helperText(__('language.code_helper'))
|
||||
->columnSpan(1),
|
||||
|
||||
TextInput::make('flag')
|
||||
->label(__('language.flag'))
|
||||
->maxLength(10)
|
||||
->placeholder('🇹🇷')
|
||||
->helperText(__('language.flag_helper'))
|
||||
->columnSpan(1),
|
||||
]),
|
||||
|
||||
Grid::make(2)->schema([
|
||||
TextInput::make('name')
|
||||
->label(__('language.name'))
|
||||
->required()
|
||||
->maxLength(100)
|
||||
->placeholder('Turkish')
|
||||
->helperText(__('language.name_helper'))
|
||||
->columnSpan(1),
|
||||
|
||||
TextInput::make('native_name')
|
||||
->label(__('language.native_name'))
|
||||
->required()
|
||||
->maxLength(100)
|
||||
->placeholder('Türkçe')
|
||||
->helperText(__('language.native_name_helper'))
|
||||
->columnSpan(1),
|
||||
]),
|
||||
|
||||
Grid::make(2)->schema([
|
||||
Select::make('direction')
|
||||
->label(__('language.direction'))
|
||||
->options([
|
||||
'ltr' => __('language.direction_ltr'),
|
||||
'rtl' => __('language.direction_rtl'),
|
||||
])
|
||||
->default('ltr')
|
||||
->required()
|
||||
->columnSpan(1),
|
||||
|
||||
TextInput::make('sort_order')
|
||||
->label(__('language.sort_order'))
|
||||
->numeric()
|
||||
->default(0)
|
||||
->helperText(__('language.sort_order_helper'))
|
||||
->columnSpan(1),
|
||||
]),
|
||||
]),
|
||||
|
||||
Section::make(__('language.settings'))
|
||||
->schema([
|
||||
Grid::make(3)->schema([
|
||||
Checkbox::make('is_active')
|
||||
->label(__('language.is_active'))
|
||||
->default(true)
|
||||
->helperText(__('language.is_active_helper'))
|
||||
->columnSpan(1),
|
||||
|
||||
Checkbox::make('is_default')
|
||||
->label(__('language.is_default'))
|
||||
->default(false)
|
||||
->helperText(__('language.is_default_helper'))
|
||||
->columnSpan(1)
|
||||
->disabled(fn ($record) => $record && $record->is_default),
|
||||
]),
|
||||
]),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Resources\Languages\Tables;
|
||||
|
||||
use Filament\Actions\BulkActionGroup;
|
||||
use Filament\Actions\DeleteBulkAction;
|
||||
use Filament\Actions\DeleteAction;
|
||||
use Filament\Actions\EditAction;
|
||||
use Filament\Actions\ForceDeleteAction;
|
||||
use Filament\Actions\ForceDeleteBulkAction;
|
||||
use Filament\Actions\RestoreAction;
|
||||
use Filament\Actions\RestoreBulkAction;
|
||||
use Filament\Tables\Columns\IconColumn;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Filters\TrashedFilter;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\SoftDeletingScope;
|
||||
|
||||
class LanguagesTable
|
||||
{
|
||||
public static function configure(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->columns([
|
||||
TextColumn::make('flag')
|
||||
->label(__('language.table_flag'))
|
||||
->sortable()
|
||||
->searchable()
|
||||
->formatStateUsing(fn (?string $state): string =>
|
||||
'<span style="font-size: 2rem; line-height: 1;">' . ($state ?? '🌐') . '</span>'
|
||||
)
|
||||
->html()
|
||||
->alignCenter(),
|
||||
|
||||
TextColumn::make('code')
|
||||
->label(__('language.table_code'))
|
||||
->sortable()
|
||||
->searchable()
|
||||
->badge()
|
||||
->color('info'),
|
||||
|
||||
TextColumn::make('name')
|
||||
->label(__('language.table_name'))
|
||||
->sortable()
|
||||
->searchable()
|
||||
->description(fn ($record) => $record->native_name),
|
||||
|
||||
IconColumn::make('is_active')
|
||||
->label(__('language.table_is_active'))
|
||||
->boolean()
|
||||
->sortable(),
|
||||
|
||||
IconColumn::make('is_default')
|
||||
->label(__('language.table_is_default'))
|
||||
->boolean()
|
||||
->sortable(),
|
||||
|
||||
TextColumn::make('direction')
|
||||
->label(__('language.table_direction'))
|
||||
->badge()
|
||||
->color(fn (string $state): string => match ($state) {
|
||||
'ltr' => 'success',
|
||||
'rtl' => 'warning',
|
||||
})
|
||||
->sortable(),
|
||||
|
||||
TextColumn::make('sort_order')
|
||||
->label(__('language.table_sort_order'))
|
||||
->numeric()
|
||||
->sortable(),
|
||||
|
||||
TextColumn::make('created_at')
|
||||
->label(__('language.table_created_at'))
|
||||
->dateTime()
|
||||
->sortable()
|
||||
->toggleable(isToggledHiddenByDefault: true),
|
||||
|
||||
TextColumn::make('updated_at')
|
||||
->label(__('language.table_updated_at'))
|
||||
->dateTime()
|
||||
->sortable()
|
||||
->toggleable(isToggledHiddenByDefault: true),
|
||||
|
||||
TextColumn::make('deleted_at')
|
||||
->label(__('language.table_deleted_at'))
|
||||
->dateTime()
|
||||
->sortable()
|
||||
->toggleable(isToggledHiddenByDefault: true),
|
||||
])
|
||||
->filters([
|
||||
TrashedFilter::make(),
|
||||
])
|
||||
->recordActions([
|
||||
EditAction::make(),
|
||||
DeleteAction::make(),
|
||||
RestoreAction::make(),
|
||||
ForceDeleteAction::make(),
|
||||
])
|
||||
->toolbarActions([
|
||||
BulkActionGroup::make([
|
||||
DeleteBulkAction::make(),
|
||||
ForceDeleteBulkAction::make(),
|
||||
RestoreBulkAction::make(),
|
||||
]),
|
||||
])
|
||||
->defaultSort('sort_order')
|
||||
->modifyQueryUsing(fn (Builder $query) => $query->withoutGlobalScopes([
|
||||
SoftDeletingScope::class,
|
||||
]));
|
||||
}
|
||||
}
|
||||
|
||||
+13
-1
@@ -2,13 +2,14 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Traits\HasTranslations;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class Blog extends Model
|
||||
{
|
||||
use HasFactory, SoftDeletes;
|
||||
use HasFactory, SoftDeletes, HasTranslations;
|
||||
|
||||
protected $fillable = [
|
||||
'title',
|
||||
@@ -28,6 +29,17 @@ class Blog extends Model
|
||||
'allow_comments',
|
||||
];
|
||||
|
||||
/**
|
||||
* Translatable fields
|
||||
*/
|
||||
protected $translatable = [
|
||||
'title',
|
||||
'content',
|
||||
'excerpt',
|
||||
'meta_title',
|
||||
'meta_description',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'published_at' => 'datetime',
|
||||
'tags' => 'array',
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
class Language extends Model
|
||||
{
|
||||
use HasFactory, SoftDeletes;
|
||||
|
||||
protected $fillable = [
|
||||
'code',
|
||||
'name',
|
||||
'native_name',
|
||||
'flag',
|
||||
'direction',
|
||||
'is_active',
|
||||
'is_default',
|
||||
'sort_order',
|
||||
'created_by',
|
||||
'updated_by',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'is_active' => 'boolean',
|
||||
'is_default' => 'boolean',
|
||||
'sort_order' => 'integer',
|
||||
];
|
||||
|
||||
/**
|
||||
* Boot method
|
||||
*/
|
||||
protected static function boot(): void
|
||||
{
|
||||
parent::boot();
|
||||
|
||||
// Only one default language allowed
|
||||
static::saving(function ($language) {
|
||||
if ($language->is_default) {
|
||||
static::where('id', '!=', $language->id)->update(['is_default' => false]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Relationships
|
||||
*/
|
||||
public function translations(): HasMany
|
||||
{
|
||||
return $this->hasMany(Translation::class, 'language_code', 'code');
|
||||
}
|
||||
|
||||
public function userLanguages(): HasMany
|
||||
{
|
||||
return $this->hasMany(UserLanguage::class, 'language_code', 'code');
|
||||
}
|
||||
|
||||
public function creator(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'created_by');
|
||||
}
|
||||
|
||||
public function updater(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'updated_by');
|
||||
}
|
||||
|
||||
/**
|
||||
* Scopes
|
||||
*/
|
||||
public function scopeActive($query)
|
||||
{
|
||||
return $query->where('is_active', true);
|
||||
}
|
||||
|
||||
public function scopeOrdered($query)
|
||||
{
|
||||
return $query->orderBy('sort_order')->orderBy('name');
|
||||
}
|
||||
|
||||
/**
|
||||
* Helpers
|
||||
*/
|
||||
public static function getDefault(): ?self
|
||||
{
|
||||
return static::where('is_default', true)->first();
|
||||
}
|
||||
|
||||
public static function getActive(): \Illuminate\Database\Eloquent\Collection
|
||||
{
|
||||
return static::active()->ordered()->get();
|
||||
}
|
||||
|
||||
public static function findByCode(string $code): ?self
|
||||
{
|
||||
return static::where('code', $code)->first();
|
||||
}
|
||||
|
||||
public function activate(): bool
|
||||
{
|
||||
$this->is_active = true;
|
||||
return $this->save();
|
||||
}
|
||||
|
||||
public function deactivate(): bool
|
||||
{
|
||||
if ($this->is_default) {
|
||||
return false; // Cannot deactivate default language
|
||||
}
|
||||
$this->is_active = false;
|
||||
return $this->save();
|
||||
}
|
||||
|
||||
public function setAsDefault(): bool
|
||||
{
|
||||
$this->is_default = true;
|
||||
$this->is_active = true;
|
||||
return $this->save();
|
||||
}
|
||||
}
|
||||
+13
-1
@@ -2,13 +2,14 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Traits\HasTranslations;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class Page extends Model
|
||||
{
|
||||
use HasFactory, SoftDeletes;
|
||||
use HasFactory, SoftDeletes, HasTranslations;
|
||||
|
||||
protected $fillable = [
|
||||
'title',
|
||||
@@ -28,6 +29,17 @@ class Page extends Model
|
||||
'show_in_menu',
|
||||
];
|
||||
|
||||
/**
|
||||
* Translatable fields
|
||||
*/
|
||||
protected $translatable = [
|
||||
'title',
|
||||
'content',
|
||||
'excerpt',
|
||||
'meta_title',
|
||||
'meta_description',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'published_at' => 'datetime',
|
||||
'is_homepage' => 'boolean',
|
||||
|
||||
@@ -0,0 +1,201 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\MorphTo;
|
||||
|
||||
class Translation extends Model
|
||||
{
|
||||
use HasFactory, SoftDeletes;
|
||||
|
||||
protected $fillable = [
|
||||
'translatable_type',
|
||||
'translatable_id',
|
||||
'language_code',
|
||||
'field_name',
|
||||
'field_value',
|
||||
'status',
|
||||
'version',
|
||||
'created_by',
|
||||
'updated_by',
|
||||
'approved_by',
|
||||
'approved_at',
|
||||
'published_at',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'version' => 'integer',
|
||||
'approved_at' => 'datetime',
|
||||
'published_at' => 'datetime',
|
||||
];
|
||||
|
||||
/**
|
||||
* Relationships
|
||||
*/
|
||||
public function translatable(): MorphTo
|
||||
{
|
||||
return $this->morphTo();
|
||||
}
|
||||
|
||||
public function language(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Language::class, 'language_code', 'code');
|
||||
}
|
||||
|
||||
public function creator(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'created_by');
|
||||
}
|
||||
|
||||
public function updater(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'updated_by');
|
||||
}
|
||||
|
||||
public function approver(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'approved_by');
|
||||
}
|
||||
|
||||
/**
|
||||
* Scopes
|
||||
*/
|
||||
public function scopeForModel($query, string $modelClass, int $modelId)
|
||||
{
|
||||
return $query->where('translatable_type', $modelClass)
|
||||
->where('translatable_id', $modelId);
|
||||
}
|
||||
|
||||
public function scopeForLanguage($query, string $languageCode)
|
||||
{
|
||||
return $query->where('language_code', $languageCode);
|
||||
}
|
||||
|
||||
public function scopeForField($query, string $fieldName)
|
||||
{
|
||||
return $query->where('field_name', $fieldName);
|
||||
}
|
||||
|
||||
public function scopePublished($query)
|
||||
{
|
||||
return $query->where('status', 'published');
|
||||
}
|
||||
|
||||
public function scopeApproved($query)
|
||||
{
|
||||
return $query->whereIn('status', ['approved', 'published']);
|
||||
}
|
||||
|
||||
public function scopeDraft($query)
|
||||
{
|
||||
return $query->where('status', 'draft');
|
||||
}
|
||||
|
||||
public function scopeReview($query)
|
||||
{
|
||||
return $query->where('status', 'review');
|
||||
}
|
||||
|
||||
public function scopePendingApproval($query)
|
||||
{
|
||||
return $query->whereIn('status', ['draft', 'review']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Workflow Methods
|
||||
*/
|
||||
public function submitForReview(): bool
|
||||
{
|
||||
if ($this->status === 'draft') {
|
||||
$this->status = 'review';
|
||||
return $this->save();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public function approve(?int $userId = null): bool
|
||||
{
|
||||
if (in_array($this->status, ['draft', 'review'])) {
|
||||
$this->status = 'approved';
|
||||
$this->approved_by = $userId ?? auth()->id();
|
||||
$this->approved_at = now();
|
||||
return $this->save();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public function publish(?int $userId = null): bool
|
||||
{
|
||||
if (in_array($this->status, ['approved', 'draft', 'review'])) {
|
||||
$this->status = 'published';
|
||||
$this->published_at = now();
|
||||
if (!$this->approved_by) {
|
||||
$this->approved_by = $userId ?? auth()->id();
|
||||
$this->approved_at = now();
|
||||
}
|
||||
return $this->save();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public function reject(): bool
|
||||
{
|
||||
if ($this->status === 'review') {
|
||||
$this->status = 'draft';
|
||||
return $this->save();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public function unpublish(): bool
|
||||
{
|
||||
if ($this->status === 'published') {
|
||||
$this->status = 'approved';
|
||||
$this->published_at = null;
|
||||
return $this->save();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper Methods
|
||||
*/
|
||||
public function isDraft(): bool
|
||||
{
|
||||
return $this->status === 'draft';
|
||||
}
|
||||
|
||||
public function isReview(): bool
|
||||
{
|
||||
return $this->status === 'review';
|
||||
}
|
||||
|
||||
public function isApproved(): bool
|
||||
{
|
||||
return $this->status === 'approved';
|
||||
}
|
||||
|
||||
public function isPublished(): bool
|
||||
{
|
||||
return $this->status === 'published';
|
||||
}
|
||||
|
||||
public function canBeEdited(): bool
|
||||
{
|
||||
return in_array($this->status, ['draft', 'review']);
|
||||
}
|
||||
|
||||
public function canBeApproved(): bool
|
||||
{
|
||||
return in_array($this->status, ['draft', 'review']);
|
||||
}
|
||||
|
||||
public function canBePublished(): bool
|
||||
{
|
||||
return in_array($this->status, ['draft', 'review', 'approved']);
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ namespace App\Models;
|
||||
|
||||
// use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
use Spatie\Permission\Traits\HasRoles;
|
||||
@@ -46,4 +47,128 @@ class User extends Authenticatable
|
||||
'password' => 'hashed',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Relationships
|
||||
*/
|
||||
public function userLanguages(): HasMany
|
||||
{
|
||||
return $this->hasMany(UserLanguage::class);
|
||||
}
|
||||
|
||||
public function languages()
|
||||
{
|
||||
return $this->belongsToMany(Language::class, 'user_languages', 'user_id', 'language_code', 'id', 'code')
|
||||
->withPivot(['can_create', 'can_edit', 'can_approve', 'can_publish'])
|
||||
->withTimestamps();
|
||||
}
|
||||
|
||||
/**
|
||||
* Language Management Methods
|
||||
*/
|
||||
public function canManageLanguage(string $languageCode): bool
|
||||
{
|
||||
// Super admin can manage all languages
|
||||
if ($this->hasRole('super_admin')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return $this->userLanguages()
|
||||
->where('language_code', $languageCode)
|
||||
->where(function ($query) {
|
||||
$query->where('can_create', true)
|
||||
->orWhere('can_edit', true)
|
||||
->orWhere('can_approve', true)
|
||||
->orWhere('can_publish', true);
|
||||
})
|
||||
->exists();
|
||||
}
|
||||
|
||||
public function canCreateInLanguage(string $languageCode): bool
|
||||
{
|
||||
if ($this->hasRole('super_admin')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return $this->userLanguages()
|
||||
->where('language_code', $languageCode)
|
||||
->where('can_create', true)
|
||||
->exists();
|
||||
}
|
||||
|
||||
public function canEditInLanguage(string $languageCode): bool
|
||||
{
|
||||
if ($this->hasRole('super_admin')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return $this->userLanguages()
|
||||
->where('language_code', $languageCode)
|
||||
->where('can_edit', true)
|
||||
->exists();
|
||||
}
|
||||
|
||||
public function canApproveInLanguage(string $languageCode): bool
|
||||
{
|
||||
if ($this->hasRole('super_admin')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return $this->userLanguages()
|
||||
->where('language_code', $languageCode)
|
||||
->where('can_approve', true)
|
||||
->exists();
|
||||
}
|
||||
|
||||
public function canPublishInLanguage(string $languageCode): bool
|
||||
{
|
||||
if ($this->hasRole('super_admin')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return $this->userLanguages()
|
||||
->where('language_code', $languageCode)
|
||||
->where('can_publish', true)
|
||||
->exists();
|
||||
}
|
||||
|
||||
public function getManagedLanguages(): \Illuminate\Support\Collection
|
||||
{
|
||||
return $this->userLanguages()
|
||||
->with('language')
|
||||
->get()
|
||||
->pluck('language');
|
||||
}
|
||||
|
||||
public function getManagedLanguageCodes(): array
|
||||
{
|
||||
return $this->userLanguages()
|
||||
->pluck('language_code')
|
||||
->toArray();
|
||||
}
|
||||
|
||||
public function assignLanguage(
|
||||
string $languageCode,
|
||||
bool $canCreate = true,
|
||||
bool $canEdit = true,
|
||||
bool $canApprove = false,
|
||||
bool $canPublish = false
|
||||
): UserLanguage {
|
||||
return $this->userLanguages()->updateOrCreate(
|
||||
['language_code' => $languageCode],
|
||||
[
|
||||
'can_create' => $canCreate,
|
||||
'can_edit' => $canEdit,
|
||||
'can_approve' => $canApprove,
|
||||
'can_publish' => $canPublish,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
public function removeLanguage(string $languageCode): bool
|
||||
{
|
||||
return $this->userLanguages()
|
||||
->where('language_code', $languageCode)
|
||||
->delete() > 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class UserLanguage extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'user_id',
|
||||
'language_code',
|
||||
'can_create',
|
||||
'can_edit',
|
||||
'can_approve',
|
||||
'can_publish',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'can_create' => 'boolean',
|
||||
'can_edit' => 'boolean',
|
||||
'can_approve' => 'boolean',
|
||||
'can_publish' => 'boolean',
|
||||
];
|
||||
|
||||
/**
|
||||
* Relationships
|
||||
*/
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
public function language(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Language::class, 'language_code', 'code');
|
||||
}
|
||||
|
||||
/**
|
||||
* Scopes
|
||||
*/
|
||||
public function scopeForUser($query, int $userId)
|
||||
{
|
||||
return $query->where('user_id', $userId);
|
||||
}
|
||||
|
||||
public function scopeForLanguage($query, string $languageCode)
|
||||
{
|
||||
return $query->where('language_code', $languageCode);
|
||||
}
|
||||
|
||||
public function scopeCanCreate($query)
|
||||
{
|
||||
return $query->where('can_create', true);
|
||||
}
|
||||
|
||||
public function scopeCanEdit($query)
|
||||
{
|
||||
return $query->where('can_edit', true);
|
||||
}
|
||||
|
||||
public function scopeCanApprove($query)
|
||||
{
|
||||
return $query->where('can_approve', true);
|
||||
}
|
||||
|
||||
public function scopeCanPublish($query)
|
||||
{
|
||||
return $query->where('can_publish', true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper Methods
|
||||
*/
|
||||
public function hasFullAccess(): bool
|
||||
{
|
||||
return $this->can_create && $this->can_edit && $this->can_approve && $this->can_publish;
|
||||
}
|
||||
|
||||
public function hasNoAccess(): bool
|
||||
{
|
||||
return !$this->can_create && !$this->can_edit && !$this->can_approve && !$this->can_publish;
|
||||
}
|
||||
|
||||
public function grantFullAccess(): bool
|
||||
{
|
||||
$this->can_create = true;
|
||||
$this->can_edit = true;
|
||||
$this->can_approve = true;
|
||||
$this->can_publish = true;
|
||||
return $this->save();
|
||||
}
|
||||
|
||||
public function revokeAllAccess(): bool
|
||||
{
|
||||
$this->can_create = false;
|
||||
$this->can_edit = false;
|
||||
$this->can_approve = false;
|
||||
$this->can_publish = false;
|
||||
return $this->save();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
<?php
|
||||
|
||||
namespace App\Policies;
|
||||
|
||||
use App\Models\Language;
|
||||
use App\Models\User;
|
||||
use Illuminate\Auth\Access\Response;
|
||||
|
||||
class LanguagePolicy
|
||||
{
|
||||
/**
|
||||
* Determine whether the user can view any models.
|
||||
*/
|
||||
public function viewAny(User $user): bool
|
||||
{
|
||||
return $user->hasRole('super_admin') || $user->can('language::viewAny');
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can view the model.
|
||||
*/
|
||||
public function view(User $user, Language $language): bool
|
||||
{
|
||||
return $user->hasRole('super_admin') || $user->can('language::view');
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can create models.
|
||||
*/
|
||||
public function create(User $user): bool
|
||||
{
|
||||
return $user->hasRole('super_admin') || $user->can('language::create');
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can update the model.
|
||||
*/
|
||||
public function update(User $user, Language $language): bool
|
||||
{
|
||||
// Cannot disable default language
|
||||
if ($language->is_default && !$user->hasRole('super_admin')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $user->hasRole('super_admin') || $user->can('language::update');
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can delete the model.
|
||||
*/
|
||||
public function delete(User $user, Language $language): bool
|
||||
{
|
||||
// Cannot delete default language
|
||||
if ($language->is_default) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $user->hasRole('super_admin') || $user->can('language::delete');
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can restore the model.
|
||||
*/
|
||||
public function restore(User $user, Language $language): bool
|
||||
{
|
||||
return $user->hasRole('super_admin') || $user->can('language::restore');
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can permanently delete the model.
|
||||
*/
|
||||
public function forceDelete(User $user, Language $language): bool
|
||||
{
|
||||
// Cannot force delete default language
|
||||
if ($language->is_default) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $user->hasRole('super_admin') || $user->can('language::forceDelete');
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can activate the language.
|
||||
*/
|
||||
public function activate(User $user): bool
|
||||
{
|
||||
return $user->hasRole('super_admin') || $user->can('language::activate');
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can set the language as default.
|
||||
*/
|
||||
public function setDefault(User $user): bool
|
||||
{
|
||||
return $user->hasRole('super_admin') || $user->can('language::setDefault');
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,9 @@
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use App\Models\Language;
|
||||
use App\Models\User;
|
||||
use App\Policies\LanguagePolicy;
|
||||
use App\Policies\UserPolicy;
|
||||
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
|
||||
use Spatie\Permission\Models\Role;
|
||||
@@ -18,6 +20,7 @@ class AuthServiceProvider extends ServiceProvider
|
||||
protected $policies = [
|
||||
User::class => UserPolicy::class,
|
||||
Role::class => RolePolicy::class,
|
||||
Language::class => LanguagePolicy::class,
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,309 @@
|
||||
<?php
|
||||
|
||||
namespace App\Traits;
|
||||
|
||||
use App\Models\Language;
|
||||
use App\Models\Translation;
|
||||
use Illuminate\Database\Eloquent\Relations\MorphMany;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
trait HasTranslations
|
||||
{
|
||||
/**
|
||||
* Get all translations for this model
|
||||
*/
|
||||
public function translations(): MorphMany
|
||||
{
|
||||
return $this->morphMany(Translation::class, 'translatable');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get translation for a specific field and language
|
||||
*/
|
||||
public function getTranslation(string $fieldName, ?string $languageCode = null, bool $published = true): ?Translation
|
||||
{
|
||||
$languageCode = $languageCode ?? $this->getCurrentLanguageCode();
|
||||
|
||||
$query = $this->translations()
|
||||
->where('field_name', $fieldName)
|
||||
->where('language_code', $languageCode);
|
||||
|
||||
if ($published) {
|
||||
$query->where('status', 'published');
|
||||
}
|
||||
|
||||
return $query->first();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get translation value for a specific field and language
|
||||
*/
|
||||
public function translate(string $fieldName, ?string $languageCode = null, bool $fallback = true): mixed
|
||||
{
|
||||
$languageCode = $languageCode ?? $this->getCurrentLanguageCode();
|
||||
|
||||
$translation = $this->getTranslation($fieldName, $languageCode);
|
||||
|
||||
if ($translation) {
|
||||
return $translation->field_value;
|
||||
}
|
||||
|
||||
// Fallback to default language
|
||||
if ($fallback && $languageCode !== $this->getDefaultLanguageCode()) {
|
||||
$defaultTranslation = $this->getTranslation($fieldName, $this->getDefaultLanguageCode());
|
||||
if ($defaultTranslation) {
|
||||
return $defaultTranslation->field_value;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to original field value
|
||||
return $this->{$fieldName} ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set translation for a specific field and language
|
||||
*/
|
||||
public function setTranslation(
|
||||
string $fieldName,
|
||||
string $languageCode,
|
||||
mixed $value,
|
||||
string $status = 'draft',
|
||||
?int $userId = null
|
||||
): Translation {
|
||||
$userId = $userId ?? auth()->id();
|
||||
|
||||
return $this->translations()->updateOrCreate(
|
||||
[
|
||||
'field_name' => $fieldName,
|
||||
'language_code' => $languageCode,
|
||||
],
|
||||
[
|
||||
'field_value' => $value,
|
||||
'status' => $status,
|
||||
'created_by' => $userId,
|
||||
'updated_by' => $userId,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all translations for a specific field
|
||||
*/
|
||||
public function getTranslations(string $fieldName, bool $published = true): Collection
|
||||
{
|
||||
$query = $this->translations()
|
||||
->where('field_name', $fieldName);
|
||||
|
||||
if ($published) {
|
||||
$query->where('status', 'published');
|
||||
}
|
||||
|
||||
return $query->get()->keyBy('language_code');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all translations for a specific language
|
||||
*/
|
||||
public function getTranslationsForLanguage(string $languageCode, bool $published = true): Collection
|
||||
{
|
||||
$query = $this->translations()
|
||||
->where('language_code', $languageCode);
|
||||
|
||||
if ($published) {
|
||||
$query->where('status', 'published');
|
||||
}
|
||||
|
||||
return $query->get()->keyBy('field_name');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if translation exists for a field and language
|
||||
*/
|
||||
public function hasTranslation(string $fieldName, string $languageCode, bool $published = true): bool
|
||||
{
|
||||
$query = $this->translations()
|
||||
->where('field_name', $fieldName)
|
||||
->where('language_code', $languageCode);
|
||||
|
||||
if ($published) {
|
||||
$query->where('status', 'published');
|
||||
}
|
||||
|
||||
return $query->exists();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get translation progress for all languages
|
||||
*/
|
||||
public function getTranslationProgress(): array
|
||||
{
|
||||
$languages = Language::active()->get();
|
||||
$translatableFields = $this->getTranslatableFields();
|
||||
$totalFields = count($translatableFields);
|
||||
|
||||
$progress = [];
|
||||
|
||||
foreach ($languages as $language) {
|
||||
$translatedCount = $this->translations()
|
||||
->where('language_code', $language->code)
|
||||
->where('status', 'published')
|
||||
->whereIn('field_name', $translatableFields)
|
||||
->count();
|
||||
|
||||
$progress[$language->code] = $totalFields > 0
|
||||
? round(($translatedCount / $totalFields) * 100)
|
||||
: 0;
|
||||
}
|
||||
|
||||
return $progress;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get missing translations for a specific language
|
||||
*/
|
||||
public function getMissingTranslations(string $languageCode): array
|
||||
{
|
||||
$translatableFields = $this->getTranslatableFields();
|
||||
|
||||
$existingTranslations = $this->translations()
|
||||
->where('language_code', $languageCode)
|
||||
->where('status', 'published')
|
||||
->pluck('field_name')
|
||||
->toArray();
|
||||
|
||||
return array_diff($translatableFields, $existingTranslations);
|
||||
}
|
||||
|
||||
/**
|
||||
* Duplicate translation from one language to another
|
||||
*/
|
||||
public function duplicateTranslation(string $fromLanguage, string $toLanguage, ?int $userId = null): int
|
||||
{
|
||||
$userId = $userId ?? auth()->id();
|
||||
$count = 0;
|
||||
|
||||
$sourceTranslations = $this->getTranslationsForLanguage($fromLanguage, false);
|
||||
|
||||
foreach ($sourceTranslations as $fieldName => $translation) {
|
||||
$this->setTranslation(
|
||||
$fieldName,
|
||||
$toLanguage,
|
||||
$translation->field_value,
|
||||
'draft',
|
||||
$userId
|
||||
);
|
||||
$count++;
|
||||
}
|
||||
|
||||
return $count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get translation status for a language
|
||||
*/
|
||||
public function getTranslationStatus(string $languageCode): string
|
||||
{
|
||||
$progress = $this->getTranslationProgress();
|
||||
$percentage = $progress[$languageCode] ?? 0;
|
||||
|
||||
if ($percentage === 100) {
|
||||
return 'complete';
|
||||
} elseif ($percentage > 0) {
|
||||
return 'partial';
|
||||
} else {
|
||||
return 'missing';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync translations for all translatable fields
|
||||
*/
|
||||
public function syncTranslations(array $languageCodes, ?int $userId = null): void
|
||||
{
|
||||
$userId = $userId ?? auth()->id();
|
||||
$translatableFields = $this->getTranslatableFields();
|
||||
|
||||
foreach ($languageCodes as $languageCode) {
|
||||
foreach ($translatableFields as $field) {
|
||||
if (!$this->hasTranslation($field, $languageCode, false)) {
|
||||
$this->setTranslation(
|
||||
$field,
|
||||
$languageCode,
|
||||
$this->{$field} ?? '',
|
||||
'draft',
|
||||
$userId
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get translatable fields for this model
|
||||
*/
|
||||
public function getTranslatableFields(): array
|
||||
{
|
||||
return $this->translatable ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current language code
|
||||
*/
|
||||
protected function getCurrentLanguageCode(): string
|
||||
{
|
||||
return app()->getLocale();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get default language code
|
||||
*/
|
||||
protected function getDefaultLanguageCode(): string
|
||||
{
|
||||
$defaultLanguage = Language::where('is_default', true)->first();
|
||||
return $defaultLanguage ? $defaultLanguage->code : config('app.locale');
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope: Filter by language
|
||||
*/
|
||||
public function scopeWhereHasTranslation($query, string $languageCode, bool $published = true)
|
||||
{
|
||||
return $query->whereHas('translations', function ($q) use ($languageCode, $published) {
|
||||
$q->where('language_code', $languageCode);
|
||||
if ($published) {
|
||||
$q->where('status', 'published');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope: Filter by user's languages
|
||||
*/
|
||||
public function scopeWhereHasTranslationForUser($query, $user)
|
||||
{
|
||||
$languageCodes = $user->userLanguages()->pluck('language_code')->toArray();
|
||||
|
||||
return $query->whereHas('translations', function ($q) use ($languageCodes) {
|
||||
$q->whereIn('language_code', $languageCodes);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get translated model for current language
|
||||
*/
|
||||
public function translated(?string $languageCode = null): self
|
||||
{
|
||||
$languageCode = $languageCode ?? $this->getCurrentLanguageCode();
|
||||
|
||||
$translations = $this->getTranslationsForLanguage($languageCode);
|
||||
|
||||
foreach ($translations as $fieldName => $translation) {
|
||||
if (in_array($fieldName, $this->getTranslatableFields())) {
|
||||
$this->{$fieldName} = $translation->field_value;
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user