Show Activity komponenti oluşturuldu.
Şu an static veriler üzerinde çalışıyor.
This commit is contained in:
parent
8cdeb0a21f
commit
1faad17e7c
16 changed files with 1441 additions and 100 deletions
|
|
@ -305,6 +305,9 @@ public class GridOptionsDto : AuditedEntityDto<Guid>
|
|||
public string ListFormType { get; set; } = ListFormTypeEnum.List;
|
||||
public bool IsSubForm { get; set; } = false;
|
||||
|
||||
/// <summary>Bu listform show activity gösterilsin mi?</summary>
|
||||
public bool ShowActivity { get; set; } = true;
|
||||
|
||||
[JsonIgnore]
|
||||
public string SubFormsJson { get; set; } // Cagrilacak ListFormlar
|
||||
public SubFormDto[] SubFormsDto
|
||||
|
|
|
|||
|
|
@ -64,6 +64,7 @@ public class ListFormsAppService : CrudAppService<
|
|||
item.Height = input.Height;
|
||||
item.Description = input.Description;
|
||||
item.IsSubForm = input.IsSubForm;
|
||||
item.ShowActivity = input.ShowActivity;
|
||||
item.ListFormType = input.ListFormType;
|
||||
item.LayoutJson = JsonSerializer.Serialize(input.LayoutDto);
|
||||
|
||||
|
|
|
|||
|
|
@ -5281,6 +5281,12 @@
|
|||
"en": "Is Sub",
|
||||
"tr": "Is Sub"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "ListForms.ListFormEdit.ShowActivity",
|
||||
"en": "Show Activity",
|
||||
"tr": "Etkinliği Göster"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "ListForms.ListFormEdit.ListFormType",
|
||||
|
|
|
|||
|
|
@ -115,6 +115,9 @@ public class ListForm : FullAuditedEntity<Guid>
|
|||
/// <summary>Bu listform sub olarak mı kullanılacak</summary>
|
||||
public bool IsSubForm { get; set; }
|
||||
|
||||
/// <summary>Bu listform show activity gösterilsin mi?</summary>
|
||||
public bool ShowActivity { get; set; }
|
||||
|
||||
/// <summary>Bu listform'un sub listformlarının listesi ve ilişkileri</summary>
|
||||
public string SubFormsJson { get; set; }
|
||||
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ using Volo.Abp.EntityFrameworkCore;
|
|||
namespace Kurs.Platform.Migrations
|
||||
{
|
||||
[DbContext(typeof(PlatformDbContext))]
|
||||
[Migration("20251013062624_Initial")]
|
||||
[Migration("20251013115205_Initial")]
|
||||
partial class Initial
|
||||
{
|
||||
/// <inheritdoc />
|
||||
|
|
@ -3841,6 +3841,9 @@ namespace Kurs.Platform.Migrations
|
|||
b.Property<string>("SeriesJson")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<bool>("ShowActivity")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<string>("SizeJson")
|
||||
.HasColumnType("text");
|
||||
|
||||
|
|
@ -1628,6 +1628,7 @@ namespace Kurs.Platform.Migrations
|
|||
IsOrganizationUnit = table.Column<bool>(type: "bit", nullable: false),
|
||||
ListFormType = table.Column<string>(type: "nvarchar(20)", maxLength: 20, nullable: true),
|
||||
IsSubForm = table.Column<bool>(type: "bit", nullable: false),
|
||||
ShowActivity = table.Column<bool>(type: "bit", nullable: false),
|
||||
SubFormsJson = table.Column<string>(type: "text", nullable: true),
|
||||
WidgetsJson = table.Column<string>(type: "text", nullable: true),
|
||||
ExtraFilterJson = table.Column<string>(type: "text", nullable: true),
|
||||
|
|
@ -3838,6 +3838,9 @@ namespace Kurs.Platform.Migrations
|
|||
b.Property<string>("SeriesJson")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<bool>("ShowActivity")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<string>("SizeJson")
|
||||
.HasColumnType("text");
|
||||
|
||||
|
|
|
|||
|
|
@ -495,6 +495,7 @@ export interface GridOptionsDto extends AuditedEntityDto<string> {
|
|||
isOrganizationUnit: boolean
|
||||
listFormType: string
|
||||
isSubForm: boolean
|
||||
showActivity: boolean
|
||||
subFormsJson?: string
|
||||
subFormsDto: SubFormDto[]
|
||||
extraFilterJson?: string
|
||||
|
|
|
|||
46
ui/src/proxy/formActivity/models.ts
Normal file
46
ui/src/proxy/formActivity/models.ts
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
export interface ActivityItem {
|
||||
id: string
|
||||
type: 'note' | 'message'
|
||||
subject: string
|
||||
content: string
|
||||
recipientUserName?: string
|
||||
creatorId: string
|
||||
creationTime: Date
|
||||
data?: NoteData | MessageData
|
||||
}
|
||||
|
||||
export interface NoteData {
|
||||
id?: string
|
||||
entityName: string
|
||||
entityId: string
|
||||
subject: string
|
||||
content: string
|
||||
creatorId?: string
|
||||
creationTime?: Date
|
||||
attachedFiles?: FileData[]
|
||||
}
|
||||
|
||||
export interface FileData {
|
||||
id?: string
|
||||
entityName: string
|
||||
entityId: string
|
||||
fileName: string
|
||||
fileSize: number
|
||||
fileType: string
|
||||
filePath: string
|
||||
creatorId?: string
|
||||
creationTime?: Date
|
||||
}
|
||||
|
||||
export interface MessageData {
|
||||
id?: string
|
||||
entityName: string
|
||||
entityId: string
|
||||
recipientUserId: string
|
||||
recipientUserName: string
|
||||
subject: string
|
||||
content: string
|
||||
creatorId?: string
|
||||
creationTime?: Date
|
||||
isRead?: boolean
|
||||
}
|
||||
|
|
@ -15,10 +15,13 @@ import { IdentityRoleDto, IdentityUserDto } from '@/proxy/admin/models'
|
|||
|
||||
const schema = Yup.object().shape({
|
||||
cultureName: Yup.string().required('Culture Name Required'),
|
||||
listFormType: Yup.string().required('List Form Type Required'),
|
||||
title: Yup.string().required('Title Required'),
|
||||
name: Yup.string(),
|
||||
pageSize: Yup.number(),
|
||||
description: Yup.string(),
|
||||
pageSize: Yup.number(),
|
||||
isSubForm: Yup.boolean(),
|
||||
showActivity: Yup.boolean(),
|
||||
layoutDto: Yup.object().shape({
|
||||
grid: Yup.boolean(),
|
||||
card: Yup.boolean(),
|
||||
|
|
@ -222,6 +225,18 @@ function FormTabDetails(
|
|||
/>
|
||||
</FormItem>
|
||||
|
||||
<FormItem
|
||||
label={translate('::ListForms.ListFormEdit.ShowActivity')}
|
||||
invalid={errors.showActivity && touched.showActivity}
|
||||
errorMessage={errors.showActivity}
|
||||
>
|
||||
<Field
|
||||
name="showActivity"
|
||||
placeholder={translate('::ListForms.ListFormEdit.ShowActivity')}
|
||||
component={Checkbox}
|
||||
/>
|
||||
</FormItem>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<FormItem
|
||||
label={translate('::ListForms.ListFormEdit.DetailsWidth')}
|
||||
|
|
|
|||
156
ui/src/views/form/FormActivityPanel/ActivityList.tsx
Normal file
156
ui/src/views/form/FormActivityPanel/ActivityList.tsx
Normal file
|
|
@ -0,0 +1,156 @@
|
|||
import React from 'react'
|
||||
import {
|
||||
FaStickyNote,
|
||||
FaFileAlt,
|
||||
FaEnvelope,
|
||||
FaTrash,
|
||||
FaDownload,
|
||||
FaUser,
|
||||
FaClock,
|
||||
} from 'react-icons/fa'
|
||||
import { Button } from '@/components/ui'
|
||||
import { ActivityItem } from '@/proxy/formActivity/models'
|
||||
|
||||
interface ActivityListProps {
|
||||
activities: ActivityItem[]
|
||||
onDeleteNote?: (noteId: string) => void
|
||||
onDeleteFile?: (fileId: string) => void
|
||||
onDeleteMessage?: (messageId: string) => void
|
||||
onDownloadFile?: (fileData: any) => void
|
||||
}
|
||||
|
||||
export const ActivityList: React.FC<ActivityListProps> = ({
|
||||
activities,
|
||||
onDeleteNote,
|
||||
onDeleteFile,
|
||||
onDeleteMessage,
|
||||
onDownloadFile,
|
||||
}) => {
|
||||
const formatDate = (date: Date) => {
|
||||
return new Intl.DateTimeFormat('tr-TR', {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
year: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
}).format(new Date(date))
|
||||
}
|
||||
|
||||
const getActivityIcon = (type: string) => {
|
||||
switch (type) {
|
||||
case 'note':
|
||||
return <FaStickyNote className="text-yellow-500" />
|
||||
case 'message':
|
||||
return <FaEnvelope className="text-green-500" />
|
||||
default:
|
||||
return <FaStickyNote className="text-gray-500" />
|
||||
}
|
||||
}
|
||||
|
||||
const handleDelete = (activity: ActivityItem) => {
|
||||
switch (activity.type) {
|
||||
case 'note':
|
||||
onDeleteNote?.(activity.id)
|
||||
break
|
||||
case 'message':
|
||||
onDeleteMessage?.(activity.id)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
const handleDownloadFile = (fileData: any) => {
|
||||
onDownloadFile?.(fileData)
|
||||
}
|
||||
|
||||
if (activities.length === 0) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center h-32 text-gray-500">
|
||||
<FaStickyNote className="text-4xl mb-2 opacity-50" />
|
||||
<p className="text-sm">Henüz hiçbir aktivite bulunmuyor</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
{activities.map((activity) => (
|
||||
<div
|
||||
key={activity.id}
|
||||
className="bg-white border border-gray-200 rounded-lg p-3 shadow-sm hover:shadow-md transition-shadow"
|
||||
>
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="flex-shrink-0 mt-0.5">{getActivityIcon(activity.type)}</div>
|
||||
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-1 font-semibold mb-1">
|
||||
<FaUser className="text-xs" />
|
||||
<span>{activity.creatorId}</span>
|
||||
|
||||
{activity.recipientUserName && (
|
||||
<>
|
||||
<span>→</span>
|
||||
<FaUser className="text-xs" />
|
||||
<span>{activity.recipientUserName}</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{activity.subject && (
|
||||
<p className="text-sm font-medium text-gray-800 mb-1 break-words">{activity.subject}</p>
|
||||
)}
|
||||
|
||||
{activity.content && (
|
||||
<p className="text-sm text-gray-700 mb-2 break-words">{activity.content}</p>
|
||||
)}
|
||||
|
||||
{/* Note tipinde dosyaları göster */}
|
||||
{activity.type === 'note' && activity.data && (activity.data as any).attachedFiles?.length > 0 && (
|
||||
<div className="mt-2 mb-2">
|
||||
<p className="text-xs font-medium text-gray-600 mb-1">Ekli Dosyalar:</p>
|
||||
<div className="space-y-1">
|
||||
{((activity.data as any).attachedFiles || []).map((file: any, index: number) => (
|
||||
<div key={index} className="flex items-center justify-between p-2 bg-gray-50 rounded border">
|
||||
<div className="flex items-center gap-2">
|
||||
<FaFileAlt className="text-blue-500 text-sm" />
|
||||
<span className="text-xs font-medium">{file.fileName}</span>
|
||||
<span className="text-xs text-gray-500">({(file.fileSize / 1024).toFixed(1)} KB)</span>
|
||||
</div>
|
||||
<Button
|
||||
variant="plain"
|
||||
size="xs"
|
||||
onClick={() => handleDownloadFile(file)}
|
||||
title="İndir"
|
||||
className="text-blue-500 hover:text-blue-700"
|
||||
>
|
||||
<FaDownload />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-1 text-xs text-gray-500">
|
||||
<FaClock />
|
||||
{formatDate(activity.creationTime)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-1">
|
||||
<Button
|
||||
variant="plain"
|
||||
size="xs"
|
||||
onClick={() => handleDelete(activity)}
|
||||
title="Sil"
|
||||
>
|
||||
<FaTrash className="text-red-500" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
531
ui/src/views/form/FormActivityPanel/ActivityModals.tsx
Normal file
531
ui/src/views/form/FormActivityPanel/ActivityModals.tsx
Normal file
|
|
@ -0,0 +1,531 @@
|
|||
import React, { useState, useEffect } from 'react'
|
||||
import { Button, Input, Dialog, Select } from '@/components/ui'
|
||||
import { FaFileUpload, FaEnvelope, FaStickyNote, FaUsers, FaTimes, FaPlus, FaTrash, FaPaperclip } from 'react-icons/fa'
|
||||
import { FileData } from '@/proxy/formActivity/models'
|
||||
import { getUsers } from '@/services/identity.service'
|
||||
import { IdentityUserDto } from '@/proxy/admin/models'
|
||||
|
||||
// Birleştirilmiş Not ve Dosya Ekleme Modal'ı
|
||||
interface AddContentModalProps {
|
||||
isOpen: boolean
|
||||
onClose: () => void
|
||||
onSaveContent: (subject: string, content: string, files: File[]) => Promise<any>
|
||||
}
|
||||
|
||||
export const AddContentModal: React.FC<AddContentModalProps> = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
onSaveContent
|
||||
}) => {
|
||||
const [subject, setSubject] = useState('')
|
||||
const [content, setContent] = useState('')
|
||||
const [selectedFiles, setSelectedFiles] = useState<File[]>([])
|
||||
const [uploading, setUploading] = useState(false)
|
||||
|
||||
const handleFileSelect = (files: FileList | null) => {
|
||||
if (files && files.length > 0) {
|
||||
const newFiles = Array.from(files)
|
||||
setSelectedFiles(prev => [...prev, ...newFiles])
|
||||
}
|
||||
}
|
||||
|
||||
const removeFile = (index: number) => {
|
||||
setSelectedFiles(prev => prev.filter((_, i) => i !== index))
|
||||
}
|
||||
|
||||
const handleSave = async () => {
|
||||
if (content.trim() || selectedFiles.length > 0) {
|
||||
setUploading(true)
|
||||
try {
|
||||
await onSaveContent(subject.trim(), content.trim(), selectedFiles)
|
||||
resetForm()
|
||||
onClose()
|
||||
} catch (error) {
|
||||
console.error('Save failed:', error)
|
||||
} finally {
|
||||
setUploading(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const resetForm = () => {
|
||||
setContent('')
|
||||
setSelectedFiles([])
|
||||
setUploading(false)
|
||||
}
|
||||
|
||||
const handleClose = () => {
|
||||
resetForm()
|
||||
onClose()
|
||||
}
|
||||
|
||||
const isFormValid = content.trim() || selectedFiles.length > 0
|
||||
const totalFileSize = selectedFiles.reduce((total, file) => total + file.size, 0)
|
||||
|
||||
return (
|
||||
<Dialog isOpen={isOpen} onClose={handleClose} onRequestClose={handleClose}>
|
||||
<div className="p-6 w-full mx-auto">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<h3 className="text-xl font-semibold flex items-center gap-3">
|
||||
<div className="p-2 bg-purple-100 rounded-full">
|
||||
<FaPlus className="text-purple-600 text-lg" />
|
||||
</div>
|
||||
Not Ekle
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3 mb-2">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Konu <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<Input
|
||||
value={subject}
|
||||
onChange={(e) => setSubject(e.target.value)}
|
||||
placeholder="Mesajın konusunu girin..."
|
||||
className="w-full"
|
||||
size="sm"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Not Alanı */}
|
||||
<div>
|
||||
<label className="flex items-center gap-2 text-sm font-medium text-gray-700 mb-2">
|
||||
<FaStickyNote className="text-yellow-500" />
|
||||
Not İçeriği <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<textarea
|
||||
value={content}
|
||||
onChange={(e) => setContent(e.target.value)}
|
||||
placeholder="Notunuzu buraya yazın... (İsteğe bağlı)"
|
||||
className="w-full h-32 p-4 border border-gray-300 rounded-md resize-none focus:ring-2 focus:ring-purple-500 focus:border-transparent transition-colors duration-200"
|
||||
style={{ fontSize: '14px', lineHeight: '1.5' }}
|
||||
/>
|
||||
<div className="mt-1 text-xs text-gray-500 text-right">
|
||||
{content.length} karakter
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Dosya Yükleme Alanı */}
|
||||
<div>
|
||||
<label className="flex items-center gap-2 text-sm font-medium text-gray-700 mb-2">
|
||||
<FaPaperclip className="text-blue-500" />
|
||||
Dosya Ekle
|
||||
</label>
|
||||
|
||||
<div className="border-2 border-dashed border-gray-300 rounded-lg p-3 text-center hover:border-purple-400 transition-colors duration-200">
|
||||
<input
|
||||
type="file"
|
||||
multiple
|
||||
onChange={(e) => handleFileSelect(e.target.files)}
|
||||
className="hidden"
|
||||
id="file-upload"
|
||||
disabled={uploading}
|
||||
/>
|
||||
<label
|
||||
htmlFor="file-upload"
|
||||
className="cursor-pointer flex flex-col items-center gap-2"
|
||||
>
|
||||
<FaFileUpload className="text-2xl text-gray-400" />
|
||||
<div className="text-sm text-gray-600">
|
||||
<span className="font-medium text-purple-600 hover:text-purple-700">
|
||||
Dosya seçmek için tıklayın
|
||||
</span>
|
||||
<div className="text-xs text-gray-500 mt-1">
|
||||
Birden fazla dosya seçebilirsiniz
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{/* Seçili Dosyalar */}
|
||||
{selectedFiles.length > 0 && (
|
||||
<div className="mt-4 space-y-2">
|
||||
<div className="text-sm font-medium text-gray-700">
|
||||
Seçili Dosyalar ({selectedFiles.length})
|
||||
</div>
|
||||
<div className="space-y-2 max-h-32 overflow-y-auto">
|
||||
{selectedFiles.map((file, index) => (
|
||||
<div key={index} className="flex items-center justify-between p-3 bg-gray-50 rounded-md">
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="font-medium text-sm truncate">{file.name}</div>
|
||||
<div className="text-xs text-gray-500">
|
||||
{(file.size / 1024).toFixed(2)} KB • {file.type || 'Bilinmeyen tür'}
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
variant="plain"
|
||||
size="xs"
|
||||
onClick={() => removeFile(index)}
|
||||
className="ml-2 text-red-500 hover:text-red-700"
|
||||
disabled={uploading}
|
||||
>
|
||||
<FaTrash />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="text-xs text-gray-500">
|
||||
Toplam boyut: {(totalFileSize / 1024).toFixed(2)} KB
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between items-center pt-4 border-t border-gray-200">
|
||||
<div className="flex gap-3">
|
||||
<Button
|
||||
variant="default"
|
||||
size="md"
|
||||
onClick={handleClose}
|
||||
disabled={uploading}
|
||||
className="px-6"
|
||||
>
|
||||
İptal
|
||||
</Button>
|
||||
<Button
|
||||
variant="solid"
|
||||
size="md"
|
||||
onClick={handleSave}
|
||||
disabled={!isFormValid || uploading}
|
||||
className="px-6 flex items-center gap-2"
|
||||
>
|
||||
{uploading ? (
|
||||
<>
|
||||
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white"></div>
|
||||
Yükleniyor...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<FaPlus />
|
||||
Ekle
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
// Eski modallar geriye dönük uyumluluk için korundu
|
||||
interface AddNoteModalProps {
|
||||
isOpen: boolean
|
||||
onClose: () => void
|
||||
onSave: (content: string) => void
|
||||
}
|
||||
|
||||
export const AddNoteModal: React.FC<AddNoteModalProps> = ({ isOpen, onClose, onSave }) => {
|
||||
return (
|
||||
<AddContentModal
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
onSaveContent={async (content: string) => {
|
||||
if (content) {
|
||||
onSave(content)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
interface AddFileModalProps {
|
||||
isOpen: boolean
|
||||
onClose: () => void
|
||||
onUpload: (file: File) => Promise<FileData>
|
||||
}
|
||||
|
||||
export const AddFileModal: React.FC<AddFileModalProps> = ({ isOpen, onClose, onUpload }) => {
|
||||
return (
|
||||
<AddContentModal
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
onSaveContent={async (subject: string, content: string, files: File[]) => {
|
||||
for (const file of files) {
|
||||
await onUpload(file)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
interface UserOption {
|
||||
value: string
|
||||
label: string
|
||||
email?: string
|
||||
isActive: boolean
|
||||
}
|
||||
|
||||
interface SendMessageModalProps {
|
||||
isOpen: boolean
|
||||
onClose: () => void
|
||||
onSend: (recipients: UserOption[], subject: string, content: string) => void
|
||||
}
|
||||
|
||||
interface MessageTemplate {
|
||||
value: string
|
||||
label: string
|
||||
subject?: string
|
||||
content?: string
|
||||
}
|
||||
|
||||
const MESSAGE_TEMPLATES: MessageTemplate[] = [
|
||||
{ value: '', label: 'Şablon seçin...' },
|
||||
{ value: 'follow_up', label: 'Takip Mesajı', subject: 'Form Durumu', content: 'Merhaba,\n\nForm ile ilgili güncelleme bekliyoruz. Lütfen en kısa sürede dönüş yapabilir misiniz?\n\nTeşekkürler.' },
|
||||
{ value: 'approval', label: 'Onay Talebi', subject: 'Form İncelemesi', content: 'Sayın yetkili,\n\nForm incelemenizi ve onayınızı rica ediyoruz.\n\nSaygılarımla.' },
|
||||
{ value: 'rejection', label: 'Red Bildirimi', subject: 'Form Red Edildi', content: 'Merhaba,\n\nForm talebiniz aşağıdaki nedenlerle red edilmiştir:\n\n- [Nedeni buraya yazın]\n\nDüzeltme sonrası tekrar başvurabilirsiniz.' },
|
||||
{ value: 'completion', label: 'Tamamlanma Bildirimi', subject: 'Form Tamamlandı', content: 'Tebrikler!\n\nForm işleminiz başarıyla tamamlanmıştır.\n\nİyi günler.' }
|
||||
]
|
||||
|
||||
export const SendMessageModal: React.FC<SendMessageModalProps> = ({ isOpen, onClose, onSend }) => {
|
||||
const [selectedUsers, setSelectedUsers] = useState<UserOption[]>([])
|
||||
const [subject, setSubject] = useState('')
|
||||
const [content, setContent] = useState('')
|
||||
const [users, setUsers] = useState<UserOption[]>([])
|
||||
const [isLoadingUsers, setIsLoadingUsers] = useState(false)
|
||||
const [error, setError] = useState<string>('')
|
||||
const [selectedTemplate, setSelectedTemplate] = useState('')
|
||||
|
||||
// Load users when modal opens
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
loadUsers()
|
||||
}
|
||||
}, [isOpen])
|
||||
|
||||
const loadUsers = async () => {
|
||||
setIsLoadingUsers(true)
|
||||
setError('')
|
||||
try {
|
||||
const response = await getUsers(0, 100) // Get first 100 users
|
||||
const userOptions: UserOption[] = (response.data.items || [])
|
||||
.filter((user: IdentityUserDto) => user.isActive && user.userName)
|
||||
.map((user: IdentityUserDto) => ({
|
||||
value: user.id || '',
|
||||
label: `${user.name} ${user.surname}` || '',
|
||||
email: user.email,
|
||||
isActive: user.isActive
|
||||
}))
|
||||
|
||||
setUsers(userOptions)
|
||||
} catch (err) {
|
||||
console.error('Failed to load users:', err)
|
||||
setError('Kullanıcılar yüklenirken hata oluştu')
|
||||
} finally {
|
||||
setIsLoadingUsers(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleSend = () => {
|
||||
if (selectedUsers.length > 0 && subject.trim() && content.trim()) {
|
||||
onSend(selectedUsers, subject.trim(), content.trim())
|
||||
resetForm()
|
||||
onClose()
|
||||
}
|
||||
}
|
||||
|
||||
const handleTemplateChange = (templateValue: string) => {
|
||||
setSelectedTemplate(templateValue)
|
||||
const template = MESSAGE_TEMPLATES.find(t => t.value === templateValue)
|
||||
if (template && template.value && template.subject && template.content) {
|
||||
setSubject(template.subject)
|
||||
setContent(template.content)
|
||||
}
|
||||
}
|
||||
|
||||
const resetForm = () => {
|
||||
setSelectedUsers([])
|
||||
setSubject('')
|
||||
setContent('')
|
||||
setError('')
|
||||
setSelectedTemplate('')
|
||||
}
|
||||
|
||||
const handleClose = () => {
|
||||
resetForm()
|
||||
onClose()
|
||||
}
|
||||
|
||||
const isFormValid = selectedUsers.length > 0 && subject.trim() && content.trim()
|
||||
|
||||
const customStyles = {
|
||||
control: (provided: any) => ({
|
||||
...provided,
|
||||
minHeight: '42px',
|
||||
borderRadius: '6px',
|
||||
borderColor: '#d1d5db',
|
||||
'&:hover': {
|
||||
borderColor: '#9ca3af',
|
||||
},
|
||||
'&:focus-within': {
|
||||
borderColor: '#3b82f6',
|
||||
boxShadow: '0 0 0 2px rgba(59, 130, 246, 0.1)',
|
||||
},
|
||||
}),
|
||||
multiValue: (provided: any) => ({
|
||||
...provided,
|
||||
backgroundColor: '#e0e7ff',
|
||||
borderRadius: '4px',
|
||||
}),
|
||||
multiValueLabel: (provided: any) => ({
|
||||
...provided,
|
||||
color: '#3730a3',
|
||||
fontSize: '14px',
|
||||
}),
|
||||
multiValueRemove: (provided: any) => ({
|
||||
...provided,
|
||||
color: '#6b7280',
|
||||
'&:hover': {
|
||||
backgroundColor: '#f87171',
|
||||
color: 'white',
|
||||
},
|
||||
}),
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog isOpen={isOpen} onClose={handleClose} onRequestClose={handleClose}>
|
||||
<div className="p-6 w-full mx-auto">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<h3 className="text-xl font-semibold flex items-center gap-3">
|
||||
<div className="p-2 bg-green-100 rounded-full">
|
||||
<FaEnvelope className="text-green-600 text-lg" />
|
||||
</div>
|
||||
Yeni Mesaj Gönder
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="mb-4 p-3 bg-red-50 border border-red-200 rounded-md">
|
||||
<p className="text-sm text-red-600">{error}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-5 mb-6">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Mesaj Şablonu
|
||||
</label>
|
||||
<Select
|
||||
value={MESSAGE_TEMPLATES.find(t => t.value === selectedTemplate)}
|
||||
onChange={(selected) => {
|
||||
const template = selected as MessageTemplate
|
||||
handleTemplateChange(template?.value || '')
|
||||
}}
|
||||
options={MESSAGE_TEMPLATES}
|
||||
placeholder="İsteğe bağlı: Hazır şablon seçin..."
|
||||
isSearchable={false}
|
||||
isClearable
|
||||
styles={{
|
||||
control: (provided: any) => ({
|
||||
...provided,
|
||||
minHeight: '38px',
|
||||
borderRadius: '6px',
|
||||
borderColor: '#e5e7eb',
|
||||
}),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="flex items-center gap-2 text-sm font-medium text-gray-700 mb-2">
|
||||
<FaUsers className="text-blue-500" />
|
||||
Alıcılar <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<Select
|
||||
isMulti
|
||||
isLoading={isLoadingUsers}
|
||||
value={selectedUsers}
|
||||
onChange={(selected) => setSelectedUsers(selected as UserOption[])}
|
||||
options={users}
|
||||
placeholder={isLoadingUsers ? "Kullanıcılar yükleniyor..." : "Kullanıcıları seçin..."}
|
||||
noOptionsMessage={() => "Kullanıcı bulunamadı"}
|
||||
loadingMessage={() => "Kullanıcılar yükleniyor..."}
|
||||
isSearchable
|
||||
styles={customStyles}
|
||||
closeMenuOnSelect={false}
|
||||
hideSelectedOptions={false}
|
||||
formatOptionLabel={(option: UserOption) => (
|
||||
<div className="flex items-center justify-between py-1">
|
||||
<div>
|
||||
<div className="font-medium text-gray-900">{option.label}</div>
|
||||
</div>
|
||||
<div className={`w-2 h-2 rounded-full ${option.isActive ? 'bg-green-400' : 'bg-gray-300'}`} />
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
{selectedUsers.length > 0 && (
|
||||
<div className="mt-2 flex items-center justify-between">
|
||||
<div className="text-sm text-gray-600">
|
||||
{selectedUsers.length} kullanıcı seçildi
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setSelectedUsers([])}
|
||||
className="text-xs text-gray-500 hover:text-red-500 underline"
|
||||
>
|
||||
Tümünü Kaldır
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Konu <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<Input
|
||||
value={subject}
|
||||
onChange={(e) => setSubject(e.target.value)}
|
||||
placeholder="Mesajın konusunu girin..."
|
||||
className="w-full"
|
||||
size="sm"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Mesaj İçeriği <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<textarea
|
||||
value={content}
|
||||
onChange={(e) => setContent(e.target.value)}
|
||||
placeholder="Mesajınızı buraya yazın..."
|
||||
className="w-full h-40 p-4 border border-gray-300 rounded-md resize-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-colors duration-200"
|
||||
style={{ fontSize: '14px', lineHeight: '1.5' }}
|
||||
/>
|
||||
<div className="mt-1 text-xs text-gray-500 text-right">
|
||||
{content.length} karakter
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between items-center pt-4 border-t border-gray-200">
|
||||
<div className="flex gap-3">
|
||||
<Button
|
||||
variant="default"
|
||||
size="md"
|
||||
onClick={handleClose}
|
||||
className="px-6"
|
||||
>
|
||||
İptal
|
||||
</Button>
|
||||
<Button
|
||||
variant="solid"
|
||||
size="md"
|
||||
onClick={handleSend}
|
||||
disabled={!isFormValid}
|
||||
className="px-6 flex items-center gap-2"
|
||||
>
|
||||
<FaEnvelope />
|
||||
Gönder
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
322
ui/src/views/form/FormActivityPanel/FormActivityPanel.tsx
Normal file
322
ui/src/views/form/FormActivityPanel/FormActivityPanel.tsx
Normal file
|
|
@ -0,0 +1,322 @@
|
|||
import React, { useState, useRef, useEffect } from 'react'
|
||||
import { useFormActivity } from './useFormActivity'
|
||||
import { AddContentModal, SendMessageModal } from './ActivityModals'
|
||||
import { ActivityList } from './ActivityList'
|
||||
import { Button, Badge } from '@/components/ui'
|
||||
import {
|
||||
FaChevronLeft,
|
||||
FaChevronRight,
|
||||
FaPlus,
|
||||
FaEnvelope,
|
||||
FaTimes,
|
||||
FaHistory,
|
||||
FaGripVertical,
|
||||
} from 'react-icons/fa'
|
||||
|
||||
interface ActivityPanelProps {
|
||||
entityName: string
|
||||
entityId: string
|
||||
isVisible: boolean
|
||||
onToggle: () => void
|
||||
}
|
||||
|
||||
export const FormActivityPanel: React.FC<ActivityPanelProps> = ({
|
||||
entityName,
|
||||
entityId,
|
||||
isVisible,
|
||||
onToggle,
|
||||
}) => {
|
||||
const [activeTab, setActiveTab] = useState<'activities' | 'notes' | 'messages'>('activities')
|
||||
const [showAddContent, setShowAddContent] = useState(false)
|
||||
const [showSendMessage, setShowSendMessage] = useState(false)
|
||||
|
||||
// Draggable button state
|
||||
const [buttonPosition, setButtonPosition] = useState({ top: '50%' })
|
||||
const [isDragging, setIsDragging] = useState(false)
|
||||
const [dragStart, setDragStart] = useState({ y: 0, startTop: 0 })
|
||||
const buttonRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
const {
|
||||
notes,
|
||||
messages,
|
||||
activities,
|
||||
addContent,
|
||||
sendMessage,
|
||||
deleteNote,
|
||||
deleteFile,
|
||||
deleteMessage,
|
||||
} = useFormActivity(entityName, entityId)
|
||||
|
||||
const handleDownloadFile = (fileData: any) => {
|
||||
// Simulate file download - gerçek implementasyonda API'dan dosya indirme yapılacak
|
||||
const link = document.createElement('a')
|
||||
link.href = '#' // gerçek dosya URL'i buraya gelecek
|
||||
link.download = fileData.fileName
|
||||
link.click()
|
||||
}
|
||||
|
||||
const getTotalCount = () => notes.length + messages.length
|
||||
|
||||
// Mouse drag handlers
|
||||
const handleMouseDown = (e: React.MouseEvent) => {
|
||||
if (!buttonRef.current) return
|
||||
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
|
||||
setIsDragging(true)
|
||||
const rect = buttonRef.current.getBoundingClientRect()
|
||||
const startTop = rect.top
|
||||
|
||||
setDragStart({
|
||||
y: e.clientY,
|
||||
startTop: startTop,
|
||||
})
|
||||
}
|
||||
|
||||
const handleMouseMove = (e: MouseEvent) => {
|
||||
if (!isDragging || !buttonRef.current) return
|
||||
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
|
||||
const deltaY = e.clientY - dragStart.y
|
||||
const newTop = dragStart.startTop + deltaY
|
||||
|
||||
// Calculate percentage based on viewport
|
||||
const viewportHeight = window.innerHeight
|
||||
const buttonHeight = buttonRef.current.offsetHeight
|
||||
|
||||
// Constrain within viewport bounds
|
||||
const minTop = 0
|
||||
const maxTop = viewportHeight - buttonHeight
|
||||
const constrainedTop = Math.max(minTop, Math.min(maxTop, newTop))
|
||||
|
||||
// Convert to percentage
|
||||
const topPercentage = (constrainedTop / viewportHeight) * 100
|
||||
|
||||
setButtonPosition({ top: `${topPercentage}%` })
|
||||
}
|
||||
|
||||
const handleMouseUp = () => {
|
||||
setIsDragging(false)
|
||||
}
|
||||
|
||||
// Add event listeners for mouse move and up
|
||||
useEffect(() => {
|
||||
if (isDragging) {
|
||||
// Disable text selection during drag
|
||||
document.body.style.userSelect = 'none'
|
||||
document.body.style.webkitUserSelect = 'none'
|
||||
|
||||
document.addEventListener('mousemove', handleMouseMove, { passive: false })
|
||||
document.addEventListener('mouseup', handleMouseUp)
|
||||
|
||||
return () => {
|
||||
// Re-enable text selection
|
||||
document.body.style.userSelect = ''
|
||||
document.body.style.webkitUserSelect = ''
|
||||
|
||||
document.removeEventListener('mousemove', handleMouseMove)
|
||||
document.removeEventListener('mouseup', handleMouseUp)
|
||||
}
|
||||
}
|
||||
}, [isDragging, dragStart])
|
||||
|
||||
// Load saved position from localStorage
|
||||
useEffect(() => {
|
||||
const savedPosition = localStorage.getItem('activityPanelButtonPosition')
|
||||
if (savedPosition) {
|
||||
try {
|
||||
const position = JSON.parse(savedPosition)
|
||||
setButtonPosition(position)
|
||||
} catch (error) {
|
||||
console.error('Failed to load button position:', error)
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
// Save position to localStorage
|
||||
useEffect(() => {
|
||||
localStorage.setItem('activityPanelButtonPosition', JSON.stringify(buttonPosition))
|
||||
}, [buttonPosition])
|
||||
|
||||
if (!entityName || !entityId) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Toggle Button - Draggable */}
|
||||
<div
|
||||
ref={buttonRef}
|
||||
className="fixed right-0 z-40"
|
||||
style={{
|
||||
top: buttonPosition.top,
|
||||
transform: 'translateY(-50%)',
|
||||
}}
|
||||
>
|
||||
<div className="group relative">
|
||||
{/* Drag Handle */}
|
||||
<div
|
||||
className={`absolute -left-2 top-1/2 transform -translate-y-1/2 opacity-0 group-hover:opacity-100 transition-opacity duration-200 select-none ${isDragging ? 'cursor-grabbing' : 'cursor-grab'}`}
|
||||
onMouseDown={handleMouseDown}
|
||||
style={{ userSelect: 'none' }}
|
||||
>
|
||||
<div className="bg-gray-600 text-white p-1 rounded-l text-xs">
|
||||
<FaGripVertical />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Main Button */}
|
||||
<Button
|
||||
variant="solid"
|
||||
size="sm"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
onToggle()
|
||||
}}
|
||||
className="rounded-l-lg rounded-r-none shadow-lg hover:shadow-xl transition-shadow"
|
||||
title={isVisible ? 'Aktivite panelini kapat' : 'Aktivite panelini aç'}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
{isVisible ? <FaChevronRight /> : <FaChevronLeft />}
|
||||
<FaHistory />
|
||||
{getTotalCount() > 0 && <Badge content={getTotalCount()} />}
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Activity Panel */}
|
||||
<div
|
||||
className={`fixed right-0 top-0 h-full bg-white border-l border-gray-300 shadow-xl transform transition-transform duration-300 ease-in-out z-30 ${
|
||||
isVisible ? 'translate-x-0' : 'translate-x-full'
|
||||
}`}
|
||||
style={{ width: '400px' }}
|
||||
>
|
||||
<div className="flex flex-col h-full">
|
||||
{/* Header */}
|
||||
<div className="p-4 border-b border-gray-200 bg-gray-50">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h3 className="text-lg font-semibold text-gray-800">Aktiviteler</h3>
|
||||
<Button variant="plain" size="xs" onClick={onToggle}>
|
||||
<FaTimes />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="text-sm text-gray-600 mb-3 flex items-center gap-2">
|
||||
<span className="font-medium">{entityName}</span>
|
||||
<code className="bg-gray-100 px-2 rounded text-gray-800 text-xs font-mono">
|
||||
<Badge className='bg-blue-100 text-blue-600' content={entityId} />
|
||||
</code>
|
||||
</div>
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className="flex gap-2 mb-3">
|
||||
<Button
|
||||
variant="solid"
|
||||
size="xs"
|
||||
onClick={() => setShowAddContent(true)}
|
||||
className="flex justify-center items-center py-4 w-full"
|
||||
>
|
||||
<FaPlus className="mr-1" />
|
||||
Not Ekle
|
||||
</Button>
|
||||
<Button
|
||||
variant="solid"
|
||||
size="xs"
|
||||
onClick={() => setShowSendMessage(true)}
|
||||
className="flex justify-center items-center py-4 w-full"
|
||||
>
|
||||
<FaEnvelope className="mr-1" />
|
||||
Mesaj Gönder
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Tabs */}
|
||||
<div className="flex border-b border-gray-200">
|
||||
<button
|
||||
onClick={() => setActiveTab('activities')}
|
||||
className={`px-3 py-2 text-sm font-medium border-b-2 transition-colors ${
|
||||
activeTab === 'activities'
|
||||
? 'border-blue-500 text-blue-600'
|
||||
: 'border-transparent text-gray-500 hover:text-gray-700'
|
||||
}`}
|
||||
>
|
||||
Tümü ({getTotalCount()})
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('notes')}
|
||||
className={`px-3 py-2 text-sm font-medium border-b-2 transition-colors ${
|
||||
activeTab === 'notes'
|
||||
? 'border-blue-500 text-blue-600'
|
||||
: 'border-transparent text-gray-500 hover:text-gray-700'
|
||||
}`}
|
||||
>
|
||||
Notlar ({notes.length})
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => setActiveTab('messages')}
|
||||
className={`px-3 py-2 text-sm font-medium border-b-2 transition-colors ${
|
||||
activeTab === 'messages'
|
||||
? 'border-blue-500 text-blue-600'
|
||||
: 'border-transparent text-gray-500 hover:text-gray-700'
|
||||
}`}
|
||||
>
|
||||
Mesajlar ({messages.length})
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex-1 overflow-y-auto p-4">
|
||||
{activeTab === 'activities' && (
|
||||
<ActivityList
|
||||
activities={activities}
|
||||
onDeleteNote={deleteNote}
|
||||
onDeleteFile={deleteFile}
|
||||
onDeleteMessage={deleteMessage}
|
||||
onDownloadFile={handleDownloadFile}
|
||||
/>
|
||||
)}
|
||||
|
||||
{activeTab === 'notes' && (
|
||||
<ActivityList
|
||||
activities={activities.filter((a) => a.type === 'note')}
|
||||
onDeleteNote={deleteNote}
|
||||
onDeleteFile={deleteFile}
|
||||
onDeleteMessage={deleteMessage}
|
||||
onDownloadFile={handleDownloadFile}
|
||||
/>
|
||||
)}
|
||||
|
||||
{activeTab === 'messages' && (
|
||||
<ActivityList
|
||||
activities={activities.filter((a) => a.type === 'message')}
|
||||
onDeleteNote={deleteNote}
|
||||
onDeleteFile={deleteFile}
|
||||
onDeleteMessage={deleteMessage}
|
||||
onDownloadFile={handleDownloadFile}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Modals */}
|
||||
<AddContentModal
|
||||
isOpen={showAddContent}
|
||||
onClose={() => setShowAddContent(false)}
|
||||
onSaveContent={addContent}
|
||||
/>
|
||||
|
||||
<SendMessageModal
|
||||
isOpen={showSendMessage}
|
||||
onClose={() => setShowSendMessage(false)}
|
||||
onSend={sendMessage}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
218
ui/src/views/form/FormActivityPanel/useFormActivity.ts
Normal file
218
ui/src/views/form/FormActivityPanel/useFormActivity.ts
Normal file
|
|
@ -0,0 +1,218 @@
|
|||
import { NoteData, FileData, MessageData, ActivityItem } from '@/proxy/formActivity/models'
|
||||
import { useState, useEffect } from 'react'
|
||||
|
||||
const STORAGE_PREFIX = 'form_activity_'
|
||||
|
||||
export const useFormActivity = (entityName: string, entityId: string) => {
|
||||
const [notes, setNotes] = useState<NoteData[]>([])
|
||||
const [files, setFiles] = useState<FileData[]>([])
|
||||
const [messages, setMessages] = useState<MessageData[]>([])
|
||||
const [activities, setActivities] = useState<ActivityItem[]>([])
|
||||
|
||||
const storageKey = `${STORAGE_PREFIX}${entityName}_${entityId}`
|
||||
|
||||
// Load data from localStorage on component mount
|
||||
useEffect(() => {
|
||||
if (!entityName || !entityId) return
|
||||
|
||||
const savedData = localStorage.getItem(storageKey)
|
||||
if (savedData) {
|
||||
try {
|
||||
const parsed = JSON.parse(savedData)
|
||||
setNotes(parsed.notes || [])
|
||||
setFiles(parsed.files || [])
|
||||
setMessages(parsed.messages || [])
|
||||
} catch (error) {
|
||||
console.error('Failed to load activity data:', error)
|
||||
}
|
||||
}
|
||||
}, [entityName, entityId])
|
||||
|
||||
// Update activities when data changes
|
||||
useEffect(() => {
|
||||
const allActivities: ActivityItem[] = []
|
||||
|
||||
// Add notes as activities (notes can contain files now)
|
||||
notes.forEach(note => {
|
||||
allActivities.push({
|
||||
id: note.id || `note_${Date.now()}`,
|
||||
type: 'note',
|
||||
subject: note.subject || '',
|
||||
content: note.content,
|
||||
creationTime: note.creationTime || new Date(),
|
||||
creatorId: note.creatorId || 'Bilinmeyen',
|
||||
data: note
|
||||
})
|
||||
})
|
||||
|
||||
// Add messages as activities
|
||||
messages.forEach(message => {
|
||||
allActivities.push({
|
||||
id: message.id || `message_${Date.now()}`,
|
||||
type: 'message',
|
||||
subject: message.subject || '',
|
||||
recipientUserName: message.recipientUserName,
|
||||
content: message.content,
|
||||
creationTime: message.creationTime || new Date(),
|
||||
creatorId: message.creatorId || 'Bilinmeyen',
|
||||
data: message
|
||||
})
|
||||
})
|
||||
|
||||
// Sort by timestamp (newest first)
|
||||
allActivities.sort((a, b) => new Date(b.creationTime).getTime() - new Date(a.creationTime).getTime())
|
||||
setActivities(allActivities)
|
||||
}, [notes, files, messages])
|
||||
|
||||
// Save to localStorage whenever data changes
|
||||
useEffect(() => {
|
||||
if (!entityName || !entityId) return
|
||||
|
||||
const dataToSave = {
|
||||
notes,
|
||||
files,
|
||||
messages
|
||||
}
|
||||
localStorage.setItem(storageKey, JSON.stringify(dataToSave))
|
||||
}, [notes, files, messages, storageKey])
|
||||
|
||||
const addNote = (subject: string, content: string) => {
|
||||
const newNote: NoteData = {
|
||||
id: `note_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
||||
entityName,
|
||||
entityId,
|
||||
subject,
|
||||
content,
|
||||
creatorId: 'Mevcut Kullanıcı', // Bu gerçek implementasyonda authentication'dan gelecek
|
||||
creationTime: new Date()
|
||||
}
|
||||
setNotes(prev => [...prev, newNote])
|
||||
return newNote
|
||||
}
|
||||
|
||||
const addContent = async (subject: string, content: string, files: File[]) => {
|
||||
const timestamp = new Date()
|
||||
const baseId = `content_${timestamp.getTime()}_${Math.random().toString(36).substr(2, 9)}`
|
||||
|
||||
// Hem not hem de dosya varsa veya sadece biri varsa tek bir note aktivitesi oluştur
|
||||
const newNote: NoteData = {
|
||||
id: baseId,
|
||||
entityName,
|
||||
entityId,
|
||||
subject,
|
||||
content: content || (files.length > 0 ? `${files.length} dosya eklendi` : ''),
|
||||
creatorId: 'Mevcut Kullanıcı',
|
||||
creationTime: timestamp,
|
||||
attachedFiles: [] // Dosyaları buraya ekleyeceğiz
|
||||
}
|
||||
|
||||
// Dosyaları yükle ve note'a attach et
|
||||
const uploadedFiles: FileData[] = []
|
||||
for (const file of files) {
|
||||
try {
|
||||
const uploadedFile = await new Promise<FileData>((resolve, reject) => {
|
||||
const reader = new FileReader()
|
||||
reader.onload = () => {
|
||||
const newFile: FileData = {
|
||||
id: `${baseId}_file_${Math.random().toString(36).substr(2, 9)}`,
|
||||
entityName,
|
||||
entityId,
|
||||
fileName: file.name,
|
||||
fileSize: file.size,
|
||||
fileType: file.type,
|
||||
filePath: `uploads/${entityName}/${entityId}/${file.name}`,
|
||||
creatorId: 'Mevcut Kullanıcı',
|
||||
creationTime: timestamp
|
||||
}
|
||||
resolve(newFile)
|
||||
}
|
||||
reader.onerror = () => reject(reader.error)
|
||||
reader.readAsDataURL(file)
|
||||
})
|
||||
uploadedFiles.push(uploadedFile)
|
||||
} catch (error) {
|
||||
console.error('File upload failed:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// Note'a dosyaları ekle
|
||||
newNote.attachedFiles = uploadedFiles
|
||||
|
||||
// Note'u kaydet
|
||||
setNotes(prev => [...prev, newNote])
|
||||
|
||||
// Dosyaları ayrı ayrı da kaydet (eski sistem uyumluluğu için)
|
||||
if (uploadedFiles.length > 0) {
|
||||
setFiles(prev => [...prev, ...uploadedFiles])
|
||||
}
|
||||
|
||||
return newNote
|
||||
}
|
||||
|
||||
const addFile = (file: File) => {
|
||||
return new Promise<FileData>((resolve, reject) => {
|
||||
// Simulate file upload - gerçek implementasyonda API call yapılacak
|
||||
const reader = new FileReader()
|
||||
reader.onload = () => {
|
||||
const newFile: FileData = {
|
||||
id: `file_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
||||
entityName,
|
||||
entityId,
|
||||
fileName: file.name,
|
||||
fileSize: file.size,
|
||||
fileType: file.type,
|
||||
filePath: `uploads/${entityName}/${entityId}/${file.name}`, // Simulated path
|
||||
creatorId: 'Mevcut Kullanıcı',
|
||||
creationTime: new Date()
|
||||
}
|
||||
setFiles(prev => [...prev, newFile])
|
||||
resolve(newFile)
|
||||
}
|
||||
reader.onerror = () => reject(reader.error)
|
||||
reader.readAsDataURL(file)
|
||||
})
|
||||
}
|
||||
|
||||
const sendMessage = (recipients: Array<{ value: string, label: string, email?: string }>, subject: string, content: string) => {
|
||||
const newMessages: MessageData[] = recipients.map(recipient => ({
|
||||
id: `message_${Date.now()}_${Math.random().toString(36).substr(2, 9)}_${recipient.value}`,
|
||||
entityName,
|
||||
entityId,
|
||||
recipientUserId: recipient.value,
|
||||
recipientUserName: recipient.label,
|
||||
subject,
|
||||
content,
|
||||
sentBy: 'Mevcut Kullanıcı',
|
||||
sentAt: new Date(),
|
||||
isRead: false
|
||||
}))
|
||||
setMessages(prev => [...prev, ...newMessages])
|
||||
return newMessages
|
||||
}
|
||||
|
||||
const deleteNote = (noteId: string) => {
|
||||
setNotes(prev => prev.filter(note => note.id !== noteId))
|
||||
}
|
||||
|
||||
const deleteFile = (fileId: string) => {
|
||||
setFiles(prev => prev.filter(file => file.id !== fileId))
|
||||
}
|
||||
|
||||
const deleteMessage = (messageId: string) => {
|
||||
setMessages(prev => prev.filter(message => message.id !== messageId))
|
||||
}
|
||||
|
||||
return {
|
||||
notes,
|
||||
files,
|
||||
messages,
|
||||
activities,
|
||||
addNote,
|
||||
addFile,
|
||||
addContent,
|
||||
sendMessage,
|
||||
deleteNote,
|
||||
deleteFile,
|
||||
deleteMessage
|
||||
}
|
||||
}
|
||||
|
|
@ -9,6 +9,8 @@ import { FormProps } from './types'
|
|||
import { useGridData } from './useGridData'
|
||||
import { useCurrentMenuIcon } from '@/utils/hooks/useCurrentMenuIcon'
|
||||
import { Badge } from '@/components/ui'
|
||||
import { useState } from 'react'
|
||||
import { FormActivityPanel } from './FormActivityPanel/FormActivityPanel'
|
||||
|
||||
const FormEdit = (
|
||||
props: FormProps = {
|
||||
|
|
@ -24,6 +26,8 @@ const FormEdit = (
|
|||
const id = props?.id ?? params?.id ?? ''
|
||||
const MenuIcon = useCurrentMenuIcon('w-5 h-5')
|
||||
|
||||
const [isActivityPanelVisible, setIsActivityPanelVisible] = useState(false)
|
||||
|
||||
const { translate } = useLocalization()
|
||||
const {
|
||||
fetchData,
|
||||
|
|
@ -60,7 +64,8 @@ const FormEdit = (
|
|||
}
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<>
|
||||
<Container className={`${isActivityPanelVisible && !isSubForm ? 'mr-[400px]' : ''} transition-all duration-300`}>
|
||||
{!isSubForm && (
|
||||
<Helmet
|
||||
titleTemplate="%s | Sözsoft Kurs Platform"
|
||||
|
|
@ -116,6 +121,17 @@ const FormEdit = (
|
|||
</div>
|
||||
<SubForms gridDto={gridDto!} formData={formData} level={level ?? 0} />
|
||||
</Container>
|
||||
|
||||
{/* Activity Panel - sadece ana formda göster */}
|
||||
{!isSubForm && listFormCode && id && gridDto?.gridOptions?.showActivity && (
|
||||
<FormActivityPanel
|
||||
entityName={listFormCode}
|
||||
entityId={id}
|
||||
isVisible={isActivityPanelVisible}
|
||||
onToggle={() => setIsActivityPanelVisible(!isActivityPanelVisible)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@ import { FormProps } from './types'
|
|||
import { useGridData } from './useGridData'
|
||||
import { useCurrentMenuIcon } from '@/utils/hooks/useCurrentMenuIcon'
|
||||
import { Badge } from '@/components/ui'
|
||||
import { useState } from 'react'
|
||||
import { FormActivityPanel } from './FormActivityPanel/FormActivityPanel'
|
||||
|
||||
const FormView = (
|
||||
props: FormProps = {
|
||||
|
|
@ -25,6 +27,8 @@ const FormView = (
|
|||
const { translate } = useLocalization()
|
||||
const MenuIcon = useCurrentMenuIcon('w-5 h-5')
|
||||
|
||||
const [isActivityPanelVisible, setIsActivityPanelVisible] = useState(false)
|
||||
|
||||
const {
|
||||
loading,
|
||||
gridDto,
|
||||
|
|
@ -56,7 +60,8 @@ const FormView = (
|
|||
}
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<>
|
||||
<Container className={`${isActivityPanelVisible && !isSubForm ? 'mr-[400px]' : ''} transition-all duration-300`}>
|
||||
{!isSubForm && (
|
||||
<Helmet
|
||||
titleTemplate="%s | Sözsoft Kurs Platform"
|
||||
|
|
@ -111,6 +116,17 @@ const FormView = (
|
|||
</div>
|
||||
<SubForms gridDto={gridDto!} formData={formData} level={level ?? 0} refreshData={fetchData} />
|
||||
</Container>
|
||||
|
||||
{/* Activity Panel - sadece ana formda göster */}
|
||||
{!isSubForm && listFormCode && id && gridDto?.gridOptions?.showActivity && (
|
||||
<FormActivityPanel
|
||||
entityName={listFormCode}
|
||||
entityId={id}
|
||||
isVisible={isActivityPanelVisible}
|
||||
onToggle={() => setIsActivityPanelVisible(!isActivityPanelVisible)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue