Activity kaydı ve Blob kaydı

This commit is contained in:
Sedat Öztürk 2025-10-14 00:47:53 +03:00
parent 5e4726c9be
commit 6f91454ae8
32 changed files with 441 additions and 2006 deletions

View file

@ -1,16 +1,18 @@
using System; using System;
using Volo.Abp.Application.Dtos; using Volo.Abp.Application.Dtos;
using Volo.Abp.Content;
namespace Kurs.Platform.Entities; namespace Kurs.Platform.Entities;
public class ActivityDto : FullAuditedEntityDto<Guid> public class ActivityDto : EntityDto<Guid>
{ {
public Guid? TenantId { get; set; } public Guid? TenantId { get; set; }
public string EntityName { get; set; }
public string EntityId { get; set; }
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 RecipientUserName { get; set; } public string FilesJson { get; set; }
protected ActivityDto() { } public IRemoteStreamContent[] Files { get; set; }
} }

View file

@ -1,18 +0,0 @@
using System;
using Volo.Abp.Application.Dtos;
namespace Kurs.Platform.Entities;
public class ActivityFileDto : FullAuditedEntityDto<Guid>
{
public Guid? TenantId { get; set; }
public string EntityName { get; set; }
public string EntityId { get; set; }
public string FileName { get; set; }
public long FileSize { get; set; }
public string FileType { get; set; }
public string FilePath { get; set; }
public Guid ActivityItemId { get; set; }
}

View file

@ -1,21 +0,0 @@
using System;
using Volo.Abp.Application.Dtos;
using Volo.Abp.MultiTenancy;
namespace Kurs.Platform.Entities;
public class ActivityItemDto : FullAuditedEntityDto<Guid>, IMultiTenant
{
public Guid? TenantId { get; set; }
public string Type { get; set; }
public string EntityName { get; set; }
public string EntityId { get; set; }
public Guid? RecipientUserId { get; set; }
public string RecipientUserName { get; set; }
public string Subject { get; set; }
public string Content { get; set; }
// Navigation properties
public Guid ActivityId { get; set; }
}

View file

@ -1,11 +1,15 @@
using System; using System;
using System.Threading.Tasks;
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 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.Domain.Repositories; using Volo.Abp.Domain.Repositories;
namespace Kurs.Platform.DataSources; namespace Kurs.Platform.Activities;
[Authorize] [Authorize]
public class ActivityAppService : CrudAppService< public class ActivityAppService : CrudAppService<
@ -14,9 +18,28 @@ public class ActivityAppService : CrudAppService<
Guid, Guid,
PagedAndSortedResultRequestDto> PagedAndSortedResultRequestDto>
{ {
public ActivityAppService( private readonly IBlobContainer<ActivityBlobContainer> ActivityBlobContainer;
IRepository<Activity, Guid> repo) : base(repo)
{
public ActivityAppService(
IRepository<Activity, Guid> repo,
IBlobContainer<ActivityBlobContainer> activityBlobContainer
) : base(repo)
{
ActivityBlobContainer = activityBlobContainer;
// CreatePolicyName = $"{AppCodes.Listforms.Listform}.Create";
// UpdatePolicyName = $"{AppCodes.Listforms.Listform}.Update";
// DeletePolicyName = $"{AppCodes.Listforms.Listform}.Delete";
// bool canAccess = await authManager.CanAccess(listFormCode, AuthorizationTypeEnum.Update);
// if (!canAccess)
// {
// throw new UserFriendlyException(L[AppErrorCodes.NoAuth]);
// }
}
public override Task<ActivityDto> CreateAsync([FromForm] ActivityDto input)
{
return base.CreateAsync(input);
} }
} }

View file

@ -1,15 +1,26 @@
using AutoMapper; using AutoMapper;
using Kurs.Platform.Entities; using Kurs.Platform.Entities;
namespace Kurs.Platform.DataSources; namespace Kurs.Platform.Activities;
public class ActivityAutoMapperProfile : Profile public class ActivityAutoMapperProfile : Profile
{ {
public ActivityAutoMapperProfile() public ActivityAutoMapperProfile()
{ {
CreateMap<Activity, ActivityDto>(); // Map from Activity to ActivityDto
CreateMap<ActivityItem, ActivityItemDto>(); CreateMap<Activity, ActivityDto>()
CreateMap<ActivityFile, ActivityFileDto>(); .ForMember(dest => dest.FilesJson, opt => opt.MapFrom(src => src.FilesJson))
.ForMember(dest => dest.Files, opt => opt.Ignore()); // Ignore Files property as we handle it manually
// Mapping from ActivityDto to Activity
CreateMap<ActivityDto, Activity>()
.ForMember(dest => dest.TenantId, opt => opt.MapFrom(src => src.TenantId))
.ForMember(dest => dest.EntityName, opt => opt.MapFrom(src => src.EntityName))
.ForMember(dest => dest.EntityId, opt => opt.MapFrom(src => src.EntityId))
.ForMember(dest => dest.Type, opt => opt.MapFrom(src => src.Type))
.ForMember(dest => dest.Subject, opt => opt.MapFrom(src => src.Subject))
.ForMember(dest => dest.Content, opt => opt.MapFrom(src => src.Content))
// Handling the FilesJson field (as a JSON string)
.ForMember(dest => dest.FilesJson, opt => opt.MapFrom(src => src.FilesJson)); // Adjust the mapping as needed
} }
} }

View file

@ -1,22 +0,0 @@
using System;
using Kurs.Platform.Entities;
using Microsoft.AspNetCore.Authorization;
using Volo.Abp.Application.Dtos;
using Volo.Abp.Application.Services;
using Volo.Abp.Domain.Repositories;
namespace Kurs.Platform.DataSources;
[Authorize]
public class ActivityFileAppService : CrudAppService<
ActivityFile,
ActivityFileDto,
Guid,
PagedAndSortedResultRequestDto>
{
public ActivityFileAppService(
IRepository<ActivityFile, Guid> repo) : base(repo)
{
}
}

View file

@ -1,22 +0,0 @@
using System;
using Kurs.Platform.Entities;
using Microsoft.AspNetCore.Authorization;
using Volo.Abp.Application.Dtos;
using Volo.Abp.Application.Services;
using Volo.Abp.Domain.Repositories;
namespace Kurs.Platform.DataSources;
[Authorize]
public class ActivityItemAppService : CrudAppService<
ActivityItem,
ActivityItemDto,
Guid,
PagedAndSortedResultRequestDto>
{
public ActivityItemAppService(
IRepository<ActivityItem, Guid> repo) : base(repo)
{
}
}

View file

@ -5785,6 +5785,12 @@
"en": "Export", "en": "Export",
"tr": "Dışa Ver" "tr": "Dışa Ver"
}, },
{
"resourceName": "Platform",
"key": "ListForms.ListFormEdit.PermissionsImport",
"en": "Import",
"tr": "İçe Aktar"
},
{ {
"resourceName": "Platform", "resourceName": "Platform",
"key": "ListForms.ListFormEdit.CommandColumns", "key": "ListForms.ListFormEdit.CommandColumns",
@ -15256,7 +15262,7 @@
"Order": 1, "Order": 1,
"Url": "/admin/list/list-bank", "Url": "/admin/list/list-bank",
"Icon": "FcMoneyTransfer", "Icon": "FcMoneyTransfer",
"RequiredPermissionName": "App.Definitions.Bank", "RequiredPermissionName": "App.Accounting.Bank",
"IsDisabled": false "IsDisabled": false
}, },
{ {

View file

@ -1428,6 +1428,7 @@ public static class PlatformConsts
{ {
public const string AvatarContainer = "Avatar"; public const string AvatarContainer = "Avatar";
public const string ImportContainer = "Import"; public const string ImportContainer = "Import";
public const string ActivityContainer = "Activity";
} }
public static readonly ReadOnlyCollection<LanguageInfo> Languages = new( public static readonly ReadOnlyCollection<LanguageInfo> Languages = new(

View file

@ -0,0 +1,8 @@
using Volo.Abp.BlobStoring;
namespace Kurs.Platform.BlobStoring;
[BlobContainerName(PlatformConsts.BlobContainers.ActivityContainer)]
public class ActivityBlobContainer
{
}

View file

@ -1,5 +1,4 @@
using System; using System;
using System.Collections.Generic;
using Volo.Abp.Domain.Entities.Auditing; using Volo.Abp.Domain.Entities.Auditing;
using Volo.Abp.MultiTenancy; using Volo.Abp.MultiTenancy;
@ -8,24 +7,11 @@ namespace Kurs.Platform.Entities;
public class Activity : FullAuditedEntity<Guid>, IMultiTenant public class Activity : FullAuditedEntity<Guid>, IMultiTenant
{ {
public Guid? TenantId { get; set; } public Guid? TenantId { get; set; }
public string EntityName { get; set; }
public string EntityId { get; set; }
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 Guid? RecipientUserId { get; set; }
public string RecipientUserName { get; set; }
public ICollection<ActivityItem> ActivityItems { get; set; } public string FilesJson { get; set; }
protected Activity() { }
public Activity(Guid id, string type, string subject, string content, string recipientUserName = null)
: base(id)
{
Type = type;
Subject = subject;
Content = content;
RecipientUserName = recipientUserName;
ActivityItems = new List<ActivityItem>();
}
} }

View file

@ -1,43 +0,0 @@
using System;
using Volo.Abp.Domain.Entities.Auditing;
using Volo.Abp.MultiTenancy;
namespace Kurs.Platform.Entities;
public class ActivityFile : FullAuditedEntity<Guid>, IMultiTenant
{
public Guid? TenantId { get; set; }
public string EntityName { get; set; }
public string EntityId { get; set; }
public string FileName { get; set; }
public long FileSize { get; set; }
public string FileType { get; set; }
public string FilePath { get; set; }
// Navigation properties
public Guid ActivityItemId { get; set; }
public ActivityItem ActivityItem { get; set; }
protected ActivityFile() { }
public ActivityFile(
Guid id,
Guid activityItemId,
string entityName,
string entityId,
string fileName,
long fileSize,
string fileType,
string filePath)
: base(id)
{
ActivityItemId = activityItemId;
EntityName = entityName;
EntityId = entityId;
FileName = fileName;
FileSize = fileSize;
FileType = fileType;
FilePath = filePath;
}
}

View file

@ -1,50 +0,0 @@
using System;
using System.Collections.Generic;
using Volo.Abp.Domain.Entities.Auditing;
using Volo.Abp.MultiTenancy;
namespace Kurs.Platform.Entities;
public class ActivityItem : FullAuditedEntity<Guid>, IMultiTenant
{
public Guid? TenantId { get; set; }
public string Type { get; set; }
public string EntityName { get; set; }
public string EntityId { get; set; }
public Guid? RecipientUserId { get; set; }
public string RecipientUserName { get; set; }
public string Subject { get; set; }
public string Content { get; set; }
// Navigation properties
public Guid ActivityId { get; set; }
public Activity Activity { get; set; }
public ICollection<ActivityFile> AttachedFiles { get; set; }
protected ActivityItem() { }
public ActivityItem(
Guid id,
Guid activityId,
string type,
string entityName,
string entityId,
string subject,
string content,
Guid? recipientUserId = null,
string recipientUserName = null)
: base(id)
{
ActivityId = activityId;
Type = type;
EntityName = entityName;
EntityId = entityId;
Subject = subject;
Content = content;
RecipientUserId = recipientUserId;
RecipientUserName = recipientUserName;
AttachedFiles = [];
}
}

View file

@ -64,8 +64,6 @@ public class PlatformDbContext :
public DbSet<ReportGenerated> ReportGenerated { get; set; } public DbSet<ReportGenerated> ReportGenerated { get; set; }
public DbSet<ReportCategory> ReportCategories { get; set; } public DbSet<ReportCategory> ReportCategories { get; set; }
public DbSet<Activity> Activities { get; set; } public DbSet<Activity> Activities { get; set; }
public DbSet<ActivityItem> ActivityItems { get; set; }
public DbSet<ActivityFile> ActivityFiles { get; set; }
#endregion #endregion
#region Definitions from the modules #region Definitions from the modules
@ -1559,65 +1557,9 @@ public class PlatformDbContext :
b.ToTable(Prefix.DbTableDefault + nameof(Activity), Prefix.DbSchema); b.ToTable(Prefix.DbTableDefault + nameof(Activity), Prefix.DbSchema);
b.ConfigureByConvention(); b.ConfigureByConvention();
b.Property(x => x.Type).IsRequired();
b.Property(x => x.Subject).IsRequired().HasMaxLength(256); b.Property(x => x.Subject).IsRequired().HasMaxLength(256);
b.Property(x => x.Content).IsRequired().HasMaxLength(2000); b.Property(x => x.Content).IsRequired().HasMaxLength(2000);
b.Property(x => x.RecipientUserName).HasMaxLength(100);
b.Property(x => x.Type).IsRequired();
b.HasIndex(x => x.Type);
b.HasIndex(x => x.CreatorId);
b.HasIndex(x => x.CreationTime);
});
builder.Entity<ActivityItem>(b =>
{
b.ToTable(Prefix.DbTableDefault + nameof(ActivityItem), Prefix.DbSchema);
b.ConfigureByConvention();
b.Property(x => x.EntityName).IsRequired().HasMaxLength(128);
b.Property(x => x.EntityId).IsRequired().HasMaxLength(128);
b.Property(x => x.Subject).IsRequired().HasMaxLength(256);
b.Property(x => x.Content).IsRequired().HasMaxLength(2000);
b.Property(x => x.RecipientUserName).HasMaxLength(100);
b.Property(x => x.Type).IsRequired();
// Foreign key relationship
b.HasOne(ai => ai.Activity)
.WithMany(a => a.ActivityItems)
.HasForeignKey(ai => ai.ActivityId)
.OnDelete(DeleteBehavior.Cascade);
b.HasIndex(x => x.ActivityId);
b.HasIndex(x => x.EntityName);
b.HasIndex(x => x.EntityId);
b.HasIndex(x => x.Type);
b.HasIndex(x => x.RecipientUserId);
b.HasIndex(x => x.CreatorId);
});
builder.Entity<ActivityFile>(b =>
{
b.ToTable(Prefix.DbTableDefault + nameof(ActivityFile), Prefix.DbSchema);
b.ConfigureByConvention();
b.Property(x => x.EntityName).IsRequired().HasMaxLength(128);
b.Property(x => x.EntityId).IsRequired().HasMaxLength(128);
b.Property(x => x.FileName).IsRequired().HasMaxLength(256);
b.Property(x => x.FileType).IsRequired().HasMaxLength(50);
b.Property(x => x.FilePath).IsRequired().HasMaxLength(512);
b.Property(x => x.FileSize).IsRequired();
// Foreign key relationship
b.HasOne(af => af.ActivityItem)
.WithMany(ai => ai.AttachedFiles)
.HasForeignKey(af => af.ActivityItemId)
.OnDelete(DeleteBehavior.Cascade);
b.HasIndex(x => x.ActivityItemId);
b.HasIndex(x => x.EntityName);
b.HasIndex(x => x.EntityId);
b.HasIndex(x => x.FileName);
b.HasIndex(x => x.CreatorId);
}); });
} }
} }

