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 public interface IIntranetAppService : IApplicationService
{ {
Task<IntranetDashboardDto> GetIntranetDashboardAsync(); 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<SurveyResponse, Guid> _surveyResponseRepository;
private readonly IRepository<SurveyAnswer, Guid> _surveyAnswerRepository; private readonly IRepository<SurveyAnswer, Guid> _surveyAnswerRepository;
private readonly IRepository<SocialPost, Guid> _socialPostRepository; 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( public IntranetAppService(
ICurrentTenant currentTenant, ICurrentTenant currentTenant,
@ -50,7 +54,11 @@ public class IntranetAppService : PlatformAppService, IIntranetAppService
IRepository<Survey, Guid> surveyRepository, IRepository<Survey, Guid> surveyRepository,
IRepository<SurveyResponse, Guid> surveyResponseRepository, IRepository<SurveyResponse, Guid> surveyResponseRepository,
IRepository<SurveyAnswer, Guid> surveyAnswerRepository, 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; _currentTenant = currentTenant;
@ -65,6 +73,10 @@ public class IntranetAppService : PlatformAppService, IIntranetAppService
_surveyResponseRepository = surveyResponseRepository; _surveyResponseRepository = surveyResponseRepository;
_surveyAnswerRepository = surveyAnswerRepository; _surveyAnswerRepository = surveyAnswerRepository;
_socialPostRepository = socialPostRepository; _socialPostRepository = socialPostRepository;
_socialCommentRepository = socialCommentRepository;
_socialLikeRepository = socialLikeRepository;
_socialMediaRepository = socialMediaRepository;
_socialPollOptionRepository = socialPollOptionRepository;
} }
[UnitOfWork] [UnitOfWork]
@ -236,7 +248,7 @@ public class IntranetAppService : PlatformAppService, IIntranetAppService
var queryable = await _socialPostRepository var queryable = await _socialPostRepository
.WithDetailsAsync(e => e.Location, e => e.Media, e => e.Comments, e => e.Likes); .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); 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; 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) public async Task<List<FileItemDto>> GetIntranetDocumentsAsync(string folderPath)
{ {
var items = new List<FileItemDto>(); var items = new List<FileItemDto>();
@ -382,8 +426,7 @@ public class IntranetAppService : PlatformAppService, IIntranetAppService
}; };
} }
[HttpPost] public async Task CreateSurveyResponseAsync(SubmitSurveyInput input)
public async Task UpdateSurveyResponseAsync(SubmitSurveyInput input)
{ {
var survey = await _surveyRepository.GetAsync(input.SurveyId); var survey = await _surveyRepository.GetAsync(input.SurveyId);
@ -455,5 +498,221 @@ public class IntranetAppService : PlatformAppService, IIntranetAppService
survey.Responses++; survey.Responses++;
await _surveyRepository.UpdateAsync(survey); 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", "tr": "Anketi Doldur",
"en": "Fill Survey" "en": "Fill Survey"
}, },
{
"resourceName": "Platform",
"key": "App.Platform.Intranet.Widgets.ActiveSurveys.ViewResponses",
"tr": "Yanıtları Görüntüle",
"en": "View Responses"
},
{ {
"resourceName": "Platform", "resourceName": "Platform",
"key": "App.Platform.Intranet.Widgets.ActiveSurveys.NoActive", "key": "App.Platform.Intranet.Widgets.ActiveSurveys.NoActive",

View file

@ -3935,10 +3935,9 @@ public class ListFormSeeder_Administration : IDataSeedContributor, ITransientDep
CaptionName = "App.Listform.ListformField.SocialPostId", CaptionName = "App.Listform.ListformField.SocialPostId",
Width = 100, Width = 100,
ListOrderNo = 3, ListOrderNo = 3,
Visible = true, Visible = false,
IsActive = true, IsActive = true,
AllowSearch = true, AllowSearch = true,
LookupJson = LookupQueryValues.DefaultLookupQueryJson(nameof(TableNameEnum.SocialPost), "Id", "Content"),
ValidationRuleJson = DefaultValidationRuleRequiredJson, ValidationRuleJson = DefaultValidationRuleRequiredJson,
ColumnCustomizationJson = DefaultColumnCustomizationJson, ColumnCustomizationJson = DefaultColumnCustomizationJson,
PermissionJson = DefaultFieldPermissionJson(listForm.Name), PermissionJson = DefaultFieldPermissionJson(listForm.Name),

View file

@ -22,6 +22,11 @@ public class SocialPost : FullAuditedEntity<Guid>, IMultiTenant
public SocialMedia Media { get; set; } public SocialMedia Media { get; set; }
public ICollection<SocialComment> Comments { get; set; } public ICollection<SocialComment> Comments { get; set; }
public ICollection<SocialLike> Likes { get; set; } public ICollection<SocialLike> Likes { get; set; }
public SocialPost(Guid id)
{
Id = id;
}
} }
public class SocialLocation : FullAuditedEntity<Guid>, IMultiTenant public class SocialLocation : FullAuditedEntity<Guid>, IMultiTenant
@ -36,6 +41,11 @@ public class SocialLocation : FullAuditedEntity<Guid>, IMultiTenant
public double? Lat { get; set; } public double? Lat { get; set; }
public double? Lng { get; set; } public double? Lng { get; set; }
public string? PlaceId { get; set; } public string? PlaceId { get; set; }
public SocialLocation(Guid id)
{
Id = id;
}
} }
public class SocialMedia : FullAuditedEntity<Guid>, IMultiTenant public class SocialMedia : FullAuditedEntity<Guid>, IMultiTenant
@ -55,6 +65,11 @@ public class SocialMedia : FullAuditedEntity<Guid>, IMultiTenant
public string? PollUserVoteId { get; set; } public string? PollUserVoteId { get; set; }
public ICollection<SocialPollOption> PollOptions { get; set; } public ICollection<SocialPollOption> PollOptions { get; set; }
public SocialMedia(Guid id)
{
Id = id;
}
} }
public class SocialPollOption : FullAuditedEntity<Guid>, IMultiTenant public class SocialPollOption : FullAuditedEntity<Guid>, IMultiTenant
@ -66,6 +81,13 @@ public class SocialPollOption : FullAuditedEntity<Guid>, IMultiTenant
public string Text { get; set; } public string Text { get; set; }
public int Votes { get; set; } public int Votes { get; set; }
public SocialPollOption(Guid id)
{
Id = id;
}
protected SocialPollOption() { }
} }
public class SocialComment : FullAuditedEntity<Guid>, IMultiTenant public class SocialComment : FullAuditedEntity<Guid>, IMultiTenant
@ -78,6 +100,13 @@ public class SocialComment : FullAuditedEntity<Guid>, IMultiTenant
public Guid? UserId { get; set; } public Guid? UserId { get; set; }
public string Content { get; set; } public string Content { get; set; }
public SocialComment(Guid id)
{
Id = id;
}
protected SocialComment() { }
} }
public class SocialLike : FullAuditedEntity<Guid>, IMultiTenant public class SocialLike : FullAuditedEntity<Guid>, IMultiTenant
@ -88,4 +117,9 @@ public class SocialLike : FullAuditedEntity<Guid>, IMultiTenant
public SocialPost SocialPost { get; set; } public SocialPost SocialPost { get; set; }
public Guid? UserId { 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.ConfigureByConvention();
b.Property(x => x.Type).IsRequired().HasMaxLength(64); 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.PollQuestion).HasMaxLength(512);
b.Property(x => x.PollUserVoteId).HasMaxLength(128); b.Property(x => x.PollUserVoteId).HasMaxLength(128);

