erp-platform/ui/src/views/forum/admin/PostManagement.tsx

411 lines
14 KiB
TypeScript
Raw Normal View History

2025-06-23 21:22:11 +00:00
import React, { useState } from 'react'
import { Plus, Edit2, Trash2, CheckCircle, Circle, Heart, Loader2 } from 'lucide-react'
import { ForumPost, ForumTopic } from '@/proxy/forum/forum'
2025-06-24 07:09:33 +00:00
import ReactQuill from 'react-quill'
import 'react-quill/dist/quill.snow.css'
import { useStoreState } from '@/store/store'
2025-06-27 15:00:47 +00:00
import { useLocalization } from '@/utils/hooks/useLocalization'
import { Formik, Form, Field, FieldProps } from 'formik'
import * as Yup from 'yup'
import { FormContainer, FormItem, Button } from '@/components/ui'
import { ConfirmDialog } from '@/components/shared'
2025-06-23 14:58:13 +00:00
interface PostManagementProps {
2025-06-23 21:22:11 +00:00
posts: ForumPost[]
topics: ForumTopic[]
loading: boolean
onCreatePost: (post: {
topicId: string
content: string
parentPostId?: string
tenantId?: string
}) => Promise<void>
2025-06-23 21:22:11 +00:00
onUpdatePost: (id: string, post: Partial<ForumPost>) => Promise<void>
onDeletePost: (id: string) => Promise<void>
onMarkPostAsAcceptedAnswer: (id: string) => Promise<void>
onUnmarkPostAsAcceptedAnswer: (id: string) => Promise<void>
2025-06-23 14:58:13 +00:00
}
2025-06-23 21:22:11 +00:00
export function PostManagement({
posts,
topics,
2025-06-23 14:58:13 +00:00
loading,
2025-06-23 21:22:11 +00:00
onCreatePost,
onUpdatePost,
2025-06-23 14:58:13 +00:00
onDeletePost,
onMarkPostAsAcceptedAnswer,
2025-06-23 21:22:11 +00:00
onUnmarkPostAsAcceptedAnswer,
2025-06-23 14:58:13 +00:00
}: PostManagementProps) {
2025-06-27 15:00:47 +00:00
const { translate } = useLocalization()
const { tenant } = useStoreState((state) => state.auth)
2025-06-24 07:09:33 +00:00
const [content, setContent] = useState('')
2025-06-23 21:22:11 +00:00
const [showCreateForm, setShowCreateForm] = useState(false)
const [editingPost, setEditingPost] = useState<ForumPost | null>(null)
2025-06-23 14:58:13 +00:00
const [formData, setFormData] = useState({
topicId: '',
parentPostId: '',
tenantId: '',
2025-06-23 14:58:13 +00:00
isAcceptedAnswer: false,
2025-06-23 21:22:11 +00:00
})
const [submitting, setSubmitting] = useState(false)
const [showConfirm, setShowConfirm] = useState(false)
const [postToDelete, setPostToDelete] = useState<ForumPost | null>(null)
const confirmDeletePost = (post: ForumPost) => {
setPostToDelete(post)
setShowConfirm(true)
}
2025-06-23 14:58:13 +00:00
const resetForm = () => {
setFormData({
topicId: '',
parentPostId: '',
tenantId: '',
2025-06-23 14:58:13 +00:00
isAcceptedAnswer: false,
2025-06-23 21:22:11 +00:00
})
setShowCreateForm(false)
setEditingPost(null)
}
2025-06-23 14:58:13 +00:00
const handleSubmit = async (e: React.FormEvent) => {
2025-06-23 21:22:11 +00:00
e.preventDefault()
2025-06-24 07:09:33 +00:00
if (submitting || !content.trim()) return
2025-06-23 14:58:13 +00:00
try {
2025-06-23 21:22:11 +00:00
setSubmitting(true)
2025-06-24 07:09:33 +00:00
const data = { ...formData, content: content.trim() }
2025-06-23 14:58:13 +00:00
if (editingPost) {
2025-06-24 07:09:33 +00:00
await onUpdatePost(editingPost.id, data)
2025-06-23 14:58:13 +00:00
} else {
2025-06-24 07:09:33 +00:00
await onCreatePost(data)
2025-06-23 14:58:13 +00:00
}
2025-06-24 07:09:33 +00:00
2025-06-23 21:22:11 +00:00
resetForm()
2025-06-23 14:58:13 +00:00
} catch (error) {
2025-06-23 21:22:11 +00:00
console.error('Error submitting form:', error)
2025-06-23 14:58:13 +00:00
} finally {
2025-06-23 21:22:11 +00:00
setSubmitting(false)
2025-06-23 14:58:13 +00:00
}
2025-06-23 21:22:11 +00:00
}
2025-06-23 14:58:13 +00:00
const handleEdit = (post: ForumPost) => {
2025-06-23 21:22:11 +00:00
setEditingPost(post)
2025-06-23 14:58:13 +00:00
setFormData({
topicId: post.topicId,
parentPostId: '',
tenantId: tenant.tenantId ?? '',
2025-06-23 14:58:13 +00:00
isAcceptedAnswer: post.isAcceptedAnswer,
2025-06-23 21:22:11 +00:00
})
2025-06-24 07:09:33 +00:00
setContent(post.content)
2025-06-23 21:22:11 +00:00
setShowCreateForm(true)
}
2025-06-23 14:58:13 +00:00
const handleToggleAcceptedAnswer = async (post: ForumPost) => {
try {
if (post.isAcceptedAnswer) {
2025-06-23 21:22:11 +00:00
await onUnmarkPostAsAcceptedAnswer(post.id)
2025-06-23 14:58:13 +00:00
} else {
2025-06-23 21:22:11 +00:00
await onMarkPostAsAcceptedAnswer(post.id)
2025-06-23 14:58:13 +00:00
}
} catch (error) {
2025-06-23 21:22:11 +00:00
console.error('Error toggling accepted answer:', error)
2025-06-23 14:58:13 +00:00
}
2025-06-23 21:22:11 +00:00
}
2025-06-23 14:58:13 +00:00
const handleDelete = async (id: string) => {
if (confirm('Are you sure you want to delete this post?')) {
try {
2025-06-23 21:22:11 +00:00
await onDeletePost(id)
2025-06-23 14:58:13 +00:00
} catch (error) {
2025-06-23 21:22:11 +00:00
console.error('Error deleting post:', error)
2025-06-23 14:58:13 +00:00
}
}
2025-06-23 21:22:11 +00:00
}
2025-06-23 14:58:13 +00:00
const getTopicTitle = (topicId: string) => {
2025-06-23 21:22:11 +00:00
const topic = topics.find((t) => t.id === topicId)
return topic ? topic.title : 'Unknown Topic'
}
const formatDate = (value: string | Date) => {
const date = value instanceof Date ? value : new Date(value)
if (isNaN(date.getTime())) return 'Invalid Date'
2025-06-23 14:58:13 +00:00
return new Intl.DateTimeFormat('en', {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
2025-06-23 21:22:11 +00:00
}).format(date)
}
2025-06-23 14:58:13 +00:00
const postValidationSchema = Yup.object().shape({
topicId: Yup.string().required('Topic is required'),
content: Yup.string()
.test('not-empty', 'Content is required', (value) => {
const plainText = value?.replace(/<[^>]+>/g, '').trim()
return !!plainText
})
.required(),
})
2025-06-23 14:58:13 +00:00
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
2025-06-27 15:00:47 +00:00
<h2 className="text-2xl font-bold text-gray-900">
{translate('::App.Forum.PostManagement.Title')}
</h2>
2025-06-23 14:58:13 +00:00
<button
onClick={() => setShowCreateForm(true)}
disabled={loading}
className="flex items-center space-x-2 bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700 transition-colors disabled:opacity-50"
>
<Plus className="w-4 h-4" />
2025-06-27 15:00:47 +00:00
<span>{translate('::App.Forum.PostManagement.AddPost')}</span>
2025-06-23 14:58:13 +00:00
</button>
</div>
{/* Create/Edit Form */}
{showCreateForm && (
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">
2025-06-27 15:00:47 +00:00
{editingPost
? translate('::App.Forum.PostManagement.EditPost')
: translate('::App.Forum.PostManagement.AddPost')}
2025-06-23 14:58:13 +00:00
</h3>
2025-06-23 21:22:11 +00:00
<Formik
initialValues={{
topicId: editingPost?.topicId || '',
content: editingPost?.content || '',
isAcceptedAnswer: editingPost?.isAcceptedAnswer || false,
parentPostId: '',
tenantId: tenant.tenantId || '',
}}
validationSchema={postValidationSchema}
enableReinitialize
onSubmit={async (values, { setSubmitting, resetForm }) => {
try {
if (editingPost) {
await onUpdatePost(editingPost.id, values)
} else {
await onCreatePost(values)
}
resetForm()
setShowCreateForm(false)
setEditingPost(null)
} finally {
setSubmitting(false)
}
}}
>
{({ values, setFieldValue, errors, touched, isSubmitting }) => (
<Form>
<FormContainer className="space-y-4">
<FormItem
label="Topic"
asterisk
invalid={!!errors.topicId && touched.topicId}
errorMessage={errors.topicId}
>
<Field
as="select"
name="topicId"
className="w-full border border-gray-300 rounded-lg px-3 py-2"
>
<option value="">Select a topic</option>
{topics.map((topic) => (
<option key={topic.id} value={topic.id}>
{topic.title}
</option>
))}
</Field>
</FormItem>
<FormItem
label="Content"
asterisk
invalid={!!errors.content && touched.content}
errorMessage={errors.content}
>
<Field name="content">
{({ field }: FieldProps) => (
<ReactQuill
theme="snow"
value={field.value}
onChange={(val) => setFieldValue('content', val)}
style={{ height: '400px', marginBottom: '50px' }}
placeholder="Write your message..."
/>
)}
</Field>
</FormItem>
2025-06-23 21:22:11 +00:00
<FormItem label="Accepted Answer">
<label className="flex items-center">
<Field type="checkbox" name="isAcceptedAnswer" className="mr-2" />
Mark as Accepted Answer
</label>
</FormItem>
2025-06-23 21:22:11 +00:00
<div className="flex justify-end space-x-3 pt-2">
<Button
variant="plain"
type="button"
onClick={() => {
setShowCreateForm(false)
setEditingPost(null)
}}
>
{translate('::Cancel')}
</Button>
<Button
variant="solid"
type="submit"
loading={isSubmitting}
disabled={
!values.topicId ||
!values.content ||
values.content.replace(/<[^>]+>/g, '').trim() === ''
}
>
{editingPost ? 'Update' : 'Create'}
</Button>
</div>
</FormContainer>
</Form>
)}
</Formik>
2025-06-23 14:58:13 +00:00
</div>
)}
{/* Posts List */}
<div className="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden">
<div className="px-6 py-4 border-b border-gray-200">
<h3 className="text-lg font-semibold text-gray-900">Posts ({posts.length})</h3>
</div>
2025-06-23 21:22:11 +00:00
2025-06-23 14:58:13 +00:00
{loading ? (
<div className="p-8 text-center">
<Loader2 className="w-8 h-8 animate-spin mx-auto mb-4 text-blue-600" />
2025-06-27 15:00:47 +00:00
<p className="text-gray-500">{translate('::App.Forum.PostManagement.Loadingtopics')}</p>
2025-06-23 14:58:13 +00:00
</div>
) : (
<div className="divide-y divide-gray-200">
{posts
2025-06-23 21:22:11 +00:00
.sort(
(a, b) => new Date(b.creationTime).getTime() - new Date(a.creationTime).getTime(),
)
2025-06-23 14:58:13 +00:00
.map((post) => (
<div key={post.id} className="p-6 hover:bg-gray-50 transition-colors">
<div className="flex items-start justify-between">
<div className="flex-1 min-w-0">
<div className="flex items-center space-x-2 mb-2">
<h4 className="text-sm font-semibold text-gray-900">{post.authorName}</h4>
{post.isAcceptedAnswer && (
<div className="flex items-center space-x-1 bg-emerald-100 text-emerald-700 px-2 py-1 rounded-full text-xs">
<CheckCircle className="w-3 h-3" />
<span>Accepted Answer</span>
</div>
)}
</div>
2025-06-23 21:22:11 +00:00
2025-06-23 14:58:13 +00:00
<div className="mb-3">
<p className="text-xs text-gray-500 mb-1">
2025-06-23 21:22:11 +00:00
Reply to:{' '}
<span className="font-medium">{getTopicTitle(post.topicId)}</span>
2025-06-23 14:58:13 +00:00
</p>
2025-06-24 07:09:33 +00:00
<p
className="text-gray-700 line-clamp-3"
dangerouslySetInnerHTML={{ __html: post.content }}
></p>
2025-06-23 14:58:13 +00:00
</div>
2025-06-23 21:22:11 +00:00
2025-06-23 14:58:13 +00:00
<div className="flex items-center justify-between text-sm text-gray-500">
<div className="flex items-center space-x-4">
<span>{formatDate(post.creationTime)}</span>
<div className="flex items-center space-x-1">
<Heart className="w-4 h-4" />
<span>{post.likeCount} likes</span>
</div>
</div>
</div>
</div>
2025-06-23 21:22:11 +00:00
2025-06-23 14:58:13 +00:00
<div className="flex items-center space-x-2 ml-4">
<button
onClick={() => handleToggleAcceptedAnswer(post)}
className={`p-2 rounded-lg transition-colors ${
post.isAcceptedAnswer
? 'text-emerald-600 hover:bg-emerald-100'
: 'text-gray-400 hover:bg-gray-100'
}`}
2025-06-23 21:22:11 +00:00
title={
post.isAcceptedAnswer
? 'Remove Accepted Answer'
: 'Mark as Accepted Answer'
}
2025-06-23 14:58:13 +00:00
>
2025-06-23 21:22:11 +00:00
{post.isAcceptedAnswer ? (
<CheckCircle className="w-4 h-4" />
) : (
<Circle className="w-4 h-4" />
)}
2025-06-23 14:58:13 +00:00
</button>
2025-06-23 21:22:11 +00:00
2025-06-23 14:58:13 +00:00
<button
onClick={() => handleEdit(post)}
className="p-2 text-blue-600 hover:bg-blue-100 rounded-lg transition-colors"
2025-06-27 15:00:47 +00:00
title={translate('::App.Forum.PostManagement.EditPost')}
2025-06-23 14:58:13 +00:00
>
<Edit2 className="w-4 h-4" />
</button>
2025-06-23 21:22:11 +00:00
2025-06-23 14:58:13 +00:00
<button
onClick={() => confirmDeletePost(post)}
2025-06-23 14:58:13 +00:00
className="p-2 text-red-600 hover:bg-red-100 rounded-lg transition-colors"
2025-06-27 15:00:47 +00:00
title={translate('::App.Forum.PostManagement.DeletePost')}
2025-06-23 14:58:13 +00:00
>
<Trash2 className="w-4 h-4" />
</button>
</div>
</div>
</div>
))}
</div>
)}
</div>
{postToDelete && (
<ConfirmDialog
isOpen={showConfirm}
type="danger"
title={translate('::DeleteConfirmation')}
confirmText={translate('::Delete')}
cancelText={translate('::Cancel')}
confirmButtonColor="red-600"
onCancel={() => {
setShowConfirm(false)
setPostToDelete(null)
}}
onConfirm={async () => {
try {
if (postToDelete) {
await onDeletePost(postToDelete.id)
}
} catch (error) {
console.error('Error deleting post:', error)
} finally {
setShowConfirm(false)
setPostToDelete(null)
}
}}
>
</ConfirmDialog>
)}
2025-06-23 14:58:13 +00:00
</div>
2025-06-23 21:22:11 +00:00
)
}