mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-30 09:58:12 +00:00
253 lines
8.0 KiB
TypeScript
253 lines
8.0 KiB
TypeScript
import React, { useState, useMemo } from "react";
|
|
import { Search, ChevronDown, Loader } from "lucide-react";
|
|
import { TemplateSampleSetting } from "@/user-management/services/TemplateConfiguration/types/templateTypes";
|
|
import { useTranslation } from "react-i18next";
|
|
|
|
interface SectionSettings {
|
|
items: TemplateSampleSetting[];
|
|
page: number;
|
|
hasMore: boolean;
|
|
loading: boolean;
|
|
}
|
|
|
|
|
|
interface SettingsPanelProps {
|
|
settings: TemplateSampleSetting[];
|
|
onSettingChange: (index: number, field: string, value: string) => void;
|
|
onLoadMore?: (sectionKey: string, nextPage: number) => void;
|
|
}
|
|
|
|
interface StyleInputsProps {
|
|
setting: TemplateSampleSetting;
|
|
index: number;
|
|
onChange: (index: number, field: string, value: string) => void;
|
|
}
|
|
|
|
const StyleInputs: React.FC<StyleInputsProps> = ({
|
|
setting,
|
|
index,
|
|
onChange,
|
|
}) => {
|
|
return (
|
|
<div className="p-4 bg-gray-50 dark:bg-gray-700 rounded-lg border border-gray-200 dark:border-gray-600 space-y-3">
|
|
<h4 className="font-medium text-sm text-gray-900 dark:text-gray-100 capitalize">
|
|
{setting.code.replace("-", " ")}
|
|
</h4>
|
|
<div className="grid grid-cols-3 gap-3">
|
|
{Object.entries(setting.value).map(([key, value]) => (
|
|
<div key={key}>
|
|
<label className="block text-xs font-medium text-gray-600 dark:text-gray-300 mb-1 capitalize">
|
|
{key.replace("-", " ")}
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={typeof value === "string" ? value : String(value ?? "")}
|
|
onChange={(e) => onChange(index, key, e.target.value)}
|
|
className="w-full px-2 py-1 text-sm border border-gray-300 dark:border-gray-600 rounded focus:outline-none focus:ring-2 focus:ring-purple-500 dark:bg-gray-600 dark:text-gray-200"
|
|
placeholder={key}
|
|
/>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
interface CollapsibleSectionProps {
|
|
title: string;
|
|
sectionKey: string;
|
|
sectionData: SectionSettings;
|
|
settings: TemplateSampleSetting[];
|
|
onSettingChange: (index: number, field: string, value: string) => void;
|
|
onLoadMore: (sectionKey: string, nextPage: number) => void;
|
|
}
|
|
|
|
const CollapsibleSection: React.FC<CollapsibleSectionProps> = ({
|
|
title,
|
|
sectionKey,
|
|
sectionData,
|
|
settings,
|
|
onSettingChange,
|
|
onLoadMore,
|
|
}) => {
|
|
const [isOpen, setIsOpen] = useState(true);
|
|
|
|
return (
|
|
<div className="border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden">
|
|
<button
|
|
onClick={() => setIsOpen(!isOpen)}
|
|
className="w-full px-4 py-3 bg-gray-50 dark:bg-gray-700 hover:bg-gray-100 dark:hover:bg-gray-600 flex items-center justify-between transition-colors"
|
|
>
|
|
<h3 className="font-semibold text-gray-900 dark:text-gray-100">{title}</h3>
|
|
<ChevronDown
|
|
className={`h-5 w-5 text-gray-600 dark:text-gray-300 transition-transform ${
|
|
isOpen ? "rotate-180" : ""
|
|
}`}
|
|
/>
|
|
</button>
|
|
|
|
{isOpen && (
|
|
<div className="p-4 space-y-4">
|
|
{sectionData.items.length === 0 ? (
|
|
<p className="text-sm text-gray-500 dark:text-gray-400">No settings available</p>
|
|
) : (
|
|
<>
|
|
{sectionData.items.map((setting) => {
|
|
const originalIndex = settings.findIndex(
|
|
(s) => s.code === setting.code,
|
|
);
|
|
return (
|
|
<StyleInputs
|
|
key={setting.code}
|
|
setting={setting}
|
|
index={originalIndex}
|
|
onChange={onSettingChange}
|
|
/>
|
|
);
|
|
})}
|
|
|
|
{sectionData.hasMore && (
|
|
<button
|
|
onClick={() =>
|
|
onLoadMore(sectionKey, sectionData.page + 1)
|
|
}
|
|
disabled={sectionData.loading}
|
|
className="w-full px-3 py-2 text-sm text-purple-600 hover:text-purple-800 hover:bg-purple-50 rounded transition-colors flex items-center justify-center gap-2 disabled:opacity-50"
|
|
>
|
|
{sectionData.loading ? (
|
|
<>
|
|
<Loader className="h-4 w-4 animate-spin" />
|
|
Loading...
|
|
</>
|
|
) : (
|
|
"Load more"
|
|
)}
|
|
</button>
|
|
)}
|
|
</>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export const SettingsPanel: React.FC<SettingsPanelProps> = ({
|
|
settings,
|
|
onSettingChange,
|
|
onLoadMore = () => {},
|
|
}) => {
|
|
const { t } = useTranslation();
|
|
const [searchTerm, setSearchTerm] = useState("");
|
|
|
|
// Flatten all settings for search
|
|
const allSettings = useMemo(() => settings, [settings]);
|
|
|
|
// Filter settings based on search term
|
|
const filteredSettings = useMemo(() => {
|
|
if (!searchTerm.trim()) return null;
|
|
|
|
return allSettings.filter(
|
|
(s) =>
|
|
s.code.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
|
Object.values(s.value).some((v) =>
|
|
String(v).toLowerCase().includes(searchTerm.toLowerCase()),
|
|
),
|
|
);
|
|
}, [searchTerm, allSettings]);
|
|
|
|
// Group settings by section (for non-search view)
|
|
const sectionSettings: Record<string, SectionSettings> = useMemo(() => {
|
|
return {
|
|
subject: {
|
|
items: allSettings.filter((s) => s.code.includes("subject")),
|
|
page: 0,
|
|
hasMore: false,
|
|
loading: false,
|
|
},
|
|
body: {
|
|
items: allSettings.filter((s) => s.code.includes("body")),
|
|
page: 0,
|
|
hasMore: false,
|
|
loading: false,
|
|
},
|
|
header: {
|
|
items: allSettings.filter((s) => s.code.includes("header")),
|
|
page: 0,
|
|
hasMore: false,
|
|
loading: false,
|
|
},
|
|
footer: {
|
|
items: allSettings.filter((s) => s.code.includes("footer")),
|
|
page: 0,
|
|
hasMore: false,
|
|
loading: false,
|
|
},
|
|
};
|
|
}, [allSettings]);
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
{/* Search Input */}
|
|
<div className="relative">
|
|
<Search className="absolute left-3 top-3 h-5 w-5 text-gray-400" />
|
|
<input
|
|
type="text"
|
|
placeholder={t("common.search", "Search settings...")}
|
|
value={searchTerm}
|
|
onChange={(e) => setSearchTerm(e.target.value)}
|
|
className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-500"
|
|
/>
|
|
</div>
|
|
|
|
{/* Filtered Results View */}
|
|
{filteredSettings !== null ? (
|
|
<div className="space-y-4">
|
|
<p className="text-sm text-gray-600">
|
|
Found {filteredSettings.length} setting
|
|
{filteredSettings.length !== 1 ? "s" : ""}
|
|
</p>
|
|
{filteredSettings.length === 0 ? (
|
|
<div className="text-center py-8">
|
|
<p className="text-gray-500">
|
|
{t("common.noResults", "No settings found")}
|
|
</p>
|
|
</div>
|
|
) : (
|
|
<div className="space-y-4">
|
|
{filteredSettings.map((setting) => {
|
|
const originalIndex = allSettings.findIndex(
|
|
(s) => s.code === setting.code,
|
|
);
|
|
return (
|
|
<StyleInputs
|
|
key={setting.code}
|
|
setting={setting}
|
|
index={originalIndex}
|
|
onChange={onSettingChange}
|
|
/>
|
|
);
|
|
})}
|
|
</div>
|
|
)}
|
|
</div>
|
|
) : (
|
|
/* Collapsible Sections View */
|
|
<div className="space-y-4">
|
|
{Object.entries(sectionSettings).map(([key, section]) => (
|
|
<CollapsibleSection
|
|
key={key}
|
|
title={key.charAt(0).toUpperCase() + key.slice(1)}
|
|
sectionKey={key}
|
|
sectionData={section}
|
|
settings={allSettings}
|
|
onSettingChange={onSettingChange}
|
|
onLoadMore={onLoadMore}
|
|
/>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|