Genel eksikler düzenlemeler

This commit is contained in:
Sedat Öztürk 2026-06-19 22:23:38 +03:00
parent 71b976b96b
commit 947d21ecb7
13 changed files with 659 additions and 418 deletions

View file

@ -693,8 +693,8 @@ public class IntranetAppService : PlatformAppService, IIntranetAppService
if (!Directory.Exists(fullPath)) if (!Directory.Exists(fullPath))
{ {
Logger.LogWarning($"Directory not found: {fullPath}"); Logger.LogInformation($"Directory not found. Creating: {fullPath}");
return items; Directory.CreateDirectory(fullPath);
} }
var files = Directory.GetFiles(fullPath); var files = Directory.GetFiles(fullPath);

View file

@ -6648,6 +6648,12 @@
"tr": "Intranet", "tr": "Intranet",
"en": "Intranet" "en": "Intranet"
}, },
{
"resourceName": "Platform",
"key": "App.Intranet.SocialLike",
"tr": "Sosyal Beğeniler",
"en": "Social Likes"
},
{ {
"resourceName": "Platform", "resourceName": "Platform",
"key": "App.Intranet.SocialComment", "key": "App.Intranet.SocialComment",

View file

@ -3423,7 +3423,7 @@ public class ListFormSeeder_Administration : IDataSeedContributor, ITransientDep
DeleteCommand = DefaultDeleteCommand(nameof(TableNameEnum.SurveyQuestion)), DeleteCommand = DefaultDeleteCommand(nameof(TableNameEnum.SurveyQuestion)),
DeleteFieldsDefaultValueJson = DefaultDeleteFieldsDefaultValueJson(), DeleteFieldsDefaultValueJson = DefaultDeleteFieldsDefaultValueJson(),
PagerOptionJson = DefaultPagerOptionJson, PagerOptionJson = DefaultPagerOptionJson,
EditingOptionJson = DefaultEditingOptionJson(listFormName, 500, 400, true, true, true, true, false, true), EditingOptionJson = DefaultEditingOptionJson(listFormName, 700, 400, true, true, true, true, false, true),
InsertFieldsDefaultValueJson = DefaultInsertFieldsDefaultValueJson(), InsertFieldsDefaultValueJson = DefaultInsertFieldsDefaultValueJson(),
EditingFormJson = JsonSerializer.Serialize(new List<EditingFormDto>() EditingFormJson = JsonSerializer.Serialize(new List<EditingFormDto>()
{ {
@ -3738,8 +3738,7 @@ public class ListFormSeeder_Administration : IDataSeedContributor, ITransientDep
new() { new() {
Order=1, ColCount=1, ColSpan=1, ItemType="group", Items =[ Order=1, ColCount=1, ColSpan=1, ItemType="group", Items =[
new EditingFormItemDto { Order = 1, DataField = "UserId", ColSpan=1, EditorType2 = EditorTypes.dxSelectBox, EditorOptions=EditorOptionValues.ShowClearButton }, new EditingFormItemDto { Order = 1, DataField = "UserId", ColSpan=1, EditorType2 = EditorTypes.dxSelectBox, EditorOptions=EditorOptionValues.ShowClearButton },
new EditingFormItemDto { Order = 2, DataField = "SubmissionTime", ColSpan=1, EditorType2 = EditorTypes.dxTextBox }, new EditingFormItemDto { Order = 2, DataField = "SubmissionTime", ColSpan=1, EditorType2 = EditorTypes.dxDateBox },
new EditingFormItemDto { Order = 3, DataField = "CreationTime", ColSpan=1, EditorType2 = EditorTypes.dxDateBox }
]} ]}
}), }),
SubFormsJson = JsonSerializer.Serialize(new List<dynamic>() { SubFormsJson = JsonSerializer.Serialize(new List<dynamic>() {
@ -3756,10 +3755,6 @@ public class ListFormSeeder_Administration : IDataSeedContributor, ITransientDep
} }
} }
}), }),
FormFieldsDefaultValueJson = JsonSerializer.Serialize(new FieldsDefaultValue[]
{
new() { FieldName = "CreationTime", FieldDbType = DbType.DateTime, Value = "@NOW", CustomValueType = FieldCustomValueTypeEnum.CustomKey }
})
}, autoSave: true }, autoSave: true
); );
@ -3819,22 +3814,6 @@ public class ListFormSeeder_Administration : IDataSeedContributor, ITransientDep
PermissionJson = DefaultFieldPermissionJson(listForm.Name), PermissionJson = DefaultFieldPermissionJson(listForm.Name),
PivotSettingsJson = DefaultPivotSettingsJson PivotSettingsJson = DefaultPivotSettingsJson
}, },
new() {
ListFormCode = listForm.ListFormCode,
CultureName = LanguageCodes.En,
SourceDbType = DbType.DateTime,
FieldName = "CreationTime",
CaptionName = "App.Listform.ListformField.CreationTime",
Width = 0,
ListOrderNo = 4,
Visible = true,
IsActive = true,
AllowSearch = true,
ValidationRuleJson = DefaultValidationRuleRequiredJson,
ColumnCustomizationJson = DefaultColumnCustomizationJson,
PermissionJson = DefaultFieldPermissionJson(listForm.Name),
PivotSettingsJson = DefaultPivotSettingsJson
},
new() { new() {
ListFormCode = listForm.ListFormCode, ListFormCode = listForm.ListFormCode,
CultureName = LanguageCodes.En, CultureName = LanguageCodes.En,
@ -3842,7 +3821,7 @@ public class ListFormSeeder_Administration : IDataSeedContributor, ITransientDep
FieldName = "SurveyId", FieldName = "SurveyId",
CaptionName = "App.Listform.ListformField.SurveyId", CaptionName = "App.Listform.ListformField.SurveyId",
Width = 0, Width = 0,
ListOrderNo = 5, ListOrderNo = 4,
Visible = false, Visible = false,
IsActive = true, IsActive = true,
AllowSearch = true, AllowSearch = true,
@ -4053,6 +4032,18 @@ public class ListFormSeeder_Administration : IDataSeedContributor, ITransientDep
ChildFieldName = "SocialPostId" ChildFieldName = "SocialPostId"
} }
} }
},
new {
TabType = ListFormTabTypeEnum.List,
TabTitle = AppCodes.Intranet.SocialLike,
Code = AppCodes.Intranet.SocialLike,
Relation = new List<dynamic>() {
new {
ParentFieldName = "Id",
DbType = DbType.Guid,
ChildFieldName = "SocialPostId"
}
}
} }
}) })
} }
@ -4269,6 +4260,114 @@ public class ListFormSeeder_Administration : IDataSeedContributor, ITransientDep
} }
#endregion #endregion
#region Social Like
listFormName = AppCodes.Intranet.SocialLike;
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 = false,
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.SocialLike)),
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.SocialLike)),
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 = "UserId", ColSpan = 1, EditorType2=EditorTypes.dxSelectBox },
]}
}),
InsertFieldsDefaultValueJson = DefaultInsertFieldsDefaultValueJson(),
}
);
#region Social Like Fields
await _listFormFieldRepository.InsertManyAsync([
new() {
ListFormCode = listForm.ListFormCode,
CultureName = LanguageCodes.En,
SourceDbType = DbType.Guid,
FieldName = "Id",
CaptionName = "App.Listform.ListformField.Id",
Width = 0,
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 = 0,
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 = "SocialPostId",
CaptionName = "App.Listform.ListformField.SocialPostId",
Width = 0,
ListOrderNo = 3,
Visible = false,
IsActive = true,
AllowSearch = true,
ValidationRuleJson = DefaultValidationRuleRequiredJson,
ColumnCustomizationJson = DefaultColumnCustomizationJson,
PermissionJson = DefaultFieldPermissionJson(listForm.Name),
PivotSettingsJson = DefaultPivotSettingsJson
}
]);
#endregion
}
#endregion
#region EventType #region EventType
listFormName = AppCodes.Intranet.EventType; listFormName = AppCodes.Intranet.EventType;
if (!await _listFormRepository.AnyAsync(a => a.ListFormCode == listFormName)) if (!await _listFormRepository.AnyAsync(a => a.ListFormCode == listFormName))

