From a3fabcde62f4288a5af0301fabe60d403290dbd1 Mon Sep 17 00:00:00 2001 From: sazzadulalambd Date: Wed, 6 May 2026 00:50:30 +0600 Subject: [PATCH] feat: add RichTextEditor component and integrate company policy management into settings page --- src/app/admin/settings/page.tsx | 287 ++++++++++++++++++++++-------- src/components/RichTextEditor.tsx | 258 +++++++++++++++++++++++++++ 2 files changed, 472 insertions(+), 73 deletions(-) create mode 100644 src/components/RichTextEditor.tsx diff --git a/src/app/admin/settings/page.tsx b/src/app/admin/settings/page.tsx index 3fbce8e..39b73db 100644 --- a/src/app/admin/settings/page.tsx +++ b/src/app/admin/settings/page.tsx @@ -1,7 +1,8 @@ 'use client'; import { useState } from 'react'; -import { Settings, Upload, Image, Globe, Mail, MessageSquare, Phone, MapPin, Link2, Clock, Save, FileText, Camera, Palette, Ruler, Sun, Moon, Monitor, Smartphone, Tablet, Package, Wrench, FileCheck, BadgeDollarSign, CreditCard, Plus, X, DollarSign, Zap, Users } from 'lucide-react'; +import { Settings, Upload, Image, Globe, Mail, MessageSquare, Phone, MapPin, Link2, Clock, Save, FileText, Camera, Palette, Ruler, Sun, Moon, Monitor, Smartphone, Tablet, Package, Wrench, FileCheck, BadgeDollarSign, CreditCard, Plus, X, DollarSign, Zap, Users, Check, Pencil } from 'lucide-react'; +import RichTextEditor from '@/components/RichTextEditor'; interface CompanySettings { name: string; @@ -71,6 +72,12 @@ interface CompanySettings { }; parts: { id: string; name: string; price?: number; minPrice?: number; maxPrice?: number; inStock: number }[]; serviceCenters: { id: string; name: string; address: string; phone: string; rating: number }[]; + companyPolicy: { + investor: { title: string; description: string; rules: { name: string; description: string }[] }; + merchant: { title: string; description: string; rules: { name: string; description: string }[] }; + swapStation: { title: string; description: string; rules: { name: string; description: string }[] }; + rentalTypes: { type: string; name: string; title: string; description: string; rules: { name: string; description: string }[]; enabled: boolean }[]; + }; rentalPolicy: { minAge: number; requireLicense: boolean; @@ -342,6 +349,64 @@ const initialSettings: CompanySettings = { { id: 'SC-001', name: 'JAIBEN Service Center - Gulshan', address: 'House 45, Road 13, Gulshan 1, Dhaka', phone: '+8801712345670', rating: 4.8 }, { id: 'SC-002', name: 'JAIBEN Service Center - Banani', address: 'Road 11, Banani, Dhaka', phone: '+8801712345671', rating: 4.5 }, ], + companyPolicy: { + investor: { + title: 'Investor Policy', + description: '

Investor Guidelines

Welcome to our investor program. This document outlines the terms and conditions for all investors participating in our EV fleet sharing initiative.

Key Requirements

Note: Past performance does not guarantee future results. Please read all terms carefully before investing.

', + rules: [ + { name: 'No smoking in vehicles', description: 'Smoking is strictly prohibited inside any vehicle in the fleet' }, + { name: 'Return with same fuel/charge level', description: 'Vehicles must be returned with the same fuel/charge level as when rented' }, + { name: 'No unauthorized drivers', description: 'Only authorized drivers listed in the agreement are permitted to drive' }, + { name: 'Follow traffic rules', description: 'All traffic laws and regulations must be followed' }, + { name: 'Report accidents within 24 hours', description: 'Any accident must be reported within 24 hours of occurrence' } + ] + }, + merchant: { + title: 'Merchant Policy', + description: '

Merchant Terms & Conditions

Thank you for joining our merchant network. These guidelines ensure smooth operations for all participating merchants.

Operational Requirements

Commission Structure

Merchants receive 15% commission on each completed delivery plus monthly bonuses for high performance.

', + rules: [ + { name: 'No smoking in vehicles', description: 'Smoking is strictly prohibited inside any vehicle in the fleet' }, + { name: 'Return with same fuel/charge level', description: 'Vehicles must be returned with the same fuel/charge level as when rented' }, + { name: 'No unauthorized drivers', description: 'Only authorized drivers listed in the agreement are permitted to drive' }, + { name: 'Follow traffic rules', description: 'All traffic laws and regulations must be followed' }, + { name: 'Report accidents within 24 hours', description: 'Any accident must be reported within 24 hours of occurrence' } + ] + }, + swapStation: { + title: 'Swap Station Policy', + description: '

Swap Station Guidelines

Welcome to our battery swap station network. Follow these safety and operational protocols for optimal service.

Safety Protocols

Operating Hours

Stations operate 24/7 for subscriber convenience. Emergency support available round the clock.

', + rules: [ + { name: 'No smoking in vehicles', description: 'Smoking is strictly prohibited inside any vehicle in the fleet' }, + { name: 'Return with same fuel/charge level', description: 'Vehicles must be returned with the same fuel/charge level as when rented' }, + { name: 'No unauthorized drivers', description: 'Only authorized drivers listed in the agreement are permitted to drive' }, + { name: 'Follow traffic rules', description: 'All traffic laws and regulations must be followed' }, + { name: 'Report accidents within 24 hours', description: 'Any accident must be reported within 24 hours of occurrence' } + ] + }, + rentalTypes: [ + { type: 'single', name: 'Rental (Single)', title: 'Rental (Single)', description: '

Single Person Rental

Perfect for individual riders who need a reliable vehicle for daily commute or delivery work.

Plan Features

Pricing

Starting from ৳400/day with deposit of ৳5,000.

', rules: [ + { name: 'No smoking in vehicles', description: 'Smoking is strictly prohibited inside any vehicle' }, + { name: 'Return with same fuel/charge level', description: 'Vehicles must be returned with the same fuel/charge level' }, + { name: 'No unauthorized drivers', description: 'Only the registered rider is permitted to drive' }, + { name: 'Follow traffic rules', description: 'All traffic laws must be followed' }, + { name: 'Report accidents within 24 hours', description: 'Any accident must be reported within 24 hours' } + ], enabled: true }, + { type: 'shared', name: 'Rental (2 Person Shared)', title: 'Rental (2 Person Shared)', description: '

Shared Rental Plan

Ideal for companions or delivery partners who want to share riding costs and responsibilities.

Plan Features

Pricing

Starting from ৳600/day (৳300 each) with deposit of ৳8,000.

', rules: [ + { name: 'No smoking in vehicles', description: 'Smoking is strictly prohibited inside any vehicle' }, + { name: 'Return with same fuel/charge level', description: 'Vehicles must be returned with the same fuel/charge level' }, + { name: 'No unauthorized drivers', description: 'Both registered riders are permitted to drive' }, + { name: 'Follow traffic rules', description: 'All traffic laws must be followed' }, + { name: 'Report accidents within 24 hours', description: 'Any accident must be reported within 24 hours' } + ], enabled: true }, + { type: 'renttoown', name: 'Rent-to-Own', title: 'Rent-to-Own', description: '

Rent-to-Own Plan

Build ownership gradually with our rent-to-own program. After completing the tenure, own the EV outright.

Program Benefits

Requirements

Good payment history required. Credit check applies.

', rules: [ + { name: 'No smoking in vehicles', description: 'Smoking is strictly prohibited inside any vehicle' }, + { name: 'Return with same fuel/charge level', description: 'Vehicles must be returned with the same fuel/charge level' }, + { name: 'No unauthorized drivers', description: 'Only the registered rider is permitted to drive' }, + { name: 'Follow traffic rules', description: 'All traffic laws must be followed' }, + { name: 'Report accidents within 24 hours', description: 'Any accident must be reported within 24 hours' } + ], enabled: true }, + ], + }, rentalPolicy: { minAge: 18, requireLicense: true, @@ -603,14 +668,17 @@ const initialSettings: CompanySettings = { export default function CompanySettingsPage() { const [settings, setSettings] = useState(initialSettings); - const [activeTab, setActiveTab] = useState<'general' | 'branding' | 'social' | 'integration' | 'landing' | 'kyc' | 'parts' | 'rental' | 'plans' | 'investment' | 'swapstation' | 'riderrequest'>('general'); - const [activeMasterTab, setActiveMasterTab] = useState<'investor' | 'merchant' | 'swapstation' | 'rental'>('investor'); + const [activeTab, setActiveTab] = useState<'general' | 'branding' | 'social' | 'integration' | 'landing' | 'kyc' | 'parts' | 'companyPolicy' | 'plans' | 'investment' | 'swapstation' | 'riderrequest'>('general'); + const [activeMasterTab, setActiveMasterTab] = useState<'investor' | 'merchant' | 'swapstation' | 'rental' | 'rentalType'>('investor'); const [saved, setSaved] = useState(false); const [activePlanTab, setActivePlanTab] = useState<'singleRent' | 'rentToOwn' | 'shareEv'>('singleRent'); const [addDocType, setAddDocType] = useState<'investor' | 'merchant' | 'swapstation' | 'rental' | null>(null); const [newDocName, setNewDocName] = useState(''); const [newDocDesc, setNewDocDesc] = useState(''); const [activeInvestTab, setActiveInvestTab] = useState(0); + const [editingPolicy, setEditingPolicy] = useState<{tab: string; index: number} | null>(null); + const [editPolicyName, setEditPolicyName] = useState(''); + const [editPolicyDesc, setEditPolicyDesc] = useState(''); const [addInvestPlan, setAddInvestPlan] = useState(false); const [newInvestName, setNewInvestName] = useState(''); const [newInvestTier, setNewInvestTier] = useState('Standard'); @@ -742,7 +810,7 @@ setNewSwapName(''); { id: 'landing', label: 'Landing Page', icon: Monitor }, { id: 'kyc', label: 'KYC Documents', icon: Package }, { id: 'parts', label: 'EV Parts', icon: Package }, - { id: 'rental', label: 'Rental Policy', icon: FileCheck }, + { id: 'companyPolicy', label: "Company's Policy", icon: FileCheck }, { id: 'plans', label: 'Plan Selection', icon: Package }, { id: 'investment', label: 'Investment Plan', icon: DollarSign }, { id: 'swapstation', label: 'Swap Station Plan', icon: Zap }, @@ -1747,85 +1815,158 @@ setNewSwapName(''); )} - {activeTab === 'rental' && ( + {activeTab === 'companyPolicy' && (
-

Rental Policy

+

Company's Policy

-
-
- - setSettings({ ...settings, rentalPolicy: { ...settings.rentalPolicy, minAge: parseInt(e.target.value) } })} - className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1" - /> -
-
- - setSettings({ ...settings, rentalPolicy: { ...settings.rentalPolicy, deposit: parseInt(e.target.value) } })} - className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1" - /> -
-
- - setSettings({ ...settings, rentalPolicy: { ...settings.rentalPolicy, lateFeePerHour: parseInt(e.target.value) } })} - className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1" - /> -
-
- - setSettings({ ...settings, rentalPolicy: { ...settings.rentalPolicy, cancellationFee: parseInt(e.target.value) } })} - className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1" - /> -
+
+ + + +
-
-
- setSettings({ ...settings, rentalPolicy: { ...settings.rentalPolicy, requireLicense: e.target.checked } })} - className="w-4 h-4" - /> - + {activeMasterTab === 'investor' && ( +
+
+ + setSettings({ ...settings, companyPolicy: { ...settings.companyPolicy, investor: { title: e.target.value, description: settings.companyPolicy?.investor?.description || '', rules: settings.companyPolicy?.investor?.rules || [] } } })} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1" /> +
+
+ + setSettings({ ...settings, companyPolicy: { ...settings.companyPolicy, investor: { title: settings.companyPolicy?.investor?.title || '', description: val, rules: settings.companyPolicy?.investor?.rules || [] } } })} placeholder="Enter policy description..." minHeight={160} /> +
+
+ +
+
+
+ +
+
+ {(settings.companyPolicy?.investor?.rules || []).map((policy, i) => ( +
+
+
+
+ {policy.name} +
+

{policy.description}

+
+
+
+ ))} +
+
-
+ )} -
-

