Event Like özelliği eklendi

This commit is contained in:
Sedat Öztürk 2026-05-08 21:11:56 +03:00
parent 0554717bc6
commit a70d8650f1
21 changed files with 595 additions and 47 deletions

View file

@ -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<EventCommentDto> Comments { get; set; } = [];

View file

@ -17,4 +17,5 @@ public interface IIntranetAppService : IApplicationService
Task IncrementAnnouncementViewCountAsync(System.Guid id);
Task<List<EventCommentDto>> GetEventCommentsAsync(Guid eventId);
Task<EventCommentDto> CreateEventCommentAsync(Guid eventId, string content);
Task<EventDto> LikeEventAsync(Guid id);
}

View file

@ -43,6 +43,7 @@ public class IntranetAppService : PlatformAppService, IIntranetAppService
private readonly IRepository<SocialMedia, Guid> _socialMediaRepository;
private readonly IRepository<SocialPollOption, Guid> _socialPollOptionRepository;
private readonly IRepository<EventComment, Guid> _eventCommentRepository;
private readonly IRepository<EventLike, Guid> _eventLikeRepository;
public IntranetAppService(
ICurrentTenant currentTenant,
@ -63,7 +64,8 @@ public class IntranetAppService : PlatformAppService, IIntranetAppService
IRepository<SocialLike, Guid> socialLikeRepository,
IRepository<SocialMedia, Guid> socialMediaRepository,
IRepository<SocialPollOption, Guid> socialPollOptionRepository,
IRepository<EventComment, Guid> eventCommentRepository
IRepository<EventComment, Guid> eventCommentRepository,
IRepository<EventLike, Guid> 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<Guid>();
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<EventDto> 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<List<UserInfoViewModel>> GetBirthdaysAsync()
{
var today = DateTime.Now;

View file

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

View file

@ -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<EditingFormDto>()
{
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<dynamic>() {
new {
TabType = ListFormTabTypeEnum.List,
TabTitle = AppCodes.Intranet.EventLike,
Code = AppCodes.Intranet.EventLike,
Relation = new List<dynamic>() {
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<EditingFormDto>() {
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
}
}

View file

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

View file

@ -76,6 +76,7 @@ public enum TableNameEnum
SocialPollOption,
SocialComment,
SocialLike,
EventLike,
EventCategory,
EventType,
Event,

View file

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

View file

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

View file

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

View file

@ -30,6 +30,7 @@ public class Event : FullAuditedEntity<Guid>, IMultiTenant
public string Photos { get; set; }
public ICollection<EventComment> Comments { get; set; } = [];
public ICollection<EventLike> EventLikes { get; set; } = [];
Guid? IMultiTenant.TenantId => TenantId;
}

View file

@ -0,0 +1,24 @@
using System;
using Volo.Abp.Domain.Entities.Auditing;
using Volo.Abp.MultiTenancy;
namespace Sozsoft.Platform.Entities;
public class EventLike : FullAuditedEntity<Guid>, 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;
}

View file

@ -114,6 +114,7 @@ public class PlatformDbContext :
public DbSet<EventCategory> EventCategories { get; set; }
public DbSet<EventType> EventTypes { get; set; }
public DbSet<EventComment> EventComments { get; set; }
public DbSet<EventLike> EventLikes { get; set; }
public DbSet<Announcement> Announcements { get; set; }
@ -1275,6 +1276,17 @@ public class PlatformDbContext :
.OnDelete(DeleteBehavior.Cascade);
});
builder.Entity<EventLike>(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<Videoroom>(b =>
{

View file

@ -13,7 +13,7 @@ using Volo.Abp.EntityFrameworkCore;
namespace Sozsoft.Platform.Migrations
{
[DbContext(typeof(PlatformDbContext))]
[Migration("20260507202053_Initial")]
[Migration("20260508144450_Initial")]
partial class Initial
{
/// <inheritdoc />
@ -2146,6 +2146,58 @@ namespace Sozsoft.Platform.Migrations
b.ToTable("Adm_T_EventComment", (string)null);
});
modelBuilder.Entity("Sozsoft.Platform.Entities.EventLike", b =>
{
b.Property<Guid>("Id")
.HasColumnType("uniqueidentifier");
b.Property<DateTime>("CreationTime")
.HasColumnType("datetime2")
.HasColumnName("CreationTime");
b.Property<Guid?>("CreatorId")
.HasColumnType("uniqueidentifier")
.HasColumnName("CreatorId");
b.Property<Guid?>("DeleterId")
.HasColumnType("uniqueidentifier")
.HasColumnName("DeleterId");
b.Property<DateTime?>("DeletionTime")
.HasColumnType("datetime2")
.HasColumnName("DeletionTime");
b.Property<Guid>("EventId")
.HasColumnType("uniqueidentifier");
b.Property<bool>("IsDeleted")
.ValueGeneratedOnAdd()
.HasColumnType("bit")
.HasDefaultValue(false)
.HasColumnName("IsDeleted");
b.Property<DateTime?>("LastModificationTime")
.HasColumnType("datetime2")
.HasColumnName("LastModificationTime");
b.Property<Guid?>("LastModifierId")
.HasColumnType("uniqueidentifier")
.HasColumnName("LastModifierId");
b.Property<Guid?>("TenantId")
.HasColumnType("uniqueidentifier")
.HasColumnName("TenantId");
b.Property<Guid?>("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<Guid>("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 =>

View file

@ -2869,6 +2869,33 @@ namespace Sozsoft.Platform.Migrations
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "Adm_T_EventLike",
columns: table => new
{
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
TenantId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
EventId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
UserId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
CreationTime = table.Column<DateTime>(type: "datetime2", nullable: false),
CreatorId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
LastModificationTime = table.Column<DateTime>(type: "datetime2", nullable: true),
LastModifierId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
IsDeleted = table.Column<bool>(type: "bit", nullable: false, defaultValue: false),
DeleterId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
DeletionTime = table.Column<DateTime>(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");

View file

@ -2143,6 +2143,58 @@ namespace Sozsoft.Platform.Migrations
b.ToTable("Adm_T_EventComment", (string)null);
});
modelBuilder.Entity("Sozsoft.Platform.Entities.EventLike", b =>
{
b.Property<Guid>("Id")
.HasColumnType("uniqueidentifier");
b.Property<DateTime>("CreationTime")
.HasColumnType("datetime2")
.HasColumnName("CreationTime");
b.Property<Guid?>("CreatorId")
.HasColumnType("uniqueidentifier")
.HasColumnName("CreatorId");
b.Property<Guid?>("DeleterId")
.HasColumnType("uniqueidentifier")
.HasColumnName("DeleterId");
b.Property<DateTime?>("DeletionTime")
.HasColumnType("datetime2")
.HasColumnName("DeletionTime");
b.Property<Guid>("EventId")
.HasColumnType("uniqueidentifier");
b.Property<bool>("IsDeleted")
.ValueGeneratedOnAdd()
.HasColumnType("bit")
.HasDefaultValue(false)
.HasColumnName("IsDeleted");
b.Property<DateTime?>("LastModificationTime")
.HasColumnType("datetime2")
.HasColumnName("LastModificationTime");
b.Property<Guid?>("LastModifierId")
.HasColumnType("uniqueidentifier")
.HasColumnName("LastModifierId");
b.Property<Guid?>("TenantId")
.HasColumnType("uniqueidentifier")
.HasColumnName("TenantId");
b.Property<Guid?>("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<Guid>("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 =>

View file

@ -22,6 +22,7 @@ export interface EventDto {
user: UserInfoViewModel
participantsCount: number
likes: number
isLiked: boolean
isPublished: boolean
photos: string
comments: EventCommentDto[]

View file

@ -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<Config>) =>
apiService.fetchData<EventDto>(
{
method: 'POST',
url: `/api/app/intranet/like-event`,
params: { id },
},
{ apiName: this.apiName, ...config },
)
}
export const intranetService = new IntranetService()

View file

@ -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<EventModalProps> = ({ event, onClose }) => {
const commentInputRef = useRef<HTMLTextAreaElement>(null)
const commentsEndRef = useRef<HTMLDivElement>(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<EventModalProps> = ({ 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<EventModalProps> = ({ event, onClose }) => {
{event.participantsCount}
</span>
)}
<div>
<button
onClick={handleLike}
disabled={liking}
className={`flex items-center gap-2 border-0 transition-colors text-xs font-xs border ${
isLiked
? 'bg-red-50 dark:bg-red-900/20 border-red-200 dark:border-red-800 text-red-600 dark:text-red-400 hover:bg-red-100 dark:hover:bg-red-900/40'
: 'bg-white dark:bg-gray-700 border-gray-200 dark:border-gray-600 text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-600'
} disabled:opacity-50 disabled:cursor-not-allowed`}
>
<FaHeart className={`w-4 h-4 ${isLiked ? 'text-red-500' : ''}`} />
{likes > 0 && likes}
</button>
</div>
</div>
{event.user && (
<div className="flex items-center gap-2 mt-3">
<Avatar
@ -319,10 +361,10 @@ const EventModal: React.FC<EventModalProps> = ({ event, onClose }) => {
</div>
{/* Footer */}
<div className="p-4 border-t border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-700/50 flex-shrink-0">
<div className="p-4 border-t border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-700/50 flex-shrink-0 flex items-center gap-3">
<button
onClick={onClose}
className="w-full px-4 py-2 bg-green-600 hover:bg-green-700 text-white rounded-lg transition-colors text-sm font-medium"
className="flex-1 px-4 py-2 bg-green-600 hover:bg-green-700 text-white rounded-lg transition-colors text-sm font-medium"
>
{translate('::App.Platform.Close')}
</button>

View file

@ -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<UpcomingEventsProps> = ({ events, onEventClick })
{translate('::App.Platform.Intranet.Widgets.UpcomingEvents.Title')}
</h2>
</div>
<div className="p-4 space-y-3">
<div className="divide-y divide-gray-200 dark:divide-gray-700">
{upcomingEvents.length > 0 ? (
upcomingEvents.slice(0, 3).map((event) => {
const firstPhoto = getFirstPhoto(event.photos)
@ -53,28 +55,59 @@ const UpcomingEvents: React.FC<UpcomingEventsProps> = ({ events, onEventClick })
<div
key={event.id}
onClick={() => 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' : ''}`}
>
<div className="flex-1 min-w-0">
<h4 className="text-sm font-medium text-gray-900 dark:text-white">{event.name}</h4>
<p className="text-xs text-gray-600 dark:text-gray-400 mt-1">
{currentLocalDate(event.date, currentLocale || 'tr')} - {event.place}
</p>
<div className="flex items-start gap-4">
<div className="flex-1 min-w-0">
<h4 className="text-base font-semibold text-gray-900 dark:text-white">{event.name}</h4>
<p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
{currentLocalDate(event.date, currentLocale || 'tr')} - {event.place}
</p>
{event.description && (
<p className="text-sm text-gray-600 dark:text-gray-400 line-clamp-2 mt-1">
{event.description}
</p>
)}
<div className="flex items-center gap-2 mt-3 text-xs text-gray-500 dark:text-gray-400">
<Avatar
size={24}
shape="circle"
src={AVATAR_URL(event.user.id, event.user.tenantId)}
/>
<span>{event.user.fullName}</span>
<span></span>
<span>{dayjs(event.date).fromNow()}</span>
{event.likes > 0 && (
<>
<span></span>
<span className={`flex items-center gap-1 ${event.isLiked ? 'text-red-500' : ''}`}>
<FaHeart className="w-3 h-3" />
{event.likes}
</span>
</>
)}
</div>
</div>
{firstPhoto && (
<img
src={photoSrc(firstPhoto)}
alt={event.name}
className="w-14 h-14 rounded-lg object-cover flex-shrink-0"
/>
)}
</div>
{firstPhoto && (
<img
src={photoSrc(firstPhoto)}
alt={event.name}
className="w-14 h-14 rounded-lg object-cover flex-shrink-0"
/>
)}
</div>
)
})
) : (
<p className="text-sm text-gray-500 dark:text-gray-400 text-center py-4">
{translate('::App.Platform.Intranet.Widgets.UpcomingEvents.NoEvent')}
</p>
<div className="text-center py-12">
<div className="inline-flex items-center justify-center w-16 h-16 bg-gray-100 dark:bg-gray-700 rounded-full mb-4">
<FaCalendarAlt className="w-8 h-8 text-gray-400" />
</div>
<p className="text-sm text-gray-500 dark:text-gray-400">
{translate('::App.Platform.Intranet.Widgets.UpcomingEvents.NoEvent')}
</p>
</div>
)}
</div>
</div>

View file

@ -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<LogLine[]>([])
const [status, setStatus] = useState<MigrationStatus>('idle')
const [pollCountdown, setPollCountdown] = useState(0)
const [dbExists, setDbExists] = useState<boolean | null>(null)
const logEndRef = useRef<HTMLDivElement>(null)
const pollTimerRef = useRef<ReturnType<typeof setTimeout> | 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 */}
<div className="flex items-center justify-between px-5 py-4 bg-gray-700 border-t border-gray-600">
<div className="text-xs text-gray-400">
{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 = () => {
</div>
<div className="flex gap-3">
{(status === 'idle' || status === 'error') && (
{(status === 'idle' || status === 'error') && !dbExists && (
<button
onClick={startMigration}
className="px-4 py-2 bg-indigo-600 hover:bg-indigo-500 text-white text-sm font-medium rounded-lg transition-colors"