View file

@ -121,6 +121,12 @@ public static class ListFormSeeder_DefaultJsons
{ {
IsPivot = true IsPivot = true
}); });
public static string DefaultGroupingJson(int groupIndex) => JsonSerializer.Serialize(new ColumnGroupingDto
{
AllowGrouping = true,
GroupIndex = groupIndex,
AutoExpandGroup = false
});
public static readonly string DefaultValidationRuleRequiredJson = JsonSerializer.Serialize(new ValidationRuleDto[] public static readonly string DefaultValidationRuleRequiredJson = JsonSerializer.Serialize(new ValidationRuleDto[]
{ {
new() { Type = Enum.GetName(UiColumnValidationRuleTypeEnum.required) } new() { Type = Enum.GetName(UiColumnValidationRuleTypeEnum.required) }

View file

@ -3297,7 +3297,8 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
ValidationRuleJson = DefaultValidationRuleRequiredJson, ValidationRuleJson = DefaultValidationRuleRequiredJson,
ColumnCustomizationJson = DefaultColumnCustomizationJson, ColumnCustomizationJson = DefaultColumnCustomizationJson,
PermissionJson = DefaultFieldPermissionJson(listForm.Name), PermissionJson = DefaultFieldPermissionJson(listForm.Name),
PivotSettingsJson = DefaultPivotSettingsJson PivotSettingsJson = DefaultPivotSettingsJson,
GroupingJson = DefaultGroupingJson(1)
}, },
new ListFormField new ListFormField
{ {
@ -3321,7 +3322,8 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
ValidationRuleJson = DefaultValidationRuleRequiredJson, ValidationRuleJson = DefaultValidationRuleRequiredJson,
ColumnCustomizationJson = DefaultColumnCustomizationJson, ColumnCustomizationJson = DefaultColumnCustomizationJson,
PermissionJson = DefaultFieldPermissionJson(listForm.Name), PermissionJson = DefaultFieldPermissionJson(listForm.Name),
PivotSettingsJson = DefaultPivotSettingsJson PivotSettingsJson = DefaultPivotSettingsJson,
GroupingJson = DefaultGroupingJson(2)
}, },
new ListFormField new ListFormField
{ {

View file

@ -3934,7 +3934,6 @@
"MultiTenancySide": 3, "MultiTenancySide": 3,
"MenuGroup": "Erp|Kurs" "MenuGroup": "Erp|Kurs"
}, },
{ {
"GroupName": "App.Administration", "GroupName": "App.Administration",
"Name": "App.Intranet.Announcement.AnnouncementComment", "Name": "App.Intranet.Announcement.AnnouncementComment",
@ -3989,7 +3988,6 @@
"MultiTenancySide": 3, "MultiTenancySide": 3,
"MenuGroup": "Erp|Kurs" "MenuGroup": "Erp|Kurs"
}, },
{ {
"GroupName": "App.Administration", "GroupName": "App.Administration",
"Name": "App.Intranet.SocialPost", "Name": "App.Intranet.SocialPost",
@ -4116,6 +4114,60 @@
"MultiTenancySide": 3, "MultiTenancySide": 3,
"MenuGroup": "Erp|Kurs" "MenuGroup": "Erp|Kurs"
}, },
{
"GroupName": "App.Administration",
"Name": "App.Intranet.SocialLike",
"ParentName": "App.Intranet",
"DisplayName": "App.Intranet.SocialLike",
"IsEnabled": true,
"MultiTenancySide": 3,
"MenuGroup": "Erp|Kurs"
},
{
"GroupName": "App.Administration",
"Name": "App.Intranet.SocialLike.Create",
"ParentName": "App.Intranet.SocialLike",
"DisplayName": "Create",
"IsEnabled": true,
"MultiTenancySide": 3,
"MenuGroup": "Erp|Kurs"
},
{
"GroupName": "App.Administration",
"Name": "App.Intranet.SocialLike.Update",
"ParentName": "App.Intranet.SocialLike",
"DisplayName": "Update",
"IsEnabled": true,
"MultiTenancySide": 3,
"MenuGroup": "Erp|Kurs"
},
{
"GroupName": "App.Administration",
"Name": "App.Intranet.SocialLike.Delete",
"ParentName": "App.Intranet.SocialLike",
"DisplayName": "Delete",
"IsEnabled": true,
"MultiTenancySide": 3,
"MenuGroup": "Erp|Kurs"
},
{
"GroupName": "App.Administration",
"Name": "App.Intranet.SocialLike.Export",
"ParentName": "App.Intranet.SocialLike",
"DisplayName": "Export",
"IsEnabled": true,
"MultiTenancySide": 3,
"MenuGroup": "Erp|Kurs"
},
{
"GroupName": "App.Administration",
"Name": "App.Intranet.SocialLike.Import",
"ParentName": "App.Intranet.SocialLike",
"DisplayName": "Import",
"IsEnabled": true,
"MultiTenancySide": 3,
"MenuGroup": "Erp|Kurs"
},
{ {
"GroupName": "App.Administration", "GroupName": "App.Administration",
"Name": "App.Intranet.Survey", "Name": "App.Intranet.Survey",
@ -4459,4 +4511,4 @@
"MenuGroup": "Kurs" "MenuGroup": "Kurs"
} }
] ]
} }

