>}>
{currentTab === 'general' && }
+ {currentTab === 'employeeInfo' && }
+ {currentTab === 'leave' && }
+ {currentTab === 'timesheet' && }
+ {currentTab === 'performance' && }
{currentTab === 'password' && }
{currentTab === 'notificationSettings' && }
diff --git a/ui/src/views/admin/profile/components/MyEmployeeInfo.tsx b/ui/src/views/admin/profile/components/MyEmployeeInfo.tsx
new file mode 100644
index 00000000..c5d844fd
--- /dev/null
+++ b/ui/src/views/admin/profile/components/MyEmployeeInfo.tsx
@@ -0,0 +1,210 @@
+import { Input } from '@/components/ui'
+import { FormContainer, FormItem } from '@/components/ui/Form'
+import { ProfileDto } from '@/proxy/account/models'
+import { getProfile } from '@/services/account.service'
+import { useLocalization } from '@/utils/hooks/useLocalization'
+import { getActiveCulture, LOCALE_DATE_FORMAT, toTwoLetterCulture } from '@/utils/localeFormat'
+import { maritalStatuses } from '@/views/admin/user-management/userLookups'
+import dayjs from 'dayjs'
+import { useEffect, useState } from 'react'
+import {
+ FaBriefcase,
+ FaCalendarAlt,
+ FaEnvelope,
+ FaHashtag,
+ FaIdCard,
+ FaMapMarkerAlt,
+ FaPhone,
+ FaUserTie,
+} from 'react-icons/fa'
+import type { IconType } from 'react-icons'
+
+type Row = [label: string, value?: string, icon?: IconType]
+
+const formatDate = (value?: string) =>
+ value
+ ? dayjs(value).locale(toTwoLetterCulture(getActiveCulture())).format(LOCALE_DATE_FORMAT)
+ : undefined
+
+/** Genel sekmesiyle aynı düzen: başlık + devre dışı alanlar; çalışan değeri yalnızca görür. */
+const InfoColumn = ({ title, rows }: { title: string; rows: Row[] }) => (
+
+
{title}
+ {rows.map(([label, value, Icon]) => (
+
+ }
+ value={value ?? ''}
+ />
+
+ ))}
+
+)
+
+/**
+ * Kullanıcının İK'nın tuttuğu kendi bilgileri (salt okunur). Değerler `my-profile` cevabının ek
+ * alanlarındandır (`PlatformProfileAppService.ProfileExtraProperties`); Genel sekmesinde gösterilen
+ * alanlar burada tekrarlanmaz.
+ */
+const MyEmployeeInfo = () => {
+ const { translate } = useLocalization()
+ const [profile, setProfile] = useState
()
+
+ useEffect(() => {
+ let active = true
+ getProfile()
+ .then((response) => active && setProfile(response.data))
+ .catch(() => active && setProfile(undefined))
+ return () => {
+ active = false
+ }
+ }, [])
+
+ if (!profile) return null
+
+ // Sunucu ek alan adlarını kayıttaki gibi (PascalCase) yazar; serileştirici ayarına karşı iki yazım da okunur.
+ const extra = (key: string) =>
+ profile.extraProperties?.[key] ??
+ profile.extraProperties?.[`${key.charAt(0).toLowerCase()}${key.slice(1)}`]
+ const text = (key: string) => {
+ const value = extra(key)
+ return typeof value === 'string' || typeof value === 'number' ? String(value) : undefined
+ }
+ const list = (key: string) => {
+ const value = extra(key)
+ return Array.isArray(value) ? value.join(', ') : undefined
+ }
+ const info = {
+ departmentName: text('DepartmentName'),
+ jobPositionName: text('JobPositionName'),
+ branches: list('Branches'),
+ roles: list('Roles'),
+ workHour: text('WorkHour'),
+ registrationNo: text('RegistrationNo'),
+ sskNo: text('SskNo'),
+ pdksCardNo: text('PdksCardNo'),
+ employmentType: text('EmploymentType'),
+ contractType: text('ContractType'),
+ hireDate: text('HireDate'),
+ seniorityStartDate: text('SeniorityStartDate'),
+ probationEndDate: text('ProbationEndDate'),
+ contractEndDate: text('ContractEndDate'),
+ secondaryEmail: text('SecondaryEmail'),
+ secondaryPhoneNumber: text('SecondaryPhoneNumber'),
+ identityNumber: text('IdentityNumber'),
+ birthPlace: text('BirthPlace'),
+ birthDate: text('BirthDate'),
+ fatherName: text('FatherName'),
+ motherName: text('MotherName'),
+ maritalStatus: text('MaritalStatus'),
+ marriageDate: text('MarriageDate'),
+ militaryStatus: text('MilitaryStatus'),
+ militaryPostponeEndDate: text('MilitaryPostponeEndDate'),
+ drivingLicenseClass: text('DrivingLicenseClass'),
+ drivingLicenseNo: text('DrivingLicenseNo'),
+ drivingLicenseExpiryDate: text('DrivingLicenseExpiryDate'),
+ }
+
+ const t = (key: string) => translate(`::${key}`)
+ const user = (key: string) => t(`Abp.Identity.User.UserInformation.${key}`)
+ const staticLookup = (value?: string, prefix = '') =>
+ value ? t(`App.StaticLookup.${prefix}${value}`) : undefined
+ const maritalKey = maritalStatuses.find(([value]) => value === info.maritalStatus)?.[1]
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
+
+export default MyEmployeeInfo
diff --git a/ui/src/views/admin/profile/components/MyLeave.tsx b/ui/src/views/admin/profile/components/MyLeave.tsx
new file mode 100644
index 00000000..e4822943
--- /dev/null
+++ b/ui/src/views/admin/profile/components/MyLeave.tsx
@@ -0,0 +1,104 @@
+import { useLocalization } from '@/utils/hooks/useLocalization'
+import { usePermission } from '@/utils/hooks/usePermission'
+import { FaCalendarCheck, FaUmbrellaBeach } from 'react-icons/fa'
+import { ProfileCard, ProfileLinkButton, ReadOnlyTable, StatusBadge } from './ProfileSections'
+import { useMyRows } from './profileData'
+
+const REQUESTS = 'App.Hr.MyLeaveRequest'
+const BALANCE = 'App.Hr.MyLeaveBalance'
+
+const formatDays = (value: number) => (Number.isInteger(value) ? String(value) : value.toFixed(1))
+
+/**
+ * İzinlerim: bakiye ve talepler salt okunur gösterilir. Çalışan yalnızca yeni talep açar; talebin
+ * düzeltilmesi, silinmesi ve kararı onay sürecinde ve İK'dadır (Hr_S_LeaveRequestGuard).
+ */
+const MyLeave = () => {
+ const { translate } = useLocalization()
+ const { checkPermission } = usePermission()
+ const requests = useMyRows(REQUESTS)
+ const balance = useMyRows(BALANCE)
+
+ const sum = (field: string) =>
+ balance.rows.reduce((total, row) => total + Number(row[field] ?? 0), 0)
+ const remaining = sum('RemainingDays')
+ const stats: [string, number, string][] = [
+ ['EntitledDays', sum('EntitledDays'), 'text-gray-900 dark:text-gray-100'],
+ ['UsedDays', sum('UsedDays'), 'text-gray-900 dark:text-gray-100'],
+ ['PendingDays', sum('PendingDays'), 'text-orange-500'],
+ ['RemainingDays', remaining, remaining < 0 ? 'text-red-600' : 'text-teal-600'],
+ ]
+
+ return (
+
+ {balance.allowed && (
+
}
+ >
+
+ {stats.map(([field, value, color]) => (
+
+
{formatDays(value)}
+
+ {translate(`::App.Listform.ListformField.${field}`)}
+
+
+ ))}
+
+
+
+ )}
+
+ {requests.allowed && (
+
}
+ action={
+ checkPermission(`${REQUESTS}.Create`) && (
+
+ {translate('::App.DashboardWidget.Leave.CreateRequest')}
+
+ )
+ }
+ >
+
+ String(b.StartDate).localeCompare(String(a.StartDate)),
+ )}
+ columns={[
+ { field: 'RequestNo' },
+ { field: 'StartDate', date: true },
+ { field: 'EndDate', date: true },
+ { field: 'Days' },
+ {
+ field: 'ApprovalStatus',
+ render: (row) => ,
+ },
+ { field: 'ApprovalDate', date: true },
+ { field: 'ApprovalDescription' },
+ ]}
+ />
+
+ )}
+
+ )
+}
+
+export default MyLeave
diff --git a/ui/src/views/admin/profile/components/MyPerformance.tsx b/ui/src/views/admin/profile/components/MyPerformance.tsx
new file mode 100644
index 00000000..1ff0fdaa
--- /dev/null
+++ b/ui/src/views/admin/profile/components/MyPerformance.tsx
@@ -0,0 +1,90 @@
+import { useLocalization } from '@/utils/hooks/useLocalization'
+import { ReactNode } from 'react'
+import { FaBullseye, FaChartLine, FaComments, FaSeedling } from 'react-icons/fa'
+import { ProfileCard, ProfileColumn, ReadOnlyTable, StatusBadge } from './ProfileSections'
+import { useMyRows } from './profileData'
+
+const Section = ({
+ listFormCode,
+ icon,
+ columns,
+}: {
+ listFormCode: string
+ icon: ReactNode
+ columns: ProfileColumn[]
+}) => {
+ const { translate } = useLocalization()
+ const { allowed, rows, loading } = useMyRows(listFormCode)
+
+ if (!allowed) return null
+
+ return (
+
+
+
+ )
+}
+
+const status = (field: string, caption?: string): ProfileColumn => ({
+ field,
+ caption,
+ render: (row) => ,
+})
+
+/**
+ * Performansım: değerlendirme, hedef, gelişim planı ve 360 talepleri salt okunur. Öz değerlendirme
+ * ve 360 puanlaması süreç ekranlarında (Intranet panosundaki widget'lardan) yapılır.
+ */
+const MyPerformance = () => (
+
+ }
+ columns={[
+ status('Status', 'ReviewStatus'),
+ { field: 'GoalScore' },
+ { field: 'CompetencyScore' },
+ { field: 'Feedback360Score' },
+ { field: 'OverallScore' },
+ { field: 'CalibratedScore' },
+ { field: 'SelfSubmittedDate', date: true },
+ ]}
+ />
+ }
+ columns={[
+ { field: 'Title', caption: 'GoalTitle' },
+ { field: 'GoalType' },
+ { field: 'TargetValue' },
+ { field: 'ActualValue' },
+ { field: 'Weight' },
+ { field: 'AchievementRate' },
+ ]}
+ />
+ }
+ columns={[
+ { field: 'Title', caption: 'ActionTitle' },
+ { field: 'ActionType' },
+ { field: 'StartDate', date: true },
+ { field: 'DueDate', date: true },
+ status('Status'),
+ { field: 'Progress' },
+ ]}
+ />
+ }
+ columns={[
+ { field: 'Relation' },
+ status('Status'),
+ { field: 'Score', caption: 'FeedbackScoreValue' },
+ { field: 'SubmittedDate', date: true },
+ ]}
+ />
+
+)
+
+export default MyPerformance
diff --git a/ui/src/views/admin/profile/components/MyTimesheet.tsx b/ui/src/views/admin/profile/components/MyTimesheet.tsx
new file mode 100644
index 00000000..b4ecdc18
--- /dev/null
+++ b/ui/src/views/admin/profile/components/MyTimesheet.tsx
@@ -0,0 +1,46 @@
+import { useLocalization } from '@/utils/hooks/useLocalization'
+import { FaClock } from 'react-icons/fa'
+import { ProfileCard, ReadOnlyTable, StatusBadge } from './ProfileSections'
+import { useMyRows } from './profileData'
+
+const TIMESHEET = 'App.Hr.MyTimesheet'
+
+/** Puantajım: aylık puantaj salt okunur; düzeltme İK'nın Puantaj ekranındadır. */
+const MyTimesheet = () => {
+ const { translate } = useLocalization()
+ const timesheet = useMyRows(TIMESHEET)
+
+ if (!timesheet.allowed) return null
+
+ const rows = [...timesheet.rows].sort(
+ (a, b) =>
+ Number(b.PeriodYear) - Number(a.PeriodYear) || Number(b.PeriodMonth) - Number(a.PeriodMonth),
+ )
+
+ return (
+ }
+ >
+ },
+ { field: 'WorkDays' },
+ { field: 'NetHours' },
+ { field: 'LateMinutes' },
+ { field: 'EarlyLeaveMinutes' },
+ { field: 'OvertimeHours' },
+ { field: 'LeaveDays' },
+ { field: 'SickDays' },
+ { field: 'HolidayDays' },
+ ]}
+ />
+
+ )
+}
+
+export default MyTimesheet
diff --git a/ui/src/views/admin/profile/components/ProfileSections.tsx b/ui/src/views/admin/profile/components/ProfileSections.tsx
new file mode 100644
index 00000000..ca87f3a2
--- /dev/null
+++ b/ui/src/views/admin/profile/components/ProfileSections.tsx
@@ -0,0 +1,114 @@
+import Table from '@/components/ui/Table'
+import TBody from '@/components/ui/Table/TBody'
+import Td from '@/components/ui/Table/Td'
+import Th from '@/components/ui/Table/Th'
+import THead from '@/components/ui/Table/THead'
+import Tr from '@/components/ui/Table/Tr'
+import { useLocalization } from '@/utils/hooks/useLocalization'
+import { formatLocaleValue } from '@/utils/localeFormat'
+import { formatDay, ProfileRow } from './profileData'
+import { ReactNode } from 'react'
+import { Link } from 'react-router-dom'
+
+/** Intranet panosundaki widget'larla aynı çerçeve: başlık, isteğe bağlı aksiyon, içerik. */
+export const ProfileCard = ({
+ title,
+ icon,
+ action,
+ children,
+}: {
+ title: string
+ icon?: ReactNode
+ action?: ReactNode
+ children: ReactNode
+}) => (
+
+
+
+ {icon}
+
{title}
+
+ {action}
+
+
{children}
+
+)
+
+export const ProfileLinkButton = ({ to, children }: { to: string; children: ReactNode }) => (
+
+ {children}
+
+)
+
+export type ProfileColumn = {
+ field: string
+ /** `App.Listform.ListformField.*` altındaki başlık anahtarı; boşsa alan adı kullanılır. */
+ caption?: string
+ /** Tarih kolonu saat taşımaz; gün olarak gösterilir. */
+ date?: boolean
+ render?: (row: ProfileRow) => ReactNode
+}
+
+const formatCell = (row: ProfileRow, column: ProfileColumn) => {
+ if (column.render) return column.render(row)
+ const value = column.date ? formatDay(row[column.field]) : formatLocaleValue(row[column.field])
+ return value || '—'
+}
+
+/** Salt okunur satır tablosu; düzenleme, silme ve satır komutu yoktur. */
+export const ReadOnlyTable = ({
+ rows,
+ columns,
+ loading,
+}: {
+ rows: ProfileRow[]
+ columns: ProfileColumn[]
+ loading?: boolean
+}) => {
+ const { translate } = useLocalization()
+
+ if (loading) return ...
+
+ if (rows.length === 0) {
+ return (
+
+ {translate('::App.Platform.NoDataFound')}
+
+ )
+ }
+
+ return (
+
+
+
+ {columns.map((column) => (
+ |
+ {translate(`::App.Listform.ListformField.${column.caption ?? column.field}`)}
+ |
+ ))}
+
+
+
+ {rows.map((row, index) => (
+
+ {columns.map((column) => (
+ | {formatCell(row, column)} |
+ ))}
+
+ ))}
+
+
+ )
+}
+
+export const StatusBadge = ({ value }: { value: unknown }) =>
+ value ? (
+
+ {String(value)}
+
+ ) : (
+ <>—>
+ )
diff --git a/ui/src/views/admin/profile/components/profileData.ts b/ui/src/views/admin/profile/components/profileData.ts
new file mode 100644
index 00000000..4d77f481
--- /dev/null
+++ b/ui/src/views/admin/profile/components/profileData.ts
@@ -0,0 +1,42 @@
+import apiService from '@/services/api.service'
+import { usePermission } from '@/utils/hooks/usePermission'
+import { getActiveCulture, LOCALE_DATE_FORMAT, toTwoLetterCulture } from '@/utils/localeFormat'
+import dayjs from 'dayjs'
+import { useEffect, useState } from 'react'
+
+export type ProfileRow = Record
+
+/**
+ * Persona ekranının ("My" ListForm) satırlarını okur. Satır süzmesi (oturumdaki kullanıcı) ve
+ * yetki ListForm'da kalır; profil yalnızca gösterir. Yetki yoksa istek atılmaz.
+ */
+export const useMyRows = (listFormCode: string) => {
+ const { checkPermission } = usePermission()
+ const allowed = checkPermission(listFormCode)
+ const [rows, setRows] = useState([])
+ const [loading, setLoading] = useState(allowed)
+
+ useEffect(() => {
+ if (!allowed) return
+ let active = true
+ apiService
+ .fetchData<{ data?: ProfileRow[] }>({
+ method: 'GET',
+ url: '/api/app/list-form-select/select',
+ params: { listFormCode, take: 500 },
+ })
+ .then((response) => active && setRows(response.data?.data ?? []))
+ .catch(() => active && setRows([]))
+ .finally(() => active && setLoading(false))
+ return () => {
+ active = false
+ }
+ }, [allowed, listFormCode])
+
+ return { allowed, rows, loading }
+}
+
+export const formatDay = (value: unknown) =>
+ value
+ ? dayjs(String(value)).locale(toTwoLetterCulture(getActiveCulture())).format(LOCALE_DATE_FORMAT)
+ : ''
diff --git a/ui/src/views/admin/user-management/Details.tsx b/ui/src/views/admin/user-management/Details.tsx
index d00a1e96..18018a07 100644
--- a/ui/src/views/admin/user-management/Details.tsx
+++ b/ui/src/views/admin/user-management/Details.tsx
@@ -110,6 +110,13 @@ import {
SelectField,
TextField,
} from './userFormFields'
+import {
+ contractTypes,
+ drivingLicenseClasses,
+ employmentTypes,
+ maritalStatuses,
+ militaryStatuses,
+} from './userLookups'
import { useUserFieldLookup } from './useUserFieldLookup'
export interface ClaimTypeDto {
@@ -117,16 +124,6 @@ export interface ClaimTypeDto {
claimValue: string
}
-const militaryStatuses = ['Done', 'Exempt', 'Postponed', 'NotDone']
-const employmentTypes = ['FullTime', 'PartTime']
-const contractTypes = ['Indefinite', 'Definite']
-const drivingLicenseClasses = ['M', 'A1', 'A2', 'A', 'B1', 'B', 'BE', 'C1', 'C1E', 'C', 'CE', 'D1', 'D1E', 'D', 'DE', 'F', 'G']
-const maritalStatuses = [
- ['Bekar', 'Single'],
- ['Evli', 'Married'],
- ['Boşanmış', 'Divorced'],
- ['Dul', 'Widowed'],
-] as const
// Uyruk ülke adıyla saklanır; ad dile göre değişebildiği için Türkiye telefon koduyla tanınır.
const TURKEY_PHONE_CODE = 90
diff --git a/ui/src/views/admin/user-management/userLookups.ts b/ui/src/views/admin/user-management/userLookups.ts
new file mode 100644
index 00000000..c733b9aa
--- /dev/null
+++ b/ui/src/views/admin/user-management/userLookups.ts
@@ -0,0 +1,14 @@
+/**
+ * Kullanıcı kaydındaki sabit seçenekli alanların değerleri. Kullanıcı detayı (düzenleme) ve profilin
+ * Çalışan Bilgileri sekmesi (salt okunur) aynı değer → dil anahtarı eşlemesini kullanır.
+ */
+export const militaryStatuses = ['Done', 'Exempt', 'Postponed', 'NotDone']
+export const employmentTypes = ['FullTime', 'PartTime']
+export const contractTypes = ['Indefinite', 'Definite']
+export const drivingLicenseClasses = ['M', 'A1', 'A2', 'A', 'B1', 'B', 'BE', 'C1', 'C1E', 'C', 'CE', 'D1', 'D1E', 'D', 'DE', 'F', 'G']
+export const maritalStatuses = [
+ ['Bekar', 'Single'],
+ ['Evli', 'Married'],
+ ['Boşanmış', 'Divorced'],
+ ['Dul', 'Widowed'],
+] as const