View file

@ -13,7 +13,7 @@ using Volo.Abp.EntityFrameworkCore;
namespace Kurs.Platform.Migrations namespace Kurs.Platform.Migrations
{ {
[DbContext(typeof(PlatformDbContext))] [DbContext(typeof(PlatformDbContext))]
[Migration("20251013134213_Initial")] [Migration("20251013214306_Initial")]
partial class Initial partial class Initial
{ {
/// <inheritdoc /> /// <inheritdoc />
@ -651,6 +651,15 @@ namespace Kurs.Platform.Migrations
.HasColumnType("datetime2") .HasColumnType("datetime2")
.HasColumnName("DeletionTime"); .HasColumnName("DeletionTime");
b.Property<string>("EntityId")
.HasColumnType("nvarchar(max)");
b.Property<string>("EntityName")
.HasColumnType("nvarchar(max)");
b.Property<string>("FilesJson")
.HasColumnType("nvarchar(max)");
b.Property<bool>("IsDeleted") b.Property<bool>("IsDeleted")
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()
.HasColumnType("bit") .HasColumnType("bit")
@ -665,10 +674,6 @@ namespace Kurs.Platform.Migrations
.HasColumnType("uniqueidentifier") .HasColumnType("uniqueidentifier")
.HasColumnName("LastModifierId"); .HasColumnName("LastModifierId");
b.Property<string>("RecipientUserName")
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<string>("Subject") b.Property<string>("Subject")
.IsRequired() .IsRequired()
.HasMaxLength(256) .HasMaxLength(256)
@ -678,194 +683,15 @@ namespace Kurs.Platform.Migrations
.HasColumnType("uniqueidentifier") .HasColumnType("uniqueidentifier")
.HasColumnName("TenantId"); .HasColumnName("TenantId");
b.Property<int>("Type") b.Property<string>("Type")
.HasColumnType("int"); .IsRequired()
.HasColumnType("nvarchar(max)");
b.HasKey("Id"); b.HasKey("Id");
b.HasIndex("CreationTime");
b.HasIndex("CreatorId");
b.HasIndex("Type");
b.ToTable("PActivity", (string)null); b.ToTable("PActivity", (string)null);
}); });
modelBuilder.Entity("Kurs.Platform.Entities.ActivityFile", b =>
{
b.Property<Guid>("Id")
.HasColumnType("uniqueidentifier");
b.Property<Guid>("ActivityItemId")
.HasColumnType("uniqueidentifier");
b.Property<DateTime>("CreationTime")
.HasColumnType("datetime2")
.HasColumnName("CreationTime");
b.Property<Guid?>("CreatorId")
.HasColumnType("uniqueidentifier")
.HasColumnName("CreatorId");
b.Property<Guid?>("DeleterId")
.HasColumnType("uniqueidentifier")
.HasColumnName("DeleterId");
b.Property<DateTime?>("DeletionTime")
.HasColumnType("datetime2")
.HasColumnName("DeletionTime");
b.Property<string>("EntityId")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("nvarchar(128)");
b.Property<string>("EntityName")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("nvarchar(128)");
b.Property<string>("FileName")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<string>("FilePath")
.IsRequired()
.HasMaxLength(512)
.HasColumnType("nvarchar(512)");
b.Property<long>("FileSize")
.HasColumnType("bigint");
b.Property<string>("FileType")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<bool>("IsDeleted")
.ValueGeneratedOnAdd()
.HasColumnType("bit")
.HasDefaultValue(false)
.HasColumnName("IsDeleted");
b.Property<DateTime?>("LastModificationTime")
.HasColumnType("datetime2")
.HasColumnName("LastModificationTime");
b.Property<Guid?>("LastModifierId")
.HasColumnType("uniqueidentifier")
.HasColumnName("LastModifierId");
b.Property<Guid?>("TenantId")
.HasColumnType("uniqueidentifier")
.HasColumnName("TenantId");
b.HasKey("Id");
b.HasIndex("ActivityItemId");
b.HasIndex("CreatorId");
b.HasIndex("EntityId");
b.HasIndex("EntityName");
b.HasIndex("FileName");
b.ToTable("PActivityFile", (string)null);
});
modelBuilder.Entity("Kurs.Platform.Entities.ActivityItem", b =>
{
b.Property<Guid>("Id")
.HasColumnType("uniqueidentifier");
b.Property<Guid>("ActivityId")
.HasColumnType("uniqueidentifier");
b.Property<string>("Content")
.IsRequired()
.HasMaxLength(2000)
.HasColumnType("nvarchar(2000)");
b.Property<DateTime>("CreationTime")
.HasColumnType("datetime2")
.HasColumnName("CreationTime");
b.Property<Guid?>("CreatorId")
.HasColumnType("uniqueidentifier")
.HasColumnName("CreatorId");
b.Property<Guid?>("DeleterId")
.HasColumnType("uniqueidentifier")
.HasColumnName("DeleterId");
b.Property<DateTime?>("DeletionTime")
.HasColumnType("datetime2")
.HasColumnName("DeletionTime");
b.Property<string>("EntityId")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("nvarchar(128)");
b.Property<string>("EntityName")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("nvarchar(128)");
b.Property<bool>("IsDeleted")
.ValueGeneratedOnAdd()
.HasColumnType("bit")
.HasDefaultValue(false)
.HasColumnName("IsDeleted");
b.Property<DateTime?>("LastModificationTime")
.HasColumnType("datetime2")
.HasColumnName("LastModificationTime");
b.Property<Guid?>("LastModifierId")
.HasColumnType("uniqueidentifier")
.HasColumnName("LastModifierId");
b.Property<Guid?>("RecipientUserId")
.HasColumnType("uniqueidentifier");
b.Property<string>("RecipientUserName")
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<string>("Subject")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<Guid?>("TenantId")
.HasColumnType("uniqueidentifier")
.HasColumnName("TenantId");
b.Property<int>("Type")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("ActivityId");
b.HasIndex("CreatorId");
b.HasIndex("EntityId");
b.HasIndex("EntityName");
b.HasIndex("RecipientUserId");
b.HasIndex("Type");
b.ToTable("PActivityItem", (string)null);
});
modelBuilder.Entity("Kurs.Platform.Entities.AiBot", b => modelBuilder.Entity("Kurs.Platform.Entities.AiBot", b =>
{ {
b.Property<Guid>("Id") b.Property<Guid>("Id")
@ -8860,28 +8686,6 @@ namespace Kurs.Platform.Migrations
b.Navigation("NotificationRule"); b.Navigation("NotificationRule");
}); });
modelBuilder.Entity("Kurs.Platform.Entities.ActivityFile", b =>
{
b.HasOne("Kurs.Platform.Entities.ActivityItem", "ActivityItem")
.WithMany("AttachedFiles")
.HasForeignKey("ActivityItemId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("ActivityItem");
});
modelBuilder.Entity("Kurs.Platform.Entities.ActivityItem", b =>
{
b.HasOne("Kurs.Platform.Entities.Activity", "Activity")
.WithMany("ActivityItems")
.HasForeignKey("ActivityId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Activity");
});
modelBuilder.Entity("Kurs.Platform.Entities.ApiEndpoint", b => modelBuilder.Entity("Kurs.Platform.Entities.ApiEndpoint", b =>
{ {
b.HasOne("Kurs.Platform.Entities.CustomEntity", "Entity") b.HasOne("Kurs.Platform.Entities.CustomEntity", "Entity")
@ -9380,16 +9184,6 @@ namespace Kurs.Platform.Migrations
b.Navigation("Notifications"); b.Navigation("Notifications");
}); });
modelBuilder.Entity("Kurs.Platform.Entities.Activity", b =>
{
b.Navigation("ActivityItems");
});
modelBuilder.Entity("Kurs.Platform.Entities.ActivityItem", b =>
{
b.Navigation("AttachedFiles");
});
modelBuilder.Entity("Kurs.Platform.Entities.BlogCategory", b => modelBuilder.Entity("Kurs.Platform.Entities.BlogCategory", b =>
{ {
b.Navigation("Posts"); b.Navigation("Posts");

View file

@ -1214,10 +1214,12 @@ namespace Kurs.Platform.Migrations
{ {
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false), Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
TenantId = table.Column<Guid>(type: "uniqueidentifier", nullable: true), TenantId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
Type = table.Column<int>(type: "int", nullable: false), EntityName = table.Column<string>(type: "nvarchar(max)", nullable: true),
EntityId = table.Column<string>(type: "nvarchar(max)", nullable: true),
Type = table.Column<string>(type: "nvarchar(max)", nullable: false),
Subject = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: false), Subject = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: false),
Content = table.Column<string>(type: "nvarchar(2000)", maxLength: 2000, nullable: false), Content = table.Column<string>(type: "nvarchar(2000)", maxLength: 2000, nullable: false),
RecipientUserName = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: true), FilesJson = table.Column<string>(type: "nvarchar(max)", nullable: true),
CreationTime = table.Column<DateTime>(type: "datetime2", nullable: false), CreationTime = table.Column<DateTime>(type: "datetime2", nullable: false),
CreatorId = table.Column<Guid>(type: "uniqueidentifier", nullable: true), CreatorId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
LastModificationTime = table.Column<DateTime>(type: "datetime2", nullable: true), LastModificationTime = table.Column<DateTime>(type: "datetime2", nullable: true),
@ -2612,39 +2614,6 @@ namespace Kurs.Platform.Migrations
principalColumn: "Id"); principalColumn: "Id");
}); });
migrationBuilder.CreateTable(
name: "PActivityItem",
columns: table => new
{
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
TenantId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
Type = table.Column<int>(type: "int", nullable: false),
EntityName = table.Column<string>(type: "nvarchar(128)", maxLength: 128, nullable: false),
EntityId = table.Column<string>(type: "nvarchar(128)", maxLength: 128, nullable: false),
RecipientUserId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
RecipientUserName = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: true),
Subject = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: false),
Content = table.Column<string>(type: "nvarchar(2000)", maxLength: 2000, nullable: false),
ActivityId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
CreationTime = table.Column<DateTime>(type: "datetime2", nullable: false),
CreatorId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
LastModificationTime = table.Column<DateTime>(type: "datetime2", nullable: true),
LastModifierId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
IsDeleted = table.Column<bool>(type: "bit", nullable: false, defaultValue: false),
DeleterId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
DeletionTime = table.Column<DateTime>(type: "datetime2", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_PActivityItem", x => x.Id);
table.ForeignKey(
name: "FK_PActivityItem_PActivity_ActivityId",
column: x => x.ActivityId,
principalTable: "PActivity",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable( migrationBuilder.CreateTable(
name: "PBackgroundWorker_MailQueue", name: "PBackgroundWorker_MailQueue",
columns: table => new columns: table => new
@ -3348,38 +3317,6 @@ namespace Kurs.Platform.Migrations
principalColumn: "Id"); principalColumn: "Id");
}); });
migrationBuilder.CreateTable(
name: "PActivityFile",
columns: table => new
{
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
TenantId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
EntityName = table.Column<string>(type: "nvarchar(128)", maxLength: 128, nullable: false),
EntityId = table.Column<string>(type: "nvarchar(128)", maxLength: 128, nullable: false),
FileName = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: false),
FileSize = table.Column<long>(type: "bigint", nullable: false),
FileType = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: false),
FilePath = table.Column<string>(type: "nvarchar(512)", maxLength: 512, nullable: false),
ActivityItemId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
CreationTime = table.Column<DateTime>(type: "datetime2", nullable: false),
CreatorId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
LastModificationTime = table.Column<DateTime>(type: "datetime2", nullable: true),
LastModifierId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
IsDeleted = table.Column<bool>(type: "bit", nullable: false, defaultValue: false),
DeleterId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
DeletionTime = table.Column<DateTime>(type: "datetime2", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_PActivityFile", x => x.Id);
table.ForeignKey(
name: "FK_PActivityFile_PActivityItem_ActivityItemId",
column: x => x.ActivityItemId,
principalTable: "PActivityItem",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable( migrationBuilder.CreateTable(
name: "PBackgroundWorker_MailQueueEvents", name: "PBackgroundWorker_MailQueueEvents",
columns: table => new columns: table => new
@ -3919,76 +3856,6 @@ namespace Kurs.Platform.Migrations
table: "OpenIddictTokens", table: "OpenIddictTokens",
column: "ReferenceId"); column: "ReferenceId");
migrationBuilder.CreateIndex(
name: "IX_PActivity_CreationTime",
table: "PActivity",
column: "CreationTime");
migrationBuilder.CreateIndex(
name: "IX_PActivity_CreatorId",
table: "PActivity",
column: "CreatorId");
migrationBuilder.CreateIndex(
name: "IX_PActivity_Type",
table: "PActivity",
column: "Type");
migrationBuilder.CreateIndex(
name: "IX_PActivityFile_ActivityItemId",
table: "PActivityFile",
column: "ActivityItemId");
migrationBuilder.CreateIndex(
name: "IX_PActivityFile_CreatorId",
table: "PActivityFile",
column: "CreatorId");
migrationBuilder.CreateIndex(
name: "IX_PActivityFile_EntityId",
table: "PActivityFile",
column: "EntityId");
migrationBuilder.CreateIndex(
name: "IX_PActivityFile_EntityName",
table: "PActivityFile",
column: "EntityName");
migrationBuilder.CreateIndex(
name: "IX_PActivityFile_FileName",
table: "PActivityFile",
column: "FileName");
migrationBuilder.CreateIndex(
name: "IX_PActivityItem_ActivityId",
table: "PActivityItem",
column: "ActivityId");
migrationBuilder.CreateIndex(
name: "IX_PActivityItem_CreatorId",
table: "PActivityItem",
column: "CreatorId");
migrationBuilder.CreateIndex(
name: "IX_PActivityItem_EntityId",
table: "PActivityItem",
column: "EntityId");
migrationBuilder.CreateIndex(
name: "IX_PActivityItem_EntityName",
table: "PActivityItem",
column: "EntityName");
migrationBuilder.CreateIndex(
name: "IX_PActivityItem_RecipientUserId",
table: "PActivityItem",
column: "RecipientUserId");
migrationBuilder.CreateIndex(
name: "IX_PActivityItem_Type",
table: "PActivityItem",
column: "Type");
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "IX_PApiEndpoint_EntityId", name: "IX_PApiEndpoint_EntityId",
table: "PApiEndpoint", table: "PApiEndpoint",
@ -4378,7 +4245,7 @@ namespace Kurs.Platform.Migrations
name: "OpenIddictTokens"); name: "OpenIddictTokens");
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "PActivityFile"); name: "PActivity");
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "PAiBot"); name: "PAiBot");
@ -4533,9 +4400,6 @@ namespace Kurs.Platform.Migrations
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "OpenIddictAuthorizations"); name: "OpenIddictAuthorizations");
migrationBuilder.DropTable(
name: "PActivityItem");
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "PBackgroundWorker_MailQueue"); name: "PBackgroundWorker_MailQueue");
@ -4581,9 +4445,6 @@ namespace Kurs.Platform.Migrations
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "OpenIddictApplications"); name: "OpenIddictApplications");
migrationBuilder.DropTable(
name: "PActivity");
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "PBackgroundWorker_MailQueueTableFormat"); name: "PBackgroundWorker_MailQueueTableFormat");

View file

@ -648,6 +648,15 @@ namespace Kurs.Platform.Migrations
.HasColumnType("datetime2") .HasColumnType("datetime2")
.HasColumnName("DeletionTime"); .HasColumnName("DeletionTime");
b.Property<string>("EntityId")
.HasColumnType("nvarchar(max)");
b.Property<string>("EntityName")
.HasColumnType("nvarchar(max)");
b.Property<string>("FilesJson")
.HasColumnType("nvarchar(max)");
b.Property<bool>("IsDeleted") b.Property<bool>("IsDeleted")
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()
.HasColumnType("bit") .HasColumnType("bit")
@ -662,10 +671,6 @@ namespace Kurs.Platform.Migrations
.HasColumnType("uniqueidentifier") .HasColumnType("uniqueidentifier")
.HasColumnName("LastModifierId"); .HasColumnName("LastModifierId");
b.Property<string>("RecipientUserName")
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<string>("Subject") b.Property<string>("Subject")
.IsRequired() .IsRequired()
.HasMaxLength(256) .HasMaxLength(256)
@ -675,194 +680,15 @@ namespace Kurs.Platform.Migrations
.HasColumnType("uniqueidentifier") .HasColumnType("uniqueidentifier")
.HasColumnName("TenantId"); .HasColumnName("TenantId");
b.Property<int>("Type") b.Property<string>("Type")
.HasColumnType("int"); .IsRequired()
.HasColumnType("nvarchar(max)");
b.HasKey("Id"); b.HasKey("Id");
b.HasIndex("CreationTime");
b.HasIndex("CreatorId");
b.HasIndex("Type");
b.ToTable("PActivity", (string)null); b.ToTable("PActivity", (string)null);
}); });
modelBuilder.Entity("Kurs.Platform.Entities.ActivityFile", b =>
{
b.Property<Guid>("Id")
.HasColumnType("uniqueidentifier");
b.Property<Guid>("ActivityItemId")
.HasColumnType("uniqueidentifier");
b.Property<DateTime>("CreationTime")
.HasColumnType("datetime2")
.HasColumnName("CreationTime");
b.Property<Guid?>("CreatorId")
.HasColumnType("uniqueidentifier")
.HasColumnName("CreatorId");
b.Property<Guid?>("DeleterId")
.HasColumnType("uniqueidentifier")
.HasColumnName("DeleterId");
b.Property<DateTime?>("DeletionTime")
.HasColumnType("datetime2")
.HasColumnName("DeletionTime");
b.Property<string>("EntityId")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("nvarchar(128)");
b.Property<string>("EntityName")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("nvarchar(128)");
b.Property<string>("FileName")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<string>("FilePath")
.IsRequired()
.HasMaxLength(512)
.HasColumnType("nvarchar(512)");
b.Property<long>("FileSize")
.HasColumnType("bigint");
b.Property<string>("FileType")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<bool>("IsDeleted")
.ValueGeneratedOnAdd()
.HasColumnType("bit")
.HasDefaultValue(false)
.HasColumnName("IsDeleted");
b.Property<DateTime?>("LastModificationTime")
.HasColumnType("datetime2")
.HasColumnName("LastModificationTime");
b.Property<Guid?>("LastModifierId")
.HasColumnType("uniqueidentifier")
.HasColumnName("LastModifierId");
b.Property<Guid?>("TenantId")
.HasColumnType("uniqueidentifier")
.HasColumnName("TenantId");
b.HasKey("Id");
b.HasIndex("ActivityItemId");
b.HasIndex("CreatorId");
b.HasIndex("EntityId");
b.HasIndex("EntityName");
b.HasIndex("FileName");
b.ToTable("PActivityFile", (string)null);
});
modelBuilder.Entity("Kurs.Platform.Entities.ActivityItem", b =>
{
b.Property<Guid>("Id")
.HasColumnType("uniqueidentifier");
b.Property<Guid>("ActivityId")
.HasColumnType("uniqueidentifier");
b.Property<string>("Content")
.IsRequired()
.HasMaxLength(2000)
.HasColumnType("nvarchar(2000)");
b.Property<DateTime>("CreationTime")
.HasColumnType("datetime2")
.HasColumnName("CreationTime");
b.Property<Guid?>("CreatorId")
.HasColumnType("uniqueidentifier")
.HasColumnName("CreatorId");
b.Property<Guid?>("DeleterId")
.HasColumnType("uniqueidentifier")
.HasColumnName("DeleterId");
b.Property<DateTime?>("DeletionTime")
.HasColumnType("datetime2")
.HasColumnName("DeletionTime");
b.Property<string>("EntityId")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("nvarchar(128)");
b.Property<string>("EntityName")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("nvarchar(128)");
b.Property<bool>("IsDeleted")
.ValueGeneratedOnAdd()
.HasColumnType("bit")
.HasDefaultValue(false)
.HasColumnName("IsDeleted");
b.Property<DateTime?>("LastModificationTime")
.HasColumnType("datetime2")
.HasColumnName("LastModificationTime");
b.Property<Guid?>("LastModifierId")
.HasColumnType("uniqueidentifier")
.HasColumnName("LastModifierId");
b.Property<Guid?>("RecipientUserId")
.HasColumnType("uniqueidentifier");
b.Property<string>("RecipientUserName")
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<string>("Subject")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<Guid?>("TenantId")
.HasColumnType("uniqueidentifier")
.HasColumnName("TenantId");
b.Property<int>("Type")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("ActivityId");
b.HasIndex("CreatorId");
b.HasIndex("EntityId");
b.HasIndex("EntityName");
b.HasIndex("RecipientUserId");
b.HasIndex("Type");
b.ToTable("PActivityItem", (string)null);
});
modelBuilder.Entity("Kurs.Platform.Entities.AiBot", b => modelBuilder.Entity("Kurs.Platform.Entities.AiBot", b =>
{ {
b.Property<Guid>("Id") b.Property<Guid>("Id")
@ -8857,28 +8683,6 @@ namespace Kurs.Platform.Migrations
b.Navigation("NotificationRule"); b.Navigation("NotificationRule");
}); });
modelBuilder.Entity("Kurs.Platform.Entities.ActivityFile", b =>
{
b.HasOne("Kurs.Platform.Entities.ActivityItem", "ActivityItem")
.WithMany("AttachedFiles")
.HasForeignKey("ActivityItemId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("ActivityItem");
});
modelBuilder.Entity("Kurs.Platform.Entities.ActivityItem", b =>
{
b.HasOne("Kurs.Platform.Entities.Activity", "Activity")
.WithMany("ActivityItems")
.HasForeignKey("ActivityId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Activity");
});
modelBuilder.Entity("Kurs.Platform.Entities.ApiEndpoint", b => modelBuilder.Entity("Kurs.Platform.Entities.ApiEndpoint", b =>
{ {
b.HasOne("Kurs.Platform.Entities.CustomEntity", "Entity") b.HasOne("Kurs.Platform.Entities.CustomEntity", "Entity")
@ -9377,16 +9181,6 @@ namespace Kurs.Platform.Migrations
b.Navigation("Notifications"); b.Navigation("Notifications");
}); });
modelBuilder.Entity("Kurs.Platform.Entities.Activity", b =>
{
b.Navigation("ActivityItems");
});
modelBuilder.Entity("Kurs.Platform.Entities.ActivityItem", b =>
{
b.Navigation("AttachedFiles");
});
modelBuilder.Entity("Kurs.Platform.Entities.BlogCategory", b => modelBuilder.Entity("Kurs.Platform.Entities.BlogCategory", b =>
{ {
b.Navigation("Posts"); b.Navigation("Posts");

View file

@ -340,6 +340,14 @@ public class PlatformHttpApiHostModule : AbpModule
fileSystem.BasePath = configuration["App:ImportPath"]; fileSystem.BasePath = configuration["App:ImportPath"];
}); });
}); });
options.Containers.Configure<ActivityBlobContainer>(container =>
{
container.UseFileSystem(fileSystem =>
{
fileSystem.BasePath = configuration["App:ActivityPath"];
});
});
}); });
} }