View file

@ -338,6 +338,7 @@ public static class SeedConsts
public const string SocialPost = Default + ".SocialPost"; public const string SocialPost = Default + ".SocialPost";
public const string SocialComment = Default + ".SocialComment"; public const string SocialComment = Default + ".SocialComment";
public const string SocialLike = Default + ".SocialLike";
public const string Events = Default + ".Events"; public const string Events = Default + ".Events";
public const string EventType = Events + ".EventType"; public const string EventType = Events + ".EventType";

View file

@ -1174,6 +1174,8 @@ public class PlatformDbContext :
b.ToTable(TableNameResolver.GetFullTableName(nameof(TableNameEnum.AnnouncementLike)), Prefix.DbSchema); b.ToTable(TableNameResolver.GetFullTableName(nameof(TableNameEnum.AnnouncementLike)), Prefix.DbSchema);
b.ConfigureByConvention(); b.ConfigureByConvention();
b.HasIndex(x => new { x.AnnouncementId, x.UserId }).IsUnique().HasFilter("[IsDeleted] = 0");
b.HasOne(x => x.Announcement) b.HasOne(x => x.Announcement)
.WithMany(x => x.AnnouncementLikes) .WithMany(x => x.AnnouncementLikes)
.HasForeignKey(x => x.AnnouncementId) .HasForeignKey(x => x.AnnouncementId)
@ -1229,6 +1231,8 @@ public class PlatformDbContext :
b.Property(x => x.SubmissionTime).IsRequired(); b.Property(x => x.SubmissionTime).IsRequired();
b.HasIndex(x => new { x.SurveyId, x.UserId }).IsUnique().HasFilter("[IsDeleted] = 0");
b.HasOne(x => x.Survey) b.HasOne(x => x.Survey)
.WithMany(x => x.SurveyResponses) .WithMany(x => x.SurveyResponses)
.HasForeignKey(x => x.SurveyId) .HasForeignKey(x => x.SurveyId)
@ -1326,6 +1330,8 @@ public class PlatformDbContext :
b.ToTable(TableNameResolver.GetFullTableName(nameof(TableNameEnum.SocialLike)), Prefix.DbSchema); b.ToTable(TableNameResolver.GetFullTableName(nameof(TableNameEnum.SocialLike)), Prefix.DbSchema);
b.ConfigureByConvention(); b.ConfigureByConvention();
b.HasIndex(x => new { x.SocialPostId, x.UserId }).IsUnique().HasFilter("[IsDeleted] = 0");
b.HasOne(x => x.SocialPost) b.HasOne(x => x.SocialPost)
.WithMany(x => x.Likes) .WithMany(x => x.Likes)
.HasForeignKey(x => x.SocialPostId) .HasForeignKey(x => x.SocialPostId)
@ -1396,6 +1402,8 @@ public class PlatformDbContext :
b.ToTable(TableNameResolver.GetFullTableName(nameof(TableNameEnum.EventLike)), Prefix.DbSchema); b.ToTable(TableNameResolver.GetFullTableName(nameof(TableNameEnum.EventLike)), Prefix.DbSchema);
b.ConfigureByConvention(); b.ConfigureByConvention();
b.HasIndex(x => new { x.EventId, x.UserId }).IsUnique().HasFilter("[IsDeleted] = 0");
b.HasOne(x => x.Event) b.HasOne(x => x.Event)
.WithMany(x => x.EventLikes) .WithMany(x => x.EventLikes)
.HasForeignKey(x => x.EventId) .HasForeignKey(x => x.EventId)

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("20260616123136_Initial")] [Migration("20260619185755_Initial")]
partial class Initial partial class Initial
{ {
/// <inheritdoc /> /// <inheritdoc />
@ -912,7 +912,9 @@ namespace Sozsoft.Platform.Migrations
b.HasKey("Id"); b.HasKey("Id");
b.HasIndex("AnnouncementId"); b.HasIndex("AnnouncementId", "UserId")
.IsUnique()
.HasFilter("[IsDeleted] = 0");
b.ToTable("Adm_T_AnnouncementLike", (string)null); b.ToTable("Adm_T_AnnouncementLike", (string)null);
}); });
@ -2512,7 +2514,9 @@ namespace Sozsoft.Platform.Migrations
b.HasKey("Id"); b.HasKey("Id");
b.HasIndex("EventId"); b.HasIndex("EventId", "UserId")
.IsUnique()
.HasFilter("[IsDeleted] = 0");
b.ToTable("Adm_T_EventLike", (string)null); b.ToTable("Adm_T_EventLike", (string)null);
}); });
@ -4885,7 +4889,9 @@ namespace Sozsoft.Platform.Migrations
b.HasKey("Id"); b.HasKey("Id");
b.HasIndex("SocialPostId"); b.HasIndex("SocialPostId", "UserId")
.IsUnique()
.HasFilter("[IsDeleted] = 0");
b.ToTable("Adm_T_SocialLike", (string)null); b.ToTable("Adm_T_SocialLike", (string)null);
}); });
@ -5464,7 +5470,9 @@ namespace Sozsoft.Platform.Migrations
b.HasKey("Id"); b.HasKey("Id");
b.HasIndex("SurveyId"); b.HasIndex("SurveyId", "UserId")
.IsUnique()
.HasFilter("[IsDeleted] = 0");
b.ToTable("Adm_T_SurveyResponse", (string)null); b.ToTable("Adm_T_SurveyResponse", (string)null);
}); });

View file

@ -3626,9 +3626,11 @@ namespace Sozsoft.Platform.Migrations
column: "AnnouncementId"); column: "AnnouncementId");
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "IX_Adm_T_AnnouncementLike_AnnouncementId", name: "IX_Adm_T_AnnouncementLike_AnnouncementId_UserId",
table: "Adm_T_AnnouncementLike", table: "Adm_T_AnnouncementLike",
column: "AnnouncementId"); columns: new[] { "AnnouncementId", "UserId" },
unique: true,
filter: "[IsDeleted] = 0");
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "IX_Adm_T_Department_TenantId_Name", name: "IX_Adm_T_Department_TenantId_Name",
@ -3667,9 +3669,11 @@ namespace Sozsoft.Platform.Migrations
column: "EventId"); column: "EventId");
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "IX_Adm_T_EventLike_EventId", name: "IX_Adm_T_EventLike_EventId_UserId",
table: "Adm_T_EventLike", table: "Adm_T_EventLike",
column: "EventId"); columns: new[] { "EventId", "UserId" },
unique: true,
filter: "[IsDeleted] = 0");
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "IX_Adm_T_EventType_TenantId_Name", name: "IX_Adm_T_EventType_TenantId_Name",
@ -3753,9 +3757,11 @@ namespace Sozsoft.Platform.Migrations
column: "SocialPostId"); column: "SocialPostId");
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "IX_Adm_T_SocialLike_SocialPostId", name: "IX_Adm_T_SocialLike_SocialPostId_UserId",
table: "Adm_T_SocialLike", table: "Adm_T_SocialLike",
column: "SocialPostId"); columns: new[] { "SocialPostId", "UserId" },
unique: true,
filter: "[IsDeleted] = 0");
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "IX_Adm_T_SocialLocation_SocialPostId", name: "IX_Adm_T_SocialLocation_SocialPostId",
@ -3802,9 +3808,11 @@ namespace Sozsoft.Platform.Migrations
column: "QuestionId"); column: "QuestionId");
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "IX_Adm_T_SurveyResponse_SurveyId", name: "IX_Adm_T_SurveyResponse_SurveyId_UserId",
table: "Adm_T_SurveyResponse", table: "Adm_T_SurveyResponse",
column: "SurveyId"); columns: new[] { "SurveyId", "UserId" },
unique: true,
filter: "[IsDeleted] = 0");
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "IX_Adm_T_Videoroom_TenantId_BranchId_Name", name: "IX_Adm_T_Videoroom_TenantId_BranchId_Name",

View file

@ -909,7 +909,9 @@ namespace Sozsoft.Platform.Migrations
b.HasKey("Id"); b.HasKey("Id");
b.HasIndex("AnnouncementId"); b.HasIndex("AnnouncementId", "UserId")
.IsUnique()
.HasFilter("[IsDeleted] = 0");
b.ToTable("Adm_T_AnnouncementLike", (string)null); b.ToTable("Adm_T_AnnouncementLike", (string)null);
}); });
@ -2509,7 +2511,9 @@ namespace Sozsoft.Platform.Migrations
b.HasKey("Id"); b.HasKey("Id");
b.HasIndex("EventId"); b.HasIndex("EventId", "UserId")
.IsUnique()
.HasFilter("[IsDeleted] = 0");
b.ToTable("Adm_T_EventLike", (string)null); b.ToTable("Adm_T_EventLike", (string)null);
}); });
@ -4882,7 +4886,9 @@ namespace Sozsoft.Platform.Migrations
b.HasKey("Id"); b.HasKey("Id");
b.HasIndex("SocialPostId"); b.HasIndex("SocialPostId", "UserId")
.IsUnique()
.HasFilter("[IsDeleted] = 0");
b.ToTable("Adm_T_SocialLike", (string)null); b.ToTable("Adm_T_SocialLike", (string)null);
}); });
@ -5461,7 +5467,9 @@ namespace Sozsoft.Platform.Migrations
b.HasKey("Id"); b.HasKey("Id");
b.HasIndex("SurveyId"); b.HasIndex("SurveyId", "UserId")
.IsUnique()
.HasFilter("[IsDeleted] = 0");
b.ToTable("Adm_T_SurveyResponse", (string)null); b.ToTable("Adm_T_SurveyResponse", (string)null);
}); });

View file

