From a70d8650f1c2836f2c070743e2e89dbdf77bf668 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sedat=20=C3=96zt=C3=BCrk?= Date: Fri, 8 May 2026 21:11:56 +0300 Subject: [PATCH] =?UTF-8?q?Event=20Like=20=C3=B6zelli=C4=9Fi=20eklendi?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Intranet/EventDto.cs | 1 + .../Intranet/IIntranetAppService.cs | 1 + .../Intranet/IntranetAppService.cs | 53 +++++- .../Seeds/LanguagesData.json | 6 + .../Seeds/ListFormSeeder_Administration.cs | 164 +++++++++++++++--- .../Seeds/PermissionsData.json | 65 +++++++ .../Enums/TableNameEnum.cs | 1 + .../PlatformConsts.cs | 1 + .../TableNameResolver.cs | 1 + .../Data/SeedConsts.cs | 1 + .../Entities/Tenant/Intranet/Event.cs | 1 + .../Entities/Tenant/Intranet/EventLike.cs | 24 +++ .../EntityFrameworkCore/PlatformDbContext.cs | 12 ++ ....cs => 20260508144450_Initial.Designer.cs} | 67 ++++++- ...3_Initial.cs => 20260508144450_Initial.cs} | 35 ++++ .../PlatformDbContextModelSnapshot.cs | 65 +++++++ ui/src/proxy/intranet/models.ts | 1 + ui/src/services/intranet.service.ts | 12 +- ui/src/views/intranet/widgets/EventModal.tsx | 46 ++++- .../views/intranet/widgets/UpcomingEvents.tsx | 69 ++++++-- ui/src/views/setup/DatabaseSetup.tsx | 16 +- 21 files changed, 595 insertions(+), 47 deletions(-) create mode 100644 api/src/Sozsoft.Platform.Domain/Entities/Tenant/Intranet/EventLike.cs rename api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/{20260507202053_Initial.Designer.cs => 20260508144450_Initial.Designer.cs} (99%) rename api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/{20260507202053_Initial.cs => 20260508144450_Initial.cs} (99%) diff --git a/api/src/Sozsoft.Platform.Application.Contracts/Intranet/EventDto.cs b/api/src/Sozsoft.Platform.Application.Contracts/Intranet/EventDto.cs index e00487d..045c389 100644 --- a/api/src/Sozsoft.Platform.Application.Contracts/Intranet/EventDto.cs +++ b/api/src/Sozsoft.Platform.Application.Contracts/Intranet/EventDto.cs @@ -18,6 +18,7 @@ public class EventDto public UserInfoViewModel User { get; set; } public int ParticipantsCount { get; set; } public int Likes { get; set; } + public bool IsLiked { get; set; } public bool IsPublished { get; set; } public string Photos { get; set; } public List Comments { get; set; } = []; diff --git a/api/src/Sozsoft.Platform.Application.Contracts/Intranet/IIntranetAppService.cs b/api/src/Sozsoft.Platform.Application.Contracts/Intranet/IIntranetAppService.cs index 0b5fce5..d528286 100644 --- a/api/src/Sozsoft.Platform.Application.Contracts/Intranet/IIntranetAppService.cs +++ b/api/src/Sozsoft.Platform.Application.Contracts/Intranet/IIntranetAppService.cs @@ -17,4 +17,5 @@ public interface IIntranetAppService : IApplicationService Task IncrementAnnouncementViewCountAsync(System.Guid id); Task> GetEventCommentsAsync(Guid eventId); Task CreateEventCommentAsync(Guid eventId, string content); + Task LikeEventAsync(Guid id); } diff --git a/api/src/Sozsoft.Platform.Application/Intranet/IntranetAppService.cs b/api/src/Sozsoft.Platform.Application/Intranet/IntranetAppService.cs index 40b93ae..50bdb7a 100644 --- a/api/src/Sozsoft.Platform.Application/Intranet/IntranetAppService.cs +++ b/api/src/Sozsoft.Platform.Application/Intranet/IntranetAppService.cs @@ -43,6 +43,7 @@ public class IntranetAppService : PlatformAppService, IIntranetAppService private readonly IRepository _socialMediaRepository; private readonly IRepository _socialPollOptionRepository; private readonly IRepository _eventCommentRepository; + private readonly IRepository _eventLikeRepository; public IntranetAppService( ICurrentTenant currentTenant, @@ -63,7 +64,8 @@ public class IntranetAppService : PlatformAppService, IIntranetAppService IRepository socialLikeRepository, IRepository socialMediaRepository, IRepository socialPollOptionRepository, - IRepository eventCommentRepository + IRepository eventCommentRepository, + IRepository eventLikeRepository ) { _currentTenant = currentTenant; @@ -84,6 +86,7 @@ public class IntranetAppService : PlatformAppService, IIntranetAppService _socialMediaRepository = socialMediaRepository; _socialPollOptionRepository = socialPollOptionRepository; _eventCommentRepository = eventCommentRepository; + _eventLikeRepository = eventLikeRepository; } [UnitOfWork] @@ -141,6 +144,16 @@ public class IntranetAppService : PlatformAppService, IIntranetAppService commentsQueryable.Where(c => eventIds.Contains(c.EventId)).OrderBy(c => c.CreationTime) ); + // Load all likes for these events + var likesQueryable = await _eventLikeRepository.GetQueryableAsync(); + var allLikes = await AsyncExecuter.ToListAsync( + likesQueryable.Where(l => eventIds.Contains(l.EventId)) + ); + var likedEventIds = allLikes + .Where(l => l.UserId == CurrentUser.Id) + .Select(l => l.EventId) + .ToHashSet(); + // Collect all unique user IDs var userIds = new HashSet(); foreach (var evt in events) @@ -199,6 +212,7 @@ public class IntranetAppService : PlatformAppService, IIntranetAppService User = user, ParticipantsCount = evt.ParticipantsCount, Likes = evt.Likes, + IsLiked = likedEventIds.Contains(evt.Id), IsPublished = evt.isPublished, Photos = evt.Photos, Comments = commentDtos @@ -267,6 +281,43 @@ public class IntranetAppService : PlatformAppService, IIntranetAppService }; } + [HttpPost("api/app/intranet/like-event")] + public async Task LikeEventAsync(Guid id) + { + var evt = await _eventRepository.GetAsync(id); + + var likeQueryable = await _eventLikeRepository.GetQueryableAsync(); + var existingLike = await AsyncExecuter.FirstOrDefaultAsync( + likeQueryable.Where(l => l.EventId == id && l.UserId == CurrentUser.Id)); + + bool isNowLiked; + if (existingLike != null) + { + await _eventLikeRepository.DeleteAsync(existingLike.Id); + evt.Likes = Math.Max(0, evt.Likes - 1); + isNowLiked = false; + } + else + { + await _eventLikeRepository.InsertAsync(new EventLike(Guid.NewGuid()) + { + EventId = id, + UserId = CurrentUser.Id, + }); + evt.Likes++; + isNowLiked = true; + } + + await _eventRepository.UpdateAsync(evt, autoSave: true); + + return new EventDto + { + Id = evt.Id, + Likes = evt.Likes, + IsLiked = isNowLiked, + }; + } + private async Task> GetBirthdaysAsync() { var today = DateTime.Now; diff --git a/api/src/Sozsoft.Platform.DbMigrator/Seeds/LanguagesData.json b/api/src/Sozsoft.Platform.DbMigrator/Seeds/LanguagesData.json index 7dd9f88..7a39b15 100644 --- a/api/src/Sozsoft.Platform.DbMigrator/Seeds/LanguagesData.json +++ b/api/src/Sozsoft.Platform.DbMigrator/Seeds/LanguagesData.json @@ -6462,6 +6462,12 @@ "tr": "Etkinlikler", "en": "Events" }, + { + "resourceName": "Platform", + "key": "App.Intranet.Events.EventLike", + "tr": "Etkinlik Beğenileri", + "en": "Event Likes" + }, { "resourceName": "Platform", "key": "App.Hr.Training", diff --git a/api/src/Sozsoft.Platform.DbMigrator/Seeds/ListFormSeeder_Administration.cs b/api/src/Sozsoft.Platform.DbMigrator/Seeds/ListFormSeeder_Administration.cs index 27ef96e..2296068 100644 --- a/api/src/Sozsoft.Platform.DbMigrator/Seeds/ListFormSeeder_Administration.cs +++ b/api/src/Sozsoft.Platform.DbMigrator/Seeds/ListFormSeeder_Administration.cs @@ -2759,21 +2759,6 @@ public class ListFormSeeder_Administration : IDataSeedContributor, ITransientDep PermissionJson = DefaultFieldPermissionJson(listForm.Name), PivotSettingsJson = DefaultPivotSettingsJson }, - new() { - ListFormCode = listForm.ListFormCode, - CultureName = LanguageCodes.En, - SourceDbType = DbType.String, - FieldName = "ImageUrl", - CaptionName = "App.Listform.ListformField.ImageUrl", - Width = 200, - ListOrderNo = 5, - Visible = true, - IsActive = true, - AllowSearch = true, - ColumnCustomizationJson = DefaultColumnCustomizationJson, - PermissionJson = DefaultFieldPermissionJson(listForm.Name), - PivotSettingsJson = DefaultPivotSettingsJson - }, new() { ListFormCode = listForm.ListFormCode, CultureName = LanguageCodes.En, @@ -2781,7 +2766,7 @@ public class ListFormSeeder_Administration : IDataSeedContributor, ITransientDep FieldName = "Category", CaptionName = "App.Listform.ListformField.Category", Width = 100, - ListOrderNo = 6, + ListOrderNo = 5, Visible = true, IsActive = true, AllowSearch = true, @@ -2809,7 +2794,7 @@ public class ListFormSeeder_Administration : IDataSeedContributor, ITransientDep FieldName = "UserId", CaptionName = "App.Listform.ListformField.UserId", Width = 100, - ListOrderNo = 7, + ListOrderNo = 6, Visible = true, IsActive = true, AllowSearch = true, @@ -2831,7 +2816,7 @@ public class ListFormSeeder_Administration : IDataSeedContributor, ITransientDep FieldName = "PublishDate", CaptionName = "App.Listform.ListformField.PublishDate", Width = 125, - ListOrderNo = 8, + ListOrderNo = 7, Visible = true, IsActive = true, AllowSearch = true, @@ -2847,7 +2832,7 @@ public class ListFormSeeder_Administration : IDataSeedContributor, ITransientDep FieldName = "ExpiryDate", CaptionName = "App.Listform.ListformField.ExpiryDate", Width = 125, - ListOrderNo = 9, + ListOrderNo = 8, Visible = true, IsActive = true, AllowSearch = true, @@ -2862,6 +2847,21 @@ public class ListFormSeeder_Administration : IDataSeedContributor, ITransientDep FieldName = "IsPinned", CaptionName = "App.Listform.ListformField.IsPinned", Width = 100, + ListOrderNo = 9, + Visible = true, + IsActive = true, + AllowSearch = true, + ColumnCustomizationJson = DefaultColumnCustomizationJson, + PermissionJson = DefaultFieldPermissionJson(listForm.Name), + PivotSettingsJson = DefaultPivotSettingsJson + }, + new() { + ListFormCode = listForm.ListFormCode, + CultureName = LanguageCodes.En, + SourceDbType = DbType.String, + FieldName = "ImageUrl", + CaptionName = "App.Listform.ListformField.ImageUrl", + Width = 200, ListOrderNo = 10, Visible = true, IsActive = true, @@ -4162,7 +4162,7 @@ public class ListFormSeeder_Administration : IDataSeedContributor, ITransientDep DeleteCommand = DefaultDeleteCommand(nameof(TableNameEnum.Event)), DeleteFieldsDefaultValueJson = DefaultDeleteFieldsDefaultValueJson(), PagerOptionJson = DefaultPagerOptionJson, - EditingOptionJson = DefaultEditingOptionJson(listFormName, 750, 500, true, true, true, true, false), + EditingOptionJson = DefaultEditingOptionJson(listFormName, 750, 500, true, true, true, true, false, true), EditingFormJson = JsonSerializer.Serialize(new List() { new() { @@ -4176,13 +4176,28 @@ public class ListFormSeeder_Administration : IDataSeedContributor, ITransientDep new EditingFormItemDto { Order = 6, DataField = "UserId", ColSpan = 1, IsRequired = true, EditorType2 = EditorTypes.dxSelectBox, EditorOptions=EditorOptionValues.ShowClearButton }, new EditingFormItemDto { Order = 7, DataField = "Description", ColSpan = 2, EditorType2 = EditorTypes.dxTextArea }, new EditingFormItemDto { Order = 8, DataField = "Status", ColSpan = 1, EditorType2 = EditorTypes.dxSelectBox, EditorOptions=EditorOptionValues.ShowClearButton }, - new EditingFormItemDto { Order = 9, DataField = "Photos", ColSpan = 1, EditorType2 = EditorTypes.dxImageUpload, EditorOptions = EditorOptionValues.ImageUploadOptions }, + new EditingFormItemDto { Order = 9, DataField = "ParticipantsCount", ColSpan = 1, EditorType2 = EditorTypes.dxNumberBox }, + new EditingFormItemDto { Order = 10, DataField = "Photos", ColSpan = 1, EditorType2 = EditorTypes.dxImageUpload, EditorOptions = EditorOptionValues.ImageUploadOptions }, ]} }), InsertFieldsDefaultValueJson = DefaultInsertFieldsDefaultValueJson(), FormFieldsDefaultValueJson = JsonSerializer.Serialize(new FieldsDefaultValue[] { new() { FieldName = "Status", FieldDbType = DbType.String, Value = "draft", CustomValueType = FieldCustomValueTypeEnum.Value }, }), + SubFormsJson = JsonSerializer.Serialize(new List() { + new { + TabType = ListFormTabTypeEnum.List, + TabTitle = AppCodes.Intranet.EventLike, + Code = AppCodes.Intranet.EventLike, + Relation = new List() { + new { + ParentFieldName = "Id", + DbType = DbType.Guid, + ChildFieldName = "EventId" + } + } + } + }) } ); @@ -4415,5 +4430,112 @@ public class ListFormSeeder_Administration : IDataSeedContributor, ITransientDep #endregion } #endregion + + #region EventLike + listFormName = AppCodes.Intranet.EventLike; + if (!await _listFormRepository.AnyAsync(a => a.ListFormCode == listFormName)) + { + var listForm = await _listFormRepository.InsertAsync( + new ListForm() + { + ListFormType = ListFormTypeEnum.List, + PageSize = 10, + ExportJson = DefaultExportJson, + IsSubForm = false, + ShowNote = true, + LayoutJson = DefaultLayoutJson(), + CultureName = LanguageCodes.En, + ListFormCode = listFormName, + Name = listFormName, + Title = listFormName, + DataSourceCode = SeedConsts.DataSources.DefaultCode, + IsTenant = true, + IsBranch = false, + IsOrganizationUnit = false, + Description = listFormName, + SelectCommandType = SelectCommandTypeEnum.Table, + SelectCommand = TableNameResolver.GetFullTableName(nameof(TableNameEnum.EventLike)), + KeyFieldName = "Id", + KeyFieldDbSourceType = DbType.Guid, + DefaultFilter = DefaultFilterJson, + SortMode = GridOptions.SortModeSingle, + FilterRowJson = DefaultFilterRowJson, + HeaderFilterJson = DefaultHeaderFilterJson, + SearchPanelJson = DefaultSearchPanelJson, + GroupPanelJson = DefaultGroupPanelJson, + SelectionJson = DefaultSelectionSingleJson, + ColumnOptionJson = DefaultColumnOptionJson(), + PermissionJson = DefaultPermissionJson(listFormName), + DeleteCommand = DefaultDeleteCommand(nameof(TableNameEnum.EventLike)), + DeleteFieldsDefaultValueJson = DefaultDeleteFieldsDefaultValueJson(), + PagerOptionJson = DefaultPagerOptionJson, + EditingOptionJson = DefaultEditingOptionJson(listFormName, 500, 250, true, true, true, true, false), + EditingFormJson = JsonSerializer.Serialize(new List() { + new() { Order=1, ColCount=1, ColSpan=1, ItemType="group", Items=[ + new EditingFormItemDto { Order = 1, DataField = "Name", ColSpan = 1, IsRequired = true, EditorType2=EditorTypes.dxTextBox }, + ]} + }), + InsertFieldsDefaultValueJson = DefaultInsertFieldsDefaultValueJson(), + } + ); + + #region EventLike Fields + await _listFormFieldRepository.InsertManyAsync(new ListFormField[] { + new() { + ListFormCode = listForm.ListFormCode, + CultureName = LanguageCodes.En, + SourceDbType = DbType.Guid, + FieldName = "Id", + CaptionName = "App.Listform.ListformField.Id", + Width = 100, + ListOrderNo = 1, + Visible = false, + IsActive = true, + ValidationRuleJson = DefaultValidationRuleRequiredJson, + ColumnCustomizationJson = DefaultColumnCustomizationJson, + PermissionJson = DefaultFieldPermissionJson(listForm.Name), + PivotSettingsJson = DefaultPivotSettingsJson + }, + new() { + ListFormCode = listForm.ListFormCode, + CultureName = LanguageCodes.En, + SourceDbType = DbType.Guid, + FieldName = "UserId", + CaptionName = "App.Listform.ListformField.UserId", + Width = 500, + ListOrderNo = 2, + Visible = true, + IsActive = true, + AllowSearch = true, + LookupJson = JsonSerializer.Serialize(new LookupDto { + DataSourceType = UiLookupDataSourceTypeEnum.Query, + DisplayExpr = "Name", + ValueExpr = "Key", + LookupQuery = LookupQueryValues.UserValues + }), + ValidationRuleJson = DefaultValidationRuleRequiredJson, + ColumnCustomizationJson = DefaultColumnCustomizationJson, + PermissionJson = DefaultFieldPermissionJson(listForm.Name), + PivotSettingsJson = DefaultPivotSettingsJson + }, + new() { + ListFormCode = listForm.ListFormCode, + CultureName = LanguageCodes.En, + SourceDbType = DbType.Guid, + FieldName = "EventId", + CaptionName = "App.Listform.ListformField.EventId", + Width = 100, + ListOrderNo = 3, + Visible = false, + IsActive = true, + ValidationRuleJson = DefaultValidationRuleRequiredJson, + ColumnCustomizationJson = DefaultColumnCustomizationJson, + PermissionJson = DefaultFieldPermissionJson(listForm.Name), + PivotSettingsJson = DefaultPivotSettingsJson + }, + }); + #endregion + } + #endregion } } diff --git a/api/src/Sozsoft.Platform.DbMigrator/Seeds/PermissionsData.json b/api/src/Sozsoft.Platform.DbMigrator/Seeds/PermissionsData.json index dc5b123..6e9b26b 100644 --- a/api/src/Sozsoft.Platform.DbMigrator/Seeds/PermissionsData.json +++ b/api/src/Sozsoft.Platform.DbMigrator/Seeds/PermissionsData.json @@ -3538,6 +3538,71 @@ "MultiTenancySide": 3, "MenuGroup": "Erp|Kurs" }, + + { + "GroupName": "App.Administration", + "Name": "App.Intranet.Events.EventLike", + "ParentName": "App.Intranet", + "DisplayName": "App.Intranet.Events.EventLike", + "IsEnabled": true, + "MultiTenancySide": 3, + "MenuGroup": "Erp|Kurs" + }, + { + "GroupName": "App.Administration", + "Name": "App.Intranet.Events.EventLike.Create", + "ParentName": "App.Intranet.Events.EventLike", + "DisplayName": "Create", + "IsEnabled": true, + "MultiTenancySide": 3, + "MenuGroup": "Erp|Kurs" + }, + { + "GroupName": "App.Administration", + "Name": "App.Intranet.Events.EventLike.Update", + "ParentName": "App.Intranet.Events.EventLike", + "DisplayName": "Update", + "IsEnabled": true, + "MultiTenancySide": 3, + "MenuGroup": "Erp|Kurs" + }, + { + "GroupName": "App.Administration", + "Name": "App.Intranet.Events.EventLike.Delete", + "ParentName": "App.Intranet.Events.EventLike", + "DisplayName": "Delete", + "IsEnabled": true, + "MultiTenancySide": 3, + "MenuGroup": "Erp|Kurs" + }, + { + "GroupName": "App.Administration", + "Name": "App.Intranet.Events.EventLike.Export", + "ParentName": "App.Intranet.Events.EventLike", + "DisplayName": "Export", + "IsEnabled": true, + "MultiTenancySide": 3, + "MenuGroup": "Erp|Kurs" + }, + { + "GroupName": "App.Administration", + "Name": "App.Intranet.Events.EventLike.Import", + "ParentName": "App.Intranet.Events.EventLike", + "DisplayName": "Import", + "IsEnabled": true, + "MultiTenancySide": 3, + "MenuGroup": "Erp|Kurs" + }, + { + "GroupName": "App.Administration", + "Name": "App.Intranet.Events.EventLike.Note", + "ParentName": "App.Intranet.Events.EventLike", + "DisplayName": "Note", + "IsEnabled": true, + "MultiTenancySide": 3, + "MenuGroup": "Erp|Kurs" + }, + { "GroupName": "App.Administration", "Name": "App.Intranet.Announcement", diff --git a/api/src/Sozsoft.Platform.Domain.Shared/Enums/TableNameEnum.cs b/api/src/Sozsoft.Platform.Domain.Shared/Enums/TableNameEnum.cs index f3ebec8..3a5cc79 100644 --- a/api/src/Sozsoft.Platform.Domain.Shared/Enums/TableNameEnum.cs +++ b/api/src/Sozsoft.Platform.Domain.Shared/Enums/TableNameEnum.cs @@ -76,6 +76,7 @@ public enum TableNameEnum SocialPollOption, SocialComment, SocialLike, + EventLike, EventCategory, EventType, Event, diff --git a/api/src/Sozsoft.Platform.Domain.Shared/PlatformConsts.cs b/api/src/Sozsoft.Platform.Domain.Shared/PlatformConsts.cs index 217db0a..81212e3 100644 --- a/api/src/Sozsoft.Platform.Domain.Shared/PlatformConsts.cs +++ b/api/src/Sozsoft.Platform.Domain.Shared/PlatformConsts.cs @@ -362,6 +362,7 @@ public static class PlatformConsts public const string EventType = Events + ".EventType"; public const string EventCategory = Events + ".EventCategory"; public const string Event = Events + ".Event"; + public const string EventLike = Events + ".EventLike"; } public static class Definitions diff --git a/api/src/Sozsoft.Platform.Domain.Shared/TableNameResolver.cs b/api/src/Sozsoft.Platform.Domain.Shared/TableNameResolver.cs index 030268c..cc24b64 100644 --- a/api/src/Sozsoft.Platform.Domain.Shared/TableNameResolver.cs +++ b/api/src/Sozsoft.Platform.Domain.Shared/TableNameResolver.cs @@ -94,6 +94,7 @@ public static class TableNameResolver { nameof(TableNameEnum.SocialPollOption), (TablePrefix.TenantByName, MenuPrefix.Administration) }, { nameof(TableNameEnum.SocialComment), (TablePrefix.TenantByName, MenuPrefix.Administration) }, { nameof(TableNameEnum.SocialLike), (TablePrefix.TenantByName, MenuPrefix.Administration) }, + { nameof(TableNameEnum.EventLike), (TablePrefix.TenantByName, MenuPrefix.Administration) }, { nameof(TableNameEnum.EventCategory), (TablePrefix.TenantByName, MenuPrefix.Administration) }, { nameof(TableNameEnum.EventType), (TablePrefix.TenantByName, MenuPrefix.Administration) }, { nameof(TableNameEnum.Event), (TablePrefix.TenantByName, MenuPrefix.Administration) }, diff --git a/api/src/Sozsoft.Platform.Domain/Data/SeedConsts.cs b/api/src/Sozsoft.Platform.Domain/Data/SeedConsts.cs index 5fb4a2e..8bf2ebf 100644 --- a/api/src/Sozsoft.Platform.Domain/Data/SeedConsts.cs +++ b/api/src/Sozsoft.Platform.Domain/Data/SeedConsts.cs @@ -341,6 +341,7 @@ public static class SeedConsts public const string EventType = Events + ".EventType"; public const string EventCategory = Events + ".EventCategory"; public const string Event = Events + ".Event"; + public const string EventLike = Events + ".EventLike"; } public static class Definitions diff --git a/api/src/Sozsoft.Platform.Domain/Entities/Tenant/Intranet/Event.cs b/api/src/Sozsoft.Platform.Domain/Entities/Tenant/Intranet/Event.cs index 66d5a2e..9b36b17 100644 --- a/api/src/Sozsoft.Platform.Domain/Entities/Tenant/Intranet/Event.cs +++ b/api/src/Sozsoft.Platform.Domain/Entities/Tenant/Intranet/Event.cs @@ -30,6 +30,7 @@ public class Event : FullAuditedEntity, IMultiTenant public string Photos { get; set; } public ICollection Comments { get; set; } = []; + public ICollection EventLikes { get; set; } = []; Guid? IMultiTenant.TenantId => TenantId; } diff --git a/api/src/Sozsoft.Platform.Domain/Entities/Tenant/Intranet/EventLike.cs b/api/src/Sozsoft.Platform.Domain/Entities/Tenant/Intranet/EventLike.cs new file mode 100644 index 0000000..445b2ac --- /dev/null +++ b/api/src/Sozsoft.Platform.Domain/Entities/Tenant/Intranet/EventLike.cs @@ -0,0 +1,24 @@ +using System; +using Volo.Abp.Domain.Entities.Auditing; +using Volo.Abp.MultiTenancy; + +namespace Sozsoft.Platform.Entities; + +public class EventLike : FullAuditedEntity, IMultiTenant +{ + public Guid? TenantId { get; set; } + + public Guid EventId { get; set; } + public Event Event { get; set; } + + public Guid? UserId { get; set; } + + public EventLike(Guid id) + { + Id = id; + } + + protected EventLike() { } + + Guid? IMultiTenant.TenantId => TenantId; +} diff --git a/api/src/Sozsoft.Platform.EntityFrameworkCore/EntityFrameworkCore/PlatformDbContext.cs b/api/src/Sozsoft.Platform.EntityFrameworkCore/EntityFrameworkCore/PlatformDbContext.cs index ee4bc61..fec66f4 100644 --- a/api/src/Sozsoft.Platform.EntityFrameworkCore/EntityFrameworkCore/PlatformDbContext.cs +++ b/api/src/Sozsoft.Platform.EntityFrameworkCore/EntityFrameworkCore/PlatformDbContext.cs @@ -114,6 +114,7 @@ public class PlatformDbContext : public DbSet EventCategories { get; set; } public DbSet EventTypes { get; set; } public DbSet EventComments { get; set; } + public DbSet EventLikes { get; set; } public DbSet Announcements { get; set; } @@ -1275,6 +1276,17 @@ public class PlatformDbContext : .OnDelete(DeleteBehavior.Cascade); }); + builder.Entity(b => + { + b.ToTable(TableNameResolver.GetFullTableName(nameof(TableNameEnum.EventLike)), Prefix.DbSchema); + b.ConfigureByConvention(); + + b.HasOne(x => x.Event) + .WithMany(x => x.EventLikes) + .HasForeignKey(x => x.EventId) + .OnDelete(DeleteBehavior.Cascade); + }); + //Videoroom builder.Entity(b => { diff --git a/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260507202053_Initial.Designer.cs b/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260508144450_Initial.Designer.cs similarity index 99% rename from api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260507202053_Initial.Designer.cs rename to api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260508144450_Initial.Designer.cs index 3cc8529..f264877 100644 --- a/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260507202053_Initial.Designer.cs +++ b/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260508144450_Initial.Designer.cs @@ -13,7 +13,7 @@ using Volo.Abp.EntityFrameworkCore; namespace Sozsoft.Platform.Migrations { [DbContext(typeof(PlatformDbContext))] - [Migration("20260507202053_Initial")] + [Migration("20260508144450_Initial")] partial class Initial { /// @@ -2146,6 +2146,58 @@ namespace Sozsoft.Platform.Migrations b.ToTable("Adm_T_EventComment", (string)null); }); + modelBuilder.Entity("Sozsoft.Platform.Entities.EventLike", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier"); + + b.Property("CreationTime") + .HasColumnType("datetime2") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uniqueidentifier") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uniqueidentifier") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("datetime2") + .HasColumnName("DeletionTime"); + + b.Property("EventId") + .HasColumnType("uniqueidentifier"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("datetime2") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uniqueidentifier") + .HasColumnName("LastModifierId"); + + b.Property("TenantId") + .HasColumnType("uniqueidentifier") + .HasColumnName("TenantId"); + + b.Property("UserId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("EventId"); + + b.ToTable("Adm_T_EventLike", (string)null); + }); + modelBuilder.Entity("Sozsoft.Platform.Entities.EventType", b => { b.Property("Id") @@ -7737,6 +7789,17 @@ namespace Sozsoft.Platform.Migrations b.Navigation("Event"); }); + modelBuilder.Entity("Sozsoft.Platform.Entities.EventLike", b => + { + b.HasOne("Sozsoft.Platform.Entities.Event", "Event") + .WithMany("EventLikes") + .HasForeignKey("EventId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Event"); + }); + modelBuilder.Entity("Sozsoft.Platform.Entities.JobPosition", b => { b.HasOne("Sozsoft.Platform.Entities.Department", "Department") @@ -8201,6 +8264,8 @@ namespace Sozsoft.Platform.Migrations modelBuilder.Entity("Sozsoft.Platform.Entities.Event", b => { b.Navigation("Comments"); + + b.Navigation("EventLikes"); }); modelBuilder.Entity("Sozsoft.Platform.Entities.EventCategory", b => diff --git a/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260507202053_Initial.cs b/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260508144450_Initial.cs similarity index 99% rename from api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260507202053_Initial.cs rename to api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260508144450_Initial.cs index 57b1273..961effa 100644 --- a/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260507202053_Initial.cs +++ b/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260508144450_Initial.cs @@ -2869,6 +2869,33 @@ namespace Sozsoft.Platform.Migrations onDelete: ReferentialAction.Cascade); }); + migrationBuilder.CreateTable( + name: "Adm_T_EventLike", + columns: table => new + { + Id = table.Column(type: "uniqueidentifier", nullable: false), + TenantId = table.Column(type: "uniqueidentifier", nullable: true), + EventId = table.Column(type: "uniqueidentifier", nullable: false), + UserId = table.Column(type: "uniqueidentifier", nullable: true), + CreationTime = table.Column(type: "datetime2", nullable: false), + CreatorId = table.Column(type: "uniqueidentifier", nullable: true), + LastModificationTime = table.Column(type: "datetime2", nullable: true), + LastModifierId = table.Column(type: "uniqueidentifier", nullable: true), + IsDeleted = table.Column(type: "bit", nullable: false, defaultValue: false), + DeleterId = table.Column(type: "uniqueidentifier", nullable: true), + DeletionTime = table.Column(type: "datetime2", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Adm_T_EventLike", x => x.Id); + table.ForeignKey( + name: "FK_Adm_T_EventLike_Adm_T_Event_EventId", + column: x => x.EventId, + principalTable: "Adm_T_Event", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + migrationBuilder.CreateTable( name: "Adm_T_SocialPollOption", columns: table => new @@ -3381,6 +3408,11 @@ namespace Sozsoft.Platform.Migrations table: "Adm_T_EventComment", column: "EventId"); + migrationBuilder.CreateIndex( + name: "IX_Adm_T_EventLike_EventId", + table: "Adm_T_EventLike", + column: "EventId"); + migrationBuilder.CreateIndex( name: "IX_Adm_T_JobPosition_DepartmentId", table: "Adm_T_JobPosition", @@ -3790,6 +3822,9 @@ namespace Sozsoft.Platform.Migrations migrationBuilder.DropTable( name: "Adm_T_EventComment"); + migrationBuilder.DropTable( + name: "Adm_T_EventLike"); + migrationBuilder.DropTable( name: "Adm_T_IpRestriction"); diff --git a/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/PlatformDbContextModelSnapshot.cs b/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/PlatformDbContextModelSnapshot.cs index 8c9f690..d699379 100644 --- a/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/PlatformDbContextModelSnapshot.cs +++ b/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/PlatformDbContextModelSnapshot.cs @@ -2143,6 +2143,58 @@ namespace Sozsoft.Platform.Migrations b.ToTable("Adm_T_EventComment", (string)null); }); + modelBuilder.Entity("Sozsoft.Platform.Entities.EventLike", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier"); + + b.Property("CreationTime") + .HasColumnType("datetime2") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uniqueidentifier") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uniqueidentifier") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("datetime2") + .HasColumnName("DeletionTime"); + + b.Property("EventId") + .HasColumnType("uniqueidentifier"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("datetime2") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uniqueidentifier") + .HasColumnName("LastModifierId"); + + b.Property("TenantId") + .HasColumnType("uniqueidentifier") + .HasColumnName("TenantId"); + + b.Property("UserId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("EventId"); + + b.ToTable("Adm_T_EventLike", (string)null); + }); + modelBuilder.Entity("Sozsoft.Platform.Entities.EventType", b => { b.Property("Id") @@ -7734,6 +7786,17 @@ namespace Sozsoft.Platform.Migrations b.Navigation("Event"); }); + modelBuilder.Entity("Sozsoft.Platform.Entities.EventLike", b => + { + b.HasOne("Sozsoft.Platform.Entities.Event", "Event") + .WithMany("EventLikes") + .HasForeignKey("EventId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Event"); + }); + modelBuilder.Entity("Sozsoft.Platform.Entities.JobPosition", b => { b.HasOne("Sozsoft.Platform.Entities.Department", "Department") @@ -8198,6 +8261,8 @@ namespace Sozsoft.Platform.Migrations modelBuilder.Entity("Sozsoft.Platform.Entities.Event", b => { b.Navigation("Comments"); + + b.Navigation("EventLikes"); }); modelBuilder.Entity("Sozsoft.Platform.Entities.EventCategory", b => diff --git a/ui/src/proxy/intranet/models.ts b/ui/src/proxy/intranet/models.ts index e9161d4..f1f65bc 100644 --- a/ui/src/proxy/intranet/models.ts +++ b/ui/src/proxy/intranet/models.ts @@ -22,6 +22,7 @@ export interface EventDto { user: UserInfoViewModel participantsCount: number likes: number + isLiked: boolean isPublished: boolean photos: string comments: EventCommentDto[] diff --git a/ui/src/services/intranet.service.ts b/ui/src/services/intranet.service.ts index 4c0caf8..77ce538 100644 --- a/ui/src/services/intranet.service.ts +++ b/ui/src/services/intranet.service.ts @@ -1,4 +1,4 @@ -import { EventCommentDto, IntranetDashboardDto, SocialCommentDto, SocialPostDto } from '@/proxy/intranet/models' +import { EventCommentDto, EventDto, IntranetDashboardDto, SocialCommentDto, SocialPostDto } from '@/proxy/intranet/models' import apiService, { Config } from './api.service' export interface CreateSocialPostInput { @@ -114,6 +114,16 @@ export class IntranetService { }, { apiName: this.apiName, ...config }, ) + + likeEvent = (id: string, config?: Partial) => + apiService.fetchData( + { + method: 'POST', + url: `/api/app/intranet/like-event`, + params: { id }, + }, + { apiName: this.apiName, ...config }, + ) } export const intranetService = new IntranetService() diff --git a/ui/src/views/intranet/widgets/EventModal.tsx b/ui/src/views/intranet/widgets/EventModal.tsx index e04b7b3..56e8936 100644 --- a/ui/src/views/intranet/widgets/EventModal.tsx +++ b/ui/src/views/intranet/widgets/EventModal.tsx @@ -10,6 +10,7 @@ import { FaExpand, FaCommentAlt, FaPaperPlane, + FaHeart, } from 'react-icons/fa' import { EventCommentDto, EventDto } from '@/proxy/intranet/models' import useLocale from '@/utils/hooks/useLocale' @@ -56,6 +57,11 @@ const EventModal: React.FC = ({ event, onClose }) => { const commentInputRef = useRef(null) const commentsEndRef = useRef(null) + // Likes state + const [likes, setLikes] = useState(event.likes) + const [isLiked, setIsLiked] = useState(event.isLiked) + const [liking, setLiking] = useState(false) + useEffect(() => { // Refresh comments from API on open intranetService.getEventComments(event.id).then((res) => { @@ -89,6 +95,27 @@ const EventModal: React.FC = ({ event, onClose }) => { } } + const handleLike = async () => { + if (liking) return + setLiking(true) + // Optimistic update + setIsLiked((prev) => !prev) + setLikes((prev) => (isLiked ? Math.max(0, prev - 1) : prev + 1)) + try { + const res = await intranetService.likeEvent(event.id) + if (res.data) { + setLikes(res.data.likes) + setIsLiked(res.data.isLiked) + } + } catch { + // Revert on error + setIsLiked((prev) => !prev) + setLikes((prev) => (isLiked ? prev + 1 : Math.max(0, prev - 1))) + } finally { + setLiking(false) + } + } + const openLightbox = (idx: number) => { setLightboxIndex(idx) setLightboxOpen(true) @@ -151,7 +178,22 @@ const EventModal: React.FC = ({ event, onClose }) => { {event.participantsCount} )} +
+ +
+ {event.user && (
= ({ event, onClose }) => {
{/* Footer */} -
+
diff --git a/ui/src/views/intranet/widgets/UpcomingEvents.tsx b/ui/src/views/intranet/widgets/UpcomingEvents.tsx index 0399d0d..4e0f4ad 100644 --- a/ui/src/views/intranet/widgets/UpcomingEvents.tsx +++ b/ui/src/views/intranet/widgets/UpcomingEvents.tsx @@ -1,10 +1,12 @@ import React from 'react' -import { FaCalendarAlt } from 'react-icons/fa' +import { FaCalendarAlt, FaHeart } from 'react-icons/fa' import dayjs from 'dayjs' import { EventDto } from '@/proxy/intranet/models' import useLocale from '@/utils/hooks/useLocale' import { currentLocalDate } from '@/utils/dateUtils' import { useLocalization } from '@/utils/hooks/useLocalization' +import { Avatar } from '@/components/ui' +import { AVATAR_URL } from '@/constants/app.constant' interface UpcomingEventsProps { events: EventDto[] @@ -45,7 +47,7 @@ const UpcomingEvents: React.FC = ({ events, onEventClick }) {translate('::App.Platform.Intranet.Widgets.UpcomingEvents.Title')}
-
+
{upcomingEvents.length > 0 ? ( upcomingEvents.slice(0, 3).map((event) => { const firstPhoto = getFirstPhoto(event.photos) @@ -53,28 +55,59 @@ const UpcomingEvents: React.FC = ({ events, onEventClick })
onEventClick?.(event)} - className={`p-3 rounded-lg border-l-4 bg-gray-50 dark:bg-gray-700/50 border-l-green-500 flex items-center gap-3 ${onEventClick ? 'cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-600/50 transition-colors' : ''}`} + className={`p-6 transition-colors ${onEventClick ? 'cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-700/50' : ''}`} > -
-

{event.name}

-

- {currentLocalDate(event.date, currentLocale || 'tr')} - {event.place} -

+
+
+

{event.name}

+

+ {currentLocalDate(event.date, currentLocale || 'tr')} - {event.place} +

+ {event.description && ( +

+ {event.description} +

+ )} +
+ + {event.user.fullName} + + {dayjs(event.date).fromNow()} + {event.likes > 0 && ( + <> + + + + {event.likes} + + + )} +
+
+ {firstPhoto && ( + {event.name} + )}
- {firstPhoto && ( - {event.name} - )}
) }) ) : ( -

- {translate('::App.Platform.Intranet.Widgets.UpcomingEvents.NoEvent')} -

+
+
+ +
+

+ {translate('::App.Platform.Intranet.Widgets.UpcomingEvents.NoEvent')} +

+
)}
diff --git a/ui/src/views/setup/DatabaseSetup.tsx b/ui/src/views/setup/DatabaseSetup.tsx index ee70727..afcf5c6 100644 --- a/ui/src/views/setup/DatabaseSetup.tsx +++ b/ui/src/views/setup/DatabaseSetup.tsx @@ -1,6 +1,6 @@ import { useEffect, useRef, useState } from 'react' import { APP_NAME } from '@/constants/app.constant' -import { getMigrateUrl } from '@/services/setup.service' +import { getMigrateUrl, getSetupStatus } from '@/services/setup.service' import { applicationConfigurationUrl } from '@/services/abpConfig.service' interface LogLine { @@ -77,6 +77,7 @@ const DatabaseSetup = () => { const [logs, setLogs] = useState([]) const [status, setStatus] = useState('idle') const [pollCountdown, setPollCountdown] = useState(0) + const [dbExists, setDbExists] = useState(null) const logEndRef = useRef(null) const pollTimerRef = useRef | null>(null) @@ -85,6 +86,13 @@ const DatabaseSetup = () => { logEndRef.current?.scrollIntoView({ behavior: 'smooth' }) }, [logs]) + // Check DB existence on mount + useEffect(() => { + getSetupStatus() + .then((res) => setDbExists(res.data.dbExists)) + .catch(() => setDbExists(false)) + }, []) + // Cleanup on component unmount useEffect(() => { return () => { @@ -227,7 +235,9 @@ const DatabaseSetup = () => { {/* Action Area */}
- {status === 'idle' && 'Database not found. Press the button to start migration.'} + {status === 'idle' && dbExists === true && 'Database already exists. Migration is not required.'} + {status === 'idle' && dbExists === false && 'Database not found. Press the button to start migration.'} + {status === 'idle' && dbExists === null && 'Checking database status...'} {status === 'running' && 'Please wait, migration is in progress...'} {status === 'success' && 'Migration completed. Server is restarting...'} {status === 'restarting' && @@ -237,7 +247,7 @@ const DatabaseSetup = () => {
- {(status === 'idle' || status === 'error') && ( + {(status === 'idle' || status === 'error') && !dbExists && (