View file

@ -7,6 +7,7 @@
"AttachmentsPath": "/etc/api/mail-queue/attachments", "AttachmentsPath": "/etc/api/mail-queue/attachments",
"CdnPath": "/etc/api/cdn", "CdnPath": "/etc/api/cdn",
"ImportPath": "/etc/api/import", "ImportPath": "/etc/api/import",
"ActivityPath": "/etc/api/activity",
"BaseDomain": "sozsoft.com" "BaseDomain": "sozsoft.com"
}, },
"ConnectionStrings": { "ConnectionStrings": {

View file

@ -7,6 +7,7 @@
"AttachmentsPath": "/etc/api/mail-queue/attachments", "AttachmentsPath": "/etc/api/mail-queue/attachments",
"CdnPath": "/etc/api/cdn", "CdnPath": "/etc/api/cdn",
"ImportPath": "/etc/api/import", "ImportPath": "/etc/api/import",
"ActivityPath": "/etc/api/activity",
"BaseDomain": "sozsoft.com" "BaseDomain": "sozsoft.com"
}, },
"ConnectionStrings": { "ConnectionStrings": {

View file

@ -7,6 +7,7 @@
"AttachmentsPath": "C:\\Private\\Projects\\sozsoft\\configs\\mail-queue\\attachments", "AttachmentsPath": "C:\\Private\\Projects\\sozsoft\\configs\\mail-queue\\attachments",
"CdnPath": "C:\\Private\\Projects\\sozsoft\\configs\\docker\\data\\cdn", "CdnPath": "C:\\Private\\Projects\\sozsoft\\configs\\docker\\data\\cdn",
"ImportPath": "C:\\Private\\Projects\\sozsoft\\configs\\docker\\data\\import", "ImportPath": "C:\\Private\\Projects\\sozsoft\\configs\\docker\\data\\import",
"ActivityPath": "C:\\Private\\Projects\\sozsoft\\configs\\docker\\data\\activity",
"Version": "1.0.1" "Version": "1.0.1"
}, },
"ConnectionStrings": { "ConnectionStrings": {

View file

@ -1,32 +1,11 @@
import { FullAuditedEntityDto } from "../abp"; import { AuditedEntityDto } from "../abp";
export interface ActivityDto extends FullAuditedEntityDto<string> { export interface ActivityDto extends AuditedEntityDto {
tenantId?: string; tenantId?: string;
type?: string; entityName: string;
subject?: string; entityId: string;
content?: string; type: string;
recipientUserName?: string; subject: string;
} content: string;
files: File[];
export interface ActivityFileDto extends FullAuditedEntityDto<string> {
tenantId?: string;
entityName?: string;
entityId?: string;
fileName?: string;
fileSize?: number;
fileType?: string;
filePath?: string;
activityItemId: string;
}
export interface ActivityItemDto extends FullAuditedEntityDto<string> {
tenantId?: string;
type?: string;
entityName?: string;
entityId?: string;
recipientUserId?: string;
recipientUserName?: string;
subject?: string;
content?: string;
activityId: string;
} }

View file

@ -1,36 +0,0 @@
export interface Activity {
id: string
type: 'note' | 'message'
subject: string
content: string
recipientUserName?: string
creatorId: string
creationTime: Date
data?: ActivityItem
}
export interface ActivityItem {
id?: string
type: 'note' | 'message'
entityName: string
entityId: string
recipientUserId?: string
recipientUserName?: string
subject: string
content: string
creatorId?: string
creationTime?: Date
attachedFiles?: ActivityFile[]
}
export interface ActivityFile {
id?: string
entityName: string
entityId: string
fileName: string
fileSize: number
fileType: string
filePath: string
creatorId?: string
creationTime?: Date
}

View file

@ -1,9 +1,11 @@
import { ActivityDto, ActivityItemDto, ActivityFileDto } from '@/proxy/activity/models' import { PagedResultDto } from '@/proxy'
import { ActivityDto } from '@/proxy/activity/models'
import apiService from '@/services/api.service' import apiService from '@/services/api.service'
import { AxiosError } from 'axios'
class ActivityService { class ActivityService {
async getList(params?: any): Promise<ActivityDto[]> { async getList(params?: any): Promise<PagedResultDto<ActivityDto>> {
const response = await apiService.fetchData<ActivityDto[]>({ const response = await apiService.fetchData<PagedResultDto<ActivityDto>>({
url: '/api/app/activity', url: '/api/app/activity',
method: 'GET', method: 'GET',
params, params,
@ -19,13 +21,45 @@ class ActivityService {
return response.data return response.data
} }
async create(data: ActivityDto): Promise<ActivityDto> { async create(data: ActivityDto): Promise<any> {
const response = await apiService.fetchData<ActivityDto>({ 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', url: '/api/app/activity',
method: 'POST', method: 'POST',
data: data as any, data: formData,
// Don't manually set the Content-Type header; let the browser handle it
}) })
return response.data return response.data
} catch (error) {
if (error instanceof AxiosError) {
console.error('Error creating activity:', error.response?.data)
return error.response?.data
} else {
console.error('Unexpected error:', error)
throw error
}
}
} }
async update(id: string, data: ActivityDto): Promise<ActivityDto> { async update(id: string, data: ActivityDto): Promise<ActivityDto> {
@ -43,92 +77,6 @@ class ActivityService {
method: 'DELETE', method: 'DELETE',
}) })
} }
// 🔹 ActivityItem kayıtları ---------------------------------------------------
async getItems(activityId?: string): Promise<ActivityItemDto[]> {
const response = await apiService.fetchData<ActivityItemDto[]>({
url: '/api/app/activity-item',
method: 'GET',
params: { activityId },
})
return response.data
}
async getItem(id: string): Promise<ActivityItemDto> {
const response = await apiService.fetchData<ActivityItemDto>({
url: `/api/app/activity-item/${id}`,
method: 'GET',
})
return response.data
}
async createItem(data: ActivityItemDto): Promise<ActivityItemDto> {
const response = await apiService.fetchData<ActivityItemDto>({
url: '/api/app/activity-item',
method: 'POST',
data: data as any,
})
return response.data
}
async updateItem(id: string, data: ActivityItemDto): Promise<ActivityItemDto> {
const response = await apiService.fetchData<ActivityItemDto>({
url: `/api/app/activity-item/${id}`,
method: 'PUT',
data: data as any,
})
return response.data
}
async deleteItem(id: string): Promise<void> {
await apiService.fetchData({
url: `/api/app/activity-item/${id}`,
method: 'DELETE',
})
}
// 🔹 ActivityFile kayıtları ---------------------------------------------------
async getFiles(activityItemId?: string): Promise<ActivityFileDto[]> {
const response = await apiService.fetchData<ActivityFileDto[]>({
url: '/api/app/activity-file',
method: 'GET',
params: { activityItemId },
})
return response.data
}
async getFile(id: string): Promise<ActivityFileDto> {
const response = await apiService.fetchData<ActivityFileDto>({
url: `/api/app/activity-file/${id}`,
method: 'GET',
})
return response.data
}
async uploadFile(data: ActivityFileDto): Promise<ActivityFileDto> {
const response = await apiService.fetchData<ActivityFileDto>({
url: '/api/app/activity-file',
method: 'POST',
data: data as any,
})
return response.data
}
async updateFile(id: string, data: ActivityFileDto): Promise<ActivityFileDto> {
const response = await apiService.fetchData<ActivityFileDto>({
url: `/api/app/activity-file/${id}`,
method: 'PUT',
data: data as any,
})
return response.data
}
async deleteFile(id: string): Promise<void> {
await apiService.fetchData({
url: `/api/app/activity-file/${id}`,
method: 'DELETE',
})
}
} }
export const activityService = new ActivityService() export const activityService = new ActivityService()

View file

@ -17,6 +17,7 @@ const schema = object().shape({
u: string(), u: string(),
d: string(), d: string(),
e: string(), e: string(),
i: string(),
}) })
function FormTabPermissions(props: FormEditProps) { function FormTabPermissions(props: FormEditProps) {
@ -183,6 +184,31 @@ function FormTabPermissions(props: FormEditProps) {
)} )}
</Field> </Field>
</FormItem> </FormItem>
<FormItem
label={translate('::ListForms.ListFormEdit.PermissionsImport')}
invalid={errors.permissionDto?.i && touched.permissionDto?.i}
errorMessage={errors.permissionDto?.i}
>
<Field
type="text"
autoComplete="off"
name="permissionDto.i"
placeholder={translate('::ListForms.ListFormEdit.PermissionsImport')}
>
{({ field, form }: FieldProps<SelectBoxOption>) => (
<Select
field={field}
form={form}
isClearable={true}
options={permissions}
value={permissions?.filter(
(option) => option.value === values.permissionDto.i,
)}
onChange={(option) => form.setFieldValue(field.name, option?.value)}
/>
)}
</Field>
</FormItem>
<Button block variant="solid" loading={isSubmitting} type="submit"> <Button block variant="solid" loading={isSubmitting} type="submit">
{isSubmitting ? translate('::SavingWithThreeDot') : translate('::Save')} {isSubmitting ? translate('::SavingWithThreeDot') : translate('::Save')}

View file

@ -9,31 +9,19 @@ import {
FaClock, FaClock,
} from 'react-icons/fa' } from 'react-icons/fa'
import { Button } from '@/components/ui' import { Button } from '@/components/ui'
import { Activity } from '@/proxy/formActivity/models' import { ActivityDto } from '@/proxy/activity/models'
interface ActivityListProps { interface ActivityListProps {
activities: Activity[] activities: ActivityDto[]
onDeleteActivity?: (activityId: string) => void onDeleteActivity?: (activityId: string) => void
onDeleteFile?: (fileId: string) => void
onDownloadFile?: (fileData: any) => void onDownloadFile?: (fileData: any) => void
} }
export const ActivityList: React.FC<ActivityListProps> = ({ export const ActivityList: React.FC<ActivityListProps> = ({
activities, activities,
onDeleteActivity, onDeleteActivity,
onDeleteFile,
onDownloadFile, onDownloadFile,
}) => { }) => {
const formatDate = (date: Date) => {
return new Intl.DateTimeFormat('tr-TR', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
hour: '2-digit',
minute: '2-digit',
}).format(new Date(date))
}
const getActivityIcon = (type: string) => { const getActivityIcon = (type: string) => {
switch (type) { switch (type) {
case 'note': case 'note':
@ -45,9 +33,9 @@ export const ActivityList: React.FC<ActivityListProps> = ({
} }
} }
const handleDelete = (activity: Activity) => { // const handleDelete = (activity: ActivityDto) => {
onDeleteActivity?.(activity.id) // onDeleteActivity?.(activity)
} // }
const handleDownloadFile = (fileData: any) => { const handleDownloadFile = (fileData: any) => {
onDownloadFile?.(fileData) onDownloadFile?.(fileData)
@ -76,18 +64,12 @@ export const ActivityList: React.FC<ActivityListProps> = ({
<div className="flex items-center gap-1 font-semibold mb-1"> <div className="flex items-center gap-1 font-semibold mb-1">
<FaUser className="text-xs" /> <FaUser className="text-xs" />
<span>{activity.creatorId}</span> <span>{activity.creatorId}</span>
{activity.recipientUserName && (
<>
<span></span>
<FaUser className="text-xs" />
<span>{activity.recipientUserName}</span>
</>
)}
</div> </div>
{activity.subject && ( {activity.subject && (
<p className="text-sm font-medium text-gray-800 mb-1 break-words">{activity.subject}</p> <p className="text-sm font-medium text-gray-800 mb-1 break-words">
{activity.subject}
</p>
)} )}
{activity.content && ( {activity.content && (
@ -95,7 +77,7 @@ export const ActivityList: React.FC<ActivityListProps> = ({
)} )}
{/* Note tipinde dosyaları göster */} {/* Note tipinde dosyaları göster */}
{activity.type === 'note' && activity.data && (activity.data as any).attachedFiles?.length > 0 && ( {/* {activity.type === 'note' && activity.data && (activity.data as any).attachedFiles?.length > 0 && (
<div className="mt-2 mb-2"> <div className="mt-2 mb-2">
{((activity.data as any).attachedFiles || []).map((file: any, index: number) => ( {((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 key={index} className="flex items-center justify-between p-2 bg-gray-50 rounded border">
@ -116,19 +98,19 @@ export const ActivityList: React.FC<ActivityListProps> = ({
</div> </div>
))} ))}
</div> </div>
)} )} */}
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex items-center gap-1 text-xs text-gray-500"> <div className="flex items-center gap-1 text-xs text-gray-500">
<FaClock /> <FaClock />
{formatDate(activity.creationTime)} {activity.creationTime?.toLocaleString()}
</div> </div>
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<Button <Button
variant="plain" variant="plain"
size="xs" size="xs"
onClick={() => handleDelete(activity)} // onClick={() => handleDelete(activity)}
title="Sil" title="Sil"
> >
<FaTrash className="text-red-500" /> <FaTrash className="text-red-500" />

View file

@ -0,0 +1,191 @@
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 * as Yup from 'yup'
import { activityService } from '@/services/activity.service'
// 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 {
entityName: string
entityId: string
isOpen: boolean
onClose: () => void
}
export const AddContentModal: React.FC<AddContentModalProps> = ({
entityName,
entityId,
isOpen,
onClose,
}) => {
const [uploading, setUploading] = useState(false)
const [files, setFiles] = useState<string[] | undefined>()
const types: SelectBoxOption[] = [
{ 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 })
//onClose() // Modal'ı kapat
} catch (error) {
console.error('Save failed:', error)
} 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`
}
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[]) => {
if (file.length === 0) {
setFiles(undefined)
return
}
setFiles(file.map((f) => URL.createObjectURL(f)))
}
return (
<Dialog isOpen={isOpen} onClose={onClose}>
<div className="p-2 w-full mx-auto">
<div className="flex items-center justify-between mb-6">
<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
</h3>
</div>
<Formik
initialValues={{
type: 'note',
subject: '',
content: '',
}}
validationSchema={validationSchema}
onSubmit={handleSave}
>
{({ values, touched, errors, setFieldValue, isSubmitting }) => (
<Form>
<FormContainer size="sm">
{/* Not Tipi */}
<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>
</FormItem>
{/* Konu */}
<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}
/>
)}
/>
</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}
beforeUpload={beforeUpload}
onChange={onChooseImage}
>
<Button icon={<FaFileUpload />} type="button"></Button>
</Upload>
</div>
</FormItem>
</FormContainer>
<div className="mt-4 flex justify-between items-center pt-4 border-t border-gray-200">
<Button variant="default" size="md" onClick={onClose} disabled={uploading}>
İptal
</Button>
<Button
variant="solid"
size="md"
type="submit"
disabled={isSubmitting || !values.subject || !values.type}
className="px-6 flex items-center gap-2"
>
{uploading ? 'Yükleniyor...' : 'Ekle'}
</Button>
</div>
</Form>
)}
</Formik>
</div>
</Dialog>
)
}

View file

@ -1,531 +0,0 @@
import React, { useState, useEffect } from 'react'
import { Button, Input, Dialog, Select } from '@/components/ui'
import { FaFileUpload, FaEnvelope, FaStickyNote, FaUsers, FaTimes, FaPlus, FaTrash, FaPaperclip } from 'react-icons/fa'
import { ActivityFile } from '@/proxy/formActivity/models'
import { getUsers } from '@/services/identity.service'
import { IdentityUserDto } from '@/proxy/admin/models'
// Birleştirilmiş Not ve Dosya Ekleme Modal'ı
interface AddContentModalProps {
isOpen: boolean
onClose: () => void
onSaveContent: (subject: string, content: string, files: File[]) => Promise<any>
}
export const AddContentModal: React.FC<AddContentModalProps> = ({
isOpen,
onClose,
onSaveContent
}) => {
const [subject, setSubject] = useState('')
const [content, setContent] = useState('')
const [selectedFiles, setSelectedFiles] = useState<File[]>([])
const [uploading, setUploading] = useState(false)
const handleFileSelect = (files: FileList | null) => {
if (files && files.length > 0) {
const newFiles = Array.from(files)
setSelectedFiles(prev => [...prev, ...newFiles])
}
}
const removeFile = (index: number) => {
setSelectedFiles(prev => prev.filter((_, i) => i !== index))
}
const handleSave = async () => {
if (content.trim() || selectedFiles.length > 0) {
setUploading(true)
try {
await onSaveContent(subject.trim(), content.trim(), selectedFiles)
resetForm()
onClose()
} catch (error) {
console.error('Save failed:', error)
} finally {
setUploading(false)
}
}
}
const resetForm = () => {
setContent('')
setSelectedFiles([])
setUploading(false)
}
const handleClose = () => {
resetForm()
onClose()
}
const isFormValid = content.trim() || selectedFiles.length > 0
const totalFileSize = selectedFiles.reduce((total, file) => total + file.size, 0)
return (
<Dialog isOpen={isOpen} onClose={handleClose} onRequestClose={handleClose}>
<div className="p-6 w-full mx-auto">
<div className="flex items-center justify-between mb-6">
<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
</h3>
</div>
<div className="space-y-3 mb-2">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Konu <span className="text-red-500">*</span>
</label>
<Input
value={subject}
onChange={(e) => setSubject(e.target.value)}
placeholder="Mesajın konusunu girin..."
className="w-full"
size="sm"
/>
</div>
{/* Not Alanı */}
<div>
<label className="flex items-center gap-2 text-sm font-medium text-gray-700 mb-2">
<FaStickyNote className="text-yellow-500" />
Not İçeriği <span className="text-red-500">*</span>
</label>
<textarea
value={content}
onChange={(e) => setContent(e.target.value)}
placeholder="Notunuzu buraya yazın... (İsteğe bağlı)"
className="w-full h-32 p-4 border border-gray-300 rounded-md resize-none focus:ring-2 focus:ring-purple-500 focus:border-transparent transition-colors duration-200"
style={{ fontSize: '14px', lineHeight: '1.5' }}
/>
<div className="mt-1 text-xs text-gray-500 text-right">
{content.length} karakter
</div>
</div>
{/* Dosya Yükleme Alanı */}
<div>
<label className="flex items-center gap-2 text-sm font-medium text-gray-700 mb-2">
<FaPaperclip className="text-blue-500" />
Dosya Ekle
</label>
<div className="border-2 border-dashed border-gray-300 rounded-lg p-3 text-center hover:border-purple-400 transition-colors duration-200">
<input
type="file"
multiple
onChange={(e) => handleFileSelect(e.target.files)}
className="hidden"
id="file-upload"
disabled={uploading}
/>
<label
htmlFor="file-upload"
className="cursor-pointer flex flex-col items-center gap-2"
>
<FaFileUpload className="text-2xl text-gray-400" />
<div className="text-sm text-gray-600">
<span className="font-medium text-purple-600 hover:text-purple-700">
Dosya seçmek için tıklayın
</span>
<div className="text-xs text-gray-500 mt-1">
Birden fazla dosya seçebilirsiniz
</div>
</div>
</label>
</div>
{/* Seçili Dosyalar */}
{selectedFiles.length > 0 && (
<div className="mt-4 space-y-2">
<div className="text-sm font-medium text-gray-700">
Seçili Dosyalar ({selectedFiles.length})
</div>
<div className="space-y-2 max-h-32 overflow-y-auto">
{selectedFiles.map((file, index) => (
<div key={index} className="flex items-center justify-between p-3 bg-gray-50 rounded-md">
<div className="flex-1 min-w-0">
<div className="font-medium text-sm truncate">{file.name}</div>
<div className="text-xs text-gray-500">
{(file.size / 1024).toFixed(2)} KB {file.type || 'Bilinmeyen tür'}
</div>
</div>
<Button
variant="plain"
size="xs"
onClick={() => removeFile(index)}
className="ml-2 text-red-500 hover:text-red-700"
disabled={uploading}
>
<FaTrash />
</Button>
</div>
))}
</div>
<div className="text-xs text-gray-500">
Toplam boyut: {(totalFileSize / 1024).toFixed(2)} KB
</div>
</div>
)}
</div>
</div>
<div className="flex justify-between items-center pt-4 border-t border-gray-200">
<div className="flex gap-3">
<Button
variant="default"
size="md"
onClick={handleClose}
disabled={uploading}
className="px-6"
>
İptal
</Button>
<Button
variant="solid"
size="md"
onClick={handleSave}
disabled={!isFormValid || uploading}
className="px-6 flex items-center gap-2"
>
{uploading ? (
<>
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white"></div>
Yükleniyor...
</>
) : (
<>
<FaPlus />
Ekle
</>
)}
</Button>
</div>
</div>
</div>
</Dialog>
)
}
// Eski modallar geriye dönük uyumluluk için korundu
interface AddNoteModalProps {
isOpen: boolean
onClose: () => void
onSave: (content: string) => void
}
export const AddNoteModal: React.FC<AddNoteModalProps> = ({ isOpen, onClose, onSave }) => {
return (
<AddContentModal
isOpen={isOpen}
onClose={onClose}
onSaveContent={async (content: string) => {
if (content) {
onSave(content)
}
}}
/>
)
}
interface AddFileModalProps {
isOpen: boolean
onClose: () => void
onUpload: (file: File) => Promise<ActivityFile>
}
export const AddFileModal: React.FC<AddFileModalProps> = ({ isOpen, onClose, onUpload }) => {
return (
<AddContentModal
isOpen={isOpen}
onClose={onClose}
onSaveContent={async (subject: string, content: string, files: File[]) => {
for (const file of files) {
await onUpload(file)
}
}}
/>
)
}
interface UserOption {
value: string
label: string
email?: string
isActive: boolean
}
interface SendMessageModalProps {
isOpen: boolean
onClose: () => void
onSend: (recipients: UserOption[], subject: string, content: string) => void
}
interface MessageTemplate {
value: string
label: string
subject?: string
content?: string
}
const MESSAGE_TEMPLATES: MessageTemplate[] = [
{ value: '', label: 'Şablon seçin...' },
{ value: 'follow_up', label: 'Takip Mesajı', subject: 'Form Durumu', content: 'Merhaba,\n\nForm ile ilgili güncelleme bekliyoruz. Lütfen en kısa sürede dönüş yapabilir misiniz?\n\nTeşekkürler.' },
{ value: 'approval', label: 'Onay Talebi', subject: 'Form İncelemesi', content: 'Sayın yetkili,\n\nForm incelemenizi ve onayınızı rica ediyoruz.\n\nSaygılarımla.' },
{ value: 'rejection', label: 'Red Bildirimi', subject: 'Form Red Edildi', content: 'Merhaba,\n\nForm talebiniz aşağıdaki nedenlerle red edilmiştir:\n\n- [Nedeni buraya yazın]\n\nDüzeltme sonrası tekrar başvurabilirsiniz.' },
{ value: 'completion', label: 'Tamamlanma Bildirimi', subject: 'Form Tamamlandı', content: 'Tebrikler!\n\nForm işleminiz başarıyla tamamlanmıştır.\n\nİyi günler.' }
]
export const SendMessageModal: React.FC<SendMessageModalProps> = ({ isOpen, onClose, onSend }) => {
const [selectedUsers, setSelectedUsers] = useState<UserOption[]>([])
const [subject, setSubject] = useState('')
const [content, setContent] = useState('')
const [users, setUsers] = useState<UserOption[]>([])
const [isLoadingUsers, setIsLoadingUsers] = useState(false)
const [error, setError] = useState<string>('')
const [selectedTemplate, setSelectedTemplate] = useState('')
// Load users when modal opens
useEffect(() => {
if (isOpen) {
loadUsers()
}
}, [isOpen])
const loadUsers = async () => {
setIsLoadingUsers(true)
setError('')
try {
const response = await getUsers(0, 100) // Get first 100 users
const userOptions: UserOption[] = (response.data.items || [])
.filter((user: IdentityUserDto) => user.isActive && user.userName)
.map((user: IdentityUserDto) => ({
value: user.id || '',
label: `${user.name} ${user.surname}` || '',
email: user.email,
isActive: user.isActive
}))
setUsers(userOptions)
} catch (err) {
console.error('Failed to load users:', err)
setError('Kullanıcılar yüklenirken hata oluştu')
} finally {
setIsLoadingUsers(false)
}
}
const handleSend = () => {
if (selectedUsers.length > 0 && subject.trim() && content.trim()) {
onSend(selectedUsers, subject.trim(), content.trim())
resetForm()
onClose()
}
}
const handleTemplateChange = (templateValue: string) => {
setSelectedTemplate(templateValue)
const template = MESSAGE_TEMPLATES.find(t => t.value === templateValue)
if (template && template.value && template.subject && template.content) {
setSubject(template.subject)
setContent(template.content)
}
}
const resetForm = () => {
setSelectedUsers([])
setSubject('')
setContent('')
setError('')
setSelectedTemplate('')
}
const handleClose = () => {
resetForm()
onClose()
}
const isFormValid = selectedUsers.length > 0 && subject.trim() && content.trim()
const customStyles = {
control: (provided: any) => ({
...provided,
minHeight: '42px',
borderRadius: '6px',
borderColor: '#d1d5db',
'&:hover': {
borderColor: '#9ca3af',
},
'&:focus-within': {
borderColor: '#3b82f6',
boxShadow: '0 0 0 2px rgba(59, 130, 246, 0.1)',
},
}),
multiValue: (provided: any) => ({
...provided,
backgroundColor: '#e0e7ff',
borderRadius: '4px',
}),
multiValueLabel: (provided: any) => ({
...provided,
color: '#3730a3',
fontSize: '14px',
}),
multiValueRemove: (provided: any) => ({
...provided,
color: '#6b7280',
'&:hover': {
backgroundColor: '#f87171',
color: 'white',
},
}),
}
return (
<Dialog isOpen={isOpen} onClose={handleClose} onRequestClose={handleClose}>
<div className="p-6 w-full mx-auto">
<div className="flex items-center justify-between mb-6">
<h3 className="text-xl font-semibold flex items-center gap-3">
<div className="p-2 bg-green-100 rounded-full">
<FaEnvelope className="text-green-600 text-lg" />
</div>
Yeni Mesaj Gönder
</h3>
</div>
{error && (
<div className="mb-4 p-3 bg-red-50 border border-red-200 rounded-md">
<p className="text-sm text-red-600">{error}</p>
</div>
)}
<div className="space-y-5 mb-6">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Mesaj Şablonu
</label>
<Select
value={MESSAGE_TEMPLATES.find(t => t.value === selectedTemplate)}
onChange={(selected) => {
const template = selected as MessageTemplate
handleTemplateChange(template?.value || '')
}}
options={MESSAGE_TEMPLATES}
placeholder="İsteğe bağlı: Hazır şablon seçin..."
isSearchable={false}
isClearable
styles={{
control: (provided: any) => ({
...provided,
minHeight: '38px',
borderRadius: '6px',
borderColor: '#e5e7eb',
}),
}}
/>
</div>
<div>
<label className="flex items-center gap-2 text-sm font-medium text-gray-700 mb-2">
<FaUsers className="text-blue-500" />
Alıcılar <span className="text-red-500">*</span>
</label>
<Select
isMulti
isLoading={isLoadingUsers}
value={selectedUsers}
onChange={(selected) => setSelectedUsers(selected as UserOption[])}
options={users}
placeholder={isLoadingUsers ? "Kullanıcılar yükleniyor..." : "Kullanıcıları seçin..."}
noOptionsMessage={() => "Kullanıcı bulunamadı"}
loadingMessage={() => "Kullanıcılar yükleniyor..."}
isSearchable
styles={customStyles}
closeMenuOnSelect={false}
hideSelectedOptions={false}
formatOptionLabel={(option: UserOption) => (
<div className="flex items-center justify-between py-1">
<div>
<div className="font-medium text-gray-900">{option.label}</div>
</div>
<div className={`w-2 h-2 rounded-full ${option.isActive ? 'bg-green-400' : 'bg-gray-300'}`} />
</div>
)}
/>
{selectedUsers.length > 0 && (
<div className="mt-2 flex items-center justify-between">
<div className="text-sm text-gray-600">
{selectedUsers.length} kullanıcı seçildi
</div>
<div className="flex gap-2">
<button
type="button"
onClick={() => setSelectedUsers([])}
className="text-xs text-gray-500 hover:text-red-500 underline"
>
Tümünü Kaldır
</button>
</div>
</div>
)}
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Konu <span className="text-red-500">*</span>
</label>
<Input
value={subject}
onChange={(e) => setSubject(e.target.value)}
placeholder="Mesajın konusunu girin..."
className="w-full"
size="sm"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Mesaj İçeriği <span className="text-red-500">*</span>
</label>
<textarea
value={content}
onChange={(e) => setContent(e.target.value)}
placeholder="Mesajınızı buraya yazın..."
className="w-full h-40 p-4 border border-gray-300 rounded-md resize-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-colors duration-200"
style={{ fontSize: '14px', lineHeight: '1.5' }}
/>
<div className="mt-1 text-xs text-gray-500 text-right">
{content.length} karakter
</div>
</div>
</div>
<div className="flex justify-between items-center pt-4 border-t border-gray-200">
<div className="flex gap-3">
<Button
variant="default"
size="md"
onClick={handleClose}
className="px-6"
>
İptal
</Button>
<Button
variant="solid"
size="md"
onClick={handleSend}
disabled={!isFormValid}
className="px-6 flex items-center gap-2"
>
<FaEnvelope />
Gönder
</Button>
</div>
</div>
</div>
</Dialog>
)
}

View file

@ -1,16 +1,10 @@
import React, { useState, useRef, useEffect } from 'react' import React, { useState, useRef, useEffect } from 'react'
import { useFormActivity } from './useFormActivity' import { AddContentModal } from './ActivityModal'
import { AddContentModal, SendMessageModal } from './ActivityModals'
import { ActivityList } from './ActivityList' import { ActivityList } from './ActivityList'
import { Button, Badge } from '@/components/ui' import { Button, Badge } from '@/components/ui'
import { import { FaChevronLeft, FaChevronRight, FaPlus, FaTimes, FaGripVertical } from 'react-icons/fa'
FaChevronLeft, import { activityService } from '@/services/activity.service'
FaChevronRight, import { ActivityDto } from '@/proxy/activity/models'
FaPlus,
FaEnvelope,
FaTimes,
FaGripVertical,
} from 'react-icons/fa'
interface ActivityPanelProps { interface ActivityPanelProps {
entityName: string entityName: string
@ -19,30 +13,33 @@ interface ActivityPanelProps {
onToggle: () => void onToggle: () => void
} }
export const FormActivityPanel: React.FC<ActivityPanelProps> = ({ export const ActivityPanel: React.FC<ActivityPanelProps> = ({
entityName, entityName,
entityId, entityId,
isVisible, isVisible,
onToggle, onToggle,
}) => { }) => {
const [activeTab, setActiveTab] = useState<'activities' | 'notes' | 'messages'>('activities')
const [showAddContent, setShowAddContent] = useState(false) const [showAddContent, setShowAddContent] = useState(false)
const [showSendMessage, setShowSendMessage] = useState(false)
// Draggable button state // Draggable button state
const [buttonPosition, setButtonPosition] = useState({ top: '50%' }) 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 { useEffect(() => {
activities, const fetchActivities = async () => {
loading, // Simulate API call to fetch activities
deleteActivity, // Gerçek implementasyonda API'dan veri çekilecek
deleteFile, const fetchedActivities = await activityService.getList()
addNote, if (fetchedActivities && fetchedActivities.items) {
sendMessage, setActivities(fetchedActivities.items)
} = useFormActivity(entityName, entityId) }
}
fetchActivities()
}, [isVisible])
const handleDownloadFile = (fileData: any) => { const handleDownloadFile = (fileData: any) => {
// Simulate file download - gerçek implementasyonda API'dan dosya indirme yapılacak // Simulate file download - gerçek implementasyonda API'dan dosya indirme yapılacak
@ -53,8 +50,6 @@ export const FormActivityPanel: React.FC<ActivityPanelProps> = ({
} }
const getTotalCount = () => activities.length const getTotalCount = () => activities.length
const getNoteCount = () => activities.filter(a => a.type === 'note').length
const getMessageCount = () => activities.filter(a => a.type === 'message').length
// Mouse drag handlers // Mouse drag handlers
const handleMouseDown = (e: React.MouseEvent) => { const handleMouseDown = (e: React.MouseEvent) => {
@ -144,6 +139,23 @@ export const FormActivityPanel: React.FC<ActivityPanelProps> = ({
return null 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 */} {/* Toggle Button - Draggable */}
@ -171,7 +183,7 @@ export const FormActivityPanel: React.FC<ActivityPanelProps> = ({
<Button <Button
variant="solid" variant="solid"
size="sm" size="sm"
shape='none' shape="none"
className="!rounded-l-full !rounded-r-none" className="!rounded-l-full !rounded-r-none"
onClick={(e) => { onClick={(e) => {
e.stopPropagation() e.stopPropagation()
@ -189,10 +201,7 @@ export const FormActivityPanel: React.FC<ActivityPanelProps> = ({
{/* Overlay - Click outside to close */} {/* Overlay - Click outside to close */}
{isVisible && ( {isVisible && (
<div <div className="fixed inset-0 bg-black bg-opacity-25 z-20" onClick={onToggle} />
className="fixed inset-0 bg-black bg-opacity-25 z-20"
onClick={onToggle}
/>
)} )}
{/* Activity Panel */} {/* Activity Panel */}
@ -216,7 +225,7 @@ export const FormActivityPanel: React.FC<ActivityPanelProps> = ({
<div className="text-sm text-gray-600 mb-3 flex items-center gap-2"> <div className="text-sm text-gray-600 mb-3 flex items-center gap-2">
<span className="font-medium">{entityName}</span> <span className="font-medium">{entityName}</span>
<code className="bg-gray-100 px-2 rounded text-gray-800 text-xs font-mono"> <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} /> <Badge className="bg-blue-100 text-blue-600" content={entityId} />
</code> </code>
</div> </div>
@ -231,105 +240,26 @@ export const FormActivityPanel: React.FC<ActivityPanelProps> = ({
<FaPlus className="mr-1" /> <FaPlus className="mr-1" />
Not Ekle Not Ekle
</Button> </Button>
<Button
variant="solid"
size="xs"
onClick={() => setShowSendMessage(true)}
className="flex justify-center items-center py-4 w-full"
>
<FaEnvelope className="mr-1" />
Mesaj Gönder
</Button>
</div>
{/* Tabs */}
<div className="flex border-b border-gray-200">
<button
onClick={() => setActiveTab('activities')}
className={`px-3 py-2 text-sm font-medium border-b-2 transition-colors ${
activeTab === 'activities'
? 'border-blue-500 text-blue-600'
: 'border-transparent text-gray-500 hover:text-gray-700'
}`}
>
Tümü ({getTotalCount()})
</button>
<button
onClick={() => setActiveTab('notes')}
className={`px-3 py-2 text-sm font-medium border-b-2 transition-colors ${
activeTab === 'notes'
? 'border-blue-500 text-blue-600'
: 'border-transparent text-gray-500 hover:text-gray-700'
}`}
>
Notlar ({getNoteCount()})
</button>
<button
onClick={() => setActiveTab('messages')}
className={`px-3 py-2 text-sm font-medium border-b-2 transition-colors ${
activeTab === 'messages'
? 'border-blue-500 text-blue-600'
: 'border-transparent text-gray-500 hover:text-gray-700'
}`}
>
Mesajlar ({getMessageCount()})
</button>
</div> </div>
</div> </div>
{/* Content */} {/* Content */}
<div className="flex-1 overflow-y-auto p-4"> <div className="flex-1 overflow-y-auto p-4">
{loading ? (
<div className="flex items-center justify-center h-32">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
<span className="ml-2 text-gray-600">Yükleniyor...</span>
</div>
) : (
<>
{activeTab === 'activities' && (
<ActivityList <ActivityList
activities={activities} activities={activities}
onDeleteActivity={deleteActivity} // onDeleteActivity={deleteActivity}
onDeleteFile={deleteFile}
onDownloadFile={handleDownloadFile} onDownloadFile={handleDownloadFile}
/> />
)}
{activeTab === 'notes' && (
<ActivityList
activities={activities.filter((a) => a.type === 'note')}
onDeleteActivity={deleteActivity}
onDeleteFile={deleteFile}
onDownloadFile={handleDownloadFile}
/>
)}
{activeTab === 'messages' && (
<ActivityList
activities={activities.filter((a) => a.type === 'message')}
onDeleteActivity={deleteActivity}
onDeleteFile={deleteFile}
onDownloadFile={handleDownloadFile}
/>
)}
</>
)}
</div> </div>
</div> </div>
</div> </div>
{/* Modals */} {/* Modals */}
<AddContentModal <AddContentModal
entityName={entityName}
entityId={entityId}
isOpen={showAddContent} isOpen={showAddContent}
onClose={() => setShowAddContent(false)} onClose={() => setShowAddContent(false)}
onSaveContent={addNote}
/>
<SendMessageModal
isOpen={showSendMessage}
onClose={() => setShowSendMessage(false)}
onSend={sendMessage}
/> />
</> </>
) )

View file

@ -1,317 +0,0 @@
import { ActivityItem, ActivityFile, Activity } from '@/proxy/formActivity/models'
import { ActivityDto, ActivityItemDto, ActivityFileDto } from '@/proxy/activity/models'
import { activityService } from '@/services/activity.service'
import { useStoreState } from '@/store/store'
import { useState, useEffect } from 'react'
export const useFormActivity = (entityName: string, entityId: string) => {
const [activityItems, setActivityItems] = useState<ActivityItem[]>([])
const [files, setFiles] = useState<ActivityFile[]>([])
const [activities, setActivities] = useState<Activity[]>([])
const [loading, setLoading] = useState(false)
const { user } = useStoreState((state) => state.auth)
// Load data from API on component mount
useEffect(() => {
if (!entityName || !entityId) return
loadActivitiesFromAPI()
}, [entityName, entityId])
const loadActivitiesFromAPI = async () => {
try {
setLoading(true)
// Tüm ActivityItem'ları al
const items = await activityService.getItems()
// EntityName ve EntityId ile filtrele
const filteredItems = items.filter(
item => item.entityName === entityName && item.entityId === entityId
)
// DTO'dan ActivityItem'a dönüştür
const convertedItems: ActivityItem[] = filteredItems.map(dto => ({
id: dto.id,
type: (dto.type as 'note' | 'message') || 'note',
entityName: dto.entityName || entityName,
entityId: dto.entityId || entityId,
recipientUserId: dto.recipientUserId,
recipientUserName: dto.recipientUserName,
subject: dto.subject || '',
content: dto.content || '',
creatorId: dto.creatorId || user.id,
creationTime: dto.creationTime ? new Date(dto.creationTime) : new Date(),
attachedFiles: []
}))
// Her ActivityItem için dosyaları yükle
for (const item of convertedItems) {
if (item.id) {
try {
const itemFiles = await activityService.getFiles(item.id)
const convertedFiles: ActivityFile[] = itemFiles.map(fileDto => ({
id: fileDto.id,
entityName: fileDto.entityName || entityName,
entityId: fileDto.entityId || entityId,
fileName: fileDto.fileName || '',
fileSize: fileDto.fileSize || 0,
fileType: fileDto.fileType || '',
filePath: fileDto.filePath || '',
creatorId: fileDto.creatorId || user.id,
creationTime: fileDto.creationTime ? new Date(fileDto.creationTime) : new Date(),
}))
item.attachedFiles = convertedFiles
setFiles(prev => [...prev, ...convertedFiles])
} catch (error) {
console.error('Failed to load files for activity item:', item.id, error)
}
}
}
setActivityItems(convertedItems)
} catch (error) {
console.error('Failed to load activities from API:', error)
} finally {
setLoading(false)
}
}
// Update activities when data changes
useEffect(() => {
const allActivities: Activity[] = []
// Convert all activity items to activities
activityItems.forEach((item) => {
allActivities.push({
id: item.id || `${item.type}_${Date.now()}`,
type: item.type,
subject: item.subject || '',
content: item.content,
recipientUserName: item.recipientUserName,
creationTime: item.creationTime || new Date(),
creatorId: item.creatorId || 'Bilinmeyen',
data: item,
})
})
// Sort by timestamp (newest first)
allActivities.sort(
(a, b) => new Date(b.creationTime).getTime() - new Date(a.creationTime).getTime(),
)
setActivities(allActivities)
}, [activityItems, files])
const addNote = async (subject: string, content: string, files: File[]) => {
try {
setLoading(true)
// Önce Activity oluştur
const activityDto: Partial<ActivityDto> = {
type: 'note',
subject: subject || 'Not',
content: content || (files.length > 0 ? `${files.length} dosya eklendi` : ''),
}
const createdActivity = await activityService.create(activityDto as ActivityDto)
// ActivityItem oluştur
const activityItemDto: Partial<ActivityItemDto> = {
activityId: createdActivity.id,
type: 'note',
entityName,
entityId,
subject: subject || 'Not',
content: content || (files.length > 0 ? `${files.length} dosya eklendi` : ''),
}
const createdActivityItem = await activityService.createItem(activityItemDto as ActivityItemDto)
// Dosyaları yükle
const uploadedFiles: ActivityFile[] = []
for (const file of files) {
try {
const activityFileDto: Partial<ActivityFileDto> = {
activityItemId: createdActivityItem.id,
entityName,
entityId,
fileName: file.name,
fileSize: file.size,
fileType: file.type,
filePath: `uploads/${entityName}/${entityId}/${file.name}`,
}
const createdFile = await activityService.uploadFile(activityFileDto as ActivityFileDto)
const newFile: ActivityFile = {
id: createdFile.id,
entityName: createdFile.entityName || entityName,
entityId: createdFile.entityId || entityId,
fileName: createdFile.fileName || file.name,
fileSize: createdFile.fileSize || file.size,
fileType: createdFile.fileType || file.type,
filePath: createdFile.filePath || '',
creatorId: createdFile.creatorId || user.id,
creationTime: createdFile.creationTime ? new Date(createdFile.creationTime) : new Date(),
}
uploadedFiles.push(newFile)
} catch (error) {
console.error('File upload failed:', error)
}
}
// Yeni ActivityItem'ı local state'e ekle
const newActivityItem: ActivityItem = {
id: createdActivityItem.id,
type: 'note',
entityName,
entityId,
subject: createdActivityItem.subject || '',
content: createdActivityItem.content || '',
creatorId: createdActivityItem.creatorId || user.id,
creationTime: createdActivityItem.creationTime ? new Date(createdActivityItem.creationTime) : new Date(),
attachedFiles: uploadedFiles
}
setActivityItems((prev) => [...prev, newActivityItem])
if (uploadedFiles.length > 0) {
setFiles((prev) => [...prev, ...uploadedFiles])
}
return newActivityItem
} catch (error) {
console.error('Failed to create note:', error)
throw error
} finally {
setLoading(false)
}
}
const deleteActivity = async (itemId: string) => {
try {
// API'dan ActivityItem'ı sil
await activityService.deleteItem(itemId)
// Local state'ten kaldır
setActivityItems((prev) => prev.filter((item) => item.id !== itemId))
// İlgili dosyaları da kaldır
setFiles((prev) => prev.filter((file) =>
!(file as any).activityItemId || (file as any).activityItemId !== itemId
))
} catch (error) {
console.error('Failed to delete activity:', error)
throw error
}
}
const deleteFile = async (fileId: string) => {
try {
// API'dan ActivityFile'ı sil
await activityService.deleteFile(fileId)
// Local state'ten kaldır
setFiles((prev) => prev.filter((file) => file.id !== fileId))
// ActivityItem'dan da kaldır
setActivityItems((prev) =>
prev.map(item => ({
...item,
attachedFiles: item.attachedFiles?.filter(file => file.id !== fileId) || []
}))
)
} catch (error) {
console.error('Failed to delete file:', error)
throw error
}
}
const sendMessage = async (
recipients: Array<{ value: string; label: string; email?: string }>,
subject: string,
content: string,
) => {
try {
setLoading(true)
const newMessageItems: ActivityItem[] = []
// Her recipient için ayrı mesaj aktivitesi oluştur
for (const recipient of recipients) {
try {
// Activity oluştur
const activityDto: Partial<ActivityDto> = {
type: 'message',
subject,
content,
recipientUserName: recipient.label,
}
const createdActivity = await activityService.create(activityDto as ActivityDto)
// ActivityItem oluştur
const activityItemDto: Partial<ActivityItemDto> = {
activityId: createdActivity.id,
type: 'message',
entityName,
entityId,
recipientUserId: recipient.value,
recipientUserName: recipient.label,
subject,
content,
}
const createdActivityItem = await activityService.createItem(activityItemDto as ActivityItemDto)
const newMessageItem: ActivityItem = {
id: createdActivityItem.id,
type: 'message',
entityName,
entityId,
recipientUserId: createdActivityItem.recipientUserId,
recipientUserName: createdActivityItem.recipientUserName,
subject: createdActivityItem.subject || '',
content: createdActivityItem.content || '',
creatorId: createdActivityItem.creatorId || user.id,
creationTime: createdActivityItem.creationTime ? new Date(createdActivityItem.creationTime) : new Date(),
}
newMessageItems.push(newMessageItem)
} catch (error) {
console.error('Failed to send message to recipient:', recipient.label, error)
}
}
// Tüm mesajları activityItems'a ekle
setActivityItems((prev) => [...prev, ...newMessageItems])
return newMessageItems
} catch (error) {
console.error('Failed to send messages:', error)
throw error
} finally {
setLoading(false)
}
}
return {
activityItems,
files,
activities,
loading,
// Ana aktivite fonksiyonları
deleteActivity,
addNote,
sendMessage,
// Dosya işlemleri
deleteFile,
// Veri yenileme
refreshActivities: loadActivitiesFromAPI,
}
}

View file

@ -10,7 +10,7 @@ import { useGridData } from './useGridData'
import { useCurrentMenuIcon } from '@/utils/hooks/useCurrentMenuIcon' import { useCurrentMenuIcon } from '@/utils/hooks/useCurrentMenuIcon'
import { Badge } from '@/components/ui' import { Badge } from '@/components/ui'
import { useState } from 'react' import { useState } from 'react'
import { FormActivityPanel } from './FormActivityPanel/FormActivityPanel' import { ActivityPanel } from './FormActivityPanel/ActivityPanel'
const FormEdit = ( const FormEdit = (
props: FormProps = { props: FormProps = {
@ -124,7 +124,7 @@ const FormEdit = (
{/* Activity Panel - sadece ana formda göster */} {/* Activity Panel - sadece ana formda göster */}
{!isSubForm && listFormCode && id && gridDto?.gridOptions?.showActivity && ( {!isSubForm && listFormCode && id && gridDto?.gridOptions?.showActivity && (
<FormActivityPanel <ActivityPanel
entityName={listFormCode} entityName={listFormCode}
entityId={id} entityId={id}
isVisible={isActivityPanelVisible} isVisible={isActivityPanelVisible}

View file

@ -10,7 +10,7 @@ import { useGridData } from './useGridData'
import { useCurrentMenuIcon } from '@/utils/hooks/useCurrentMenuIcon' import { useCurrentMenuIcon } from '@/utils/hooks/useCurrentMenuIcon'
import { Badge } from '@/components/ui' import { Badge } from '@/components/ui'
import { useState } from 'react' import { useState } from 'react'
import { FormActivityPanel } from './FormActivityPanel/FormActivityPanel' import { ActivityPanel } from './FormActivityPanel/ActivityPanel'
const FormView = ( const FormView = (
props: FormProps = { props: FormProps = {
@ -119,7 +119,7 @@ const FormView = (
{/* Activity Panel - sadece ana formda göster */} {/* Activity Panel - sadece ana formda göster */}
{!isSubForm && listFormCode && id && gridDto?.gridOptions?.showActivity && ( {!isSubForm && listFormCode && id && gridDto?.gridOptions?.showActivity && (
<FormActivityPanel <ActivityPanel
entityName={listFormCode} entityName={listFormCode}
entityId={id} entityId={id}
isVisible={isActivityPanelVisible} isVisible={isActivityPanelVisible}