erp-platform/ui/src/views/forum/ForumManagement.tsx
2025-06-20 15:38:59 +03:00

541 lines
17 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useState, useEffect } from 'react'
import Card from '@/components/ui/Card'
import Button from '@/components/ui/Button'
import Table from '@/components/ui/Table'
import Tag from '@/components/ui/Tag'
import Dialog from '@/components/ui/Dialog'
import { FormContainer, FormItem } from '@/components/ui/Form'
import Input from '@/components/ui/Input'
import Switcher from '@/components/ui/Switcher'
import { HiPlus, HiPencil, HiTrash, HiEye, HiLockClosed, HiLockOpen } from 'react-icons/hi'
import { useNavigate } from 'react-router-dom'
import { format } from 'date-fns'
import { tr } from 'date-fns/locale'
import { Field, Form, Formik } from 'formik'
import * as Yup from 'yup'
import toast from '@/components/ui/toast'
import Notification from '@/components/ui/Notification'
import {
CreateUpdateForumCategoryDto,
ForumCategory,
forumService,
ForumTopic,
} from '@/services/forum.service'
import THead from '@/components/ui/Table/THead'
import Tr from '@/components/ui/Table/Tr'
import Th from '@/components/ui/Table/Th'
import TBody from '@/components/ui/Table/TBody'
import Td from '@/components/ui/Table/Td'
import { useLocalization } from '@/utils/hooks/useLocalization'
import { Helmet } from 'react-helmet'
const categoryValidationSchema = Yup.object().shape({
name: Yup.string().required('İsim gereklidir'),
slug: Yup.string().required('Slug gereklidir'),
description: Yup.string().required('Açıklama gereklidir'),
})
const ForumManagement = () => {
const { translate } = useLocalization()
const navigate = useNavigate()
const [activeTab, setActiveTab] = useState<'topics' | 'categories'>('topics')
const [categories, setCategories] = useState<ForumCategory[]>([])
const [topics, setTopics] = useState<ForumTopic[]>([])
const [loading, setLoading] = useState(false)
const [categoryModalVisible, setCategoryModalVisible] = useState(false)
const [editingCategory, setEditingCategory] = useState<ForumCategory | null>(null)
useEffect(() => {
loadData()
}, [activeTab])
const loadData = async () => {
setLoading(true)
try {
if (activeTab === 'categories') {
const data = await forumService.getCategories()
setCategories(data)
} else {
const data = await forumService.getTopics({ pageSize: 100 })
setTopics(data.items)
}
} catch (error) {
toast.push(
<Notification title="Hata" type="danger">
Veriler yüklenirken hata oluştu
</Notification>,
{
placement: 'top-center',
},
)
} finally {
setLoading(false)
}
}
const handleCreateCategory = () => {
setEditingCategory(null)
setCategoryModalVisible(true)
}
const handleEditCategory = (category: ForumCategory) => {
setEditingCategory(category)
setCategoryModalVisible(true)
}
const handleDeleteCategory = async (id: string) => {
try {
await forumService.deleteCategory(id)
toast.push(
<Notification title="Başarılı" type="success">
Kategori silindi
</Notification>,
{
placement: 'top-center',
},
)
loadData()
} catch (error) {
toast.push(
<Notification title="Hata" type="danger">
Silme işlemi başarısız
</Notification>,
{
placement: 'top-center',
},
)
}
}
const handleSubmitCategory = async (values: any, { setSubmitting }: any) => {
try {
const data: CreateUpdateForumCategoryDto = {
name: values.name,
slug: values.slug,
description: values.description,
icon: values.icon,
displayOrder: values.displayOrder,
isActive: values.isActive,
isLocked: values.isLocked,
}
if (editingCategory) {
await forumService.updateCategory(editingCategory.id, data)
toast.push(
<Notification title="Başarılı" type="success">
Kategori güncellendi
</Notification>,
{
placement: 'top-center',
},
)
} else {
await forumService.createCategory(data)
toast.push(
<Notification title="Başarılı" type="success">
Kategori oluşturuldu
</Notification>,
{
placement: 'top-center',
},
)
}
setCategoryModalVisible(false)
loadData()
} catch (error) {
toast.push(
<Notification title="Hata" type="danger">
İşlem başarısız
</Notification>,
{
placement: 'top-center',
},
)
} finally {
setSubmitting(false)
}
}
const handleToggleLock = async (topic: ForumTopic) => {
try {
if (topic.isLocked) {
await forumService.unlockTopic(topic.id)
toast.push(
<Notification title="Başarılı" type="success">
Konu kilidi ıldı
</Notification>,
{
placement: 'top-center',
},
)
} else {
await forumService.lockTopic(topic.id)
toast.push(
<Notification title="Başarılı" type="success">
Konu kilitlendi
</Notification>,
{
placement: 'top-center',
},
)
}
loadData()
} catch (error) {
toast.push(
<Notification title="Hata" type="danger">
İşlem başarısız
</Notification>,
{
placement: 'top-center',
},
)
}
}
const handleTogglePin = async (topic: ForumTopic) => {
try {
if (topic.isPinned) {
await forumService.unpinTopic(topic.id)
toast.push(
<Notification title="Başarılı" type="success">
Sabitleme kaldırıldı
</Notification>,
{
placement: 'top-center',
},
)
} else {
await forumService.pinTopic(topic.id)
toast.push(
<Notification title="Başarılı" type="success">
Konu sabitlendi
</Notification>,
{
placement: 'top-center',
},
)
}
loadData()
} catch (error) {
toast.push(
<Notification title="Hata" type="danger">
İşlem başarısız
</Notification>,
{
placement: 'top-center',
},
)
}
}
const initialCategoryValues = editingCategory
? {
name: editingCategory.name,
slug: editingCategory.slug,
description: editingCategory.description,
icon: editingCategory.icon || '',
displayOrder: editingCategory.displayOrder,
isActive: editingCategory.isActive,
isLocked: editingCategory.isLocked,
}
: {
name: '',
slug: '',
description: '',
icon: '',
displayOrder: 0,
isActive: true,
isLocked: false,
}
return (
<>
<Helmet
titleTemplate="%s | Kurs Platform"
title={translate('::' + 'Forum Management')}
defaultTitle="Kurs Platform"
></Helmet>
<Card>
<div className="mb-4">
<div className="flex gap-4 border-b">
<button
className={`pb-2 px-1 ${activeTab === 'topics' ? 'border-b-2 border-blue-600 text-blue-600' : 'text-gray-600'}`}
onClick={() => setActiveTab('topics')}
>
<b>Konular</b>
</button>
<button
className={`pb-2 px-1 ${activeTab === 'categories' ? 'border-b-2 border-blue-600 text-blue-600' : 'text-gray-600'}`}
onClick={() => setActiveTab('categories')}
>
<b>Kategoriler</b>
</button>
</div>
</div>
{activeTab === 'categories' ? (
<Table>
<THead>
<Tr>
<Th>İsim</Th>
<Th>Slug</Th>
<Th>ıklama</Th>
<Th>Konu Sayısı</Th>
<Th>Mesaj Sayısı</Th>
<Th>Durum</Th>
<Th>Kilit</Th>
<Th>
<Button
variant="solid"
size="xs"
icon={<HiPlus />}
onClick={handleCreateCategory}
>
Yeni
</Button>
</Th>
</Tr>
</THead>
<TBody>
{loading ? (
<Tr>
<Td colSpan={8} className="text-center">
Yükleniyor...
</Td>
</Tr>
) : (
categories.map((category) => (
<Tr key={category.id}>
<Td>
<div className="flex items-center">
{category.icon && <span className="mr-2">{category.icon}</span>}
<span className="font-medium">{category.name}</span>
</div>
</Td>
<Td>{category.slug}</Td>
<Td>{category.description}</Td>
<Td>{category.topicCount}</Td>
<Td>{category.postCount}</Td>
<Td>
<Tag
className={
category.isActive
? 'bg-green-100 text-green-800'
: 'bg-red-100 text-red-800'
}
>
{category.isActive ? 'Aktif' : 'Pasif'}
</Tag>
</Td>
<Td>
{category.isLocked ? (
<HiLockClosed className="text-red-500" />
) : (
<HiLockOpen className="text-green-500" />
)}
</Td>
<Td>
<div className="flex gap-2">
<Button
size="sm"
icon={<HiPencil />}
onClick={() => handleEditCategory(category)}
/>
<Button
size="sm"
variant="solid"
color="red-600"
icon={<HiTrash />}
onClick={() => handleDeleteCategory(category.id)}
/>
</div>
</Td>
</Tr>
))
)}
</TBody>
</Table>
) : (
<Table>
<THead>
<Tr>
<Th>Başlık</Th>
<Th>Kategori</Th>
<Th>Yazar</Th>
<Th>Görüntülenme</Th>
<Th>Cevap</Th>
<Th>Son Aktivite</Th>
<Th>İşlemler</Th>
</Tr>
</THead>
<TBody>
{loading ? (
<Tr>
<Td colSpan={7} className="text-center">
Yükleniyor...
</Td>
</Tr>
) : (
topics.map((topic) => (
<Tr key={topic.id}>
<Td>
<div>
<a
className="text-blue-600 hover:underline cursor-pointer font-medium"
onClick={() => navigate(`/forum/topic/${topic.slug || topic.id}`)}
>
{topic.title}
</a>
<div className="flex gap-2 mt-1">
{topic.isPinned && (
<Tag className="bg-yellow-100 text-yellow-800 text-xs">Sabit</Tag>
)}
{topic.isLocked && (
<Tag className="bg-red-100 text-red-800 text-xs">Kilitli</Tag>
)}
</div>
</div>
</Td>
<Td>{topic.category?.name}</Td>
<Td>{topic.author?.name}</Td>
<Td>{topic.viewCount}</Td>
<Td>{topic.replyCount}</Td>
<Td>
{topic.lastActivityAt
? format(new Date(topic.lastActivityAt), 'dd MMM yyyy HH:mm', {
locale: tr,
})
: '-'}
</Td>
<Td>
<div className="flex gap-2">
<Button
size="sm"
icon={<HiEye />}
onClick={() => navigate(`/forum/topic/${topic.slug || topic.id}`)}
/>
<Switcher
checked={topic.isPinned}
onChange={() => handleTogglePin(topic)}
checkedContent="📌"
unCheckedContent="📌"
/>
<Switcher
checked={topic.isLocked}
onChange={() => handleToggleLock(topic)}
checkedContent="🔒"
unCheckedContent="🔓"
/>
</div>
</Td>
</Tr>
))
)}
</TBody>
</Table>
)}
</Card>
<Dialog
isOpen={categoryModalVisible}
onClose={() => setCategoryModalVisible(false)}
onRequestClose={() => setCategoryModalVisible(false)}
width={600}
>
<h5 className="mb-4">{editingCategory ? 'Kategoriyi Düzenle' : 'Yeni Kategori'}</h5>
<Formik
initialValues={initialCategoryValues}
validationSchema={categoryValidationSchema}
onSubmit={handleSubmitCategory}
enableReinitialize
>
{({ values, touched, errors, isSubmitting }) => (
<Form>
<FormContainer>
<FormItem
label="İsim"
invalid={errors.name && touched.name}
errorMessage={errors.name}
>
<Field type="text" name="name" placeholder="Kategori ismi" component={Input} />
</FormItem>
<FormItem
label="Slug"
invalid={errors.slug && touched.slug}
errorMessage={errors.slug}
>
<Field type="text" name="slug" placeholder="kategori-slug" component={Input} />
</FormItem>
<FormItem
label="Açıklama"
invalid={errors.description && touched.description}
errorMessage={errors.description}
>
<Field
name="description"
placeholder="Kategori açıklaması"
component={Input}
textArea={true}
rows={3}
/>
</FormItem>
<FormItem label="İkon (Emoji)">
<Field type="text" name="icon" placeholder="📚" component={Input} />
</FormItem>
<FormItem label="Sıralama">
<Field type="number" name="displayOrder" placeholder="0" component={Input} />
</FormItem>
<FormItem>
<Field name="isActive">
{({ field, form }: any) => (
<Switcher
{...field}
onChange={(checked) => form.setFieldValue(field.name, checked)}
checkedContent="Aktif"
unCheckedContent="Pasif"
/>
)}
</Field>
</FormItem>
<FormItem>
<Field name="isLocked">
{({ field, form }: any) => (
<Switcher
{...field}
onChange={(checked) => form.setFieldValue(field.name, checked)}
checkedContent="Kilitli"
unCheckedContent="Açık"
/>
)}
</Field>
</FormItem>
<FormItem>
<div className="flex gap-2">
<Button variant="solid" type="submit" loading={isSubmitting}>
{editingCategory ? 'Güncelle' : 'Oluştur'}
</Button>
<Button variant="plain" onClick={() => setCategoryModalVisible(false)}>
İptal
</Button>
</div>
</FormItem>
</FormContainer>
</Form>
)}
</Formik>
</Dialog>
</>
)
}
export default ForumManagement