From 56233c54df80ae5bca2909315c4a3cd1265c9a49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sedat=20=C3=96ZT=C3=9CRK?= <76204082+iamsedatozturk@users.noreply.github.com> Date: Mon, 15 Jun 2026 16:08:54 +0300 Subject: [PATCH] AnnouncementComment ve AnnouncementLike sistemi dahil edildi. --- .../Intranet/AnnouncementDto.cs | 14 + .../Intranet/IIntranetAppService.cs | 3 + .../Intranet/IntranetAppService.cs | 159 ++++++- .../Seeds/LanguagesData.json | 32 +- .../Seeds/ListFormSeeder_Administration.cs | 396 +++++++++++++++++- .../Seeds/ListFormSeeder_Saas.cs | 1 + .../Seeds/PermissionsData.json | 175 +++++++- .../Enums/TableNameEnum.cs | 2 + .../TableNameResolver.cs | 2 + .../Data/SeedConsts.cs | 3 + .../Entities/Tenant/Intranet/Announcement.cs | 4 + .../Tenant/Intranet/AnnouncementComment.cs | 19 + .../Tenant/Intranet/AnnouncementLike.cs | 24 ++ .../EntityFrameworkCore/PlatformDbContext.cs | 27 ++ ....cs => 20260615110840_Initial.Designer.cs} | 148 ++++++- ...8_Initial.cs => 20260615110840_Initial.cs} | 75 +++- .../PlatformDbContextModelSnapshot.cs | 146 +++++++ ui/src/proxy/intranet/models.ts | 13 + ui/src/services/intranet.service.ts | 43 +- .../intranet/widgets/AnnouncementModal.tsx | 151 ++++++- ui/src/views/intranet/widgets/EventModal.tsx | 2 +- 21 files changed, 1419 insertions(+), 20 deletions(-) create mode 100644 api/src/Sozsoft.Platform.Domain/Entities/Tenant/Intranet/AnnouncementComment.cs create mode 100644 api/src/Sozsoft.Platform.Domain/Entities/Tenant/Intranet/AnnouncementLike.cs rename api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/{20260612183658_Initial.Designer.cs => 20260615110840_Initial.Designer.cs} (98%) rename api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/{20260612183658_Initial.cs => 20260615110840_Initial.cs} (98%) diff --git a/api/src/Sozsoft.Platform.Application.Contracts/Intranet/AnnouncementDto.cs b/api/src/Sozsoft.Platform.Application.Contracts/Intranet/AnnouncementDto.cs index 52d6ff4..85082c9 100644 --- a/api/src/Sozsoft.Platform.Application.Contracts/Intranet/AnnouncementDto.cs +++ b/api/src/Sozsoft.Platform.Application.Contracts/Intranet/AnnouncementDto.cs @@ -19,5 +19,19 @@ public class AnnouncementDto : FullAuditedEntityDto public DateTime? ExpiryDate { get; set; } public bool IsPinned { get; set; } public int ViewCount { get; set; } + public int Likes { get; set; } + public bool IsLiked { get; set; } public string Attachments { get; set; } = string.Empty; + public System.Collections.Generic.List Comments { get; set; } = []; +} + +public class AnnouncementCommentDto +{ + public Guid Id { get; set; } + public Guid AnnouncementId { get; set; } + public Guid UserId { get; set; } + public UserInfoViewModel? User { get; set; } + public string Content { get; set; } = string.Empty; + public int Likes { get; set; } + public DateTime CreationTime { get; set; } } diff --git a/api/src/Sozsoft.Platform.Application.Contracts/Intranet/IIntranetAppService.cs b/api/src/Sozsoft.Platform.Application.Contracts/Intranet/IIntranetAppService.cs index 99c22e5..f5e7932 100644 --- a/api/src/Sozsoft.Platform.Application.Contracts/Intranet/IIntranetAppService.cs +++ b/api/src/Sozsoft.Platform.Application.Contracts/Intranet/IIntranetAppService.cs @@ -16,6 +16,9 @@ public interface IIntranetAppService : IApplicationService Task CommentSocialPostAsync(System.Guid id, string content); Task VoteSocialPollAsync(System.Guid postId, System.Guid optionId); Task IncrementAnnouncementViewCountAsync(System.Guid id); + Task> GetAnnouncementCommentsAsync(Guid announcementId); + Task CreateAnnouncementCommentAsync(Guid announcementId, string content); + Task LikeAnnouncementAsync(Guid id); Task> GetEventCommentsAsync(Guid eventId); Task CreateEventCommentAsync(Guid eventId, string content); Task LikeEventAsync(Guid id); diff --git a/api/src/Sozsoft.Platform.Application/Intranet/IntranetAppService.cs b/api/src/Sozsoft.Platform.Application/Intranet/IntranetAppService.cs index 671f388..1e64778 100644 --- a/api/src/Sozsoft.Platform.Application/Intranet/IntranetAppService.cs +++ b/api/src/Sozsoft.Platform.Application/Intranet/IntranetAppService.cs @@ -45,6 +45,8 @@ public class IntranetAppService : PlatformAppService, IIntranetAppService private readonly IRepository _socialPollOptionRepository; private readonly IRepository _eventCommentRepository; private readonly IRepository _eventLikeRepository; + private readonly IRepository _announcementCommentRepository; + private readonly IRepository _announcementLikeRepository; public IntranetAppService( ICurrentTenant currentTenant, @@ -67,7 +69,9 @@ public class IntranetAppService : PlatformAppService, IIntranetAppService IRepository socialMediaRepository, IRepository socialPollOptionRepository, IRepository eventCommentRepository, - IRepository eventLikeRepository + IRepository eventLikeRepository, + IRepository announcementCommentRepository, + IRepository announcementLikeRepository ) { _currentTenant = currentTenant; @@ -90,6 +94,8 @@ public class IntranetAppService : PlatformAppService, IIntranetAppService _socialPollOptionRepository = socialPollOptionRepository; _eventCommentRepository = eventCommentRepository; _eventLikeRepository = eventLikeRepository; + _announcementCommentRepository = announcementCommentRepository; + _announcementLikeRepository = announcementLikeRepository; } [UnitOfWork] @@ -368,22 +374,67 @@ public class IntranetAppService : PlatformAppService, IIntranetAppService var announcements = await _announcementRepository.GetListAsync(); var announcementDtos = new List(); + if (announcements.Count == 0) + return announcementDtos; + + var announcementIds = announcements.Select(a => a.Id).ToList(); + + var commentsQueryable = await _announcementCommentRepository.GetQueryableAsync(); + var allComments = await AsyncExecuter.ToListAsync( + commentsQueryable.Where(c => announcementIds.Contains(c.AnnouncementId)).OrderBy(c => c.CreationTime) + ); + + var likesQueryable = await _announcementLikeRepository.GetQueryableAsync(); + var allLikes = await AsyncExecuter.ToListAsync( + likesQueryable.Where(l => announcementIds.Contains(l.AnnouncementId)) + ); + + var likedAnnouncementIds = allLikes + .Where(l => l.UserId == CurrentUser.Id) + .Select(l => l.AnnouncementId) + .ToHashSet(); + + var userIds = announcements + .Where(a => a.UserId.HasValue) + .Select(a => a.UserId!.Value) + .Union(allComments.Select(c => c.UserId)) + .Distinct() + .ToList(); + var (departmentDict, jobPositionDict) = await GetUserLookupDictionariesAsync(); var fallbackUser = await GetDashboardFallbackUserAsync(departmentDict, jobPositionDict); + var users = await _identityUserRepository.GetListAsync(); + var userDict = users + .Where(u => userIds.Contains(u.Id)) + .ToDictionary(u => u.Id, u => MapUserInfoViewModel(u, departmentDict, jobPositionDict)); + + var commentsByAnnouncement = allComments + .GroupBy(c => c.AnnouncementId) + .ToDictionary(g => g.Key, g => g.ToList()); foreach (var announcement in announcements) { var dto = ObjectMapper.Map(announcement); - if (announcement.UserId.HasValue) + if (announcement.UserId.HasValue && userDict.TryGetValue(announcement.UserId.Value, out var announcementUser)) { - var user = await _identityUserRepository.FindAsync(announcement.UserId.Value); - if (user != null) - { - dto.User = MapUserInfoViewModel(user, departmentDict, jobPositionDict); - } + dto.User = announcementUser; } + dto.IsLiked = likedAnnouncementIds.Contains(announcement.Id); + dto.Comments = commentsByAnnouncement.TryGetValue(announcement.Id, out var comments) + ? comments.Select(c => new AnnouncementCommentDto + { + Id = c.Id, + AnnouncementId = c.AnnouncementId, + UserId = c.UserId, + User = userDict.TryGetValue(c.UserId, out var commentUser) ? commentUser : null, + Content = c.Content, + Likes = c.Likes, + CreationTime = c.CreationTime + }).ToList() + : []; + dto.User ??= fallbackUser; announcementDtos.Add(dto); } @@ -391,6 +442,100 @@ public class IntranetAppService : PlatformAppService, IIntranetAppService return announcementDtos; } + public async Task> GetAnnouncementCommentsAsync(Guid announcementId) + { + var commentsQueryable = await _announcementCommentRepository.GetQueryableAsync(); + var comments = await AsyncExecuter.ToListAsync( + commentsQueryable.Where(c => c.AnnouncementId == announcementId).OrderBy(c => c.CreationTime) + ); + + if (comments.Count == 0) + return []; + + var userIds = comments.Select(c => c.UserId).Distinct().ToList(); + var (departmentDict, jobPositionDict) = await GetUserLookupDictionariesAsync(); + var users = await _identityUserRepository.GetListAsync(); + var userDict = users + .Where(u => userIds.Contains(u.Id)) + .ToDictionary(u => u.Id, u => MapUserInfoViewModel(u, departmentDict, jobPositionDict)); + + return comments.Select(c => new AnnouncementCommentDto + { + Id = c.Id, + AnnouncementId = c.AnnouncementId, + UserId = c.UserId, + User = userDict.TryGetValue(c.UserId, out var u) ? u : null, + Content = c.Content, + Likes = c.Likes, + CreationTime = c.CreationTime + }).ToList(); + } + + public async Task CreateAnnouncementCommentAsync(Guid announcementId, string content) + { + var comment = new AnnouncementComment + { + AnnouncementId = announcementId, + UserId = CurrentUser.Id ?? Guid.Empty, + Content = content, + Likes = 0 + }; + + comment = await _announcementCommentRepository.InsertAsync(comment, autoSave: true); + + var (departmentDict, jobPositionDict) = await GetUserLookupDictionariesAsync(); + var user = await _identityUserRepository.FindAsync(comment.UserId); + var userViewModel = user != null ? MapUserInfoViewModel(user, departmentDict, jobPositionDict) : null; + + return new AnnouncementCommentDto + { + Id = comment.Id, + AnnouncementId = comment.AnnouncementId, + UserId = comment.UserId, + User = userViewModel, + Content = comment.Content, + Likes = comment.Likes, + CreationTime = comment.CreationTime + }; + } + + [HttpPost("api/app/intranet/like-announcement")] + public async Task LikeAnnouncementAsync(Guid id) + { + var announcement = await _announcementRepository.GetAsync(id); + + var likeQueryable = await _announcementLikeRepository.GetQueryableAsync(); + var existingLike = await AsyncExecuter.FirstOrDefaultAsync( + likeQueryable.Where(l => l.AnnouncementId == id && l.UserId == CurrentUser.Id)); + + bool isNowLiked; + if (existingLike != null) + { + await _announcementLikeRepository.DeleteAsync(existingLike.Id); + announcement.Likes = Math.Max(0, announcement.Likes - 1); + isNowLiked = false; + } + else + { + await _announcementLikeRepository.InsertAsync(new AnnouncementLike(Guid.NewGuid()) + { + AnnouncementId = id, + UserId = CurrentUser.Id, + }); + announcement.Likes++; + isNowLiked = true; + } + + await _announcementRepository.UpdateAsync(announcement, autoSave: true); + + return new AnnouncementDto + { + Id = announcement.Id, + Likes = announcement.Likes, + IsLiked = isNowLiked, + }; + } + private async Task> GetSurveysAsync() { var queryable = await _surveyRepository.GetQueryableAsync(); diff --git a/api/src/Sozsoft.Platform.DbMigrator/Seeds/LanguagesData.json b/api/src/Sozsoft.Platform.DbMigrator/Seeds/LanguagesData.json index b07e93f..5c588db 100644 --- a/api/src/Sozsoft.Platform.DbMigrator/Seeds/LanguagesData.json +++ b/api/src/Sozsoft.Platform.DbMigrator/Seeds/LanguagesData.json @@ -6674,7 +6674,7 @@ }, { "resourceName": "Platform", - "key": "App.Intranet.Events.EventComment", + "key": "App.Intranet.Events.NoComment", "tr": "Henüz yorum yok. İlk yorumu sen yap!", "en": "No comments yet. Be the first to comment!" }, @@ -6720,6 +6720,12 @@ "tr": "Etkinlik Beğenileri", "en": "Event Likes" }, + { + "resourceName": "Platform", + "key": "App.Intranet.Events.EventComment", + "tr": "Etkinlik Yorumları", + "en": "Event Comments" + }, { "resourceName": "Platform", "key": "App.Hr.Training", @@ -13484,6 +13490,30 @@ } ], "LanguageFieldTitles": [ + { + "resourceName": "Platform", + "key": "App.Intranet.Announcement.AnnouncementLike", + "en": "Announcements Like", + "tr": "Duyuru Beğenileri" + }, + { + "resourceName": "Platform", + "key": "App.Intranet.Announcement.AnnouncementComment", + "en": "Announcements Comments", + "tr": "Duyuru Yorumları" + }, + { + "resourceName": "Platform", + "key": "App.Listform.ListformField.AnnouncementId", + "en": "Announcement Number", + "tr": "Duyuru No" + }, + { + "resourceName": "Platform", + "key": "App.Listform.ListformField.EventId", + "en": "Event Number", + "tr": "Olay No" + }, { "resourceName": "Platform", "key": "App.Listform.ListformField.Photos", diff --git a/api/src/Sozsoft.Platform.DbMigrator/Seeds/ListFormSeeder_Administration.cs b/api/src/Sozsoft.Platform.DbMigrator/Seeds/ListFormSeeder_Administration.cs index f66d03a..631cee3 100644 --- a/api/src/Sozsoft.Platform.DbMigrator/Seeds/ListFormSeeder_Administration.cs +++ b/api/src/Sozsoft.Platform.DbMigrator/Seeds/ListFormSeeder_Administration.cs @@ -84,6 +84,7 @@ public class ListFormSeeder_Administration : IDataSeedContributor, ITransientDep ColumnOptionJson = DefaultColumnOptionJson(), PermissionJson = DefaultPermissionJson(AbpIdentity.PermissionGroups.Create, AbpIdentity.PermissionGroups.Default, AbpIdentity.PermissionGroups.Update, AbpIdentity.PermissionGroups.Delete, AbpIdentity.PermissionGroups.Export, AbpIdentity.PermissionGroups.Import, AbpIdentity.PermissionGroups.Note), PagerOptionJson = DefaultPagerOptionJson, + InsertFieldsDefaultValueJson = DefaultInsertFieldsDefaultValueJson(), EditingOptionJson = DefaultEditingOptionJson(listFormName, 600, 350, true, true, true, true, false), EditingFormJson = JsonSerializer.Serialize(new List() { @@ -215,6 +216,7 @@ public class ListFormSeeder_Administration : IDataSeedContributor, ITransientDep PermissionJson = DefaultPermissionJson(AbpIdentity.Permissions.Create, listFormName, AbpIdentity.Permissions.Update, AbpIdentity.Permissions.Delete, AbpIdentity.Permissions.Export, AbpIdentity.Permissions.Import, AbpIdentity.Permissions.Note), PagerOptionJson = DefaultPagerOptionJson, TreeOptionJson = DefaultTreeOptionJson("Name", "ParentName", true), + InsertFieldsDefaultValueJson = DefaultInsertFieldsDefaultValueJson(), EditingOptionJson = DefaultEditingOptionJson(listFormName, 600, 600, true, true, true, true, false), EditingFormJson = JsonSerializer.Serialize(new List() { @@ -2665,7 +2667,7 @@ public class ListFormSeeder_Administration : IDataSeedContributor, ITransientDep } #endregion - #region Announncements + #region Announcement listFormName = AppCodes.Intranet.Announcement; if (!await _listFormRepository.AnyAsync(a => a.ListFormCode == listFormName)) { @@ -2676,7 +2678,7 @@ public class ListFormSeeder_Administration : IDataSeedContributor, ITransientDep PageSize = 10, ExportJson = DefaultExportJson, IsSubForm = false, - ShowNote = false, + ShowNote = true, LayoutJson = DefaultLayoutJson(), CultureName = LanguageCodes.En, ListFormCode = listFormName, @@ -2703,7 +2705,7 @@ public class ListFormSeeder_Administration : IDataSeedContributor, ITransientDep DeleteCommand = DefaultDeleteCommand(nameof(TableNameEnum.Announcement)), DeleteFieldsDefaultValueJson = DefaultDeleteFieldsDefaultValueJson(), PagerOptionJson = DefaultPagerOptionJson, - EditingOptionJson = DefaultEditingOptionJson(listFormName, 750, 700, true, true, true, true, false), + EditingOptionJson = DefaultEditingOptionJson(listFormName, 750, 700, true, true, true, true, false, true), InsertFieldsDefaultValueJson = DefaultInsertFieldsDefaultValueJson(), EditingFormJson = JsonSerializer.Serialize(new List() { @@ -2724,6 +2726,32 @@ public class ListFormSeeder_Administration : IDataSeedContributor, ITransientDep { new() { FieldName = "IsPinned", FieldDbType = DbType.Boolean, Value = "false", CustomValueType = FieldCustomValueTypeEnum.Value }, new() { FieldName = "PublishDate", FieldDbType = DbType.Date, Value = "@NOW", CustomValueType = FieldCustomValueTypeEnum.CustomKey } + }), + SubFormsJson = JsonSerializer.Serialize(new List() { + new { + TabType = ListFormTabTypeEnum.List, + TabTitle = AppCodes.Intranet.AnnouncementLike, + Code = AppCodes.Intranet.AnnouncementLike, + Relation = new List() { + new { + ParentFieldName = "Id", + DbType = DbType.Guid, + ChildFieldName = "AnnouncementId" + } + } + }, + new { + TabType = ListFormTabTypeEnum.List, + TabTitle = AppCodes.Intranet.AnnouncementComment, + Code = AppCodes.Intranet.AnnouncementComment, + Relation = new List() { + new { + ParentFieldName = "Id", + DbType = DbType.Guid, + ChildFieldName = "AnnouncementId" + } + } + } }) } ); @@ -2910,6 +2938,234 @@ public class ListFormSeeder_Administration : IDataSeedContributor, ITransientDep } #endregion + #region AnnouncementLike + listFormName = AppCodes.Intranet.AnnouncementLike; + if (!await _listFormRepository.AnyAsync(a => a.ListFormCode == listFormName)) + { + var listForm = await _listFormRepository.InsertAsync( + new ListForm() + { + ListFormType = ListFormTypeEnum.List, + PageSize = 10, + ExportJson = DefaultExportJson, + IsSubForm = false, + ShowNote = false, + LayoutJson = DefaultLayoutJson(), + CultureName = LanguageCodes.En, + ListFormCode = listFormName, + Name = listFormName, + Title = listFormName, + DataSourceCode = SeedConsts.DataSources.DefaultCode, + IsTenant = true, + IsBranch = false, + IsOrganizationUnit = false, + Description = listFormName, + SelectCommandType = SelectCommandTypeEnum.Table, + SelectCommand = TableNameResolver.GetFullTableName(nameof(TableNameEnum.AnnouncementLike)), + KeyFieldName = "Id", + KeyFieldDbSourceType = DbType.Guid, + DefaultFilter = DefaultFilterJson, + SortMode = GridOptions.SortModeSingle, + FilterRowJson = DefaultFilterRowJson, + HeaderFilterJson = DefaultHeaderFilterJson, + SearchPanelJson = DefaultSearchPanelJson, + GroupPanelJson = DefaultGroupPanelJson, + SelectionJson = DefaultSelectionSingleJson, + ColumnOptionJson = DefaultColumnOptionJson(), + PermissionJson = DefaultPermissionJson(listFormName), + DeleteCommand = DefaultDeleteCommand(nameof(TableNameEnum.AnnouncementLike)), + DeleteFieldsDefaultValueJson = DefaultDeleteFieldsDefaultValueJson(), + PagerOptionJson = DefaultPagerOptionJson, + EditingOptionJson = DefaultEditingOptionJson(listFormName, 500, 250, true, true, true, true, false), + EditingFormJson = JsonSerializer.Serialize(new List() { + new() { Order=1, ColCount=1, ColSpan=1, ItemType="group", Items=[ + new EditingFormItemDto { Order = 1, DataField = "UserId", ColSpan = 1, EditorType2=EditorTypes.dxSelectBox }, + ]} + }), + InsertFieldsDefaultValueJson = DefaultInsertFieldsDefaultValueJson(), + } + ); + + #region AnnouncementLike Fields + await _listFormFieldRepository.InsertManyAsync(new ListFormField[] { + new() { + ListFormCode = listForm.ListFormCode, + CultureName = LanguageCodes.En, + SourceDbType = DbType.Guid, + FieldName = "Id", + CaptionName = "App.Listform.ListformField.Id", + Width = 0, + ListOrderNo = 1, + Visible = false, + IsActive = true, + ValidationRuleJson = DefaultValidationRuleRequiredJson, + ColumnCustomizationJson = DefaultColumnCustomizationJson, + PermissionJson = DefaultFieldPermissionJson(listForm.Name), + PivotSettingsJson = DefaultPivotSettingsJson + }, + new() { + ListFormCode = listForm.ListFormCode, + CultureName = LanguageCodes.En, + SourceDbType = DbType.Guid, + FieldName = "UserId", + CaptionName = "App.Listform.ListformField.UserId", + Width = 0, + ListOrderNo = 2, + Visible = true, + IsActive = true, + AllowSearch = true, + LookupJson = JsonSerializer.Serialize(new LookupDto { + DataSourceType = UiLookupDataSourceTypeEnum.Query, + DisplayExpr = "Name", + ValueExpr = "Key", + LookupQuery = LookupQueryValues.UserValues + }), + ValidationRuleJson = DefaultValidationRuleRequiredJson, + ColumnCustomizationJson = DefaultColumnCustomizationJson, + PermissionJson = DefaultFieldPermissionJson(listForm.Name), + PivotSettingsJson = DefaultPivotSettingsJson + }, + new() { + ListFormCode = listForm.ListFormCode, + CultureName = LanguageCodes.En, + SourceDbType = DbType.Guid, + FieldName = "AnnouncementId", + CaptionName = "App.Listform.ListformField.AnnouncementId", + Width = 0, + ListOrderNo = 3, + Visible = false, + IsActive = true, + ColumnCustomizationJson = DefaultColumnCustomizationJson, + PermissionJson = DefaultFieldPermissionJson(listForm.Name), + PivotSettingsJson = DefaultPivotSettingsJson + }, + }); + #endregion + } + #endregion + + #region AnnouncementComment + listFormName = AppCodes.Intranet.AnnouncementComment; + if (!await _listFormRepository.AnyAsync(a => a.ListFormCode == listFormName)) + { + var listForm = await _listFormRepository.InsertAsync( + new ListForm() + { + ListFormType = ListFormTypeEnum.List, + PageSize = 10, + ExportJson = DefaultExportJson, + IsSubForm = false, + ShowNote = false, + LayoutJson = DefaultLayoutJson(), + CultureName = LanguageCodes.En, + ListFormCode = listFormName, + Name = listFormName, + Title = listFormName, + DataSourceCode = SeedConsts.DataSources.DefaultCode, + IsTenant = true, + IsBranch = false, + IsOrganizationUnit = false, + Description = listFormName, + SelectCommandType = SelectCommandTypeEnum.Table, + SelectCommand = TableNameResolver.GetFullTableName(nameof(TableNameEnum.AnnouncementComment)), + KeyFieldName = "Id", + KeyFieldDbSourceType = DbType.Guid, + DefaultFilter = DefaultFilterJson, + SortMode = GridOptions.SortModeSingle, + FilterRowJson = DefaultFilterRowJson, + HeaderFilterJson = DefaultHeaderFilterJson, + SearchPanelJson = DefaultSearchPanelJson, + GroupPanelJson = DefaultGroupPanelJson, + SelectionJson = DefaultSelectionSingleJson, + ColumnOptionJson = DefaultColumnOptionJson(), + PermissionJson = DefaultPermissionJson(listFormName), + DeleteCommand = DefaultDeleteCommand(nameof(TableNameEnum.AnnouncementComment)), + DeleteFieldsDefaultValueJson = DefaultDeleteFieldsDefaultValueJson(), + PagerOptionJson = DefaultPagerOptionJson, + EditingOptionJson = DefaultEditingOptionJson(listFormName, 500, 450, true, true, true, true, false), + EditingFormJson = JsonSerializer.Serialize(new List() { + new() { Order=1, ColCount=1, ColSpan=1, ItemType="group", Items=[ + new EditingFormItemDto { Order = 1, DataField = "UserId", ColSpan = 1, EditorType2=EditorTypes.dxSelectBox }, + new EditingFormItemDto { Order = 2, DataField = "Content", ColSpan = 1, EditorType2=EditorTypes.dxTextArea }, + ]} + }), + InsertFieldsDefaultValueJson = DefaultInsertFieldsDefaultValueJson(), + } + ); + + #region AnnouncementComment Fields + await _listFormFieldRepository.InsertManyAsync(new ListFormField[] { + new() { + ListFormCode = listForm.ListFormCode, + CultureName = LanguageCodes.En, + SourceDbType = DbType.Guid, + FieldName = "Id", + CaptionName = "App.Listform.ListformField.Id", + Width = 0, + ListOrderNo = 1, + Visible = false, + IsActive = true, + ValidationRuleJson = DefaultValidationRuleRequiredJson, + ColumnCustomizationJson = DefaultColumnCustomizationJson, + PermissionJson = DefaultFieldPermissionJson(listForm.Name), + PivotSettingsJson = DefaultPivotSettingsJson + }, + new() { + ListFormCode = listForm.ListFormCode, + CultureName = LanguageCodes.En, + SourceDbType = DbType.Guid, + FieldName = "UserId", + CaptionName = "App.Listform.ListformField.UserId", + Width = 0, + ListOrderNo = 2, + Visible = true, + IsActive = true, + AllowSearch = true, + LookupJson = JsonSerializer.Serialize(new LookupDto { + DataSourceType = UiLookupDataSourceTypeEnum.Query, + DisplayExpr = "Name", + ValueExpr = "Key", + LookupQuery = LookupQueryValues.UserValues + }), + ValidationRuleJson = DefaultValidationRuleRequiredJson, + ColumnCustomizationJson = DefaultColumnCustomizationJson, + PermissionJson = DefaultFieldPermissionJson(listForm.Name), + PivotSettingsJson = DefaultPivotSettingsJson + }, + new() { + ListFormCode = listForm.ListFormCode, + CultureName = LanguageCodes.En, + SourceDbType = DbType.String, + FieldName = "Content", + CaptionName = "App.Listform.ListformField.Content", + Width = 0, + ListOrderNo = 3, + Visible = true, + IsActive = true, + ValidationRuleJson = DefaultValidationRuleRequiredJson, + ColumnCustomizationJson = DefaultColumnCustomizationJson, + PermissionJson = DefaultFieldPermissionJson(listForm.Name), + PivotSettingsJson = DefaultPivotSettingsJson + }, + new() { + ListFormCode = listForm.ListFormCode, + CultureName = LanguageCodes.En, + SourceDbType = DbType.Guid, + FieldName = "AnnouncementId", + CaptionName = "App.Listform.ListformField.AnnouncementId", + Width = 0, + ListOrderNo = 4, + Visible = false, + IsActive = true, + ColumnCustomizationJson = DefaultColumnCustomizationJson, + PermissionJson = DefaultFieldPermissionJson(listForm.Name), + PivotSettingsJson = DefaultPivotSettingsJson + }, + }); + #endregion + } + #endregion + #region Survey listFormName = AppCodes.Intranet.Survey; if (!await _listFormRepository.AnyAsync(a => a.ListFormCode == listFormName)) @@ -4237,6 +4493,18 @@ public class ListFormSeeder_Administration : IDataSeedContributor, ITransientDep ChildFieldName = "EventId" } } + }, + new { + TabType = ListFormTabTypeEnum.List, + TabTitle = AppCodes.Intranet.EventComment, + Code = AppCodes.Intranet.EventComment, + Relation = new List() { + new { + ParentFieldName = "Id", + DbType = DbType.Guid, + ChildFieldName = "EventId" + } + } } }) } @@ -4579,5 +4847,127 @@ public class ListFormSeeder_Administration : IDataSeedContributor, ITransientDep #endregion } #endregion + + #region EventComment + listFormName = AppCodes.Intranet.EventComment; + if (!await _listFormRepository.AnyAsync(a => a.ListFormCode == listFormName)) + { + var listForm = await _listFormRepository.InsertAsync( + new ListForm() + { + ListFormType = ListFormTypeEnum.List, + PageSize = 10, + ExportJson = DefaultExportJson, + IsSubForm = false, + ShowNote = false, + LayoutJson = DefaultLayoutJson(), + CultureName = LanguageCodes.En, + ListFormCode = listFormName, + Name = listFormName, + Title = listFormName, + DataSourceCode = SeedConsts.DataSources.DefaultCode, + IsTenant = true, + IsBranch = false, + IsOrganizationUnit = false, + Description = listFormName, + SelectCommandType = SelectCommandTypeEnum.Table, + SelectCommand = TableNameResolver.GetFullTableName(nameof(TableNameEnum.EventComment)), + KeyFieldName = "Id", + KeyFieldDbSourceType = DbType.Guid, + DefaultFilter = DefaultFilterJson, + SortMode = GridOptions.SortModeSingle, + FilterRowJson = DefaultFilterRowJson, + HeaderFilterJson = DefaultHeaderFilterJson, + SearchPanelJson = DefaultSearchPanelJson, + GroupPanelJson = DefaultGroupPanelJson, + SelectionJson = DefaultSelectionSingleJson, + ColumnOptionJson = DefaultColumnOptionJson(), + PermissionJson = DefaultPermissionJson(listFormName), + DeleteCommand = DefaultDeleteCommand(nameof(TableNameEnum.EventComment)), + DeleteFieldsDefaultValueJson = DefaultDeleteFieldsDefaultValueJson(), + PagerOptionJson = DefaultPagerOptionJson, + EditingOptionJson = DefaultEditingOptionJson(listFormName, 500, 450, true, true, true, true, false), + EditingFormJson = JsonSerializer.Serialize(new List() { + new() { Order=1, ColCount=1, ColSpan=1, ItemType="group", Items=[ + new EditingFormItemDto { Order = 1, DataField = "UserId", ColSpan = 1, EditorType2=EditorTypes.dxSelectBox }, + new EditingFormItemDto { Order = 2, DataField = "Content", ColSpan = 1, EditorType2=EditorTypes.dxTextArea }, + ]} + }), + InsertFieldsDefaultValueJson = DefaultInsertFieldsDefaultValueJson(), + } + ); + + #region EventComment Fields + await _listFormFieldRepository.InsertManyAsync(new ListFormField[] { + new() { + ListFormCode = listForm.ListFormCode, + CultureName = LanguageCodes.En, + SourceDbType = DbType.Guid, + FieldName = "Id", + CaptionName = "App.Listform.ListformField.Id", + Width = 0, + ListOrderNo = 1, + Visible = false, + IsActive = true, + ValidationRuleJson = DefaultValidationRuleRequiredJson, + ColumnCustomizationJson = DefaultColumnCustomizationJson, + PermissionJson = DefaultFieldPermissionJson(listForm.Name), + PivotSettingsJson = DefaultPivotSettingsJson + }, + new() { + ListFormCode = listForm.ListFormCode, + CultureName = LanguageCodes.En, + SourceDbType = DbType.Guid, + FieldName = "UserId", + CaptionName = "App.Listform.ListformField.UserId", + Width = 0, + ListOrderNo = 2, + Visible = true, + IsActive = true, + AllowSearch = true, + LookupJson = JsonSerializer.Serialize(new LookupDto { + DataSourceType = UiLookupDataSourceTypeEnum.Query, + DisplayExpr = "Name", + ValueExpr = "Key", + LookupQuery = LookupQueryValues.UserValues + }), + ValidationRuleJson = DefaultValidationRuleRequiredJson, + ColumnCustomizationJson = DefaultColumnCustomizationJson, + PermissionJson = DefaultFieldPermissionJson(listForm.Name), + PivotSettingsJson = DefaultPivotSettingsJson + }, + new() { + ListFormCode = listForm.ListFormCode, + CultureName = LanguageCodes.En, + SourceDbType = DbType.String, + FieldName = "Content", + CaptionName = "App.Listform.ListformField.Content", + Width = 0, + ListOrderNo = 3, + Visible = true, + IsActive = true, + ValidationRuleJson = DefaultValidationRuleRequiredJson, + ColumnCustomizationJson = DefaultColumnCustomizationJson, + PermissionJson = DefaultFieldPermissionJson(listForm.Name), + PivotSettingsJson = DefaultPivotSettingsJson + }, + new() { + ListFormCode = listForm.ListFormCode, + CultureName = LanguageCodes.En, + SourceDbType = DbType.Guid, + FieldName = "EventId", + CaptionName = "App.Listform.ListformField.EventId", + Width = 0, + ListOrderNo = 4, + Visible = false, + IsActive = true, + ColumnCustomizationJson = DefaultColumnCustomizationJson, + PermissionJson = DefaultFieldPermissionJson(listForm.Name), + PivotSettingsJson = DefaultPivotSettingsJson + }, + }); + #endregion + } + #endregion } } diff --git a/api/src/Sozsoft.Platform.DbMigrator/Seeds/ListFormSeeder_Saas.cs b/api/src/Sozsoft.Platform.DbMigrator/Seeds/ListFormSeeder_Saas.cs index 7e9b1f7..2e72dac 100644 --- a/api/src/Sozsoft.Platform.DbMigrator/Seeds/ListFormSeeder_Saas.cs +++ b/api/src/Sozsoft.Platform.DbMigrator/Seeds/ListFormSeeder_Saas.cs @@ -1064,6 +1064,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency ColumnOptionJson = DefaultColumnOptionJson(), PermissionJson = DefaultPermissionJson(listFormName), PagerOptionJson = DefaultPagerOptionJson, + InsertFieldsDefaultValueJson = DefaultInsertFieldsDefaultValueJson(), DeleteCommand = $"DELETE FROM \"{TableNameResolver.GetFullTableName(nameof(TableNameEnum.GlobalSearch))}\" WHERE \"Id\"=@Id", DeleteFieldsDefaultValueJson = JsonSerializer.Serialize(new FieldsDefaultValue[] { new() { FieldName = "Id", FieldDbType = DbType.Int32, Value = "@ID", CustomValueType = FieldCustomValueTypeEnum.CustomKey } diff --git a/api/src/Sozsoft.Platform.DbMigrator/Seeds/PermissionsData.json b/api/src/Sozsoft.Platform.DbMigrator/Seeds/PermissionsData.json index d27534c..a60008e 100644 --- a/api/src/Sozsoft.Platform.DbMigrator/Seeds/PermissionsData.json +++ b/api/src/Sozsoft.Platform.DbMigrator/Seeds/PermissionsData.json @@ -3754,6 +3754,60 @@ "MultiTenancySide": 3, "MenuGroup": "Erp|Kurs" }, + { + "GroupName": "App.Administration", + "Name": "App.Intranet.Events.EventComment", + "ParentName": "App.Intranet", + "DisplayName": "App.Intranet.Events.EventComment", + "IsEnabled": true, + "MultiTenancySide": 3, + "MenuGroup": "Erp|Kurs" + }, + { + "GroupName": "App.Administration", + "Name": "App.Intranet.Events.EventComment.Create", + "ParentName": "App.Intranet.Events.EventComment", + "DisplayName": "Create", + "IsEnabled": true, + "MultiTenancySide": 3, + "MenuGroup": "Erp|Kurs" + }, + { + "GroupName": "App.Administration", + "Name": "App.Intranet.Events.EventComment.Update", + "ParentName": "App.Intranet.Events.EventComment", + "DisplayName": "Update", + "IsEnabled": true, + "MultiTenancySide": 3, + "MenuGroup": "Erp|Kurs" + }, + { + "GroupName": "App.Administration", + "Name": "App.Intranet.Events.EventComment.Delete", + "ParentName": "App.Intranet.Events.EventComment", + "DisplayName": "Delete", + "IsEnabled": true, + "MultiTenancySide": 3, + "MenuGroup": "Erp|Kurs" + }, + { + "GroupName": "App.Administration", + "Name": "App.Intranet.Events.EventComment.Export", + "ParentName": "App.Intranet.Events.EventComment", + "DisplayName": "Export", + "IsEnabled": true, + "MultiTenancySide": 3, + "MenuGroup": "Erp|Kurs" + }, + { + "GroupName": "App.Administration", + "Name": "App.Intranet.Events.EventComment.Import", + "ParentName": "App.Intranet.Events.EventComment", + "DisplayName": "Import", + "IsEnabled": true, + "MultiTenancySide": 3, + "MenuGroup": "Erp|Kurs" + }, { "GroupName": "App.Administration", "Name": "App.Intranet.Announcement", @@ -3817,6 +3871,125 @@ "MultiTenancySide": 3, "MenuGroup": "Erp|Kurs" }, + { + "GroupName": "App.Administration", + "Name": "App.Intranet.Announcement.Note", + "ParentName": "App.Intranet.Announcement", + "DisplayName": "Note", + "IsEnabled": true, + "MultiTenancySide": 3, + "MenuGroup": "Erp|Kurs" + }, + { + "GroupName": "App.Administration", + "Name": "App.Intranet.Announcement.AnnouncementLike", + "ParentName": "App.Intranet", + "DisplayName": "App.Intranet.Announcement.AnnouncementLike", + "IsEnabled": true, + "MultiTenancySide": 3, + "MenuGroup": "Erp|Kurs" + }, + { + "GroupName": "App.Administration", + "Name": "App.Intranet.Announcement.AnnouncementLike.Create", + "ParentName": "App.Intranet.Announcement.AnnouncementLike", + "DisplayName": "Create", + "IsEnabled": true, + "MultiTenancySide": 3, + "MenuGroup": "Erp|Kurs" + }, + { + "GroupName": "App.Administration", + "Name": "App.Intranet.Announcement.AnnouncementLike.Update", + "ParentName": "App.Intranet.Announcement.AnnouncementLike", + "DisplayName": "Update", + "IsEnabled": true, + "MultiTenancySide": 3, + "MenuGroup": "Erp|Kurs" + }, + { + "GroupName": "App.Administration", + "Name": "App.Intranet.Announcement.AnnouncementLike.Delete", + "ParentName": "App.Intranet.Announcement.AnnouncementLike", + "DisplayName": "Delete", + "IsEnabled": true, + "MultiTenancySide": 3, + "MenuGroup": "Erp|Kurs" + }, + { + "GroupName": "App.Administration", + "Name": "App.Intranet.Announcement.AnnouncementLike.Export", + "ParentName": "App.Intranet.Announcement.AnnouncementLike", + "DisplayName": "Export", + "IsEnabled": true, + "MultiTenancySide": 3, + "MenuGroup": "Erp|Kurs" + }, + { + "GroupName": "App.Administration", + "Name": "App.Intranet.Announcement.AnnouncementLike.Import", + "ParentName": "App.Intranet.Announcement.AnnouncementLike", + "DisplayName": "Import", + "IsEnabled": true, + "MultiTenancySide": 3, + "MenuGroup": "Erp|Kurs" + }, + + { + "GroupName": "App.Administration", + "Name": "App.Intranet.Announcement.AnnouncementComment", + "ParentName": "App.Intranet", + "DisplayName": "App.Intranet.Announcement.AnnouncementComment", + "IsEnabled": true, + "MultiTenancySide": 3, + "MenuGroup": "Erp|Kurs" + }, + { + "GroupName": "App.Administration", + "Name": "App.Intranet.Announcement.AnnouncementComment.Create", + "ParentName": "App.Intranet.Announcement.AnnouncementComment", + "DisplayName": "Create", + "IsEnabled": true, + "MultiTenancySide": 3, + "MenuGroup": "Erp|Kurs" + }, + { + "GroupName": "App.Administration", + "Name": "App.Intranet.Announcement.AnnouncementComment.Update", + "ParentName": "App.Intranet.Announcement.AnnouncementComment", + "DisplayName": "Update", + "IsEnabled": true, + "MultiTenancySide": 3, + "MenuGroup": "Erp|Kurs" + }, + { + "GroupName": "App.Administration", + "Name": "App.Intranet.Announcement.AnnouncementComment.Delete", + "ParentName": "App.Intranet.Announcement.AnnouncementComment", + "DisplayName": "Delete", + "IsEnabled": true, + "MultiTenancySide": 3, + "MenuGroup": "Erp|Kurs" + }, + { + "GroupName": "App.Administration", + "Name": "App.Intranet.Announcement.AnnouncementComment.Export", + "ParentName": "App.Intranet.Announcement.AnnouncementComment", + "DisplayName": "Export", + "IsEnabled": true, + "MultiTenancySide": 3, + "MenuGroup": "Erp|Kurs" + }, + { + "GroupName": "App.Administration", + "Name": "App.Intranet.Announcement.AnnouncementComment.Import", + "ParentName": "App.Intranet.Announcement.AnnouncementComment", + "DisplayName": "Import", + "IsEnabled": true, + "MultiTenancySide": 3, + "MenuGroup": "Erp|Kurs" + }, + { "GroupName": "App.Administration", "Name": "App.Intranet.SocialPost", @@ -4286,4 +4459,4 @@ "MenuGroup": "Kurs" } ] -} \ No newline at end of file +} diff --git a/api/src/Sozsoft.Platform.Domain.Shared/Enums/TableNameEnum.cs b/api/src/Sozsoft.Platform.Domain.Shared/Enums/TableNameEnum.cs index 56c846b..7a6bcb8 100644 --- a/api/src/Sozsoft.Platform.Domain.Shared/Enums/TableNameEnum.cs +++ b/api/src/Sozsoft.Platform.Domain.Shared/Enums/TableNameEnum.cs @@ -68,6 +68,8 @@ public enum TableNameEnum Demo, Contact, Announcement, + AnnouncementComment, + AnnouncementLike, Survey, SurveyQuestion, SurveyQuestionOption, diff --git a/api/src/Sozsoft.Platform.Domain.Shared/TableNameResolver.cs b/api/src/Sozsoft.Platform.Domain.Shared/TableNameResolver.cs index 965620b..c0262a6 100644 --- a/api/src/Sozsoft.Platform.Domain.Shared/TableNameResolver.cs +++ b/api/src/Sozsoft.Platform.Domain.Shared/TableNameResolver.cs @@ -87,6 +87,8 @@ public static class TableNameResolver // 🔹 INTRANET TABLOLARI { nameof(TableNameEnum.Announcement), (TablePrefix.TenantByName, MenuPrefix.Administration) }, + { nameof(TableNameEnum.AnnouncementComment), (TablePrefix.TenantByName, MenuPrefix.Administration) }, + { nameof(TableNameEnum.AnnouncementLike), (TablePrefix.TenantByName, MenuPrefix.Administration) }, { nameof(TableNameEnum.Survey), (TablePrefix.TenantByName, MenuPrefix.Administration) }, { nameof(TableNameEnum.SurveyQuestion), (TablePrefix.TenantByName, MenuPrefix.Administration) }, { nameof(TableNameEnum.SurveyQuestionOption), (TablePrefix.TenantByName, MenuPrefix.Administration) }, diff --git a/api/src/Sozsoft.Platform.Domain/Data/SeedConsts.cs b/api/src/Sozsoft.Platform.Domain/Data/SeedConsts.cs index bd98dc7..029404e 100644 --- a/api/src/Sozsoft.Platform.Domain/Data/SeedConsts.cs +++ b/api/src/Sozsoft.Platform.Domain/Data/SeedConsts.cs @@ -327,6 +327,8 @@ public static class SeedConsts public const string Default = Prefix.App + ".Intranet"; public const string Announcement = Default + ".Announcement"; + public const string AnnouncementLike = Announcement + ".AnnouncementLike"; + public const string AnnouncementComment = Announcement + ".AnnouncementComment"; public const string Survey = Default + ".Survey"; public const string SurveyQuestion = Default + ".SurveyQuestion"; @@ -342,6 +344,7 @@ public static class SeedConsts public const string EventCategory = Events + ".EventCategory"; public const string Event = Events + ".Event"; public const string EventLike = Events + ".EventLike"; + public const string EventComment = Events + ".EventComment"; } public static class Definitions diff --git a/api/src/Sozsoft.Platform.Domain/Entities/Tenant/Intranet/Announcement.cs b/api/src/Sozsoft.Platform.Domain/Entities/Tenant/Intranet/Announcement.cs index e3f91dd..d4ef700 100644 --- a/api/src/Sozsoft.Platform.Domain/Entities/Tenant/Intranet/Announcement.cs +++ b/api/src/Sozsoft.Platform.Domain/Entities/Tenant/Intranet/Announcement.cs @@ -18,6 +18,10 @@ public class Announcement : FullAuditedEntity, IMultiTenant public DateTime? ExpiryDate { get; set; } public bool IsPinned { get; set; } public int ViewCount { get; set; } + public int Likes { get; set; } public string Attachments { get; set; } + + public System.Collections.Generic.ICollection Comments { get; set; } = []; + public System.Collections.Generic.ICollection AnnouncementLikes { get; set; } = []; } diff --git a/api/src/Sozsoft.Platform.Domain/Entities/Tenant/Intranet/AnnouncementComment.cs b/api/src/Sozsoft.Platform.Domain/Entities/Tenant/Intranet/AnnouncementComment.cs new file mode 100644 index 0000000..a52cc4a --- /dev/null +++ b/api/src/Sozsoft.Platform.Domain/Entities/Tenant/Intranet/AnnouncementComment.cs @@ -0,0 +1,19 @@ +using System; +using Volo.Abp.Domain.Entities.Auditing; +using Volo.Abp.MultiTenancy; + +namespace Sozsoft.Platform.Entities; + +public class AnnouncementComment : FullAuditedEntity, IMultiTenant +{ + public Guid? TenantId { get; set; } + + public Guid AnnouncementId { get; set; } + public Announcement Announcement { get; set; } + + public Guid UserId { get; set; } + public string Content { get; set; } + public int Likes { get; set; } + + Guid? IMultiTenant.TenantId => TenantId; +} diff --git a/api/src/Sozsoft.Platform.Domain/Entities/Tenant/Intranet/AnnouncementLike.cs b/api/src/Sozsoft.Platform.Domain/Entities/Tenant/Intranet/AnnouncementLike.cs new file mode 100644 index 0000000..18d3a34 --- /dev/null +++ b/api/src/Sozsoft.Platform.Domain/Entities/Tenant/Intranet/AnnouncementLike.cs @@ -0,0 +1,24 @@ +using System; +using Volo.Abp.Domain.Entities.Auditing; +using Volo.Abp.MultiTenancy; + +namespace Sozsoft.Platform.Entities; + +public class AnnouncementLike : FullAuditedEntity, IMultiTenant +{ + public Guid? TenantId { get; set; } + + public Guid AnnouncementId { get; set; } + public Announcement Announcement { get; set; } + + public Guid? UserId { get; set; } + + public AnnouncementLike(Guid id) + { + Id = id; + } + + protected AnnouncementLike() { } + + Guid? IMultiTenant.TenantId => TenantId; +} diff --git a/api/src/Sozsoft.Platform.EntityFrameworkCore/EntityFrameworkCore/PlatformDbContext.cs b/api/src/Sozsoft.Platform.EntityFrameworkCore/EntityFrameworkCore/PlatformDbContext.cs index 327d713..f1e7b4b 100644 --- a/api/src/Sozsoft.Platform.EntityFrameworkCore/EntityFrameworkCore/PlatformDbContext.cs +++ b/api/src/Sozsoft.Platform.EntityFrameworkCore/EntityFrameworkCore/PlatformDbContext.cs @@ -124,6 +124,8 @@ public class PlatformDbContext : public DbSet MessengerConversationMessages { get; set; } public DbSet Announcements { get; set; } + public DbSet AnnouncementComments { get; set; } + public DbSet AnnouncementLikes { get; set; } public DbSet Surveys { get; set; } public DbSet SurveyQuestions { get; set; } @@ -1151,6 +1153,31 @@ public class PlatformDbContext : b.HasIndex(x => new { x.TenantId, x.Title }).IsUnique().HasFilter("[IsDeleted] = 0"); }); + builder.Entity(b => + { + b.ToTable(TableNameResolver.GetFullTableName(nameof(TableNameEnum.AnnouncementComment)), Prefix.DbSchema); + b.ConfigureByConvention(); + + b.Property(x => x.Content).IsRequired().HasMaxLength(8192); + b.Property(x => x.Likes).HasDefaultValue(0); + + b.HasOne(x => x.Announcement) + .WithMany(x => x.Comments) + .HasForeignKey(x => x.AnnouncementId) + .OnDelete(DeleteBehavior.Cascade); + }); + + builder.Entity(b => + { + b.ToTable(TableNameResolver.GetFullTableName(nameof(TableNameEnum.AnnouncementLike)), Prefix.DbSchema); + b.ConfigureByConvention(); + + b.HasOne(x => x.Announcement) + .WithMany(x => x.AnnouncementLikes) + .HasForeignKey(x => x.AnnouncementId) + .OnDelete(DeleteBehavior.Cascade); + }); + builder.Entity(b => { b.ToTable(TableNameResolver.GetFullTableName(nameof(TableNameEnum.Survey)), Prefix.DbSchema); diff --git a/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260612183658_Initial.Designer.cs b/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260615110840_Initial.Designer.cs similarity index 98% rename from api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260612183658_Initial.Designer.cs rename to api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260615110840_Initial.Designer.cs index f938a22..da3fe50 100644 --- a/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260612183658_Initial.Designer.cs +++ b/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260615110840_Initial.Designer.cs @@ -13,7 +13,7 @@ using Volo.Abp.EntityFrameworkCore; namespace Sozsoft.Platform.Migrations { [DbContext(typeof(PlatformDbContext))] - [Migration("20260612183658_Initial")] + [Migration("20260615110840_Initial")] partial class Initial { /// @@ -771,6 +771,9 @@ namespace Sozsoft.Platform.Migrations .HasColumnType("uniqueidentifier") .HasColumnName("LastModifierId"); + b.Property("Likes") + .HasColumnType("int"); + b.Property("PublishDate") .HasColumnType("datetime2"); @@ -800,6 +803,120 @@ namespace Sozsoft.Platform.Migrations b.ToTable("Adm_T_Announcement", (string)null); }); + modelBuilder.Entity("Sozsoft.Platform.Entities.AnnouncementComment", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier"); + + b.Property("AnnouncementId") + .HasColumnType("uniqueidentifier"); + + b.Property("Content") + .IsRequired() + .HasMaxLength(8192) + .HasColumnType("nvarchar(max)"); + + b.Property("CreationTime") + .HasColumnType("datetime2") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uniqueidentifier") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uniqueidentifier") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("datetime2") + .HasColumnName("DeletionTime"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("datetime2") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uniqueidentifier") + .HasColumnName("LastModifierId"); + + b.Property("Likes") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValue(0); + + b.Property("TenantId") + .HasColumnType("uniqueidentifier") + .HasColumnName("TenantId"); + + b.Property("UserId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("AnnouncementId"); + + b.ToTable("Adm_T_AnnouncementComment", (string)null); + }); + + modelBuilder.Entity("Sozsoft.Platform.Entities.AnnouncementLike", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier"); + + b.Property("AnnouncementId") + .HasColumnType("uniqueidentifier"); + + b.Property("CreationTime") + .HasColumnType("datetime2") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uniqueidentifier") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uniqueidentifier") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("datetime2") + .HasColumnName("DeletionTime"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("datetime2") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uniqueidentifier") + .HasColumnName("LastModifierId"); + + b.Property("TenantId") + .HasColumnType("uniqueidentifier") + .HasColumnName("TenantId"); + + b.Property("UserId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("AnnouncementId"); + + b.ToTable("Adm_T_AnnouncementLike", (string)null); + }); + modelBuilder.Entity("Sozsoft.Platform.Entities.BackgroundWorker", b => { b.Property("Id") @@ -8295,6 +8412,28 @@ namespace Sozsoft.Platform.Migrations b.Navigation("NotificationType"); }); + modelBuilder.Entity("Sozsoft.Platform.Entities.AnnouncementComment", b => + { + b.HasOne("Sozsoft.Platform.Entities.Announcement", "Announcement") + .WithMany("Comments") + .HasForeignKey("AnnouncementId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Announcement"); + }); + + modelBuilder.Entity("Sozsoft.Platform.Entities.AnnouncementLike", b => + { + b.HasOne("Sozsoft.Platform.Entities.Announcement", "Announcement") + .WithMany("AnnouncementLikes") + .HasForeignKey("AnnouncementId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Announcement"); + }); + modelBuilder.Entity("Sozsoft.Platform.Entities.BlogPost", b => { b.HasOne("Sozsoft.Platform.Entities.BlogCategory", "Category") @@ -8824,6 +8963,13 @@ namespace Sozsoft.Platform.Migrations b.Navigation("Notifications"); }); + modelBuilder.Entity("Sozsoft.Platform.Entities.Announcement", b => + { + b.Navigation("AnnouncementLikes"); + + b.Navigation("Comments"); + }); + modelBuilder.Entity("Sozsoft.Platform.Entities.BlogCategory", b => { b.Navigation("Posts"); diff --git a/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260612183658_Initial.cs b/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260615110840_Initial.cs similarity index 98% rename from api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260612183658_Initial.cs rename to api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260615110840_Initial.cs index 68ae347..8ffba73 100644 --- a/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260612183658_Initial.cs +++ b/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260615110840_Initial.cs @@ -497,6 +497,7 @@ namespace Sozsoft.Platform.Migrations ExpiryDate = table.Column(type: "datetime2", nullable: true), IsPinned = table.Column(type: "bit", nullable: false), ViewCount = table.Column(type: "int", nullable: false, defaultValue: 0), + Likes = table.Column(type: "int", nullable: false), Attachments = table.Column(type: "nvarchar(2048)", maxLength: 2048, nullable: true), CreationTime = table.Column(type: "datetime2", nullable: false), CreatorId = table.Column(type: "uniqueidentifier", nullable: true), @@ -2074,6 +2075,62 @@ namespace Sozsoft.Platform.Migrations onDelete: ReferentialAction.Cascade); }); + migrationBuilder.CreateTable( + name: "Adm_T_AnnouncementComment", + columns: table => new + { + Id = table.Column(type: "uniqueidentifier", nullable: false), + TenantId = table.Column(type: "uniqueidentifier", nullable: true), + AnnouncementId = table.Column(type: "uniqueidentifier", nullable: false), + UserId = table.Column(type: "uniqueidentifier", nullable: false), + Content = table.Column(type: "nvarchar(max)", maxLength: 8192, nullable: false), + Likes = table.Column(type: "int", nullable: false, defaultValue: 0), + CreationTime = table.Column(type: "datetime2", nullable: false), + CreatorId = table.Column(type: "uniqueidentifier", nullable: true), + LastModificationTime = table.Column(type: "datetime2", nullable: true), + LastModifierId = table.Column(type: "uniqueidentifier", nullable: true), + IsDeleted = table.Column(type: "bit", nullable: false, defaultValue: false), + DeleterId = table.Column(type: "uniqueidentifier", nullable: true), + DeletionTime = table.Column(type: "datetime2", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Adm_T_AnnouncementComment", x => x.Id); + table.ForeignKey( + name: "FK_Adm_T_AnnouncementComment_Adm_T_Announcement_AnnouncementId", + column: x => x.AnnouncementId, + principalTable: "Adm_T_Announcement", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "Adm_T_AnnouncementLike", + columns: table => new + { + Id = table.Column(type: "uniqueidentifier", nullable: false), + TenantId = table.Column(type: "uniqueidentifier", nullable: true), + AnnouncementId = table.Column(type: "uniqueidentifier", nullable: false), + UserId = table.Column(type: "uniqueidentifier", nullable: true), + CreationTime = table.Column(type: "datetime2", nullable: false), + CreatorId = table.Column(type: "uniqueidentifier", nullable: true), + LastModificationTime = table.Column(type: "datetime2", nullable: true), + LastModifierId = table.Column(type: "uniqueidentifier", nullable: true), + IsDeleted = table.Column(type: "bit", nullable: false, defaultValue: false), + DeleterId = table.Column(type: "uniqueidentifier", nullable: true), + DeletionTime = table.Column(type: "datetime2", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Adm_T_AnnouncementLike", x => x.Id); + table.ForeignKey( + name: "FK_Adm_T_AnnouncementLike_Adm_T_Announcement_AnnouncementId", + column: x => x.AnnouncementId, + principalTable: "Adm_T_Announcement", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + migrationBuilder.CreateTable( name: "Adm_T_JobPosition", columns: table => new @@ -3563,6 +3620,16 @@ namespace Sozsoft.Platform.Migrations unique: true, filter: "[IsDeleted] = 0"); + migrationBuilder.CreateIndex( + name: "IX_Adm_T_AnnouncementComment_AnnouncementId", + table: "Adm_T_AnnouncementComment", + column: "AnnouncementId"); + + migrationBuilder.CreateIndex( + name: "IX_Adm_T_AnnouncementLike_AnnouncementId", + table: "Adm_T_AnnouncementLike", + column: "AnnouncementId"); + migrationBuilder.CreateIndex( name: "IX_Adm_T_Department_TenantId_Name", table: "Adm_T_Department", @@ -4211,7 +4278,10 @@ namespace Sozsoft.Platform.Migrations name: "AbpUserTokens"); migrationBuilder.DropTable( - name: "Adm_T_Announcement"); + name: "Adm_T_AnnouncementComment"); + + migrationBuilder.DropTable( + name: "Adm_T_AnnouncementLike"); migrationBuilder.DropTable( name: "Adm_T_EventComment"); @@ -4408,6 +4478,9 @@ namespace Sozsoft.Platform.Migrations migrationBuilder.DropTable( name: "AbpUsers"); + migrationBuilder.DropTable( + name: "Adm_T_Announcement"); + migrationBuilder.DropTable( name: "Adm_T_Event"); diff --git a/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/PlatformDbContextModelSnapshot.cs b/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/PlatformDbContextModelSnapshot.cs index d664154..e36709a 100644 --- a/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/PlatformDbContextModelSnapshot.cs +++ b/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/PlatformDbContextModelSnapshot.cs @@ -768,6 +768,9 @@ namespace Sozsoft.Platform.Migrations .HasColumnType("uniqueidentifier") .HasColumnName("LastModifierId"); + b.Property("Likes") + .HasColumnType("int"); + b.Property("PublishDate") .HasColumnType("datetime2"); @@ -797,6 +800,120 @@ namespace Sozsoft.Platform.Migrations b.ToTable("Adm_T_Announcement", (string)null); }); + modelBuilder.Entity("Sozsoft.Platform.Entities.AnnouncementComment", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier"); + + b.Property("AnnouncementId") + .HasColumnType("uniqueidentifier"); + + b.Property("Content") + .IsRequired() + .HasMaxLength(8192) + .HasColumnType("nvarchar(max)"); + + b.Property("CreationTime") + .HasColumnType("datetime2") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uniqueidentifier") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uniqueidentifier") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("datetime2") + .HasColumnName("DeletionTime"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("datetime2") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uniqueidentifier") + .HasColumnName("LastModifierId"); + + b.Property("Likes") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValue(0); + + b.Property("TenantId") + .HasColumnType("uniqueidentifier") + .HasColumnName("TenantId"); + + b.Property("UserId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("AnnouncementId"); + + b.ToTable("Adm_T_AnnouncementComment", (string)null); + }); + + modelBuilder.Entity("Sozsoft.Platform.Entities.AnnouncementLike", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier"); + + b.Property("AnnouncementId") + .HasColumnType("uniqueidentifier"); + + b.Property("CreationTime") + .HasColumnType("datetime2") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uniqueidentifier") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uniqueidentifier") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("datetime2") + .HasColumnName("DeletionTime"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("datetime2") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uniqueidentifier") + .HasColumnName("LastModifierId"); + + b.Property("TenantId") + .HasColumnType("uniqueidentifier") + .HasColumnName("TenantId"); + + b.Property("UserId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("AnnouncementId"); + + b.ToTable("Adm_T_AnnouncementLike", (string)null); + }); + modelBuilder.Entity("Sozsoft.Platform.Entities.BackgroundWorker", b => { b.Property("Id") @@ -8292,6 +8409,28 @@ namespace Sozsoft.Platform.Migrations b.Navigation("NotificationType"); }); + modelBuilder.Entity("Sozsoft.Platform.Entities.AnnouncementComment", b => + { + b.HasOne("Sozsoft.Platform.Entities.Announcement", "Announcement") + .WithMany("Comments") + .HasForeignKey("AnnouncementId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Announcement"); + }); + + modelBuilder.Entity("Sozsoft.Platform.Entities.AnnouncementLike", b => + { + b.HasOne("Sozsoft.Platform.Entities.Announcement", "Announcement") + .WithMany("AnnouncementLikes") + .HasForeignKey("AnnouncementId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Announcement"); + }); + modelBuilder.Entity("Sozsoft.Platform.Entities.BlogPost", b => { b.HasOne("Sozsoft.Platform.Entities.BlogCategory", "Category") @@ -8821,6 +8960,13 @@ namespace Sozsoft.Platform.Migrations b.Navigation("Notifications"); }); + modelBuilder.Entity("Sozsoft.Platform.Entities.Announcement", b => + { + b.Navigation("AnnouncementLikes"); + + b.Navigation("Comments"); + }); + modelBuilder.Entity("Sozsoft.Platform.Entities.BlogCategory", b => { b.Navigation("Posts"); diff --git a/ui/src/proxy/intranet/models.ts b/ui/src/proxy/intranet/models.ts index 228eed2..ce117f5 100644 --- a/ui/src/proxy/intranet/models.ts +++ b/ui/src/proxy/intranet/models.ts @@ -36,6 +36,16 @@ export interface EventCommentDto { likes: number } +export interface AnnouncementCommentDto { + id: string + announcementId: string + userId: string + user: UserInfoViewModel + content: string + creationTime: Date + likes: number +} + // Doküman (FileItemDto ile uyumlu) export interface DocumentDto { id: string @@ -68,6 +78,9 @@ export interface AnnouncementDto { expiryDate?: Date isPinned: boolean viewCount: number + likes: number + isLiked: boolean + comments: AnnouncementCommentDto[] departments?: string[] attachments?: { name: string; url: string; size: string }[] } diff --git a/ui/src/services/intranet.service.ts b/ui/src/services/intranet.service.ts index 93df248..b125ad8 100644 --- a/ui/src/services/intranet.service.ts +++ b/ui/src/services/intranet.service.ts @@ -1,4 +1,12 @@ -import { EventCommentDto, EventDto, IntranetDashboardDto, SocialCommentDto, SocialPostDto } from '@/proxy/intranet/models' +import { + AnnouncementCommentDto, + AnnouncementDto, + EventCommentDto, + EventDto, + IntranetDashboardDto, + SocialCommentDto, + SocialPostDto, +} from '@/proxy/intranet/models' import apiService, { Config } from './api.service' export interface CreateSocialPostInput { @@ -106,6 +114,39 @@ export class IntranetService { { apiName: this.apiName, ...config }, ) + getAnnouncementComments = (announcementId: string, config?: Partial) => + apiService.fetchData( + { + method: 'GET', + url: `/api/app/intranet/announcement-comments/${announcementId}`, + }, + { apiName: this.apiName, ...config }, + ) + + createAnnouncementComment = ( + announcementId: string, + content: string, + config?: Partial, + ) => + apiService.fetchData( + { + method: 'POST', + url: `/api/app/intranet/announcement-comment/${announcementId}`, + params: { content }, + }, + { apiName: this.apiName, ...config }, + ) + + likeAnnouncement = (id: string, config?: Partial) => + apiService.fetchData( + { + method: 'POST', + url: `/api/app/intranet/like-announcement`, + params: { id }, + }, + { apiName: this.apiName, ...config }, + ) + getEventComments = (eventId: string, config?: Partial) => apiService.fetchData( { diff --git a/ui/src/views/intranet/widgets/AnnouncementModal.tsx b/ui/src/views/intranet/widgets/AnnouncementModal.tsx index 3c8818e..f2cd8ff 100644 --- a/ui/src/views/intranet/widgets/AnnouncementModal.tsx +++ b/ui/src/views/intranet/widgets/AnnouncementModal.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from 'react' +import React, { useEffect, useRef, useState } from 'react' import { createPortal } from 'react-dom' import { useLocalization } from '@/utils/hooks/useLocalization' import { motion, AnimatePresence } from 'framer-motion' @@ -9,15 +9,22 @@ import { FaChevronLeft, FaChevronRight, FaExpand, + FaCommentAlt, + FaPaperPlane, + FaHeart, } from 'react-icons/fa' import { Dialog } from '@/components/ui' import { useDialogContext } from '@/components/ui/Dialog/Dialog' -import { AnnouncementDto } from '@/proxy/intranet/models' +import { AnnouncementCommentDto, AnnouncementDto } from '@/proxy/intranet/models' import useLocale from '@/utils/hooks/useLocale' import { currentLocalDate } from '@/utils/dateUtils' import Avatar from '@/components/ui/Avatar/Avatar' import { AVATAR_URL } from '@/constants/app.constant' import { intranetService } from '@/services/intranet.service' +import dayjs from 'dayjs' +import relativeTime from 'dayjs/plugin/relativeTime' + +dayjs.extend(relativeTime) interface AnnouncementModalProps { announcement: AnnouncementDto @@ -31,6 +38,13 @@ function AnnouncementModalContent({ announcement, onClose }: AnnouncementModalPr const [activePhoto, setActivePhoto] = useState(0) const [lightboxOpen, setLightboxOpen] = useState(false) const [lightboxIndex, setLightboxIndex] = useState(0) + const [comments, setComments] = useState(announcement.comments || []) + const [commentText, setCommentText] = useState('') + const [submitting, setSubmitting] = useState(false) + const [likes, setLikes] = useState(announcement.likes ?? 0) + const [isLiked, setIsLiked] = useState(announcement.isLiked ?? false) + const [liking, setLiking] = useState(false) + const commentsEndRef = useRef(null) const openLightbox = (idx: number) => { setLightboxIndex(idx) @@ -50,6 +64,60 @@ function AnnouncementModalContent({ announcement, onClose }: AnnouncementModalPr intranetService.incrementAnnouncementViewCount(announcement.id) }, [announcement.id]) + useEffect(() => { + intranetService.getAnnouncementComments(announcement.id).then((res) => { + if (res.data) setComments(res.data) + }) + }, [announcement.id]) + + const scrollToBottom = () => { + commentsEndRef.current?.scrollIntoView({ behavior: 'smooth' }) + } + + const handleSubmitComment = async () => { + if (!commentText.trim() || submitting) return + setSubmitting(true) + try { + const res = await intranetService.createAnnouncementComment( + announcement.id, + commentText.trim(), + ) + if (res.data) { + setComments((prev) => [...prev, res.data!]) + setCommentText('') + setTimeout(scrollToBottom, 100) + } + } finally { + setSubmitting(false) + } + } + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault() + handleSubmitComment() + } + } + + const handleLike = async () => { + if (liking) return + setLiking(true) + setIsLiked((prev) => !prev) + setLikes((prev) => (isLiked ? Math.max(0, prev - 1) : prev + 1)) + try { + const res = await intranetService.likeAnnouncement(announcement.id) + if (res.data) { + setLikes(res.data.likes ?? 0) + setIsLiked(res.data.isLiked ?? false) + } + } catch { + setIsLiked((prev) => !prev) + setLikes((prev) => (isLiked ? prev + 1 : Math.max(0, prev - 1))) + } finally { + setLiking(false) + } + } + const announcementUser = announcement.user ?? {} const announcementUserName = announcementUser.fullName || @@ -129,6 +197,19 @@ function AnnouncementModalContent({ announcement, onClose }: AnnouncementModalPr {announcement.viewCount}{' '} {translate('::App.Platform.Intranet.AnnouncementDetailModal.Views')} + @@ -272,13 +353,75 @@ function AnnouncementModalContent({ announcement, onClose }: AnnouncementModalPr )} + +
+

+ + {translate('::App.Intranet.Events.Comments')} ({comments.length}) +

+ +
+ {comments.length === 0 ? ( +

+ {translate('::App.Intranet.Events.NoComment')} +

+ ) : ( + comments.map((comment) => ( +
+ {comment.user && ( + + )} +
+
+

+ {comment.user?.fullName ?? 'Kullanıcı'} +

+

+ {comment.content} +

+
+

+ {dayjs(comment.creationTime).fromNow()} +

+
+
+ )) + )} +
+
+
- + +
+