230 lines
7.6 KiB
TypeScript
230 lines
7.6 KiB
TypeScript
import React, { useState, useRef, useEffect } from 'react'
|
||
import { NoteModal } from './NoteModal'
|
||
import { NoteList } from './NoteList'
|
||
import { Button, Badge } from '@/components/ui'
|
||
import {
|
||
FaChevronLeft,
|
||
FaChevronRight,
|
||
FaPlus,
|
||
FaTimes,
|
||
FaGripVertical,
|
||
FaChevronUp,
|
||
FaChevronDown,
|
||
} from 'react-icons/fa'
|
||
import { noteService } from '@/services/note.service'
|
||
import { NoteDto } from '@/proxy/note/models'
|
||
|
||
interface NotePanelProps {
|
||
entityName: string
|
||
entityId: string
|
||
isVisible: boolean
|
||
onToggle: () => void
|
||
}
|
||
|
||
export const NotePanel: React.FC<NotePanelProps> = ({
|
||
entityName,
|
||
entityId,
|
||
isVisible,
|
||
onToggle,
|
||
}) => {
|
||
const [showAddModal, setShowAddModal] = useState(false)
|
||
const [activities, setActivities] = useState<NoteDto[]>([])
|
||
const [buttonPosition, setButtonPosition] = useState({ top: '75%' })
|
||
const [isDragging, setIsDragging] = useState(false)
|
||
const [dragStart, setDragStart] = useState({ y: 0, startTop: 0 })
|
||
const buttonRef = useRef<HTMLDivElement>(null)
|
||
const [showEntityInfo, setShowEntityInfo] = useState(false)
|
||
|
||
// Fetch activities
|
||
const fetchActivities = async () => {
|
||
try {
|
||
const res = await noteService.getList({ entityName, entityId })
|
||
if (res?.items) setActivities(res.items)
|
||
} catch (err) {
|
||
console.error(err)
|
||
}
|
||
}
|
||
|
||
useEffect(() => {
|
||
if (isVisible) fetchActivities()
|
||
}, [isVisible])
|
||
|
||
const handleDownloadFile = async (fileData: any) => {
|
||
if (!fileData?.SavedFileName) return
|
||
try {
|
||
await noteService.downloadFile(
|
||
fileData.SavedFileName,
|
||
fileData.FileName,
|
||
fileData.FileType,
|
||
)
|
||
} catch (err) {
|
||
console.error('Dosya indirilemedi', err)
|
||
}
|
||
}
|
||
|
||
const handleDeleteActivity = async (activityId: string) => {
|
||
if (!confirm('Bu aktiviteyi silmek istediğinize emin misiniz?')) return
|
||
try {
|
||
await noteService.delete(activityId)
|
||
setActivities((prev) => prev.filter((a) => a.id !== activityId))
|
||
} catch (err) {
|
||
console.error(err)
|
||
}
|
||
}
|
||
|
||
const getTotalCount = () => activities.length
|
||
|
||
// Draggable button handlers
|
||
const handleMouseDown = (e: React.MouseEvent) => {
|
||
if (!buttonRef.current) return
|
||
e.preventDefault()
|
||
setIsDragging(true)
|
||
setDragStart({ y: e.clientY, startTop: buttonRef.current.getBoundingClientRect().top })
|
||
}
|
||
|
||
const handleMouseMove = (e: MouseEvent) => {
|
||
if (!isDragging || !buttonRef.current) return
|
||
e.preventDefault()
|
||
const deltaY = e.clientY - dragStart.y
|
||
const newTop = dragStart.startTop + deltaY
|
||
const viewportHeight = window.innerHeight
|
||
const buttonHeight = buttonRef.current.offsetHeight
|
||
const constrainedTop = Math.max(0, Math.min(viewportHeight - buttonHeight, newTop))
|
||
const topPercentage = (constrainedTop / viewportHeight) * 100
|
||
setButtonPosition({ top: `${topPercentage}%` })
|
||
}
|
||
|
||
const handleMouseUp = () => setIsDragging(false)
|
||
|
||
useEffect(() => {
|
||
if (isDragging) {
|
||
document.body.style.userSelect = 'none'
|
||
document.addEventListener('mousemove', handleMouseMove, { passive: false })
|
||
document.addEventListener('mouseup', handleMouseUp)
|
||
return () => {
|
||
document.body.style.userSelect = ''
|
||
document.removeEventListener('mousemove', handleMouseMove)
|
||
document.removeEventListener('mouseup', handleMouseUp)
|
||
}
|
||
}
|
||
}, [isDragging, dragStart])
|
||
|
||
if (!entityName || !entityId) return null
|
||
|
||
return (
|
||
<>
|
||
{/* Draggable toggle button */}
|
||
<div
|
||
ref={buttonRef}
|
||
className="fixed right-0 z-40"
|
||
style={{ top: buttonPosition.top, transform: 'translateY(-50%)' }}
|
||
>
|
||
<div className="group relative">
|
||
<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}
|
||
>
|
||
<div className="bg-gray-600 text-white p-1 rounded-l text-xs">
|
||
<FaGripVertical />
|
||
</div>
|
||
</div>
|
||
<Button
|
||
variant="solid"
|
||
size="sm"
|
||
className="!rounded-l-full !rounded-r-none"
|
||
onClick={(e) => {
|
||
e.stopPropagation()
|
||
onToggle()
|
||
}}
|
||
title={isVisible ? 'Paneli kapat' : 'Paneli aç'}
|
||
>
|
||
<div className="flex items-center gap-2">
|
||
{isVisible ? <FaChevronRight /> : <FaChevronLeft />}
|
||
{getTotalCount() > 0 && <Badge content={getTotalCount()} />}
|
||
</div>
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Overlay */}
|
||
{isVisible && (
|
||
<div className="fixed inset-0 bg-black bg-opacity-25 z-20" onClick={onToggle} />
|
||
)}
|
||
|
||
{/* 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: '450px' }}
|
||
onClick={(e) => e.stopPropagation()}
|
||
>
|
||
<div className="flex flex-col h-full">
|
||
<div className="p-4 border-b border-gray-200 bg-gray-50">
|
||
{/* Üst Satır: Başlık, Kayıt Bilgisi Toggle ve Kapat Butonu */}
|
||
<div className="flex items-center justify-between mb-3">
|
||
<h3 className="text-lg font-semibold text-gray-800">Notlar</h3>
|
||
|
||
<div className="flex items-center gap-3">
|
||
{/* 👇 Kayıt Bilgisi Aç/Kapa Butonu */}
|
||
<button
|
||
onClick={() => setShowEntityInfo((prev) => !prev)}
|
||
className="flex items-center gap-1 text-sm text-gray-600 hover:text-gray-800 cursor-pointer select-none"
|
||
title="Kayıt Bilgisi"
|
||
>
|
||
{showEntityInfo ? <FaChevronUp /> : <FaChevronDown />}
|
||
</button>
|
||
|
||
{/* Kapat Butonu */}
|
||
<Button variant="plain" size="xs" onClick={onToggle} title="Paneli kapat">
|
||
<FaTimes />
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 👇 Açılır Kayıt Bilgisi İçeriği */}
|
||
<div
|
||
className={`transition-all duration-300 overflow-hidden ${
|
||
showEntityInfo ? 'max-h-20 mt-2 opacity-100' : 'max-h-0 opacity-0'
|
||
}`}
|
||
>
|
||
<div className="flex items-center gap-1 text-sm text-gray-700">
|
||
<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>
|
||
</div>
|
||
|
||
{/* Alt buton: Not Ekle */}
|
||
<div className="flex gap-2 mt-3">
|
||
<Button
|
||
variant="solid"
|
||
size="xs"
|
||
onClick={() => setShowAddModal(true)}
|
||
className="flex justify-center items-center py-4 w-full"
|
||
>
|
||
<FaPlus className="mr-1" /> Not Ekle
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="flex-1 overflow-y-auto p-4">
|
||
<NoteList
|
||
notes={activities}
|
||
onDeleteNote={handleDeleteActivity}
|
||
onDownloadFile={handleDownloadFile}
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Modal */}
|
||
<NoteModal
|
||
entityName={entityName}
|
||
entityId={entityId}
|
||
isOpen={showAddModal}
|
||
onClose={() => setShowAddModal(false)}
|
||
onNoteAdded={(act) => setActivities((prev) => [act, ...prev])}
|
||
/>
|
||
</>
|
||
)
|
||
}
|