SocialWall

This commit is contained in:
Sedat ÖZTÜRK 2026-05-06 16:47:18 +03:00
parent 3219265c12
commit bc192a584b
21 changed files with 706 additions and 241 deletions

View file

@ -0,0 +1,37 @@
using System.Collections.Generic;
namespace Sozsoft.Platform.Intranet;
public class CreateSocialPostInput
{
public string Content { get; set; } = string.Empty;
/// <summary>
/// JSON string containing location data (name, address, lat, lng, placeId).
/// </summary>
public string? LocationJson { get; set; }
public CreateSocialPostMediaInput? Media { get; set; }
}
public class CreateSocialPostMediaInput
{
/// <summary>
/// "image", "video", or "poll"
/// </summary>
public string Type { get; set; } = string.Empty;
/// <summary>
/// URLs for image/video type media.
/// </summary>
public string[]? Urls { get; set; }
// Poll fields
public string? PollQuestion { get; set; }
public List<CreateSocialPollOptionInput>? PollOptions { get; set; }
}
public class CreateSocialPollOptionInput
{
public string Text { get; set; } = string.Empty;
}

View file

@ -6,5 +6,11 @@ namespace Sozsoft.Platform.Intranet;
public interface IIntranetAppService : IApplicationService
{
Task<IntranetDashboardDto> GetIntranetDashboardAsync();
Task UpdateSurveyResponseAsync(SubmitSurveyInput input);
Task CreateSurveyResponseAsync(SubmitSurveyInput input);
Task<SocialPostDto> CreateSocialPostAsync(CreateSocialPostInput input);
Task DeleteSocialPostAsync(System.Guid id);
Task<SocialPostDto> LikeSocialPostAsync(System.Guid id);
Task<SocialCommentDto> CommentSocialPostAsync(System.Guid id, string content);
Task VoteSocialPollAsync(System.Guid postId, System.Guid optionId);
Task IncrementAnnouncementViewCountAsync(System.Guid id);
}

View file

@ -36,6 +36,10 @@ public class IntranetAppService : PlatformAppService, IIntranetAppService
private readonly IRepository<SurveyResponse, Guid> _surveyResponseRepository;
private readonly IRepository<SurveyAnswer, Guid> _surveyAnswerRepository;
private readonly IRepository<SocialPost, Guid> _socialPostRepository;
private readonly IRepository<SocialComment, Guid> _socialCommentRepository;
private readonly IRepository<SocialLike, Guid> _socialLikeRepository;
private readonly IRepository<SocialMedia, Guid> _socialMediaRepository;
private readonly IRepository<SocialPollOption, Guid> _socialPollOptionRepository;
public IntranetAppService(
ICurrentTenant currentTenant,
@ -50,7 +54,11 @@ public class IntranetAppService : PlatformAppService, IIntranetAppService
IRepository<Survey, Guid> surveyRepository,
IRepository<SurveyResponse, Guid> surveyResponseRepository,
IRepository<SurveyAnswer, Guid> surveyAnswerRepository,
IRepository<SocialPost, Guid> socialPostRepository
IRepository<SocialPost, Guid> socialPostRepository,
IRepository<SocialComment, Guid> socialCommentRepository,
IRepository<SocialLike, Guid> socialLikeRepository,
IRepository<SocialMedia, Guid> socialMediaRepository,
IRepository<SocialPollOption, Guid> 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<SocialPost>, List<SocialPostDto>>(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<SocialPostDto> 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<SocialPollOption>, List<SocialPollOptionDto>>(opts);
}
}
public async Task<List<FileItemDto>> GetIntranetDocumentsAsync(string folderPath)
{
var items = new List<FileItemDto>();
@ -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<SocialPostDto> 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<System.Text.Json.JsonElement>(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<SocialPost, SocialPostDto>(savedPost!);
dto.IsOwnPost = true;
if (CurrentUser.Id.HasValue)
{
var user = await _identityUserRepository.FindAsync(CurrentUser.Id.Value);
if (user != null)
dto.User = ObjectMapper.Map<IdentityUser, UserInfoViewModel>(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<SocialPostDto> 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<SocialPost, SocialPostDto>(updated!);
// Resolve user info
var userIds = new List<Guid?> { 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<IdentityUser, UserInfoViewModel>(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<SocialCommentDto> 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<SocialComment, SocialCommentDto>(comment);
if (CurrentUser.Id.HasValue)
{
var user = await _identityUserRepository.FindAsync(CurrentUser.Id.Value);
if (user != null)
dto.User = ObjectMapper.Map<IdentityUser, UserInfoViewModel>(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);
}
}

View file

@ -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",

View file

@ -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),

View file

@ -22,6 +22,11 @@ public class SocialPost : FullAuditedEntity<Guid>, IMultiTenant
public SocialMedia Media { get; set; }
public ICollection<SocialComment> Comments { get; set; }
public ICollection<SocialLike> Likes { get; set; }
public SocialPost(Guid id)
{
Id = id;
}
}
public class SocialLocation : FullAuditedEntity<Guid>, IMultiTenant
@ -36,6 +41,11 @@ public class SocialLocation : FullAuditedEntity<Guid>, 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<Guid>, IMultiTenant
@ -55,6 +65,11 @@ public class SocialMedia : FullAuditedEntity<Guid>, IMultiTenant
public string? PollUserVoteId { get; set; }
public ICollection<SocialPollOption> PollOptions { get; set; }
public SocialMedia(Guid id)
{
Id = id;
}
}
public class SocialPollOption : FullAuditedEntity<Guid>, IMultiTenant
@ -66,6 +81,13 @@ public class SocialPollOption : FullAuditedEntity<Guid>, IMultiTenant
public string Text { get; set; }
public int Votes { get; set; }
public SocialPollOption(Guid id)
{
Id = id;
}
protected SocialPollOption() { }
}
public class SocialComment : FullAuditedEntity<Guid>, IMultiTenant
@ -78,6 +100,13 @@ public class SocialComment : FullAuditedEntity<Guid>, IMultiTenant
public Guid? UserId { get; set; }
public string Content { get; set; }
public SocialComment(Guid id)
{
Id = id;
}
protected SocialComment() { }
}
public class SocialLike : FullAuditedEntity<Guid>, IMultiTenant
@ -88,4 +117,9 @@ public class SocialLike : FullAuditedEntity<Guid>, IMultiTenant
public SocialPost SocialPost { get; set; }
public Guid? UserId { get; set; }
public SocialLike(Guid id)
{
Id = id;
}
}

