Files
edr-platform/apps/edr-freight-web/backoffice/src/user-management/TemplateSample/TemplateSampleForm.tsx
natib21 22cf8eabd1 fix ui
2026-07-14 07:59:17 +00:00

1759 lines
70 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 { useState, useEffect, useCallback, useRef } from "react";
import { sanitizeHtml } from "@/shared/lib/sanitize";
import { useTranslation } from "react-i18next";
import { ArrowLeft, Loader, X, Plus } from "lucide-react";
import { TemplateService } from "@/user-management/services/api/templateService";
import {
TemplateSampleRequest,
TemplateResource,
TemplateSampleData,
TemplateSampleSetting,
} from "@/user-management/services/TemplateConfiguration/types/templateTypes";
import { useTemplateFormData } from "./hooks/useTemplateFormData";
import { useHeaderFooterPresigned } from "@/record-management/components/hooks/useHeaderFooterPresigned";
import { useAuthUser } from "@/shared/hooks/useAuthUser";
import { toast } from "sonner";
import { TemplateLivePreview } from "./TemplateLivePreview";
import {
isLetterLabelsSetting,
normalizeLetterLabelsValue,
} from "./utils/letterLabelsConstants";
import {
isPageMarginSetting,
normalizePageMarginValue,
} from "./components/PageMarginEditor";
import { StyleSettingsEditor } from "./components/StyleSettingsEditor";
import CollapsibleSection from "./CollapsibleSection";
import {
normalizeListStyleProps,
styleObjectToCss,
parseCSSDeclarations,
cssStringToObject,
parseCSSStringToObject,
} from "./TemplateSampleForm.utils";
import { useListBulletResources } from "./hooks/useListBulletResources";
import { buildTemplateSamplePayload } from "./utils/templateSamplePayloadUtils";
interface TemplateSampleFormProps {
templateId: string;
onBack: () => void;
onGeneratePreview: (
pdfUrl: string,
valuesToSave: Array<{
templateSettingId: string;
resourceId: string | null;
value: Record<string, string>;
}>,
) => void;
initialPayload?: {
data: TemplateSampleData;
settings: TemplateSampleSetting[];
};
}
export const TemplateSampleForm = ({
templateId,
onBack,
onGeneratePreview,
initialPayload,
}: TemplateSampleFormProps) => {
const { t } = useTranslation();
const { defaultFormData, departments, employees } = useTemplateFormData();
const { userDetails } = useAuthUser();
const unitId = userDetails?.employee?.[0]?.unitId;
const [loading, setLoading] = useState(false);
const [template, setTemplate] = useState<TemplateResource | null>(null);
const [error, setError] = useState<string | null>(null);
const [selectedCollaborators, setSelectedCollaborators] = useState<string[]>(
[],
);
const [selectedInternalCC, setSelectedInternalCC] = useState<string[]>([]);
const [selectedExternalCC, setSelectedExternalCC] = useState<string[]>([]);
const [selectedRecipients, setSelectedRecipients] = useState<string[]>([]);
const [formData, setFormData] = useState<TemplateSampleData>({
collaborators: [
{ am: "ዮሃንስ ዶ", en: "John Doe" },
{ am: "ማርያም ስሚዝ", en: "Mary Smith" },
],
preferredLanguage: "am",
content: {
isWithDelegateSignature: false,
delegatorName: "",
body: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.\n\n- First item\n- Second item\n- Third item",
date: new Date().toISOString().split("T")[0],
internalCC: ["Finance Department", "HR Department"],
externalCC: ["External Unit 1", "External Unit 2"],
prefixCC: "CC:",
suffixCC: "---",
from: ["John Doe", "Mary Smith"],
subject: "Sample Letter Subject - Important Notice",
sincerelyText: "Sincerely",
to: ["Jane Smith", "Robert Johnson"],
prefix: "RE:",
suffix: "---",
},
recordType: "external",
});
const [selectedHeaderId, setSelectedHeaderId] = useState<string>("");
const [selectedFooterId, setSelectedFooterId] = useState<string>("");
const [selectedHeader, setSelectedHeader] = useState<string>("");
const [selectedFooter, setSelectedFooter] = useState<string>("");
// Use the same hook as record form for headers/footers
const { data: headerFooterData, isLoading: isHeaderFooterLoading } =
useHeaderFooterPresigned(unitId!);
const headers = headerFooterData?.headers || [];
const footers = headerFooterData?.footers || [];
// Settings state with default/custom tracking
const [settings, setSettings] = useState<TemplateSampleSetting[]>([]);
const [settingsLoading, setSettingsLoading] = useState(false);
const [useDefaultSettings, setUseDefaultSettings] = useState<
Record<string, boolean>
>({});
const [customSettings, setCustomSettings] = useState<Record<string, string>>(
{},
);
const [resourceIds, setResourceIds] = useState<Record<string, string | null>>(
{},
);
const { resources: bulletResources } = useListBulletResources();
const [payloadSample, setPayloadSample] = useState<TemplateSampleRequest>();
// State for collapsible sections
const [expandedSections, setExpandedSections] = useState({
basicInfo: true,
headerFooter: false,
content: false,
collaborators: false,
recipients: false,
});
const toggleSection = (section: keyof typeof expandedSections) => {
setExpandedSections((prev) => ({ ...prev, [section]: !prev[section] }));
};
useEffect(() => {
if (templateId) {
loadTemplate();
// Only load settings if they haven't been loaded yet
if (settings.length === 0) {
loadSettings();
}
}
// Cleanup timer on unmount
return () => {
if (settingsDebounceTimer.current) {
clearTimeout(settingsDebounceTimer.current);
}
};
}, [templateId]);
// Load initial payload if provided
useEffect(() => {
if (initialPayload) {
// Set form data from payload
if (initialPayload.data) {
setFormData(initialPayload.data);
}
// Set settings from payload - use the outer 'value' property
if (initialPayload.settings && initialPayload.settings.length > 0) {
const processedSettings = initialPayload.settings.map((setting) => {
let cssString = "";
// Use the outer 'value' property, not templateSettingValues
const valueToUse = setting.value;
// Typed (non-CSS) settings keep their object value intact.
if (isLetterLabelsSetting(setting.code)) {
return { ...setting, value: normalizeLetterLabelsValue(valueToUse) };
}
if (isPageMarginSetting(setting.code)) {
return { ...setting, value: normalizePageMarginValue(valueToUse) };
}
// Convert object to CSS string if needed
if (typeof valueToUse === "object" && valueToUse !== null) {
cssString = Object.entries(valueToUse)
.map(([key, value]) => {
// Skip null values but keep SVG data URIs
if (value === null || value === "null") return null;
return `${key}: ${value};`;
})
.filter(Boolean)
.join(" ");
} else if (typeof valueToUse === "string") {
cssString = valueToUse;
}
return {
...setting,
value: cssString,
};
});
setSettings(processedSettings);
// Mark all as using defaults initially
const defaultMap: Record<string, boolean> = {};
processedSettings.forEach((setting) => {
defaultMap[setting.id] = true;
});
setUseDefaultSettings(defaultMap);
}
}
}, [initialPayload]);
useEffect(() => {
if (!defaultFormData) return;
// Only update with defaultFormData if it has meaningful values
// Otherwise keep the initialized defaults from component state
setFormData((prev) => ({
...prev,
recordType: defaultFormData.recordType || prev.recordType,
preferredLanguage:
defaultFormData.preferredLanguage || prev.preferredLanguage,
collaborators:
defaultFormData.collaborators?.length > 0
? defaultFormData.collaborators
: prev.collaborators,
content: {
...prev.content,
isWithDelegateSignature:
defaultFormData.content.isWithDelegateSignature ??
prev.content.isWithDelegateSignature,
delegatorName:
defaultFormData.content.delegatorName || prev.content.delegatorName,
// Keep initialized defaults for body, subject, etc. if defaultFormData doesn't have them
body: defaultFormData.content.body || prev.content.body,
date: defaultFormData.content.date || prev.content.date,
subject: defaultFormData.content.subject?.trim()
? defaultFormData.content.subject
: prev.content.subject,
sincerelyText:
defaultFormData.content.sincerelyText || prev.content.sincerelyText,
prefix: defaultFormData.content.prefix || prev.content.prefix,
suffix: defaultFormData.content.suffix || prev.content.suffix,
// Keep initialized defaults for CC and recipients if defaultFormData doesn't have them
internalCC:
defaultFormData.content.internalCC?.length > 0
? defaultFormData.content.internalCC
: prev.content.internalCC,
externalCC:
defaultFormData.content.externalCC?.length > 0
? defaultFormData.content.externalCC
: prev.content.externalCC,
prefixCC: defaultFormData.content.prefixCC || prev.content.prefixCC,
suffixCC: defaultFormData.content.suffixCC || prev.content.suffixCC,
from:
defaultFormData.content.from?.length > 0
? defaultFormData.content.from
: prev.content.from,
to:
defaultFormData.content.to?.length > 0
? defaultFormData.content.to
: prev.content.to,
},
}));
}, [defaultFormData]);
// Keep UI selection state in sync when form data/defaults are loaded.
useEffect(() => {
if (!departments.length && !employees.length) return;
const recipientIds = (formData.content.to || [])
.map((name) => {
const dept = departments.find(
(d: any) => (d?.name?.en || d?.name?.am) === name,
);
if (dept?.id) return dept.id;
const emp = employees.find(
(e: any) => (e?.user?.name?.en || e?.user?.name?.am) === name,
);
return emp?.employeePositions?.[0]?.id || "";
})
.filter(Boolean);
const internalCcIds = (formData.content.internalCC || [])
.map((name) => {
const dept = departments.find(
(d: any) => (d?.name?.en || d?.name?.am) === name,
);
return dept?.id || "";
})
.filter(Boolean);
const collaboratorIds = (formData.collaborators || [])
.map((collab) => {
const fullName = collab.en || collab.am;
const emp = employees.find(
(e: any) => (e?.user?.name?.en || e?.user?.name?.am) === fullName,
);
return emp?.employeePositions?.[0]?.id || "";
})
.filter(Boolean);
setSelectedRecipients(recipientIds);
setSelectedInternalCC(internalCcIds);
setSelectedExternalCC(formData.content.externalCC || []);
setSelectedCollaborators(collaboratorIds);
}, [
formData.content.to,
formData.content.internalCC,
formData.content.externalCC,
formData.collaborators,
departments,
employees,
]);
// Update header/footer previews when selected or when headers/footers load
useEffect(() => {
if (headers.length) {
if (selectedHeaderId) {
const h = headers.find((x) => x.id === selectedHeaderId);
if (h)
setSelectedHeader(
`<div class="letter-header"><img src="${h.presigned}" alt="Header" style="width: 100%; max-height: 200px;" /></div>`,
);
} else {
// Show first header by default
setSelectedHeader(
`<div class="letter-header"><img src="${headers[0].presigned}" alt="Header" style="width: 100%; max-height: 200px;" /></div>`,
);
}
}
if (footers.length) {
if (selectedFooterId) {
const f = footers.find((x) => x.id === selectedFooterId);
if (f)
setSelectedFooter(
`<div class="letter-footer"><img src="${f.presigned}" alt="Footer" style="width: 100%; max-height: 200px;" /></div>`,
);
} else {
// Show first footer by default
setSelectedFooter(
`<div class="letter-footer"><img src="${footers[0].presigned}" alt="Footer" style="width: 100%; max-height: 200px;" /></div>`,
);
}
}
}, [headers, footers, selectedHeaderId, selectedFooterId]);
const loadTemplate = async () => {
try {
setLoading(true);
const data = await TemplateService.getTemplate(templateId);
setTemplate(data);
} catch (err) {
setError(t("error.loadingTemplate", "Error loading template"));
console.error(err);
} finally {
setLoading(false);
}
};
const loadSettings = async () => {
try {
setSettingsLoading(true);
setError(null);
const response =
await TemplateService.getTemplateSettingsByUnit(templateId);
const fetchedSettings = Array.isArray(response?.items)
? response.items
: [];
// ─── RAW API RESPONSE ────────────────────────────────────────────────
console.groupCollapsed(
`%c[TemplateSettings] Raw fetch — ${fetchedSettings.length} settings`,
"color:#6366f1; font-weight:bold;",
);
console.table(
fetchedSettings.map((s: any) => ({
code: s.code,
id: s.id,
rank: s.rank,
outerValue: JSON.stringify(s.value),
settingValuesCount: s.templateSettingValues?.length ?? 0,
})),
);
console.groupEnd();
// ─────────────────────────────────────────────────────────────────────
if (fetchedSettings.length === 0) {
console.warn("No template settings found for template:", templateId);
}
// Initialize settings with default values from API
const initializedSettings = fetchedSettings
.map((setting: any) => {
// Ensure setting has required fields
if (!setting.id || !setting.code) {
console.warn("Setting missing required fields:", setting);
return null;
}
let defaultValue: any = undefined;
let chosenFrom = "none";
// ── 1. Prefer the outer `value` this is the API's resolved current value
// (e.g. what was saved last time the user clicked Generate Sample)
if (setting.value !== null && setting.value !== undefined) {
defaultValue = setting.value;
chosenFrom = "outer setting.value (API resolved)";
}
// ── 2. Fall back to templateSettingValues prioritise entry with a unitId
else if (setting.templateSettingValues?.length > 0) {
const valueWithUnitId = setting.templateSettingValues.find(
(tv: any) => tv.unitId !== null && tv.unitId !== undefined,
);
if (valueWithUnitId?.value != null) {
defaultValue = valueWithUnitId.value;
chosenFrom = `templateSettingValues (unitId: ${valueWithUnitId.unitId})`;
} else {
const fallback = setting.templateSettingValues.find(
(tv: any) => tv.value !== null && tv.value !== undefined,
);
defaultValue = fallback?.value;
chosenFrom = fallback
? "templateSettingValues (no unitId)"
: "none";
}
}
// Typed (non-CSS) settings keep their object value intact.
if (isLetterLabelsSetting(setting.code)) {
return { ...setting, value: normalizeLetterLabelsValue(defaultValue) };
}
if (isPageMarginSetting(setting.code)) {
return { ...setting, value: normalizePageMarginValue(defaultValue) };
}
// Convert the resolved value object → CSS string
let cssString = "";
if (
typeof defaultValue === "object" &&
defaultValue !== null &&
Object.keys(defaultValue).length > 0
) {
cssString = Object.entries(defaultValue)
.filter(([, v]) => v !== null && v !== undefined && v !== "null")
.map(([key, value]) => `${key}: ${value};`)
.join(" ");
} else if (typeof defaultValue === "string" && defaultValue.trim()) {
cssString = defaultValue;
}
// ─── PER-SETTING DEBUG ─────────────────────────────────────────────
console.groupCollapsed(
`%c[TemplateSettings] ${setting.code}`,
"color:#0ea5e9; font-weight:bold;",
);
console.log(" Full setting object: ", setting);
console.log(" Outer value (API): ", setting.value);
console.log(
" templateSettingValues: ",
setting.templateSettingValues,
);
console.log(" ✅ Chosen from: ", chosenFrom);
console.log(" Raw defaultValue used: ", defaultValue);
console.log(
" Final CSS string: ",
cssString || "(empty — no value)",
);
console.groupEnd();
// ──────────────────────────────────────────────────────────────────
return {
...setting,
value: cssString,
};
})
.filter((s: any) => s !== null);
setSettings(initializedSettings);
const loadedResourceIds: Record<string, string | null> = {};
initializedSettings.forEach((setting) => {
if (!setting) return;
const unitValue = setting.templateSettingValues?.find(
(value: NonNullable<TemplateSampleSetting["templateSettingValues"]>[number]) =>
value.unitId != null,
);
const defaultValue = setting.templateSettingValues?.find(
(value: NonNullable<TemplateSampleSetting["templateSettingValues"]>[number]) =>
value.unitId == null,
);
loadedResourceIds[setting.id] =
setting.resourceId ??
unitValue?.resourceId ??
defaultValue?.resourceId ??
null;
});
setResourceIds(loadedResourceIds);
// Initialize all settings as using defaults
const defaultMap: Record<string, boolean> = {};
initializedSettings.forEach((setting) => {
if (setting) {
defaultMap[setting.id] = true;
}
});
setUseDefaultSettings(defaultMap);
setCustomSettings({});
} catch (err) {
console.error("Error loading settings:", err);
setError(t("error.loadingSettings", "Error loading style settings"));
toast.error(t("error.loadingSettings", "Error loading style settings"));
} finally {
setSettingsLoading(false);
}
};
const handleResourceIdChange = useCallback(
(settingId: string, resourceId: string | null) => {
setResourceIds((previous) => ({ ...previous, [settingId]: resourceId }));
},
[],
);
const handleGenerateSample = async () => {
if (!templateId) return;
try {
setLoading(true);
setError(null);
// Build header object - use first header if available
let headerObj: any = undefined;
if (selectedHeaderId) {
const selectedHeaderData = headers.find(
(h) => h.id === selectedHeaderId,
);
if (selectedHeaderData?.fileInfo) {
headerObj = selectedHeaderData.fileInfo;
}
} else if (headers.length > 0) {
// Use first header as default
headerObj = headers[0].fileInfo;
}
// Build footer object - use first footer if available
let footerObj: any = undefined;
if (selectedFooterId) {
const selectedFooterData = footers.find(
(f) => f.id === selectedFooterId,
);
if (selectedFooterData?.fileInfo) {
footerObj = selectedFooterData.fileInfo;
}
} else if (footers.length > 0) {
// Use first footer as default
footerObj = footers[0].fileInfo;
}
// Add unitId and header/footer to formData
const dataWithUnitIdAndHeaderFooter = {
...formData,
unitId: unitId || "",
...(headerObj && { header: headerObj }),
...(footerObj && { footer: footerObj }),
};
const resolveSettingValue = (
setting: TemplateSampleSetting,
): Record<string, unknown> => {
if (isLetterLabelsSetting(setting.code)) {
return normalizeLetterLabelsValue(setting.value);
}
if (isPageMarginSetting(setting.code)) {
return normalizePageMarginValue(setting.value);
}
const cssString =
typeof setting.value === "string" ? setting.value : "";
const valueObj = cssStringToObject(cssString);
if (valueObj["list-style"] === "disclosure-closed") {
const svgTriangle = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16" fill="currentColor"><path d="M4 2v12l10-6z"/></svg>`;
valueObj["list-style-image"] =
`url('data:image/svg+xml;base64,${btoa(svgTriangle)}')`;
}
if (
valueObj["list-style-image"] === "null" &&
valueObj["list-style"] === "disclosure-closed"
) {
const svgTriangle = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16" fill="currentColor"><path d="M4 2v12l10-6z"/></svg>`;
valueObj["list-style-image"] =
`url('data:image/svg+xml;base64,${btoa(svgTriangle)}')`;
}
return valueObj;
};
const payload = buildTemplateSamplePayload({
data: dataWithUnitIdAndHeaderFooter,
settings,
resourceIds,
bulletResources,
resolveSettingValue,
});
const settingsForAPI = payload.settings ?? [];
const valuesToSave = settingsForAPI
.map((setting) => {
const valueObj = setting.value as Record<string, string>;
if (!valueObj || Object.keys(valueObj).length === 0) return null;
const resourceId = setting.resourceId ?? null;
return {
templateSettingId: setting.id!,
resourceId,
value: valueObj,
};
})
.filter((item) => item !== null) as Array<{
templateSettingId: string;
resourceId: string | null;
value: Record<string, string>;
}>;
console.log("Values to save:", valuesToSave);
const response = await TemplateService.generateSampleTemplate(
templateId,
payload,
);
if (response.pdf) {
console.log("PDF URL set:", response.pdf);
onGeneratePreview(response.pdf, valuesToSave);
} else {
setError(t("error.noPdfInResponse", "No PDF in response"));
}
} catch (err: any) {
const errorMessage =
err?.response?.data?.message ||
err?.message ||
t("error.generatingSample", "Error generating sample");
console.error("Error generating sample:", err);
setError(errorMessage);
} finally {
setLoading(false);
}
};
const handleContentChange = useCallback((field: string, value: any) => {
setFormData((prev) => ({
...prev,
content: {
...prev.content,
[field]: value,
},
}));
}, []);
// Debounce timer for settings changes
const settingsDebounceTimer = useRef<NodeJS.Timeout | null>(null);
const handleSettingChange = useCallback(
(index: number, cssString: string) => {
console.log("handleSettingChange called", { index, cssString });
// ✅ Update settings immediately using functional update
setSettings((prevSettings) => {
const settingId = prevSettings[index]?.id;
if (!settingId) {
console.log("No settingId found at index", index);
return prevSettings;
}
// Create a new array with a new object for the changed item (avoid mutation)
const newSettings = prevSettings.map((s, i) =>
i === index ? { ...s, value: cssString } : s,
);
// Update related state immediately
setUseDefaultSettings((prev) => ({
...prev,
[settingId]: false,
}));
setCustomSettings((prev) => ({
...prev,
[settingId]: cssString,
}));
return newSettings;
});
},
[], // ✅ No dependencies - uses functional updates to avoid stale closures
);
const handleToggleDefault = useCallback(
(settingId: string) => {
setUseDefaultSettings((prev) => {
const newValue = !prev[settingId];
if (newValue) {
// Switching back to default - remove from custom
setCustomSettings((prevCustom) => {
const newCustom = { ...prevCustom };
delete newCustom[settingId];
return newCustom;
});
// Reset the setting value to default
const settingIndex = settings.findIndex((s) => s.id === settingId);
if (settingIndex >= 0) {
const defaultValue =
(settings[settingIndex] as any).templateSettingValues?.[0]
?.value || {};
let cssString = "";
if (typeof defaultValue === "object" && defaultValue !== null) {
cssString = Object.entries(defaultValue)
.map(([key, value]) => `${key}: ${value};`)
.join(" ");
}
setSettings((prev) => {
const newSettings = [...prev];
newSettings[settingIndex].value = cssString;
return newSettings;
});
}
}
return { ...prev, [settingId]: newValue };
});
},
[settings],
);
const handleAddCollaborator = () => {
setFormData((prev) => ({
...prev,
collaborators: [...prev.collaborators, { am: "", en: "" }],
}));
};
const handleCollaboratorChange = (
index: number,
lang: "am" | "en",
value: string,
) => {
const newCollaborators = [...formData.collaborators];
newCollaborators[index][lang] = value;
setFormData((prev) => ({
...prev,
collaborators: newCollaborators,
}));
};
const handleSelectCollaborator = (employeeId: string) => {
const employee = employees.find(
(emp: any) => emp.employeePositions?.[0]?.id === employeeId,
);
if (employee && !selectedCollaborators.includes(employeeId)) {
const newCollaborators = [
...formData.collaborators,
{
am: employee.user?.name?.am || "",
en: employee.user?.name?.en || "",
},
];
setFormData((prev) => ({
...prev,
collaborators: newCollaborators,
content: {
...prev.content,
from: newCollaborators.map((c) => c.en || c.am),
},
}));
setSelectedCollaborators([...selectedCollaborators, employeeId]);
}
};
const handleRemoveCollaborator = (index: number) => {
const newCollaborators = formData.collaborators.filter(
(_, i) => i !== index,
);
setFormData((prev) => ({
...prev,
collaborators: newCollaborators,
content: {
...prev.content,
from: newCollaborators.map((c) => c.en || c.am),
},
}));
};
const handleSelectInternalCC = (departmentId: string) => {
const dept = departments.find((d: any) => d.id === departmentId);
if (dept && !selectedInternalCC.includes(departmentId)) {
const newInternalCC = [...selectedInternalCC, departmentId];
setSelectedInternalCC(newInternalCC);
setFormData((prev) => ({
...prev,
content: {
...prev.content,
internalCC: newInternalCC.map((id) => {
const d = departments.find((d: any) => d.id === id);
return d?.name?.en || d?.name?.am || id;
}),
},
}));
}
};
const handleRemoveInternalCC = (departmentId: string) => {
const newInternalCC = selectedInternalCC.filter(
(id) => id !== departmentId,
);
setSelectedInternalCC(newInternalCC);
setFormData((prev) => ({
...prev,
content: {
...prev.content,
internalCC: newInternalCC.map((id) => {
const d = departments.find((d: any) => d.id === id);
return d?.name?.en || d?.name?.am || id;
}),
},
}));
};
const handleSelectExternalCC = (unitName: string) => {
if (!selectedExternalCC.includes(unitName)) {
const newExternalCC = [...selectedExternalCC, unitName];
setSelectedExternalCC(newExternalCC);
setFormData((prev) => ({
...prev,
content: {
...prev.content,
externalCC: newExternalCC,
},
}));
}
};
const handleRemoveExternalCC = (unitName: string) => {
const newExternalCC = selectedExternalCC.filter((u) => u !== unitName);
setSelectedExternalCC(newExternalCC);
setFormData((prev) => ({
...prev,
content: {
...prev.content,
externalCC: newExternalCC,
},
}));
};
const handleSelectRecipient = (recipientId: string) => {
if (!selectedRecipients.includes(recipientId)) {
const newRecipients = [...selectedRecipients, recipientId];
setSelectedRecipients(newRecipients);
setFormData((prev) => ({
...prev,
content: {
...prev.content,
to: newRecipients.map((id) => {
const dept = departments.find((d: any) => d.id === id);
const emp = employees.find(
(e: any) => e.employeePositions?.[0]?.id === id,
);
return (
dept?.name?.en ||
dept?.name?.am ||
emp?.user?.name?.en ||
emp?.user?.name?.am ||
id
);
}),
},
}));
}
};
const handleRemoveRecipient = (recipientId: string) => {
const newRecipients = selectedRecipients.filter((id) => id !== recipientId);
setSelectedRecipients(newRecipients);
setFormData((prev) => ({
...prev,
content: {
...prev.content,
to: newRecipients.map((id) => {
const dept = departments.find((d: any) => d.id === id);
const emp = employees.find(
(e: any) => e.employeePositions?.[0]?.id === id,
);
return (
dept?.name?.en ||
dept?.name?.am ||
emp?.user?.name?.en ||
emp?.user?.name?.am ||
id
);
}),
},
}));
};
if (loading && !template) {
return (
<div className="flex items-center justify-center h-96">
<Loader className="h-8 w-8 animate-spin text-purple-600" />
</div>
);
}
// Helper to get settings for a specific section
const getSettingsBySection = (sectionCode: string) => {
return settings.filter((s) => s.code.includes(sectionCode));
};
// Match any of the provided aliases so API code variations can still show in UI.
const getSettingsByAliases = (aliases: string[]) => {
const normalizedAliases = aliases.map((alias) => alias.toLowerCase());
return settings.filter((setting) => {
const code = setting.code.toLowerCase();
return normalizedAliases.some((alias) => code.includes(alias));
});
};
const renderSettingsEditors = (matchedSettings: TemplateSampleSetting[]) => {
if (matchedSettings.length === 0) return null;
return matchedSettings.map((setting) => {
const originalIndex = settings.findIndex((s) => s.code === setting.code);
return (
<StyleSettingsEditor
key={setting.code}
setting={setting}
index={originalIndex}
onChange={handleSettingChange}
isDefault={useDefaultSettings[setting.id] ?? true}
onToggleDefault={handleToggleDefault}
onListStyleResourceChange={(resourceId) =>
handleResourceIdChange(setting.id, resourceId)
}
/>
);
});
};
console.log("form data", formData);
console.log("settings", settings);
return (
<div className="space-y-6">
{/* Header */}
<div className="flex items-center gap-4">
<button
onClick={onBack}
className="p-2 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg transition-colors">
<ArrowLeft className="h-5 w-5 text-gray-600 dark:text-gray-400" />
</button>
<div>
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">
{t("template.sample", "Template Sample")}
</h1>
{template && (
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">
{template.name?.en || template.name?.am}
</p>
)}
</div>
</div>
{error && (
<div className="p-4 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg text-red-700 dark:text-red-400">
{error}
</div>
)}
{/* Two-column layout - grid with proper alignment */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Left column: collapsible sections (2 columns wide) */}
<div className="lg:col-span-2 space-y-4">
{/* Basic Information (unchanged) */}
<CollapsibleSection
title={t("template.basicInfo", "Basic Information")}
isExpanded={expandedSections.basicInfo}
onToggle={() => toggleSection("basicInfo")}>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
{t("template.recordType", "Record Type")}
</label>
<select
value={formData.recordType}
onChange={(e) =>
setFormData((prev) => ({
...prev,
recordType: e.target.value as "external" | "internal",
}))
}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-500">
<option value="external">
{t("template.external", "External")}
</option>
<option value="internal">
{t("template.internal", "Internal")}
</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
{t("template.preferredLanguage", "Preferred Language")}
</label>
<select
value={formData.preferredLanguage}
onChange={(e) =>
setFormData((prev) => ({
...prev,
preferredLanguage: e.target.value as "am" | "en",
}))
}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-500">
<option value="am">{t("language.amharic", "Amharic")}</option>
<option value="en">{t("language.english", "English")}</option>
</select>
</div>
</div>
</CollapsibleSection>
{/* Header & Footer Section */}
<CollapsibleSection
title={t("template.headerFooter", "Header & Footer")}
isExpanded={expandedSections.headerFooter}
onToggle={() => toggleSection("headerFooter")}>
<div className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
{t("addRecord.Header", "Header")}
</label>
<select
value={selectedHeaderId}
onChange={(e) => setSelectedHeaderId(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-500">
<option value="">
{headers?.length
? t("addRecord.Select Header", "Select Header")
: t(
"addRecord.Loading Headers...",
"Loading Headers...",
)}
</option>
{headers?.map((header) => (
<option key={header.id} value={header.id}>
{header.name?.en || header.name?.am}
</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
{t("addRecord.Footer", "Footer")}
</label>
<select
value={selectedFooterId}
onChange={(e) => setSelectedFooterId(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-500">
<option value="">
{footers?.length
? t("addRecord.Select Footer", "Select Footer")
: t(
"addRecord.Loading Footers...",
"Loading Footers...",
)}
</option>
{footers?.map((footer) => (
<option key={footer.id} value={footer.id}>
{footer.name?.en || footer.name?.am}
</option>
))}
</select>
</div>
</div>
{selectedHeader && (
<div className="bg-gray-50 dark:bg-gray-700 rounded-lg p-4 border-2 border-dashed border-gray-300 dark:border-gray-600">
<p className="text-sm font-medium text-gray-600 dark:text-gray-300 mb-2">
{t("addRecord.Header Preview", "Header Preview")}
</p>
<div
dangerouslySetInnerHTML={{
__html: sanitizeHtml(selectedHeader),
}}
/>
</div>
)}
{/* Header Style Settings */}
{getSettingsBySection("header").length > 0 && (
<div className="mt-4">
<h3 className="text-sm font-semibold text-gray-800 dark:text-gray-200 mb-3">
{t("template.headerStyleSettings", "Header Style Settings")}
</h3>
{getSettingsBySection("header").map((setting) => {
const originalIndex = settings.findIndex(
(s) => s.code === setting.code,
);
return (
<StyleSettingsEditor
key={setting.code}
setting={setting}
index={originalIndex}
onChange={handleSettingChange}
isDefault={useDefaultSettings[setting.id] ?? true}
onToggleDefault={handleToggleDefault}
/>
);
})}
</div>
)}
{selectedFooter && (
<div className="bg-gray-50 dark:bg-gray-700 rounded-lg p-4 border-2 border-dashed border-gray-300 dark:border-gray-600">
<p className="text-sm font-medium text-gray-600 dark:text-gray-300 mb-2">
{t("addRecord.Footer Preview", "Footer Preview")}
</p>
<div
dangerouslySetInnerHTML={{
__html: sanitizeHtml(selectedFooter),
}}
/>
</div>
)}
{/* Footer Style Settings */}
{getSettingsBySection("footer").length > 0 && (
<div className="mt-4">
<h3 className="text-sm font-semibold text-gray-800 dark:text-gray-200 mb-3">
{t("template.footerStyleSettings", "Footer Style Settings")}
</h3>
{getSettingsBySection("footer").map((setting) => {
const originalIndex = settings.findIndex(
(s) => s.code === setting.code,
);
return (
<StyleSettingsEditor
key={setting.code}
setting={setting}
index={originalIndex}
onChange={handleSettingChange}
isDefault={useDefaultSettings[setting.id] ?? true}
onToggleDefault={handleToggleDefault}
/>
);
})}
</div>
)}
</div>
</CollapsibleSection>
{/* Content Section */}
<CollapsibleSection
title={t("template.content", "Content")}
isExpanded={expandedSections.content}
onToggle={() => toggleSection("content")}>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
{t("template.subject", "Subject")}
</label>
<input
type="text"
value={formData.content.subject}
onChange={(e) =>
handleContentChange("subject", e.target.value)
}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-500"
/>
{/* Subject Style Settings */}
{getSettingsBySection("subject").length > 0 && (
<div className="mt-4">
<h4 className="text-sm font-semibold text-gray-800 dark:text-gray-200 mb-3">
{t(
"template.subjectStyleSettings",
"Subject Style Settings",
)}
</h4>
{getSettingsBySection("subject").map((setting) => {
const originalIndex = settings.findIndex(
(s) => s.code === setting.code,
);
return (
<StyleSettingsEditor
key={setting.code}
setting={setting}
index={originalIndex}
onChange={handleSettingChange}
isDefault={useDefaultSettings[setting.id] ?? true}
onToggleDefault={handleToggleDefault}
/>
);
})}
</div>
)}
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
{t("template.date", "Date")}
</label>
<input
type="date"
value={formData.content.date}
onChange={(e) => handleContentChange("date", e.target.value)}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
{t("template.body", "Body")}
</label>
<textarea
value={formData.content.body}
onChange={(e) => handleContentChange("body", e.target.value)}
rows={6}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-500"
/>
{/* Body Style Settings */}
{getSettingsBySection("body").length > 0 && (
<div className="mt-4">
<h4 className="text-sm font-semibold text-gray-800 dark:text-gray-200 mb-3">
{t("template.bodyStyleSettings", "Body Style Settings")}
</h4>
{getSettingsBySection("body").map((setting) => {
const originalIndex = settings.findIndex(
(s) => s.code === setting.code,
);
return (
<StyleSettingsEditor
key={setting.code}
setting={setting}
index={originalIndex}
onChange={handleSettingChange}
isDefault={useDefaultSettings[setting.id] ?? true}
onToggleDefault={handleToggleDefault}
/>
);
})}
</div>
)}
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
{t("template.sincerelyText", "Sincerely Text")}
</label>
<input
type="text"
value={formData.content.sincerelyText}
onChange={(e) =>
handleContentChange("sincerelyText", e.target.value)
}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-500"
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
{t("template.prefix", "Prefix")}
</label>
<input
type="text"
value={formData.content.prefix}
onChange={(e) =>
handleContentChange("prefix", e.target.value)
}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-500"
/>
{/* Prefix Style Settings */}
{getSettingsBySection("prefix").length > 0 && (
<div className="mt-3">
<h5 className="text-xs font-semibold text-gray-700 dark:text-gray-300 mb-2">
{t("template.prefixStyleSettings", "Prefix Style")}
</h5>
{getSettingsBySection("prefix").map((setting) => {
const originalIndex = settings.findIndex(
(s) => s.code === setting.code,
);
return (
<StyleSettingsEditor
key={setting.code}
setting={setting}
index={originalIndex}
onChange={handleSettingChange}
isDefault={useDefaultSettings[setting.id] ?? true}
onToggleDefault={handleToggleDefault}
/>
);
})}
</div>
)}
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
{t("template.suffix", "Suffix")}
</label>
<input
type="text"
value={formData.content.suffix}
onChange={(e) =>
handleContentChange("suffix", e.target.value)
}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-500"
/>
{/* Suffix Style Settings */}
{getSettingsBySection("suffix").length > 0 && (
<div className="mt-3">
<h5 className="text-xs font-semibold text-gray-700 dark:text-gray-300 mb-2">
{t("template.suffixStyleSettings", "Suffix Style")}
</h5>
{getSettingsBySection("suffix").map((setting) => {
const originalIndex = settings.findIndex(
(s) => s.code === setting.code,
);
return (
<StyleSettingsEditor
key={setting.code}
setting={setting}
index={originalIndex}
onChange={handleSettingChange}
isDefault={useDefaultSettings[setting.id] ?? true}
onToggleDefault={handleToggleDefault}
/>
);
})}
</div>
)}
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
{t("template.prefixCC", "Prefix CC")}
</label>
<input
type="text"
value={formData.content.prefixCC}
onChange={(e) =>
handleContentChange("prefixCC", e.target.value)
}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-500"
/>
{/* CC Prefix Style Settings */}
{getSettingsBySection("cc-prefix").length > 0 && (
<div className="mt-3">
<h5 className="text-xs font-semibold text-gray-700 dark:text-gray-300 mb-2">
{t("template.ccPrefixStyleSettings", "CC Prefix Style")}
</h5>
{getSettingsBySection("cc-prefix").map((setting) => {
const originalIndex = settings.findIndex(
(s) => s.code === setting.code,
);
return (
<StyleSettingsEditor
key={setting.code}
setting={setting}
index={originalIndex}
onChange={handleSettingChange}
isDefault={useDefaultSettings[setting.id] ?? true}
onToggleDefault={handleToggleDefault}
/>
);
})}
</div>
)}
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
{t("template.suffixCC", "Suffix CC")}
</label>
<input
type="text"
value={formData.content.suffixCC}
onChange={(e) =>
handleContentChange("suffixCC", e.target.value)
}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-500"
/>
{/* CC Suffix Style Settings */}
{getSettingsBySection("cc-suffix").length > 0 && (
<div className="mt-3">
<h5 className="text-xs font-semibold text-gray-700 dark:text-gray-300 mb-2">
{t("template.ccSuffixStyleSettings", "CC Suffix Style")}
</h5>
{getSettingsBySection("cc-suffix").map((setting) => {
const originalIndex = settings.findIndex(
(s) => s.code === setting.code,
);
return (
<StyleSettingsEditor
key={setting.code}
setting={setting}
index={originalIndex}
onChange={handleSettingChange}
isDefault={useDefaultSettings[setting.id] ?? true}
onToggleDefault={handleToggleDefault}
/>
);
})}
</div>
)}
</div>
</div>
<div>
<label className="flex items-center gap-2 cursor-pointer">
<input
type="checkbox"
checked={formData.content.isWithDelegateSignature}
onChange={(e) =>
handleContentChange(
"isWithDelegateSignature",
e.target.checked,
)
}
className="w-4 h-4 rounded border-gray-300 dark:border-gray-600 dark:bg-gray-800"
/>
<span className="text-sm font-medium text-gray-700 dark:text-gray-300">
{t(
"template.withDelegateSignature",
"With Delegate Signature",
)}
</span>
</label>
</div>
{formData.content.isWithDelegateSignature && (
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
{t("template.delegatorName", "Delegator Name")}
</label>
<input
type="text"
value={formData.content.delegatorName}
onChange={(e) =>
handleContentChange("delegatorName", e.target.value)
}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-500"
/>
</div>
)}
</div>
</CollapsibleSection>
{/* Collaborators (unchanged) */}
<CollapsibleSection
title={t("template.collaborators", "Collaborators")}
isExpanded={expandedSections.collaborators}
onToggle={() => toggleSection("collaborators")}>
<div className="space-y-4">
<div className="flex items-center justify-between">
<div className="flex gap-2">
<select
onChange={(e) => {
if (e.target.value) {
handleSelectCollaborator(e.target.value);
e.target.value = "";
}
}}
className="px-3 py-1 text-sm border border-gray-300 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-500">
<option value="">
{t("common.selectEmployee", "Select Employee")}
</option>
{employees.map((emp: any) => (
<option
key={emp.employeePositions?.[0]?.id}
value={emp.employeePositions?.[0]?.id}>
{emp.user?.name?.en || emp.user?.name?.am}
</option>
))}
</select>
<button
onClick={handleAddCollaborator}
className="px-3 py-1 text-sm bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors flex items-center gap-1">
<Plus className="h-4 w-4" />
{t("common.add", "Add")}
</button>
</div>
</div>
{formData.collaborators.map((collab, idx) => (
<div
key={idx}
className="grid grid-cols-2 gap-4 p-4 bg-gray-50 dark:bg-gray-700 rounded-lg relative">
<button
onClick={() => handleRemoveCollaborator(idx)}
className="absolute top-2 right-2 p-1 hover:bg-red-100 dark:hover:bg-red-900 rounded transition-colors">
<X className="h-4 w-4 text-red-600 dark:text-red-400" />
</button>
<div>
<label className="block text-xs font-medium text-gray-600 dark:text-gray-300 mb-1">
{t("language.amharic", "Amharic")}
</label>
<input
type="text"
value={collab.am}
onChange={(e) =>
handleCollaboratorChange(idx, "am", e.target.value)
}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-500 text-sm dark:bg-gray-600 dark:text-gray-200"
/>
</div>
<div>
<label className="block text-xs font-medium text-gray-600 dark:text-gray-300 mb-1">
{t("language.english", "English")}
</label>
<input
type="text"
value={collab.en}
onChange={(e) =>
handleCollaboratorChange(idx, "en", e.target.value)
}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-500 text-sm dark:bg-gray-600 dark:text-gray-200"
/>
</div>
</div>
))}
</div>
</CollapsibleSection>
{/* Recipients & CC (unchanged) */}
<CollapsibleSection
title={t("template.recipientsAndCC", "Recipients & CC")}
isExpanded={expandedSections.recipients}
onToggle={() => toggleSection("recipients")}>
<div className="space-y-6">
{/* Internal CC */}
<div>
<h3 className="text-md font-medium text-gray-800 dark:text-gray-200 mb-2">
{t("template.internalCC", "Internal CC")}
</h3>
<div className="flex items-center justify-between mb-2">
<select
onChange={(e) => {
if (e.target.value) {
handleSelectInternalCC(e.target.value);
e.target.value = "";
}
}}
className="px-3 py-1 text-sm border border-gray-300 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-500">
<option value="">
{t("common.selectDepartment", "Select Department")}
</option>
{departments.map((dept: any) => (
<option key={dept.id} value={dept.id}>
{dept.name?.en || dept.name?.am}
</option>
))}
</select>
</div>
<div className="flex flex-wrap gap-2">
{selectedInternalCC.map((deptId) => {
const dept = departments.find((d: any) => d.id === deptId);
return (
<div
key={deptId}
className="flex items-center gap-2 px-3 py-1 bg-purple-100 dark:bg-purple-900/40 text-purple-700 dark:text-purple-300 rounded-full text-sm">
<span>{dept?.name?.en || dept?.name?.am}</span>
<button
onClick={() => handleRemoveInternalCC(deptId)}
className="hover:text-purple-900 dark:hover:text-purple-100">
<X className="h-4 w-4" />
</button>
</div>
);
})}
</div>
{renderSettingsEditors(
getSettingsByAliases([
"internal-cc",
"internalcc",
"cc-internal",
"ccinternal",
]),
)}
</div>
{/* External CC */}
<div>
<h3 className="text-md font-medium text-gray-800 dark:text-gray-200 mb-2">
{t("template.externalCC", "External CC")}
</h3>
<div className="flex items-center justify-between mb-2">
<input
type="text"
placeholder={t("common.enterName", "Enter name")}
onKeyPress={(e) => {
if (e.key === "Enter" && e.currentTarget.value) {
handleSelectExternalCC(e.currentTarget.value);
e.currentTarget.value = "";
}
}}
className="px-3 py-1 text-sm border border-gray-300 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100 dark:placeholder-gray-500 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-500"
/>
</div>
<div className="flex flex-wrap gap-2">
{selectedExternalCC.map((unitName, idx) => (
<div
key={idx}
className="flex items-center gap-2 px-3 py-1 bg-primary-100 dark:bg-primary-900/40 text-primary-700 dark:text-primary-300 rounded-full text-sm">
<span>{unitName}</span>
<button
onClick={() => handleRemoveExternalCC(unitName)}
className="hover:text-primary-900 dark:hover:text-primary-100">
<X className="h-4 w-4" />
</button>
</div>
))}
</div>
{renderSettingsEditors(
getSettingsByAliases([
"external-cc",
"externalcc",
"cc-external",
"ccexternal",
]),
)}
</div>
{/* Shared CC Receiver Style */}
<div>
{renderSettingsEditors(getSettingsByAliases(["cc-receiver"]))}
</div>
{/* Recipients */}
<div>
<h3 className="text-md font-medium text-gray-800 dark:text-gray-200 mb-2">
{t("template.recipients", "Recipients")}
</h3>
<div className="flex items-center justify-between mb-2">
<select
onChange={(e) => {
if (e.target.value) {
handleSelectRecipient(e.target.value);
e.target.value = "";
}
}}
className="px-3 py-1 text-sm border border-gray-300 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-500">
<option value="">
{t("common.selectRecipient", "Select Recipient")}
</option>
{departments.map((dept: any) => (
<option key={dept.id} value={dept.id}>
{dept.name?.en || dept.name?.am}
</option>
))}
{employees.map((emp: any) => (
<option
key={emp.employeePositions?.[0]?.id}
value={emp.employeePositions?.[0]?.id}>
{emp.user?.name?.en || emp.user?.name?.am}
</option>
))}
</select>
</div>
<div className="flex flex-wrap gap-2">
{selectedRecipients.map((recipientId) => {
const dept = departments.find(
(d: any) => d.id === recipientId,
);
const emp = employees.find(
(e: any) => e.employeePositions?.[0]?.id === recipientId,
);
const name =
dept?.name?.en ||
dept?.name?.am ||
emp?.user?.name?.en ||
emp?.user?.name?.am;
return (
<div
key={recipientId}
className="flex items-center gap-2 px-3 py-1 bg-primary-100 dark:bg-primary-900/40 text-primary-700 dark:text-primary-300 rounded-full text-sm">
<span>{name}</span>
<button
onClick={() => handleRemoveRecipient(recipientId)}
className="hover:text-primary-900 dark:hover:text-primary-100">
<X className="h-4 w-4" />
</button>
</div>
);
})}
</div>
{renderSettingsEditors(
getSettingsByAliases([
"recipient",
"recipients",
"to",
"receiver",
]),
)}
</div>
</div>
</CollapsibleSection>
{/* Generate Button */}
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-6">
<button
onClick={handleGenerateSample}
disabled={loading || settingsLoading}
className="w-full px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 disabled:bg-gray-400 dark:disabled:bg-gray-600 transition-colors font-medium flex items-center justify-center gap-2">
{loading || settingsLoading ? (
<>
<Loader className="h-4 w-4 animate-spin" />
{t("common.generating", "Generating...")}
</>
) : (
t("template.generateSample", "Generate Sample")
)}
</button>
</div>
</div>
{/* Right column: live preview - scrolls with form */}
<div className="lg:col-span-1">
<div>
{settingsLoading ? (
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-6 flex items-center justify-center min-h-96">
<Loader className="h-8 w-8 animate-spin text-purple-600" />
</div>
) : (
<TemplateLivePreview
formData={formData}
settings={settings}
selectedHeader={selectedHeader}
selectedFooter={selectedFooter}
templateName={template?.name?.en || template?.name?.am}
/>
)}
</div>
</div>
</div>
</div>
);
};