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;
|
||||
|
||||
public class ActivityDto : EntityDto<Guid>
|
||||
public class ActivityDto : FullAuditedEntityDto<Guid>
|
||||
{
|
||||
public Guid? TenantId { get; set; }
|
||||
public string EntityName { get; set; }
|
||||
|
|
@ -12,7 +12,8 @@ public class ActivityDto : EntityDto<Guid>
|
|||
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; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.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> ActivityBlobContainer;
|
||||
private readonly IBlobContainer<ActivityBlobContainer> _activityBlobContainer;
|
||||
private readonly IRepository<IdentityUser, Guid> _repositoryUser;
|
||||
|
||||
public ActivityAppService(
|
||||
IRepository<Activity, Guid> repo,
|
||||
IBlobContainer<ActivityBlobContainer> activityBlobContainer
|
||||
IBlobContainer<ActivityBlobContainer> activityBlobContainer,
|
||||
IRepository<IdentityUser, Guid> 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<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 {
|
||||
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
|
||||
}
|
||||
|
|
@ -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<any> {
|
||||
async create(data: FormData): Promise<any> {
|
||||
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<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()
|
||||
|
|
|
|||
|
|
@ -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<ActivityListProps> = ({
|
|||
onDeleteActivity,
|
||||
onDownloadFile,
|
||||
}) => {
|
||||
const getActivityIcon = (type: string) => {
|
||||
const user = useStoreState((state) => state.auth.user)
|
||||
|
||||
const getActivityStyle = (type: string) => {
|
||||
switch (type) {
|
||||
case 'note':
|
||||
return <FaStickyNote className="text-yellow-500" />
|
||||
return {
|
||||
icon: <FaStickyNote className="text-yellow-500" />,
|
||||
border: 'border-yellow-400',
|
||||
}
|
||||
case 'message':
|
||||
return <FaEnvelope className="text-green-500" />
|
||||
return {
|
||||
icon: <FaEnvelope className="text-green-500" />,
|
||||
border: 'border-green-400',
|
||||
}
|
||||
default:
|
||||
return <FaStickyNote className="text-gray-500" />
|
||||
return {
|
||||
icon: <FaStickyNote className="text-gray-400" />,
|
||||
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 (
|
||||
<div className="flex flex-col items-center justify-center h-32 text-gray-500">
|
||||
<FaStickyNote className="text-4xl mb-2 opacity-50" />
|
||||
<p className="text-sm">Henüz hiçbir aktivite bulunmuyor</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
{activities.map((activity) => (
|
||||
<div
|
||||
key={activity.id}
|
||||
className="bg-white border border-gray-200 rounded-lg p-3 shadow-sm hover:shadow-md transition-shadow"
|
||||
>
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="flex-shrink-0 mt-0.5">{getActivityIcon(activity.type)}</div>
|
||||
<div className="relative">
|
||||
<div className="space-y-5 ml-5">
|
||||
{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)
|
||||
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-1 font-semibold mb-1">
|
||||
<FaUser className="text-xs" />
|
||||
<span>{activity.creatorId}</span>
|
||||
return (
|
||||
<div
|
||||
key={activity.id || index}
|
||||
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>
|
||||
|
||||
{activity.subject && (
|
||||
<p className="text-sm font-medium text-gray-800 mb-1 break-words">
|
||||
{activity.subject}
|
||||
</p>
|
||||
)}
|
||||
<div className="p-4">
|
||||
{/* Header */}
|
||||
<div className="flex justify-between items-start">
|
||||
<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 && (
|
||||
<p className="text-sm text-gray-700 mb-2 break-words">{activity.content}</p>
|
||||
)}
|
||||
{/* Sil butonu */}
|
||||
{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 */}
|
||||
{/* {activity.type === 'note' && activity.data && (activity.data as any).attachedFiles?.length > 0 && (
|
||||
<div className="mt-2 mb-2">
|
||||
{((activity.data as any).attachedFiles || []).map((file: any, index: number) => (
|
||||
<div key={index} className="flex items-center justify-between p-2 bg-gray-50 rounded border">
|
||||
<div className="flex items-center gap-2">
|
||||
<FaFileAlt className="text-blue-500 text-sm" />
|
||||
<span className="text-xs font-medium">{file.fileName}</span>
|
||||
<span className="text-xs text-gray-500">({(file.fileSize / 1024).toFixed(1)} KB)</span>
|
||||
</div>
|
||||
{/* Body */}
|
||||
<div className="mt-3 ml-1">
|
||||
{activity.subject && (
|
||||
<h4 className="text-sm font-bold text-gray-900 mb-1">{activity.subject}</h4>
|
||||
)}
|
||||
{activity.content && (
|
||||
<div dangerouslySetInnerHTML={{ __html: activity.content }} />
|
||||
)}
|
||||
</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
|
||||
variant="plain"
|
||||
size="xs"
|
||||
onClick={() => handleDownloadFile(file)}
|
||||
onClick={() => onDownloadFile?.(file)}
|
||||
title="İndir"
|
||||
className="text-blue-500 hover:text-blue-700"
|
||||
className="text-blue-500 hover:text-blue-700 ml-1"
|
||||
>
|
||||
<FaDownload />
|
||||
</Button>
|
||||
</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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<AddContentModalProps> = ({
|
||||
export const ActivityModal: React.FC<ActivityModalProps> = ({
|
||||
entityName,
|
||||
entityId,
|
||||
isOpen,
|
||||
onClose,
|
||||
onActivityAdded,
|
||||
}) => {
|
||||
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: '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 (
|
||||
<Dialog isOpen={isOpen} onClose={onClose}>
|
||||
<Dialog isOpen={isOpen} onClose={onClose} width={700}>
|
||||
<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">
|
||||
<div className="p-2 bg-purple-100 rounded-full">
|
||||
<FaPlus className="text-purple-600 text-lg" />
|
||||
</div>
|
||||
Not Ekle
|
||||
Not / Aktivite Ekle
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<Formik
|
||||
initialValues={{
|
||||
type: 'note',
|
||||
subject: '',
|
||||
content: '',
|
||||
}}
|
||||
initialValues={{ type: 'note', subject: '', content: '' }}
|
||||
validationSchema={validationSchema}
|
||||
onSubmit={handleSave}
|
||||
>
|
||||
{({ values, touched, errors, setFieldValue, isSubmitting }) => (
|
||||
<Form>
|
||||
<FormContainer size="sm">
|
||||
{/* Not Tipi */}
|
||||
{/* NOT TİPİ */}
|
||||
<FormItem
|
||||
label="Not Tipi"
|
||||
invalid={!!(errors.type && touched.type)}
|
||||
errorMessage={errors.type}
|
||||
>
|
||||
<Field name="type">
|
||||
{({ field }: any) => (
|
||||
<Select
|
||||
{...field}
|
||||
value={types.find((t) => t.value === values.type)}
|
||||
onChange={(selected: any) => setFieldValue('type', selected.value)}
|
||||
options={types}
|
||||
isSearchable={false}
|
||||
isClearable
|
||||
/>
|
||||
)}
|
||||
</Field>
|
||||
<div className="flex gap-4">
|
||||
{types.map((t) => (
|
||||
<label
|
||||
key={t.value}
|
||||
className={`flex items-center gap-2 px-3 py-2 rounded-md cursor-pointer transition-all duration-200 ${
|
||||
values.type === t.value
|
||||
? 'border-purple-500 bg-purple-50 text-purple-700'
|
||||
: 'border-gray-300 hover:border-purple-400'
|
||||
}`}
|
||||
>
|
||||
<Radio
|
||||
value={t.value}
|
||||
checked={values.type === t.value}
|
||||
onChange={() => setFieldValue('type', t.value)}
|
||||
/>
|
||||
{t.label}
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</FormItem>
|
||||
|
||||
{/* Konu */}
|
||||
{/* KONUSU */}
|
||||
<FormItem
|
||||
label="Konu"
|
||||
invalid={!!(errors.subject && touched.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
|
||||
name="content"
|
||||
render={({ field }: any) => (
|
||||
<textarea
|
||||
{...field}
|
||||
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}
|
||||
/>
|
||||
)}
|
||||
type="text"
|
||||
name="subject"
|
||||
as={Input}
|
||||
placeholder="Kısa bir başlık girin..."
|
||||
/>
|
||||
</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">
|
||||
<div className="border-2 border-dashed border-gray-300 rounded-lg p-3 text-center hover:border-purple-400 transition-colors duration-200">
|
||||
<Upload
|
||||
className="cursor-pointer"
|
||||
showList={false}
|
||||
multiple={false}
|
||||
uploadLimit={1}
|
||||
multiple
|
||||
fileList={fileList}
|
||||
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>
|
||||
|
||||
{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>
|
||||
</FormItem>
|
||||
</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}>
|
||||
İptal
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -1,8 +1,16 @@
|
|||
import React, { useState, useRef, useEffect } from 'react'
|
||||
import { AddContentModal } from './ActivityModal'
|
||||
import { ActivityModal } from './ActivityModal'
|
||||
import { ActivityList } from './ActivityList'
|
||||
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 { ActivityDto } from '@/proxy/activity/models'
|
||||
|
||||
|
|
@ -19,177 +27,116 @@ export const ActivityPanel: React.FC<ActivityPanelProps> = ({
|
|||
isVisible,
|
||||
onToggle,
|
||||
}) => {
|
||||
const [showAddContent, setShowAddContent] = useState(false)
|
||||
|
||||
// Draggable button state
|
||||
const [buttonPosition, setButtonPosition] = useState({ top: '50%' })
|
||||
const [showAddModal, setShowAddModal] = useState(false)
|
||||
const [activities, setActivities] = useState<ActivityDto[]>([])
|
||||
const [buttonPosition, setButtonPosition] = useState({ top: '75%' })
|
||||
const [isDragging, setIsDragging] = useState(false)
|
||||
const [dragStart, setDragStart] = useState({ y: 0, startTop: 0 })
|
||||
const [activities, setActivities] = useState<ActivityDto[]>([])
|
||||
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(() => {
|
||||
const fetchActivities = async () => {
|
||||
// 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()
|
||||
if (isVisible) fetchActivities()
|
||||
}, [isVisible])
|
||||
|
||||
const handleDownloadFile = (fileData: any) => {
|
||||
// Simulate file download - gerçek implementasyonda API'dan dosya indirme yapılacak
|
||||
const link = document.createElement('a')
|
||||
link.href = '#' // gerçek dosya URL'i buraya gelecek
|
||||
link.download = fileData.fileName
|
||||
link.click()
|
||||
const handleDownloadFile = async (fileData: any) => {
|
||||
if (!fileData?.SavedFileName) return
|
||||
try {
|
||||
await activityService.downloadFile(
|
||||
fileData.SavedFileName,
|
||||
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
|
||||
|
||||
// Mouse drag handlers
|
||||
// Draggable button handlers
|
||||
const handleMouseDown = (e: React.MouseEvent) => {
|
||||
if (!buttonRef.current) return
|
||||
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
|
||||
setIsDragging(true)
|
||||
const rect = buttonRef.current.getBoundingClientRect()
|
||||
const startTop = rect.top
|
||||
|
||||
setDragStart({
|
||||
y: e.clientY,
|
||||
startTop: startTop,
|
||||
})
|
||||
setDragStart({ y: e.clientY, startTop: buttonRef.current.getBoundingClientRect().top })
|
||||
}
|
||||
|
||||
const handleMouseMove = (e: MouseEvent) => {
|
||||
if (!isDragging || !buttonRef.current) return
|
||||
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
|
||||
const deltaY = e.clientY - dragStart.y
|
||||
const newTop = dragStart.startTop + deltaY
|
||||
|
||||
// Calculate percentage based on viewport
|
||||
const viewportHeight = window.innerHeight
|
||||
const buttonHeight = buttonRef.current.offsetHeight
|
||||
|
||||
// Constrain within viewport bounds
|
||||
const minTop = 0
|
||||
const maxTop = viewportHeight - buttonHeight
|
||||
const constrainedTop = Math.max(minTop, Math.min(maxTop, newTop))
|
||||
|
||||
// Convert to percentage
|
||||
const constrainedTop = Math.max(0, Math.min(viewportHeight - buttonHeight, newTop))
|
||||
const topPercentage = (constrainedTop / viewportHeight) * 100
|
||||
|
||||
setButtonPosition({ top: `${topPercentage}%` })
|
||||
}
|
||||
|
||||
const handleMouseUp = () => {
|
||||
setIsDragging(false)
|
||||
}
|
||||
const handleMouseUp = () => setIsDragging(false)
|
||||
|
||||
// Add event listeners for mouse move and up
|
||||
useEffect(() => {
|
||||
if (isDragging) {
|
||||
// Disable text selection during drag
|
||||
document.body.style.userSelect = 'none'
|
||||
document.body.style.webkitUserSelect = 'none'
|
||||
|
||||
document.addEventListener('mousemove', handleMouseMove, { passive: false })
|
||||
document.addEventListener('mouseup', handleMouseUp)
|
||||
|
||||
return () => {
|
||||
// Re-enable text selection
|
||||
document.body.style.userSelect = ''
|
||||
document.body.style.webkitUserSelect = ''
|
||||
|
||||
document.removeEventListener('mousemove', handleMouseMove)
|
||||
document.removeEventListener('mouseup', handleMouseUp)
|
||||
}
|
||||
}
|
||||
}, [isDragging, dragStart])
|
||||
|
||||
// Load saved position from localStorage
|
||||
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
|
||||
// }
|
||||
// }
|
||||
if (!entityName || !entityId) return null
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Toggle Button - Draggable */}
|
||||
{/* Draggable toggle button */}
|
||||
<div
|
||||
ref={buttonRef}
|
||||
className="fixed right-0 z-40"
|
||||
style={{
|
||||
top: buttonPosition.top,
|
||||
transform: 'translateY(-50%)',
|
||||
}}
|
||||
style={{ top: buttonPosition.top, transform: 'translateY(-50%)' }}
|
||||
>
|
||||
<div className="group relative">
|
||||
{/* Drag Handle */}
|
||||
<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'}`}
|
||||
onMouseDown={handleMouseDown}
|
||||
style={{ userSelect: 'none' }}
|
||||
>
|
||||
<div className="bg-gray-600 text-white p-1 rounded-l text-xs">
|
||||
<FaGripVertical />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Main Button */}
|
||||
<Button
|
||||
variant="solid"
|
||||
size="sm"
|
||||
shape="none"
|
||||
className="!rounded-l-full !rounded-r-none"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
onToggle()
|
||||
}}
|
||||
title={isVisible ? 'Aktivite panelini kapat' : 'Aktivite panelini aç'}
|
||||
title={isVisible ? 'Paneli kapat' : 'Paneli aç'}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
{isVisible ? <FaChevronRight /> : <FaChevronLeft />}
|
||||
|
|
@ -199,67 +146,84 @@ export const ActivityPanel: React.FC<ActivityPanelProps> = ({
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{/* Overlay - Click outside to close */}
|
||||
{/* Overlay */}
|
||||
{isVisible && (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-25 z-20" onClick={onToggle} />
|
||||
)}
|
||||
|
||||
{/* Activity Panel */}
|
||||
{/* Panel */}
|
||||
<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 ${
|
||||
isVisible ? 'translate-x-0' : 'translate-x-full'
|
||||
}`}
|
||||
style={{ width: '400px' }}
|
||||
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'}`}
|
||||
style={{ width: '450px' }}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className="flex flex-col h-full">
|
||||
{/* Header */}
|
||||
<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">
|
||||
<h3 className="text-lg font-semibold text-gray-800">Aktiviteler</h3>
|
||||
<Button variant="plain" size="xs" onClick={onToggle}>
|
||||
<FaTimes />
|
||||
</Button>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
{/* 👇 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 className="text-sm text-gray-600 mb-3 flex items-center gap-2">
|
||||
<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>
|
||||
{/* 👇 Açılır Kayıt Bilgisi İçeriği */}
|
||||
<div
|
||||
className={`transition-all duration-300 overflow-hidden ${
|
||||
showEntityInfo ? 'max-h-20 mt-2 opacity-100' : 'max-h-0 opacity-0'
|
||||
}`}
|
||||
>
|
||||
<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>
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className="flex gap-2 mb-3">
|
||||
{/* Alt buton: Not Ekle */}
|
||||
<div className="flex gap-2 mt-3">
|
||||
<Button
|
||||
variant="solid"
|
||||
size="xs"
|
||||
onClick={() => setShowAddContent(true)}
|
||||
onClick={() => setShowAddModal(true)}
|
||||
className="flex justify-center items-center py-4 w-full"
|
||||
>
|
||||
<FaPlus className="mr-1" />
|
||||
Not Ekle
|
||||
<FaPlus className="mr-1" /> Not Ekle
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex-1 overflow-y-auto p-4">
|
||||
<ActivityList
|
||||
activities={activities}
|
||||
// onDeleteActivity={deleteActivity}
|
||||
onDeleteActivity={handleDeleteActivity}
|
||||
onDownloadFile={handleDownloadFile}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Modals */}
|
||||
<AddContentModal
|
||||
{/* Modal */}
|
||||
<ActivityModal
|
||||
entityName={entityName}
|
||||
entityId={entityId}
|
||||
isOpen={showAddContent}
|
||||
onClose={() => setShowAddContent(false)}
|
||||
isOpen={showAddModal}
|
||||
onClose={() => setShowAddModal(false)}
|
||||
onActivityAdded={(act) => setActivities((prev) => [act, ...prev])}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
|
|
|
|||
Loading…
Reference in a new issue