Report Viewer Dashboard

This commit is contained in:
Sedat Öztürk 2025-08-15 22:39:11 +03:00
parent 9ca2b81a50
commit 82958c1872
13 changed files with 393 additions and 241 deletions

View file

@ -6,7 +6,7 @@ namespace Kurs.Platform.Reports
public class GetReportTemplatesInput : PagedAndSortedResultRequestDto public class GetReportTemplatesInput : PagedAndSortedResultRequestDto
{ {
public string Filter { get; set; } public string Filter { get; set; }
public string Category { get; set; } public string CategoryName { get; set; }
} }
public class GetGeneratedReportsInput : PagedAndSortedResultRequestDto public class GetGeneratedReportsInput : PagedAndSortedResultRequestDto

View file

@ -9,7 +9,6 @@ using Volo.Abp.Application.Dtos;
using Volo.Abp.Domain.Repositories; using Volo.Abp.Domain.Repositories;
using System.Linq.Dynamic.Core; using System.Linq.Dynamic.Core;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Volo.Abp.Domain.Entities;
namespace Kurs.Platform.Reports; namespace Kurs.Platform.Reports;
@ -56,9 +55,9 @@ public class ReportAppService : PlatformAppService, IReportAppService
); );
} }
if (!string.IsNullOrWhiteSpace(input.Category)) if (!string.IsNullOrWhiteSpace(input.CategoryName))
{ {
query = query.Where(x => x.CategoryName == input.Category); query = query.Where(x => x.CategoryName == input.CategoryName);
} }
// Toplam kayıt sayısı // Toplam kayıt sayısı
@ -127,43 +126,97 @@ public class ReportAppService : PlatformAppService, IReportAppService
public async Task<ReportTemplateDto> UpdateTemplateAsync(Guid id, UpdateReportTemplateDto input) public async Task<ReportTemplateDto> UpdateTemplateAsync(Guid id, UpdateReportTemplateDto input)
{ {
// 1) Şablonu getir ve alanlarını güncelle
var template = await _reportTemplateRepository.GetAsync(id); var template = await _reportTemplateRepository.GetAsync(id);
template.Name = input.Name; template.Name = input.Name;
template.Description = input.Description; template.Description = input.Description;
template.HtmlContent = input.HtmlContent; template.HtmlContent = input.HtmlContent;
template.CategoryName = input.CategoryName; template.CategoryName = input.CategoryName;
template.Tags = JsonSerializer.Serialize(input.Tags); template.Tags = JsonSerializer.Serialize(input.Tags ?? []);
await _reportTemplateRepository.UpdateAsync(template); // Şablonu hemen persist et (audit alanları için de iyi olur)
await _reportTemplateRepository.UpdateAsync(template, autoSave: true);
// Mevcut parametreleri sil // 2) Parametrelerde upsert + artıklarını sil
var existingParameters = await _reportParameterRepository.GetListAsync(p => p.ReportTemplateId == id); var existingParams = await _reportParameterRepository.GetListAsync(p => p.ReportTemplateId == id);
foreach (var param in existingParameters) var existingById = existingParams.ToDictionary(p => p.Id, p => p);
var inputParams = input.Parameters ?? new List<UpdateReportParameterDto>();
// Id'si olan/olmayan diye ayır
var withId = inputParams.Where(x => x.Id.HasValue).ToList();
var withoutId = inputParams.Where(x => !x.Id.HasValue).ToList();
// 2.a) Id'si olanları güncelle (varsa) ya da ekle (yoksa)
foreach (var dto in withId)
{ {
await _reportParameterRepository.DeleteAsync(param); var pid = dto.Id!.Value;
if (existingById.TryGetValue(pid, out var entity))
{
// Güncelle
entity.Name = dto.Name;
entity.Placeholder = dto.Placeholder;
entity.Type = dto.Type;
entity.Required = dto.Required;
entity.DefaultValue = dto.DefaultValue;
entity.Description = dto.Description;
await _reportParameterRepository.UpdateAsync(entity);
existingById.Remove(pid); // kalanlar silinecek listesinde kalmasın
}
else
{
// DB'de yoksa yeni ekle (istemci Id göndermiş olabilir)
var newParam = new ReportParameter(
pid,
template.Id,
dto.Name,
dto.Placeholder,
dto.Type,
dto.Required)
{
DefaultValue = dto.DefaultValue,
Description = dto.Description
};
await _reportParameterRepository.InsertAsync(newParam);
}
} }
// Yeni parametreleri ekle // 2.b) Id'siz gelenleri yeni olarak ekle
foreach (var paramDto in input.Parameters) foreach (var dto in withoutId)
{ {
var parameter = new ReportParameter( var newParam = new ReportParameter(
paramDto.Id ?? GuidGenerator.Create(), GuidGenerator.Create(),
template.Id, template.Id,
paramDto.Name, dto.Name,
paramDto.Placeholder, dto.Placeholder,
paramDto.Type, dto.Type,
paramDto.Required); dto.Required)
{
DefaultValue = dto.DefaultValue,
Description = dto.Description
};
parameter.DefaultValue = paramDto.DefaultValue; await _reportParameterRepository.InsertAsync(newParam);
parameter.Description = paramDto.Description;
await _reportParameterRepository.InsertAsync(parameter);
} }
// 2.c) Input'ta olmayan eski parametreleri sil
foreach (var leftover in existingById.Values)
{
await _reportParameterRepository.DeleteAsync(leftover);
}
// 3) Değişiklikleri tek seferde kaydet
await CurrentUnitOfWork.SaveChangesAsync();
// 4) Güncel DTO'yu dön
return await GetTemplateAsync(template.Id); return await GetTemplateAsync(template.Id);
} }
public async Task DeleteTemplateAsync(Guid id) public async Task DeleteTemplateAsync(Guid id)
{ {
await _reportTemplateRepository.DeleteAsync(id); await _reportTemplateRepository.DeleteAsync(id);

View file

@ -26002,7 +26002,7 @@
{ {
"id": "5f4d6c1f-b1e0-4f91-854c-1d59c25e7191", "id": "5f4d6c1f-b1e0-4f91-854c-1d59c25e7191",
"name": "Genel Raporlar", "name": "Genel Raporlar",
"description": "Genel Şirket içi raporlar", "description": "Şirket içi genel tüm raporlar",
"icon": "📊" "icon": "📊"
}, },
{ {

View file

@ -82,7 +82,7 @@ define(['./workbox-54d0af47'], (function (workbox) { 'use strict';
"revision": "3ca0b8505b4bec776b69afdba2768812" "revision": "3ca0b8505b4bec776b69afdba2768812"
}, { }, {
"url": "index.html", "url": "index.html",
"revision": "0.71ce98091og" "revision": "0.760g82pgmf"
}], {}); }], {});
workbox.cleanupOutdatedCaches(); workbox.cleanupOutdatedCaches();
workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), { workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), {

View file

@ -25,7 +25,7 @@ export const Dashboard: React.FC = () => {
() => ({ () => ({
id: 'tumu-category', id: 'tumu-category',
name: 'Tümü', name: 'Tümü',
description: '', description: 'Tüm kategorilere ait raporlar',
icon: '📋', icon: '📋',
}), }),
[], [],
@ -148,7 +148,7 @@ export const Dashboard: React.FC = () => {
<button <button
key={category.id} key={category.id}
onClick={() => setSelectedCategory(category)} onClick={() => setSelectedCategory(category)}
className={`w-full flex items-center space-x-3 px-4 py-3 rounded-lg text-left transition-colors ${ className={`w-full flex items-center space-x-3 px-2 py-3 rounded-lg text-left transition-colors ${
selectedCategory?.id === category.id selectedCategory?.id === category.id
? 'bg-blue-100 text-blue-700' ? 'bg-blue-100 text-blue-700'
: 'text-gray-600 hover:bg-gray-100 hover:text-gray-900' : 'text-gray-600 hover:bg-gray-100 hover:text-gray-900'
@ -174,9 +174,23 @@ export const Dashboard: React.FC = () => {
{/* Main Content */} {/* Main Content */}
<div className="flex-1"> <div className="flex-1">
<h2 className="text-2xl font-bold text-gray-900 mb-6"> <div className="flex items-center justify-between">
{translate('::App.Forum.Dashboard.Statistics')} <h2 className="text-2xl font-bold text-gray-900">
</h2> {translate('::' + selectedCategory?.name)}
</h2>
<Button
variant="solid"
onClick={handleCreateTemplate}
className="bg-blue-600 hover:bg-blue-700 font-medium px-6 py-2.5 rounded-lg shadow-md hover:shadow-lg transition-all duration-200 flex items-center space-x-2"
>
<Plus className="h-5 w-5" />
<span>Yeni Şablon</span>
</Button>
</div>
<div className="text-sm text-gray-500 ml-1 mb-4">
{translate('::' + selectedCategory?.description)}
</div>
{/* Stats */} {/* Stats */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8"> <div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
<div className="bg-white rounded-xl shadow-md p-6 border border-gray-200"> <div className="bg-white rounded-xl shadow-md p-6 border border-gray-200">
@ -219,27 +233,21 @@ export const Dashboard: React.FC = () => {
</div> </div>
{/* Filters */} {/* Filters */}
<div className="bg-white rounded-xl shadow-md p-6 mb-8 border border-gray-200"> <div className="bg-white rounded-xl shadow-md p-6 mb-8 border border-gray-200">
<div className="flex flex-col md:flex-row gap-4"> <div className="flex-1">
<div className="flex-1"> <div className="relative">
<div className="relative"> <Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400" />
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400" /> <Input
<Input placeholder="Şablon ara..."
placeholder="Şablon ara..." value={searchQuery}
value={searchQuery} onChange={(e) => setSearchQuery(e.target.value)}
onChange={(e) => setSearchQuery(e.target.value)} className="pl-10"
className="pl-10" />
/>
</div>
</div> </div>
<Button onClick={handleCreateTemplate}>
<Plus className="h-4 w-4 mr-2" />
Yeni Şablon
</Button>
</div> </div>
</div> </div>
{/* Templates Grid */} {/* Templates Grid */}
{filteredTemplates.length === 0 ? ( {filteredTemplates.length === 0 ? (
<div className="bg-white rounded-xl shadow-md p-12 border border-gray-200"> <div className="bg-white rounded-xl shadow-md p-12 border border-gray-200 text-center">
<FileText className="h-16 w-16 text-gray-400 mx-auto mb-4" /> <FileText className="h-16 w-16 text-gray-400 mx-auto mb-4" />
<h3 className="text-lg font-medium text-gray-900 mb-2"> <h3 className="text-lg font-medium text-gray-900 mb-2">
{templates.length === 0 ? 'Henüz şablon oluşturulmamış' : 'Şablon bulunamadı'} {templates.length === 0 ? 'Henüz şablon oluşturulmamış' : 'Şablon bulunamadı'}
@ -249,12 +257,6 @@ export const Dashboard: React.FC = () => {
? 'İlk rapor şablonunuzu oluşturarak başlayın.' ? 'İlk rapor şablonunuzu oluşturarak başlayın.'
: 'Arama kriterlerinize uygun şablon bulunamadı.'} : 'Arama kriterlerinize uygun şablon bulunamadı.'}
</p> </p>
{templates.length === 0 && (
<Button onClick={handleCreateTemplate}>
<Plus className="h-4 w-4 mr-2" />
İlk Şablonu Oluştur
</Button>
)}
</div> </div>
) : ( ) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
@ -282,6 +284,7 @@ export const Dashboard: React.FC = () => {
}} }}
onSave={handleSaveTemplate} onSave={handleSaveTemplate}
template={editingTemplate} template={editingTemplate}
categories={categories}
/> />
<ReportGenerator <ReportGenerator

View file

@ -49,7 +49,7 @@ export const ReportGenerator: React.FC<ReportGeneratorProps> = ({
if (reportId) { if (reportId) {
// Yeni sekmede rapor URL'sini aç // Yeni sekmede rapor URL'sini aç
const reportUrl = `/admin/report/${reportId}` const reportUrl = `/admin/reports/${reportId}`
window.open(reportUrl, '_blank') window.open(reportUrl, '_blank')
onClose() // Modal'ı kapat onClose() // Modal'ı kapat
} }
@ -78,7 +78,7 @@ export const ReportGenerator: React.FC<ReportGeneratorProps> = ({
<p className="text-gray-600 text-sm">{template.description}</p> <p className="text-gray-600 text-sm">{template.description}</p>
<div className="flex items-center mt-2 space-x-2"> <div className="flex items-center mt-2 space-x-2">
<span className="text-xs bg-blue-100 text-blue-800 px-2 py-1 rounded"> <span className="text-xs bg-blue-100 text-blue-800 px-2 py-1 rounded">
{template.category} {template.categoryName}
</span> </span>
{template.tags.map((tag) => ( {template.tags.map((tag) => (
<span key={tag} className="text-xs bg-gray-100 text-gray-800 px-2 py-1 rounded"> <span key={tag} className="text-xs bg-gray-100 text-gray-800 px-2 py-1 rounded">
@ -116,10 +116,13 @@ export const ReportGenerator: React.FC<ReportGeneratorProps> = ({
)} )}
<div className="flex justify-end space-x-3"> <div className="flex justify-end space-x-3">
<Button variant="solid" onClick={onClose} disabled={isGenerating}> <Button onClick={onClose} disabled={isGenerating}>
İptal İptal
</Button> </Button>
<Button onClick={handleGenerateAndShow} disabled={!isValid || isGenerating}> <Button variant="solid" onClick={handleGenerateAndShow} disabled={!isValid || isGenerating}
className="bg-blue-600 hover:bg-blue-700 font-medium px-2 sm:px-3 py-1.5 rounded text-xs flex items-center gap-1"
>
<FileText className="h-4 w-4 mr-2" /> <FileText className="h-4 w-4 mr-2" />
{isGenerating ? 'Oluşturuluyor...' : 'Rapor Oluştur'} {isGenerating ? 'Oluşturuluyor...' : 'Rapor Oluştur'}
</Button> </Button>

View file

@ -234,14 +234,10 @@ export const ReportViewer: React.FC = () => {
<div className="min-h-screen bg-gray-50"> <div className="min-h-screen bg-gray-50">
{/* Header - Print edilmeyecek */} {/* Header - Print edilmeyecek */}
<div className="print:hidden bg-white shadow-sm border-b border-gray-200 sticky top-0 z-10"> <div className="print:hidden bg-white shadow-sm border-b border-gray-200 sticky top-0 z-10">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> <div className="w-full px-4 sm:px-6 lg:px-8">
<div className="flex items-center justify-between h-16"> <div className="flex items-center justify-between h-16">
<div className="flex items-center space-x-4"> <div className="flex items-center space-x-4 flex-1">
<Button variant="solid" onClick={() => navigate('/')}> <div className="flex-1">
<ArrowLeft className="h-4 w-4 mr-2" />
Geri Dön
</Button>
<div>
<h1 className="text-lg font-semibold text-gray-900">{report.templateName}</h1> <h1 className="text-lg font-semibold text-gray-900">{report.templateName}</h1>
<div className="flex items-center space-x-4 text-sm text-gray-500"> <div className="flex items-center space-x-4 text-sm text-gray-500">
<span className="flex items-center"> <span className="flex items-center">
@ -257,14 +253,14 @@ export const ReportViewer: React.FC = () => {
{template && ( {template && (
<span className="flex items-center"> <span className="flex items-center">
<FileText className="h-4 w-4 mr-1" /> <FileText className="h-4 w-4 mr-1" />
{template.category} {template.categoryName}
</span> </span>
)} )}
</div> </div>
</div> </div>
</div> </div>
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2 flex-shrink-0">
<Button variant="solid" onClick={handleZoomOut} disabled={zoomLevel <= 50} size="sm"> <Button variant="solid" onClick={handleZoomOut} disabled={zoomLevel <= 50} size="sm">
<ZoomOut className="h-4 w-4" /> <ZoomOut className="h-4 w-4" />
</Button> </Button>
@ -275,11 +271,14 @@ export const ReportViewer: React.FC = () => {
<ZoomIn className="h-4 w-4" /> <ZoomIn className="h-4 w-4" />
</Button> </Button>
<div className="w-px h-6 bg-gray-300 mx-2"></div> <div className="w-px h-6 bg-gray-300 mx-2"></div>
<Button variant="solid" onClick={handleDownloadPdf}> <Button
onClick={handleDownloadPdf}
className="bg-gray-600 hover:bg-gray-700 font-medium px-2 sm:px-3 py-1.5 rounded text-xs flex items-center gap-1"
>
<Download className="h-4 w-4 mr-2" /> <Download className="h-4 w-4 mr-2" />
PDF İndir PDF İndir
</Button> </Button>
<Button onClick={handlePrint}>Yazdır</Button> <Button variant="solid" onClick={handlePrint}>Yazdır</Button>
</div> </div>
</div> </div>
</div> </div>

View file

@ -17,70 +17,79 @@ export const TemplateCard: React.FC<TemplateCardProps> = ({
onGenerate, onGenerate,
}) => { }) => {
return ( return (
<div className="bg-white rounded-xl shadow-md hover:shadow-lg transition-all duration-200 border border-gray-200"> <div className="bg-white rounded-xl shadow-md hover:shadow-lg transition-all duration-200 border border-gray-200 flex flex-col">
<div className="p-6"> <div className="p-4 flex-1 flex flex-col">
<div className="flex items-start justify-between mb-4"> {/* Header with title and parameter count */}
<div className="flex-1"> <div className="flex items-start justify-between mb-3 min-h-0">
<h3 className="text-lg font-semibold text-gray-900 mb-2">{template.name}</h3> <div className="flex-1 min-w-0 pr-3">
<p className="text-gray-600 text-sm mb-3">{template.description}</p> <h3 className="text-lg font-semibold text-gray-900 mb-2 truncate">{template.name}</h3>
<p className="text-gray-600 text-sm mb-3 line-clamp-2">{template.description}</p>
<div className="flex items-center space-x-2 mb-3">
<span className="text-xs bg-blue-100 text-blue-800 px-2 py-1 rounded-full font-medium">
{template.category}
</span>
{template.tags.slice(0, 2).map((tag) => (
<span
key={tag}
className="text-xs bg-gray-100 text-gray-600 px-2 py-1 rounded-full"
>
{tag}
</span>
))}
{template.tags.length > 2 && (
<span className="text-xs text-gray-500">+{template.tags.length - 2}</span>
)}
</div>
</div> </div>
<div className="flex items-center space-x-2"> <div className="flex items-center gap-1 flex-shrink-0 bg-gray-50 px-2 py-1 rounded-lg">
<FileText className="h-5 w-5 text-blue-500" /> <FileText className="h-4 w-4 text-blue-500" />
<span className="text-sm text-gray-500">{template.parameters.length} parametre</span> <span className="text-xs text-gray-500 whitespace-nowrap">{template.parameters.length}</span>
</div> </div>
</div> </div>
<div className="flex items-center justify-between"> {/* Tags section with proper wrapping */}
<div className="text-xs text-gray-500"> <div className="flex items-center gap-1 mb-4 flex-wrap min-h-0">
<p>Güncellenme: {template.lastModificationTime}</p> <span className="text-xs bg-blue-100 text-blue-800 px-2 py-1 rounded-full font-medium flex-shrink-0">
{template.categoryName}
</span>
{template.tags.slice(0, 2).map((tag) => (
<span
key={tag}
className="text-xs bg-gray-100 text-gray-600 px-2 py-1 rounded-full flex-shrink-0 max-w-20 truncate"
>
{tag}
</span>
))}
{template.tags.length > 2 && (
<span className="text-xs text-gray-500 flex-shrink-0">+{template.tags.length - 2}</span>
)}
</div>
{/* Footer with date and actions */}
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-2 mt-auto pt-3 border-t border-gray-100">
<div className="text-xs text-gray-500 flex-shrink-0">
<p className="truncate">{new Date(template.lastModificationTime || template.creationTime).toLocaleString('tr-TR', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
hour: '2-digit',
minute: '2-digit'
})}</p>
</div> </div>
<div className="flex items-center space-x-2"> <div className="flex items-center gap-1 flex-shrink-0">
<Button <Button
variant="solid" variant="solid"
size="sm" size="sm"
onClick={() => onGenerate(template)} onClick={() => onGenerate(template)}
className="hover:scale-105 transform transition-transform" className="bg-blue-600 hover:bg-blue-700 font-medium px-2 sm:px-3 py-1.5 rounded text-xs flex items-center gap-1"
> >
<Play className="h-4 w-4 mr-1" /> <Play className="h-3 w-3" />
Üret <span className="hidden sm:inline">Üret</span>
</Button> </Button>
<Button <Button
variant="solid" variant="solid"
size="sm" size="sm"
onClick={() => onEdit(template)} onClick={() => onEdit(template)}
className="hover:scale-105 transform transition-transform" className="bg-gray-600 hover:bg-gray-700 font-medium px-2 sm:px-3 py-1.5 rounded text-xs flex items-center gap-1"
> >
<Edit className="h-4 w-4" /> <Edit className="h-3 w-3" />
Düzenle <span className="hidden sm:inline">Düzenle</span>
</Button> </Button>
<Button <Button
variant="solid" variant="solid"
size="sm" size="sm"
onClick={() => onDelete(template.id)} onClick={() => onDelete(template.id)}
className="hover:scale-105 transform transition-transform" className="bg-red-600 hover:bg-red-700 font-medium px-2 py-1.5 rounded text-xs flex items-center"
> >
<Trash2 className="h-4 w-4" /> <Trash2 className="h-3 w-3" />
</Button> </Button>
</div> </div>
</div> </div>

View file

@ -1,14 +1,16 @@
import React, { useState, useEffect } from 'react' import React, { useState, useEffect } from 'react'
import { Save, X, FileText, Code } from 'lucide-react' import { Save, X, FileText, Code, Settings } from 'lucide-react'
import { ReportHtmlEditor } from './ReportHtmlEditor' import { ReportHtmlEditor } from './ReportHtmlEditor'
import { ReportParameterDto, ReportTemplateDto } from '@/proxy/reports/models' import { ReportParameterDto, ReportTemplateDto, ReportCategoryDto } from '@/proxy/reports/models'
import { Button, Input, Dialog } from '../ui' import { Button, Input, Dialog } from '../ui'
import { useLocalization } from '@/utils/hooks/useLocalization'
interface TemplateEditorProps { interface TemplateEditorProps {
isOpen: boolean isOpen: boolean
onClose: () => void onClose: () => void
onSave: (template: ReportTemplateDto) => Promise<void> onSave: (template: ReportTemplateDto) => Promise<void>
template?: ReportTemplateDto | null template?: ReportTemplateDto | null
categories: ReportCategoryDto[]
} }
export const TemplateEditor: React.FC<TemplateEditorProps> = ({ export const TemplateEditor: React.FC<TemplateEditorProps> = ({
@ -16,17 +18,19 @@ export const TemplateEditor: React.FC<TemplateEditorProps> = ({
onClose, onClose,
onSave, onSave,
template, template,
categories,
}) => { }) => {
const [activeTab, setActiveTab] = useState<'info' | 'content'>('info') const [activeTab, setActiveTab] = useState<'info' | 'parameters' | 'content'>('info')
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
name: '', name: '',
description: '', description: '',
htmlContent: '', htmlContent: '',
category: 'Genel', categoryName: 'Genel Raporlar',
tags: [] as string[], tags: [] as string[],
parameters: [] as ReportParameterDto[], parameters: [] as ReportParameterDto[],
}) })
const { translate } = useLocalization()
const [tagInput, setTagInput] = useState('') const [tagInput, setTagInput] = useState('')
const [isSaving, setIsSaving] = useState(false) const [isSaving, setIsSaving] = useState(false)
@ -36,7 +40,7 @@ export const TemplateEditor: React.FC<TemplateEditorProps> = ({
name: template.name, name: template.name,
description: template.description || '', description: template.description || '',
htmlContent: template.htmlContent, htmlContent: template.htmlContent,
category: template.category || 'Genel', categoryName: template.categoryName!,
tags: template.tags, tags: template.tags,
parameters: template.parameters, parameters: template.parameters,
}) })
@ -45,7 +49,7 @@ export const TemplateEditor: React.FC<TemplateEditorProps> = ({
name: '', name: '',
description: '', description: '',
htmlContent: '', htmlContent: '',
category: 'Genel', categoryName: 'Genel Raporlar',
tags: [], tags: [],
parameters: [], parameters: [],
}) })
@ -56,7 +60,7 @@ export const TemplateEditor: React.FC<TemplateEditorProps> = ({
// Otomatik parametre algılama // Otomatik parametre algılama
useEffect(() => { useEffect(() => {
const extractParameters = (htmlContent: string) => { const extractParameters = (htmlContent: string) => {
const paramRegex = /@@([A-Z_][A-Z0-9_]*)/g const paramRegex = /@@([\p{L}0-9_]+)/gu
const matches = [...htmlContent.matchAll(paramRegex)] const matches = [...htmlContent.matchAll(paramRegex)]
const uniqueParams = [...new Set(matches.map((match) => match[1]))] const uniqueParams = [...new Set(matches.map((match) => match[1]))]
@ -124,14 +128,24 @@ export const TemplateEditor: React.FC<TemplateEditorProps> = ({
})) }))
} }
const updateParameter = (paramId: string, updates: Partial<ReportParameterDto>) => {
setFormData((prev) => ({
...prev,
parameters: prev.parameters.map((param) =>
param.id === paramId ? { ...param, ...updates } : param
),
}))
}
const tabs = [ const tabs = [
{ id: 'info', label: 'Şablon Bilgileri', icon: FileText }, { id: 'info', label: translate('::Şablon Bilgileri'), icon: FileText },
{ id: 'content', label: 'HTML İçerik', icon: Code }, { id: 'parameters', label: translate('::Parametreler'), icon: Settings },
{ id: 'content', label: translate('::HTML İçerik'), icon: Code },
] ]
return ( return (
<> <>
<Dialog isOpen={isOpen} onClose={onClose} width="60%"> <Dialog isOpen={isOpen} onClose={onClose} width="100%">
<h5 className="mb-4">{template ? 'Şablon Düzenle' : 'Yeni Şablon Oluştur'}</h5> <h5 className="mb-4">{template ? 'Şablon Düzenle' : 'Yeni Şablon Oluştur'}</h5>
<div className="flex flex-col h-full"> <div className="flex flex-col h-full">
{/* Tab Navigation */} {/* Tab Navigation */}
@ -142,7 +156,7 @@ export const TemplateEditor: React.FC<TemplateEditorProps> = ({
return ( return (
<button <button
key={tab.id} key={tab.id}
onClick={() => setActiveTab(tab.id as 'info' | 'content')} onClick={() => setActiveTab(tab.id as 'info' | 'content' | 'parameters')}
className={` className={`
flex items-center py-4 px-1 border-b-2 font-medium text-sm transition-colors flex items-center py-4 px-1 border-b-2 font-medium text-sm transition-colors
${ ${
@ -163,151 +177,218 @@ export const TemplateEditor: React.FC<TemplateEditorProps> = ({
{/* Tab Content */} {/* Tab Content */}
<div className="flex-1 flex flex-col min-h-0"> <div className="flex-1 flex flex-col min-h-0">
{activeTab === 'info' && ( {activeTab === 'info' && (
<div className="overflow-y-auto flex-1 p-6"> <div className="overflow-y-auto flex-1 p-3">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 h-full"> <div className="max-w-6xl mx-auto">
<div className="space-y-4"> <div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
<Input {/* Left Column - Basic Info */}
autoFocus <div className="space-y-6">
value={formData.name} <div>
onChange={(e) => <label className="block text-sm font-medium text-gray-700 mb-2">
setFormData((prev) => ({ Şablon Adı
...prev, </label>
name: e.target.value,
}))
}
placeholder="Rapor şablonu adı"
className="text-left"
/>
<Input
value={formData.description}
onChange={(e) =>
setFormData((prev) => ({
...prev,
description: e.target.value,
}))
}
placeholder="Şablon açıklaması"
textArea={true}
rows={3}
className="text-left"
/>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Kategori
</label>
<select
value={formData.category}
onChange={(e) =>
setFormData((prev) => ({
...prev,
category: e.target.value,
}))
}
className="block w-full px-3 py-2 border border-gray-300 rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
>
<option value="Genel">Genel</option>
<option value="Finansal">Finansal</option>
<option value="İnsan Kaynakları">İnsan Kaynakları</option>
<option value="Satış">Satış</option>
<option value="Operasyonel">Operasyonel</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Etiketler
</label>
<div className="flex space-x-2 mb-2">
<input <input
type="text" autoFocus
value={tagInput} value={formData.name}
onChange={(e) => setTagInput(e.target.value)} onChange={(e) =>
onKeyPress={(e) => e.key === 'Enter' && addTag()} setFormData((prev) => ({
placeholder="Etiket ekle..." ...prev,
className="flex-1 px-3 py-2 border border-gray-300 rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500" name: e.target.value,
}))
}
placeholder="Rapor şablonu adı"
className="w-full flex-1 px-3 py-2 border border-gray-300 rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
/> />
<Button type="button" onClick={addTag} size="sm">
Ekle
</Button>
</div> </div>
<div className="flex flex-wrap gap-2">
{formData.tags.map((tag) => (
<span
key={tag}
className="inline-flex items-center px-2 py-1 text-sm bg-blue-100 text-blue-800 rounded-full"
>
{tag}
<button
onClick={() => removeTag(tag)}
className="ml-1 text-blue-600 hover:text-blue-800"
>
<X className="h-3 w-3" />
</button>
</span>
))}
</div>
</div>
</div>
<div className="bg-gray-50 rounded-lg p-4 flex flex-col"> <div>
<h3 className="font-medium text-gray-900 mb-4 flex-shrink-0"> <label className="block text-sm font-medium text-gray-700 mb-2">
Algılanan Parametreler Kategori
</h3> </label>
{formData.parameters.length === 0 ? ( <select
<p className="text-gray-500 text-sm"> value={formData.categoryName}
HTML içeriğinde @@PARAMETRE formatında parametreler kullandığınızda burada onChange={(e) =>
görünecek. setFormData((prev) => ({
</p> ...prev,
) : ( categoryName: e.target.value,
<div className="flex-1 overflow-y-auto max-h-80"> }))
<div className="grid grid-cols-1 gap-3 pr-2"> }
{formData.parameters.map((param) => ( className="block w-full px-3 py-2 border border-gray-300 rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
<div >
key={param.id} {categories.map((category) => (
className="bg-white p-4 rounded-lg border border-gray-200 shadow-sm hover:shadow-md transition-shadow" <option key={category.id} value={category.name}>
{category.name}
</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Etiketler
</label>
<div className="flex space-x-2 mb-3">
<input
type="text"
value={tagInput}
onChange={(e) => setTagInput(e.target.value)}
onKeyPress={(e) => e.key === 'Enter' && addTag()}
placeholder="Etiket ekle..."
className="flex-1 px-3 py-2 border border-gray-300 rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
/>
<Button type="button" onClick={addTag} size="sm">
Ekle
</Button>
</div>
<div className="flex flex-wrap gap-2">
{formData.tags.map((tag) => (
<span
key={tag}
className="inline-flex items-center px-3 py-1 text-sm bg-blue-100 text-blue-800 rounded-full"
> >
<div className="flex items-start justify-between mb-2"> {tag}
<div className="flex items-center space-x-2"> <button
<div className="w-2 h-2 bg-blue-500 rounded-full"></div> onClick={() => removeTag(tag)}
<code className="text-sm font-mono text-blue-700 bg-blue-50 px-2 py-1 rounded"> className="ml-2 text-blue-600 hover:text-blue-800"
@@{param.name} >
</code> <X className="h-3 w-3" />
</div> </button>
<span className="text-xs bg-gray-100 text-gray-600 px-2 py-1 rounded-full"> </span>
{param.type}
</span>
</div>
<p className="text-sm text-gray-600 text-left">{param.description}</p>
</div>
))} ))}
</div> </div>
</div> </div>
)} </div>
{/* Right Column - Description */}
<div className="space-y-6">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Şablon ıklaması
</label>
<Input
value={formData.description}
onChange={(e) =>
setFormData((prev) => ({
...prev,
description: e.target.value,
}))
}
placeholder="Şablon hakkında detaylııklama yazın..."
textArea={true}
rows={12}
className="text-left h-full"
/>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
)} )}
{activeTab === 'content' && ( {activeTab === 'content' && (
<div className="flex-1 flex flex-col p-6 min-h-0"> <div className="flex-1 flex flex-col min-h-0">
<ReportHtmlEditor <div className="flex-1 p-3 min-h-0">
value={formData.htmlContent} <ReportHtmlEditor
onChange={(content) => setFormData((prev) => ({ ...prev, htmlContent: content }))} value={formData.htmlContent}
height="80%" onChange={(content) =>
/> setFormData((prev) => ({ ...prev, htmlContent: content }))
}
height="50vh"
/>
</div>
</div>
)}
{activeTab === 'parameters' && (
<div className="overflow-y-auto flex-1 p-6">
<div className="max-w-full mx-auto">
{formData.parameters.length === 0 ? (
<div className="text-center py-12">
<Settings className="h-16 w-16 text-gray-400 mx-auto mb-4" />
<p className="text-gray-500 text-lg mb-2">Henüz parametre algılanmadı</p>
<p className="text-gray-400 text-sm">
HTML içeriğinde @@PARAMETRE formatında parametreler kullandığınızda burada
görünecek.
</p>
</div>
) : (
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
{formData.parameters.map((param) => (
<div
key={param.id}
className="bg-white p-3 rounded-lg border border-gray-200 shadow-sm hover:shadow-md transition-shadow min-w-0"
>
<div className="flex items-start justify-between mb-2 min-w-0">
<div className="flex items-center gap-1 min-w-0 flex-1">
<div className="w-2 h-2 bg-blue-500 rounded-full flex-shrink-0"></div>
<code className="text-xs font-mono text-blue-700 bg-blue-50 px-1.5 py-0.5 rounded truncate">
@@{param.name}
</code>
</div>
<select
value={param.type}
onChange={(e) => updateParameter(param.id, { type: e.target.value as any })}
className="text-xs bg-gray-100 text-gray-600 px-1.5 py-0.5 rounded-full flex-shrink-0 ml-1 border-none outline-none cursor-pointer"
>
<option value="text">text</option>
<option value="number">number</option>
<option value="date">date</option>
<option value="select">select</option>
<option value="checkbox">checkbox</option>
</select>
</div>
<div className="mb-2">
<input
type="text"
value={param.description || ''}
onChange={(e) => updateParameter(param.id, { description: e.target.value })}
placeholder="Parametre açıklaması"
className="w-full text-xs text-gray-600 bg-transparent border-none outline-none resize-none"
/>
</div>
<div className="mb-2">
<input
type="text"
value={param.defaultValue || ''}
onChange={(e) => updateParameter(param.id, { defaultValue: e.target.value })}
placeholder="Varsayılan değer"
className="w-full text-xs bg-gray-50 px-1.5 py-0.5 rounded border border-gray-200 outline-none"
/>
</div>
<div className="flex items-center gap-2">
<label className="flex items-center gap-1 cursor-pointer">
<input
type="checkbox"
checked={param.required}
onChange={(e) => updateParameter(param.id, { required: e.target.checked })}
className="w-3 h-3 text-red-600 rounded border-gray-300 focus:ring-red-500"
/>
<span className="text-xs text-gray-600">Zorunlu</span>
</label>
</div>
</div>
))}
</div>
)}
</div>
</div> </div>
)} )}
</div> </div>
{/* Tab Footer */} {/* Tab Footer */}
<div className="flex justify-between"> <div className="flex justify-between">
<Button variant="solid" onClick={onClose} disabled={isSaving}> <Button variant="default" onClick={onClose} disabled={isSaving}>
İptal İptal
</Button> </Button>
<Button onClick={handleSave} disabled={isSaving}>
<Save className="h-4 w-4 mr-2" /> <Button
variant="solid"
onClick={handleSave}
className="bg-blue-600 hover:bg-blue-700 font-medium px-2 py-2 rounded-lg shadow-md hover:shadow-lg transition-all duration-200 flex items-center space-x-2"
>
<Save className="h-5 w-5" />
{isSaving ? 'Kaydediliyor...' : template ? 'Güncelle' : 'Kaydet'} {isSaving ? 'Kaydediliyor...' : template ? 'Güncelle' : 'Kaydet'}
</Button> </Button>
</div> </div>

View file

@ -68,7 +68,7 @@ const FooterContent = () => {
export default function Footer({ pageContainerType = 'contained' }: FooterProps) { export default function Footer({ pageContainerType = 'contained' }: FooterProps) {
return ( return (
<footer <footer
className={classNames(`footer flex flex-auto items-center h-6 ${PAGE_CONTAINER_GUTTER_X}`)} className={classNames(`print:hidden footer flex flex-auto items-center h-6 ${PAGE_CONTAINER_GUTTER_X}`)}
> >
{pageContainerType === 'contained' ? ( {pageContainerType === 'contained' ? (
<Container> <Container>

View file

@ -95,7 +95,7 @@ export interface GetReportTemplatesInput {
maxResultCount?: number maxResultCount?: number
sorting?: string sorting?: string
filter?: string filter?: string
category?: string categoryName?: string
} }
export interface GetReportsGeneratedInput { export interface GetReportsGeneratedInput {

View file

@ -37,6 +37,8 @@ export class ReportsService {
sorting: input.sorting, sorting: input.sorting,
skipCount: input.skipCount, skipCount: input.skipCount,
maxResultCount: input.maxResultCount, maxResultCount: input.maxResultCount,
filter: input.filter,
categoryName: input.categoryName,
}, },
}, },
{ apiName: this.apiName }, { apiName: this.apiName },

View file

@ -92,6 +92,8 @@ export const useReports = () => {
const currentTemplateResponse = await reportsService.getTemplateById(id) const currentTemplateResponse = await reportsService.getTemplateById(id)
const currentTemplate = currentTemplateResponse.data as ReportTemplateDto const currentTemplate = currentTemplateResponse.data as ReportTemplateDto
console.log('Current Template:', currentTemplate)
const updatedTemplate = { ...currentTemplate, ...updates } const updatedTemplate = { ...currentTemplate, ...updates }
await reportsService.updateTemplate(id, updatedTemplate) await reportsService.updateTemplate(id, updatedTemplate)
@ -196,7 +198,7 @@ export const useReports = () => {
sorting: '', sorting: '',
skipCount: 0, skipCount: 0,
maxResultCount: 1000, maxResultCount: 1000,
category: categoryName && categoryName !== 'Tümü' ? categoryName : undefined, categoryName: categoryName && categoryName !== 'Tümü' ? categoryName : undefined,
}) })
setData((prevData) => ({ setData((prevData) => ({