From 250b240337733362cb9a74b248ac78b6e9347bf7 Mon Sep 17 00:00:00 2001 From: Luca <102960244+Luca-Timo@users.noreply.github.com> Date: Thu, 25 Jun 2026 19:22:04 +0200 Subject: [PATCH 1/5] fix(crm): pre-event reminder passes raw event_date (fixes "Invalid Date" in the email) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit composePayload pre-formatted event_date to DD.MM.YYYY, but emailProcessor runs date variables through formatDate(value, language) — new Date("25.06.2026") can't parse → the email rendered "Invalid Date". Pass the raw event_date and let the processor localise it, matching the expiry mailer's contract. Pre-existing in the migration-143 composePayload (dormant while the legacy pass was gated off); surfaced once the pre_event_email flow ran. --- .../__tests__/integration/workflowEngine.test.js | 10 ++++++++++ backend/src/services/eventReminderService.js | 14 +++++--------- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/backend/__tests__/integration/workflowEngine.test.js b/backend/__tests__/integration/workflowEngine.test.js index 5981efc5..d247609e 100644 --- a/backend/__tests__/integration/workflowEngine.test.js +++ b/backend/__tests__/integration/workflowEngine.test.js @@ -415,6 +415,16 @@ describe('workflow engine', () => { expect(await _internal.resolveTemplateKey('zzznotype', 'promo_')).toBe('promo_default'); }); + test('pre-event payload passes the RAW event_date (processor formats it — no "Invalid Date")', async () => { + const { _internal } = require('../../src/services/eventReminderService'); + const p = _internal.composePayload({ + event: { id: 1, event_name: 'X', event_date: '2026-06-25', customer_name: 'A' }, + recipientEmail: 'a@x.test', daysBefore: 2, businessName: 'Biz', + }); + expect(p.event_date).toBe('2026-06-25'); // raw, not pre-formatted DD.MM.YYYY + expect(p.event_date).not.toMatch(/invalid/i); + }); + test('webhook action enqueues a delivery for a configured subscription (full pipeline)', async () => { const webhook = engine.registry.getAction('webhook'); expect(typeof webhook).toBe('function'); // registered — no longer a silent no-op diff --git a/backend/src/services/eventReminderService.js b/backend/src/services/eventReminderService.js index c22c3f62..a9972407 100644 --- a/backend/src/services/eventReminderService.js +++ b/backend/src/services/eventReminderService.js @@ -103,18 +103,14 @@ function composePayload({ event, recipientEmail, daysBefore, businessName }) { || event.host_name || recipientEmail || ''; - // Event date formatted DD.MM.YYYY here for simplicity; the rendered - // email may further re-locale via the template engine when locale- - // aware formatters are introduced. - const ed = event.event_date instanceof Date ? event.event_date : new Date(event.event_date); - const day = String(ed.getUTCDate()).padStart(2, '0'); - const month = String(ed.getUTCMonth() + 1).padStart(2, '0'); - const year = ed.getUTCFullYear(); - const eventDateFormatted = `${day}.${month}.${year}`; + // Pass the RAW event_date — emailProcessor.processTemplate runs it through + // formatDate(value, recipientLanguage). Pre-formatting it (e.g. DD.MM.YYYY) + // makes the processor's new Date(...) reparse fail → "Invalid Date". Same + // contract the expiry mailer uses. return { customer_name: customerName, event_name: event.event_name || `Event #${event.id}`, - event_date: eventDateFormatted, + event_date: event.event_date || '', event_type: event.event_type || '', days_before: daysBefore, business_name: businessName || '', From 10559fd68e77eb00f3dd79366fcbb7880321e6b8 Mon Sep 17 00:00:00 2001 From: Luca <102960244+Luca-Timo@users.noreply.github.com> Date: Thu, 25 Jun 2026 19:50:08 +0200 Subject: [PATCH 2/5] fix(email): resolve recipient language from the queue row's event_id, not just email_data MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Language priority is event.language → customer preferred_language → app default → … → en, but it was keyed on email_data.eventId, which only queueEmail injects. Direct email_queue inserts (e.g. the gallery-publish "notify customer" path) set the event_id COLUMN but not email_data.eventId, so those mails skipped event.language and fell through to the default — e.g. a gallery-ready mail in EN while the same event's pre-event reminder (sent via queueEmail) was DE. The processor now backfills emailData.eventId from the authoritative event_id column before rendering, so every send path resolves language from the event consistently. --- backend/src/services/emailProcessor.js | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/backend/src/services/emailProcessor.js b/backend/src/services/emailProcessor.js index 42a454aa..65be426d 100644 --- a/backend/src/services/emailProcessor.js +++ b/backend/src/services/emailProcessor.js @@ -860,10 +860,19 @@ async function processEmailQueue({ ignoreSchedule = false, limit = 10, onlyId = for (const email of pendingEmails) { try { - const emailData = typeof email.email_data === 'string' + const emailData = typeof email.email_data === 'string' ? JSON.parse(email.email_data || '{}') : email.email_data || {}; - + + // Language is resolved from emailData.eventId (event.language is the top + // priority). queueEmail injects it, but direct email_queue inserts (e.g. + // the gallery-publish notification) only set the event_id COLUMN — so + // backfill from the authoritative column so every send path resolves the + // recipient language from the event consistently. + if (emailData.eventId == null && email.event_id != null) { + emailData.eventId = email.event_id; + } + const sendResult = await sendTemplateEmail( email.recipient_email, email.email_type, From 3ccaed06a226cdc4a18399f8daa9b2d420e3684b Mon Sep 17 00:00:00 2001 From: Luca <102960244+Luca-Timo@users.noreply.github.com> Date: Thu, 25 Jun 2026 20:01:40 +0200 Subject: [PATCH 3/5] feat(crm): pre-event reminder falls back to the assigned customer account MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When an event has no inline customer_email/host_email but has customer account(s) assigned (event_customer_assignments), the pre-event reminder now sends to those registered customers instead of skipping with no_recipient. Recipients sent to an assigned account are queued WITHOUT eventId so the language resolver uses the customer's preferred_language (vs the event's language for inline-email sends). Applies to both the flow path (sendReminderForEvent) and the legacy pass. The gallery-ready mail deliberately does NOT fall back to accounts — only the reminder does. Test covers the no-inline-email + assigned-customer case. --- .../integration/workflowEngine.test.js | 23 ++++++ backend/src/services/eventReminderService.js | 80 ++++++++++++++----- 2 files changed, 82 insertions(+), 21 deletions(-) diff --git a/backend/__tests__/integration/workflowEngine.test.js b/backend/__tests__/integration/workflowEngine.test.js index d247609e..84dbc2c9 100644 --- a/backend/__tests__/integration/workflowEngine.test.js +++ b/backend/__tests__/integration/workflowEngine.test.js @@ -460,6 +460,29 @@ describe('workflow engine', () => { expect((await webhook(ctx({ webhookId: whId }))).skipped).toBe(true); }); + test('pre-event falls back to the assigned customer account when the event has no inline email', async () => { + const eventReminderService = require('../../src/services/eventReminderService'); + const farFuture = new Date(Date.now() + 365 * 86400000).toISOString(); + const [custId] = await db('customer_accounts').insert({ + email: 'assigned@x.test', preferred_language: 'en', is_active: true, created_at: new Date(), + }); + // Event with NO inline customer_email / host_email. + await db('events').insert({ + event_type: 'wedding', password_hash: 'x', expires_at: farFuture, is_active: true, is_archived: false, + slug: 'rem-assigned', share_link: 'rem-assigned', event_name: 'Assigned', + event_date: new Date(Date.now() + 2 * 86400000).toISOString().slice(0, 10), + }); + const ev = await db('events').where({ slug: 'rem-assigned' }).first(); + await db('event_customer_assignments').insert({ event_id: ev.id, customer_account_id: custId, assigned_at: new Date() }); + + const res = await eventReminderService.sendReminderForEvent(ev.id); + expect(res.sent).toBe(1); + const mail = await db('email_queue').where({ recipient_email: 'assigned@x.test' }).first(); + expect(mail).toBeTruthy(); + // Queued WITHOUT event_id so the resolver uses the customer's preferred_language. + expect(mail.event_id == null).toBe(true); + }); + test('isBuiltinFlowActive reflects the built-in ENABLED state (enabled-based mutex)', async () => { const { seedBuiltinWorkflowsAtBoot } = require('../../src/services/_workflowSeedBoot'); await seedBuiltinWorkflowsAtBoot(db, { info() {}, warn() {} }); diff --git a/backend/src/services/eventReminderService.js b/backend/src/services/eventReminderService.js index a9972407..6d46d769 100644 --- a/backend/src/services/eventReminderService.js +++ b/backend/src/services/eventReminderService.js @@ -187,8 +187,9 @@ async function runEventReminderPass() { let skipped = 0; for (const row of rows) { try { - const recipientEmail = row.customer_email || row.host_email; - if (!recipientEmail) { skipped += 1; continue; } + // Inline event email, else the assigned customer account(s). + const recipients = await resolveReminderRecipients(row); + if (!recipients.length) { skipped += 1; continue; } const rawOffset = row.event_reminder_offset_days; const offsetDays = (rawOffset != null && rawOffset !== '' && Number.isFinite(Number(rawOffset))) ? Number(rawOffset) @@ -199,21 +200,18 @@ async function runEventReminderPass() { if (now < triggerAt) { skipped += 1; continue; } const templateKey = await resolveTemplateKey(row.event_type); - const payload = composePayload({ - event: row, recipientEmail, daysBefore: offsetDays, businessName, - }); - // Per-event body override: when present, append as a synthetic - // `body_override` field. The template engine should branch on it - // (e.g. Handlebars `{{#if body_override}}{{body_override}}{{else}}…default body…{{/if}}`). - // For installs where the templates don't yet handle the branch, - // the override still rides through as a variable the admin can - // reference manually. - if (row.event_reminder_body_override) { - payload.body_override = row.event_reminder_body_override; + for (const r of recipients) { + const payload = composePayload({ + event: row, recipientEmail: r.email, daysBefore: offsetDays, businessName, + }); + // Per-event body override rides through as a variable the template can branch on. + if (row.event_reminder_body_override) { + payload.body_override = row.event_reminder_body_override; + } + // Inline → event language; assigned account → customer's preferred language (no eventId). + await emailProcessor.queueEmail(r.fromEvent ? row.id : null, r.email, templateKey, payload); } - await emailProcessor.queueEmail(row.id, recipientEmail, templateKey, payload); - // Stamp sent_at immediately so a same-pass-re-entrancy (or a // crash between queueEmail and the update) doesn't double-send // on the next tick. The queueEmail call is itself idempotent at @@ -279,8 +277,17 @@ async function sendReminderForEvent(eventId, { templateGroup = null } = {}) { if (row.is_active === false || row.is_active === 0 || row.is_archived === true || row.is_archived === 1) { return { sent: 0, skipped: 1, reason: 'inactive' }; } - const recipientEmail = row.customer_email || row.host_email; - if (!recipientEmail) return { sent: 0, skipped: 1, reason: 'no_recipient' }; + + // Recipient resolution: + // - inline event email (customer_email / host_email) → send there, language + // follows the event (eventId passed); + // - else fall back to the assigned customer account(s) (event_customer_assignments) + // → send to each registered customer, honouring THEIR preferred_language + // (queued without eventId so the resolver uses the customer, not the event). + // The gallery-ready mail deliberately doesn't fall back to accounts, but a + // pre-event reminder should still reach an assigned customer. + const recipients = await resolveReminderRecipients(row); + if (!recipients.length) return { sent: 0, skipped: 1, reason: 'no_recipient' }; const globalDaysBefore = Number(await getAppSetting('crm_event_reminders_days_before')); const daysBeforeDefault = Number.isFinite(globalDaysBefore) && globalDaysBefore >= 0 @@ -295,12 +302,43 @@ async function sendReminderForEvent(eventId, { templateGroup = null } = {}) { // The flow block chooses the template GROUP (blank → the default group); the // exact template is still auto-picked by event type within that group. const templateKey = await resolveTemplateKey(row.event_type, templateGroup || DEFAULT_TEMPLATE_GROUP); - const payload = composePayload({ event: row, recipientEmail, daysBefore: offsetDays, businessName }); - if (row.event_reminder_body_override) payload.body_override = row.event_reminder_body_override; - await emailProcessor.queueEmail(row.id, recipientEmail, templateKey, payload); + let sent = 0; + for (const r of recipients) { + const payload = composePayload({ event: row, recipientEmail: r.email, daysBefore: offsetDays, businessName }); + if (row.event_reminder_body_override) payload.body_override = row.event_reminder_body_override; + // Inline → pass eventId (event language). Assigned account → no eventId so + // the resolver picks the customer's preferred_language. + await emailProcessor.queueEmail(r.fromEvent ? row.id : null, r.email, templateKey, payload); + sent += 1; + } await db('events').where({ id: row.id }).update({ event_reminder_sent_at: new Date() }); - return { sent: 1, skipped: 0, offsetDays }; + return { sent, skipped: 0, offsetDays }; +} + +/** + * Who receives the pre-event reminder for an event: the inline event email if + * present, otherwise the active assigned customer account(s). `fromEvent` flags + * which language path to use (event vs customer). + */ +async function resolveReminderRecipients(eventRow) { + const inline = eventRow.customer_email || eventRow.host_email; + if (inline) return [{ email: inline, fromEvent: true }]; + + const assigned = await db('event_customer_assignments as a') + .join('customer_accounts as c', 'c.id', 'a.customer_account_id') + .where('a.event_id', eventRow.id) + .where('c.is_active', true) + .whereNotNull('c.email') + .select('c.email'); + // De-dup emails defensively (a customer assigned twice, etc.). + const seen = new Set(); + const out = []; + for (const a of assigned) { + const e = String(a.email).toLowerCase(); + if (!seen.has(e)) { seen.add(e); out.push({ email: a.email, fromEvent: false }); } + } + return out; } module.exports = { From c657892bc87f44e854827cb8a65f87cc528a2f71 Mon Sep 17 00:00:00 2001 From: Luca <102960244+Luca-Timo@users.noreply.github.com> Date: Fri, 26 Jun 2026 15:26:28 +0200 Subject: [PATCH 4/5] feat(gallery): publish notifies assigned customer accounts via the account email Publishing a gallery with no inline customer_email but assigned customer account(s) previously sent nothing (the dialog said "no notification"). Now the publish route falls back to the existing customer_gallery_assigned "your galleries" email (sent per assigned active account in their preferred language) so registered customers learn the gallery is available. Inline-email path (gallery_created) is unchanged. The publish dialog now reflects this: with an inline email it notifies that address; with only assigned accounts it says the account(s) will be notified; with neither, the button is just "Publish" (no false notify promise). Exports notifyCustomerOfNewAssignments; EN/DE strings added. --- backend/src/routes/adminEvents.js | 16 +++++++++++ .../src/services/customerAccountsService.js | 1 + .../components/admin/PublishGalleryDialog.tsx | 27 ++++++++++++++----- frontend/src/i18n/locales/de.json | 2 ++ frontend/src/i18n/locales/en.json | 2 ++ frontend/src/pages/admin/EventDetailsPage.tsx | 1 + 6 files changed, 42 insertions(+), 7 deletions(-) diff --git a/backend/src/routes/adminEvents.js b/backend/src/routes/adminEvents.js index a1cc3f10..4b30546d 100644 --- a/backend/src/routes/adminEvents.js +++ b/backend/src/routes/adminEvents.js @@ -1189,6 +1189,22 @@ router.post('/:id/publish', adminAuth, requirePermission('events.edit'), require status: 'pending', created_at: new Date() }); + } else { + // No inline email, but the gallery may be assigned to registered customer + // account(s). Notify them via the account "your galleries" email + // (customer_gallery_assigned, in the customer's own language) instead of + // the gallery_created mail, which needs an inline recipient. Best-effort. + try { + const customerAccountsService = require('../services/customerAccountsService'); + const assigned = await customerAccountsService.getAssignmentsForEvent(parseInt(id, 10)); + for (const c of assigned.filter((a) => a.is_active !== false && a.is_active !== 0 && a.email)) { + await customerAccountsService + .notifyCustomerOfNewAssignments(c.id, [parseInt(id, 10)]) + .catch((err) => logger.warn('Publish: customer gallery notice failed', { customerId: c.id, error: err.message })); + } + } catch (err) { + logger.warn('Publish: assigned-customer notification skipped', { eventId: id, error: err.message }); + } } // WhatsApp gallery_ready on publish-from-draft (#640D). The PublishGallery diff --git a/backend/src/services/customerAccountsService.js b/backend/src/services/customerAccountsService.js index 8b8061d1..2dd76b38 100644 --- a/backend/src/services/customerAccountsService.js +++ b/backend/src/services/customerAccountsService.js @@ -1456,6 +1456,7 @@ module.exports = { setAssignmentsForEvent, setAssignmentsForCustomer, getAssignmentsForEvent, + notifyCustomerOfNewAssignments, listEventsForCustomer, customerHasAccessToEvent, getPendingInvitations, diff --git a/frontend/src/components/admin/PublishGalleryDialog.tsx b/frontend/src/components/admin/PublishGalleryDialog.tsx index d0421602..5aa09c29 100644 --- a/frontend/src/components/admin/PublishGalleryDialog.tsx +++ b/frontend/src/components/admin/PublishGalleryDialog.tsx @@ -7,6 +7,8 @@ interface PublishGalleryDialogProps { eventName: string; requirePassword: boolean; customerEmail?: string | null; + /** Assigned customer accounts — notified via the account "your galleries" email when there's no inline email. */ + assignedCustomerCount?: number; isPublishing: boolean; onConfirm: (password?: string) => void; onClose: () => void; @@ -28,11 +30,15 @@ export const PublishGalleryDialog: React.FC = ({ eventName, requirePassword, customerEmail, + assignedCustomerCount = 0, isPublishing, onConfirm, onClose, }) => { const { t } = useTranslation(); + // Someone gets notified if there's an inline email OR an assigned account + // (the latter via the account "your galleries" email). + const willNotify = !!customerEmail || assignedCustomerCount > 0; const [password, setPassword] = useState(''); const [showPassword, setShowPassword] = useState(false); const [error, setError] = useState(undefined); @@ -72,11 +78,18 @@ export const PublishGalleryDialog: React.FC = ({ defaultValue: 'Publishing "{{eventName}}" makes the gallery accessible and sends the notification email to {{customerEmail}}.', }) - : t('events.publishDialog.descriptionNoEmail', { - eventName, - defaultValue: - 'Publishing "{{eventName}}" makes the gallery accessible. No customer email is set, so no notification will be sent.', - })} + : assignedCustomerCount > 0 + ? t('events.publishDialog.descriptionAssignedAccount', { + eventName, + count: assignedCustomerCount, + defaultValue: + 'Publishing "{{eventName}}" makes the gallery accessible. The assigned customer account(s) will be notified by email (in their language) that it is available.', + }) + : t('events.publishDialog.descriptionNoEmail', { + eventName, + defaultValue: + 'Publishing "{{eventName}}" makes the gallery accessible. No customer email is set, so no notification will be sent.', + })}

{requirePassword && customerEmail && ( @@ -124,10 +137,10 @@ export const PublishGalleryDialog: React.FC = ({ onClick={handleSubmit} disabled={isPublishing} isLoading={isPublishing} - leftIcon={} + leftIcon={willNotify ? : undefined} className="flex-1" > - {t('events.publishAndNotify')} + {willNotify ? t('events.publishAndNotify') : t('events.publishDialog.justPublish', 'Publish')} diff --git a/frontend/src/i18n/locales/de.json b/frontend/src/i18n/locales/de.json index 127a834c..ee26b006 100644 --- a/frontend/src/i18n/locales/de.json +++ b/frontend/src/i18n/locales/de.json @@ -1125,6 +1125,8 @@ "title": "Galerie veröffentlichen", "descriptionWithEmail": "Die Galerie \"{{eventName}}\" wird zugänglich gemacht und die Benachrichtigungs-E-Mail an {{customerEmail}} gesendet.", "descriptionNoEmail": "Die Galerie \"{{eventName}}\" wird zugänglich gemacht. Es ist keine Kunden-E-Mail hinterlegt – es wird keine Benachrichtigung gesendet.", + "descriptionAssignedAccount": "Die Galerie \"{{eventName}}\" wird zugänglich gemacht. Die zugewiesenen Kundenkonten werden per E-Mail (in ihrer Sprache) benachrichtigt, dass sie verfügbar ist.", + "justPublish": "Veröffentlichen", "passwordLabel": "Galerie-Passwort", "passwordPlaceholder": "Galerie-Passwort eingeben", "passwordHelp": "Gib das bei der Erstellung gesetzte Passwort erneut ein (oder wähle ein neues). Die E-Mail enthält genau diesen Text; das Backend hasht es erneut, sodass die Galerie-Anmeldung weiterhin funktioniert.", diff --git a/frontend/src/i18n/locales/en.json b/frontend/src/i18n/locales/en.json index 081363f1..d15f0fe2 100644 --- a/frontend/src/i18n/locales/en.json +++ b/frontend/src/i18n/locales/en.json @@ -670,6 +670,8 @@ "title": "Publish gallery", "descriptionWithEmail": "Publishing \"{{eventName}}\" makes the gallery accessible and sends the notification email to {{customerEmail}}.", "descriptionNoEmail": "Publishing \"{{eventName}}\" makes the gallery accessible. No customer email is set, so no notification will be sent.", + "descriptionAssignedAccount": "Publishing \"{{eventName}}\" makes the gallery accessible. The assigned customer account(s) will be notified by email (in their language) that it is available.", + "justPublish": "Publish", "passwordLabel": "Gallery password", "passwordPlaceholder": "Enter the gallery password", "passwordHelp": "Re-type the password set at creation (or pick a new one). The email includes this exact text; the backend re-hashes it so the gallery login still works.", diff --git a/frontend/src/pages/admin/EventDetailsPage.tsx b/frontend/src/pages/admin/EventDetailsPage.tsx index 079a8b73..2d7b1097 100644 --- a/frontend/src/pages/admin/EventDetailsPage.tsx +++ b/frontend/src/pages/admin/EventDetailsPage.tsx @@ -2661,6 +2661,7 @@ export const EventDetailsPage: React.FC = () => { eventName={event.event_name} requirePassword={isGalleryPublic(event) ? false : true} customerEmail={event.customer_email} + assignedCustomerCount={((event as { customer_accounts?: Array<{ id: number }> }).customer_accounts || []).length} isPublishing={publishMutation.isPending} onConfirm={(password) => publishMutation.mutate(password)} onClose={() => { From aa3471efe1629f36adeff8774dfdc379d4652e26 Mon Sep 17 00:00:00 2001 From: Luca <102960244+Luca-Timo@users.noreply.github.com> Date: Fri, 26 Jun 2026 15:28:58 +0200 Subject: [PATCH 5/5] fix(gallery): publish dialog stuck for password-protected galleries with no inline email handleSubmit required a password whenever requirePassword was true, but the password field only renders on the inline-email path (requirePassword && customerEmail). For a password-protected gallery with no inline email the field was hidden, so submit blocked on the missing password and the dialog never closed. Gate password collection + validation on a single `needsPassword` (requirePassword && customerEmail); the no-inline-email path publishes without re-entering the password (existing hash kept, customer reaches it via portal). --- .../src/components/admin/PublishGalleryDialog.tsx | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/frontend/src/components/admin/PublishGalleryDialog.tsx b/frontend/src/components/admin/PublishGalleryDialog.tsx index 5aa09c29..55bdcdcb 100644 --- a/frontend/src/components/admin/PublishGalleryDialog.tsx +++ b/frontend/src/components/admin/PublishGalleryDialog.tsx @@ -39,19 +39,24 @@ export const PublishGalleryDialog: React.FC = ({ // Someone gets notified if there's an inline email OR an assigned account // (the latter via the account "your galleries" email). const willNotify = !!customerEmail || assignedCustomerCount > 0; + // The password is only collected (and required) on the inline-email path, + // because the gallery_created email carries it. With no inline email the field + // is hidden and the existing hash is kept — so don't gate submit on it, or a + // password-protected gallery without an email could never be published. + const needsPassword = requirePassword && !!customerEmail; const [password, setPassword] = useState(''); const [showPassword, setShowPassword] = useState(false); const [error, setError] = useState(undefined); const handleSubmit = () => { - if (requirePassword) { + if (needsPassword) { if (!password || password.trim().length < 6) { setError(t('events.publishDialog.errorMinLength', 'Password must be at least 6 characters long.')); return; } } setError(undefined); - onConfirm(requirePassword ? password : undefined); + onConfirm(needsPassword ? password : undefined); }; return ( @@ -92,7 +97,7 @@ export const PublishGalleryDialog: React.FC = ({ })}

- {requirePassword && customerEmail && ( + {needsPassword && (