Report Viewer Dashboard
This commit is contained in:
parent
9ca2b81a50
commit
82958c1872
13 changed files with 393 additions and 241 deletions
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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": "📊"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -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"), {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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 Açıklaması
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
value={formData.description}
|
||||||
|
onChange={(e) =>
|
||||||
|
setFormData((prev) => ({
|
||||||
|
...prev,
|
||||||
|
description: e.target.value,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
placeholder="Şablon hakkında detaylı açı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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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 },
|
||||||
|
|
|
||||||
|
|
@ -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) => ({
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue