Intranet -> Dashboard Diary

This commit is contained in:
Sedat ÖZTÜRK 2026-10-09 15:24:53 +03:00
parent 78ecb82b34
commit 4323e87572
9 changed files with 573 additions and 5 deletions

View file

@ -15204,6 +15204,16 @@
"MultiTenancySide": 3, "MultiTenancySide": 3,
"Modules": "Prj" "Modules": "Prj"
}, },
{
"GroupName": "App.Prj",
"Name": "App.Prj.Diary.Widget",
"ParentName": "App.Prj.Diary",
"DisplayName": "App.Platform.Widget",
"Order": 7,
"IsEnabled": true,
"MultiTenancySide": 3,
"Modules": "Prj"
},
{ {
"GroupName": "App.Prj", "GroupName": "App.Prj",
"Name": "App.Prj.ProjectSummary", "Name": "App.Prj.ProjectSummary",

File diff suppressed because one or more lines are too long

View file

@ -158,6 +158,19 @@
"Order": 37, "Order": 37,
"Props": "", "Props": "",
"IsActive": true "IsActive": true
},
{
"Id": "70166a4b-cdec-56d0-918c-a98581b02d62",
"Code": "diary",
"Name": "Work Diary",
"LabelKey": "App.DashboardWidget.Diary",
"SourceType": 0,
"ComponentName": "",
"PermissionName": "App.Prj.Diary.Widget",
"Column": 0,
"Order": 38,
"Props": "",
"IsActive": true
} }
] ]
} }

View file

