feat(email-templates): group Templates UI by category + Feature off chip
This commit is contained in:
@@ -2226,7 +2226,16 @@
|
||||
"mutedTextColor": "Fußzeilentext",
|
||||
"mutedTextColorHelp": "Fußzeilentext und Copyright-Zeile. Entspricht Branding → Sekundärer Text.",
|
||||
"buttonTextColor": "Button-Text",
|
||||
"buttonTextColorHelp": "Textfarbe auf gefüllten Buttons. Sollte klar gegen die Primärfarbe kontrastieren. Kein Branding-Äquivalent — meistens weiß."
|
||||
"buttonTextColorHelp": "Textfarbe auf gefüllten Buttons. Sollte klar gegen die Primärfarbe kontrastieren. Kein Branding-Äquivalent — meistens weiß.",
|
||||
"categories": {
|
||||
"core": "Kern",
|
||||
"customers": "Kunden",
|
||||
"calendar": "Kalender",
|
||||
"quotes": "Angebote",
|
||||
"billing": "Rechnungen"
|
||||
},
|
||||
"featureOff": "Funktion aus",
|
||||
"featureOffTooltip": "Die zugehörige Funktion ist derzeit deaktiviert. Du kannst die Vorlage trotzdem bearbeiten — sie wird verwendet, sobald die Funktion wieder aktiviert ist."
|
||||
},
|
||||
"cms": {
|
||||
"title": "CMS-Seiten",
|
||||
|
||||
@@ -1880,7 +1880,16 @@
|
||||
"mutedTextColor": "Footer text",
|
||||
"mutedTextColorHelp": "Footer text and copyright line. Maps to Branding → Secondary text.",
|
||||
"buttonTextColor": "Button text",
|
||||
"buttonTextColorHelp": "Text colour on filled buttons. Should contrast cleanly against the Primary colour. No Branding equivalent — usually white."
|
||||
"buttonTextColorHelp": "Text colour on filled buttons. Should contrast cleanly against the Primary colour. No Branding equivalent — usually white.",
|
||||
"categories": {
|
||||
"core": "Core",
|
||||
"customers": "Customers",
|
||||
"calendar": "Calendar",
|
||||
"quotes": "Quotes",
|
||||
"billing": "Billing"
|
||||
},
|
||||
"featureOff": "Feature off",
|
||||
"featureOffTooltip": "The feature this template belongs to is currently disabled. You can still edit the template — it will be used once the feature is re-enabled."
|
||||
},
|
||||
"cms": {
|
||||
"title": "CMS Pages",
|
||||
|
||||
@@ -1826,7 +1826,16 @@
|
||||
"enterUrl": "Saisir une URL…",
|
||||
"addLink": "Ajouter",
|
||||
"cancel": "Annuler"
|
||||
}
|
||||
},
|
||||
"categories": {
|
||||
"core": "Principal",
|
||||
"customers": "Clients",
|
||||
"calendar": "Calendrier",
|
||||
"quotes": "Devis",
|
||||
"billing": "Facturation"
|
||||
},
|
||||
"featureOff": "Fonction désactivée",
|
||||
"featureOffTooltip": "La fonctionnalité à laquelle appartient ce modèle est actuellement désactivée. Vous pouvez toujours modifier le modèle — il sera utilisé une fois la fonctionnalité réactivée."
|
||||
},
|
||||
"cms": {
|
||||
"title": "Pages CMS",
|
||||
|
||||
@@ -1880,7 +1880,16 @@
|
||||
"mutedTextColor": "Voetteksttekst",
|
||||
"mutedTextColorHelp": "Voetteksttekst en copyrightregel. Komt overeen met Huisstijl → Secundaire tekst.",
|
||||
"buttonTextColor": "Knoptekst",
|
||||
"buttonTextColorHelp": "Tekstkleur op gevulde knoppen. Moet goed contrasteren met de primaire kleur. Doorgaans wit."
|
||||
"buttonTextColorHelp": "Tekstkleur op gevulde knoppen. Moet goed contrasteren met de primaire kleur. Doorgaans wit.",
|
||||
"categories": {
|
||||
"core": "Kern",
|
||||
"customers": "Klanten",
|
||||
"calendar": "Agenda",
|
||||
"quotes": "Offertes",
|
||||
"billing": "Facturatie"
|
||||
},
|
||||
"featureOff": "Functie uit",
|
||||
"featureOffTooltip": "De functie waar deze sjabloon bij hoort, is momenteel uitgeschakeld. U kunt de sjabloon nog steeds bewerken — deze wordt gebruikt zodra de functie weer is ingeschakeld."
|
||||
},
|
||||
"cms": {
|
||||
"title": "CMS-pagina's",
|
||||
|
||||
@@ -1905,7 +1905,16 @@
|
||||
"mutedTextColor": "Texto do rodapé",
|
||||
"mutedTextColorHelp": "Texto do rodapé e linha de copyright. Corresponde a Identidade Visual → Texto secundário.",
|
||||
"buttonTextColor": "Texto dos botões",
|
||||
"buttonTextColorHelp": "Cor do texto em botões preenchidos. Deve contrastar bem com a cor primária. Geralmente branco."
|
||||
"buttonTextColorHelp": "Cor do texto em botões preenchidos. Deve contrastar bem com a cor primária. Geralmente branco.",
|
||||
"categories": {
|
||||
"core": "Principal",
|
||||
"customers": "Clientes",
|
||||
"calendar": "Calendário",
|
||||
"quotes": "Orçamentos",
|
||||
"billing": "Faturamento"
|
||||
},
|
||||
"featureOff": "Recurso desativado",
|
||||
"featureOffTooltip": "O recurso ao qual este modelo pertence está atualmente desativado. Você ainda pode editar o modelo — ele será usado assim que o recurso for reativado."
|
||||
},
|
||||
"cms": {
|
||||
"title": "Páginas CMS",
|
||||
|
||||
@@ -1930,7 +1930,16 @@
|
||||
"mutedTextColor": "Текст подвала",
|
||||
"mutedTextColorHelp": "Текст подвала и строка авторского права. Соответствует Брендинг → Дополнительный текст.",
|
||||
"buttonTextColor": "Текст кнопок",
|
||||
"buttonTextColorHelp": "Цвет текста на заполненных кнопках. Должен хорошо контрастировать с основным цветом. Обычно белый."
|
||||
"buttonTextColorHelp": "Цвет текста на заполненных кнопках. Должен хорошо контрастировать с основным цветом. Обычно белый.",
|
||||
"categories": {
|
||||
"core": "Основное",
|
||||
"customers": "Клиенты",
|
||||
"calendar": "Календарь",
|
||||
"quotes": "Сметы",
|
||||
"billing": "Счета"
|
||||
},
|
||||
"featureOff": "Функция выключена",
|
||||
"featureOffTooltip": "Связанная функция в данный момент отключена. Вы по-прежнему можете редактировать шаблон — он будет использоваться после повторного включения функции."
|
||||
},
|
||||
"cms": {
|
||||
"title": "Страницы CMS",
|
||||
|
||||
@@ -24,6 +24,22 @@ import { emailService, type EmailConfig, type EmailTemplate, type EmailTemplateT
|
||||
import { settingsService } from '../../services/settings.service';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { SUPPORTED_LANGUAGES } from "../../components/common/LanguageSelector.tsx";
|
||||
import { useFeatureFlags, type FeatureKey } from '../../contexts/FeatureFlagsContext';
|
||||
|
||||
/**
|
||||
* Template categorisation (migration 098). Order matters — sidebar
|
||||
* sections render in this sequence. Empty categories are hidden
|
||||
* automatically. New categories: add the key here, give it an i18n
|
||||
* label below, set `feature_flag` on templates that should chip
|
||||
* out when the matching flag is off. No other UI changes required.
|
||||
*/
|
||||
const CATEGORY_ORDER: readonly string[] = [
|
||||
'core',
|
||||
'customers',
|
||||
'calendar',
|
||||
'quotes',
|
||||
'billing',
|
||||
] as const;
|
||||
|
||||
const defaultTemplateKeys = [
|
||||
{
|
||||
@@ -117,6 +133,7 @@ export const EmailConfigPage: React.FC = () => {
|
||||
const [emailMutedTextColor, setEmailMutedTextColor] = useState('#666666');
|
||||
const [emailButtonTextColor, setEmailButtonTextColor] = useState('#ffffff');
|
||||
const queryClient = useQueryClient();
|
||||
const { flags: featureFlags } = useFeatureFlags();
|
||||
|
||||
// SMTP Configuration state
|
||||
const [smtpConfig, setSmtpConfig] = useState<EmailConfig>({
|
||||
@@ -714,11 +731,40 @@ export const EmailConfigPage: React.FC = () => {
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
<Card padding="sm">
|
||||
<h3 className="text-lg font-semibold text-neutral-900 dark:text-neutral-100 mb-4">{t('email.templates')}</h3>
|
||||
{/* Templates grouped by category (migration 098). Categories
|
||||
in CATEGORY_ORDER render in sequence; templates that
|
||||
report an unrecognised category fall into 'core' so a
|
||||
forward-compat row never disappears from the UI.
|
||||
Empty categories are hidden — admins don't see a
|
||||
section header with no body. Templates whose
|
||||
feature_flag is currently false stay fully visible and
|
||||
editable, just chip-tagged so the admin knows the
|
||||
feature is dormant. */}
|
||||
{(() => {
|
||||
const grouped: Record<string, EmailTemplate[]> = {};
|
||||
for (const template of templates) {
|
||||
const cat = CATEGORY_ORDER.includes(template.category || 'core')
|
||||
? (template.category || 'core')
|
||||
: 'core';
|
||||
(grouped[cat] = grouped[cat] || []).push(template);
|
||||
}
|
||||
const visibleSections = CATEGORY_ORDER.filter((c) => grouped[c]?.length);
|
||||
|
||||
return (
|
||||
<div className="space-y-5">
|
||||
{visibleSections.map((category) => (
|
||||
<div key={category}>
|
||||
<h4 className="px-1 mb-2 text-[11px] font-semibold uppercase tracking-wider text-neutral-500 dark:text-neutral-400">
|
||||
{t(`email.categories.${category}`, category)}
|
||||
</h4>
|
||||
<div className="space-y-2">
|
||||
{templates.map(template => {
|
||||
const templateInfo = defaultTemplateKeys.find(t => t.key === template.template_key);
|
||||
{grouped[category].map((template) => {
|
||||
const templateInfo = defaultTemplateKeys.find((t) => t.key === template.template_key);
|
||||
const translationCount = getTranslationCount(template);
|
||||
const enTranslation = template.translations?.en;
|
||||
const featureOff = template.feature_flag
|
||||
? featureFlags[template.feature_flag as FeatureKey] === false
|
||||
: false;
|
||||
return (
|
||||
<button
|
||||
key={template.template_key}
|
||||
@@ -732,14 +778,24 @@ export const EmailConfigPage: React.FC = () => {
|
||||
: 'bg-neutral-50 dark:bg-neutral-700 border-2 border-transparent hover:bg-neutral-100 dark:hover:bg-neutral-600'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="font-medium text-neutral-900 dark:text-neutral-100">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<p className="font-medium text-neutral-900 dark:text-neutral-100 truncate">
|
||||
{templateInfo?.name || template.template_key}
|
||||
</p>
|
||||
<div className="flex items-center gap-1.5 flex-shrink-0">
|
||||
{featureOff && (
|
||||
<span
|
||||
className="text-[10px] uppercase tracking-wider px-1.5 py-0.5 rounded font-semibold bg-amber-100 text-amber-800 dark:bg-amber-900/40 dark:text-amber-200"
|
||||
title={t('email.featureOffTooltip', 'The feature this template belongs to is currently disabled. You can still edit the template — it will be used once the feature is re-enabled.')}
|
||||
>
|
||||
{t('email.featureOff', 'Feature off')}
|
||||
</span>
|
||||
)}
|
||||
<span className="text-xs px-2 py-0.5 rounded-full bg-neutral-200 dark:bg-neutral-600 text-neutral-600 dark:text-neutral-300">
|
||||
{translationCount}/{SUPPORTED_LANGUAGES.length}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-sm text-neutral-500 dark:text-neutral-400 mt-1 truncate">
|
||||
{enTranslation?.subject || ''}
|
||||
</p>
|
||||
@@ -747,6 +803,11 @@ export const EmailConfigPage: React.FC = () => {
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
</Card>
|
||||
|
||||
<div className="lg:col-span-2">
|
||||
|
||||
Reference in New Issue
Block a user