@ -27,6 +27,7 @@ import {
FaSearch, FaSearch,
FaTimes, FaTimes,
FaTrash, FaTrash,
FaUsers,
} from 'react-icons/fa' } from 'react-icons/fa'
import { IoCheckmarkDone, IoChatbubbleEllipsesOutline, IoSend } from 'react-icons/io5' import { IoCheckmarkDone, IoChatbubbleEllipsesOutline, IoSend } from 'react-icons/io5'
import { useLocalization } from '@/utils/hooks/useLocalization' import { useLocalization } from '@/utils/hooks/useLocalization'
@ -315,413 +316,451 @@ const MessengerWidget = () => {
if (!auth.session.signedIn) return null if (!auth.session.signedIn) return null
const panelClassName = maximized const panelClassName = maximized
? 'pointer-events-auto fixed inset-2 overflow-hidden rounded-lg border border-gray-200 bg-white shadow-2xl dark:border-gray-700 dark:bg-gray-800 sm:inset-4' ? 'pointer-events-auto fixed inset-2 z-[51] overflow-hidden rounded-xl border border-blue-300 bg-slate-50 shadow-[0_24px_80px_rgba(15,23,42,0.45)] dark:border-blue-800 dark:bg-slate-900 sm:inset-4'
: 'pointer-events-auto fixed inset-x-2 bottom-20 top-3 overflow-hidden rounded-lg border border-gray-200 bg-white shadow-2xl dark:border-gray-700 dark:bg-gray-800 sm:absolute sm:inset-auto sm:bottom-16 sm:right-0 sm:h-[min(70vh,640px)] sm:w-[min(78vw,800px)]' : 'pointer-events-auto fixed inset-x-2 bottom-20 top-3 z-[51] overflow-hidden rounded-xl border border-blue-300 bg-slate-50 shadow-[0_24px_80px_rgba(15,23,42,0.45)] dark:border-blue-800 dark:bg-slate-900 sm:inset-auto sm:bottom-20 sm:right-5 sm:h-[min(75vh,680px)] sm:w-[min(78vw,800px)]'
const headerActionClassName = 'text-white hover:bg-white/15 hover:text-white active:bg-white/25'
return ( return (
<div className="pointer-events-none fixed bottom-5 right-5 z-50 h-14 w-14"> <>
{open && ( {open && (
<div className={panelClassName}> <>
<div className="flex h-full min-h-0"> <button
<aside type="button"
className={`min-h-0 w-full shrink-0 flex-col border-gray-200 bg-gray-50 dark:border-gray-700 dark:bg-gray-900 sm:flex sm:w-72 sm:border-r ${ aria-label={translate('::MessengerWidget.Minimize')}
selectedIds.length > 0 ? 'hidden' : 'flex' className="fixed inset-0 z-50 cursor-default bg-slate-950/25 backdrop-blur-[1px] dark:bg-black/55"
}`} onClick={closeWidget}
> />
<div className="flex h-16 items-center justify-between border-b border-gray-200 px-4 dark:border-gray-700"> <div className={panelClassName} role="dialog" aria-modal="true">
<div className="flex min-w-0 items-center gap-3"> <div className="flex h-full min-h-0">
<Avatar <aside
size={36} className={`min-h-0 w-full shrink-0 flex-col bg-slate-100 dark:bg-slate-950 sm:flex sm:w-72 ${
shape="circle" selectedIds.length > 0 ? 'hidden' : 'flex'
src={auth.user.avatar || AVATAR_URL(auth.user.id, tenantId)} }`}
/> >
<div className="min-w-0"> <div
<div className="truncate text-sm font-semibold text-gray-900 dark:text-gray-100"> className={
{auth.user.name || auth.user.userName || translate('::MessengerWidget.Title')} 'flex h-16 items-center justify-between border-b border-r-2 border-blue-500/40 bg-blue-700 px-4 text-white dark:bg-blue-950'
</div> }
<div className="flex items-center gap-1 text-xs text-gray-500">
<span
className={`h-2 w-2 rounded-full ${connected ? 'bg-emerald-500' : 'bg-gray-400'}`}
/>
{connected
? translate('::MessengerWidget.Connected')
: translate('::MessengerWidget.Connecting')}
</div>
</div>
</div>
<div className="flex shrink-0 items-center gap-1 sm:hidden">
<Tooltip
title={
maximized
? translate('::MessengerWidget.Minimize')
: translate('::MessengerWidget.Maximize')
}
>
<Button
size="xs"
variant="plain"
icon={maximized ? <FaCompress /> : <FaExpand />}
onClick={() => setMaximized((value) => !value)}
/>
</Tooltip>
<Button size="xs" variant="plain" icon={<FaTimes />} onClick={closeWidget} />
</div>
</div>
<div className="space-y-2 p-3">
<Input
size="sm"
value={filter}
prefix={<FaSearch />}
placeholder={translate('::MessengerWidget.SearchPerson')}
onChange={(event) => setFilter(event.target.value)}
/>
<Button
size="xs"
variant={multiSelect ? 'twoTone' : 'default'}
block
onClick={toggleMultiSelect}
> >
{multiSelect <div className="flex min-w-0 items-center gap-3">
? translate('::MessengerWidget.MultiSelectEnabled')
: translate('::MessengerWidget.MultiSelect')}
</Button>
</div>
<div className="min-h-0 flex-1 overflow-y-auto px-2 pb-2">
{contacts.map((contact) => {
const active = selectedIds.includes(contact.id)
const contactUnread = unreadByContact[contact.id] || 0
return (
<button
key={contact.id}
type="button"
onClick={() => toggleContact(contact.id)}
disabled={!contact.isOnline}
className={`mb-1 flex w-full items-center gap-3 rounded-md px-2 py-2 text-left transition ${
active
? 'bg-emerald-50 text-emerald-700 dark:bg-emerald-500/15 dark:text-emerald-100'
: contact.isOnline
? 'hover:bg-white dark:hover:bg-gray-800'
: 'cursor-not-allowed opacity-60'
}`}
>
<Avatar
size={36}
shape="circle"
src={AVATAR_URL(contact.id, contact.tenantId || tenantId)}
/>
<span className="min-w-0 flex-1">
<span className="block truncate text-sm font-medium">
{contact.fullName}
</span>
<span
className={`mt-1 inline-flex items-center gap-1 text-xs font-medium ${
contact.isOnline ? 'text-emerald-600' : 'text-red-500'
}`}
>
<span
className={`h-2 w-2 rounded-full ${
contact.isOnline ? 'bg-emerald-500' : 'bg-red-500'
}`}
/>
{contact.isOnline
? translate('::MessengerWidget.Online')
: translate('::MessengerWidget.Offline')}
</span>
</span>
{contactUnread > 0 && (
<span className="shrink-0 rounded-full bg-red-500 px-2 py-0.5 text-xs font-semibold text-white">
{contactUnread > 99 ? '99+' : contactUnread}
</span>
)}
{active && contactUnread === 0 && (
<IoCheckmarkDone className="shrink-0 text-lg" />
)}
</button>
)
})}
</div>
</aside>
<main
className={`min-w-0 flex-1 flex-col ${
selectedIds.length === 0 ? 'hidden sm:flex' : 'flex'
}`}
>
<div className="flex h-16 items-center justify-between border-b border-gray-200 px-4 dark:border-gray-700">
<div className="flex min-w-0 items-center gap-3">
<Button
size="xs"
variant="plain"
icon={<FaArrowLeft />}
className="shrink-0 sm:hidden"
onClick={() => setSelectedIds([])}
/>
{selectedContacts.length === 1 && (
<Avatar <Avatar
size={36} size={36}
shape="circle" shape="circle"
src={AVATAR_URL( src={auth.user.avatar || AVATAR_URL(auth.user.id, tenantId)}
selectedContacts[0].id,
selectedContacts[0].tenantId || tenantId,
)}
/> />
)} <div className="min-w-0">
{selectedContacts.length > 1 && ( <div className="truncate text-sm font-semibold text-white">
<Avatar size={36} shape="circle" className="bg-emerald-100 text-emerald-700"> {auth.user.name ||
{selectedContacts.length} auth.user.userName ||
</Avatar> translate('::MessengerWidget.Title')}
)} </div>
<div className="min-w-0 flex-1"> <div className="flex items-center gap-1 text-xs text-blue-100">
<div className="truncate text-sm font-semibold text-gray-900 dark:text-gray-100"> <span
{selectedContacts.length === 1 className={`h-2 w-2 rounded-full ${connected ? 'bg-emerald-500' : 'bg-gray-400'}`}
? selectedContacts[0].fullName />
: selectedContacts.length > 1 {connected
? `${selectedContacts.length} ${translate('::MessengerWidget.PeopleSelected')}` ? translate('::MessengerWidget.Connected')
: translate('::MessengerWidget.Chat')} : translate('::MessengerWidget.Connecting')}
</div> </div>
<div className="truncate text-xs text-gray-500">
{selectedContacts.length === 1
? `${selectedContacts[0].isOnline ? translate('::MessengerWidget.Online') : translate('::MessengerWidget.Offline')} - ${
selectedContacts[0].email || selectedContacts[0].userName
}`
: selectedContacts.length > 1
? canSendToSelectedContacts
? translate('::MessengerWidget.GroupMessage')
: translate('::MessengerWidget.OfflineUserSelected')
: translate('::MessengerWidget.SelectPersonToSendMessage')}
</div> </div>
</div> </div>
<div className="flex shrink-0 items-center gap-1 sm:hidden">
<Tooltip
title={
maximized
? translate('::MessengerWidget.Minimize')
: translate('::MessengerWidget.Maximize')
}
>
<Button
size="xs"
variant="plain"
icon={maximized ? <FaCompress /> : <FaExpand />}
className={headerActionClassName}
onClick={() => setMaximized((value) => !value)}
/>
</Tooltip>
<Button
size="xs"
variant="plain"
icon={<FaTimes />}
className={headerActionClassName}
onClick={closeWidget}
/>
</div>
</div> </div>
<div className="flex shrink-0 items-center gap-1">
<Tooltip <div className="flex items-center gap-2 p-3">
<div className="min-w-0 flex-[6]">
<Input
size="sm"
value={filter}
prefix={<FaSearch />}
placeholder={translate('::MessengerWidget.SearchPerson')}
onChange={(event) => setFilter(event.target.value)}
/>
</div>
<Button
size="sm"
variant={multiSelect ? 'twoTone' : 'default'}
icon={<FaUsers />}
className="flex-1 shrink-0"
aria-label={translate('::MessengerWidget.MultiSelect')}
onClick={toggleMultiSelect}
title={ title={
maximized multiSelect
? translate('::MessengerWidget.Minimize') ? translate('::MessengerWidget.MultiSelect')
: translate('::MessengerWidget.Maximize') : translate('::MessengerWidget.MultiSelectEnabled')
} }
> />
</div>
<div className="min-h-0 flex-1 overflow-y-auto px-2 pb-2">
{contacts.map((contact) => {
const active = selectedIds.includes(contact.id)
const contactUnread = unreadByContact[contact.id] || 0
return (
<button
key={contact.id}
type="button"
onClick={() => toggleContact(contact.id)}
disabled={!contact.isOnline}
className={`mb-1 flex w-full items-center gap-3 rounded-md px-2 py-2 text-left transition ${
active
? 'bg-emerald-50 text-emerald-700 dark:bg-emerald-500/15 dark:text-emerald-100'
: contact.isOnline
? 'hover:bg-white dark:hover:bg-gray-800'
: 'cursor-not-allowed opacity-60'
}`}
>
<Avatar
size={36}
shape="circle"
src={AVATAR_URL(contact.id, contact.tenantId || tenantId)}
/>
<span className="min-w-0 flex-1">
<span className="block truncate text-sm font-medium">
{contact.fullName}
</span>
<span
className={`inline-flex items-center gap-1 text-xs font-medium ${
contact.isOnline ? 'text-emerald-600' : 'text-red-500'
}`}
>
<span
className={`h-2 w-2 rounded-full ${
contact.isOnline ? 'bg-emerald-500' : 'bg-red-500'
}`}
/>
{contact.isOnline
? translate('::MessengerWidget.Online')
: translate('::MessengerWidget.Offline')}
</span>
</span>
{contactUnread > 0 && (
<span className="shrink-0 rounded-full bg-red-500 px-2 py-0.5 text-xs font-semibold text-white">
{contactUnread > 99 ? '99+' : contactUnread}
</span>
)}
{active && contactUnread === 0 && (
<IoCheckmarkDone className="shrink-0 text-lg" />
)}
</button>
)
})}
</div>
</aside>
<main
className={`min-w-0 flex-1 flex-col ${
selectedIds.length === 0 ? 'hidden sm:flex' : 'flex'
}`}
>
<div
className={
'flex h-16 items-center justify-between border-b border-blue-500/40 bg-blue-700 px-4 text-white dark:bg-blue-950'
}
>
<div className="flex min-w-0 items-center gap-3">
<Button
size="xs"
variant="plain"
icon={<FaArrowLeft />}
className={`shrink-0 sm:hidden ${headerActionClassName}`}
onClick={() => setSelectedIds([])}
/>
{selectedContacts.length === 1 && (
<Avatar
size={36}
shape="circle"
src={AVATAR_URL(
selectedContacts[0].id,
selectedContacts[0].tenantId || tenantId,
)}
/>
)}
{selectedContacts.length > 1 && (
<Avatar size={36} shape="circle" className="bg-emerald-100 text-emerald-700">
{selectedContacts.length}
</Avatar>
)}
<div className="min-w-0 flex-1">
<div className="truncate text-sm font-semibold text-white">
{selectedContacts.length === 1
? selectedContacts[0].fullName
: selectedContacts.length > 1
? `${selectedContacts.length} ${translate('::MessengerWidget.PeopleSelected')}`
: translate('::MessengerWidget.Chat')}
</div>
<div className="truncate text-xs text-blue-100">
{selectedContacts.length === 1
? `${selectedContacts[0].isOnline ? translate('::MessengerWidget.Online') : translate('::MessengerWidget.Offline')} - ${
selectedContacts[0].email || selectedContacts[0].userName
}`
: selectedContacts.length > 1
? canSendToSelectedContacts
? translate('::MessengerWidget.GroupMessage')
: translate('::MessengerWidget.OfflineUserSelected')
: translate('::MessengerWidget.SelectPersonToSendMessage')}
</div>
</div>
</div>
<div className="flex shrink-0 items-center gap-1">
<Button <Button
size="xs" size="xs"
variant="plain" variant="plain"
icon={maximized ? <FaCompress /> : <FaExpand />} icon={maximized ? <FaCompress /> : <FaExpand />}
className={headerActionClassName}
onClick={() => setMaximized((value) => !value)} onClick={() => setMaximized((value) => !value)}
title={
maximized
? translate('::MessengerWidget.Minimize')
: translate('::MessengerWidget.Maximize')
}
/> />
</Tooltip> <Button
<Button size="xs" variant="plain" icon={<FaTimes />} onClick={closeWidget} /> size="xs"
variant="plain"
icon={<FaTimes />}
className={headerActionClassName}
onClick={closeWidget}
/>
</div>
</div> </div>
</div>
<div className="flex-1 overflow-y-auto bg-white px-4 py-3 dark:bg-gray-800"> <div className="flex-1 overflow-y-auto bg-slate-50 px-4 py-3 dark:bg-slate-900">
{visibleMessages.length === 0 && ( {visibleMessages.length === 0 && (
<div className="flex h-full items-center justify-center text-sm text-gray-400"> <div className="flex h-full items-center justify-center text-sm text-gray-400">
{translate('::MessengerWidget.NoMessagesYet')} {translate('::MessengerWidget.NoMessagesYet')}
</div>
)}
{visibleMessages.map((message) => {
const own = message.senderId === auth.user.id
const deletable = canDeleteMessage(message, auth.user.id)
return (
<div
key={message.id}
className={`mb-3 flex ${own ? 'justify-end' : 'justify-start'}`}
>
<div
className={`group flex max-w-[76%] items-start gap-1.5 ${own ? 'flex-row-reverse' : ''}`}
>
<div
className={`rounded-lg px-3 py-2 shadow-sm ${
own
? 'bg-emerald-600 text-white'
: 'bg-gray-100 text-gray-900 dark:bg-gray-700 dark:text-gray-100'
}`}
>
<div className="mb-1 flex items-center gap-1.5 text-xs font-semibold">
<Avatar
size={20}
shape="circle"
src={
own
? auth.user.avatar || AVATAR_URL(auth.user.id, tenantId)
: AVATAR_URL(message.senderId, message.tenantId || tenantId)
}
/>
<span className="max-w-[160px] truncate">
{own
? auth.user.name ||
auth.user.userName ||
translate('::MessengerWidget.Me')
: message.senderName}
</span>
</div>
{message.text && (
<div className="whitespace-pre-wrap text-sm leading-5">
{message.text}
</div>
)}
{message.attachments.length > 0 && (
<div className="mt-2 space-y-1">
{message.attachments.map((file) => (
<a
key={file.savedFileName}
href={file.url}
target="_blank"
rel="noreferrer"
className={`block rounded-md px-2 py-1 text-xs ${
own
? 'bg-emerald-700 text-white'
: 'bg-white text-gray-700 dark:bg-gray-800 dark:text-gray-100'
}`}
>
<span className="block truncate">{file.fileName}</span>
<span className="opacity-75">
{formatFileSize(file.fileSize)}
</span>
</a>
))}
</div>
)}
<div
className={`mt-1 flex items-center gap-2 text-[11px] ${
own ? 'justify-end text-emerald-100' : 'justify-start text-gray-500'
}`}
>
<span>{dayjs(message.sentAt).format('HH:mm')}</span>
</div>
</div>
{own && deletable && (
<button
type="button"
title={translate('::MessengerWidget.DeleteMessage')}
className="mt-1 flex h-5 w-5 shrink-0 items-center justify-center rounded-full text-[10px] text-gray-400 opacity-0 transition hover:bg-red-500 hover:text-white group-hover:opacity-100 focus:opacity-100"
onClick={() => deleteMessage(message.id)}
>
<FaTrash />
</button>
)}
</div>
</div>
)
})}
<div ref={messagesEndRef} />
</div>
{selectedIds.length > 0 && attachments.length > 0 && (
<div className="flex flex-wrap gap-2 border-t border-gray-200 px-4 py-2 dark:border-gray-700">
{attachments.map((file) => (
<span
key={file.savedFileName}
className="inline-flex max-w-[220px] items-center gap-2 rounded-md bg-gray-100 px-2 py-1 text-xs dark:bg-gray-700"
>
<span className="truncate">{file.fileName}</span>
<button
type="button"
onClick={() =>
setAttachments((prev) =>
prev.filter((item) => item.savedFileName !== file.savedFileName),
)
}
>
<FaTrash />
</button>
</span>
))}
</div> </div>
)} )}
{visibleMessages.map((message) => { {selectedIds.length > 0 && (
const own = message.senderId === auth.user.id <div className="relative border-t border-gray-200 p-3 dark:border-gray-700">
const deletable = canDeleteMessage(message, auth.user.id) {showEmoji && (
return ( <div className="absolute bottom-16 left-2 z-10 sm:left-10">
<div <EmojiPicker
key={message.id} onEmojiClick={onEmojiClick}
className={`mb-3 flex ${own ? 'justify-end' : 'justify-start'}`} width="min(320px, calc(100vw - 32px))"
> height={380}
<div />
className={`group flex max-w-[76%] items-start gap-1.5 ${own ? 'flex-row-reverse' : ''}`}
>
<div
className={`rounded-lg px-3 py-2 shadow-sm ${
own
? 'bg-emerald-600 text-white'
: 'bg-gray-100 text-gray-900 dark:bg-gray-700 dark:text-gray-100'
}`}
>
<div className="mb-1 flex items-center gap-1.5 text-xs font-semibold">
<Avatar
size={20}
shape="circle"
src={
own
? auth.user.avatar || AVATAR_URL(auth.user.id, tenantId)
: AVATAR_URL(message.senderId, message.tenantId || tenantId)
}
/>
<span className="max-w-[160px] truncate">
{own
? auth.user.name ||
auth.user.userName ||
translate('::MessengerWidget.Me')
: message.senderName}
</span>
</div>
{message.text && (
<div className="whitespace-pre-wrap text-sm leading-5">
{message.text}
</div>
)}
{message.attachments.length > 0 && (
<div className="mt-2 space-y-1">
{message.attachments.map((file) => (
<a
key={file.savedFileName}
href={file.url}
target="_blank"
rel="noreferrer"
className={`block rounded-md px-2 py-1 text-xs ${
own
? 'bg-emerald-700 text-white'
: 'bg-white text-gray-700 dark:bg-gray-800 dark:text-gray-100'
}`}
>
<span className="block truncate">{file.fileName}</span>
<span className="opacity-75">
{formatFileSize(file.fileSize)}
</span>
</a>
))}
</div>
)}
<div
className={`mt-1 flex items-center gap-2 text-[11px] ${
own ? 'justify-end text-emerald-100' : 'justify-start text-gray-500'
}`}
>
<span>{dayjs(message.sentAt).format('HH:mm')}</span>
</div>
</div>
{own && deletable && (
<button
type="button"
title={translate('::MessengerWidget.DeleteMessage')}
className="mt-1 flex h-5 w-5 shrink-0 items-center justify-center rounded-full text-[10px] text-gray-400 opacity-0 transition hover:bg-red-500 hover:text-white group-hover:opacity-100 focus:opacity-100"
onClick={() => deleteMessage(message.id)}
>
<FaTrash />
</button>
)}
</div> </div>
</div> )}
) <div className="flex min-h-[52px] items-center gap-2 rounded-md border border-gray-300 bg-white px-2 py-1.5 focus-within:border-emerald-500 focus-within:ring-1 focus-within:ring-emerald-500 dark:border-gray-600 dark:bg-gray-800">
})} <input
<div ref={messagesEndRef} /> ref={fileInputRef}
</div> type="file"
multiple
{selectedIds.length > 0 && attachments.length > 0 && ( className="hidden"
<div className="flex flex-wrap gap-2 border-t border-gray-200 px-4 py-2 dark:border-gray-700"> onChange={(event) => handleFiles(event.target.files)}
{attachments.map((file) => (
<span
key={file.savedFileName}
className="inline-flex max-w-[220px] items-center gap-2 rounded-md bg-gray-100 px-2 py-1 text-xs dark:bg-gray-700"
>
<span className="truncate">{file.fileName}</span>
<button
type="button"
onClick={() =>
setAttachments((prev) =>
prev.filter((item) => item.savedFileName !== file.savedFileName),
)
}
>
<FaTrash />
</button>
</span>
))}
</div>
)}
{selectedIds.length > 0 && (
<div className="relative border-t border-gray-200 p-3 dark:border-gray-700">
{showEmoji && (
<div className="absolute bottom-16 left-2 z-10 sm:left-10">
<EmojiPicker
onEmojiClick={onEmojiClick}
width="min(320px, calc(100vw - 32px))"
height={380}
/> />
</div> <Tooltip title={translate('::MessengerWidget.AddFile')}>
)} <Button
<div className="flex min-h-[52px] items-center gap-2 rounded-md border border-gray-300 bg-white px-2 py-1.5 focus-within:border-emerald-500 focus-within:ring-1 focus-within:ring-emerald-500 dark:border-gray-600 dark:bg-gray-800"> type="button"
<input size="sm"
ref={fileInputRef} variant="plain"
type="file" loading={uploading}
multiple icon={<FaPaperclip />}
className="hidden" className="shrink-0"
onChange={(event) => handleFiles(event.target.files)} disabled={!canSendToSelectedContacts}
/> onClick={() => fileInputRef.current?.click()}
<Tooltip title={translate('::MessengerWidget.AddFile')}> />
<Button </Tooltip>
type="button" <Tooltip title={translate('::MessengerWidget.Emoji')}>
size="sm" <Button
variant="plain" type="button"
loading={uploading} size="sm"
icon={<FaPaperclip />} variant="plain"
className="shrink-0" icon={<FaRegSmile />}
className="shrink-0"
onClick={() => setShowEmoji((value) => !value)}
/>
</Tooltip>
<textarea
ref={messageInputRef}
value={text}
placeholder={translate('::MessengerWidget.WriteMessage')}
rows={1}
autoFocus
disabled={!canSendToSelectedContacts} disabled={!canSendToSelectedContacts}
onClick={() => fileInputRef.current?.click()} className="min-h-[36px] flex-1 resize-none border-0 bg-transparent px-2 py-2 text-sm text-gray-900 outline-none placeholder:text-gray-400 focus:ring-0 dark:text-gray-100"
onChange={(event) => setText(event.target.value)}
onKeyDown={(event) => {
if (event.key === 'Enter' && !event.shiftKey) {
event.preventDefault()
sendMessage()
}
}}
/> />
</Tooltip>
<Tooltip title={translate('::MessengerWidget.Emoji')}>
<Button <Button
type="button" type="button"
size="sm" size="sm"
variant="plain" variant="solid"
icon={<FaRegSmile />} icon={<IoSend />}
className="shrink-0" className="shrink-0"
onClick={() => setShowEmoji((value) => !value)} disabled={
/> !canSendToSelectedContacts || (!text.trim() && attachments.length === 0)
</Tooltip>
<textarea
ref={messageInputRef}
value={text}
placeholder={translate('::MessengerWidget.WriteMessage')}
rows={1}
autoFocus
disabled={!canSendToSelectedContacts}
className="min-h-[36px] flex-1 resize-none border-0 bg-transparent px-2 py-2 text-sm text-gray-900 outline-none placeholder:text-gray-400 focus:ring-0 dark:text-gray-100"
onChange={(event) => setText(event.target.value)}
onKeyDown={(event) => {
if (event.key === 'Enter' && !event.shiftKey) {
event.preventDefault()
sendMessage()
} }
}} onClick={sendMessage}
/> />
<Button </div>
type="button"
size="sm"
variant="solid"
icon={<IoSend />}
className="shrink-0"
disabled={
!canSendToSelectedContacts || (!text.trim() && attachments.length === 0)
}
onClick={sendMessage}
/>
</div> </div>
</div> )}
)} </main>
</main> </div>
</div> </div>
</div> </>
)} )}
<Tooltip title={translate('::MessengerWidget.Title')}> <div className="fixed bottom-5 right-5 z-[52]">
<button <Tooltip title={translate('::MessengerWidget.Title')}>
type="button" <button
onClick={() => (open ? closeWidget() : setOpen(true))} type="button"
className="pointer-events-auto relative flex h-12 w-12 items-center justify-center rounded-full bg-emerald-600 text-2xl text-white shadow-xl transition hover:bg-emerald-700" onClick={() => (open ? closeWidget() : setOpen(true))}
> className="pointer-events-auto relative flex h-12 w-12 items-center justify-center rounded-full bg-emerald-600 text-2xl text-white shadow-xl transition hover:bg-emerald-700"
<IoChatbubbleEllipsesOutline /> >
{unread > 0 && ( <IoChatbubbleEllipsesOutline />
<span className="absolute -right-1 -top-1 flex min-h-[20px] min-w-[20px] items-center justify-center rounded-full bg-red-500 px-1.5 text-xs font-semibold leading-none text-white ring-2 ring-white dark:ring-gray-800"> {unread > 0 && (
{unread > 99 ? '99+' : unread} <span className="absolute -right-1 -top-1 flex min-h-[20px] min-w-[20px] items-center justify-center rounded-full bg-red-500 px-1.5 text-xs font-semibold leading-none text-white ring-2 ring-white dark:ring-gray-800">
</span> {unread > 99 ? '99+' : unread}
)} </span>
</button> )}
</Tooltip> </button>
</div> </Tooltip>
</div>
</>
) )
} }

View file

@ -282,6 +282,10 @@ div.dialog-after-open > div.dialog-content.maximized {
margin-left: 2px !important; margin-left: 2px !important;
} }
.dx-datagrid-rowsview .dx-datagrid-group-row-container {
background-color: transparent !important;
}
@media (max-width: 767px), (pointer: coarse) { @media (max-width: 767px), (pointer: coarse) {
.mobile-edit-popup.dx-overlay-wrapper, .mobile-edit-popup.dx-overlay-wrapper,
.dx-overlay-wrapper.mobile-edit-popup { .dx-overlay-wrapper.mobile-edit-popup {