diff --git a/api/src/Sozsoft.Platform.Application.Contracts/Messenger/MessengerDtos.cs b/api/src/Sozsoft.Platform.Application.Contracts/Messenger/MessengerDtos.cs index 972ddeb..e5e4471 100644 --- a/api/src/Sozsoft.Platform.Application.Contracts/Messenger/MessengerDtos.cs +++ b/api/src/Sozsoft.Platform.Application.Contracts/Messenger/MessengerDtos.cs @@ -18,6 +18,7 @@ public class MessengerContactDto public string FullName { get; set; } = string.Empty; public string? Email { get; set; } public bool IsActive { get; set; } + public bool IsOnline { get; set; } } public class MessengerAttachmentDto diff --git a/api/src/Sozsoft.Platform.Application.Contracts/OrgChart/OrgChartDto.cs b/api/src/Sozsoft.Platform.Application.Contracts/OrgChart/OrgChartDto.cs index 13efa91..a11f385 100644 --- a/api/src/Sozsoft.Platform.Application.Contracts/OrgChart/OrgChartDto.cs +++ b/api/src/Sozsoft.Platform.Application.Contracts/OrgChart/OrgChartDto.cs @@ -9,6 +9,7 @@ public class OrgChartUserDto public string FullName { get; set; } public string Email { get; set; } public string UserName { get; set; } + public string PhoneNumber { get; set; } } public class OrgChartNodeDto @@ -21,5 +22,8 @@ public class OrgChartNodeDto public Guid? DepartmentId { get; set; } public string DepartmentName { get; set; } + public Guid? JobPositionId { get; set; } + public string JobPositionName { get; set; } + public List Users { get; set; } = []; } diff --git a/api/src/Sozsoft.Platform.Application/Messenger/MessengerAppService.cs b/api/src/Sozsoft.Platform.Application/Messenger/MessengerAppService.cs index 73f4761..79ef625 100644 --- a/api/src/Sozsoft.Platform.Application/Messenger/MessengerAppService.cs +++ b/api/src/Sozsoft.Platform.Application/Messenger/MessengerAppService.cs @@ -26,6 +26,7 @@ public class MessengerAppService : ApplicationService private readonly IRepository _userRepository; private readonly IRepository _conversationRepository; private readonly IRepository _messageRepository; + private readonly IIdentitySessionRepository _identitySessionRepository; private readonly BlobManager _blobManager; private readonly IConfiguration _configuration; @@ -33,12 +34,14 @@ public class MessengerAppService : ApplicationService IRepository userRepository, IRepository conversationRepository, IRepository messageRepository, + IIdentitySessionRepository identitySessionRepository, BlobManager blobManager, IConfiguration configuration) { _userRepository = userRepository; _conversationRepository = conversationRepository; _messageRepository = messageRepository; + _identitySessionRepository = identitySessionRepository; _blobManager = blobManager; _configuration = configuration; } @@ -71,6 +74,8 @@ public class MessengerAppService : ApplicationService .Take(100) ); + var onlineUserIds = await GetOnlineUserIdsAsync(users.Select(user => user.Id)); + return users.Select(user => { var fullName = $"{user.Name} {user.Surname}".Trim(); @@ -83,7 +88,8 @@ public class MessengerAppService : ApplicationService Surname = user.Surname, FullName = fullName.IsNullOrWhiteSpace() ? user.UserName : fullName, Email = user.Email, - IsActive = user.IsActive + IsActive = user.IsActive, + IsOnline = onlineUserIds.Contains(user.Id) }; }).ToList(); } @@ -238,6 +244,12 @@ public class MessengerAppService : ApplicationService throw new UserFriendlyException("Bu tenant icinde gecersiz alici var."); } + var onlineRecipientIds = await GetOnlineUserIdsAsync(recipientIds); + if (onlineRecipientIds.Count != recipientIds.Count) + { + throw new UserFriendlyException("Sadece online kullanicilara mesaj gonderebilirsiniz."); + } + MessengerConversation? conversation = null; if (input.ConversationId.HasValue) { @@ -406,6 +418,25 @@ public class MessengerAppService : ApplicationService user.IsActive); } + private async Task> GetOnlineUserIdsAsync(IEnumerable userIds) + { + var normalizedUserIds = userIds + .Where(id => id != Guid.Empty) + .Distinct() + .ToList(); + + if (normalizedUserIds.Count == 0) + { + return new HashSet(); + } + + var sessions = await _identitySessionRepository.GetListAsync(); + return sessions + .Where(session => normalizedUserIds.Contains(session.UserId)) + .Select(session => session.UserId) + .ToHashSet(); + } + private async Task FindConversationByParticipantKeyAsync(string participantKey) { var query = await _conversationRepository.GetQueryableAsync(); diff --git a/api/src/Sozsoft.Platform.Application/OrgChart/OrgChartAppService.cs b/api/src/Sozsoft.Platform.Application/OrgChart/OrgChartAppService.cs index f9f4d0d..84279cb 100644 --- a/api/src/Sozsoft.Platform.Application/OrgChart/OrgChartAppService.cs +++ b/api/src/Sozsoft.Platform.Application/OrgChart/OrgChartAppService.cs @@ -66,23 +66,31 @@ public class OrgChartAppService : PlatformAppService topPositionByDepartment[department.Id] = selectedTop; } - var nodes = departments.Select(d => new OrgChartNodeDto + var nodes = departments.Select(d => { - Id = d.Id, - Name = d.Name, - ParentId = d.ParentId, - Users = users - .Where(u => - topPositionByDepartment.TryGetValue(d.Id, out var topPosition) && - u.GetJobPositionId() == topPosition.Id) - .Select(u => new OrgChartUserDto - { - Id = u.Id, - FullName = u.GetFullName(), - Email = u.Email, - UserName = u.UserName, - }) - .ToList(), + topPositionByDepartment.TryGetValue(d.Id, out var topPosition); + + return new OrgChartNodeDto + { + Id = d.Id, + Name = d.Name, + ParentId = d.ParentId, + JobPositionId = topPosition?.Id, + JobPositionName = topPosition?.Name, + Users = users + .Where(u => + topPosition != null && + u.GetJobPositionId() == topPosition.Id) + .Select(u => new OrgChartUserDto + { + Id = u.Id, + FullName = u.GetFullName(), + Email = u.Email, + UserName = u.UserName, + PhoneNumber = u.PhoneNumber + }) + .ToList(), + }; }).ToList(); return nodes; @@ -104,6 +112,8 @@ public class OrgChartAppService : PlatformAppService ParentId = jp.ParentId, DepartmentId = jp.DepartmentId, DepartmentName = deptDict.TryGetValue(jp.DepartmentId, out var name) ? name : null, + JobPositionId = jp.Id, + JobPositionName = jp.Name, Users = users .Where(u => u.GetJobPositionId() == jp.Id) .Select(u => new OrgChartUserDto @@ -112,6 +122,7 @@ public class OrgChartAppService : PlatformAppService FullName = u.GetFullName(), Email = u.Email, UserName = u.UserName, + PhoneNumber = u.PhoneNumber }) .ToList(), }).ToList(); diff --git a/api/src/Sozsoft.Platform.EntityFrameworkCore/Seeds/TenantData.json b/api/src/Sozsoft.Platform.EntityFrameworkCore/Seeds/TenantData.json index 8c50fdf..3a8480e 100644 --- a/api/src/Sozsoft.Platform.EntityFrameworkCore/Seeds/TenantData.json +++ b/api/src/Sozsoft.Platform.EntityFrameworkCore/Seeds/TenantData.json @@ -1439,12 +1439,6 @@ "DepartmentName": "Yönetim", "Name": "Genel Müdür" }, - { - "Id": "a2b3c4d5-e6f7-4890-ab12-cd34ef56ab78", - "DepartmentName": "Yönetim", - "Name": "Genel Müdür Yardımcısı", - "ParentName": "Genel Müdür" - }, { "Id": "b7c8d9e0-f1a2-4b3c-8d9e-0f1a2b3c4d5e", "DepartmentName": "Muhasebe", diff --git a/ui/src/components/template/MessengerWidget.tsx b/ui/src/components/template/MessengerWidget.tsx index 2323e61..ebc5dc7 100644 --- a/ui/src/components/template/MessengerWidget.tsx +++ b/ui/src/components/template/MessengerWidget.tsx @@ -31,6 +31,7 @@ import { import { IoCheckmarkDone, IoChatbubbleEllipsesOutline, IoSend } from 'react-icons/io5' const MAX_UPLOAD_SIZE = 10 * 1024 * 1024 +const CONTACT_REFRESH_INTERVAL = 30000 const formatFileSize = (size: number) => { if (size < 1024 * 1024) return `${Math.max(1, Math.round(size / 1024))} KB` @@ -99,9 +100,25 @@ const MessengerWidget = () => { useEffect(() => { if (!open) return - getMessengerContacts(filter) - .then((response) => setContacts(response.data)) - .catch(() => setContacts([])) + let active = true + + const loadContacts = () => { + getMessengerContacts(filter) + .then((response) => { + if (active) setContacts(response.data) + }) + .catch(() => { + if (active) setContacts([]) + }) + } + + loadContacts() + const timer = window.setInterval(loadContacts, CONTACT_REFRESH_INTERVAL) + + return () => { + active = false + window.clearInterval(timer) + } }, [filter, open]) useEffect(() => { @@ -135,6 +152,9 @@ const MessengerWidget = () => { [contacts, selectedIds], ) + const canSendToSelectedContacts = + selectedContacts.length > 0 && selectedContacts.every((contact) => contact.isOnline) + const visibleMessages = useMemo(() => { if (selectedIds.length === 0) return [] @@ -146,6 +166,14 @@ const MessengerWidget = () => { }, [auth.user.id, messages, selectedIds]) const toggleContact = (id: string) => { + const contact = contacts.find((item) => item.id === id) + if (!contact?.isOnline) { + toast.push(, { + placement: 'top-end', + }) + return + } + setUnreadByContact((current) => { if (!current[id]) return current @@ -210,6 +238,12 @@ const MessengerWidget = () => { const sendMessage = async () => { const trimmedText = text.trim() if (selectedIds.length === 0 || (!trimmedText && attachments.length === 0)) return + if (!canSendToSelectedContacts) { + toast.push(, { + placement: 'top-end', + }) + return + } try { await messengerSignalR.sendMessage({ @@ -311,16 +345,31 @@ const MessengerWidget = () => { 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' - : 'hover:bg-white dark:hover:bg-gray-800' + : contact.isOnline + ? 'hover:bg-white dark:hover:bg-gray-800' + : 'cursor-not-allowed opacity-60' }`} > {contact.fullName} {contact.email || contact.userName} + + + {contact.isOnline ? 'Online' : 'Offline'} + {contactUnread > 0 && ( @@ -370,9 +419,13 @@ const MessengerWidget = () => {
{selectedContacts.length === 1 - ? selectedContacts[0].email || selectedContacts[0].userName + ? `${selectedContacts[0].isOnline ? 'Online' : 'Offline'} - ${ + selectedContacts[0].email || selectedContacts[0].userName + }` : selectedContacts.length > 1 - ? 'Toplu mesaj' + ? canSendToSelectedContacts + ? 'Toplu mesaj' + : 'Offline kullanici secili' : 'Mesaj göndermek için kişi seçin'}
@@ -518,6 +571,7 @@ const MessengerWidget = () => { loading={uploading} icon={} className="shrink-0" + disabled={!canSendToSelectedContacts} onClick={() => fileInputRef.current?.click()} /> @@ -537,6 +591,7 @@ const MessengerWidget = () => { placeholder="Mesaj yazın" 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) => { @@ -552,7 +607,7 @@ const MessengerWidget = () => { variant="solid" icon={} className="shrink-0" - disabled={!text.trim() && attachments.length === 0} + disabled={!canSendToSelectedContacts || (!text.trim() && attachments.length === 0)} onClick={sendMessage} /> diff --git a/ui/src/services/messenger.service.ts b/ui/src/services/messenger.service.ts index e0ce60e..87fcde4 100644 --- a/ui/src/services/messenger.service.ts +++ b/ui/src/services/messenger.service.ts @@ -9,6 +9,7 @@ export interface MessengerContactDto { fullName: string email?: string isActive: boolean + isOnline: boolean } export interface MessengerAttachmentDto { diff --git a/ui/src/services/orgChart.service.ts b/ui/src/services/orgChart.service.ts index 4fea9d4..0fe39d1 100644 --- a/ui/src/services/orgChart.service.ts +++ b/ui/src/services/orgChart.service.ts @@ -5,6 +5,7 @@ export interface OrgChartUserDto { fullName: string email: string userName: string + phoneNumber?: string } export interface OrgChartNodeDto { @@ -13,6 +14,8 @@ export interface OrgChartNodeDto { parentId?: string departmentId?: string departmentName?: string + jobPositionId?: string + jobPositionName?: string users: OrgChartUserDto[] } diff --git a/ui/src/views/admin/hr/OrgChart.tsx b/ui/src/views/admin/hr/OrgChart.tsx index b20af0b..47c6449 100644 --- a/ui/src/views/admin/hr/OrgChart.tsx +++ b/ui/src/views/admin/hr/OrgChart.tsx @@ -342,12 +342,12 @@ function buildEdges(tree: TreeNode[]): EdgeLink[] { function UserAvatar({ user, tenantId, - title, + jobPosition, department, }: { user: OrgChartUserDto tenantId?: string - title: string + jobPosition?: string department?: string }) { const [showUserCard, setShowUserCard] = useState(false) @@ -381,8 +381,9 @@ function UserAvatar({ user={{ id: user.id, name: user.fullName || user.userName, - title, + title: jobPosition || '', email: user.email, + phoneNumber: user.phoneNumber, department, tenantId: tenantId ?? '', }} @@ -518,16 +519,7 @@ function OrgChartNode({ )} - {/* Department label for job positions */} - {mode === 'jobPosition' && node.departmentName && ( -
- - {node.departmentName} - -
- )} + {/* Users section */} {showUsers && ( @@ -539,7 +531,7 @@ function OrgChartNode({ diff --git a/ui/src/views/intranet/SocialWall/PostItem.tsx b/ui/src/views/intranet/SocialWall/PostItem.tsx index b081f24..e8b1a60 100644 --- a/ui/src/views/intranet/SocialWall/PostItem.tsx +++ b/ui/src/views/intranet/SocialWall/PostItem.tsx @@ -386,6 +386,7 @@ const PostItem: React.FC = ({ post, onLike, onComment, onDelete, id: comment.user?.id || '', name: comment.user?.fullName || '', title: comment.user?.jobPositions?.[0]?.name || '', + phoneNumber: comment.user.phoneNumber, tenantId: comment.user?.tenantId || '', }} position="bottom" diff --git a/ui/src/views/intranet/SocialWall/UserProfileCard.tsx b/ui/src/views/intranet/SocialWall/UserProfileCard.tsx index f21ab4f..b9d60c3 100644 --- a/ui/src/views/intranet/SocialWall/UserProfileCard.tsx +++ b/ui/src/views/intranet/SocialWall/UserProfileCard.tsx @@ -12,7 +12,6 @@ interface UserProfileCardProps { email?: string phoneNumber?: string department?: string - location?: string tenantId: string } position?: 'top' | 'bottom' @@ -76,13 +75,6 @@ const UserProfileCard: React.FC = ({ user, position = 'bot {user.department} )} - - {user.location && ( -
- - {user.location} -
- )} {/* Arrow indicator */}