Damage Penalties

-
- {settings.rentalPolicy.damagePenalty.map((penalty, i) => ( -
- {penalty.level} - ৳{penalty.amount} + {activeMasterTab === 'merchant' && ( +
+
+ + setSettings({ ...settings, companyPolicy: { ...settings.companyPolicy, merchant: { title: e.target.value, description: settings.companyPolicy?.merchant?.description || '', rules: settings.companyPolicy?.merchant?.rules || [] } } })} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1" /> +
+
+ + setSettings({ ...settings, companyPolicy: { ...settings.companyPolicy, merchant: { title: settings.companyPolicy?.merchant?.title || '', description: val, rules: settings.companyPolicy?.merchant?.rules || [] } } })} placeholder="Enter policy description..." minHeight={160} /> +
+
+ +
+
+
+ +
+
+ {(settings.companyPolicy?.merchant?.rules || []).map((policy, i) => ( +
+
+
+
+ {policy.name} +
+

{policy.description}

+
+
+
+ ))} +
+
+
+ )} + + {activeMasterTab === 'swapstation' && ( +
+
+ + setSettings({ ...settings, companyPolicy: { ...settings.companyPolicy, swapStation: { title: e.target.value, description: settings.companyPolicy?.swapStation?.description || '', rules: settings.companyPolicy?.swapStation?.rules || [] } } })} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1" /> +
+
+ + setSettings({ ...settings, companyPolicy: { ...settings.companyPolicy, swapStation: { title: settings.companyPolicy?.swapStation?.title || '', description: val, rules: settings.companyPolicy?.swapStation?.rules || [] } } })} placeholder="Enter policy description..." minHeight={160} /> +
+
+ +
+
+
+ +
+
+ {(settings.companyPolicy?.swapStation?.rules || []).map((policy, i) => ( +
+
+
+
+ {policy.name} +
+

{policy.description}

+
+
+
+ ))} +
+
+
+ )} + + {activeMasterTab === 'rentalType' && ( +
+ {(settings.companyPolicy?.rentalTypes || []).map((rtype, idx) => ( +
+
+
+ { + const updated = [...(settings.companyPolicy?.rentalTypes || [])]; + updated[idx] = { ...updated[idx], enabled: e.target.checked }; + setSettings({ ...settings, companyPolicy: { ...settings.companyPolicy, rentalTypes: updated } }); + }} className="w-4 h-4" /> +

{rtype.name}

+
+
+
+ { + const updated = [...(settings.companyPolicy?.rentalTypes || [])]; + updated[idx] = { ...updated[idx], title: e.target.value }; + setSettings({ ...settings, companyPolicy: { ...settings.companyPolicy, rentalTypes: updated } }); + }} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" placeholder="Policy Title" /> + { + const updated = [...(settings.companyPolicy?.rentalTypes || [])]; + updated[idx] = { ...updated[idx], description: val }; + setSettings({ ...settings, companyPolicy: { ...settings.companyPolicy, rentalTypes: updated } }); + }} placeholder="Enter policy description..." minHeight={120} /> +
))} +
+ +
-
- -
-