View file

@ -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);

View file

@ -13,7 +13,7 @@ using Volo.Abp.EntityFrameworkCore;
namespace Sozsoft.Platform.Migrations
{
[DbContext(typeof(PlatformDbContext))]
[Migration("20260506093149_Initial")]
[Migration("20260506113136_Initial")]
partial class Initial
{
/// <inheritdoc />
@ -4085,8 +4085,7 @@ namespace Sozsoft.Platform.Migrations
.HasColumnType("nvarchar(64)");
b.PrimitiveCollection<string>("Urls")
.HasMaxLength(2048)
.HasColumnType("nvarchar(2048)");
.HasColumnType("nvarchar(max)");
b.HasKey("Id");

View file

@ -2040,7 +2040,7 @@ namespace Sozsoft.Platform.Migrations
TenantId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
SocialPostId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
Type = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: false),
Urls = table.Column<string>(type: "nvarchar(2048)", maxLength: 2048, nullable: true),
Urls = table.Column<string>(type: "nvarchar(max)", nullable: true),
PollQuestion = table.Column<string>(type: "nvarchar(512)", maxLength: 512, nullable: true),
PollTotalVotes = table.Column<int>(type: "int", nullable: true),
PollEndsAt = table.Column<DateTime>(type: "datetime2", nullable: true),

View file

@ -4082,8 +4082,7 @@ namespace Sozsoft.Platform.Migrations
.HasColumnType("nvarchar(64)");
b.PrimitiveCollection<string>("Urls")
.HasMaxLength(2048)
.HasColumnType("nvarchar(2048)");
.HasColumnType("nvarchar(max)");
b.HasKey("Id");

View file

@ -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": [

View file

@ -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

View file

@ -95,7 +95,7 @@ export interface SurveyDto {
// Sosyal Duvar - Comment Interface
export interface SocialCommentDto {
id: string
creator: UserInfoViewModel
user: UserInfoViewModel
content: string
creationTime: Date
}

View file

@ -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<Config>,
@ -21,11 +32,69 @@ export class IntranetService {
apiService.fetchData<void>(
{
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<Config>) =>
apiService.fetchData<SocialPostDto, CreateSocialPostInput>(
{
method: 'POST',
url: '/api/app/intranet/social-post',
data: input,
},
{ apiName: this.apiName, ...config },
)
deleteSocialPost = (id: string, config?: Partial<Config>) =>
apiService.fetchData<void>(
{
method: 'DELETE',
url: `/api/app/intranet/${id}/social-post`,
},
{ apiName: this.apiName, ...config },
)
likeSocialPost = (id: string, config?: Partial<Config>) =>
apiService.fetchData<SocialPostDto>(
{
method: 'POST',
url: `/api/app/intranet/like-social-post`,
params: { id },
},
{ apiName: this.apiName, ...config },
)
commentSocialPost = (id: string, content: string, config?: Partial<Config>) =>
apiService.fetchData<SocialCommentDto>(
{
method: 'POST',
url: `/api/app/intranet/comment-social-post`,
params: { id, content },
},
{ apiName: this.apiName, ...config },
)
voteSocialPoll = (postId: string, optionId: string, config?: Partial<Config>) =>
apiService.fetchData<void>(
{
method: 'POST',
url: `/api/app/intranet/vote-social-poll`,
params: { postId, optionId },
},
{ apiName: this.apiName, ...config },
)
incrementAnnouncementViewCount = (id: string, config?: Partial<Config>) =>
apiService.fetchData<void>(
{
method: 'POST',
url: `/api/app/intranet/${id}/announcement-view`,
},
{ apiName: this.apiName, ...config },
)
}
export const intranetService = new IntranetService()

View file

@ -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,

View file

@ -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<CreatePostProps> = ({ onCreatePost }) => {
const { translate } = useLocalization();
const { translate } = useLocalization()
const [content, setContent] = useState('')
const [mediaType, setMediaType] = useState<'media' | 'poll' | null>(null)
const [mediaItems, setMediaItems] = useState<SocialMediaDto[]>([])
@ -58,22 +52,26 @@ const CreatePost: React.FC<CreatePostProps> = ({ 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<CreatePostProps> = ({ 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<CreatePostProps> = ({ onCreatePost }) => {
>
<div className="flex items-center justify-between mb-2">
<h4 className="text-sm font-medium text-gray-700 dark:text-gray-300">
{translate('::App.Platform.Intranet.SocialWall.CreatePost.MediaTitle')} ({mediaItems.length})
{translate('::App.Platform.Intranet.SocialWall.CreatePost.MediaTitle')} (
{mediaItems.length})
</h4>
<button
type="button"
@ -195,7 +194,9 @@ const CreatePost: React.FC<CreatePostProps> = ({ 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')}
</button>
@ -211,7 +212,10 @@ const CreatePost: React.FC<CreatePostProps> = ({ onCreatePost }) => {
/>
) : (
<div className="w-full h-24 bg-gray-900 rounded-lg relative">
<video src={item.urls?.[0]} className="w-full h-full object-cover rounded-lg" />
<video
src={item.urls?.[0]}
className="w-full h-full object-cover rounded-lg"
/>
<div className="absolute inset-0 flex items-center justify-center">
<div className="w-10 h-10 bg-black bg-opacity-50 rounded-full flex items-center justify-center">
<div className="w-0 h-0 border-t-8 border-t-transparent border-l-12 border-l-white border-b-8 border-b-transparent ml-1"></div>
@ -250,12 +254,16 @@ const CreatePost: React.FC<CreatePostProps> = ({ onCreatePost }) => {
className="mb-4"
>
<div className="flex items-center justify-between mb-2">
<h4 className="text-sm font-medium text-gray-700 dark:text-gray-300">{translate('::App.Platform.Intranet.SocialWall.CreatePost.Location')}</h4>
<h4 className="text-sm font-medium text-gray-700 dark:text-gray-300">
{translate('::App.Platform.Intranet.SocialWall.CreatePost.Location')}
</h4>
<button
type="button"
onClick={() => 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')}
</button>
@ -284,7 +292,9 @@ const CreatePost: React.FC<CreatePostProps> = ({ onCreatePost }) => {
className="mb-4"
>
<div className="flex items-center justify-between mb-2">
<h4 className="text-sm font-medium text-gray-700 dark:text-gray-300">{translate('::App.Platform.Intranet.SocialWall.CreatePost.Poll')}</h4>
<h4 className="text-sm font-medium text-gray-700 dark:text-gray-300">
{translate('::App.Platform.Intranet.SocialWall.CreatePost.Poll')}
</h4>
<button
type="button"
onClick={() => {
@ -300,7 +310,9 @@ const CreatePost: React.FC<CreatePostProps> = ({ 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"
/>
<div className="space-y-2">
@ -310,7 +322,9 @@ const CreatePost: React.FC<CreatePostProps> = ({ 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<CreatePostProps> = ({ 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')
}
>
<FaImages className="w-5 h-5" />
{mediaType === 'media' && mediaItems.length > 0 && (
@ -389,9 +407,13 @@ const CreatePost: React.FC<CreatePostProps> = ({ 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')
}
>
<FaChartBar className="w-5 h-5" />
</button>
@ -410,9 +432,13 @@ const CreatePost: React.FC<CreatePostProps> = ({ 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')
}
>
<FaMapMarkerAlt className="w-5 h-5" />
</button>
@ -420,10 +446,7 @@ const CreatePost: React.FC<CreatePostProps> = ({ onCreatePost }) => {
{/* Emoji Picker */}
{showEmojiPicker && (
<div ref={emojiPickerRef} className="absolute bottom-12 left-0 z-50">
<EmojiPicker
onEmojiClick={handleEmojiClick}
autoFocusSearch={false}
/>
<EmojiPicker onEmojiClick={handleEmojiClick} autoFocusSearch={false} />
</div>
)}
</div>
@ -453,10 +476,7 @@ const CreatePost: React.FC<CreatePostProps> = ({ onCreatePost }) => {
{/* Location Picker Modal */}
<AnimatePresence>
{showLocationPicker && (
<LocationPicker
onSelect={setLocation}
onClose={() => setShowLocationPicker(false)}
/>
<LocationPicker onSelect={setLocation} onClose={() => setShowLocationPicker(false)} />
)}
</AnimatePresence>
</div>

View file

@ -21,14 +21,26 @@ const MediaManager: React.FC<MediaManagerProps> = ({ 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<SocialMediaDto>((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 = ''
}

View file

@ -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<PostItemProps> = ({ post, onLike, onComment, onDelete,
<Avatar
size={32}
shape="circle"
src={AVATAR_URL(comment.creator.id, comment.creator.tenantId)}
src={AVATAR_URL(comment.user.id, comment.user.tenantId)}
/>
<AnimatePresence>
{hoveredCommentAuthor === comment.id && (
<UserProfileCard
user={{
id: comment.creator.id || '',
name: comment.creator.fullName || '',
title: comment.creator.jobPositions?.[0]?.name || '',
tenantId: comment.creator.tenantId || '',
id: comment.user.id || '',
name: comment.user.fullName || '',
title: comment.user.jobPositions?.[0]?.name || '',
tenantId: comment.user.tenantId || '',
}}
position="bottom"
/>
@ -388,7 +387,7 @@ const PostItem: React.FC<PostItemProps> = ({ post, onLike, onComment, onDelete,
<div className="flex-1">
<div className="bg-gray-100 dark:bg-gray-700 rounded-lg px-4 py-2">
<h4 className="font-semibold text-sm text-gray-900 dark:text-gray-100">
{comment.creator.fullName}
{comment.user.fullName}
</h4>
<p className="text-sm text-gray-800 dark:text-gray-200">{comment.content}</p>
</div>

View file

@ -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<SocialPost[]>(mockSocialPosts)
const SocialWall: React.FC<{ posts: SocialPostDto[]; onPostsChange?: (posts: SocialPostDto[]) => void }> = ({ posts: initialPosts, onPostsChange }) => {
const [posts, setPosts] = useState<SocialPostDto[]>(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

View file

@ -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<AnnouncementModalProps> = ({
announcement,
onClose,
}) => {
const AnnouncementModal: React.FC<AnnouncementModalProps> = ({ announcement, onClose }) => {
const { translate } = useLocalization()
const currentLocale = useLocale()
useEffect(() => {
intranetService.incrementAnnouncementViewCount(announcement.id)
}, [announcement.id])
const getCategoryColor = (category: string) => {
const colors: Record<string, string> = {
general: 'bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300',
@ -111,21 +114,42 @@ const AnnouncementModal: React.FC<AnnouncementModalProps> = ({
{/* Content */}
<div className="p-6 max-h-[60vh] overflow-y-auto">
{/* Images if exist */}
{announcement.imageUrl && (() => {
const images = announcement.imageUrl.split('|').filter(Boolean)
return images.length > 0 ? (
<div className={`mb-6 ${images.length > 1 ? 'grid grid-cols-2 gap-2' : ''}`}>
{images.map((img, idx) => (
<img
key={idx}
src={img.startsWith('data:') ? img : `data:image/jpeg;base64,${img}`}
alt={`${announcement.title} ${images.length > 1 ? idx + 1 : ''}`.trim()}
className="w-full rounded-lg object-cover"
/>
))}
</div>
) : 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 (
<div className={`mb-6 ${getGridClass(images.length)}`}>
{images.map((img, idx) => (
<img
key={idx}
src={
img.startsWith('data:') ||
img.startsWith('http://') ||
img.startsWith('https://') ||
img.startsWith('/')
? img
: `data:image/jpeg;base64,${img}`
}
alt={`${announcement.title} ${images.length > 1 ? idx + 1 : ''}`.trim()}
className={getImgClass(images.length, idx)}
/>
))}
</div>
)
})()}
{/* Full Content */}
<div className="prose prose-sm dark:prose-invert max-w-none">

View file

@ -26,26 +26,48 @@ const Surveys: React.FC<SurveysProps> = ({ surveys, onTakeSurvey }) => {
</h2>
</div>
<div className="p-6 space-y-4">
<div className="p-3 space-y-4">
{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 (
<div
key={survey.id}
onClick={() => 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 */}
<div className="absolute inset-0 bg-gradient-to-r from-purple-50 to-pink-50 dark:from-purple-900/10 dark:to-pink-900/10 rounded-xl opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
<div className={`absolute inset-0 rounded-xl opacity-0 group-hover:opacity-100 transition-opacity duration-300 ${
isCompleted
? 'bg-gradient-to-r from-green-50 to-emerald-50 dark:from-green-900/10 dark:to-emerald-900/10'
: 'bg-gradient-to-r from-purple-50 to-pink-50 dark:from-purple-900/10 dark:to-pink-900/10'
}`}></div>
<div className="relative">
{/* Survey Title */}
<div className="flex items-start justify-between mb-3">
<h4 className="text-base font-semibold text-gray-900 dark:text-white group-hover:text-purple-700 dark:group-hover:text-purple-300 transition-colors">
{survey.title}
</h4>
<div className="flex items-center gap-2 flex-1 min-w-0">
{isCompleted && (
<span className="flex-shrink-0 inline-flex items-center justify-center w-5 h-5 bg-green-500 rounded-full">
<svg className="w-3 h-3 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={3}>
<path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
</svg>
</span>
)}
<h4 className={`text-base font-semibold transition-colors ${
isCompleted
? 'text-green-800 dark:text-green-300 group-hover:text-green-700 dark:group-hover:text-green-200'
: 'text-gray-900 dark:text-white group-hover:text-purple-700 dark:group-hover:text-purple-300'
}`}>
{survey.title}
</h4>
</div>
<div
className={`px-2 py-1 text-center rounded-full text-xs font-medium ${
urgency === 'urgent'
@ -119,8 +141,14 @@ const Surveys: React.FC<SurveysProps> = ({ surveys, onTakeSurvey }) => {
</p>
{/* Action Button */}
<button className="w-full flex items-center justify-center gap-2 px-4 py-3 bg-gradient-to-r from-purple-600 to-pink-600 hover:from-purple-700 hover:to-pink-700 text-white text-sm font-medium rounded-lg transition-all duration-300 transform group-hover:scale-[1.02] shadow-sm hover:shadow-md">
{translate('::App.Platform.Intranet.Widgets.ActiveSurveys.FillSurvey')}
<button className={`w-full flex items-center justify-center gap-2 px-4 py-3 text-white text-sm font-medium rounded-lg transition-all duration-300 transform group-hover:scale-[1.02] shadow-sm hover:shadow-md ${
isCompleted
? 'bg-gradient-to-r from-green-500 to-emerald-500 hover:from-green-600 hover:to-emerald-600'
: 'bg-gradient-to-r from-purple-600 to-pink-600 hover:from-purple-700 hover:to-pink-700'
}`}>
{isCompleted
? translate('::App.Platform.Intranet.Widgets.ActiveSurveys.ViewResponses')
: translate('::App.Platform.Intranet.Widgets.ActiveSurveys.FillSurvey')}
<FaArrowRight className="w-3 h-3 transition-transform group-hover:translate-x-1" />
</button>
</div>