ActivityAppService ve Dosya Upload işlemi
This commit is contained in:
parent
6f91454ae8
commit
8564bff367
9 changed files with 583 additions and 340 deletions
|
|
@ -4,7 +4,7 @@ using Volo.Abp.Content;
|
||||||
|
|
||||||
namespace Kurs.Platform.Entities;
|
namespace Kurs.Platform.Entities;
|
||||||
|
|
||||||
public class ActivityDto : EntityDto<Guid>
|
public class ActivityDto : FullAuditedEntityDto<Guid>
|
||||||
{
|
{
|
||||||
public Guid? TenantId { get; set; }
|
public Guid? TenantId { get; set; }
|
||||||
public string EntityName { get; set; }
|
public string EntityName { get; set; }
|
||||||
|
|
@ -12,7 +12,8 @@ public class ActivityDto : EntityDto<Guid>
|
||||||
public string Type { get; set; }
|
public string Type { get; set; }
|
||||||
public string Subject { get; set; }
|
public string Subject { get; set; }
|
||||||
public string Content { 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; }
|
public IRemoteStreamContent[] Files { get; set; }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
using Volo.Abp.Application.Dtos;
|
||||||
|
|
||||||
|
public class ActivityListRequestDto : PagedAndSortedResultRequestDto
|
||||||
|
{
|
||||||
|
public string EntityName { get; set; }
|
||||||
|
public string EntityId { get; set; }
|
||||||
|
}
|
||||||
|
|
@ -1,13 +1,22 @@
|
||||||
using System;
|
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 System.Threading.Tasks;
|
||||||
using Kurs.Platform.BlobStoring;
|
using Kurs.Platform.BlobStoring;
|
||||||
using Kurs.Platform.Entities;
|
using Kurs.Platform.Entities;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Volo.Abp;
|
||||||
using Volo.Abp.Application.Dtos;
|
using Volo.Abp.Application.Dtos;
|
||||||
using Volo.Abp.Application.Services;
|
using Volo.Abp.Application.Services;
|
||||||
using Volo.Abp.BlobStoring;
|
using Volo.Abp.BlobStoring;
|
||||||
using Volo.Abp.Domain.Repositories;
|
using Volo.Abp.Domain.Repositories;
|
||||||
|
using Volo.Abp.Identity;
|
||||||
|
|
||||||
namespace Kurs.Platform.Activities;
|
namespace Kurs.Platform.Activities;
|
||||||
|
|
||||||
|
|
@ -16,16 +25,19 @@ public class ActivityAppService : CrudAppService<
|
||||||
Activity,
|
Activity,
|
||||||
ActivityDto,
|
ActivityDto,
|
||||||
Guid,
|
Guid,
|
||||||
PagedAndSortedResultRequestDto>
|
ActivityListRequestDto>
|
||||||
{
|
{
|
||||||
private readonly IBlobContainer<ActivityBlobContainer> ActivityBlobContainer;
|
private readonly IBlobContainer<ActivityBlobContainer> _activityBlobContainer;
|
||||||
|
private readonly IRepository<IdentityUser, Guid> _repositoryUser;
|
||||||
|
|
||||||
public ActivityAppService(
|
public ActivityAppService(
|
||||||
IRepository<Activity, Guid> repo,
|
IRepository<Activity, Guid> repo,
|
||||||
IBlobContainer<ActivityBlobContainer> activityBlobContainer
|
IBlobContainer<ActivityBlobContainer> activityBlobContainer,
|
||||||
|
IRepository<IdentityUser, Guid> repositoryUser
|
||||||
) : base(repo)
|
) : base(repo)
|
||||||
{
|
{
|
||||||
ActivityBlobContainer = activityBlobContainer;
|
_activityBlobContainer = activityBlobContainer;
|
||||||
|
_repositoryUser = repositoryUser;
|
||||||
|
|
||||||
// CreatePolicyName = $"{AppCodes.Listforms.Listform}.Create";
|
// CreatePolicyName = $"{AppCodes.Listforms.Listform}.Create";
|
||||||
// UpdatePolicyName = $"{AppCodes.Listforms.Listform}.Update";
|
// UpdatePolicyName = $"{AppCodes.Listforms.Listform}.Update";
|
||||||
|
|
@ -38,8 +50,143 @@ public class ActivityAppService : CrudAppService<
|
||||||
// }
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
public override Task<ActivityDto> CreateAsync([FromForm] ActivityDto input)
|
public async Task<ActivityFileDto> 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<List<ActivityFileDto>>(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<PagedResultDto<ActivityDto>> 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<Activity, ActivityDto>(a);
|
||||||
|
dto.CreateUserName = users.FirstOrDefault(u => u.Id == a.CreatorId)?.UserName ?? "Unknown";
|
||||||
|
return dto;
|
||||||
|
}).ToList();
|
||||||
|
|
||||||
|
// 6️⃣ Sonuç dön
|
||||||
|
return new PagedResultDto<ActivityDto>(totalCount, activityDtos);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public override async Task<ActivityDto> CreateAsync([FromForm] ActivityDto input)
|
||||||
|
{
|
||||||
|
var fileDtos = new List<ActivityFileDto>();
|
||||||
|
|
||||||
|
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<List<ActivityFileDto>>(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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,21 @@
|
||||||
import { AuditedEntityDto } from "../abp";
|
import { FullAuditedEntityDto } from '../abp'
|
||||||
|
|
||||||
export interface ActivityDto extends AuditedEntityDto {
|
export interface ActivityDto extends FullAuditedEntityDto {
|
||||||
tenantId?: string;
|
tenantId?: string
|
||||||
entityName: string;
|
entityName: string
|
||||||
entityId: string;
|
entityId: string
|
||||||
type: string;
|
type: string
|
||||||
subject: string;
|
subject: string
|
||||||
content: string;
|
content: string
|
||||||
files: File[];
|
filesJson?: string
|
||||||
|
createUserName: string
|
||||||
|
files: File[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ActivityFileDto {
|
||||||
|
fileName: string
|
||||||
|
fileType: string
|
||||||
|
fileSize: number
|
||||||
|
savedFileName: string
|
||||||
|
fileBase64: string
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { PagedResultDto } from '@/proxy'
|
import { PagedResultDto } from '@/proxy'
|
||||||
import { ActivityDto } from '@/proxy/activity/models'
|
import { ActivityDto, ActivityFileDto } from '@/proxy/activity/models'
|
||||||
import apiService from '@/services/api.service'
|
import apiService from '@/services/api.service'
|
||||||
import { AxiosError } from 'axios'
|
import { AxiosError } from 'axios'
|
||||||
|
|
||||||
|
|
@ -21,35 +21,13 @@ class ActivityService {
|
||||||
return response.data
|
return response.data
|
||||||
}
|
}
|
||||||
|
|
||||||
async create(data: ActivityDto): Promise<any> {
|
async create(data: FormData): Promise<any> {
|
||||||
try {
|
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({
|
const response = await apiService.fetchData({
|
||||||
url: '/api/app/activity',
|
url: '/api/app/activity',
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
data: formData,
|
data, // FormData olduğu için headers belirtmeye gerek yok
|
||||||
// Don't manually set the Content-Type header; let the browser handle it
|
|
||||||
})
|
})
|
||||||
|
|
||||||
return response.data
|
return response.data
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof AxiosError) {
|
if (error instanceof AxiosError) {
|
||||||
|
|
@ -77,6 +55,27 @@ class ActivityService {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async downloadFile(savedFileName: string, fileName: string, fileType: string) {
|
||||||
|
const response = await apiService.fetchData<ActivityFileDto>({
|
||||||
|
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()
|
export const activityService = new ActivityService()
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,9 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {
|
import { FaStickyNote, FaEnvelope, FaTrash, FaDownload, FaClock, FaPaperclip } from 'react-icons/fa'
|
||||||
FaStickyNote,
|
import { Avatar, Button } from '@/components/ui'
|
||||||
FaFileAlt,
|
|
||||||
FaEnvelope,
|
|
||||||
FaTrash,
|
|
||||||
FaDownload,
|
|
||||||
FaUser,
|
|
||||||
FaClock,
|
|
||||||
} from 'react-icons/fa'
|
|
||||||
import { Button } from '@/components/ui'
|
|
||||||
import { ActivityDto } from '@/proxy/activity/models'
|
import { ActivityDto } from '@/proxy/activity/models'
|
||||||
|
import { AVATAR_URL } from '@/constants/app.constant'
|
||||||
|
import { useStoreState } from '@/store/store'
|
||||||
|
|
||||||
interface ActivityListProps {
|
interface ActivityListProps {
|
||||||
activities: ActivityDto[]
|
activities: ActivityDto[]
|
||||||
|
|
@ -22,105 +16,125 @@ export const ActivityList: React.FC<ActivityListProps> = ({
|
||||||
onDeleteActivity,
|
onDeleteActivity,
|
||||||
onDownloadFile,
|
onDownloadFile,
|
||||||
}) => {
|
}) => {
|
||||||
const getActivityIcon = (type: string) => {
|
const user = useStoreState((state) => state.auth.user)
|
||||||
|
|
||||||
|
const getActivityStyle = (type: string) => {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'note':
|
case 'note':
|
||||||
return <FaStickyNote className="text-yellow-500" />
|
return {
|
||||||
|
icon: <FaStickyNote className="text-yellow-500" />,
|
||||||
|
border: 'border-yellow-400',
|
||||||
|
}
|
||||||
case 'message':
|
case 'message':
|
||||||
return <FaEnvelope className="text-green-500" />
|
return {
|
||||||
|
icon: <FaEnvelope className="text-green-500" />,
|
||||||
|
border: 'border-green-400',
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
return <FaStickyNote className="text-gray-500" />
|
return {
|
||||||
|
icon: <FaStickyNote className="text-gray-400" />,
|
||||||
|
border: 'border-gray-300',
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// const handleDelete = (activity: ActivityDto) => {
|
if (activities.length === 0)
|
||||||
// onDeleteActivity?.(activity)
|
|
||||||
// }
|
|
||||||
|
|
||||||
const handleDownloadFile = (fileData: any) => {
|
|
||||||
onDownloadFile?.(fileData)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (activities.length === 0) {
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-center justify-center h-32 text-gray-500">
|
<div className="flex flex-col items-center justify-center h-32 text-gray-500">
|
||||||
<FaStickyNote className="text-4xl mb-2 opacity-50" />
|
<FaStickyNote className="text-4xl mb-2 opacity-50" />
|
||||||
<p className="text-sm">Henüz hiçbir aktivite bulunmuyor</p>
|
<p className="text-sm">Henüz hiçbir aktivite bulunmuyor</p>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-3">
|
<div className="relative">
|
||||||
{activities.map((activity) => (
|
<div className="space-y-5 ml-5">
|
||||||
<div
|
{activities.map((activity, index) => {
|
||||||
key={activity.id}
|
const files = activity.filesJson ? JSON.parse(activity.filesJson) : []
|
||||||
className="bg-white border border-gray-200 rounded-lg p-3 shadow-sm hover:shadow-md transition-shadow"
|
const creationDate = activity.creationTime ? new Date(activity.creationTime) : null
|
||||||
>
|
const { icon, border } = getActivityStyle(activity.type)
|
||||||
<div className="flex items-start gap-3">
|
|
||||||
<div className="flex-shrink-0 mt-0.5">{getActivityIcon(activity.type)}</div>
|
|
||||||
|
|
||||||
<div className="flex-1 min-w-0">
|
return (
|
||||||
<div className="flex items-center gap-1 font-semibold mb-1">
|
<div
|
||||||
<FaUser className="text-xs" />
|
key={activity.id || index}
|
||||||
<span>{activity.creatorId}</span>
|
className={`relative bg-white border-l-4 rounded-lg shadow-sm hover:shadow-md transition-shadow duration-200 ${border}`}
|
||||||
|
>
|
||||||
|
{/* Timeline Düğmesi */}
|
||||||
|
<div className="absolute -left-7 top-4 bg-white rounded-full border-2 border-gray-300 p-2">
|
||||||
|
{icon}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{activity.subject && (
|
<div className="p-4">
|
||||||
<p className="text-sm font-medium text-gray-800 mb-1 break-words">
|
{/* Header */}
|
||||||
{activity.subject}
|
<div className="flex justify-between items-start">
|
||||||
</p>
|
<div>
|
||||||
)}
|
<div className="flex items-center gap-1 text-sm font-semibold text-gray-800">
|
||||||
|
<Avatar
|
||||||
|
size={25}
|
||||||
|
shape="circle"
|
||||||
|
src={AVATAR_URL(activity.creatorId, activity.tenantId)}
|
||||||
|
/>
|
||||||
|
{activity.createUserName}
|
||||||
|
</div>
|
||||||
|
{creationDate && (
|
||||||
|
<div className="flex items-center gap-1 text-xs text-gray-400 mt-1 ml-1">
|
||||||
|
<FaClock /> {creationDate.toLocaleString()}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
{activity.content && (
|
{/* Sil butonu */}
|
||||||
<p className="text-sm text-gray-700 mb-2 break-words">{activity.content}</p>
|
{user?.id === activity.creatorId && (
|
||||||
)}
|
<Button
|
||||||
|
variant="plain"
|
||||||
|
size="xs"
|
||||||
|
onClick={() => onDeleteActivity?.(activity.id as string)}
|
||||||
|
title="Sil"
|
||||||
|
className="text-red-400 hover:text-red-600"
|
||||||
|
>
|
||||||
|
<FaTrash />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Note tipinde dosyaları göster */}
|
{/* Body */}
|
||||||
{/* {activity.type === 'note' && activity.data && (activity.data as any).attachedFiles?.length > 0 && (
|
<div className="mt-3 ml-1">
|
||||||
<div className="mt-2 mb-2">
|
{activity.subject && (
|
||||||
{((activity.data as any).attachedFiles || []).map((file: any, index: number) => (
|
<h4 className="text-sm font-bold text-gray-900 mb-1">{activity.subject}</h4>
|
||||||
<div key={index} className="flex items-center justify-between p-2 bg-gray-50 rounded border">
|
)}
|
||||||
<div className="flex items-center gap-2">
|
{activity.content && (
|
||||||
<FaFileAlt className="text-blue-500 text-sm" />
|
<div dangerouslySetInnerHTML={{ __html: activity.content }} />
|
||||||
<span className="text-xs font-medium">{file.fileName}</span>
|
)}
|
||||||
<span className="text-xs text-gray-500">({(file.fileSize / 1024).toFixed(1)} KB)</span>
|
</div>
|
||||||
</div>
|
|
||||||
|
{/* Files */}
|
||||||
|
{files.length > 0 && (
|
||||||
|
<div className="mt-3 flex flex-wrap gap-2">
|
||||||
|
{files.map((file: any, index: number) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className="group flex items-center gap-2 bg-gray-50 border border-gray-200 px-2 py-1 rounded-md text-xs text-gray-600 hover:bg-gray-100 transition"
|
||||||
|
>
|
||||||
|
<FaPaperclip className="text-blue-500" />
|
||||||
|
<span className="truncate max-w-[150px]">{file.FileName}</span>
|
||||||
<Button
|
<Button
|
||||||
variant="plain"
|
variant="plain"
|
||||||
size="xs"
|
size="xs"
|
||||||
onClick={() => handleDownloadFile(file)}
|
onClick={() => onDownloadFile?.(file)}
|
||||||
title="İndir"
|
title="İndir"
|
||||||
className="text-blue-500 hover:text-blue-700"
|
className="text-blue-500 hover:text-blue-700 ml-1"
|
||||||
>
|
>
|
||||||
<FaDownload />
|
<FaDownload />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)} */}
|
)}
|
||||||
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div className="flex items-center gap-1 text-xs text-gray-500">
|
|
||||||
<FaClock />
|
|
||||||
{activity.creationTime?.toLocaleString()}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center gap-1">
|
|
||||||
<Button
|
|
||||||
variant="plain"
|
|
||||||
size="xs"
|
|
||||||
// onClick={() => handleDelete(activity)}
|
|
||||||
title="Sil"
|
|
||||||
>
|
|
||||||
<FaTrash className="text-red-500" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)
|
||||||
</div>
|
})}
|
||||||
))}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,174 +1,265 @@
|
||||||
import React, { useState } from 'react'
|
import React, { useState } from 'react'
|
||||||
import { Button, Input, Dialog, Select, FormContainer, FormItem, Upload } from '@/components/ui'
|
import { Button, Input, Dialog, FormContainer, FormItem, Upload, Radio } from '@/components/ui'
|
||||||
import { FaFileUpload, FaStickyNote, FaPlus, FaTrash, FaPaperclip } from 'react-icons/fa'
|
import { FaFileAlt, FaFileUpload, FaPlus, FaTrash } from 'react-icons/fa'
|
||||||
import { Field, Form, Formik } from 'formik'
|
import { Field, FieldProps, Form, Formik } from 'formik'
|
||||||
import { SelectBoxOption } from '@/shared/types'
|
|
||||||
import * as Yup from 'yup'
|
import * as Yup from 'yup'
|
||||||
import { activityService } from '@/services/activity.service'
|
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({
|
const validationSchema = Yup.object({
|
||||||
type: Yup.string().required('Not tipi zorunludur'),
|
type: Yup.string().required('Not tipi zorunludur'),
|
||||||
subject: Yup.string().required('Konu zorunludur'),
|
subject: Yup.string().required('Konu zorunludur'),
|
||||||
content: Yup.string().required('İçerik zorunludur'),
|
content: Yup.string().required('İçerik zorunludur'),
|
||||||
})
|
})
|
||||||
|
|
||||||
interface AddContentModalProps {
|
interface ActivityModalProps {
|
||||||
entityName: string
|
entityName: string
|
||||||
entityId: string
|
entityId: string
|
||||||
isOpen: boolean
|
isOpen: boolean
|
||||||
onClose: () => void
|
onClose: () => void
|
||||||
|
onActivityAdded?: (activity: any) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AddContentModal: React.FC<AddContentModalProps> = ({
|
export const ActivityModal: React.FC<ActivityModalProps> = ({
|
||||||
entityName,
|
entityName,
|
||||||
entityId,
|
entityId,
|
||||||
isOpen,
|
isOpen,
|
||||||
onClose,
|
onClose,
|
||||||
|
onActivityAdded,
|
||||||
}) => {
|
}) => {
|
||||||
const [uploading, setUploading] = useState(false)
|
const [uploading, setUploading] = useState(false)
|
||||||
const [files, setFiles] = useState<string[] | undefined>()
|
const [fileList, setFileList] = useState<File[]>([])
|
||||||
|
|
||||||
const types: SelectBoxOption[] = [
|
const types = [
|
||||||
{ value: 'note', label: 'Not' },
|
{ value: 'note', label: 'Not' },
|
||||||
{ value: 'message', label: 'Mesaj' },
|
{ value: 'message', label: 'Mesaj' },
|
||||||
{ value: 'activity', label: 'Aktivite' },
|
{ value: 'activity', label: 'Aktivite' },
|
||||||
]
|
]
|
||||||
|
|
||||||
// onSaveContent güncelleme
|
|
||||||
const handleSave = async (values: any) => {
|
const handleSave = async (values: any) => {
|
||||||
setUploading(true)
|
setUploading(true)
|
||||||
try {
|
try {
|
||||||
// API çağrısı
|
const formData = new FormData()
|
||||||
await activityService.create({ ...values, entityName, entityId, files })
|
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
|
const createdActivity = await activityService.create(formData)
|
||||||
} catch (error) {
|
if (onActivityAdded) onActivityAdded(createdActivity)
|
||||||
console.error('Save failed:', error)
|
setFileList([])
|
||||||
|
onClose()
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err)
|
||||||
} finally {
|
} finally {
|
||||||
setUploading(false)
|
setUploading(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const beforeUpload = (files: FileList | null, fileList: File[]) => {
|
const beforeUpload = (files: FileList | null) => {
|
||||||
let valid: string | boolean = true
|
if (!files) return true
|
||||||
|
for (const f of Array.from(files)) {
|
||||||
const maxFileSize = 2000000
|
if (f.size > 2 * 1024 * 1024) return 'En fazla 2MB dosya yükleyebilirsiniz'
|
||||||
|
|
||||||
if (fileList.length >= 1) {
|
|
||||||
return `Sadece bir dosya seçebilirsiniz`
|
|
||||||
}
|
}
|
||||||
|
return true
|
||||||
if (files) {
|
|
||||||
for (const f of files) {
|
|
||||||
if (f.size >= maxFileSize) {
|
|
||||||
valid = 'En fazla 2mb dosya yükleyebilirsiniz'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return valid
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const onChooseImage = async (file: File[]) => {
|
const removeFile = (index: number) => {
|
||||||
if (file.length === 0) {
|
setFileList((prev) => prev.filter((_, i) => i !== index))
|
||||||
setFiles(undefined)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
setFiles(file.map((f) => URL.createObjectURL(f)))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog isOpen={isOpen} onClose={onClose}>
|
<Dialog isOpen={isOpen} onClose={onClose} width={700}>
|
||||||
<div className="p-2 w-full mx-auto">
|
<div className="p-2 w-full mx-auto">
|
||||||
<div className="flex items-center justify-between mb-6">
|
{/* Başlık */}
|
||||||
|
<div className="flex items-center justify-between mb-5">
|
||||||
<h3 className="text-xl font-semibold flex items-center gap-3">
|
<h3 className="text-xl font-semibold flex items-center gap-3">
|
||||||
<div className="p-2 bg-purple-100 rounded-full">
|
<div className="p-2 bg-purple-100 rounded-full">
|
||||||
<FaPlus className="text-purple-600 text-lg" />
|
<FaPlus className="text-purple-600 text-lg" />
|
||||||
</div>
|
</div>
|
||||||
Not Ekle
|
Not / Aktivite Ekle
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Formik
|
<Formik
|
||||||
initialValues={{
|
initialValues={{ type: 'note', subject: '', content: '' }}
|
||||||
type: 'note',
|
|
||||||
subject: '',
|
|
||||||
content: '',
|
|
||||||
}}
|
|
||||||
validationSchema={validationSchema}
|
validationSchema={validationSchema}
|
||||||
onSubmit={handleSave}
|
onSubmit={handleSave}
|
||||||
>
|
>
|
||||||
{({ values, touched, errors, setFieldValue, isSubmitting }) => (
|
{({ values, touched, errors, setFieldValue, isSubmitting }) => (
|
||||||
<Form>
|
<Form>
|
||||||
<FormContainer size="sm">
|
<FormContainer size="sm">
|
||||||
{/* Not Tipi */}
|
{/* NOT TİPİ */}
|
||||||
<FormItem
|
<FormItem
|
||||||
label="Not Tipi"
|
|
||||||
invalid={!!(errors.type && touched.type)}
|
invalid={!!(errors.type && touched.type)}
|
||||||
errorMessage={errors.type}
|
errorMessage={errors.type}
|
||||||
>
|
>
|
||||||
<Field name="type">
|
<div className="flex gap-4">
|
||||||
{({ field }: any) => (
|
{types.map((t) => (
|
||||||
<Select
|
<label
|
||||||
{...field}
|
key={t.value}
|
||||||
value={types.find((t) => t.value === values.type)}
|
className={`flex items-center gap-2 px-3 py-2 rounded-md cursor-pointer transition-all duration-200 ${
|
||||||
onChange={(selected: any) => setFieldValue('type', selected.value)}
|
values.type === t.value
|
||||||
options={types}
|
? 'border-purple-500 bg-purple-50 text-purple-700'
|
||||||
isSearchable={false}
|
: 'border-gray-300 hover:border-purple-400'
|
||||||
isClearable
|
}`}
|
||||||
/>
|
>
|
||||||
)}
|
<Radio
|
||||||
</Field>
|
value={t.value}
|
||||||
|
checked={values.type === t.value}
|
||||||
|
onChange={() => setFieldValue('type', t.value)}
|
||||||
|
/>
|
||||||
|
{t.label}
|
||||||
|
</label>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
|
|
||||||
{/* Konu */}
|
{/* KONUSU */}
|
||||||
<FormItem
|
<FormItem
|
||||||
label="Konu"
|
label="Konu"
|
||||||
invalid={!!(errors.subject && touched.subject)}
|
invalid={!!(errors.subject && touched.subject)}
|
||||||
errorMessage={errors.subject}
|
errorMessage={errors.subject}
|
||||||
>
|
|
||||||
<Field type="text" name="subject" component={Input} />
|
|
||||||
</FormItem>
|
|
||||||
|
|
||||||
{/* Not İçeriği */}
|
|
||||||
<FormItem
|
|
||||||
label="Not İçeriği"
|
|
||||||
invalid={!!(errors.content && touched.content)}
|
|
||||||
errorMessage={errors.content}
|
|
||||||
>
|
>
|
||||||
<Field
|
<Field
|
||||||
name="content"
|
type="text"
|
||||||
render={({ field }: any) => (
|
name="subject"
|
||||||
<textarea
|
as={Input}
|
||||||
{...field}
|
placeholder="Kısa bir başlık girin..."
|
||||||
placeholder="Notunuzu buraya yazın..."
|
|
||||||
className="w-full p-2 border border-gray-300 rounded-md resize-none focus:ring-2 focus:ring-purple-500 focus:border-transparent"
|
|
||||||
rows={4}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
/>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
|
|
||||||
{/* Dosya Yükleme */}
|
{/* İÇERİK */}
|
||||||
|
<FormItem
|
||||||
|
label="İçerik"
|
||||||
|
asterisk
|
||||||
|
invalid={!!(errors.content && touched.content)}
|
||||||
|
errorMessage={errors.content}
|
||||||
|
>
|
||||||
|
<Field name="content">
|
||||||
|
{({ field }: FieldProps) => (
|
||||||
|
<HtmlEditor
|
||||||
|
value={field.value}
|
||||||
|
onValueChanged={(e) => setFieldValue('content', e.value)}
|
||||||
|
height={220}
|
||||||
|
placeholder="Notunuzu buraya yazın..."
|
||||||
|
>
|
||||||
|
<MediaResizing enabled={true} />
|
||||||
|
<ImageUpload fileUploadMode="base64" />
|
||||||
|
<Toolbar multiline>
|
||||||
|
<Item name="undo" />
|
||||||
|
<Item name="redo" />
|
||||||
|
<Item name="separator" />
|
||||||
|
<Item name="size" acceptedValues={sizeValues} options={fontSizeOptions} />
|
||||||
|
<Item
|
||||||
|
name="font"
|
||||||
|
acceptedValues={fontValues}
|
||||||
|
options={fontFamilyOptions}
|
||||||
|
/>
|
||||||
|
<Item name="separator" />
|
||||||
|
<Item name="bold" />
|
||||||
|
<Item name="italic" />
|
||||||
|
<Item name="underline" />
|
||||||
|
<Item name="strike" />
|
||||||
|
<Item name="separator" />
|
||||||
|
<Item name="orderedList" />
|
||||||
|
<Item name="bulletList" />
|
||||||
|
<Item name="separator" />
|
||||||
|
<Item
|
||||||
|
name="header"
|
||||||
|
acceptedValues={headerValues}
|
||||||
|
options={headerOptions}
|
||||||
|
/>
|
||||||
|
<Item name="separator" />
|
||||||
|
<Item name="color" />
|
||||||
|
<Item name="background" />
|
||||||
|
<Item name="separator" />
|
||||||
|
<Item name="alignLeft" />
|
||||||
|
<Item name="alignCenter" />
|
||||||
|
<Item name="alignRight" />
|
||||||
|
<Item name="alignJustify" />
|
||||||
|
<Item name="separator" />
|
||||||
|
<Item name="link" />
|
||||||
|
<Item name="image" />
|
||||||
|
<Item name="separator" />
|
||||||
|
<Item name="clear" />
|
||||||
|
</Toolbar>
|
||||||
|
</HtmlEditor>
|
||||||
|
)}
|
||||||
|
</Field>
|
||||||
|
</FormItem>
|
||||||
|
|
||||||
|
{/* DOSYA YÜKLEME */}
|
||||||
<FormItem label="Dosya Ekle">
|
<FormItem label="Dosya Ekle">
|
||||||
<div className="border-2 border-dashed border-gray-300 rounded-lg p-3 text-center hover:border-purple-400 transition-colors duration-200">
|
<div className="border-2 border-dashed border-gray-300 rounded-lg p-3 text-center hover:border-purple-400 transition-colors duration-200">
|
||||||
<Upload
|
<Upload
|
||||||
className="cursor-pointer"
|
className="cursor-pointer"
|
||||||
showList={false}
|
showList={false}
|
||||||
multiple={false}
|
multiple
|
||||||
uploadLimit={1}
|
fileList={fileList}
|
||||||
beforeUpload={beforeUpload}
|
beforeUpload={beforeUpload}
|
||||||
onChange={onChooseImage}
|
onChange={(files: File[]) => {
|
||||||
|
setFileList((prev) => [
|
||||||
|
...prev,
|
||||||
|
...files.filter(
|
||||||
|
(f) => !prev.some((p) => p.name === f.name && p.size === f.size),
|
||||||
|
),
|
||||||
|
])
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Button icon={<FaFileUpload />} type="button"></Button>
|
<Button
|
||||||
|
icon={<FaFileUpload />}
|
||||||
|
type="button"
|
||||||
|
variant="twoTone"
|
||||||
|
className="flex items-center justify-center mx-auto"
|
||||||
|
>
|
||||||
|
Dosya Yükle
|
||||||
|
</Button>
|
||||||
</Upload>
|
</Upload>
|
||||||
|
|
||||||
|
{fileList.length > 0 && (
|
||||||
|
<div className="mt-2 max-h-28 overflow-y-auto border rounded-md bg-gray-50 text-left">
|
||||||
|
{fileList.map((file, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className="flex items-center justify-between p-2 border-b last:border-none"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2 truncate">
|
||||||
|
<FaFileAlt className="text-blue-500 text-sm flex-shrink-0" />
|
||||||
|
<span className="text-sm truncate">{file.name}</span>
|
||||||
|
<span className="text-xs text-gray-500">
|
||||||
|
({(file.size / 1024).toFixed(1)} KB)
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant="plain"
|
||||||
|
size="xs"
|
||||||
|
type="button"
|
||||||
|
onClick={() => removeFile(index)}
|
||||||
|
className="text-red-500 hover:text-red-700"
|
||||||
|
>
|
||||||
|
<FaTrash />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
</FormContainer>
|
</FormContainer>
|
||||||
|
|
||||||
<div className="mt-4 flex justify-between items-center pt-4 border-t border-gray-200">
|
{/* ALT BUTONLAR */}
|
||||||
|
<div className="mt-5 flex justify-between items-center pt-4 border-t border-gray-200">
|
||||||
<Button variant="default" size="md" onClick={onClose} disabled={uploading}>
|
<Button variant="default" size="md" onClick={onClose} disabled={uploading}>
|
||||||
İptal
|
İptal
|
||||||
</Button>
|
</Button>
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,16 @@
|
||||||
import React, { useState, useRef, useEffect } from 'react'
|
import React, { useState, useRef, useEffect } from 'react'
|
||||||
import { AddContentModal } from './ActivityModal'
|
import { ActivityModal } from './ActivityModal'
|
||||||
import { ActivityList } from './ActivityList'
|
import { ActivityList } from './ActivityList'
|
||||||
import { Button, Badge } from '@/components/ui'
|
import { Button, Badge } from '@/components/ui'
|
||||||
import { FaChevronLeft, FaChevronRight, FaPlus, FaTimes, FaGripVertical } from 'react-icons/fa'
|
import {
|
||||||
|
FaChevronLeft,
|
||||||
|
FaChevronRight,
|
||||||
|
FaPlus,
|
||||||
|
FaTimes,
|
||||||
|
FaGripVertical,
|
||||||
|
FaChevronUp,
|
||||||
|
FaChevronDown,
|
||||||
|
} from 'react-icons/fa'
|
||||||
import { activityService } from '@/services/activity.service'
|
import { activityService } from '@/services/activity.service'
|
||||||
import { ActivityDto } from '@/proxy/activity/models'
|
import { ActivityDto } from '@/proxy/activity/models'
|
||||||
|
|
||||||
|
|
@ -19,177 +27,116 @@ export const ActivityPanel: React.FC<ActivityPanelProps> = ({
|
||||||
isVisible,
|
isVisible,
|
||||||
onToggle,
|
onToggle,
|
||||||
}) => {
|
}) => {
|
||||||
const [showAddContent, setShowAddContent] = useState(false)
|
const [showAddModal, setShowAddModal] = useState(false)
|
||||||
|
const [activities, setActivities] = useState<ActivityDto[]>([])
|
||||||
// Draggable button state
|
const [buttonPosition, setButtonPosition] = useState({ top: '75%' })
|
||||||
const [buttonPosition, setButtonPosition] = useState({ top: '50%' })
|
|
||||||
const [isDragging, setIsDragging] = useState(false)
|
const [isDragging, setIsDragging] = useState(false)
|
||||||
const [dragStart, setDragStart] = useState({ y: 0, startTop: 0 })
|
const [dragStart, setDragStart] = useState({ y: 0, startTop: 0 })
|
||||||
const [activities, setActivities] = useState<ActivityDto[]>([])
|
|
||||||
const buttonRef = useRef<HTMLDivElement>(null)
|
const buttonRef = useRef<HTMLDivElement>(null)
|
||||||
|
const [showEntityInfo, setShowEntityInfo] = useState(false)
|
||||||
|
|
||||||
|
// Fetch activities
|
||||||
|
const fetchActivities = async () => {
|
||||||
|
try {
|
||||||
|
const res = await activityService.getList({ entityName, entityId })
|
||||||
|
if (res?.items) setActivities(res.items)
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchActivities = async () => {
|
if (isVisible) fetchActivities()
|
||||||
// Simulate API call to fetch activities
|
|
||||||
// Gerçek implementasyonda API'dan veri çekilecek
|
|
||||||
const fetchedActivities = await activityService.getList()
|
|
||||||
if (fetchedActivities && fetchedActivities.items) {
|
|
||||||
setActivities(fetchedActivities.items)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fetchActivities()
|
|
||||||
}, [isVisible])
|
}, [isVisible])
|
||||||
|
|
||||||
const handleDownloadFile = (fileData: any) => {
|
const handleDownloadFile = async (fileData: any) => {
|
||||||
// Simulate file download - gerçek implementasyonda API'dan dosya indirme yapılacak
|
if (!fileData?.SavedFileName) return
|
||||||
const link = document.createElement('a')
|
try {
|
||||||
link.href = '#' // gerçek dosya URL'i buraya gelecek
|
await activityService.downloadFile(
|
||||||
link.download = fileData.fileName
|
fileData.SavedFileName,
|
||||||
link.click()
|
fileData.FileName,
|
||||||
|
fileData.FileType,
|
||||||
|
)
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Dosya indirilemedi', err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDeleteActivity = async (activityId: string) => {
|
||||||
|
if (!confirm('Bu aktiviteyi silmek istediğinize emin misiniz?')) return
|
||||||
|
try {
|
||||||
|
await activityService.delete(activityId)
|
||||||
|
setActivities((prev) => prev.filter((a) => a.id !== activityId))
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const getTotalCount = () => activities.length
|
const getTotalCount = () => activities.length
|
||||||
|
|
||||||
// Mouse drag handlers
|
// Draggable button handlers
|
||||||
const handleMouseDown = (e: React.MouseEvent) => {
|
const handleMouseDown = (e: React.MouseEvent) => {
|
||||||
if (!buttonRef.current) return
|
if (!buttonRef.current) return
|
||||||
|
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
e.stopPropagation()
|
|
||||||
|
|
||||||
setIsDragging(true)
|
setIsDragging(true)
|
||||||
const rect = buttonRef.current.getBoundingClientRect()
|
setDragStart({ y: e.clientY, startTop: buttonRef.current.getBoundingClientRect().top })
|
||||||
const startTop = rect.top
|
|
||||||
|
|
||||||
setDragStart({
|
|
||||||
y: e.clientY,
|
|
||||||
startTop: startTop,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleMouseMove = (e: MouseEvent) => {
|
const handleMouseMove = (e: MouseEvent) => {
|
||||||
if (!isDragging || !buttonRef.current) return
|
if (!isDragging || !buttonRef.current) return
|
||||||
|
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
e.stopPropagation()
|
|
||||||
|
|
||||||
const deltaY = e.clientY - dragStart.y
|
const deltaY = e.clientY - dragStart.y
|
||||||
const newTop = dragStart.startTop + deltaY
|
const newTop = dragStart.startTop + deltaY
|
||||||
|
|
||||||
// Calculate percentage based on viewport
|
|
||||||
const viewportHeight = window.innerHeight
|
const viewportHeight = window.innerHeight
|
||||||
const buttonHeight = buttonRef.current.offsetHeight
|
const buttonHeight = buttonRef.current.offsetHeight
|
||||||
|
const constrainedTop = Math.max(0, Math.min(viewportHeight - buttonHeight, newTop))
|
||||||
// Constrain within viewport bounds
|
|
||||||
const minTop = 0
|
|
||||||
const maxTop = viewportHeight - buttonHeight
|
|
||||||
const constrainedTop = Math.max(minTop, Math.min(maxTop, newTop))
|
|
||||||
|
|
||||||
// Convert to percentage
|
|
||||||
const topPercentage = (constrainedTop / viewportHeight) * 100
|
const topPercentage = (constrainedTop / viewportHeight) * 100
|
||||||
|
|
||||||
setButtonPosition({ top: `${topPercentage}%` })
|
setButtonPosition({ top: `${topPercentage}%` })
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleMouseUp = () => {
|
const handleMouseUp = () => setIsDragging(false)
|
||||||
setIsDragging(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add event listeners for mouse move and up
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isDragging) {
|
if (isDragging) {
|
||||||
// Disable text selection during drag
|
|
||||||
document.body.style.userSelect = 'none'
|
document.body.style.userSelect = 'none'
|
||||||
document.body.style.webkitUserSelect = 'none'
|
|
||||||
|
|
||||||
document.addEventListener('mousemove', handleMouseMove, { passive: false })
|
document.addEventListener('mousemove', handleMouseMove, { passive: false })
|
||||||
document.addEventListener('mouseup', handleMouseUp)
|
document.addEventListener('mouseup', handleMouseUp)
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
// Re-enable text selection
|
|
||||||
document.body.style.userSelect = ''
|
document.body.style.userSelect = ''
|
||||||
document.body.style.webkitUserSelect = ''
|
|
||||||
|
|
||||||
document.removeEventListener('mousemove', handleMouseMove)
|
document.removeEventListener('mousemove', handleMouseMove)
|
||||||
document.removeEventListener('mouseup', handleMouseUp)
|
document.removeEventListener('mouseup', handleMouseUp)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [isDragging, dragStart])
|
}, [isDragging, dragStart])
|
||||||
|
|
||||||
// Load saved position from localStorage
|
if (!entityName || !entityId) return null
|
||||||
useEffect(() => {
|
|
||||||
const savedPosition = localStorage.getItem('activityPanelButtonPosition')
|
|
||||||
if (savedPosition) {
|
|
||||||
try {
|
|
||||||
const position = JSON.parse(savedPosition)
|
|
||||||
setButtonPosition(position)
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to load button position:', error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
// Save position to localStorage
|
|
||||||
useEffect(() => {
|
|
||||||
localStorage.setItem('activityPanelButtonPosition', JSON.stringify(buttonPosition))
|
|
||||||
}, [buttonPosition])
|
|
||||||
|
|
||||||
if (!entityName || !entityId) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
// // Example onSaveContent function
|
|
||||||
// const onSaveContent = async (
|
|
||||||
// entityName: string,
|
|
||||||
// entityId: string,
|
|
||||||
// type: string,
|
|
||||||
// subject: string,
|
|
||||||
// content: string,
|
|
||||||
// files: File[],
|
|
||||||
// ) => {
|
|
||||||
// try {
|
|
||||||
// await activityService.create({ entityName, entityId, type, subject, content, files } as any)
|
|
||||||
// } catch (error) {
|
|
||||||
// console.error('Error saving content:', error)
|
|
||||||
// throw error
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* Toggle Button - Draggable */}
|
{/* Draggable toggle button */}
|
||||||
<div
|
<div
|
||||||
ref={buttonRef}
|
ref={buttonRef}
|
||||||
className="fixed right-0 z-40"
|
className="fixed right-0 z-40"
|
||||||
style={{
|
style={{ top: buttonPosition.top, transform: 'translateY(-50%)' }}
|
||||||
top: buttonPosition.top,
|
|
||||||
transform: 'translateY(-50%)',
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<div className="group relative">
|
<div className="group relative">
|
||||||
{/* Drag Handle */}
|
|
||||||
<div
|
<div
|
||||||
className={`absolute -left-2 top-1/2 transform -translate-y-1/2 opacity-0 group-hover:opacity-100 transition-opacity duration-200 select-none ${isDragging ? 'cursor-grabbing' : 'cursor-grab'}`}
|
className={`absolute -left-2 top-1/2 transform -translate-y-1/2 opacity-0 group-hover:opacity-100 transition-opacity duration-200 select-none ${isDragging ? 'cursor-grabbing' : 'cursor-grab'}`}
|
||||||
onMouseDown={handleMouseDown}
|
onMouseDown={handleMouseDown}
|
||||||
style={{ userSelect: 'none' }}
|
|
||||||
>
|
>
|
||||||
<div className="bg-gray-600 text-white p-1 rounded-l text-xs">
|
<div className="bg-gray-600 text-white p-1 rounded-l text-xs">
|
||||||
<FaGripVertical />
|
<FaGripVertical />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Main Button */}
|
|
||||||
<Button
|
<Button
|
||||||
variant="solid"
|
variant="solid"
|
||||||
size="sm"
|
size="sm"
|
||||||
shape="none"
|
|
||||||
className="!rounded-l-full !rounded-r-none"
|
className="!rounded-l-full !rounded-r-none"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
onToggle()
|
onToggle()
|
||||||
}}
|
}}
|
||||||
title={isVisible ? 'Aktivite panelini kapat' : 'Aktivite panelini aç'}
|
title={isVisible ? 'Paneli kapat' : 'Paneli aç'}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{isVisible ? <FaChevronRight /> : <FaChevronLeft />}
|
{isVisible ? <FaChevronRight /> : <FaChevronLeft />}
|
||||||
|
|
@ -199,67 +146,84 @@ export const ActivityPanel: React.FC<ActivityPanelProps> = ({
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Overlay - Click outside to close */}
|
{/* Overlay */}
|
||||||
{isVisible && (
|
{isVisible && (
|
||||||
<div className="fixed inset-0 bg-black bg-opacity-25 z-20" onClick={onToggle} />
|
<div className="fixed inset-0 bg-black bg-opacity-25 z-20" onClick={onToggle} />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Activity Panel */}
|
{/* Panel */}
|
||||||
<div
|
<div
|
||||||
className={`fixed right-0 top-0 h-full bg-white border-l border-gray-300 shadow-xl transform transition-transform duration-300 ease-in-out z-30 ${
|
className={`fixed right-0 top-0 h-full bg-white border-l border-gray-300 shadow-xl transform transition-transform duration-300 ease-in-out z-30 ${isVisible ? 'translate-x-0' : 'translate-x-full'}`}
|
||||||
isVisible ? 'translate-x-0' : 'translate-x-full'
|
style={{ width: '450px' }}
|
||||||
}`}
|
|
||||||
style={{ width: '400px' }}
|
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
<div className="flex flex-col h-full">
|
<div className="flex flex-col h-full">
|
||||||
{/* Header */}
|
|
||||||
<div className="p-4 border-b border-gray-200 bg-gray-50">
|
<div className="p-4 border-b border-gray-200 bg-gray-50">
|
||||||
|
{/* Üst Satır: Başlık, Kayıt Bilgisi Toggle ve Kapat Butonu */}
|
||||||
<div className="flex items-center justify-between mb-3">
|
<div className="flex items-center justify-between mb-3">
|
||||||
<h3 className="text-lg font-semibold text-gray-800">Aktiviteler</h3>
|
<h3 className="text-lg font-semibold text-gray-800">Aktiviteler</h3>
|
||||||
<Button variant="plain" size="xs" onClick={onToggle}>
|
|
||||||
<FaTimes />
|
<div className="flex items-center gap-3">
|
||||||
</Button>
|
{/* 👇 Kayıt Bilgisi Aç/Kapa Butonu */}
|
||||||
|
<button
|
||||||
|
onClick={() => setShowEntityInfo((prev) => !prev)}
|
||||||
|
className="flex items-center gap-1 text-sm text-gray-600 hover:text-gray-800 cursor-pointer select-none"
|
||||||
|
title="Kayıt Bilgisi"
|
||||||
|
>
|
||||||
|
{showEntityInfo ? <FaChevronUp /> : <FaChevronDown />}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Kapat Butonu */}
|
||||||
|
<Button variant="plain" size="xs" onClick={onToggle} title="Paneli kapat">
|
||||||
|
<FaTimes />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="text-sm text-gray-600 mb-3 flex items-center gap-2">
|
{/* 👇 Açılır Kayıt Bilgisi İçeriği */}
|
||||||
<span className="font-medium">{entityName}</span>
|
<div
|
||||||
<code className="bg-gray-100 px-2 rounded text-gray-800 text-xs font-mono">
|
className={`transition-all duration-300 overflow-hidden ${
|
||||||
<Badge className="bg-blue-100 text-blue-600" content={entityId} />
|
showEntityInfo ? 'max-h-20 mt-2 opacity-100' : 'max-h-0 opacity-0'
|
||||||
</code>
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-1 text-sm text-gray-700">
|
||||||
|
<span className="font-medium">{entityName}</span>
|
||||||
|
<code className="bg-gray-100 px-2 rounded text-gray-800 text-xs font-mono">
|
||||||
|
<Badge className="bg-blue-100 text-blue-600" content={entityId} />
|
||||||
|
</code>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Action Buttons */}
|
{/* Alt buton: Not Ekle */}
|
||||||
<div className="flex gap-2 mb-3">
|
<div className="flex gap-2 mt-3">
|
||||||
<Button
|
<Button
|
||||||
variant="solid"
|
variant="solid"
|
||||||
size="xs"
|
size="xs"
|
||||||
onClick={() => setShowAddContent(true)}
|
onClick={() => setShowAddModal(true)}
|
||||||
className="flex justify-center items-center py-4 w-full"
|
className="flex justify-center items-center py-4 w-full"
|
||||||
>
|
>
|
||||||
<FaPlus className="mr-1" />
|
<FaPlus className="mr-1" /> Not Ekle
|
||||||
Not Ekle
|
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Content */}
|
|
||||||
<div className="flex-1 overflow-y-auto p-4">
|
<div className="flex-1 overflow-y-auto p-4">
|
||||||
<ActivityList
|
<ActivityList
|
||||||
activities={activities}
|
activities={activities}
|
||||||
// onDeleteActivity={deleteActivity}
|
onDeleteActivity={handleDeleteActivity}
|
||||||
onDownloadFile={handleDownloadFile}
|
onDownloadFile={handleDownloadFile}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Modals */}
|
{/* Modal */}
|
||||||
<AddContentModal
|
<ActivityModal
|
||||||
entityName={entityName}
|
entityName={entityName}
|
||||||
entityId={entityId}
|
entityId={entityId}
|
||||||
isOpen={showAddContent}
|
isOpen={showAddModal}
|
||||||
onClose={() => setShowAddContent(false)}
|
onClose={() => setShowAddModal(false)}
|
||||||
|
onActivityAdded={(act) => setActivities((prev) => [act, ...prev])}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue