diff --git a/api/src/Kurs.Platform.Application.Contracts/Reports/GetReportsInput.cs b/api/src/Kurs.Platform.Application.Contracts/Reports/GetReportsInput.cs index fa7c44f1..adb82091 100644 --- a/api/src/Kurs.Platform.Application.Contracts/Reports/GetReportsInput.cs +++ b/api/src/Kurs.Platform.Application.Contracts/Reports/GetReportsInput.cs @@ -6,7 +6,7 @@ namespace Kurs.Platform.Reports public class GetReportTemplatesInput : PagedAndSortedResultRequestDto { public string Filter { get; set; } - public string Category { get; set; } + public string CategoryName { get; set; } } public class GetGeneratedReportsInput : PagedAndSortedResultRequestDto diff --git a/api/src/Kurs.Platform.Application/Reports/ReportAppService.cs b/api/src/Kurs.Platform.Application/Reports/ReportAppService.cs index bdd85733..3b05f7f0 100644 --- a/api/src/Kurs.Platform.Application/Reports/ReportAppService.cs +++ b/api/src/Kurs.Platform.Application/Reports/ReportAppService.cs @@ -9,7 +9,6 @@ using Volo.Abp.Application.Dtos; using Volo.Abp.Domain.Repositories; using System.Linq.Dynamic.Core; using Microsoft.EntityFrameworkCore; -using Volo.Abp.Domain.Entities; 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ı @@ -127,43 +126,97 @@ public class ReportAppService : PlatformAppService, IReportAppService public async Task UpdateTemplateAsync(Guid id, UpdateReportTemplateDto input) { + // 1) Şablonu getir ve alanlarını güncelle var template = await _reportTemplateRepository.GetAsync(id); template.Name = input.Name; template.Description = input.Description; template.HtmlContent = input.HtmlContent; 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 - var existingParameters = await _reportParameterRepository.GetListAsync(p => p.ReportTemplateId == id); - foreach (var param in existingParameters) + // 2) Parametrelerde upsert + artıklarını sil + var existingParams = await _reportParameterRepository.GetListAsync(p => p.ReportTemplateId == id); + var existingById = existingParams.ToDictionary(p => p.Id, p => p); + + var inputParams = input.Parameters ?? new List(); + + // 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 - foreach (var paramDto in input.Parameters) + // 2.b) Id'siz gelenleri yeni olarak ekle + foreach (var dto in withoutId) { - var parameter = new ReportParameter( - paramDto.Id ?? GuidGenerator.Create(), + var newParam = new ReportParameter( + GuidGenerator.Create(), template.Id, - paramDto.Name, - paramDto.Placeholder, - paramDto.Type, - paramDto.Required); + dto.Name, + dto.Placeholder, + dto.Type, + dto.Required) + { + DefaultValue = dto.DefaultValue, + Description = dto.Description + }; - parameter.DefaultValue = paramDto.DefaultValue; - parameter.Description = paramDto.Description; - - await _reportParameterRepository.InsertAsync(parameter); + await _reportParameterRepository.InsertAsync(newParam); } + // 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); } + public async Task DeleteTemplateAsync(Guid id) { await _reportTemplateRepository.DeleteAsync(id); diff --git a/api/src/Kurs.Platform.DbMigrator/Seeds/SeederData.json b/api/src/Kurs.Platform.DbMigrator/Seeds/SeederData.json index 443e0827..4bac6d8d 100644 --- a/api/src/Kurs.Platform.DbMigrator/Seeds/SeederData.json +++ b/api/src/Kurs.Platform.DbMigrator/Seeds/SeederData.json @@ -26002,7 +26002,7 @@ { "id": "5f4d6c1f-b1e0-4f91-854c-1d59c25e7191", "name": "Genel Raporlar", - "description": "Genel Şirket içi raporlar", + "description": "Şirket içi genel tüm raporlar", "icon": "📊" }, { diff --git a/ui/dev-dist/sw.js b/ui/dev-dist/sw.js index 67568d52..62336dc8 100644 --- a/ui/dev-dist/sw.js +++ b/ui/dev-dist/sw.js @@ -82,7 +82,7 @@ define(['./workbox-54d0af47'], (function (workbox) { 'use strict'; "revision": "3ca0b8505b4bec776b69afdba2768812" }, { "url": "index.html", - "revision": "0.71ce98091og" + "revision": "0.760g82pgmf" }], {}); workbox.cleanupOutdatedCaches(); workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), { diff --git a/ui/src/components/reports/Dashboard.tsx b/ui/src/components/reports/Dashboard.tsx index 482d907c..23edd2ec 100644 --- a/ui/src/components/reports/Dashboard.tsx +++ b/ui/src/components/reports/Dashboard.tsx @@ -25,7 +25,7 @@ export const Dashboard: React.FC = () => { () => ({ id: 'tumu-category', name: 'Tümü', - description: '', + description: 'Tüm kategorilere ait raporlar', icon: '📋', }), [], @@ -148,7 +148,7 @@ export const Dashboard: React.FC = () => { + +
+ {translate('::' + selectedCategory?.description)} +
+ {/* Stats */}
@@ -219,27 +233,21 @@ export const Dashboard: React.FC = () => {
{/* Filters */}
-
-
-
- - setSearchQuery(e.target.value)} - className="pl-10" - /> -
+
+
+ + setSearchQuery(e.target.value)} + className="pl-10" + />
-
{/* Templates Grid */} {filteredTemplates.length === 0 ? ( -
+

{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.' : 'Arama kriterlerinize uygun şablon bulunamadı.'}

- {templates.length === 0 && ( - - )}

) : (
@@ -282,6 +284,7 @@ export const Dashboard: React.FC = () => { }} onSave={handleSaveTemplate} template={editingTemplate} + categories={categories} /> = ({ if (reportId) { // Yeni sekmede rapor URL'sini aç - const reportUrl = `/admin/report/${reportId}` + const reportUrl = `/admin/reports/${reportId}` window.open(reportUrl, '_blank') onClose() // Modal'ı kapat } @@ -78,7 +78,7 @@ export const ReportGenerator: React.FC = ({

{template.description}

- {template.category} + {template.categoryName} {template.tags.map((tag) => ( @@ -116,10 +116,13 @@ export const ReportGenerator: React.FC = ({ )}
- - diff --git a/ui/src/components/reports/ReportViewer.tsx b/ui/src/components/reports/ReportViewer.tsx index 309162c2..7b9e2c95 100644 --- a/ui/src/components/reports/ReportViewer.tsx +++ b/ui/src/components/reports/ReportViewer.tsx @@ -234,14 +234,10 @@ export const ReportViewer: React.FC = () => {
{/* Header - Print edilmeyecek */}
-
+
-
- -
+
+

{report.templateName}

@@ -257,14 +253,14 @@ export const ReportViewer: React.FC = () => { {template && ( - {template.category} + {template.categoryName} )}
-
+
@@ -275,11 +271,14 @@ export const ReportViewer: React.FC = () => {
- - +
diff --git a/ui/src/components/reports/TemplateCard.tsx b/ui/src/components/reports/TemplateCard.tsx index b509775b..93178100 100644 --- a/ui/src/components/reports/TemplateCard.tsx +++ b/ui/src/components/reports/TemplateCard.tsx @@ -17,70 +17,79 @@ export const TemplateCard: React.FC = ({ onGenerate, }) => { return ( -
-
-
-
-

{template.name}

-

{template.description}

- -
- - {template.category} - - {template.tags.slice(0, 2).map((tag) => ( - - {tag} - - ))} - {template.tags.length > 2 && ( - +{template.tags.length - 2} - )} -
+
+
+ {/* Header with title and parameter count */} +
+
+

{template.name}

+

{template.description}

-
- - {template.parameters.length} parametre +
+ + {template.parameters.length}
-
-
-

Güncellenme: {template.lastModificationTime}

+ {/* Tags section with proper wrapping */} +
+ + {template.categoryName} + + {template.tags.slice(0, 2).map((tag) => ( + + {tag} + + ))} + {template.tags.length > 2 && ( + +{template.tags.length - 2} + )} +
+ + {/* Footer with date and actions */} +
+
+

{new Date(template.lastModificationTime || template.creationTime).toLocaleString('tr-TR', { + day: '2-digit', + month: '2-digit', + year: 'numeric', + hour: '2-digit', + minute: '2-digit' + })}

-
+
diff --git a/ui/src/components/reports/TemplateEditor.tsx b/ui/src/components/reports/TemplateEditor.tsx index c10763d8..327bee2d 100644 --- a/ui/src/components/reports/TemplateEditor.tsx +++ b/ui/src/components/reports/TemplateEditor.tsx @@ -1,14 +1,16 @@ 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 { ReportParameterDto, ReportTemplateDto } from '@/proxy/reports/models' +import { ReportParameterDto, ReportTemplateDto, ReportCategoryDto } from '@/proxy/reports/models' import { Button, Input, Dialog } from '../ui' +import { useLocalization } from '@/utils/hooks/useLocalization' interface TemplateEditorProps { isOpen: boolean onClose: () => void onSave: (template: ReportTemplateDto) => Promise template?: ReportTemplateDto | null + categories: ReportCategoryDto[] } export const TemplateEditor: React.FC = ({ @@ -16,17 +18,19 @@ export const TemplateEditor: React.FC = ({ onClose, onSave, template, + categories, }) => { - const [activeTab, setActiveTab] = useState<'info' | 'content'>('info') + const [activeTab, setActiveTab] = useState<'info' | 'parameters' | 'content'>('info') const [formData, setFormData] = useState({ name: '', description: '', htmlContent: '', - category: 'Genel', + categoryName: 'Genel Raporlar', tags: [] as string[], parameters: [] as ReportParameterDto[], }) + const { translate } = useLocalization() const [tagInput, setTagInput] = useState('') const [isSaving, setIsSaving] = useState(false) @@ -36,7 +40,7 @@ export const TemplateEditor: React.FC = ({ name: template.name, description: template.description || '', htmlContent: template.htmlContent, - category: template.category || 'Genel', + categoryName: template.categoryName!, tags: template.tags, parameters: template.parameters, }) @@ -45,7 +49,7 @@ export const TemplateEditor: React.FC = ({ name: '', description: '', htmlContent: '', - category: 'Genel', + categoryName: 'Genel Raporlar', tags: [], parameters: [], }) @@ -56,7 +60,7 @@ export const TemplateEditor: React.FC = ({ // Otomatik parametre algılama useEffect(() => { 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 uniqueParams = [...new Set(matches.map((match) => match[1]))] @@ -124,14 +128,24 @@ export const TemplateEditor: React.FC = ({ })) } + const updateParameter = (paramId: string, updates: Partial) => { + setFormData((prev) => ({ + ...prev, + parameters: prev.parameters.map((param) => + param.id === paramId ? { ...param, ...updates } : param + ), + })) + } + const tabs = [ - { id: 'info', label: 'Şablon Bilgileri', icon: FileText }, - { id: 'content', label: 'HTML İçerik', icon: Code }, + { id: 'info', label: translate('::Şablon Bilgileri'), icon: FileText }, + { id: 'parameters', label: translate('::Parametreler'), icon: Settings }, + { id: 'content', label: translate('::HTML İçerik'), icon: Code }, ] return ( <> - +
{template ? 'Şablon Düzenle' : 'Yeni Şablon Oluştur'}
{/* Tab Navigation */} @@ -142,7 +156,7 @@ export const TemplateEditor: React.FC = ({ return (
-
- {formData.tags.map((tag) => ( - - {tag} - - - ))} -
-
-
-
-

- Algılanan Parametreler -

- {formData.parameters.length === 0 ? ( -

- HTML içeriğinde @@PARAMETRE formatında parametreler kullandığınızda burada - görünecek. -

- ) : ( -
-
- {formData.parameters.map((param) => ( -
+ + +
+ +
+ +
+ 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" + /> + +
+
+ {formData.tags.map((tag) => ( + -
-
-
- - @@{param.name} - -
- - {param.type} - -
-

{param.description}

-
+ {tag} + + ))}
- )} +
+ + {/* Right Column - Description */} +
+
+ + + 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" + /> +
+
)} {activeTab === 'content' && ( -
- setFormData((prev) => ({ ...prev, htmlContent: content }))} - height="80%" - /> +
+
+ + setFormData((prev) => ({ ...prev, htmlContent: content })) + } + height="50vh" + /> +
+
+ )} + + {activeTab === 'parameters' && ( +
+
+ {formData.parameters.length === 0 ? ( +
+ +

Henüz parametre algılanmadı

+

+ HTML içeriğinde @@PARAMETRE formatında parametreler kullandığınızda burada + görünecek. +

+
+ ) : ( +
+ {formData.parameters.map((param) => ( +
+
+
+
+ + @@{param.name} + +
+ +
+ +
+ 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" + /> +
+ +
+ 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" + /> +
+ +
+ +
+
+ ))} +
+ )} +
)}
{/* Tab Footer */}
- -
diff --git a/ui/src/components/template/Footer.tsx b/ui/src/components/template/Footer.tsx index 9f82c2c2..12703940 100644 --- a/ui/src/components/template/Footer.tsx +++ b/ui/src/components/template/Footer.tsx @@ -68,7 +68,7 @@ const FooterContent = () => { export default function Footer({ pageContainerType = 'contained' }: FooterProps) { return (