Intranet -> Dashboard Diary
This commit is contained in:
parent
78ecb82b34
commit
4323e87572
9 changed files with 573 additions and 5 deletions
|
|
@ -15204,6 +15204,16 @@
|
|||
"MultiTenancySide": 3,
|
||||
"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",
|
||||
"Name": "App.Prj.ProjectSummary",
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -158,6 +158,19 @@
|
|||
"Order": 37,
|
||||
"Props": "",
|
||||
"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
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -85712,6 +85712,160 @@
|
|||
"Key": "App.DashboardWidget.MyTimesheet",
|
||||
"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",
|
||||
"CultureName": "en",
|
||||
|
|
|
|||
|
|
@ -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 |
|
||||
| `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 |
|
||||
| `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ı) |
|
||||
| `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 |
|
||||
|
|
@ -3006,7 +3007,7 @@ Pano kutuları (seed):
|
|||
| `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 |
|
||||
|
||||
İ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
|
||||
kutudaki bağlantıyla açılır. Ayrıntı: Performans Yönetimi bölümü §3.3.
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
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
|
||||
|
||||
`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
|
||||
|
||||
`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
|
||||
|
||||
|
|
|
|||
215
ui/src/views/intranet/DiaryModal.tsx
Normal file
215
ui/src/views/intranet/DiaryModal.tsx
Normal 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
|
||||
172
ui/src/views/intranet/widgets/Diary.tsx
Normal file
172
ui/src/views/intranet/widgets/Diary.tsx
Normal 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
|
||||
|
|
@ -17,6 +17,7 @@ const dialogs: Record<string, ComponentType<any>> = {
|
|||
AuditLogDetail: lazy(() => import('@/views/admin/auditLog/AuditLogDetail')),
|
||||
DynamicServiceEditor: lazy(() => import('@/views/developerKit/DynamicServiceEditor')),
|
||||
BranchSeed: lazy(() => import('@/views/branch/BranchSeed')),
|
||||
DiaryEntry: lazy(() => import('@/views/intranet/DiaryModal')),
|
||||
}
|
||||
|
||||
const DialogShowComponent = (): ReactElement => {
|
||||
|
|
|
|||
Loading…
Reference in a new issue