Implement Site Translations API and DataGrid View: Introduced a new API controller for managing site translations, including methods for listing, storing, updating, and deleting translations. Added a DataGrid view for the site translations page, allowing for enhanced user interaction and management of translations. Implemented session-based view toggling between traditional table and DataGrid formats, improving usability. Localization support has been integrated for all text elements, ensuring a seamless experience for users across different languages.

This commit is contained in:
Ümit Tunç
2026-01-10 21:54:56 +03:00
parent 2fdf187658
commit e80c94146d
4 changed files with 730 additions and 1 deletions
@@ -5,17 +5,54 @@ namespace App\Filament\Admin\Resources\SiteTranslations\Pages;
use App\Filament\Admin\Resources\SiteTranslations\SiteTranslationResource;
use Filament\Actions\CreateAction;
use Filament\Resources\Pages\ListRecords;
use Filament\Actions\Action;
use Illuminate\Contracts\View\View;
class ListSiteTranslations extends ListRecords
{
protected static string $resource = SiteTranslationResource::class;
public bool $useDataGrid = false;
public function mount(): void
{
parent::mount();
// Session'dan görünüm tercihini al, yoksa varsayılan olarak false (eski görünüm)
$this->useDataGrid = session('site_translations_use_datagrid', false);
}
protected function getHeaderActions(): array
{
return [
CreateAction::make(),
Action::make('toggleView')
->label(fn() => $this->useDataGrid ? 'Tablo Görünümü' : 'DataGrid Görünümü')
->icon(fn() => $this->useDataGrid ? 'heroicon-o-table-cells' : 'heroicon-o-squares-2x2')
->color('gray')
->action(function () {
$this->useDataGrid = !$this->useDataGrid;
session(['site_translations_use_datagrid' => $this->useDataGrid]);
// Sayfayı yenile ki getContent() yeniden çağrılsın
return redirect()->to($this->getResource()::getUrl('index'));
}),
CreateAction::make()
->visible(fn() => !$this->useDataGrid), // Sadece eski görünümde göster
];
}
public function getContent(): View
{
// Session'dan direkt oku ki her render'da güncel değeri alsın
$useDataGrid = session('site_translations_use_datagrid', false);
if ($useDataGrid) {
return view('filament.admin.resources.site-translations.datagrid');
}
// Eski Filament table görünümü
return parent::getContent();
}
}
@@ -0,0 +1,292 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\SiteTranslation;
use App\Models\Language;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Validator;
class SiteTranslationController extends Controller
{
/**
* Display a listing of the resource.
*/
public function index(Request $request): JsonResponse
{
$query = SiteTranslation::query();
// Search
if ($request->has('search')) {
$search = $request->get('search');
$query->where(function ($q) use ($search) {
$q->where('key', 'like', "%{$search}%")
->orWhereRaw("JSON_SEARCH(translations, 'one', ?, NULL, '$[*]') IS NOT NULL", ["%{$search}%"]);
});
}
// Sorting
$sortField = $request->get('sort', 'id');
$sortOrder = $request->get('order', 'desc');
$query->orderBy($sortField, $sortOrder);
// Pagination
$page = $request->get('page', 1);
$pageSize = $request->get('pageSize', 20);
$totalCount = $query->count();
$items = $query->skip(($page - 1) * $pageSize)->take($pageSize)->get();
// Format data for DataGrid
$data = $items->map(function ($item) {
$formatted = [
'id' => $item->id,
'key' => $item->key,
'hash' => $item->hash,
'created_at' => $item->created_at?->toDateTimeString(),
'updated_at' => $item->updated_at?->toDateTimeString(),
];
// Add translation fields for each active language
$languages = Language::where('is_active', true)->get();
$translations = $item->translations ?? [];
foreach ($languages as $language) {
$formatted[$language->code] = $translations[$language->code] ?? '';
}
return $formatted;
});
return response()->json([
'data' => $data,
'totalCount' => $totalCount,
]);
}
/**
* Store a newly created resource in storage.
*/
public function store(Request $request): JsonResponse
{
$validator = Validator::make($request->all(), [
'key' => 'required|string|max:500',
]);
if ($validator->fails()) {
return response()->json([
'success' => false,
'errors' => $validator->errors(),
], 422);
}
$hash = md5($request->key);
// Check if translation already exists
$exists = SiteTranslation::where('hash', $hash)->first();
if ($exists) {
return response()->json([
'success' => false,
'message' => 'Bu key zaten mevcut.',
], 409);
}
$languages = Language::where('is_active', true)->get();
$translations = [];
foreach ($languages as $language) {
if ($request->has($language->code)) {
$translations[$language->code] = $request->get($language->code, '');
}
}
$translation = SiteTranslation::create([
'hash' => $hash,
'key' => $request->key,
'translations' => $translations,
]);
Cache::forget("site_translation_{$hash}");
return response()->json([
'success' => true,
'data' => $translation->load('translations'),
'message' => 'Çeviri başarıyla oluşturuldu.',
], 201);
}
/**
* Update the specified resource in storage.
*/
public function update(Request $request, $id): JsonResponse
{
$translation = SiteTranslation::findOrFail($id);
$validator = Validator::make($request->all(), [
'key' => 'sometimes|required|string|max:500',
]);
if ($validator->fails()) {
return response()->json([
'success' => false,
'errors' => $validator->errors(),
], 422);
}
$languages = Language::where('is_active', true)->get();
$translations = $translation->translations ?? [];
// Update translations
foreach ($languages as $language) {
if ($request->has($language->code)) {
$translations[$language->code] = $request->get($language->code, '');
}
}
// Update key if provided
if ($request->has('key') && $request->key !== $translation->key) {
$translation->key = $request->key;
$translation->hash = md5($request->key);
}
$translation->translations = $translations;
$translation->save();
Cache::forget("site_translation_{$translation->hash}");
return response()->json([
'success' => true,
'data' => $translation->fresh(),
'message' => 'Çeviri başarıyla güncellendi.',
]);
}
/**
* Batch update multiple resources.
*/
public function batchUpdate(Request $request): JsonResponse
{
$validator = Validator::make($request->all(), [
'changes' => 'required|array',
]);
if ($validator->fails()) {
return response()->json([
'success' => false,
'errors' => $validator->fails(),
], 422);
}
$changes = $request->get('changes', []);
$updated = [];
$errors = [];
foreach ($changes as $change) {
try {
// ID can be in 'key' field (integer) or directly in 'key' field
$id = is_numeric($change['key'] ?? null)
? (int)$change['key']
: ($change['key']['id'] ?? null);
if (!$id) {
$errors[] = 'ID bulunamadı.';
continue;
}
$translation = SiteTranslation::find($id);
if (!$translation) {
$errors[] = "ID {$id} için kayıt bulunamadı.";
continue;
}
$data = $change['data'] ?? [];
$languages = Language::where('is_active', true)->get();
$translations = $translation->translations ?? [];
// Update translations
foreach ($languages as $language) {
if (isset($data[$language->code])) {
$translations[$language->code] = $data[$language->code];
}
}
// Update key if provided
if (isset($data['key']) && $data['key'] !== $translation->key) {
$translation->key = $data['key'];
$translation->hash = md5($data['key']);
}
$translation->translations = $translations;
$translation->save();
Cache::forget("site_translation_{$translation->hash}");
$updated[] = $translation->id;
} catch (\Exception $e) {
$id = is_numeric($change['key'] ?? null)
? (int)$change['key']
: ($change['key']['id'] ?? 'unknown');
$errors[] = "ID {$id} için hata: " . $e->getMessage();
}
}
return response()->json([
'success' => count($errors) === 0,
'updated' => $updated,
'errors' => $errors,
'message' => count($updated) . ' kayıt güncellendi.',
]);
}
/**
* Remove the specified resource from storage.
*/
public function destroy($id): JsonResponse
{
$translation = SiteTranslation::findOrFail($id);
$hash = $translation->hash;
$translation->delete();
Cache::forget("site_translation_{$hash}");
return response()->json([
'success' => true,
'message' => 'Çeviri başarıyla silindi.',
]);
}
/**
* Batch delete multiple resources.
*/
public function batchDestroy(Request $request): JsonResponse
{
$validator = Validator::make($request->all(), [
'ids' => 'required|array',
'ids.*' => 'required|integer|exists:site_translations,id',
]);
if ($validator->fails()) {
return response()->json([
'success' => false,
'errors' => $validator->errors(),
], 422);
}
$ids = $request->get('ids', []);
$translations = SiteTranslation::whereIn('id', $ids)->get();
foreach ($translations as $translation) {
Cache::forget("site_translation_{$translation->hash}");
}
SiteTranslation::whereIn('id', $ids)->delete();
return response()->json([
'success' => true,
'message' => count($ids) . ' kayıt başarıyla silindi.',
]);
}
}
@@ -0,0 +1,389 @@
<x-filament-panels::page>
@php
$languages = \App\Models\Language::where('is_active', true)->get();
$apiUrl = route('api.site-translations.index');
$apiStoreUrl = route('api.site-translations.store');
$apiUpdateUrl = route('api.site-translations.update', ['id' => '__ID__']);
$apiDeleteUrl = route('api.site-translations.destroy', ['id' => '__ID__']);
$apiBatchUpdateUrl = route('api.site-translations.batch-update');
$apiBatchDeleteUrl = route('api.site-translations.batch-destroy');
// CSRF token
$csrfToken = csrf_token();
@endphp
<div id="site-translations-datagrid" style="margin-top: 1rem;">
<div style="padding: 20px; text-align: center; color: #666;">
<p>DataGrid yükleniyor...</p>
</div>
</div>
<style>
#site-translations-datagrid {
width: 100%;
height: calc(100vh - 250px);
min-height: 600px;
}
.dx-datagrid-headers {
background-color: var(--fi-primary-50, #f0f9ff);
}
.dx-state-focused {
border-color: var(--fi-primary-600, #0284c7);
}
.dx-button.dx-button-success {
background-color: var(--fi-success-600, #16a34a);
border-color: var(--fi-success-600, #16a34a);
}
</style>
<link rel="stylesheet" href="https://cdn3.devexpress.com/jslib/23.1.5/css/dx.light.css" />
<script src="https://cdn3.devexpress.com/jslib/23.1.5/js/dx.all.js"></script>
<script>
(function() {
function initializeDataGrid() {
// DOM element'inin hazır olduğunu kontrol et
const gridElement = document.getElementById('site-translations-datagrid');
if (!gridElement) {
console.error('DataGrid container bulunamadı!');
setTimeout(initializeDataGrid, 100);
return;
}
// DevExtreme'in yüklendiğini kontrol et
if (typeof DevExpress === 'undefined') {
console.error('DevExtreme yüklenemedi!');
gridElement.innerHTML =
'<div style="padding: 20px; color: red;">DevExtreme kütüphanesi yüklenemedi. Lütfen sayfayı yenileyin.</div>';
return;
}
console.log('DevExtreme yüklendi, DataGrid başlatılıyor...');
const languages = @json($languages->map(fn($lang) => ['code' => $lang->code, 'name' => $lang->name]));
const apiUrl = @json($apiUrl);
const apiStoreUrl = @json($apiStoreUrl);
const apiUpdateUrl = @json($apiUpdateUrl);
const apiDeleteUrl = @json($apiDeleteUrl);
const apiBatchUpdateUrl = @json($apiBatchUpdateUrl);
const apiBatchDeleteUrl = @json($apiBatchDeleteUrl);
const csrfToken = @json($csrfToken);
// Column definitions
const columns = [
{
dataField: 'key',
caption: 'Key / Orijinal Metin',
width: 300,
allowEditing: true,
editorType: 'dxTextArea',
editorOptions: {
height: 60
},
validationRules: [{ type: 'required', message: 'Key gereklidir' }]
}
];
// Add language columns
languages.forEach(lang => {
columns.push({
dataField: lang.code,
caption: `${lang.name} (${lang.code})`,
allowEditing: true,
width: 250,
editorType: 'dxTextArea',
editorOptions: {
height: 80,
maxHeight: 200
}
});
});
columns.push({
dataField: 'created_at',
caption: 'Oluşturulma',
dataType: 'datetime',
allowEditing: false,
width: 150,
visible: false
});
columns.push({
dataField: 'updated_at',
caption: 'Güncellenme',
dataType: 'datetime',
allowEditing: false,
width: 150,
visible: false
});
// Helper function for API calls
function apiCall(url, method, data) {
const options = {
method: method,
headers: {
'X-CSRF-TOKEN': csrfToken,
'Accept': 'application/json',
'Content-Type': 'application/json'
}
};
if (data) {
options.body = JSON.stringify(data);
}
return fetch(url, options)
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
})
.catch(error => {
console.error('API Error:', error);
return { success: false, message: error.message };
});
}
// DataGrid configuration
const dataGrid = new DevExpress.data.DataSource({
load: function(loadOptions) {
const params = new URLSearchParams();
// Pagination
if (loadOptions.skip !== undefined && loadOptions.take) {
params.append('page', Math.floor(loadOptions.skip / loadOptions.take) + 1);
params.append('pageSize', loadOptions.take);
} else {
params.append('page', '1');
params.append('pageSize', '20');
}
// Sorting
if (loadOptions.sort) {
loadOptions.sort.forEach(sort => {
params.append('sort', sort.selector || sort);
params.append('order', sort.desc ? 'desc' : 'asc');
});
}
// Search
if (loadOptions.searchValue) {
params.append('search', loadOptions.searchValue);
}
const requestUrl = apiUrl + '?' + params.toString();
console.log('DataGrid loading data from:', requestUrl);
return apiCall(requestUrl, 'GET')
.then(function(response) {
console.log('DataGrid API response:', response);
if (!response || !response.data) {
console.error('Invalid API response:', response);
return {
data: [],
totalCount: 0
};
}
return {
data: response.data || [],
totalCount: response.totalCount || 0
};
})
.catch(function(error) {
console.error('DataGrid load error:', error);
DevExpress.ui.notify('Veri yüklenirken hata oluştu: ' + (error.message || 'Bilinmeyen hata'), 'error', 5000);
return {
data: [],
totalCount: 0
};
});
}
});
const dataGridInstance = new DevExpress.ui.dxDataGrid(gridElement, {
dataSource: dataGrid,
columns: columns,
editing: {
mode: 'batch',
allowAdding: true,
allowUpdating: true,
allowDeleting: true,
useIcons: true,
texts: {
addRow: 'Yeni Ekle',
saveAllChanges: 'Tüm Değişiklikleri Kaydet',
cancelAllChanges: 'İptal',
confirmDeleteMessage: 'Bu kaydı silmek istediğinizden emin misiniz?'
}
},
searchPanel: {
visible: true,
width: 300,
placeholder: 'Ara...'
},
headerFilter: {
visible: true
},
filterRow: {
visible: true
},
paging: {
pageSize: 20,
pageIndex: 0
},
pager: {
showPageSizeSelector: true,
allowedPageSizes: [10, 20, 50, 100],
showInfo: true,
showNavigationButtons: true
},
sorting: {
mode: 'multiple'
},
showBorders: true,
rowAlternationEnabled: true,
hoverStateEnabled: true,
columnAutoWidth: true,
allowColumnResizing: true,
columnResizingMode: 'widget',
wordWrapEnabled: true,
onSaving: function(e) {
e.cancel = true;
const changes = e.changes || [];
const insertPromises = [];
const updateChanges = [];
const deletePromises = [];
// Separate changes by type
changes.forEach(function(change) {
if (change.type === 'insert') {
const requestData = {
key: change.data.key || ''
};
languages.forEach(lang => {
if (change.data[lang.code] !== undefined) {
requestData[lang.code] = change.data[lang.code] || '';
}
});
insertPromises.push(apiCall(apiStoreUrl, 'POST', requestData));
} else if (change.type === 'update') {
const oldData = change.key;
const newData = change.data;
const requestData = {
key: newData.key !== undefined ? newData.key : oldData.key
};
languages.forEach(lang => {
if (newData[lang.code] !== undefined) {
requestData[lang.code] = newData[lang.code] || '';
} else if (oldData[lang.code] !== undefined) {
requestData[lang.code] = oldData[lang.code] || '';
}
});
updateChanges.push({
key: oldData.id,
data: requestData
});
} else if (change.type === 'remove') {
deletePromises.push(
apiCall(apiDeleteUrl.replace('__ID__', change.key.id), 'DELETE')
);
}
});
// Handle batch updates
let updatePromise = Promise.resolve({ success: true, updated: [] });
if (updateChanges.length > 0) {
updatePromise = apiCall(apiBatchUpdateUrl, 'POST', {
changes: updateChanges
});
}
// Execute all promises
Promise.all([
Promise.all(insertPromises),
updatePromise,
Promise.all(deletePromises)
]).then(function(results) {
const [insertResults, updateResult, deleteResults] = results;
let successCount = 0;
let errorCount = 0;
let errorMessages = [];
insertResults.forEach(function(result) {
if (result.success) {
successCount++;
} else {
errorCount++;
if (result.message) errorMessages.push(result.message);
}
});
if (updateResult.success) {
successCount += updateResult.updated ? updateResult.updated.length : 0;
if (updateResult.errors && updateResult.errors.length > 0) {
errorCount += updateResult.errors.length;
errorMessages = errorMessages.concat(updateResult.errors);
}
} else {
errorCount++;
if (updateResult.message) errorMessages.push(updateResult.message);
}
deleteResults.forEach(function(result) {
if (result.success) {
successCount++;
} else {
errorCount++;
if (result.message) errorMessages.push(result.message);
}
});
if (errorCount === 0) {
DevExpress.ui.notify(
`${successCount} işlem başarıyla tamamlandı`,
'success',
3000
);
e.component.refresh();
} else {
const message = errorMessages.length > 0
? errorMessages.join('; ')
: `${errorCount} işlem başarısız oldu`;
DevExpress.ui.notify(message, 'error', 5000);
e.component.refresh();
}
}).catch(function(error) {
DevExpress.ui.notify(
'İşlem sırasında hata oluştu: ' + (error.message || 'Bilinmeyen hata'),
'error',
5000
);
e.component.refresh();
});
}
});
console.log('DataGrid başlatıldı:', dataGridInstance);
}
// Sayfa yüklendiğinde başlat
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', function() {
setTimeout(initializeDataGrid, 500);
});
} else {
// DOM zaten hazır, DevExtreme yüklenene kadar bekle
setTimeout(initializeDataGrid, 500);
}
})();
</script>
</x-filament-panels::page>
+11
View File
@@ -4,6 +4,7 @@ use Illuminate\Support\Facades\Route;
use App\Http\Controllers\PageController;
use App\Http\Controllers\BlogController;
use App\Http\Controllers\TemplatePreviewController;
use App\Http\Controllers\Api\SiteTranslationController;
use App\Models\Page;
// Debug Route - Veritabanı kontrolü
@@ -85,6 +86,16 @@ Route::any('/admin/template-preview', [TemplatePreviewController::class, 'previe
->middleware(['auth'])
->name('template.preview');
// Site Translations API (DevExtreme DataGrid için)
Route::prefix('admin/api')->middleware(['auth'])->group(function () {
Route::get('/site-translations', [SiteTranslationController::class, 'index'])->name('api.site-translations.index');
Route::post('/site-translations', [SiteTranslationController::class, 'store'])->name('api.site-translations.store');
Route::put('/site-translations/{id}', [SiteTranslationController::class, 'update'])->name('api.site-translations.update');
Route::delete('/site-translations/{id}', [SiteTranslationController::class, 'destroy'])->name('api.site-translations.destroy');
Route::post('/site-translations/batch-update', [SiteTranslationController::class, 'batchUpdate'])->name('api.site-translations.batch-update');
Route::post('/site-translations/batch-destroy', [SiteTranslationController::class, 'batchDestroy'])->name('api.site-translations.batch-destroy');
});
// Products & Services
Route::get('/urun-hizmet/{slug}', [\App\Http\Controllers\ProductController::class, 'show'])->name('products.show');