diff --git a/api/src/Kurs.Platform.Application.Contracts/Activity/ActivityDto.cs b/api/src/Kurs.Platform.Application.Contracts/Activity/ActivityDto.cs index 85f5bf1d..ff098715 100644 --- a/api/src/Kurs.Platform.Application.Contracts/Activity/ActivityDto.cs +++ b/api/src/Kurs.Platform.Application.Contracts/Activity/ActivityDto.cs @@ -4,7 +4,7 @@ using Volo.Abp.Content; namespace Kurs.Platform.Entities; -public class ActivityDto : EntityDto +public class ActivityDto : FullAuditedEntityDto { public Guid? TenantId { get; set; } public string EntityName { get; set; } @@ -12,7 +12,8 @@ public class ActivityDto : EntityDto public string Type { get; set; } public string Subject { get; set; } public string Content { get; set; } - public string FilesJson { get; set; } - + public string FilesJson { get; set; } + public string CreateUserName { get; set; } + public IRemoteStreamContent[] Files { get; set; } } diff --git a/api/src/Kurs.Platform.Application.Contracts/Activity/ActivityFileDto.cs b/api/src/Kurs.Platform.Application.Contracts/Activity/ActivityFileDto.cs new file mode 100644 index 00000000..d0ef80d5 --- /dev/null +++ b/api/src/Kurs.Platform.Application.Contracts/Activity/ActivityFileDto.cs @@ -0,0 +1,10 @@ +namespace Kurs.Platform.Activities; + +public class ActivityFileDto +{ + public string FileName { get; set; } // Dosya adı + public string FileType { get; set; } // MIME tipi + public long FileSize { get; set; } // Boyut (byte) + public string SavedFileName { get; set; } // Blob üzerinde kaydedilen dosya adı + public string FileBase64 { get; set; } // Dosya içeriği +} diff --git a/api/src/Kurs.Platform.Application.Contracts/Activity/ActivityListRequestDto.cs b/api/src/Kurs.Platform.Application.Contracts/Activity/ActivityListRequestDto.cs new file mode 100644 index 00000000..7d9ad135 --- /dev/null +++ b/api/src/Kurs.Platform.Application.Contracts/Activity/ActivityListRequestDto.cs @@ -0,0 +1,7 @@ +using Volo.Abp.Application.Dtos; + +public class ActivityListRequestDto : PagedAndSortedResultRequestDto + { + public string EntityName { get; set; } + public string EntityId { get; set; } + } \ No newline at end of file diff --git a/api/src/Kurs.Platform.Application/Activity/ActivityAppService.cs b/api/src/Kurs.Platform.Application/Activity/ActivityAppService.cs index aadde1de..3ff8a13b 100644 --- a/api/src/Kurs.Platform.Application/Activity/ActivityAppService.cs +++ b/api/src/Kurs.Platform.Application/Activity/ActivityAppService.cs @@ -1,13 +1,22 @@ using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Linq.Dynamic.Core; +using System.Text.Json; using System.Threading.Tasks; using Kurs.Platform.BlobStoring; using Kurs.Platform.Entities; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using Volo.Abp; using Volo.Abp.Application.Dtos; using Volo.Abp.Application.Services; using Volo.Abp.BlobStoring; using Volo.Abp.Domain.Repositories; +using Volo.Abp.Identity; namespace Kurs.Platform.Activities; @@ -16,16 +25,19 @@ public class ActivityAppService : CrudAppService< Activity, ActivityDto, Guid, - PagedAndSortedResultRequestDto> + ActivityListRequestDto> { - private readonly IBlobContainer ActivityBlobContainer; + private readonly IBlobContainer _activityBlobContainer; + private readonly IRepository _repositoryUser; public ActivityAppService( IRepository repo, - IBlobContainer activityBlobContainer + IBlobContainer activityBlobContainer, + IRepository repositoryUser ) : base(repo) { - ActivityBlobContainer = activityBlobContainer; + _activityBlobContainer = activityBlobContainer; + _repositoryUser = repositoryUser; // CreatePolicyName = $"{AppCodes.Listforms.Listform}.Create"; // UpdatePolicyName = $"{AppCodes.Listforms.Listform}.Update"; @@ -38,8 +50,143 @@ public class ActivityAppService : CrudAppService< // } } - public override Task CreateAsync([FromForm] ActivityDto input) + public async Task GetDownloadAsync(string savedFileName) { - return base.CreateAsync(input); + if (string.IsNullOrWhiteSpace(savedFileName)) + throw new UserFriendlyException("Dosya adı geçersiz"); + + var stream = await _activityBlobContainer.GetAsync(savedFileName); + if (stream == null) + throw new UserFriendlyException("Dosya bulunamadı"); + + var activities = await Repository.GetListAsync(); + var fileDto = activities + .SelectMany(a => JsonSerializer.Deserialize>(a.FilesJson ?? "[]")) + .FirstOrDefault(f => f.SavedFileName == savedFileName); + + using var ms = new MemoryStream(); + await stream.CopyToAsync(ms); + var bytes = ms.ToArray(); + + // Base64 olarak encode ediyoruz + return new ActivityFileDto + { + FileName = fileDto?.FileName ?? savedFileName, + FileType = fileDto?.FileType ?? "application/octet-stream", + FileBase64 = Convert.ToBase64String(bytes) // <-- byte[] yerine Base64 string + }; + } + + public override async Task> GetListAsync(ActivityListRequestDto input) + { + // 1️⃣ Filtreleme + var query = await Repository.GetQueryableAsync(); + + if (!string.IsNullOrWhiteSpace(input.EntityName)) + query = query.Where(a => a.EntityName == input.EntityName); + + if (!string.IsNullOrWhiteSpace(input.EntityId)) + query = query.Where(a => a.EntityId == input.EntityId); + + // 2️⃣ Sıralama (opsiyonel) + if (!string.IsNullOrWhiteSpace(input.Sorting)) + query = query.OrderBy(input.Sorting); + else + query = query.OrderByDescending(a => a.CreationTime); + + // 3️⃣ Paging + var totalCount = await AsyncExecuter.CountAsync(query); + var activities = await AsyncExecuter.ToListAsync( + query.Skip(input.SkipCount).Take(input.MaxResultCount) + ); + + // 4️⃣ Kullanıcı bilgilerini al + var userIds = activities.Select(a => a.CreatorId).Distinct().ToList(); + var userQueryable = await _repositoryUser.GetQueryableAsync(); + var users = await userQueryable + .Where(u => userIds.Contains(u.Id)) + .Select(u => new { u.Id, u.UserName }) + .ToListAsync(); + + // 5️⃣ DTO map ve kullanıcı adı ekleme + var activityDtos = activities.Select(a => + { + var dto = ObjectMapper.Map(a); + dto.CreateUserName = users.FirstOrDefault(u => u.Id == a.CreatorId)?.UserName ?? "Unknown"; + return dto; + }).ToList(); + + // 6️⃣ Sonuç dön + return new PagedResultDto(totalCount, activityDtos); + } + + + public override async Task CreateAsync([FromForm] ActivityDto input) + { + var fileDtos = new List(); + + if (input.Files != null && input.Files.Length > 0) + { + foreach (var file in input.Files) + { + await using var stream = file.GetStream(); + var savedFileName = $"{Guid.NewGuid()}_{file.FileName}"; + + await _activityBlobContainer.SaveAsync( + savedFileName, + stream, + true + ); + + // Dosya bilgisini DTO olarak ekle + fileDtos.Add(new ActivityFileDto + { + FileName = file.FileName, + FileType = file.ContentType, + FileSize = file.ContentLength ?? 0, + SavedFileName = savedFileName + }); + } + } + + input.FilesJson = JsonSerializer.Serialize(fileDtos); + + var dto = await base.CreateAsync(input); + + dto.CreateUserName = CurrentUser.UserName; + + return dto; + } + + public override async Task DeleteAsync(Guid id) + { + // Önce entity'i alıyoruz + var activity = await Repository.GetAsync(id); + + // if (!string.IsNullOrEmpty(activity.FilesJson)) + // { + // try + // { + // // FilesJson içindeki dosya bilgilerini deserialize ediyoruz + // var files = JsonSerializer.Deserialize>(activity.FilesJson); + + // if (files != null) + // { + // foreach (var file in files) + // { + // // Blob storage'dan sil + // await _activityBlobContainer.DeleteAsync(file.SavedFileName, cancellationToken: default); + // } + // } + // } + // catch (Exception ex) + // { + // // Opsiyonel: silme sırasında hata loglayabilirsin + // Logger.LogWarning(ex, "Blob dosyaları silinirken hata oluştu."); + // } + // } + + // Sonra veritabanındaki kaydı sil + await base.DeleteAsync(id); } } diff --git a/ui/src/proxy/activity/models.ts b/ui/src/proxy/activity/models.ts index 41987ed1..6447ed92 100644 --- a/ui/src/proxy/activity/models.ts +++ b/ui/src/proxy/activity/models.ts @@ -1,11 +1,21 @@ -import { AuditedEntityDto } from "../abp"; +import { FullAuditedEntityDto } from '../abp' -export interface ActivityDto extends AuditedEntityDto { - tenantId?: string; - entityName: string; - entityId: string; - type: string; - subject: string; - content: string; - files: File[]; +export interface ActivityDto extends FullAuditedEntityDto { + tenantId?: string + entityName: string + entityId: string + type: string + subject: string + content: string + filesJson?: string + createUserName: string + files: File[] } + +export interface ActivityFileDto { + fileName: string + fileType: string + fileSize: number + savedFileName: string + fileBase64: string +} \ No newline at end of file diff --git a/ui/src/services/activity.service.ts b/ui/src/services/activity.service.ts index 8559cfa1..a29baf8d 100644 --- a/ui/src/services/activity.service.ts +++ b/ui/src/services/activity.service.ts @@ -1,5 +1,5 @@ import { PagedResultDto } from '@/proxy' -import { ActivityDto } from '@/proxy/activity/models' +import { ActivityDto, ActivityFileDto } from '@/proxy/activity/models' import apiService from '@/services/api.service' import { AxiosError } from 'axios' @@ -21,35 +21,13 @@ class ActivityService { return response.data } - async create(data: ActivityDto): Promise { + async create(data: FormData): Promise { try { - const formData = new FormData() - formData.append('entityName', data.entityName) - formData.append('entityId', data.entityId) - formData.append('type', data.type) - formData.append('subject', data.subject) - formData.append('content', data.content) - - // Ensure files are actual File objects, not blob URLs - if (data.files && data.files.length > 0) { - for (const file of data.files) { - // Ensure file is a File object (in case it's a Blob URL, you need to get the actual file) - if (file instanceof File) { - formData.append('files', file) - } - } - } - - console.log('FormData:', formData) - - // Make the POST request const response = await apiService.fetchData({ url: '/api/app/activity', method: 'POST', - data: formData, - // Don't manually set the Content-Type header; let the browser handle it + data, // FormData olduğu için headers belirtmeye gerek yok }) - return response.data } catch (error) { if (error instanceof AxiosError) { @@ -77,6 +55,27 @@ class ActivityService { method: 'DELETE', }) } + + async downloadFile(savedFileName: string, fileName: string, fileType: string) { + const response = await apiService.fetchData({ + url: `/api/app/activity/download?savedFileName=${savedFileName}`, + method: 'GET', + }) + + const base64 = response.data.fileBase64 + const binary = atob(base64) // base64 -> binary string + const bytes = new Uint8Array(binary.length) + + for (let i = 0; i < binary.length; i++) { + bytes[i] = binary.charCodeAt(i) + } + + const blob = new Blob([bytes], { type: response.data.fileType }) + const link = document.createElement('a') + link.href = URL.createObjectURL(blob) + link.download = response.data.fileName || fileName + link.click() + } } export const activityService = new ActivityService() diff --git a/ui/src/views/form/FormActivityPanel/ActivityList.tsx b/ui/src/views/form/FormActivityPanel/ActivityList.tsx index 125ffd19..82873e0d 100644 --- a/ui/src/views/form/FormActivityPanel/ActivityList.tsx +++ b/ui/src/views/form/FormActivityPanel/ActivityList.tsx @@ -1,15 +1,9 @@ import React from 'react' -import { - FaStickyNote, - FaFileAlt, - FaEnvelope, - FaTrash, - FaDownload, - FaUser, - FaClock, -} from 'react-icons/fa' -import { Button } from '@/components/ui' +import { FaStickyNote, FaEnvelope, FaTrash, FaDownload, FaClock, FaPaperclip } from 'react-icons/fa' +import { Avatar, Button } from '@/components/ui' import { ActivityDto } from '@/proxy/activity/models' +import { AVATAR_URL } from '@/constants/app.constant' +import { useStoreState } from '@/store/store' interface ActivityListProps { activities: ActivityDto[] @@ -22,105 +16,125 @@ export const ActivityList: React.FC = ({ onDeleteActivity, onDownloadFile, }) => { - const getActivityIcon = (type: string) => { + const user = useStoreState((state) => state.auth.user) + + const getActivityStyle = (type: string) => { switch (type) { case 'note': - return + return { + icon: , + border: 'border-yellow-400', + } case 'message': - return + return { + icon: , + border: 'border-green-400', + } default: - return + return { + icon: , + border: 'border-gray-300', + } } } - // const handleDelete = (activity: ActivityDto) => { - // onDeleteActivity?.(activity) - // } - - const handleDownloadFile = (fileData: any) => { - onDownloadFile?.(fileData) - } - - if (activities.length === 0) { + if (activities.length === 0) return (

Henüz hiçbir aktivite bulunmuyor

) - } return ( -
- {activities.map((activity) => ( -
-
-
{getActivityIcon(activity.type)}
+
+
+ {activities.map((activity, index) => { + const files = activity.filesJson ? JSON.parse(activity.filesJson) : [] + const creationDate = activity.creationTime ? new Date(activity.creationTime) : null + const { icon, border } = getActivityStyle(activity.type) -
-
- - {activity.creatorId} + return ( +
+ {/* Timeline Düğmesi */} +
+ {icon}
- {activity.subject && ( -

- {activity.subject} -

- )} +
+ {/* Header */} +
+
+
+ + {activity.createUserName} +
+ {creationDate && ( +
+ {creationDate.toLocaleString()} +
+ )} +
- {activity.content && ( -

{activity.content}

- )} + {/* Sil butonu */} + {user?.id === activity.creatorId && ( + + )} +
- {/* Note tipinde dosyaları göster */} - {/* {activity.type === 'note' && activity.data && (activity.data as any).attachedFiles?.length > 0 && ( -
- {((activity.data as any).attachedFiles || []).map((file: any, index: number) => ( -
-
- - {file.fileName} - ({(file.fileSize / 1024).toFixed(1)} KB) -
+ {/* Body */} +
+ {activity.subject && ( +

{activity.subject}

+ )} + {activity.content && ( +
+ )} +
+ + {/* Files */} + {files.length > 0 && ( +
+ {files.map((file: any, index: number) => ( +
+ + {file.FileName}
))} -
- )} */} - -
-
- - {activity.creationTime?.toLocaleString()} -
- -
- -
+
+ )}
-
-
- ))} + ) + })} +
) } diff --git a/ui/src/views/form/FormActivityPanel/ActivityModal.tsx b/ui/src/views/form/FormActivityPanel/ActivityModal.tsx index 38c02ac8..a85743d0 100644 --- a/ui/src/views/form/FormActivityPanel/ActivityModal.tsx +++ b/ui/src/views/form/FormActivityPanel/ActivityModal.tsx @@ -1,174 +1,265 @@ import React, { useState } from 'react' -import { Button, Input, Dialog, Select, FormContainer, FormItem, Upload } from '@/components/ui' -import { FaFileUpload, FaStickyNote, FaPlus, FaTrash, FaPaperclip } from 'react-icons/fa' -import { Field, Form, Formik } from 'formik' -import { SelectBoxOption } from '@/shared/types' +import { Button, Input, Dialog, FormContainer, FormItem, Upload, Radio } from '@/components/ui' +import { FaFileAlt, FaFileUpload, FaPlus, FaTrash } from 'react-icons/fa' +import { Field, FieldProps, Form, Formik } from 'formik' import * as Yup from 'yup' import { activityService } from '@/services/activity.service' +import { + sizeValues, + fontSizeOptions, + fontValues, + fontFamilyOptions, + headerValues, + headerOptions, +} from '@/proxy/reports/data' +import { HtmlEditor, ImageUpload, Item, MediaResizing, Toolbar } from 'devextreme-react/html-editor' -// Validation schema const validationSchema = Yup.object({ type: Yup.string().required('Not tipi zorunludur'), subject: Yup.string().required('Konu zorunludur'), content: Yup.string().required('İçerik zorunludur'), }) -interface AddContentModalProps { +interface ActivityModalProps { entityName: string entityId: string isOpen: boolean onClose: () => void + onActivityAdded?: (activity: any) => void } -export const AddContentModal: React.FC = ({ +export const ActivityModal: React.FC = ({ entityName, entityId, isOpen, onClose, + onActivityAdded, }) => { const [uploading, setUploading] = useState(false) - const [files, setFiles] = useState() + const [fileList, setFileList] = useState([]) - const types: SelectBoxOption[] = [ + const types = [ { value: 'note', label: 'Not' }, { value: 'message', label: 'Mesaj' }, { value: 'activity', label: 'Aktivite' }, ] - // onSaveContent güncelleme const handleSave = async (values: any) => { setUploading(true) try { - // API çağrısı - await activityService.create({ ...values, entityName, entityId, files }) + const formData = new FormData() + formData.append('entityName', entityName) + formData.append('entityId', entityId) + formData.append('type', values.type) + formData.append('subject', values.subject) + formData.append('content', values.content) + fileList.forEach((file) => formData.append('Files', file)) - //onClose() // Modal'ı kapat - } catch (error) { - console.error('Save failed:', error) + const createdActivity = await activityService.create(formData) + if (onActivityAdded) onActivityAdded(createdActivity) + setFileList([]) + onClose() + } catch (err) { + console.error(err) } finally { setUploading(false) } } - const beforeUpload = (files: FileList | null, fileList: File[]) => { - let valid: string | boolean = true - - const maxFileSize = 2000000 - - if (fileList.length >= 1) { - return `Sadece bir dosya seçebilirsiniz` + const beforeUpload = (files: FileList | null) => { + if (!files) return true + for (const f of Array.from(files)) { + if (f.size > 2 * 1024 * 1024) return 'En fazla 2MB dosya yükleyebilirsiniz' } - - if (files) { - for (const f of files) { - if (f.size >= maxFileSize) { - valid = 'En fazla 2mb dosya yükleyebilirsiniz' - } - } - } - - return valid + return true } - const onChooseImage = async (file: File[]) => { - if (file.length === 0) { - setFiles(undefined) - return - } - - setFiles(file.map((f) => URL.createObjectURL(f))) + const removeFile = (index: number) => { + setFileList((prev) => prev.filter((_, i) => i !== index)) } return ( - +
-
+ {/* Başlık */} +

- Not Ekle + Not / Aktivite Ekle

{({ values, touched, errors, setFieldValue, isSubmitting }) => (
- {/* Not Tipi */} + {/* NOT TİPİ */} - - {({ field }: any) => ( -