@ -85712,6 +85712,160 @@
"Key": "App.DashboardWidget.MyTimesheet", "Key": "App.DashboardWidget.MyTimesheet",
"Value": "Puantajım" "Value": "Puantajım"
}, },
{
"Id": "6bbf76da-186c-5a1b-be44-632559aed7f0",
"CultureName": "en",
"ResourceName": "Platform",
"Key": "App.DashboardWidget.Diary",
"Value": "My Diary"
},
{
"Id": "9732cd75-149d-5356-9773-297d3865e7f1",
"CultureName": "tr",
"ResourceName": "Platform",
"Key": "App.DashboardWidget.Diary",
"Value": "Güncem"
},
{
"Id": "ab9c1bba-f0ea-5728-8924-b1fac77d2543",
"CultureName": "en",
"ResourceName": "Platform",
"Key": "App.DashboardWidget.Diary.Hint",
"Value": "Write down what you worked on, by project and work package."
},
{
"Id": "c10834f5-a148-556a-8320-3fa58bf797d0",
"CultureName": "tr",
"ResourceName": "Platform",
"Key": "App.DashboardWidget.Diary.Hint",
"Value": "Proje ve iş paketine göre yaptığın çalışmaları günceye yaz."
},
{
"Id": "fd95069c-a741-5804-be4e-a2f3f4593e7a",
"CultureName": "en",
"ResourceName": "Platform",
"Key": "App.DashboardWidget.Diary.Write",
"Value": "Write Diary"
},
{
"Id": "2b0024a8-238f-51e9-959e-443a27c530f5",
"CultureName": "tr",
"ResourceName": "Platform",
"Key": "App.DashboardWidget.Diary.Write",
"Value": "Günce Yaz"
},
{
"Id": "4a8e4698-86fe-517c-9835-8b36017508bf",
"CultureName": "en",
"ResourceName": "Platform",
"Key": "App.DashboardWidget.Diary.EndBeforeStart",
"Value": "End time must be after the start time."
},
{
"Id": "07942047-bc00-5b08-bdaa-a8ebf24840d9",
"CultureName": "tr",
"ResourceName": "Platform",
"Key": "App.DashboardWidget.Diary.EndBeforeStart",
"Value": "Bitiş zamanı başlangıçtan sonra olmalı."
},
{
"Id": "72de8ab1-9ee9-56e5-bed6-390c0ecb620e",
"CultureName": "en",
"ResourceName": "Platform",
"Key": "App.DashboardWidget.Diary.NoPermission",
"Value": "You are not authorized to add diary entries."
},
{
"Id": "5f4dfa82-460f-5208-acdd-f4fbb548d218",
"CultureName": "tr",
"ResourceName": "Platform",
"Key": "App.DashboardWidget.Diary.NoPermission",
"Value": "Günce ekleme yetkiniz yok."
},
{
"Id": "19063665-2b05-5a28-8c2f-762adcb59881",
"CultureName": "en",
"ResourceName": "Platform",
"Key": "App.DashboardWidget.Diary.TodayHours",
"Value": "Today (h)"
},
{
"Id": "61455ad0-2fc2-50d0-8fa0-4e3466f0d759",
"CultureName": "tr",
"ResourceName": "Platform",
"Key": "App.DashboardWidget.Diary.TodayHours",
"Value": "Bugün (sa)"
},
{
"Id": "e4e1d710-fe63-5df9-bb35-efa85d6a43a0",
"CultureName": "en",
"ResourceName": "Platform",
"Key": "App.DashboardWidget.Diary.WeekHours",
"Value": "This Week (h)"
},
{
"Id": "f14385fa-48d1-5cb3-8914-c45dbae29813",
"CultureName": "tr",
"ResourceName": "Platform",
"Key": "App.DashboardWidget.Diary.WeekHours",
"Value": "Bu Hafta (sa)"
},
{
"Id": "87eb6d48-d820-59c3-bc5f-b7a46f747dbf",
"CultureName": "en",
"ResourceName": "Platform",
"Key": "App.DashboardWidget.Diary.TodayEntries",
"Value": "Entries Today"
},
{
"Id": "10ea505d-1a2d-5c25-b5c5-d7c51fbf2940",
"CultureName": "tr",
"ResourceName": "Platform",
"Key": "App.DashboardWidget.Diary.TodayEntries",
"Value": "Bugünkü Kayıt"
},
{
"Id": "8b431e51-134f-5a9e-a5b0-e51e2807c79e",
"CultureName": "en",
"ResourceName": "Platform",
"Key": "App.DashboardWidget.Diary.Recent",
"Value": "Recent Entries"
},
{
"Id": "5f4b2e77-8129-5a11-94d9-ffc5bd00087a",
"CultureName": "tr",
"ResourceName": "Platform",
"Key": "App.DashboardWidget.Diary.Recent",
"Value": "Son Güncelerim"
},
{
"Id": "a3052970-151a-5ab5-aaf9-8a25de3d0729",
"CultureName": "en",
"ResourceName": "Platform",
"Key": "App.DashboardWidget.Diary.Empty",
"Value": "No diary entries in the last 30 days."
},
{
"Id": "f16782fe-6321-50e1-8759-feeeef8a2e4e",
"CultureName": "tr",
"ResourceName": "Platform",
"Key": "App.DashboardWidget.Diary.Empty",
"Value": "Son 30 günde günce yok."
},
{
"Id": "427597d6-ee21-5c81-ae1e-322643a2adfe",
"CultureName": "en",
"ResourceName": "Platform",
"Key": "App.DashboardWidget.Diary.HourShort",
"Value": "h"
},
{
"Id": "36da653b-6691-57e9-9c29-8592044b6ee3",
"CultureName": "tr",
"ResourceName": "Platform",
"Key": "App.DashboardWidget.Diary.HourShort",
"Value": "sa"
},
{ {
"Id": "340dd8e3-8d4b-5d19-a3b2-2aaf77205677", "Id": "340dd8e3-8d4b-5d19-a3b2-2aaf77205677",
"CultureName": "en", "CultureName": "en",

View file

@ -2999,6 +2999,7 @@ Pano kutuları (seed):
| `my-leave` | Sol / 35 | `App.Hr.MyLeaveRequest` | Custom `MyLeaveWidget`: izin bakiyesi, süren talepler → İzin Talebi Oluştur · Taleplerim · Bakiyem | | `my-leave` | Sol / 35 | `App.Hr.MyLeaveRequest` | Custom `MyLeaveWidget`: izin bakiyesi, süren talepler → İzin Talebi Oluştur · Taleplerim · Bakiyem |
| `team-leave` | Sol / 36 | `App.Hr.TeamLeaveRequest` | Custom `TeamLeaveWidget`: müdürün onayını bekleyen izin talepleri, bugün izinde olanlar → Onay Bekleyenler | | `team-leave` | Sol / 36 | `App.Hr.TeamLeaveRequest` | Custom `TeamLeaveWidget`: müdürün onayını bekleyen izin talepleri, bugün izinde olanlar → Onay Bekleyenler |
| `my-timesheet` | Sol / 37 | `App.Hr.MyTimesheet` | Custom `MyTimesheetWidget`: seçilen ayın puantajı (net saat, geç/erken dk, fazla mesai, devamsız/izinli/raporlu) ve yıl toplamı, 270 saatlik fazla mesai çubuğu → Puantajım | | `my-timesheet` | Sol / 37 | `App.Hr.MyTimesheet` | Custom `MyTimesheetWidget`: seçilen ayın puantajı (net saat, geç/erken dk, fazla mesai, devamsız/izinli/raporlu) ve yıl toplamı, 270 saatlik fazla mesai çubuğu → Puantajım |
| `diary` | Sol / 38 | `App.Prj.Diary.Widget` | Builtin (`widgets/Diary.tsx`) Güncem: bugün / bu hafta saat, bugünkü kayıt, son 5 günce (son 30 gün); Günce Yaz butonu çekirdek `DiaryEntry` diyaloğunu açar, kapanınca özet yenilenir; kayıt Günce Listesi'ne (`App.Prj.Diary`) yazılır, kaydetmek için ayrıca `App.Prj.Diary` + `.Create` gerekir |
| `social-wall` | Orta / 40 | `App.Hr.Intranet.SocialPost.Widget` | Sosyal duvar (Tümü / Paylaşımlarım, sonsuz kaydırma 10'arlı) | | `social-wall` | Orta / 40 | `App.Hr.Intranet.SocialPost.Widget` | Sosyal duvar (Tümü / Paylaşımlarım, sonsuz kaydırma 10'arlı) |
| `announcements` | Sağ / 50 | `App.Hr.Intranet.Announcement.Widget` | Sabitlenmiş duyurular (en fazla 3) | | `announcements` | Sağ / 50 | `App.Hr.Intranet.Announcement.Widget` | Sabitlenmiş duyurular (en fazla 3) |
| `upcoming-events` | Sağ / 60 | `App.Hr.Intranet.Events.Event.Widget` | Bugünden sonraki yayınlanmış ilk 3 etkinlik | | `upcoming-events` | Sağ / 60 | `App.Hr.Intranet.Events.Event.Widget` | Bugünden sonraki yayınlanmış ilk 3 etkinlik |
@ -3006,7 +3007,7 @@ Pano kutuları (seed):
| `team-performance` | Sağ / 80 | `App.Hr.TeamPerformanceReview` | Custom `TeamPerformanceWidget`: yöneticinin ekip değerlendirmeleri → Ekibimin Değerlendirmeleri | | `team-performance` | Sağ / 80 | `App.Hr.TeamPerformanceReview` | Custom `TeamPerformanceWidget`: yöneticinin ekip değerlendirmeleri → Ekibimin Değerlendirmeleri |
| `my-360-feedback` | Sağ / 90 | `App.Hr.My360Feedback` | Custom `My360FeedbackWidget`: kullanıcıya atanan 360 talepleri → 360 Taleplerim | | `my-360-feedback` | Sağ / 90 | `App.Hr.My360Feedback` | Custom `My360FeedbackWidget`: kullanıcıya atanan 360 talepleri → 360 Taleplerim |
İlk altı kutu Builtin'dir (koddaki `Code`). `my-leave` / `team-leave` İzin Yönetimi'nin (İzin Yönetimi bölümü §3.3), `my-timesheet` Puantaj ve Devam Takibi'nin (Puantaj ve Devam Takibi bölümü §3.3), son üçü Performans Yönetimi'nin çalışana ait özetleridir: İlk altı kutu ve `diary` Builtin'dir (koddaki `Code`). `my-leave` / `team-leave` İzin Yönetimi'nin (İzin Yönetimi bölümü §3.3), `my-timesheet` Puantaj ve Devam Takibi'nin (Puantaj ve Devam Takibi bölümü §3.3), son üçü Performans Yönetimi'nin çalışana ait özetleridir:
İK menüsü İK departmanınındır, çalışan kendi değerlendirmesini buradan takip eder; listeler menüsüzdür ve İK menüsü İK departmanınındır, çalışan kendi değerlendirmesini buradan takip eder; listeler menüsüzdür ve
kutudaki bağlantıyla açılır. Ayrıntı: Performans Yönetimi bölümü §3.3. kutudaki bağlantıyla açılır. Ayrıntı: Performans Yönetimi bölümü §3.3.

View file

@ -163,6 +163,8 @@ Personelin proje ve iş paketine göre günlük yaptığı çalışmaları yazd
`Proje › Günce Listesi`; ayrıca `Projeler` kartında Günce sekmesi (ProjectId ilişkisiyle). Widget'lar: toplam kayıt, bugünkü kayıt, toplam saat. `Proje › Günce Listesi`; ayrıca `Projeler` kartında Günce sekmesi (ProjectId ilişkisiyle). Widget'lar: toplam kayıt, bugünkü kayıt, toplam saat.
Intranet › Dashboard'daki **Güncem** kutusu (`diary`, builtin `ui/src/views/intranet/widgets/Diary.tsx`) kişinin son 30 günlük güncesini bu ekranın sorgusundan okur (`UserId` = oturumdaki kişi): bugün / bu hafta saat, bugünkü kayıt sayısı, son 5 günce; özet `App.Prj.Diary` okuma yetkisiyle görünür. Günce Yaz butonuyla çekirdek diyalog kayıt defterindeki `DiaryEntry` modalını (`ui/src/views/intranet/DiaryModal.tsx`) açar. Modal bu ekranın lookup'larını (Proje, projeye göre İş Paketi) ve ekleme ucunu (`list-form-data/insert`) kullanır; Kullanıcı oturumdaki kişi, Dakika başlangıç–bitiş farkıdır, düz metin satırları HTML paragrafına çevrilir.
### 4. Veri modeli ### 4. Veri modeli
`Prj_T_Diary`: ProjectId, WorkPackageId (projeye göre süzülür), UserId, StartTime, EndTime, Minutes, Content (nvarchar(MAX), HTML). Kiracı + audit + soft delete. `Prj_T_Diary`: ProjectId, WorkPackageId (projeye göre süzülür), UserId, StartTime, EndTime, Minutes, Content (nvarchar(MAX), HTML). Kiracı + audit + soft delete.
@ -173,7 +175,7 @@ Dakika = (Bitiş − Başlangıç) saat × 60, yuvarlanır, negatif olamaz; form
### 6. Yetki ve görünürlük ### 6. Yetki ve görünürlük
`App.Prj › App.Prj.Diary` (+ C/U/D/E/I/N). Kayıtlar kiracı içinde tüm yetkililere görünür. `App.Prj › App.Prj.Diary` (+ C/U/D/E/I/N, `.Widget`). Kayıtlar kiracı içinde tüm yetkililere görünür. `App.Prj.Diary.Widget` panodaki Günce kutusunu gösterir; modalda kaydetmek ekran yetkisini (`App.Prj.Diary` + `.Create`) ister, yoksa modal yetki uyarısı gösterir.
### 7. Entegrasyonlar ### 7. Entegrasyonlar

View file

@ -0,0 +1,215 @@
import { useEffect, useState } from 'react'
import dayjs from 'dayjs'
import { Field, Form, Formik, FormikHelpers } from 'formik'
import * as Yup from 'yup'
import {
Button,
Dialog,
FormContainer,
FormItem,
Input,
Notification,
Select,
toast,
} from '@/components/ui'
import { dynamicFetch } from '@/services/form.service'
import { useStoreState } from '@/store/store'
import { useLocalization } from '@/utils/hooks/useLocalization'
import { usePermission } from '@/utils/hooks/usePermission'
// Kayıt Günce Listesi ekranının kendi uçlarından geçer: lookup'lar ekranın sorgusunu, ekleme
// ekranın insert komutunu ve Create yetkisini kullanır. Modal yalnızca giriş arayüzüdür.
const LIST_FORM_CODE = 'App.Prj.Diary'
const DATE_TIME_FORMAT = 'YYYY-MM-DDTHH:mm'
interface LookupOption {
value: string
label: string
}
interface DiaryValues {
projectId: string
workPackageId: string
startTime: string
endTime: string
content: string
}
const loadLookup = async (listFormFieldName: string, filters: unknown[] | null = null) => {
const response = await dynamicFetch('list-form-select/lookup', 'POST', null, {
listFormCode: LIST_FORM_CODE,
listFormFieldName,
filters,
})
const data = response?.data ?? []
return (Array.isArray(data) ? data : [data]).map(
(item: { Key: string; Name: string }): LookupOption => ({ value: item.Key, label: item.Name }),
)
}
// Liste ekranındaki editör HTML'dir; düz metnin satırları paragrafa çevrilir.
const toHtml = (text: string) =>
text
.split(/\r?\n/)
.filter((line) => line.trim())
.map((line) => {
const element = document.createElement('p')
element.textContent = line
return element.outerHTML
})
.join('')
function DiaryModal({ open, onDialogClose }: { open: boolean; onDialogClose: () => void }) {
const { translate } = useLocalization()
const { checkPermission } = usePermission()
const userId = useStoreState((state) => state.auth.user.id)
const canCreate = checkPermission(`${LIST_FORM_CODE}.Create`)
const [projects, setProjects] = useState<LookupOption[]>([])
const [workPackages, setWorkPackages] = useState<LookupOption[]>([])
useEffect(() => {
if (canCreate) loadLookup('ProjectId').then(setProjects).catch(() => setProjects([]))
}, [canCreate])
// İş paketi lookup'ı ekranda projeye cascade bağlıdır; seçim değişince yeniden yüklenir.
const selectProject = (value: string) => {
setWorkPackages([])
if (value) loadLookup('WorkPackageId', [value]).then(setWorkPackages).catch(() => {})
}
const schema = Yup.object().shape({
projectId: Yup.string().required(),
startTime: Yup.string().required(),
endTime: Yup.string()
.required()
.test('after-start', translate('::App.DashboardWidget.Diary.EndBeforeStart'), (value, context) =>
dayjs(value).isAfter(dayjs(context.parent.startTime)),
),
content: Yup.string().trim().required(),
})
const now = dayjs()
const initialValues: DiaryValues = {
projectId: '',
workPackageId: '',
startTime: now.subtract(1, 'hour').startOf('hour').format(DATE_TIME_FORMAT),
endTime: now.startOf('minute').format(DATE_TIME_FORMAT),
content: '',
}
const notify = (type: 'success' | 'danger', key: string) =>
toast.push(
<Notification type={type} duration={2000}>
{translate(key)}
</Notification>,
{ placement: 'bottom-end' },
)
const handleSubmit = async (values: DiaryValues, { setSubmitting }: FormikHelpers<DiaryValues>) => {
try {
await dynamicFetch('list-form-data/insert', 'POST', null, {
listFormCode: LIST_FORM_CODE,
data: {
ProjectId: values.projectId,
WorkPackageId: values.workPackageId || null,
UserId: userId,
StartTime: values.startTime,
EndTime: values.endTime,
Minutes: dayjs(values.endTime).diff(dayjs(values.startTime), 'minute'),
Content: toHtml(values.content),
},
})
notify('success', '::App.Platform.Success')
onDialogClose()
} catch {
notify('danger', '::App.Platform.Error')
} finally {
setSubmitting(false)
}
}
return (
<Dialog isOpen={open} width={640} onClose={onDialogClose} onRequestClose={onDialogClose}>
<h5 className="mb-1">{translate('::App.DashboardWidget.Diary')}</h5>
<p className="mb-4 text-xs">{translate('::App.DashboardWidget.Diary.Hint')}</p>
{!canCreate ? (
<p className="text-sm text-gray-500 dark:text-gray-400">
{translate('::App.DashboardWidget.Diary.NoPermission')}
</p>
) : (
<Formik initialValues={initialValues} validationSchema={schema} onSubmit={handleSubmit}>
{({ values, touched, errors, isSubmitting, setFieldValue }) => (
<Form>
<FormContainer size="sm">
<div className="grid grid-cols-1 gap-x-4 md:grid-cols-2">
<FormItem
className="md:col-span-2"
label={translate('::App.Listform.ListformField.ProjectId')}
invalid={!!errors.projectId && touched.projectId}
>
<Select
options={projects}
value={projects.find((option) => option.value === values.projectId) ?? null}
onChange={(option) => {
const value = (option as LookupOption | null)?.value ?? ''
setFieldValue('projectId', value)
setFieldValue('workPackageId', '')
selectProject(value)
}}
/>
</FormItem>
<FormItem
className="md:col-span-2"
label={translate('::App.Listform.ListformField.WorkPackageId')}
>
<Select
isClearable
isDisabled={!values.projectId}
options={workPackages}
value={workPackages.find((option) => option.value === values.workPackageId) ?? null}
onChange={(option) =>
setFieldValue('workPackageId', (option as LookupOption | null)?.value ?? '')
}
/>
</FormItem>
<FormItem
label={translate('::App.Listform.ListformField.StartTime')}
invalid={!!errors.startTime && touched.startTime}
>
<Field type="datetime-local" name="startTime" component={Input} />
</FormItem>
<FormItem
label={translate('::App.Listform.ListformField.EndTime')}
invalid={!!errors.endTime && touched.endTime}
errorMessage={errors.endTime}
>
<Field type="datetime-local" name="endTime" component={Input} />
</FormItem>
</div>
<FormItem
label={translate('::App.Listform.ListformField.Diary')}
invalid={!!errors.content && touched.content}
>
<Field textArea="true" rows={6} name="content" component={Input} />
</FormItem>
<div className="mt-6 flex flex-row justify-end gap-3">
<Button size="sm" variant="solid" loading={isSubmitting} type="submit">
{isSubmitting ? translate('::App.Platform.Saving') : translate('::App.Platform.Save')}
</Button>
<Button size="sm" type="button" variant="plain" onClick={onDialogClose}>
{translate('::App.Platform.Cancel')}
</Button>
</div>
</FormContainer>
</Form>
)}
</Formik>
)}
</Dialog>
)
}
export default DiaryModal

View file

@ -0,0 +1,172 @@
import React, { useCallback, useEffect, useState } from 'react'
import { FaBook, FaPen } from 'react-icons/fa'
import dayjs from 'dayjs'
import Button from '@/components/ui/Button'
import { dynamicFetch } from '@/services/form.service'
import { useStoreState } from '@/store/store'
import { useDialogContext } from '@/views/shared/DialogContext'
import { useLocalization } from '@/utils/hooks/useLocalization'
import { usePermission } from '@/utils/hooks/usePermission'
import type { DashboardWidgetDefinition } from '../dashboardWidget'
// Görünürlük IntranetWidget kaydının yetkisinden (App.Prj.Diary.Widget) gelir; özet Günce Listesi
// ekranının sorgusundan okunur (okuma yetkisi ekranda kalır), giriş çekirdek DiaryEntry diyaloğundadır.
const LIST_FORM_CODE = 'App.Prj.Diary'
const RECENT_DAYS = 30
const RECENT_COUNT = 5
interface DiaryRow {
Id: string
ProjectId?: string
StartTime?: string
Minutes?: number
Content?: string
}
// DOMParser betik ve olay niteliklerini çalıştırmaz; kayıttaki HTML güvenle düz metne iner.
const plainText = (html?: string) =>
(new DOMParser().parseFromString(html ?? '', 'text/html').body.textContent ?? '')
.replace(/\s+/g, ' ')
.trim()
const formatHours = (minutes: number) => {
const hours = minutes / 60
return Number.isInteger(hours) ? String(hours) : hours.toFixed(1)
}
const Stat = ({ label, value, className }: { label: string; value: string; className: string }) => (
<div className="rounded-lg border border-gray-200 bg-gray-50 p-2 text-center dark:border-gray-700 dark:bg-gray-900/40">
<div className={'text-lg font-semibold ' + className}>{value}</div>
<div className="truncate text-xs text-gray-500 dark:text-gray-400">{label}</div>
</div>
)
const Diary: React.FC = () => {
const { translate } = useLocalization()
const { checkPermission } = usePermission()
const dialog: any = useDialogContext()
const userId = useStoreState((state) => state.auth.user.id)
const canRead = checkPermission(LIST_FORM_CODE)
const [rows, setRows] = useState<DiaryRow[]>([])
const [projects, setProjects] = useState(new Map<string, string>())
const load = useCallback(async () => {
if (!canRead || !userId) return
const since = dayjs().subtract(RECENT_DAYS, 'day').format('YYYY-MM-DD')
const [rowResponse, projectResponse] = await Promise.all([
dynamicFetch('list-form-select/select', 'GET', {
listFormCode: LIST_FORM_CODE,
take: 500,
filter: JSON.stringify([['UserId', '=', userId], 'and', ['StartTime', '>=', since]]),
}),
dynamicFetch('list-form-select/lookup', 'POST', null, {
listFormCode: LIST_FORM_CODE,
listFormFieldName: 'ProjectId',
filters: null,
}),
])
setRows(rowResponse?.data?.data ?? [])
const items: { Key: string; Name: string }[] = Array.isArray(projectResponse?.data)
? projectResponse.data
: []
setProjects(new Map(items.map((item) => [String(item.Key).toLowerCase(), item.Name])))
}, [canRead, userId])
useEffect(() => {
load().catch(() => setRows([]))
}, [load])
const minutesSince = (start: dayjs.Dayjs) =>
rows
.filter((row) => !dayjs(row.StartTime).isBefore(start))
.reduce((total, row) => total + Number(row.Minutes ?? 0), 0)
const today = dayjs().startOf('day')
const todayCount = rows.filter((row) => dayjs(row.StartTime).isSame(today, 'day')).length
const recent = [...rows]
.sort((a, b) => String(b.StartTime).localeCompare(String(a.StartTime)))
.slice(0, RECENT_COUNT)
return (
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-lg border border-gray-200/50 dark:border-gray-700/50 overflow-hidden">
<div className="p-4 border-b border-gray-200 dark:border-gray-700">
<h2 className="text-base font-semibold text-gray-900 dark:text-white flex items-center gap-2">
<FaBook className="w-5 h-5 text-teal-500" />
{translate('::App.DashboardWidget.Diary')}
</h2>
</div>
<div className="p-3 space-y-3">
{canRead && (
<>
<div className="grid grid-cols-3 gap-2">
<Stat
label={translate('::App.DashboardWidget.Diary.TodayHours')}
value={formatHours(minutesSince(today))}
className="text-teal-600 dark:text-teal-400"
/>
<Stat
label={translate('::App.DashboardWidget.Diary.WeekHours')}
value={formatHours(minutesSince(dayjs().subtract((dayjs().day() + 6) % 7, 'day').startOf('day')))}
className="text-gray-900 dark:text-white"
/>
<Stat
label={translate('::App.DashboardWidget.Diary.TodayEntries')}
value={String(todayCount)}
className={todayCount === 0 ? 'text-orange-500' : 'text-gray-900 dark:text-white'}
/>
</div>
<div>
<div className="mb-1 text-xs font-semibold text-gray-500 dark:text-gray-400">
{translate('::App.DashboardWidget.Diary.Recent')}
</div>
{recent.length === 0 ? (
<div className="text-sm text-gray-500 dark:text-gray-400">
{translate('::App.DashboardWidget.Diary.Empty')}
</div>
) : (
<ul className="space-y-1">
{recent.map((row) => {
const text = plainText(row.Content)
return (
<li key={row.Id} className="rounded-md bg-gray-50 px-2 py-1 text-xs dark:bg-gray-900/40">
<div className="flex items-center justify-between gap-2">
<span className="truncate font-medium text-gray-700 dark:text-gray-200">
{projects.get(String(row.ProjectId).toLowerCase()) ?? '-'}
</span>
<span className="shrink-0 text-gray-500 dark:text-gray-400">
{dayjs(row.StartTime).format('DD.MM HH:mm')}
{row.Minutes
? ` · ${formatHours(Number(row.Minutes))} ${translate('::App.DashboardWidget.Diary.HourShort')}`
: ''}
</span>
</div>
{text && <div className="truncate text-gray-500 dark:text-gray-400">{text}</div>}
</li>
)
})}
</ul>
)}
</div>
</>
)}
<Button
block
size="sm"
variant="solid"
icon={<FaPen />}
// Diyalog kapanınca özet yenilenir; yeni günce hemen görünür.
onClick={() => dialog.setConfig({ component: 'DiaryEntry', onClose: () => load() })}
>
{translate('::App.DashboardWidget.Diary.Write')}
</Button>
</div>
</div>
)
}
export const dashboardWidget: DashboardWidgetDefinition = {
id: 'diary',
render: () => <Diary />,
}
export default Diary

View file

@ -17,6 +17,7 @@ const dialogs: Record<string, ComponentType<any>> = {
AuditLogDetail: lazy(() => import('@/views/admin/auditLog/AuditLogDetail')), AuditLogDetail: lazy(() => import('@/views/admin/auditLog/AuditLogDetail')),
DynamicServiceEditor: lazy(() => import('@/views/developerKit/DynamicServiceEditor')), DynamicServiceEditor: lazy(() => import('@/views/developerKit/DynamicServiceEditor')),
BranchSeed: lazy(() => import('@/views/branch/BranchSeed')), BranchSeed: lazy(() => import('@/views/branch/BranchSeed')),
DiaryEntry: lazy(() => import('@/views/intranet/DiaryModal')),
} }
const DialogShowComponent = (): ReactElement => { const DialogShowComponent = (): ReactElement => {