/* Copyright (C) 2023-2026 QuantumNous This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . For commercial licensing, please contact support@quantumnous.com */ import { useEffect, useState } from 'react' import * as z from 'zod' import { useForm } from 'react-hook-form' import { zodResolver } from '@hookform/resolvers/zod' import { Plus, Edit, Trash2, Save } from 'lucide-react' import { useTranslation } from 'react-i18next' import { toast } from 'sonner' import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from '@/components/ui/alert-dialog' import { Button } from '@/components/ui/button' import { Checkbox } from '@/components/ui/checkbox' import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage, } from '@/components/ui/form' import { Input } from '@/components/ui/input' import { Textarea } from '@/components/ui/textarea' import { StaticDataTable } from '@/components/data-table' import { Dialog } from '@/components/dialog' import { SettingsSwitchField } from '../components/settings-form-layout' import { SettingsSection } from '../components/settings-section' import { useUpdateOption } from '../hooks/use-update-option' type FAQ = { id: number question: string answer: string } type FAQSectionProps = { enabled: boolean data: string } const faqSchema = z.object({ question: z .string() .min(1, 'Question is required') .max(200, 'Question must be less than 200 characters'), answer: z .string() .min(1, 'Answer is required') .max(1000, 'Answer must be less than 1000 characters'), }) type FAQFormValues = z.infer const FAQ_FORM_ID = 'faq-form' export function FAQSection({ enabled, data }: FAQSectionProps) { const { t } = useTranslation() const updateOption = useUpdateOption() const [faqList, setFaqList] = useState([]) const [isEnabled, setIsEnabled] = useState(enabled) const [hasChanges, setHasChanges] = useState(false) const [selectedIds, setSelectedIds] = useState([]) const [showDialog, setShowDialog] = useState(false) const [showDeleteDialog, setShowDeleteDialog] = useState(false) const [editingFaq, setEditingFaq] = useState(null) const [deleteTarget, setDeleteTarget] = useState<'single' | 'batch'>('single') const form = useForm({ resolver: zodResolver(faqSchema), defaultValues: { question: '', answer: '', }, }) useEffect(() => { try { const parsed = JSON.parse(data || '[]') if (Array.isArray(parsed)) { setFaqList( parsed.map((item, idx) => ({ ...item, id: item.id || idx + 1, })) ) } } catch { setFaqList([]) } }, [data]) useEffect(() => { setIsEnabled(enabled) }, [enabled]) const handleToggleEnabled = async (checked: boolean) => { try { await updateOption.mutateAsync({ key: 'console_setting.faq_enabled', value: checked, }) setIsEnabled(checked) toast.success(t('Setting saved')) } catch { toast.error(t('Failed to update setting')) } } const handleAdd = () => { setEditingFaq(null) form.reset({ question: '', answer: '', }) setShowDialog(true) } const handleEdit = (faq: FAQ) => { setEditingFaq(faq) form.reset({ question: faq.question, answer: faq.answer, }) setShowDialog(true) } const handleDelete = (faq: FAQ) => { setEditingFaq(faq) setDeleteTarget('single') setShowDeleteDialog(true) } const handleBatchDelete = () => { if (selectedIds.length === 0) { toast.error(t('Please select items to delete')) return } setDeleteTarget('batch') setShowDeleteDialog(true) } const confirmDelete = () => { if (deleteTarget === 'single' && editingFaq) { setFaqList((prev) => prev.filter((item) => item.id !== editingFaq.id)) setHasChanges(true) toast.success(t('FAQ deleted. Click "Save Settings" to apply.')) } else if (deleteTarget === 'batch') { setFaqList((prev) => prev.filter((item) => !selectedIds.includes(item.id)) ) setSelectedIds([]) setHasChanges(true) toast.success( t('{{count}} FAQs deleted. Click "Save Settings" to apply.', { count: selectedIds.length, }) ) } setShowDeleteDialog(false) setEditingFaq(null) } const handleSubmitForm = (values: FAQFormValues) => { if (editingFaq) { setFaqList((prev) => prev.map((item) => item.id === editingFaq.id ? { ...item, ...values } : item ) ) toast.success(t('FAQ updated. Click "Save Settings" to apply.')) } else { const newId = Math.max(...faqList.map((item) => item.id), 0) + 1 setFaqList((prev) => [...prev, { id: newId, ...values }]) toast.success(t('FAQ added. Click "Save Settings" to apply.')) } setHasChanges(true) setShowDialog(false) } const handleSaveAll = async () => { try { await updateOption.mutateAsync({ key: 'console_setting.faq', value: JSON.stringify(faqList), }) setHasChanges(false) toast.success(t('FAQ saved successfully')) } catch { toast.error(t('Failed to save FAQ')) } } const toggleSelectAll = (checked: boolean) => { setSelectedIds(checked ? faqList.map((item) => item.id) : []) } const toggleSelectOne = (id: number, checked: boolean) => { setSelectedIds((prev) => checked ? [...prev, id] : prev.filter((item) => item !== id) ) } return (
faq.id} emptyContent={t('No FAQ entries yet. Click "Add FAQ" to create one.')} columns={[ { id: 'select', header: ( 0 } onCheckedChange={toggleSelectAll} /> ), className: 'w-12', cell: (faq) => ( toggleSelectOne(faq.id, checked as boolean) } /> ), }, { id: 'question', header: t('Question'), cellClassName: 'max-w-xs truncate font-medium', cell: (faq) => faq.question, }, { id: 'answer', header: t('Answer'), cellClassName: 'text-muted-foreground max-w-md truncate', cell: (faq) => faq.answer, }, { id: 'actions', header: t('Actions'), className: 'w-32', cell: (faq) => (
), }, ]} />
} >
( {t('Question')} {t('Maximum 200 characters')} )} /> ( {t('Answer')}