From bc192a584b88b164ad9640fcd698a77bfcf1fc81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sedat=20=C3=96ZT=C3=9CRK?= <76204082+iamsedatozturk@users.noreply.github.com> Date: Wed, 6 May 2026 16:47:18 +0300 Subject: [PATCH] SocialWall --- .../Intranet/CreateSocialPostInput.cs | 37 +++ .../Intranet/IIntranetAppService.cs | 8 +- .../Intranet/IntranetAppService.cs | 267 +++++++++++++++++- .../Seeds/LanguagesData.json | 6 + .../Seeds/ListFormSeeder_Administration.cs | 3 +- .../Entities/Tenant/Definitions/SocialPost.cs | 34 +++ .../EntityFrameworkCore/PlatformDbContext.cs | 1 - ....cs => 20260506113136_Initial.Designer.cs} | 5 +- ...9_Initial.cs => 20260506113136_Initial.cs} | 2 +- .../PlatformDbContextModelSnapshot.cs | 3 +- .../Seeds/TenantData.json | 48 ++-- .../Seeds/TenantDataSeeder.cs | 12 +- ui/src/proxy/intranet/models.ts | 2 +- ui/src/services/intranet.service.ts | 75 ++++- ui/src/views/intranet/Dashboard.tsx | 2 +- .../views/intranet/SocialWall/CreatePost.tsx | 104 ++++--- .../intranet/SocialWall/MediaManager.tsx | 26 +- ui/src/views/intranet/SocialWall/PostItem.tsx | 13 +- ui/src/views/intranet/SocialWall/index.tsx | 191 ++++++------- .../intranet/widgets/AnnouncementModal.tsx | 64 +++-- ui/src/views/intranet/widgets/Surveys.tsx | 44 ++- 21 files changed, 706 insertions(+), 241 deletions(-) create mode 100644 api/src/Sozsoft.Platform.Application.Contracts/Intranet/CreateSocialPostInput.cs rename api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/{20260506093149_Initial.Designer.cs => 20260506113136_Initial.Designer.cs} (99%) rename api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/{20260506093149_Initial.cs => 20260506113136_Initial.cs} (99%) diff --git a/api/src/Sozsoft.Platform.Application.Contracts/Intranet/CreateSocialPostInput.cs b/api/src/Sozsoft.Platform.Application.Contracts/Intranet/CreateSocialPostInput.cs new file mode 100644 index 0000000..e8afd73 --- /dev/null +++ b/api/src/Sozsoft.Platform.Application.Contracts/Intranet/CreateSocialPostInput.cs @@ -0,0 +1,37 @@ +using System.Collections.Generic; + +namespace Sozsoft.Platform.Intranet; + +public class CreateSocialPostInput +{ + public string Content { get; set; } = string.Empty; + + /// + /// JSON string containing location data (name, address, lat, lng, placeId). + /// + public string? LocationJson { get; set; } + + public CreateSocialPostMediaInput? Media { get; set; } +} + +public class CreateSocialPostMediaInput +{ + /// + /// "image", "video", or "poll" + /// + public string Type { get; set; } = string.Empty; + + /// + /// URLs for image/video type media. + /// + public string[]? Urls { get; set; } + + // Poll fields + public string? PollQuestion { get; set; } + public List? PollOptions { get; set; } +} + +public class CreateSocialPollOptionInput +{ + public string Text { get; set; } = string.Empty; +} diff --git a/api/src/Sozsoft.Platform.Application.Contracts/Intranet/IIntranetAppService.cs b/api/src/Sozsoft.Platform.Application.Contracts/Intranet/IIntranetAppService.cs index 14cf61f..bebf119 100644 --- a/api/src/Sozsoft.Platform.Application.Contracts/Intranet/IIntranetAppService.cs +++ b/api/src/Sozsoft.Platform.Application.Contracts/Intranet/IIntranetAppService.cs @@ -6,5 +6,11 @@ namespace Sozsoft.Platform.Intranet; public interface IIntranetAppService : IApplicationService { Task GetIntranetDashboardAsync(); - Task UpdateSurveyResponseAsync(SubmitSurveyInput input); + Task CreateSurveyResponseAsync(SubmitSurveyInput input); + Task CreateSocialPostAsync(CreateSocialPostInput input); + Task DeleteSocialPostAsync(System.Guid id); + Task LikeSocialPostAsync(System.Guid id); + Task CommentSocialPostAsync(System.Guid id, string content); + Task VoteSocialPollAsync(System.Guid postId, System.Guid optionId); + Task IncrementAnnouncementViewCountAsync(System.Guid id); } diff --git a/api/src/Sozsoft.Platform.Application/Intranet/IntranetAppService.cs b/api/src/Sozsoft.Platform.Application/Intranet/IntranetAppService.cs index 78d4e9f..014e8b6 100644 --- a/api/src/Sozsoft.Platform.Application/Intranet/IntranetAppService.cs +++ b/api/src/Sozsoft.Platform.Application/Intranet/IntranetAppService.cs @@ -36,6 +36,10 @@ public class IntranetAppService : PlatformAppService, IIntranetAppService private readonly IRepository _surveyResponseRepository; private readonly IRepository _surveyAnswerRepository; private readonly IRepository _socialPostRepository; + private readonly IRepository _socialCommentRepository; + private readonly IRepository _socialLikeRepository; + private readonly IRepository _socialMediaRepository; + private readonly IRepository _socialPollOptionRepository; public IntranetAppService( ICurrentTenant currentTenant, @@ -50,7 +54,11 @@ public class IntranetAppService : PlatformAppService, IIntranetAppService IRepository surveyRepository, IRepository surveyResponseRepository, IRepository surveyAnswerRepository, - IRepository socialPostRepository + IRepository socialPostRepository, + IRepository socialCommentRepository, + IRepository socialLikeRepository, + IRepository socialMediaRepository, + IRepository socialPollOptionRepository ) { _currentTenant = currentTenant; @@ -65,6 +73,10 @@ public class IntranetAppService : PlatformAppService, IIntranetAppService _surveyResponseRepository = surveyResponseRepository; _surveyAnswerRepository = surveyAnswerRepository; _socialPostRepository = socialPostRepository; + _socialCommentRepository = socialCommentRepository; + _socialLikeRepository = socialLikeRepository; + _socialMediaRepository = socialMediaRepository; + _socialPollOptionRepository = socialPollOptionRepository; } [UnitOfWork] @@ -236,7 +248,7 @@ public class IntranetAppService : PlatformAppService, IIntranetAppService var queryable = await _socialPostRepository .WithDetailsAsync(e => e.Location, e => e.Media, e => e.Comments, e => e.Likes); - var socialPosts = await AsyncExecuter.ToListAsync(queryable); + var socialPosts = await AsyncExecuter.ToListAsync(queryable.OrderByDescending(p => p.CreationTime)); var dtos = ObjectMapper.Map, List>(socialPosts); @@ -306,9 +318,41 @@ public class IntranetAppService : PlatformAppService, IIntranetAppService } } + foreach (var dto in dtos) + { + dto.IsOwnPost = dto.UserId == CurrentUser.Id; + dto.IsLiked = dto.Likes.Any(l => l.UserId == CurrentUser.Id); + } + + await EnrichPollOptionsAsync(dtos); + return dtos; } + private async Task EnrichPollOptionsAsync(IEnumerable dtos) + { + var pollMediaIds = dtos + .Where(d => d.Media?.Type == "poll") + .Select(d => d.Media!.Id) + .ToList(); + + if (pollMediaIds.Count == 0) return; + + var optionsQueryable = await _socialPollOptionRepository.GetQueryableAsync(); + var allOptions = await AsyncExecuter.ToListAsync( + optionsQueryable.Where(o => pollMediaIds.Contains(o.SocialMediaId) && !o.IsDeleted)); + + var optionsByMedia = allOptions + .GroupBy(o => o.SocialMediaId) + .ToDictionary(g => g.Key, g => g.ToList()); + + foreach (var dto in dtos) + { + if (dto.Media?.Type == "poll" && optionsByMedia.TryGetValue(dto.Media.Id, out var opts)) + dto.Media.PollOptions = ObjectMapper.Map, List>(opts); + } + } + public async Task> GetIntranetDocumentsAsync(string folderPath) { var items = new List(); @@ -382,8 +426,7 @@ public class IntranetAppService : PlatformAppService, IIntranetAppService }; } - [HttpPost] - public async Task UpdateSurveyResponseAsync(SubmitSurveyInput input) + public async Task CreateSurveyResponseAsync(SubmitSurveyInput input) { var survey = await _surveyRepository.GetAsync(input.SurveyId); @@ -455,5 +498,221 @@ public class IntranetAppService : PlatformAppService, IIntranetAppService survey.Responses++; await _surveyRepository.UpdateAsync(survey); } + + public async Task CreateSocialPostAsync(CreateSocialPostInput input) + { + var post = new SocialPost(Guid.NewGuid()) + { + UserId = CurrentUser.Id, + Content = input.Content, + }; + + if (!string.IsNullOrWhiteSpace(input.LocationJson)) + { + var locData = System.Text.Json.JsonSerializer.Deserialize(input.LocationJson); + post.Location = new SocialLocation(Guid.NewGuid()) + { + SocialPostId = post.Id, + Name = locData.TryGetProperty("name", out var nameProp) ? nameProp.GetString() ?? string.Empty : string.Empty, + Address = locData.TryGetProperty("address", out var addrProp) ? addrProp.GetString() : null, + Lat = locData.TryGetProperty("lat", out var latProp) && latProp.TryGetDouble(out var latVal) ? latVal : null, + Lng = locData.TryGetProperty("lng", out var lngProp) && lngProp.TryGetDouble(out var lngVal) ? lngVal : null, + PlaceId = locData.TryGetProperty("placeId", out var placeIdProp) ? placeIdProp.GetString() : null, + }; + } + + if (input.Media != null) + { + var media = new SocialMedia(Guid.NewGuid()) + { + SocialPostId = post.Id, + Type = input.Media.Type, + Urls = input.Media.Urls ?? [], + PollQuestion = input.Media.PollQuestion, + }; + + if (input.Media.PollOptions is { Count: > 0 }) + { + media.PollOptions = input.Media.PollOptions + .Select(o => new SocialPollOption(Guid.NewGuid()) + { + SocialMediaId = media.Id, + Text = o.Text, + Votes = 0, + }) + .ToList(); + } + + post.Media = media; + } + + await _socialPostRepository.InsertAsync(post, autoSave: true); + + // Reload with full navigation properties for mapping + var queryable = await _socialPostRepository + .WithDetailsAsync(e => e.Location, e => e.Media, e => e.Comments, e => e.Likes); + var savedPost = await AsyncExecuter.FirstOrDefaultAsync(queryable.Where(p => p.Id == post.Id)); + + var dto = ObjectMapper.Map(savedPost!); + dto.IsOwnPost = true; + + if (CurrentUser.Id.HasValue) + { + var user = await _identityUserRepository.FindAsync(CurrentUser.Id.Value); + if (user != null) + dto.User = ObjectMapper.Map(user); + } + + await EnrichPollOptionsAsync([dto]); + + return dto; + } + + public async Task DeleteSocialPostAsync(Guid id) + { + var post = await _socialPostRepository.GetAsync(id); + if (post.UserId != CurrentUser.Id) + throw new Volo.Abp.Authorization.AbpAuthorizationException("You can only delete your own posts."); + + await _socialPostRepository.DeleteAsync(id); + } + + [HttpPost("api/app/intranet/like-social-post")] + public async Task LikeSocialPostAsync(Guid id) + { + var post = await _socialPostRepository.GetAsync(id); + + var likeQueryable = await _socialLikeRepository.GetQueryableAsync(); + var existingLike = await AsyncExecuter.FirstOrDefaultAsync( + likeQueryable.Where(l => l.SocialPostId == id && l.UserId == CurrentUser.Id)); + + bool isNowLiked; + if (existingLike != null) + { + await _socialLikeRepository.DeleteAsync(existingLike.Id); + post.LikeCount = Math.Max(0, post.LikeCount - 1); + isNowLiked = false; + } + else + { + await _socialLikeRepository.InsertAsync(new SocialLike(Guid.NewGuid()) + { + SocialPostId = id, + UserId = CurrentUser.Id, + }); + post.LikeCount++; + isNowLiked = true; + } + + post.IsLiked = isNowLiked; + await _socialPostRepository.UpdateAsync(post, autoSave: true); + + var queryable = await _socialPostRepository + .WithDetailsAsync(e => e.Location, e => e.Media, e => e.Comments, e => e.Likes); + var updated = await AsyncExecuter.FirstOrDefaultAsync(queryable.Where(p => p.Id == id)); + var dto = ObjectMapper.Map(updated!); + + // Resolve user info + var userIds = new List { dto.UserId } + .Union(dto.Comments.Select(c => c.UserId)) + .Union(dto.Likes.Select(l => l.UserId)) + .Where(uid => uid.HasValue) + .Select(uid => uid!.Value) + .Distinct() + .ToList(); + + if (userIds.Count > 0) + { + var allDepartments = await _departmentRepository.GetListAsync(); + var departmentDict = allDepartments.ToDictionary(d => d.Id, d => d.Name); + var allJobPositions = await _jobPositionRepository.GetListAsync(); + var jobPositionDict = allJobPositions.ToDictionary(j => j.Id, j => j); + var users = await _identityUserRepository.GetListAsync(); + var userMap = users.Where(u => userIds.Contains(u.Id)).ToDictionary(u => u.Id, u => + { + var vm = ObjectMapper.Map(u); + var deptId = u.GetDepartmentId(); + if (deptId != Guid.Empty && departmentDict.TryGetValue(deptId, out var deptName)) + { + vm.DepartmentId = deptId; + vm.Departments = [new AssignedDepartmentViewModel { Id = deptId, Name = deptName, IsAssigned = true }]; + } + var jobPosId = u.GetJobPositionId(); + if (jobPosId != Guid.Empty && jobPositionDict.TryGetValue(jobPosId, out var jobPosition)) + { + vm.JobPositionId = jobPosId; + vm.JobPositions = [new AssignedJobPoisitionViewModel { Id = jobPosId, Name = jobPosition.Name, DepartmentId = jobPosition.DepartmentId, IsAssigned = true }]; + } + return vm; + }); + + if (dto.UserId.HasValue && userMap.TryGetValue(dto.UserId.Value, out var postUser)) + dto.User = postUser; + foreach (var comment in dto.Comments) + if (comment.UserId.HasValue && userMap.TryGetValue(comment.UserId.Value, out var commentUser)) + comment.User = commentUser; + foreach (var like in dto.Likes) + if (like.UserId.HasValue && userMap.TryGetValue(like.UserId.Value, out var likeUser)) + like.User = likeUser; + } + + await EnrichPollOptionsAsync([dto]); + + dto.IsLiked = isNowLiked; + dto.IsOwnPost = dto.UserId == CurrentUser.Id; + return dto; + } + + [HttpPost("api/app/intranet/comment-social-post")] + public async Task CommentSocialPostAsync(Guid id, string content) + { + var comment = new SocialComment(Guid.NewGuid()) + { + SocialPostId = id, + UserId = CurrentUser.Id, + Content = content, + }; + + await _socialCommentRepository.InsertAsync(comment, autoSave: true); + + var dto = ObjectMapper.Map(comment); + + if (CurrentUser.Id.HasValue) + { + var user = await _identityUserRepository.FindAsync(CurrentUser.Id.Value); + if (user != null) + dto.User = ObjectMapper.Map(user); + } + + return dto; + } + + [HttpPost("api/app/intranet/vote-social-poll")] + public async Task VoteSocialPollAsync(Guid postId, Guid optionId) + { + var post = await _socialPostRepository.GetAsync(postId); + + var mediaQueryable = await _socialMediaRepository.GetQueryableAsync(); + var media = await AsyncExecuter.FirstOrDefaultAsync( + mediaQueryable.Where(m => m.SocialPostId == postId)); + + if (media == null) return; + + var option = await _socialPollOptionRepository.GetAsync(optionId); + option.Votes++; + await _socialPollOptionRepository.UpdateAsync(option, autoSave: true); + + media.PollTotalVotes = (media.PollTotalVotes ?? 0) + 1; + media.PollUserVoteId = optionId.ToString(); + await _socialMediaRepository.UpdateAsync(media, autoSave: true); + } + + [HttpPost("api/app/intranet/{id}/announcement-view")] + public async Task IncrementAnnouncementViewCountAsync(Guid id) + { + var announcement = await _announcementRepository.GetAsync(id); + announcement.ViewCount++; + await _announcementRepository.UpdateAsync(announcement, autoSave: true); + } } diff --git a/api/src/Sozsoft.Platform.DbMigrator/Seeds/LanguagesData.json b/api/src/Sozsoft.Platform.DbMigrator/Seeds/LanguagesData.json index 8f13083..16caa31 100644 --- a/api/src/Sozsoft.Platform.DbMigrator/Seeds/LanguagesData.json +++ b/api/src/Sozsoft.Platform.DbMigrator/Seeds/LanguagesData.json @@ -12372,6 +12372,12 @@ "tr": "Anketi Doldur", "en": "Fill Survey" }, + { + "resourceName": "Platform", + "key": "App.Platform.Intranet.Widgets.ActiveSurveys.ViewResponses", + "tr": "Yanıtları Görüntüle", + "en": "View Responses" + }, { "resourceName": "Platform", "key": "App.Platform.Intranet.Widgets.ActiveSurveys.NoActive", diff --git a/api/src/Sozsoft.Platform.DbMigrator/Seeds/ListFormSeeder_Administration.cs b/api/src/Sozsoft.Platform.DbMigrator/Seeds/ListFormSeeder_Administration.cs index 4195c4d..3c802bd 100644 --- a/api/src/Sozsoft.Platform.DbMigrator/Seeds/ListFormSeeder_Administration.cs +++ b/api/src/Sozsoft.Platform.DbMigrator/Seeds/ListFormSeeder_Administration.cs @@ -3935,10 +3935,9 @@ public class ListFormSeeder_Administration : IDataSeedContributor, ITransientDep CaptionName = "App.Listform.ListformField.SocialPostId", Width = 100, ListOrderNo = 3, - Visible = true, + Visible = false, IsActive = true, AllowSearch = true, - LookupJson = LookupQueryValues.DefaultLookupQueryJson(nameof(TableNameEnum.SocialPost), "Id", "Content"), ValidationRuleJson = DefaultValidationRuleRequiredJson, ColumnCustomizationJson = DefaultColumnCustomizationJson, PermissionJson = DefaultFieldPermissionJson(listForm.Name), diff --git a/api/src/Sozsoft.Platform.Domain/Entities/Tenant/Definitions/SocialPost.cs b/api/src/Sozsoft.Platform.Domain/Entities/Tenant/Definitions/SocialPost.cs index fa03308..42d18a1 100644 --- a/api/src/Sozsoft.Platform.Domain/Entities/Tenant/Definitions/SocialPost.cs +++ b/api/src/Sozsoft.Platform.Domain/Entities/Tenant/Definitions/SocialPost.cs @@ -22,6 +22,11 @@ public class SocialPost : FullAuditedEntity, IMultiTenant public SocialMedia Media { get; set; } public ICollection Comments { get; set; } public ICollection Likes { get; set; } + + public SocialPost(Guid id) + { + Id = id; + } } public class SocialLocation : FullAuditedEntity, IMultiTenant @@ -36,6 +41,11 @@ public class SocialLocation : FullAuditedEntity, IMultiTenant public double? Lat { get; set; } public double? Lng { get; set; } public string? PlaceId { get; set; } + + public SocialLocation(Guid id) + { + Id = id; + } } public class SocialMedia : FullAuditedEntity, IMultiTenant @@ -55,6 +65,11 @@ public class SocialMedia : FullAuditedEntity, IMultiTenant public string? PollUserVoteId { get; set; } public ICollection PollOptions { get; set; } + + public SocialMedia(Guid id) + { + Id = id; + } } public class SocialPollOption : FullAuditedEntity, IMultiTenant @@ -66,6 +81,13 @@ public class SocialPollOption : FullAuditedEntity, IMultiTenant public string Text { get; set; } public int Votes { get; set; } + + public SocialPollOption(Guid id) + { + Id = id; + } + + protected SocialPollOption() { } } public class SocialComment : FullAuditedEntity, IMultiTenant @@ -78,6 +100,13 @@ public class SocialComment : FullAuditedEntity, IMultiTenant public Guid? UserId { get; set; } public string Content { get; set; } + + public SocialComment(Guid id) + { + Id = id; + } + + protected SocialComment() { } } public class SocialLike : FullAuditedEntity, IMultiTenant @@ -88,4 +117,9 @@ public class SocialLike : FullAuditedEntity, IMultiTenant public SocialPost SocialPost { get; set; } public Guid? UserId { get; set; } + + public SocialLike(Guid id) + { + Id = id; + } } diff --git a/api/src/Sozsoft.Platform.EntityFrameworkCore/EntityFrameworkCore/PlatformDbContext.cs b/api/src/Sozsoft.Platform.EntityFrameworkCore/EntityFrameworkCore/PlatformDbContext.cs index e5aca43..2b0a5dd 100644 --- a/api/src/Sozsoft.Platform.EntityFrameworkCore/EntityFrameworkCore/PlatformDbContext.cs +++ b/api/src/Sozsoft.Platform.EntityFrameworkCore/EntityFrameworkCore/PlatformDbContext.cs @@ -1162,7 +1162,6 @@ public class PlatformDbContext : b.ConfigureByConvention(); b.Property(x => x.Type).IsRequired().HasMaxLength(64); - b.Property(x => x.Urls).HasMaxLength(2048); b.Property(x => x.PollQuestion).HasMaxLength(512); b.Property(x => x.PollUserVoteId).HasMaxLength(128); diff --git a/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260506093149_Initial.Designer.cs b/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260506113136_Initial.Designer.cs similarity index 99% rename from api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260506093149_Initial.Designer.cs rename to api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260506113136_Initial.Designer.cs index 18479f2..d87816e 100644 --- a/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260506093149_Initial.Designer.cs +++ b/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260506113136_Initial.Designer.cs @@ -13,7 +13,7 @@ using Volo.Abp.EntityFrameworkCore; namespace Sozsoft.Platform.Migrations { [DbContext(typeof(PlatformDbContext))] - [Migration("20260506093149_Initial")] + [Migration("20260506113136_Initial")] partial class Initial { /// @@ -4085,8 +4085,7 @@ namespace Sozsoft.Platform.Migrations .HasColumnType("nvarchar(64)"); b.PrimitiveCollection("Urls") - .HasMaxLength(2048) - .HasColumnType("nvarchar(2048)"); + .HasColumnType("nvarchar(max)"); b.HasKey("Id"); diff --git a/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260506093149_Initial.cs b/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260506113136_Initial.cs similarity index 99% rename from api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260506093149_Initial.cs rename to api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260506113136_Initial.cs index 19009c9..c598019 100644 --- a/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260506093149_Initial.cs +++ b/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260506113136_Initial.cs @@ -2040,7 +2040,7 @@ namespace Sozsoft.Platform.Migrations TenantId = table.Column(type: "uniqueidentifier", nullable: true), SocialPostId = table.Column(type: "uniqueidentifier", nullable: false), Type = table.Column(type: "nvarchar(64)", maxLength: 64, nullable: false), - Urls = table.Column(type: "nvarchar(2048)", maxLength: 2048, nullable: true), + Urls = table.Column(type: "nvarchar(max)", nullable: true), PollQuestion = table.Column(type: "nvarchar(512)", maxLength: 512, nullable: true), PollTotalVotes = table.Column(type: "int", nullable: true), PollEndsAt = table.Column(type: "datetime2", nullable: true), diff --git a/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/PlatformDbContextModelSnapshot.cs b/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/PlatformDbContextModelSnapshot.cs index 445beb6..b42a7b7 100644 --- a/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/PlatformDbContextModelSnapshot.cs +++ b/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/PlatformDbContextModelSnapshot.cs @@ -4082,8 +4082,7 @@ namespace Sozsoft.Platform.Migrations .HasColumnType("nvarchar(64)"); b.PrimitiveCollection("Urls") - .HasMaxLength(2048) - .HasColumnType("nvarchar(2048)"); + .HasColumnType("nvarchar(max)"); b.HasKey("Id"); diff --git a/api/src/Sozsoft.Platform.EntityFrameworkCore/Seeds/TenantData.json b/api/src/Sozsoft.Platform.EntityFrameworkCore/Seeds/TenantData.json index 75695c7..bf62672 100644 --- a/api/src/Sozsoft.Platform.EntityFrameworkCore/Seeds/TenantData.json +++ b/api/src/Sozsoft.Platform.EntityFrameworkCore/Seeds/TenantData.json @@ -1301,7 +1301,7 @@ "userName": "system@sozsoft.com", "publishDate": "12-10-2024", "isPinned": true, - "viewCount": 156, + "viewCount": 0, "imageUrl": "https://images.unsplash.com/photo-1497366216548-37526070297c?w=800&q=80" }, { @@ -1313,7 +1313,7 @@ "publishDate": "08-10-2024", "expiryDate": "05-11-2024", "isPinned": true, - "viewCount": 89 + "viewCount": 0 }, { "title": "💻 Sistem Bakımı Duyurusu", @@ -1323,7 +1323,7 @@ "userName": "system@sozsoft.com", "publishDate": "08-10-2024", "isPinned": false, - "viewCount": 234 + "viewCount": 0 }, { "title": "🎓 React İleri Seviye Eğitimi", @@ -1333,7 +1333,7 @@ "userName": "system@sozsoft.com", "publishDate": "09-10-2024", "isPinned": false, - "viewCount": 67 + "viewCount": 0 }, { "title": "⚠️ Güvenlik Politikası Güncellemesi", @@ -1343,7 +1343,7 @@ "userName": "system@sozsoft.com", "publishDate": "04-10-2024", "isPinned": true, - "viewCount": 312 + "viewCount": 0 } ], "Surveys": [ @@ -1515,44 +1515,44 @@ { "content": "Yeni proje üzerinde çalışıyoruz! React ve TypeScript ile harika bir deneyim oluşturuyoruz. Ekip çalışması harika gidiyor! 🚀", "userName": "system@sozsoft.com", - "likeCount": 24, - "isLiked": true, - "isOwnPost": false + "likeCount": 0, + "isLiked": false, + "isOwnPost": true }, { "content": "Bu hafta sprint planlamasını yaptık. Ekibimizle birlikte yeni özellikleri değerlendirdik. Heyecan verici bir hafta olacak!", "userName": "system@sozsoft.com", - "likeCount": 18, + "likeCount": 0, "isLiked": false, "isOwnPost": true }, { "content": "Yeni tasarım sistemimizin ilk prototipini hazırladık! Kullanıcı deneyimini iyileştirmek için çok çalıştık. Geri bildirimlerinizi bekliyorum! 🎨", "userName": "system@sozsoft.com", - "likeCount": 42, - "isLiked": true, - "isOwnPost": false + "likeCount": 0, + "isLiked": false, + "isOwnPost": true }, { "content": "CI/CD pipeline güncellememiz tamamlandı! Deployment süremiz %40 azaldı. Otomasyonun gücü 💪", "userName": "system@sozsoft.com", - "likeCount": 31, + "likeCount": 0, "isLiked": false, - "isOwnPost": false + "isOwnPost": true }, { "content": "Ekip üyelerimize yeni eğitim programımızı duyurmak istiyorum! 🎓 React, TypeScript ve Modern Web Geliştirme konularında kapsamlı bir program hazırladık.", "userName": "system@sozsoft.com", - "likeCount": 56, - "isLiked": true, - "isOwnPost": false + "likeCount": 0, + "isLiked": false, + "isOwnPost": true }, { "content": "Bugün müşteri ile harika bir toplantı yaptık! Yeni projenin detaylarını konuştuk. 🎯", "userName": "system@sozsoft.com", - "likeCount": 18, + "likeCount": 0, "isLiked": false, - "isOwnPost": false + "isOwnPost": true } ], "SocialLocations": [ @@ -1585,7 +1585,7 @@ "postContent": "Bu hafta sprint planlamasını yaptık. Ekibimizle birlikte yeni özellikleri değerlendirdik. Heyecan verici bir hafta olacak!", "type": "poll", "pollQuestion": "Hangi özelliği öncelikli olarak geliştirmeliyiz?", - "pollTotalVotes": 40, + "pollTotalVotes": 0, "pollEndsAt": "2024-10-20T23:59:59", "pollUserVoteId": "p3" }, @@ -1611,25 +1611,25 @@ "postContent": "Bu hafta sprint planlamasını yaptık. Ekibimizle birlikte yeni özellikleri değerlendirdik. Heyecan verici bir hafta olacak!", "pollQuestion": "Hangi özelliği öncelikli olarak geliştirmeliyiz?", "Text": "Kullanıcı profilleri", - "Votes": 12 + "Votes": 0 }, { "postContent": "Bu hafta sprint planlamasını yaptık. Ekibimizle birlikte yeni özellikleri değerlendirdik. Heyecan verici bir hafta olacak!", "pollQuestion": "Hangi özelliği öncelikli olarak geliştirmeliyiz?", "Text": "Bildirim sistemi", - "Votes": 8 + "Votes": 0 }, { "postContent": "Bu hafta sprint planlamasını yaptık. Ekibimizle birlikte yeni özellikleri değerlendirdik. Heyecan verici bir hafta olacak!", "pollQuestion": "Hangi özelliği öncelikli olarak geliştirmeliyiz?", "Text": "Mesajlaşma", - "Votes": 15 + "Votes": 0 }, { "postContent": "Bu hafta sprint planlamasını yaptık. Ekibimizle birlikte yeni özellikleri değerlendirdik. Heyecan verici bir hafta olacak!", "pollQuestion": "Hangi özelliği öncelikli olarak geliştirmeliyiz?", "Text": "Raporlama", - "Votes": 5 + "Votes": 0 } ], "SocialComments": [ diff --git a/api/src/Sozsoft.Platform.EntityFrameworkCore/Seeds/TenantDataSeeder.cs b/api/src/Sozsoft.Platform.EntityFrameworkCore/Seeds/TenantDataSeeder.cs index 5e8d0e7..36b358d 100644 --- a/api/src/Sozsoft.Platform.EntityFrameworkCore/Seeds/TenantDataSeeder.cs +++ b/api/src/Sozsoft.Platform.EntityFrameworkCore/Seeds/TenantDataSeeder.cs @@ -992,7 +992,7 @@ public class TenantDataSeeder : IDataSeedContributor, ITransientDependency var user = await _repositoryUser.FindByNormalizedUserNameAsync(item.UserName); - await _socialPostRepository.InsertAsync(new SocialPost + await _socialPostRepository.InsertAsync(new SocialPost(Guid.NewGuid()) { UserId = user != null ? user.Id : null, Content = item.Content, @@ -1013,7 +1013,7 @@ public class TenantDataSeeder : IDataSeedContributor, ITransientDependency if (exists) continue; - await _socialLocationRepository.InsertAsync(new SocialLocation + await _socialLocationRepository.InsertAsync(new SocialLocation(Guid.NewGuid()) { SocialPostId = post != null ? post.Id : Guid.Empty, Name = item.Name, @@ -1035,7 +1035,7 @@ public class TenantDataSeeder : IDataSeedContributor, ITransientDependency if (exists) continue; - await _socialMediaRepository.InsertAsync(new SocialMedia + await _socialMediaRepository.InsertAsync(new SocialMedia(Guid.NewGuid()) { SocialPostId = post != null ? post.Id : Guid.Empty, Type = item.Type, @@ -1061,7 +1061,7 @@ public class TenantDataSeeder : IDataSeedContributor, ITransientDependency if (exists) continue; - await _socialPollOptionRepository.InsertAsync(new SocialPollOption + await _socialPollOptionRepository.InsertAsync(new SocialPollOption(Guid.NewGuid()) { SocialMediaId = media != null ? media.Id : Guid.Empty, Text = item.Text, @@ -1081,7 +1081,7 @@ public class TenantDataSeeder : IDataSeedContributor, ITransientDependency continue; var user = await _repositoryUser.FindByNormalizedUserNameAsync(item.UserName); - await _socialCommentRepository.InsertAsync(new SocialComment + await _socialCommentRepository.InsertAsync(new SocialComment(Guid.NewGuid()) { UserId = user != null ? user.Id : null, SocialPostId = post != null ? post.Id : Guid.Empty, @@ -1101,7 +1101,7 @@ public class TenantDataSeeder : IDataSeedContributor, ITransientDependency continue; var user = await _repositoryUser.FindByNormalizedUserNameAsync(item.UserName); - await _socialLikeRepository.InsertAsync(new SocialLike + await _socialLikeRepository.InsertAsync(new SocialLike(Guid.NewGuid()) { SocialPostId = post != null ? post.Id : Guid.Empty, UserId = user != null ? user.Id : null diff --git a/ui/src/proxy/intranet/models.ts b/ui/src/proxy/intranet/models.ts index f407c9e..bb91177 100644 --- a/ui/src/proxy/intranet/models.ts +++ b/ui/src/proxy/intranet/models.ts @@ -95,7 +95,7 @@ export interface SurveyDto { // Sosyal Duvar - Comment Interface export interface SocialCommentDto { id: string - creator: UserInfoViewModel + user: UserInfoViewModel content: string creationTime: Date } diff --git a/ui/src/services/intranet.service.ts b/ui/src/services/intranet.service.ts index c98633e..007bdcb 100644 --- a/ui/src/services/intranet.service.ts +++ b/ui/src/services/intranet.service.ts @@ -1,6 +1,17 @@ -import { IntranetDashboardDto } from '@/proxy/intranet/models' +import { IntranetDashboardDto, SocialCommentDto, SocialPostDto } from '@/proxy/intranet/models' import apiService, { Config } from './api.service' +export interface CreateSocialPostInput { + content: string + locationJson?: string + media?: { + type: 'image' | 'video' | 'poll' + urls?: string[] + pollQuestion?: string + pollOptions?: { text: string }[] + } +} + export class IntranetService { apiName = 'Default' @@ -13,7 +24,7 @@ export class IntranetService { { apiName: this.apiName, ...config }, ) - updateSurveyResponse = ( + createSurveyResponse = ( surveyId: string, answers: { questionId: string; questionType: string; value: string }[], config?: Partial, @@ -21,11 +32,69 @@ export class IntranetService { apiService.fetchData( { method: 'POST', - url: '/api/app/intranet/update-survey-response', + url: '/api/app/intranet/survey-response', data: { surveyId, answers }, }, { apiName: this.apiName, ...config }, ) + + createSocialPost = (input: CreateSocialPostInput, config?: Partial) => + apiService.fetchData( + { + method: 'POST', + url: '/api/app/intranet/social-post', + data: input, + }, + { apiName: this.apiName, ...config }, + ) + + deleteSocialPost = (id: string, config?: Partial) => + apiService.fetchData( + { + method: 'DELETE', + url: `/api/app/intranet/${id}/social-post`, + }, + { apiName: this.apiName, ...config }, + ) + + likeSocialPost = (id: string, config?: Partial) => + apiService.fetchData( + { + method: 'POST', + url: `/api/app/intranet/like-social-post`, + params: { id }, + }, + { apiName: this.apiName, ...config }, + ) + + commentSocialPost = (id: string, content: string, config?: Partial) => + apiService.fetchData( + { + method: 'POST', + url: `/api/app/intranet/comment-social-post`, + params: { id, content }, + }, + { apiName: this.apiName, ...config }, + ) + + voteSocialPoll = (postId: string, optionId: string, config?: Partial) => + apiService.fetchData( + { + method: 'POST', + url: `/api/app/intranet/vote-social-poll`, + params: { postId, optionId }, + }, + { apiName: this.apiName, ...config }, + ) + + incrementAnnouncementViewCount = (id: string, config?: Partial) => + apiService.fetchData( + { + method: 'POST', + url: `/api/app/intranet/${id}/announcement-view`, + }, + { apiName: this.apiName, ...config }, + ) } export const intranetService = new IntranetService() diff --git a/ui/src/views/intranet/Dashboard.tsx b/ui/src/views/intranet/Dashboard.tsx index bcb64ab..446eec3 100644 --- a/ui/src/views/intranet/Dashboard.tsx +++ b/ui/src/views/intranet/Dashboard.tsx @@ -83,7 +83,7 @@ const IntranetDashboard: React.FC = () => { const handleSubmitSurvey = async (answers: SurveyAnswerDto[]) => { if (!selectedSurvey) return try { - await intranetService.updateSurveyResponse( + await intranetService.createSurveyResponse( selectedSurvey.id, answers.map((a) => ({ questionId: a.questionId, diff --git a/ui/src/views/intranet/SocialWall/CreatePost.tsx b/ui/src/views/intranet/SocialWall/CreatePost.tsx index 20d0b87..e6e040f 100644 --- a/ui/src/views/intranet/SocialWall/CreatePost.tsx +++ b/ui/src/views/intranet/SocialWall/CreatePost.tsx @@ -2,16 +2,14 @@ import React, { useState, useRef } from 'react' import { motion, AnimatePresence } from 'framer-motion' import classNames from 'classnames' import EmojiPicker, { EmojiClickData } from 'emoji-picker-react' -import { - FaChartBar, - FaSmile, - FaTimes, - FaImages, - FaMapMarkerAlt -} from 'react-icons/fa' +import { FaChartBar, FaSmile, FaTimes, FaImages, FaMapMarkerAlt } from 'react-icons/fa' import MediaManager from './MediaManager' import LocationPicker from './LocationPicker' import { SocialMediaDto } from '@/proxy/intranet/models' +import { useLocalization } from '@/utils/hooks/useLocalization' +import { useStoreState } from '@/store/store' +import { Avatar } from '@/components/ui' +import { AVATAR_URL } from '@/constants/app.constant' interface CreatePostProps { onCreatePost: (post: { @@ -28,12 +26,8 @@ interface CreatePostProps { }) => void } -import { useLocalization } from '@/utils/hooks/useLocalization' -import { useStoreState } from '@/store/store' -import { Avatar } from '@/components/ui' -import { AVATAR_URL } from '@/constants/app.constant' const CreatePost: React.FC = ({ onCreatePost }) => { - const { translate } = useLocalization(); + const { translate } = useLocalization() const [content, setContent] = useState('') const [mediaType, setMediaType] = useState<'media' | 'poll' | null>(null) const [mediaItems, setMediaItems] = useState([]) @@ -58,22 +52,26 @@ const CreatePost: React.FC = ({ onCreatePost }) => { if (mediaType === 'media' && mediaItems.length > 0) { media = { type: 'mixed' as const, - mediaItems + mediaItems, } - } else if (mediaType === 'poll' && pollQuestion && pollOptions.filter(o => o.trim()).length >= 2) { + } else if ( + mediaType === 'poll' && + pollQuestion && + pollOptions.filter((o) => o.trim()).length >= 2 + ) { media = { type: 'poll' as const, poll: { question: pollQuestion, - options: pollOptions.filter(o => o.trim()).map(text => ({ text })) - } + options: pollOptions.filter((o) => o.trim()).map((text) => ({ text })), + }, } } - onCreatePost({ - content, + onCreatePost({ + content, media, - location: location || undefined + location: location || undefined, }) // Reset form @@ -169,7 +167,7 @@ const CreatePost: React.FC = ({ onCreatePost }) => { placeholder={translate('::App.Platform.Intranet.SocialWall.CreatePost.Placeholder')} className={classNames( 'w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500 resize-none transition-all', - isExpanded ? 'min-h-[120px]' : 'min-h-[48px]' + isExpanded ? 'min-h-[120px]' : 'min-h-[48px]', )} rows={isExpanded ? 4 : 1} /> @@ -187,7 +185,8 @@ const CreatePost: React.FC = ({ onCreatePost }) => { > - {translate('::App.Platform.Intranet.SocialWall.CreatePost.MediaTitle')} ({mediaItems.length}) + {translate('::App.Platform.Intranet.SocialWall.CreatePost.MediaTitle')} ( + {mediaItems.length}) = ({ onCreatePost }) => { clearMedia() }} className="text-sm text-red-600 hover:text-red-700 font-medium" - title={translate('::App.Platform.Intranet.SocialWall.CreatePost.RemoveAllMediaTitle')} + title={translate( + '::App.Platform.Intranet.SocialWall.CreatePost.RemoveAllMediaTitle', + )} > {translate('::Cancel')} @@ -211,7 +212,10 @@ const CreatePost: React.FC = ({ onCreatePost }) => { /> ) : ( - + @@ -250,12 +254,16 @@ const CreatePost: React.FC = ({ onCreatePost }) => { className="mb-4" > - {translate('::App.Platform.Intranet.SocialWall.CreatePost.Location')} + + {translate('::App.Platform.Intranet.SocialWall.CreatePost.Location')} + setLocation(null)} className="text-sm text-red-600 hover:text-red-700 font-medium" - title={translate('::App.Platform.Intranet.SocialWall.CreatePost.RemoveLocationTitle')} + title={translate( + '::App.Platform.Intranet.SocialWall.CreatePost.RemoveLocationTitle', + )} > {translate('::Cancel')} @@ -284,7 +292,9 @@ const CreatePost: React.FC = ({ onCreatePost }) => { className="mb-4" > - {translate('::App.Platform.Intranet.SocialWall.CreatePost.Poll')} + + {translate('::App.Platform.Intranet.SocialWall.CreatePost.Poll')} + { @@ -300,7 +310,9 @@ const CreatePost: React.FC = ({ onCreatePost }) => { type="text" value={pollQuestion} onChange={(e) => setPollQuestion(e.target.value)} - placeholder={translate('::App.Platform.Intranet.SocialWall.CreatePost.PollQuestionPlaceholder')} + placeholder={translate( + '::App.Platform.Intranet.SocialWall.CreatePost.PollQuestionPlaceholder', + )} className="w-full px-4 py-2 mb-3 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500" /> @@ -310,7 +322,9 @@ const CreatePost: React.FC = ({ onCreatePost }) => { type="text" value={option} onChange={(e) => updatePollOption(index, e.target.value)} - placeholder={translate('::App.Platform.Intranet.SocialWall.CreatePost.PollOptionPlaceholder')} + placeholder={translate( + '::App.Platform.Intranet.SocialWall.CreatePost.PollOptionPlaceholder', + )} className="flex-1 px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500" /> {pollOptions.length > 2 && ( @@ -365,9 +379,13 @@ const CreatePost: React.FC = ({ onCreatePost }) => { 'p-2 rounded-full transition-colors', mediaType === 'media' ? 'bg-blue-100 text-blue-600 dark:bg-blue-900 dark:text-blue-400' - : 'text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700' + : 'text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700', )} - title={mediaType === 'media' ? translate('::App.Platform.Intranet.SocialWall.CreatePost.EditMediaTitle') : translate('::App.Platform.Intranet.SocialWall.CreatePost.AddMediaTitle')} + title={ + mediaType === 'media' + ? translate('::App.Platform.Intranet.SocialWall.CreatePost.EditMediaTitle') + : translate('::App.Platform.Intranet.SocialWall.CreatePost.AddMediaTitle') + } > {mediaType === 'media' && mediaItems.length > 0 && ( @@ -389,9 +407,13 @@ const CreatePost: React.FC = ({ onCreatePost }) => { 'p-2 rounded-full transition-colors', mediaType === 'poll' ? 'bg-blue-100 text-blue-600 dark:bg-blue-900 dark:text-blue-400' - : 'text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700' + : 'text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700', )} - title={mediaType === 'poll' ? translate('::App.Platform.Intranet.SocialWall.CreatePost.RemovePollTitle') : translate('::App.Platform.Intranet.SocialWall.CreatePost.AddPollTitle')} + title={ + mediaType === 'poll' + ? translate('::App.Platform.Intranet.SocialWall.CreatePost.RemovePollTitle') + : translate('::App.Platform.Intranet.SocialWall.CreatePost.AddPollTitle') + } > @@ -410,9 +432,13 @@ const CreatePost: React.FC = ({ onCreatePost }) => { 'p-2 rounded-full transition-colors', location ? 'bg-blue-100 text-blue-600 dark:bg-blue-900 dark:text-blue-400' - : 'text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700' + : 'text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700', )} - title={location ? translate('::App.Platform.Intranet.SocialWall.CreatePost.EditLocationTitle') : translate('::App.Platform.Intranet.SocialWall.CreatePost.AddLocationTitle')} + title={ + location + ? translate('::App.Platform.Intranet.SocialWall.CreatePost.EditLocationTitle') + : translate('::App.Platform.Intranet.SocialWall.CreatePost.AddLocationTitle') + } > @@ -420,10 +446,7 @@ const CreatePost: React.FC = ({ onCreatePost }) => { {/* Emoji Picker */} {showEmojiPicker && ( - + )} @@ -453,10 +476,7 @@ const CreatePost: React.FC = ({ onCreatePost }) => { {/* Location Picker Modal */} {showLocationPicker && ( - setShowLocationPicker(false)} - /> + setShowLocationPicker(false)} /> )} diff --git a/ui/src/views/intranet/SocialWall/MediaManager.tsx b/ui/src/views/intranet/SocialWall/MediaManager.tsx index a2bdf03..3a20b94 100644 --- a/ui/src/views/intranet/SocialWall/MediaManager.tsx +++ b/ui/src/views/intranet/SocialWall/MediaManager.tsx @@ -21,14 +21,26 @@ const MediaManager: React.FC = ({ media, onChange, onClose }) const files = e.target.files if (!files) return - const newMedia: SocialMediaDto[] = Array.from(files).map((file) => ({ - id: Math.random().toString(36).substr(2, 9), - type: file.type.startsWith('video/') ? 'video' : 'image', - urls: [URL.createObjectURL(file)], - file - })) + const fileArray = Array.from(files) + const readers = fileArray.map( + (file) => + new Promise((resolve) => { + const reader = new FileReader() + reader.onload = () => { + resolve({ + id: Math.random().toString(36).substr(2, 9), + type: file.type.startsWith('video/') ? 'video' : 'image', + urls: [reader.result as string], + }) + } + reader.readAsDataURL(file) + }), + ) + + Promise.all(readers).then((newMedia) => { + onChange([...media, ...newMedia]) + }) - onChange([...media, ...newMedia]) e.target.value = '' } diff --git a/ui/src/views/intranet/SocialWall/PostItem.tsx b/ui/src/views/intranet/SocialWall/PostItem.tsx index 7975f2c..b2f0659 100644 --- a/ui/src/views/intranet/SocialWall/PostItem.tsx +++ b/ui/src/views/intranet/SocialWall/PostItem.tsx @@ -7,7 +7,6 @@ import relativeTime from 'dayjs/plugin/relativeTime' import 'dayjs/locale/tr' import { FaHeart, FaRegHeart, FaRegCommentAlt, FaTrash, FaPaperPlane } from 'react-icons/fa' import MediaLightbox from './MediaLightbox' -import LocationMap from './LocationMap' import UserProfileCard from './UserProfileCard' import { SocialPostDto } from '@/proxy/intranet/models' import { useStoreState } from '@/store/store' @@ -369,16 +368,16 @@ const PostItem: React.FC = ({ post, onLike, onComment, onDelete, {hoveredCommentAuthor === comment.id && ( @@ -388,7 +387,7 @@ const PostItem: React.FC = ({ post, onLike, onComment, onDelete, - {comment.creator.fullName} + {comment.user.fullName} {comment.content} diff --git a/ui/src/views/intranet/SocialWall/index.tsx b/ui/src/views/intranet/SocialWall/index.tsx index d86fabb..9e08407 100644 --- a/ui/src/views/intranet/SocialWall/index.tsx +++ b/ui/src/views/intranet/SocialWall/index.tsx @@ -1,31 +1,29 @@ -import React, { useState } from 'react' +import React, { useState, useEffect } from 'react' import { useLocalization } from '@/utils/hooks/useLocalization' import { AnimatePresence } from 'framer-motion' import PostItem from './PostItem' import CreatePost from './CreatePost' import { SocialMediaDto, SocialPostDto } from '@/proxy/intranet/models' import { useStoreState } from '@/store/store' -import { IdentityUserDto } from '@/proxy/admin/models' +import { intranetService } from '@/services/intranet.service' -const SocialWall: React.FC<{ posts: SocialPostDto[] }> = ({ posts }) => { - // const [posts, setPosts] = useState(mockSocialPosts) +const SocialWall: React.FC<{ posts: SocialPostDto[]; onPostsChange?: (posts: SocialPostDto[]) => void }> = ({ posts: initialPosts, onPostsChange }) => { + const [posts, setPosts] = useState(initialPosts) + + useEffect(() => { + setPosts(initialPosts) + }, [initialPosts]) const [filter, setFilter] = useState<'all' | 'mine'>('all') const { translate } = useLocalization() - // Ali Öztürk'ü "Siz" kullanıcısı olarak kullan const { user } = useStoreState((state) => state.auth) - const currentUserAuthor: IdentityUserDto = { - id: user?.id || '0', - userName: user.userName || 'unknown', - name: user?.name || 'Siz', - email: user?.email || '', - isActive: true, - emailConfirmed: true, - phoneNumberConfirmed: true, - lockoutEnabled: false, + + const updatePosts = (updated: SocialPostDto[]) => { + setPosts(updated) + onPostsChange?.(updated) } - const handleCreatePost = (postData: { + const handleCreatePost = async (postData: { content: string location?: string media?: { @@ -37,128 +35,105 @@ const SocialWall: React.FC<{ posts: SocialPostDto[] }> = ({ posts }) => { } } }) => { - let mediaForPost = undefined + let mediaInput: { type: 'image' | 'video' | 'poll'; urls?: string[]; pollQuestion?: string; pollOptions?: { text: string }[] } | undefined if (postData.media) { if (postData.media.type === 'mixed' && postData.media.mediaItems) { - // Convert MediaItems to post format const images = postData.media.mediaItems.filter((m) => m.type === 'image') const videos = postData.media.mediaItems.filter((m) => m.type === 'video') if (images.length > 0 && videos.length === 0) { - mediaForPost = { - type: 'image' as const, - urls: images.map((i) => i.urls?.[0]).filter((url) => url !== undefined) as string[], + mediaInput = { + type: 'image', + urls: images.map((i) => i.urls?.[0]).filter(Boolean) as string[], } } else if (videos.length > 0 && images.length === 0) { - mediaForPost = { - type: 'video' as const, - urls: videos[0].urls || [], - } + mediaInput = { type: 'video', urls: videos[0].urls || [] } } else if (images.length > 0 || videos.length > 0) { - // Mixed media - use first image for now - mediaForPost = { - type: 'image' as const, - urls: images.map((i) => i.urls?.[0]).filter((url) => url !== undefined) as string[], + mediaInput = { + type: 'image', + urls: images.map((i) => i.urls?.[0]).filter(Boolean) as string[], } } } else if (postData.media.type === 'poll' && postData.media.poll) { - mediaForPost = { - type: 'poll' as const, + mediaInput = { + type: 'poll', pollQuestion: postData.media.poll.question, - pollOptions: postData.media.poll.options.map((opt, index) => ({ - id: `opt-${index}`, - text: opt.text, - votes: 0, - })), - pollTotalVotes: 0, - pollEndsAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), + pollOptions: postData.media.poll.options, } } } - const newPost: SocialPostDto = { - id: Date.now().toString(), - user: currentUserAuthor, - content: postData.content, - creationTime: new Date(), - media: mediaForPost, - locationJson: postData.location, - likeCount: 0, - isLiked: false, - likeUsers: [], - comments: [], - isOwnPost: true, + try { + const response = await intranetService.createSocialPost({ + content: postData.content, + locationJson: postData.location, + media: mediaInput, + }) + updatePosts([response.data, ...posts]) + } catch { + // error handled by apiService } - - // setPosts([newPost, ...posts]) } - const handleLike = (postId: string) => { - // setPosts( - // posts.map((post) => { - // if (post.id === postId) { - // return { - // ...post, - // likeCount: post.isLiked ? post.likeCount - 1 : post.likeCount + 1, - // isLiked: !post.isLiked - // } - // } - // return post - // }) - // ) + const handleLike = async (postId: string) => { + try { + const response = await intranetService.likeSocialPost(postId) + updatePosts(posts.map((p) => (p.id === postId ? response.data : p))) + } catch { + // error handled by apiService + } } - const handleComment = (postId: string, content: string) => { - // setPosts( - // posts.map((post) => { - // if (post.id === postId) { - // const commentAuthor = currentUserAuthor - // const newComment = { - // id: Date.now().toString(), - // creator: commentAuthor, - // content, - // creationTime: new Date() - // } - // return { - // ...post, - // comments: [...post.comments, newComment] - // } - // } - // return post - // }) - // ) + const handleComment = async (postId: string, content: string) => { + try { + const response = await intranetService.commentSocialPost(postId, content) + updatePosts( + posts.map((p) => + p.id === postId ? { ...p, comments: [...(p.comments || []), response.data] } : p, + ), + ) + } catch { + // error handled by apiService + } } - const handleDelete = (postId: string) => { + const handleDelete = async (postId: string) => { if (window.confirm(translate('::App.Platform.Intranet.SocialWall.DeleteConfirm'))) { - // setPosts(posts.filter((post) => post.id !== postId)) + try { + await intranetService.deleteSocialPost(postId) + updatePosts(posts.filter((p) => p.id !== postId)) + } catch { + // error handled by apiService + } } } - const handleVote = (postId: string, optionId: string) => { - // setPosts( - // posts.map((post) => { - // if (post.id === postId && post.media?.type === 'poll' && post.media.pollOptions) { - // // If user already voted, don't allow voting again - // if (post.media.pollUserVoteId) { - // return post - // } - // return { - // ...post, - // media: { - // ...post.media, - // pollOptions: post.media.pollOptions.map((opt) => - // opt.id === optionId ? { ...opt, votes: opt.votes + 1 } : opt - // ), - // pollTotalVotes: (post.media.pollTotalVotes || 0) + 1, - // pollUserVoteId: optionId - // } - // } - // } - // return post - // }) - // ) + const handleVote = async (postId: string, optionId: string) => { + try { + await intranetService.voteSocialPoll(postId, optionId) + updatePosts( + posts.map((p) => { + if (p.id === postId && p.media?.type === 'poll' && p.media.pollOptions) { + if (p.media.pollUserVoteId) return p + return { + ...p, + media: { + ...p.media, + pollOptions: p.media.pollOptions.map((opt) => + opt.id === optionId ? { ...opt, votes: opt.votes + 1 } : opt, + ), + pollTotalVotes: (p.media.pollTotalVotes || 0) + 1, + pollUserVoteId: optionId, + }, + } + } + return p + }), + ) + } catch { + // error handled by apiService + } } const filteredPosts = filter === 'mine' ? posts.filter((post) => post.isOwnPost) : posts diff --git a/ui/src/views/intranet/widgets/AnnouncementModal.tsx b/ui/src/views/intranet/widgets/AnnouncementModal.tsx index a48e0b9..a1bcbac 100644 --- a/ui/src/views/intranet/widgets/AnnouncementModal.tsx +++ b/ui/src/views/intranet/widgets/AnnouncementModal.tsx @@ -1,4 +1,4 @@ -import React from 'react' +import React, { useEffect } from 'react' import { useLocalization } from '@/utils/hooks/useLocalization' import { motion } from 'framer-motion' import { FaTimes, FaEye, FaClipboard } from 'react-icons/fa' @@ -7,18 +7,21 @@ 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' interface AnnouncementModalProps { announcement: AnnouncementDto onClose: () => void } -const AnnouncementModal: React.FC = ({ - announcement, - onClose, -}) => { +const AnnouncementModal: React.FC = ({ announcement, onClose }) => { const { translate } = useLocalization() const currentLocale = useLocale() + + useEffect(() => { + intranetService.incrementAnnouncementViewCount(announcement.id) + }, [announcement.id]) + const getCategoryColor = (category: string) => { const colors: Record = { general: 'bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300', @@ -111,21 +114,42 @@ const AnnouncementModal: React.FC = ({ {/* Content */} {/* Images if exist */} - {announcement.imageUrl && (() => { - const images = announcement.imageUrl.split('|').filter(Boolean) - return images.length > 0 ? ( - 1 ? 'grid grid-cols-2 gap-2' : ''}`}> - {images.map((img, idx) => ( - 1 ? idx + 1 : ''}`.trim()} - className="w-full rounded-lg object-cover" - /> - ))} - - ) : null - })()} + {announcement.imageUrl && + (() => { + const images = announcement.imageUrl.split('|').filter(Boolean) + if (images.length === 0) return null + const getGridClass = (count: number) => { + if (count === 1) return '' + if (count === 2) return 'grid grid-cols-2 gap-2' + if (count === 3) return 'grid grid-cols-3 gap-2' + return 'grid grid-cols-2 gap-2' + } + const getImgClass = (count: number, idx: number) => { + if (count === 1) return 'w-full rounded-lg object-cover max-h-96' + if (count === 3 && idx === 0) return 'col-span-3 w-full rounded-lg object-cover max-h-64' + if (count >= 4 && idx === 0) return 'col-span-2 w-full rounded-lg object-cover max-h-64' + return 'w-full rounded-lg object-cover h-40' + } + return ( + + {images.map((img, idx) => ( + 1 ? idx + 1 : ''}`.trim()} + className={getImgClass(images.length, idx)} + /> + ))} + + ) + })()} {/* Full Content */} diff --git a/ui/src/views/intranet/widgets/Surveys.tsx b/ui/src/views/intranet/widgets/Surveys.tsx index 43eb4c4..78e16c5 100644 --- a/ui/src/views/intranet/widgets/Surveys.tsx +++ b/ui/src/views/intranet/widgets/Surveys.tsx @@ -26,26 +26,48 @@ const Surveys: React.FC = ({ surveys, onTakeSurvey }) => { - + {surveys?.map((survey, index) => { const daysLeft = dayjs(survey.deadline).diff(dayjs(), 'day') const urgency = daysLeft <= 3 ? 'urgent' : daysLeft <= 7 ? 'warning' : 'normal' + const isCompleted = !!survey.myResponse return ( onTakeSurvey(survey)} - className="group relative p-5 rounded-xl bg-white dark:bg-gray-750 border border-gray-200 dark:border-gray-600 hover:border-purple-300 dark:hover:border-purple-500 cursor-pointer transition-all duration-300 hover:shadow-lg hover:-translate-y-1" + className={`group relative p-5 rounded-xl border cursor-pointer transition-all duration-300 hover:shadow-lg hover:-translate-y-1 ${ + isCompleted + ? 'bg-green-50 dark:bg-green-900/20 border-green-300 dark:border-green-700 hover:border-green-400 dark:hover:border-green-500' + : 'bg-white dark:bg-gray-750 border-gray-200 dark:border-gray-600 hover:border-purple-300 dark:hover:border-purple-500' + }`} > {/* Background gradient on hover */} - + {/* Survey Title */} - - {survey.title} - + + {isCompleted && ( + + + + + + )} + + {survey.title} + + = ({ surveys, onTakeSurvey }) => { {/* Action Button */} - - {translate('::App.Platform.Intranet.Widgets.ActiveSurveys.FillSurvey')} + + {isCompleted + ? translate('::App.Platform.Intranet.Widgets.ActiveSurveys.ViewResponses') + : translate('::App.Platform.Intranet.Widgets.ActiveSurveys.FillSurvey')}
{comment.content}