Rental Rules

-
- {settings.rentalPolicy.rules.map((rule, i) => ( -
- {rule} -
- ))} -
-
+ )}
- )} +)} + {activeTab === 'plans' && (
diff --git a/src/components/RichTextEditor.tsx b/src/components/RichTextEditor.tsx new file mode 100644 index 0000000..097dc7e --- /dev/null +++ b/src/components/RichTextEditor.tsx @@ -0,0 +1,258 @@ +'use client'; + +import { useState, useRef, useEffect, useCallback, Fragment } from 'react'; +import { Bold, Italic, Underline, List, ListOrdered, AlignLeft, AlignCenter, AlignRight, Undo, Redo, Table, Trash2, Rows, Columns } from 'lucide-react'; + +interface RichTextEditorProps { + value: string; + onChange: (value: string) => void; + placeholder?: string; + minHeight?: number; +} + +export default function RichTextEditor({ value, onChange, placeholder = 'Enter description...', minHeight = 150 }: RichTextEditorProps) { + const editorRef = useRef(null); + const [isInitialized, setIsInitialized] = useState(false); + const [showTableMenu, setShowTableMenu] = useState(false); + const lastValueRef = useRef(value); + + useEffect(() => { + if (editorRef.current && !isInitialized) { + editorRef.current.innerHTML = value || ''; + lastValueRef.current = value; + setIsInitialized(true); + } + }, [value, isInitialized]); + + useEffect(() => { + if (editorRef.current && isInitialized && value !== lastValueRef.current) { + if (document.activeElement !== editorRef.current) { + editorRef.current.innerHTML = value || ''; + lastValueRef.current = value; + } + } + }, [value, isInitialized]); + + const execCommand = (command: string) => { + editorRef.current?.focus(); + + if (command === 'bold') { + document.execCommand('bold', false); + } else if (command === 'italic') { + document.execCommand('italic', false); + } else if (command === 'underline') { + document.execCommand('underline', false); + } else if (command === 'insertUnorderedList') { + document.execCommand('insertUnorderedList', false); + } else if (command === 'insertOrderedList') { + document.execCommand('insertOrderedList', false); + } else if (command === 'justifyLeft') { + document.execCommand('justifyLeft', false); + } else if (command === 'justifyCenter') { + document.execCommand('justifyCenter', false); + } else if (command === 'justifyRight') { + document.execCommand('justifyRight', false); + } else if (command === 'undo') { + document.execCommand('undo', false); + } else if (command === 'redo') { + document.execCommand('redo', false); + } + + updateValue(); + }; + + const updateValue = useCallback(() => { + if (editorRef.current) { + lastValueRef.current = editorRef.current.innerHTML; + onChange(editorRef.current.innerHTML); + } + }, [onChange]); + + const insertTable = (rows: number, cols: number) => { + let tableHTML = ''; + for (let r = 0; r < rows; r++) { + tableHTML += ''; + for (let c = 0; c < cols; c++) { + const cellTag = r === 0 ? 'th' : 'td'; + const border = 'border: 1px solid #ccc; padding: 8px;'; + tableHTML += `<${cellTag} style="${border}">
`; + } + tableHTML += ''; + } + tableHTML += '


'; + + if (editorRef.current) { + editorRef.current.focus(); + document.execCommand('insertHTML', false, tableHTML); + updateValue(); + } + setShowTableMenu(false); + }; + + const insertRow = () => { + if (!editorRef.current) return; + const selection = window.getSelection(); + if (!selection || selection.rangeCount === 0) return; + + const range = selection.getRangeAt(0); + let cell = range.commonAncestorContainer; + while (cell && cell !== editorRef.current) { + if ((cell as HTMLElement).tagName === 'TD' || (cell as HTMLElement).tagName === 'TH') { + const row = (cell as HTMLElement).parentElement; + if (row && row.tagName === 'TR') { + const newRow = row.cloneNode(true) as HTMLElement; + row.parentElement?.insertBefore(newRow, row.nextSibling); + updateValue(); + break; + } + } + cell = (cell as HTMLElement).parentElement || (cell as Text).parentElement!; + } + }; + + const insertColumn = () => { + if (!editorRef.current) return; + const selection = window.getSelection(); + if (!selection || selection.rangeCount === 0) return; + + const range = selection.getRangeAt(0); + let cell = range.commonAncestorContainer; + while (cell && cell !== editorRef.current) { + if ((cell as HTMLElement).tagName === 'TD' || (cell as HTMLElement).tagName === 'TH') { + const table = (cell as HTMLElement).closest('table'); + if (table) { + const colIndex = Array.from((cell as HTMLElement).parentElement?.children || []).indexOf(cell as HTMLElement); + table.querySelectorAll('tr').forEach(row => { + const newCell = (cell as HTMLElement).cloneNode() as HTMLElement; + newCell.innerHTML = '
'; + if (row.children[colIndex]) { + row.insertBefore(newCell, row.children[colIndex]); + } else { + row.appendChild(newCell); + } + }); + updateValue(); + break; + } + } + cell = (cell as HTMLElement).parentElement || (cell as Text).parentElement!; + } + }; + + const deleteTable = () => { + if (!editorRef.current) return; + const selection = window.getSelection(); + if (!selection || selection.rangeCount === 0) return; + + const range = selection.getRangeAt(0); + let cell = range.commonAncestorContainer; + while (cell && cell !== editorRef.current) { + if ((cell as HTMLElement).tagName === 'TABLE') { + (cell as HTMLElement).remove(); + updateValue(); + break; + } + cell = (cell as HTMLElement).parentElement || (cell as Text).parentElement!; + } + }; + + const ToolbarButton = ({ onClick, children, title }: { onClick: () => void; children: React.ReactNode; title: string }) => ( + + ); + + const ToolbarDivider = () =>
; + + return ( +
+
+ execCommand('bold')} title="Bold"> + + + execCommand('italic')} title="Italic"> + + + execCommand('underline')} title="Underline"> + + + + execCommand('insertUnorderedList')} title="Bullet List"> + + + execCommand('insertOrderedList')} title="Numbered List"> + + + + execCommand('justifyLeft')} title="Align Left"> + + + execCommand('justifyCenter')} title="Align Center"> + + + execCommand('justifyRight')} title="Align Right"> + + + +
+ setShowTableMenu(!showTableMenu)} title="Insert Table"> + + + {showTableMenu && ( +
+
Insert Table
+
+ {[2, 3, 4, 5].map(rows => + + {[2, 3, 4, 5].map(cols => ( + + ))} + + )} +
+
+ + + +
+
+ )} + + + execCommand('undo')} title="Undo"> + + + execCommand('redo')} title="Redo"> + + + +
+
+ ); +} \ No newline at end of file