View file

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

View file

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

View file

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

View file

@ -1301,7 +1301,7 @@
"userName": "system@sozsoft.com", "userName": "system@sozsoft.com",
"publishDate": "12-10-2024", "publishDate": "12-10-2024",
"isPinned": true, "isPinned": true,
"viewCount": 156, "viewCount": 0,
"imageUrl": "https://images.unsplash.com/photo-1497366216548-37526070297c?w=800&q=80" "imageUrl": "https://images.unsplash.com/photo-1497366216548-37526070297c?w=800&q=80"
}, },
{ {
@ -1313,7 +1313,7 @@
"publishDate": "08-10-2024", "publishDate": "08-10-2024",
"expiryDate": "05-11-2024", "expiryDate": "05-11-2024",
"isPinned": true, "isPinned": true,
"viewCount": 89 "viewCount": 0
}, },
{ {
"title": "💻 Sistem Bakımı Duyurusu", "title": "💻 Sistem Bakımı Duyurusu",
@ -1323,7 +1323,7 @@
"userName": "system@sozsoft.com", "userName": "system@sozsoft.com",
"publishDate": "08-10-2024", "publishDate": "08-10-2024",
"isPinned": false, "isPinned": false,
"viewCount": 234 "viewCount": 0
}, },
{ {
"title": "🎓 React İleri Seviye Eğitimi", "title": "🎓 React İleri Seviye Eğitimi",
@ -1333,7 +1333,7 @@
"userName": "system@sozsoft.com", "userName": "system@sozsoft.com",
"publishDate": "09-10-2024", "publishDate": "09-10-2024",
"isPinned": false, "isPinned": false,
"viewCount": 67 "viewCount": 0
}, },
{ {
"title": "⚠️ Güvenlik Politikası Güncellemesi", "title": "⚠️ Güvenlik Politikası Güncellemesi",
@ -1343,7 +1343,7 @@
"userName": "system@sozsoft.com", "userName": "system@sozsoft.com",
"publishDate": "04-10-2024", "publishDate": "04-10-2024",
"isPinned": true, "isPinned": true,
"viewCount": 312 "viewCount": 0
} }
], ],
"Surveys": [ "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! 🚀", "content": "Yeni proje üzerinde çalışıyoruz! React ve TypeScript ile harika bir deneyim oluşturuyoruz. Ekip çalışması harika gidiyor! 🚀",
"userName": "system@sozsoft.com", "userName": "system@sozsoft.com",
"likeCount": 24, "likeCount": 0,
"isLiked": true, "isLiked": false,
"isOwnPost": false "isOwnPost": true
}, },
{ {
"content": "Bu hafta sprint planlamasını yaptık. Ekibimizle birlikte yeni özellikleri değerlendirdik. Heyecan verici bir hafta olacak!", "content": "Bu hafta sprint planlamasını yaptık. Ekibimizle birlikte yeni özellikleri değerlendirdik. Heyecan verici bir hafta olacak!",
"userName": "system@sozsoft.com", "userName": "system@sozsoft.com",
"likeCount": 18, "likeCount": 0,
"isLiked": false, "isLiked": false,
"isOwnPost": true "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! 🎨", "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", "userName": "system@sozsoft.com",
"likeCount": 42, "likeCount": 0,
"isLiked": true, "isLiked": false,
"isOwnPost": false "isOwnPost": true
}, },
{ {
"content": "CI/CD pipeline güncellememiz tamamlandı! Deployment süremiz %40 azaldı. Otomasyonun gücü 💪", "content": "CI/CD pipeline güncellememiz tamamlandı! Deployment süremiz %40 azaldı. Otomasyonun gücü 💪",
"userName": "system@sozsoft.com", "userName": "system@sozsoft.com",
"likeCount": 31, "likeCount": 0,
"isLiked": false, "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.", "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", "userName": "system@sozsoft.com",
"likeCount": 56, "likeCount": 0,
"isLiked": true, "isLiked": false,
"isOwnPost": false "isOwnPost": true
}, },
{ {
"content": "Bugün müşteri ile harika bir toplantı yaptık! Yeni projenin detaylarını konuştuk. 🎯", "content": "Bugün müşteri ile harika bir toplantı yaptık! Yeni projenin detaylarını konuştuk. 🎯",
"userName": "system@sozsoft.com", "userName": "system@sozsoft.com",
"likeCount": 18, "likeCount": 0,
"isLiked": false, "isLiked": false,
"isOwnPost": false "isOwnPost": true
} }
], ],
"SocialLocations": [ "SocialLocations": [
@ -1585,7 +1585,7 @@
"postContent": "Bu hafta sprint planlamasını yaptık. Ekibimizle birlikte yeni özellikleri değerlendirdik. Heyecan verici bir hafta olacak!", "postContent": "Bu hafta sprint planlamasını yaptık. Ekibimizle birlikte yeni özellikleri değerlendirdik. Heyecan verici bir hafta olacak!",
"type": "poll", "type": "poll",
"pollQuestion": "Hangi özelliği öncelikli olarak geliştirmeliyiz?", "pollQuestion": "Hangi özelliği öncelikli olarak geliştirmeliyiz?",
"pollTotalVotes": 40, "pollTotalVotes": 0,
"pollEndsAt": "2024-10-20T23:59:59", "pollEndsAt": "2024-10-20T23:59:59",
"pollUserVoteId": "p3" "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!", "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?", "pollQuestion": "Hangi özelliği öncelikli olarak geliştirmeliyiz?",
"Text": "Kullanıcı profilleri", "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!", "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?", "pollQuestion": "Hangi özelliği öncelikli olarak geliştirmeliyiz?",
"Text": "Bildirim sistemi", "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!", "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?", "pollQuestion": "Hangi özelliği öncelikli olarak geliştirmeliyiz?",
"Text": "Mesajlaşma", "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!", "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?", "pollQuestion": "Hangi özelliği öncelikli olarak geliştirmeliyiz?",
"Text": "Raporlama", "Text": "Raporlama",
"Votes": 5 "Votes": 0
} }
], ],
"SocialComments": [ "SocialComments": [

View file

@ -992,7 +992,7 @@ public class TenantDataSeeder : IDataSeedContributor, ITransientDependency
var user = await _repositoryUser.FindByNormalizedUserNameAsync(item.UserName); 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, UserId = user != null ? user.Id : null,
Content = item.Content, Content = item.Content,
@ -1013,7 +1013,7 @@ public class TenantDataSeeder : IDataSeedContributor, ITransientDependency
if (exists) if (exists)
continue; continue;
await _socialLocationRepository.InsertAsync(new SocialLocation await _socialLocationRepository.InsertAsync(new SocialLocation(Guid.NewGuid())
{ {
SocialPostId = post != null ? post.Id : Guid.Empty, SocialPostId = post != null ? post.Id : Guid.Empty,
Name = item.Name, Name = item.Name,
@ -1035,7 +1035,7 @@ public class TenantDataSeeder : IDataSeedContributor, ITransientDependency
if (exists) if (exists)
continue; continue;
await _socialMediaRepository.InsertAsync(new SocialMedia await _socialMediaRepository.InsertAsync(new SocialMedia(Guid.NewGuid())
{ {
SocialPostId = post != null ? post.Id : Guid.Empty, SocialPostId = post != null ? post.Id : Guid.Empty,
Type = item.Type, Type = item.Type,
@ -1061,7 +1061,7 @@ public class TenantDataSeeder : IDataSeedContributor, ITransientDependency
if (exists) if (exists)
continue; continue;
await _socialPollOptionRepository.InsertAsync(new SocialPollOption await _socialPollOptionRepository.InsertAsync(new SocialPollOption(Guid.NewGuid())
{ {
SocialMediaId = media != null ? media.Id : Guid.Empty, SocialMediaId = media != null ? media.Id : Guid.Empty,
Text = item.Text, Text = item.Text,
@ -1081,7 +1081,7 @@ public class TenantDataSeeder : IDataSeedContributor, ITransientDependency
continue; continue;
var user = await _repositoryUser.FindByNormalizedUserNameAsync(item.UserName); 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, UserId = user != null ? user.Id : null,
SocialPostId = post != null ? post.Id : Guid.Empty, SocialPostId = post != null ? post.Id : Guid.Empty,
@ -1101,7 +1101,7 @@ public class TenantDataSeeder : IDataSeedContributor, ITransientDependency
continue; continue;
var user = await _repositoryUser.FindByNormalizedUserNameAsync(item.UserName); 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, SocialPostId = post != null ? post.Id : Guid.Empty,
UserId = user != null ? user.Id : null UserId = user != null ? user.Id : null

View file

@ -95,7 +95,7 @@ export interface SurveyDto {
// Sosyal Duvar - Comment Interface // Sosyal Duvar - Comment Interface
export interface SocialCommentDto { export interface SocialCommentDto {
id: string id: string
creator: UserInfoViewModel user: UserInfoViewModel
content: string content: string
creationTime: Date 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' 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 { export class IntranetService {
apiName = 'Default' apiName = 'Default'
@ -13,7 +24,7 @@ export class IntranetService {
{ apiName: this.apiName, ...config }, { apiName: this.apiName, ...config },
) )
updateSurveyResponse = ( createSurveyResponse = (
surveyId: string, surveyId: string,
answers: { questionId: string; questionType: string; value: string }[], answers: { questionId: string; questionType: string; value: string }[],
config?: Partial<Config>, config?: Partial<Config>,
@ -21,11 +32,69 @@ export class IntranetService {
apiService.fetchData<void>( apiService.fetchData<void>(
{ {
method: 'POST', method: 'POST',
url: '/api/app/intranet/update-survey-response', url: '/api/app/intranet/survey-response',
data: { surveyId, answers }, data: { surveyId, answers },
}, },
{ apiName: this.apiName, ...config }, { 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() export const intranetService = new IntranetService()

View file

@ -83,7 +83,7 @@ const IntranetDashboard: React.FC = () => {
const handleSubmitSurvey = async (answers: SurveyAnswerDto[]) => { const handleSubmitSurvey = async (answers: SurveyAnswerDto[]) => {
if (!selectedSurvey) return if (!selectedSurvey) return
try { try {
await intranetService.updateSurveyResponse( await intranetService.createSurveyResponse(
selectedSurvey.id, selectedSurvey.id,
answers.map((a) => ({ answers.map((a) => ({
questionId: a.questionId, questionId: a.questionId,

View file

@ -2,16 +2,14 @@ import React, { useState, useRef } from 'react'
import { motion, AnimatePresence } from 'framer-motion' import { motion, AnimatePresence } from 'framer-motion'
import classNames from 'classnames' import classNames from 'classnames'
import EmojiPicker, { EmojiClickData } from 'emoji-picker-react' import EmojiPicker, { EmojiClickData } from 'emoji-picker-react'
import { import { FaChartBar, FaSmile, FaTimes, FaImages, FaMapMarkerAlt } from 'react-icons/fa'
FaChartBar,
FaSmile,
FaTimes,
FaImages,
FaMapMarkerAlt
} from 'react-icons/fa'
import MediaManager from './MediaManager' import MediaManager from './MediaManager'
import LocationPicker from './LocationPicker' import LocationPicker from './LocationPicker'
import { SocialMediaDto } from '@/proxy/intranet/models' 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 { interface CreatePostProps {
onCreatePost: (post: { onCreatePost: (post: {
@ -28,12 +26,8 @@ interface CreatePostProps {
}) => void }) => 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 CreatePost: React.FC<CreatePostProps> = ({ onCreatePost }) => {
const { translate } = useLocalization(); const { translate } = useLocalization()
const [content, setContent] = useState('') const [content, setContent] = useState('')
const [mediaType, setMediaType] = useState<'media' | 'poll' | null>(null) const [mediaType, setMediaType] = useState<'media' | 'poll' | null>(null)
const [mediaItems, setMediaItems] = useState<SocialMediaDto[]>([]) const [mediaItems, setMediaItems] = useState<SocialMediaDto[]>([])
@ -58,22 +52,26 @@ const CreatePost: React.FC<CreatePostProps> = ({ onCreatePost }) => {
if (mediaType === 'media' && mediaItems.length > 0) { if (mediaType === 'media' && mediaItems.length > 0) {
media = { media = {
type: 'mixed' as const, 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 = { media = {
type: 'poll' as const, type: 'poll' as const,
poll: { poll: {
question: pollQuestion, question: pollQuestion,
options: pollOptions.filter(o => o.trim()).map(text => ({ text })) options: pollOptions.filter((o) => o.trim()).map((text) => ({ text })),
} },
} }
} }
onCreatePost({ onCreatePost({
content, content,
media, media,
location: location || undefined location: location || undefined,
}) })
// Reset form // Reset form
@ -169,7 +167,7 @@ const CreatePost: React.FC<CreatePostProps> = ({ onCreatePost }) => {
placeholder={translate('::App.Platform.Intranet.SocialWall.CreatePost.Placeholder')} placeholder={translate('::App.Platform.Intranet.SocialWall.CreatePost.Placeholder')}
className={classNames( 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', '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} rows={isExpanded ? 4 : 1}
/> />
@ -187,7 +185,8 @@ const CreatePost: React.FC<CreatePostProps> = ({ onCreatePost }) => {
> >
<div className="flex items-center justify-between mb-2"> <div className="flex items-center justify-between mb-2">
<h4 className="text-sm font-medium text-gray-700 dark:text-gray-300"> <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> </h4>
<button <button
type="button" type="button"
@ -195,7 +194,9 @@ const CreatePost: React.FC<CreatePostProps> = ({ onCreatePost }) => {
clearMedia() clearMedia()
}} }}
className="text-sm text-red-600 hover:text-red-700 font-medium" 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')} {translate('::Cancel')}
</button> </button>
@ -211,7 +212,10 @@ const CreatePost: React.FC<CreatePostProps> = ({ onCreatePost }) => {
/> />
) : ( ) : (
<div className="w-full h-24 bg-gray-900 rounded-lg relative"> <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="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-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> <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" className="mb-4"
> >
<div className="flex items-center justify-between mb-2"> <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 <button
type="button" type="button"
onClick={() => setLocation(null)} onClick={() => setLocation(null)}
className="text-sm text-red-600 hover:text-red-700 font-medium" 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')} {translate('::Cancel')}
</button> </button>
@ -284,7 +292,9 @@ const CreatePost: React.FC<CreatePostProps> = ({ onCreatePost }) => {
className="mb-4" className="mb-4"
> >
<div className="flex items-center justify-between mb-2"> <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 <button
type="button" type="button"
onClick={() => { onClick={() => {
@ -300,7 +310,9 @@ const CreatePost: React.FC<CreatePostProps> = ({ onCreatePost }) => {
type="text" type="text"
value={pollQuestion} value={pollQuestion}
onChange={(e) => setPollQuestion(e.target.value)} 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" 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"> <div className="space-y-2">
@ -310,7 +322,9 @@ const CreatePost: React.FC<CreatePostProps> = ({ onCreatePost }) => {
type="text" type="text"
value={option} value={option}
onChange={(e) => updatePollOption(index, e.target.value)} 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" 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 && ( {pollOptions.length > 2 && (
@ -365,9 +379,13 @@ const CreatePost: React.FC<CreatePostProps> = ({ onCreatePost }) => {
'p-2 rounded-full transition-colors', 'p-2 rounded-full transition-colors',
mediaType === 'media' mediaType === 'media'
? 'bg-blue-100 text-blue-600 dark:bg-blue-900 dark:text-blue-400' ? '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" /> <FaImages className="w-5 h-5" />
{mediaType === 'media' && mediaItems.length > 0 && ( {mediaType === 'media' && mediaItems.length > 0 && (
@ -389,9 +407,13 @@ const CreatePost: React.FC<CreatePostProps> = ({ onCreatePost }) => {
'p-2 rounded-full transition-colors', 'p-2 rounded-full transition-colors',
mediaType === 'poll' mediaType === 'poll'
? 'bg-blue-100 text-blue-600 dark:bg-blue-900 dark:text-blue-400' ? '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" /> <FaChartBar className="w-5 h-5" />
</button> </button>
@ -410,9 +432,13 @@ const CreatePost: React.FC<CreatePostProps> = ({ onCreatePost }) => {
'p-2 rounded-full transition-colors', 'p-2 rounded-full transition-colors',
location location
? 'bg-blue-100 text-blue-600 dark:bg-blue-900 dark:text-blue-400' ? '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" /> <FaMapMarkerAlt className="w-5 h-5" />
</button> </button>
@ -420,10 +446,7 @@ const CreatePost: React.FC<CreatePostProps> = ({ onCreatePost }) => {
{/* Emoji Picker */} {/* Emoji Picker */}
{showEmojiPicker && ( {showEmojiPicker && (
<div ref={emojiPickerRef} className="absolute bottom-12 left-0 z-50"> <div ref={emojiPickerRef} className="absolute bottom-12 left-0 z-50">
<EmojiPicker <EmojiPicker onEmojiClick={handleEmojiClick} autoFocusSearch={false} />
onEmojiClick={handleEmojiClick}
autoFocusSearch={false}
/>
</div> </div>
)} )}
</div> </div>
@ -453,10 +476,7 @@ const CreatePost: React.FC<CreatePostProps> = ({ onCreatePost }) => {
{/* Location Picker Modal */} {/* Location Picker Modal */}
<AnimatePresence> <AnimatePresence>
{showLocationPicker && ( {showLocationPicker && (
<LocationPicker <LocationPicker onSelect={setLocation} onClose={() => setShowLocationPicker(false)} />
onSelect={setLocation}
onClose={() => setShowLocationPicker(false)}
/>
)} )}
</AnimatePresence> </AnimatePresence>
</div> </div>

View file

@ -21,14 +21,26 @@ const MediaManager: React.FC<MediaManagerProps> = ({ media, onChange, onClose })
const files = e.target.files const files = e.target.files
if (!files) return if (!files) return
const newMedia: SocialMediaDto[] = Array.from(files).map((file) => ({ const fileArray = Array.from(files)
id: Math.random().toString(36).substr(2, 9), const readers = fileArray.map(
type: file.type.startsWith('video/') ? 'video' : 'image', (file) =>
urls: [URL.createObjectURL(file)], new Promise<SocialMediaDto>((resolve) => {
file 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 = '' e.target.value = ''
} }

View file

@ -7,7 +7,6 @@ import relativeTime from 'dayjs/plugin/relativeTime'
import 'dayjs/locale/tr' import 'dayjs/locale/tr'
import { FaHeart, FaRegHeart, FaRegCommentAlt, FaTrash, FaPaperPlane } from 'react-icons/fa' import { FaHeart, FaRegHeart, FaRegCommentAlt, FaTrash, FaPaperPlane } from 'react-icons/fa'
import MediaLightbox from './MediaLightbox' import MediaLightbox from './MediaLightbox'
import LocationMap from './LocationMap'
import UserProfileCard from './UserProfileCard' import UserProfileCard from './UserProfileCard'
import { SocialPostDto } from '@/proxy/intranet/models' import { SocialPostDto } from '@/proxy/intranet/models'
import { useStoreState } from '@/store/store' import { useStoreState } from '@/store/store'
@ -369,16 +368,16 @@ const PostItem: React.FC<PostItemProps> = ({ post, onLike, onComment, onDelete,
<Avatar <Avatar
size={32} size={32}
shape="circle" shape="circle"
src={AVATAR_URL(comment.creator.id, comment.creator.tenantId)} src={AVATAR_URL(comment.user.id, comment.user.tenantId)}
/> />
<AnimatePresence> <AnimatePresence>
{hoveredCommentAuthor === comment.id && ( {hoveredCommentAuthor === comment.id && (
<UserProfileCard <UserProfileCard
user={{ user={{
id: comment.creator.id || '', id: comment.user.id || '',
name: comment.creator.fullName || '', name: comment.user.fullName || '',
title: comment.creator.jobPositions?.[0]?.name || '', title: comment.user.jobPositions?.[0]?.name || '',
tenantId: comment.creator.tenantId || '', tenantId: comment.user.tenantId || '',
}} }}
position="bottom" position="bottom"
/> />
@ -388,7 +387,7 @@ const PostItem: React.FC<PostItemProps> = ({ post, onLike, onComment, onDelete,
<div className="flex-1"> <div className="flex-1">
<div className="bg-gray-100 dark:bg-gray-700 rounded-lg px-4 py-2"> <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"> <h4 className="font-semibold text-sm text-gray-900 dark:text-gray-100">
{comment.creator.fullName} {comment.user.fullName}
</h4> </h4>
<p className="text-sm text-gray-800 dark:text-gray-200">{comment.content}</p> <p className="text-sm text-gray-800 dark:text-gray-200">{comment.content}</p>
</div> </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 { useLocalization } from '@/utils/hooks/useLocalization'
import { AnimatePresence } from 'framer-motion' import { AnimatePresence } from 'framer-motion'
import PostItem from './PostItem' import PostItem from './PostItem'
import CreatePost from './CreatePost' import CreatePost from './CreatePost'
import { SocialMediaDto, SocialPostDto } from '@/proxy/intranet/models' import { SocialMediaDto, SocialPostDto } from '@/proxy/intranet/models'
import { useStoreState } from '@/store/store' 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 SocialWall: React.FC<{ posts: SocialPostDto[]; onPostsChange?: (posts: SocialPostDto[]) => void }> = ({ posts: initialPosts, onPostsChange }) => {
// const [posts, setPosts] = useState<SocialPost[]>(mockSocialPosts) const [posts, setPosts] = useState<SocialPostDto[]>(initialPosts)
useEffect(() => {
setPosts(initialPosts)
}, [initialPosts])
const [filter, setFilter] = useState<'all' | 'mine'>('all') const [filter, setFilter] = useState<'all' | 'mine'>('all')
const { translate } = useLocalization() const { translate } = useLocalization()
// Ali Öztürk'ü "Siz" kullanıcısı olarak kullan
const { user } = useStoreState((state) => state.auth) const { user } = useStoreState((state) => state.auth)
const currentUserAuthor: IdentityUserDto = {
id: user?.id || '0', const updatePosts = (updated: SocialPostDto[]) => {
userName: user.userName || 'unknown', setPosts(updated)
name: user?.name || 'Siz', onPostsChange?.(updated)
email: user?.email || '',
isActive: true,
emailConfirmed: true,
phoneNumberConfirmed: true,
lockoutEnabled: false,
} }
const handleCreatePost = (postData: { const handleCreatePost = async (postData: {
content: string content: string
location?: string location?: string
media?: { 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) {
if (postData.media.type === 'mixed' && postData.media.mediaItems) { if (postData.media.type === 'mixed' && postData.media.mediaItems) {
// Convert MediaItems to post format
const images = postData.media.mediaItems.filter((m) => m.type === 'image') const images = postData.media.mediaItems.filter((m) => m.type === 'image')
const videos = postData.media.mediaItems.filter((m) => m.type === 'video') const videos = postData.media.mediaItems.filter((m) => m.type === 'video')
if (images.length > 0 && videos.length === 0) { if (images.length > 0 && videos.length === 0) {
mediaForPost = { mediaInput = {
type: 'image' as const, type: 'image',
urls: images.map((i) => i.urls?.[0]).filter((url) => url !== undefined) as string[], urls: images.map((i) => i.urls?.[0]).filter(Boolean) as string[],
} }
} else if (videos.length > 0 && images.length === 0) { } else if (videos.length > 0 && images.length === 0) {
mediaForPost = { mediaInput = { type: 'video', urls: videos[0].urls || [] }
type: 'video' as const,
urls: videos[0].urls || [],
}
} else if (images.length > 0 || videos.length > 0) { } else if (images.length > 0 || videos.length > 0) {
// Mixed media - use first image for now mediaInput = {
mediaForPost = { type: 'image',
type: 'image' as const, urls: images.map((i) => i.urls?.[0]).filter(Boolean) as string[],
urls: images.map((i) => i.urls?.[0]).filter((url) => url !== undefined) as string[],
} }
} }
} else if (postData.media.type === 'poll' && postData.media.poll) { } else if (postData.media.type === 'poll' && postData.media.poll) {
mediaForPost = { mediaInput = {
type: 'poll' as const, type: 'poll',
pollQuestion: postData.media.poll.question, pollQuestion: postData.media.poll.question,
pollOptions: postData.media.poll.options.map((opt, index) => ({ pollOptions: postData.media.poll.options,
id: `opt-${index}`,
text: opt.text,
votes: 0,
})),
pollTotalVotes: 0,
pollEndsAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
} }
} }
} }
const newPost: SocialPostDto = { try {
id: Date.now().toString(), const response = await intranetService.createSocialPost({
user: currentUserAuthor, content: postData.content,
content: postData.content, locationJson: postData.location,
creationTime: new Date(), media: mediaInput,
media: mediaForPost, })
locationJson: postData.location, updatePosts([response.data, ...posts])
likeCount: 0, } catch {
isLiked: false, // error handled by apiService
likeUsers: [],
comments: [],
isOwnPost: true,
} }
// setPosts([newPost, ...posts])
} }
const handleLike = (postId: string) => { const handleLike = async (postId: string) => {
// setPosts( try {
// posts.map((post) => { const response = await intranetService.likeSocialPost(postId)
// if (post.id === postId) { updatePosts(posts.map((p) => (p.id === postId ? response.data : p)))
// return { } catch {
// ...post, // error handled by apiService
// likeCount: post.isLiked ? post.likeCount - 1 : post.likeCount + 1, }
// isLiked: !post.isLiked
// }
// }
// return post
// })
// )
} }
const handleComment = (postId: string, content: string) => { const handleComment = async (postId: string, content: string) => {
// setPosts( try {
// posts.map((post) => { const response = await intranetService.commentSocialPost(postId, content)
// if (post.id === postId) { updatePosts(
// const commentAuthor = currentUserAuthor posts.map((p) =>
// const newComment = { p.id === postId ? { ...p, comments: [...(p.comments || []), response.data] } : p,
// id: Date.now().toString(), ),
// creator: commentAuthor, )
// content, } catch {
// creationTime: new Date() // error handled by apiService
// } }
// return {
// ...post,
// comments: [...post.comments, newComment]
// }
// }
// return post
// })
// )
} }
const handleDelete = (postId: string) => { const handleDelete = async (postId: string) => {
if (window.confirm(translate('::App.Platform.Intranet.SocialWall.DeleteConfirm'))) { 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) => { const handleVote = async (postId: string, optionId: string) => {
// setPosts( try {
// posts.map((post) => { await intranetService.voteSocialPoll(postId, optionId)
// if (post.id === postId && post.media?.type === 'poll' && post.media.pollOptions) { updatePosts(
// // If user already voted, don't allow voting again posts.map((p) => {
// if (post.media.pollUserVoteId) { if (p.id === postId && p.media?.type === 'poll' && p.media.pollOptions) {
// return post if (p.media.pollUserVoteId) return p
// } return {
// return { ...p,
// ...post, media: {
// media: { ...p.media,
// ...post.media, pollOptions: p.media.pollOptions.map((opt) =>
// pollOptions: post.media.pollOptions.map((opt) => opt.id === optionId ? { ...opt, votes: opt.votes + 1 } : opt,
// opt.id === optionId ? { ...opt, votes: opt.votes + 1 } : opt ),
// ), pollTotalVotes: (p.media.pollTotalVotes || 0) + 1,
// pollTotalVotes: (post.media.pollTotalVotes || 0) + 1, pollUserVoteId: optionId,
// pollUserVoteId: optionId },
// } }
// } }
// } return p
// return post }),
// }) )
// ) } catch {
// error handled by apiService
}
} }
const filteredPosts = filter === 'mine' ? posts.filter((post) => post.isOwnPost) : posts 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 { useLocalization } from '@/utils/hooks/useLocalization'
import { motion } from 'framer-motion' import { motion } from 'framer-motion'
import { FaTimes, FaEye, FaClipboard } from 'react-icons/fa' import { FaTimes, FaEye, FaClipboard } from 'react-icons/fa'
@ -7,18 +7,21 @@ import useLocale from '@/utils/hooks/useLocale'
import { currentLocalDate } from '@/utils/dateUtils' import { currentLocalDate } from '@/utils/dateUtils'
import Avatar from '@/components/ui/Avatar/Avatar' import Avatar from '@/components/ui/Avatar/Avatar'
import { AVATAR_URL } from '@/constants/app.constant' import { AVATAR_URL } from '@/constants/app.constant'
import { intranetService } from '@/services/intranet.service'
interface AnnouncementModalProps { interface AnnouncementModalProps {
announcement: AnnouncementDto announcement: AnnouncementDto
onClose: () => void onClose: () => void
} }
const AnnouncementModal: React.FC<AnnouncementModalProps> = ({ const AnnouncementModal: React.FC<AnnouncementModalProps> = ({ announcement, onClose }) => {
announcement,
onClose,
}) => {
const { translate } = useLocalization() const { translate } = useLocalization()
const currentLocale = useLocale() const currentLocale = useLocale()
useEffect(() => {
intranetService.incrementAnnouncementViewCount(announcement.id)
}, [announcement.id])
const getCategoryColor = (category: string) => { const getCategoryColor = (category: string) => {
const colors: Record<string, string> = { const colors: Record<string, string> = {
general: 'bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300', 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 */} {/* Content */}
<div className="p-6 max-h-[60vh] overflow-y-auto"> <div className="p-6 max-h-[60vh] overflow-y-auto">
{/* Images if exist */} {/* Images if exist */}
{announcement.imageUrl && (() => { {announcement.imageUrl &&
const images = announcement.imageUrl.split('|').filter(Boolean) (() => {
return images.length > 0 ? ( const images = announcement.imageUrl.split('|').filter(Boolean)
<div className={`mb-6 ${images.length > 1 ? 'grid grid-cols-2 gap-2' : ''}`}> if (images.length === 0) return null
{images.map((img, idx) => ( const getGridClass = (count: number) => {
<img if (count === 1) return ''
key={idx} if (count === 2) return 'grid grid-cols-2 gap-2'
src={img.startsWith('data:') ? img : `data:image/jpeg;base64,${img}`} if (count === 3) return 'grid grid-cols-3 gap-2'
alt={`${announcement.title} ${images.length > 1 ? idx + 1 : ''}`.trim()} return 'grid grid-cols-2 gap-2'
className="w-full rounded-lg object-cover" }
/> const getImgClass = (count: number, idx: number) => {
))} if (count === 1) return 'w-full rounded-lg object-cover max-h-96'
</div> if (count === 3 && idx === 0) return 'col-span-3 w-full rounded-lg object-cover max-h-64'
) : null 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 */} {/* Full Content */}
<div className="prose prose-sm dark:prose-invert max-w-none"> <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> </h2>
</div> </div>
<div className="p-6 space-y-4"> <div className="p-3 space-y-4">
{surveys?.map((survey, index) => { {surveys?.map((survey, index) => {
const daysLeft = dayjs(survey.deadline).diff(dayjs(), 'day') const daysLeft = dayjs(survey.deadline).diff(dayjs(), 'day')
const urgency = daysLeft <= 3 ? 'urgent' : daysLeft <= 7 ? 'warning' : 'normal' const urgency = daysLeft <= 3 ? 'urgent' : daysLeft <= 7 ? 'warning' : 'normal'
const isCompleted = !!survey.myResponse
return ( return (
<div <div
key={survey.id} key={survey.id}
onClick={() => onTakeSurvey(survey)} 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 */} {/* 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"> <div className="relative">
{/* Survey Title */} {/* Survey Title */}
<div className="flex items-start justify-between mb-3"> <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"> <div className="flex items-center gap-2 flex-1 min-w-0">
{survey.title} {isCompleted && (
</h4> <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 <div
className={`px-2 py-1 text-center rounded-full text-xs font-medium ${ className={`px-2 py-1 text-center rounded-full text-xs font-medium ${
urgency === 'urgent' urgency === 'urgent'
@ -119,8 +141,14 @@ const Surveys: React.FC<SurveysProps> = ({ surveys, onTakeSurvey }) => {
</p> </p>
{/* Action Button */} {/* 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"> <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 ${
{translate('::App.Platform.Intranet.Widgets.ActiveSurveys.FillSurvey')} 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" /> <FaArrowRight className="w-3 h-3 transition-transform group-hover:translate-x-1" />
</button> </button>
</div> </div>