diff --git a/app/javascript/dashboard/helper/scheduleDateShortcutHelpers.js b/app/javascript/dashboard/helper/scheduleDateShortcutHelpers.js new file mode 100644 index 000000000..3c5b0a824 --- /dev/null +++ b/app/javascript/dashboard/helper/scheduleDateShortcutHelpers.js @@ -0,0 +1,199 @@ +import { + getDay, + addDays, + setHours, + setMinutes, + setSeconds, + isBefore, +} from 'date-fns'; +import * as chrono from 'chrono-node'; + +export const SHORTCUT_KEYS = { + TOMORROW_MORNING: 'tomorrow_morning', + TOMORROW_AFTERNOON: 'tomorrow_afternoon', + MONDAY_MORNING: 'monday_morning', + CUSTOM: 'custom', +}; + +/** + * Normalize a locale tag to BCP 47 format (e.g. 'pt_BR' → 'pt-BR'). + */ +const toBcp47 = locale => (locale || 'en').replace('_', '-'); + +/** + * Get the date for "tomorrow" (always the next calendar day). + */ +export const getTomorrowDate = (now = new Date()) => { + const today = new Date(now); + today.setHours(0, 0, 0, 0); + return addDays(today, 1); +}; + +/** + * Get the date for the "Monday" shortcut. + * On Sunday, tomorrow is already Monday, so this returns next week's Monday. + * On all other days, returns the upcoming Monday. + */ +export const getMondayDate = (now = new Date()) => { + const today = new Date(now); + today.setHours(0, 0, 0, 0); + const dayOfWeek = getDay(now); // 0=Sun, 6=Sat + + if (dayOfWeek === 0) return addDays(today, 8); + return addDays(today, 8 - dayOfWeek); +}; + +/** + * Apply an hour to a date, returning a new Date with that hour set (minutes/seconds zeroed). + */ +export const applyHour = (date, hour) => + setSeconds(setMinutes(setHours(new Date(date), hour), 0), 0); + +/** + * Format an hour (0-23) as a locale-aware time string (e.g. '18:00' or '6:00 PM'). + */ +export const formatHour = (hour, locale = 'en') => { + const date = new Date(2023, 0, 1, hour, 0, 0); + return new Intl.DateTimeFormat(toBcp47(locale), { + hour: 'numeric', + minute: '2-digit', + }).format(date); +}; + +/** + * Format a date as a locale-aware short date with month name (e.g. '11 de mar.' / 'Mar 11'). + */ +export const formatShortDate = (date, locale = 'en') => + new Intl.DateTimeFormat(toBcp47(locale), { + day: 'numeric', + month: 'short', + }).format(date); + +/** + * Build the 3 predefined schedule shortcuts with pre-computed dates. + * Shortcuts whose datetime is already in the past are excluded. + */ +export const getScheduleShortcuts = (now = new Date(), locale = 'en') => { + const tomorrow = getTomorrowDate(now); + const monday = getMondayDate(now); + + const shortcuts = [ + { + key: SHORTCUT_KEYS.TOMORROW_MORNING, + labelI18nKey: 'SCHEDULED_MESSAGES.MODAL.SHORTCUTS.TOMORROW_MORNING', + date: tomorrow, + hour: 8, + }, + { + key: SHORTCUT_KEYS.TOMORROW_AFTERNOON, + labelI18nKey: 'SCHEDULED_MESSAGES.MODAL.SHORTCUTS.TOMORROW_AFTERNOON', + date: tomorrow, + hour: 13, + }, + { + key: SHORTCUT_KEYS.MONDAY_MORNING, + labelI18nKey: 'SCHEDULED_MESSAGES.MODAL.SHORTCUTS.MONDAY_MORNING', + date: monday, + hour: 8, + }, + ]; + + return shortcuts + .map(s => { + const dateTime = applyHour(s.date, s.hour); + const formattedDate = formatShortDate(s.date, locale); + const formattedTime = formatHour(s.hour, locale); + return { + ...s, + dateTime, + formattedDate, + formattedTime, + detail: `${formattedDate}, ${formattedTime}`, + }; + }) + .filter(s => !isBefore(s.dateTime, now)); +}; + +/** + * Pre-process natural language input to normalize PT/EN time expressions + * before passing to chrono-node. + */ +export const preProcessDateInput = text => { + let result = text; + // PT: normalize common words typed without accents + result = result.replace(/\bamanha\b/gi, 'amanhã'); + result = result.replace(/\bsabado\b/gi, 'sábado'); + result = result.replace(/\bproxim([ao])\b/gi, 'próxim$1'); + // PT: normalize 'as' → 'às' before digits or time-of-day words + result = result.replace(/\bas\s+(\d)/gi, 'às $1'); + result = result.replace(/\bas\s+(manh|tard|noit)/gi, 'às $1'); + // PT: insert 'às' connector between weekday name and bare number/time + // chrono.pt needs 'às' to link weekday + time (e.g. "quarta 10" → "quarta às 10") + const ptWeekdays = + '(?:segunda(?:-feira)?|ter[çc]a(?:-feira)?|quarta(?:-feira)?|quinta(?:-feira)?|sexta(?:-feira)?|s[áa]bado|domingo)'; + result = result.replace( + new RegExp(`(${ptWeekdays})\\s+(?!às|as)(\\d)`, 'gi'), + '$1 às $2' + ); + // PT: 'Xh' or 'XhMM' → 'X:00' or 'X:MM' (e.g. '14h' → '14:00', '14h30' → '14:30') + result = result.replace( + /(\d{1,2})h(\d{2})?(?=\s|$|,)/gi, + (_, h, min) => `${h}:${min || '00'}` + ); + // PT: time-of-day expressions (de manhã, pela tarde, no período da noite, etc.) + result = result.replace( + /(?:no per[ií]odo da|pela|de|à|às)\s+manh[ãa]/gi, + '8:00' + ); + result = result.replace( + /(?:no per[ií]odo da|pela|de|à|às)\s+tarde/gi, + '13:00' + ); + result = result.replace( + /(?:no per[ií]odo da|pela|de|à|às)\s+noite/gi, + '18:00' + ); + return result; +}; + +/** + * Parse a natural language date/time string using chrono-node. + * Supports both PT and EN locales. + * Returns a Date object if successfully parsed, otherwise null. + */ +export const parseNaturalDate = (text, locale = 'en', now = new Date()) => { + if (!text || !text.trim()) return null; + const processed = preProcessDateInput(text.trim()); + const opts = { forwardDate: true }; + const isPt = locale.startsWith('pt'); + const primaryResults = (isPt ? chrono.pt : chrono).parse( + processed, + now, + opts + ); + const fallbackResults = (isPt ? chrono : chrono.pt).parse( + processed, + now, + opts + ); + const matchLen = results => results.reduce((s, r) => s + r.text.length, 0); + // Pick the parser that matched more of the input text + const best = + matchLen(primaryResults) >= matchLen(fallbackResults) + ? primaryResults + : fallbackResults; + return best.length ? best[0].start.date() : null; +}; + +/** + * Format a Date as a full locale-aware date-time string for preview display. + * e.g. '17 de mar. de 2026, 08:00' (pt-BR) or 'Mar 17, 2026, 8:00 AM' (en) + */ +export const formatFullDateTime = (date, locale = 'en') => + new Intl.DateTimeFormat(toBcp47(locale), { + day: 'numeric', + month: 'short', + year: 'numeric', + hour: 'numeric', + minute: '2-digit', + }).format(date); diff --git a/app/javascript/dashboard/helper/specs/scheduleDateShortcutHelpers.spec.js b/app/javascript/dashboard/helper/specs/scheduleDateShortcutHelpers.spec.js new file mode 100644 index 000000000..6ee3bd96a --- /dev/null +++ b/app/javascript/dashboard/helper/specs/scheduleDateShortcutHelpers.spec.js @@ -0,0 +1,352 @@ +import { + SHORTCUT_KEYS, + getTomorrowDate, + getMondayDate, + applyHour, + formatShortDate, + formatHour, + getScheduleShortcuts, + preProcessDateInput, + parseNaturalDate, + formatFullDateTime, +} from '../scheduleDateShortcutHelpers'; + +describe('#scheduleDateShortcutHelpers', () => { + // Wednesday 2023-06-14 + const wednesday = new Date('2023-06-14T10:30:00'); + // Saturday 2023-06-17 + const saturday = new Date('2023-06-17T10:30:00'); + // Sunday 2023-06-18 + const sunday = new Date('2023-06-18T10:30:00'); + // Monday 2023-06-19 + const monday = new Date('2023-06-19T10:30:00'); + + describe('getTomorrowDate', () => { + it('returns the next day at midnight', () => { + expect(getTomorrowDate(wednesday)).toEqual( + new Date('2023-06-15T00:00:00') + ); + }); + + it('returns Monday from Sunday', () => { + expect(getTomorrowDate(sunday)).toEqual(new Date('2023-06-19T00:00:00')); + }); + + it('returns Sunday from Saturday', () => { + expect(getTomorrowDate(saturday)).toEqual( + new Date('2023-06-18T00:00:00') + ); + }); + }); + + describe('getMondayDate', () => { + it('returns next Monday from a weekday (Wednesday)', () => { + expect(getMondayDate(wednesday)).toEqual(new Date('2023-06-19T00:00:00')); + }); + + it('returns next Monday from Saturday', () => { + expect(getMondayDate(saturday)).toEqual(new Date('2023-06-19T00:00:00')); + }); + + it('returns Monday of NEXT week from Sunday (8 days away)', () => { + // On Sunday, tomorrow is already Monday, so this returns the Monday after + expect(getMondayDate(sunday)).toEqual(new Date('2023-06-26T00:00:00')); + }); + + it('returns next Monday from Monday (7 days away)', () => { + expect(getMondayDate(monday)).toEqual(new Date('2023-06-26T00:00:00')); + }); + }); + + describe('applyHour', () => { + const baseDate = new Date('2023-06-14T00:00:00'); + + it('sets 8:00 for hour 8', () => { + const result = applyHour(baseDate, 8); + expect(result.getHours()).toBe(8); + expect(result.getMinutes()).toBe(0); + expect(result.getSeconds()).toBe(0); + }); + + it('sets 13:00 for hour 13', () => { + const result = applyHour(baseDate, 13); + expect(result.getHours()).toBe(13); + expect(result.getMinutes()).toBe(0); + }); + + it('does not mutate the original date', () => { + const original = new Date('2023-06-14T00:00:00'); + applyHour(original, 18); + expect(original.getHours()).toBe(0); + }); + }); + + describe('formatShortDate', () => { + const date = new Date('2023-03-15T00:00:00'); + + it('formats with en locale using month name', () => { + const result = formatShortDate(date, 'en'); + expect(result).toMatch(/Mar.*15|15.*Mar/i); + }); + + it('handles underscore locale tags like pt_BR', () => { + expect(() => formatShortDate(date, 'pt_BR')).not.toThrow(); + }); + + it('falls back to en for empty locale', () => { + expect(() => formatShortDate(date, '')).not.toThrow(); + }); + }); + + describe('formatHour', () => { + it('formats 8 for en locale with AM', () => { + const result = formatHour(8, 'en'); + expect(result).toMatch(/8.*AM/i); + }); + + it('formats 13 for en locale with PM', () => { + const result = formatHour(13, 'en'); + expect(result).toMatch(/1.*PM/i); + }); + + it('handles underscore locale tags', () => { + expect(() => formatHour(18, 'pt_BR')).not.toThrow(); + }); + }); + + describe('preProcessDateInput', () => { + it('converts PT hour format "8h" to "8:00"', () => { + expect(preProcessDateInput('amanhã às 8h')).toBe('amanhã às 8:00'); + }); + + it('converts PT hour+min format "14h30" to "14:30"', () => { + expect(preProcessDateInput('amanhã às 14h30')).toBe('amanhã às 14:30'); + }); + + it('converts PT "de manhã" to "8:00"', () => { + expect(preProcessDateInput('amanhã de manhã')).toBe('amanhã 8:00'); + }); + + it('converts PT "à tarde" to "13:00"', () => { + expect(preProcessDateInput('amanhã à tarde')).toBe('amanhã 13:00'); + }); + + it('converts PT "de noite" to "18:00"', () => { + expect(preProcessDateInput('amanhã de noite')).toBe('amanhã 18:00'); + }); + + it('normalizes "amanha" without accent to "amanhã"', () => { + expect(preProcessDateInput('amanha as 8h')).toBe('amanhã às 8:00'); + }); + + it('normalizes "as" to "às" before digits', () => { + expect(preProcessDateInput('amanhã as 19h')).toBe('amanhã às 19:00'); + }); + + it('normalizes "as" to "às" before time-of-day words', () => { + expect(preProcessDateInput('amanhã as manha')).toBe('amanhã 8:00'); + }); + + it('normalizes "sabado" to "sábado"', () => { + expect(preProcessDateInput('sabado as 10h')).toContain('sábado'); + }); + + it('normalizes "proxima" to "próxima"', () => { + expect(preProcessDateInput('proxima segunda')).toContain('próxima'); + }); + + it('converts "pela manhã" to "8:00"', () => { + expect(preProcessDateInput('amanhã pela manhã')).toBe('amanhã 8:00'); + }); + + it('converts "pela tarde" to "13:00"', () => { + expect(preProcessDateInput('amanhã pela tarde')).toBe('amanhã 13:00'); + }); + + it('converts "no período da noite" to "18:00"', () => { + expect(preProcessDateInput('amanhã no período da noite')).toBe( + 'amanhã 18:00' + ); + }); + + it('leaves EN text with explicit times unchanged', () => { + expect(preProcessDateInput('tomorrow at 2pm')).toBe('tomorrow at 2pm'); + }); + + it('inserts "às" between PT weekday and bare number', () => { + expect(preProcessDateInput('quarta 10')).toBe('quarta às 10'); + }); + + it('inserts "às" between PT weekday and am/pm time', () => { + expect(preProcessDateInput('sexta 2pm')).toBe('sexta às 2pm'); + }); + + it('inserts "às" between PT weekday-feira and number', () => { + expect(preProcessDateInput('quarta-feira 14h')).toBe( + 'quarta-feira às 14:00' + ); + }); + + it('does not double-insert "às" when already present', () => { + expect(preProcessDateInput('quarta às 10')).toBe('quarta às 10'); + }); + }); + + describe('parseNaturalDate', () => { + const now = new Date('2023-06-14T10:00:00'); + + it('returns null for empty string', () => { + expect(parseNaturalDate('', 'en', now)).toBeNull(); + }); + + it('returns null for whitespace-only string', () => { + expect(parseNaturalDate(' ', 'en', now)).toBeNull(); + }); + + it('parses EN "tomorrow at 8am"', () => { + const result = parseNaturalDate('tomorrow at 8am', 'en', now); + expect(result).toBeInstanceOf(Date); + expect(result.getDate()).toBe(15); + expect(result.getHours()).toBe(8); + }); + + it('parses EN "tomorrow at 1pm"', () => { + const result = parseNaturalDate('tomorrow at 1pm', 'en', now); + expect(result).toBeInstanceOf(Date); + expect(result.getHours()).toBe(13); + }); + + it('parses PT "amanhã às 8h" via preprocessing', () => { + const result = parseNaturalDate('amanhã às 8h', 'pt_BR', now); + expect(result).toBeInstanceOf(Date); + expect(result.getDate()).toBe(15); + expect(result.getHours()).toBe(8); + }); + + it('parses PT "amanhã à tarde" via preprocessing', () => { + const result = parseNaturalDate('amanhã à tarde', 'pt_BR', now); + expect(result).toBeInstanceOf(Date); + expect(result.getDate()).toBe(15); + expect(result.getHours()).toBe(13); + }); + + it('parses PT "amanha as 19h" (no accents) via preprocessing', () => { + const result = parseNaturalDate('amanha as 19h', 'pt_BR', now); + expect(result).toBeInstanceOf(Date); + expect(result.getDate()).toBe(15); + expect(result.getHours()).toBe(19); + }); + + it('parses PT "amanhã pela manhã" via preprocessing', () => { + const result = parseNaturalDate('amanhã pela manhã', 'pt_BR', now); + expect(result).toBeInstanceOf(Date); + expect(result.getDate()).toBe(15); + expect(result.getHours()).toBe(8); + }); + + it('parses weekday names forward (not past) — PT "sexta"', () => { + // now is Wednesday Jun 14; "sexta" should be upcoming Friday Jun 16 + const result = parseNaturalDate('sexta', 'pt_BR', now); + expect(result).toBeInstanceOf(Date); + expect(result.getDate()).toBe(16); + }); + + it('parses weekday names forward (not past) — EN "friday"', () => { + const result = parseNaturalDate('friday', 'en', now); + expect(result).toBeInstanceOf(Date); + expect(result.getDate()).toBe(16); + }); + + it('returns null for unrecognizable input', () => { + expect(parseNaturalDate('xyz abc', 'en', now)).toBeNull(); + }); + }); + + describe('formatFullDateTime', () => { + const date = new Date('2023-03-17T08:00:00'); + + it('formats in EN locale', () => { + const result = formatFullDateTime(date, 'en'); + expect(result).toMatch(/Mar.*17.*2023.*8.*00/i); + }); + + it('handles pt_BR locale without errors', () => { + expect(() => formatFullDateTime(date, 'pt_BR')).not.toThrow(); + }); + }); + + describe('getScheduleShortcuts', () => { + it('returns 3 shortcuts on a normal weekday', () => { + const shortcuts = getScheduleShortcuts(wednesday); + expect(shortcuts).toHaveLength(3); + }); + + it('returns correct keys', () => { + const shortcuts = getScheduleShortcuts(wednesday); + const keys = shortcuts.map(s => s.key); + expect(keys).toEqual([ + SHORTCUT_KEYS.TOMORROW_MORNING, + SHORTCUT_KEYS.TOMORROW_AFTERNOON, + SHORTCUT_KEYS.MONDAY_MORNING, + ]); + }); + + it('computes correct dates for Wednesday', () => { + const shortcuts = getScheduleShortcuts(wednesday); + // Tomorrow = Thursday 2023-06-15 + expect(shortcuts[0].dateTime).toEqual(new Date('2023-06-15T08:00:00')); + expect(shortcuts[1].dateTime).toEqual(new Date('2023-06-15T13:00:00')); + // Monday = 2023-06-19 + expect(shortcuts[2].dateTime).toEqual(new Date('2023-06-19T08:00:00')); + }); + + it('on Sunday: tomorrow is Monday, Monday shortcut is next weeks Monday', () => { + const shortcuts = getScheduleShortcuts(sunday); + // Tomorrow = Monday 2023-06-19 + expect(shortcuts[0].dateTime).toEqual(new Date('2023-06-19T08:00:00')); + expect(shortcuts[1].dateTime).toEqual(new Date('2023-06-19T13:00:00')); + // Monday = next week's Monday 2023-06-26 + expect(shortcuts[2].dateTime).toEqual(new Date('2023-06-26T08:00:00')); + }); + + it('on Saturday: tomorrow is Sunday, Monday is the day after', () => { + const shortcuts = getScheduleShortcuts(saturday); + // Tomorrow = Sunday 2023-06-18 + expect(shortcuts[0].dateTime).toEqual(new Date('2023-06-18T08:00:00')); + expect(shortcuts[1].dateTime).toEqual(new Date('2023-06-18T13:00:00')); + // Monday = 2023-06-19 + expect(shortcuts[2].dateTime).toEqual(new Date('2023-06-19T08:00:00')); + }); + + it('on Monday: tomorrow is Tuesday, Monday shortcut is next Monday', () => { + const shortcuts = getScheduleShortcuts(monday); + // Tomorrow = Tuesday 2023-06-20 + expect(shortcuts[0].dateTime).toEqual(new Date('2023-06-20T08:00:00')); + expect(shortcuts[1].dateTime).toEqual(new Date('2023-06-20T13:00:00')); + // Monday = 2023-06-26 + expect(shortcuts[2].dateTime).toEqual(new Date('2023-06-26T08:00:00')); + }); + + it('includes formatted date and time', () => { + const shortcuts = getScheduleShortcuts(wednesday, 'en'); + shortcuts.forEach(s => { + expect(s.formattedDate).toBeTruthy(); + expect(s.formattedTime).toBeTruthy(); + }); + }); + + it('handles pt_BR locale', () => { + expect(() => getScheduleShortcuts(wednesday, 'pt_BR')).not.toThrow(); + }); + + it('filters out shortcuts that are in the past', () => { + // Late Wednesday night — tomorrow morning 08:00 is still in the future + const lateWednesday = new Date('2023-06-14T23:59:00'); + const shortcuts = getScheduleShortcuts(lateWednesday); + expect(shortcuts.length).toBeGreaterThanOrEqual(2); + shortcuts.forEach(s => { + expect(s.dateTime.getTime()).toBeGreaterThan(lateWednesday.getTime()); + }); + }); + }); +}); diff --git a/app/javascript/dashboard/i18n/locale/en/conversation.json b/app/javascript/dashboard/i18n/locale/en/conversation.json index e7f35d90f..433676385 100644 --- a/app/javascript/dashboard/i18n/locale/en/conversation.json +++ b/app/javascript/dashboard/i18n/locale/en/conversation.json @@ -446,8 +446,6 @@ "MESSAGE_LABEL": "Message", "MESSAGE_PLACEHOLDER": "Write your message...", "DATETIME_LABEL": "Date and time to send", - "DATETIME_PLACEHOLDER": "Select date and time", - "DATETIME_FORMAT": "MMM D, YYYY h:mm A", "ATTACHMENT_LABEL": "Attachment", "ATTACHMENT_ADD": "Attach file", "ATTACHMENT_CURRENT": "Current attachment: {filename}", @@ -456,7 +454,18 @@ "TEMPLATE_ACTION": "Schedule message", "CANCEL": "Cancel", "SAVE_DRAFT": "Save as draft", - "SCHEDULE": "Schedule" + "SCHEDULE": "Schedule", + "SHORTCUTS": { + "TOMORROW_MORNING": "Tomorrow morning", + "TOMORROW_AFTERNOON": "Tomorrow afternoon", + "MONDAY_MORNING": "Monday morning", + "CUSTOM": "Type a date and time" + }, + "CUSTOM_INPUT_PLACEHOLDER": "e.g. tomorrow at 2pm, next friday morning", + "CUSTOM_INPUT_HINT": "Try: tomorrow at 3pm, next monday, in 2 hours", + "PARSED_DATE_IN_PAST": "This date is in the past", + "DATEPICKER_TOOLTIP": "Pick from calendar", + "SCHEDULE_LABEL": "Schedule send" }, "CONFIRM_CLOSE": { "TITLE": "Unsaved changes", diff --git a/app/javascript/dashboard/i18n/locale/pt_BR/conversation.json b/app/javascript/dashboard/i18n/locale/pt_BR/conversation.json index a12ff0264..f6e7b07d4 100644 --- a/app/javascript/dashboard/i18n/locale/pt_BR/conversation.json +++ b/app/javascript/dashboard/i18n/locale/pt_BR/conversation.json @@ -435,8 +435,6 @@ "MESSAGE_LABEL": "Mensagem", "MESSAGE_PLACEHOLDER": "Escreva sua mensagem...", "DATETIME_LABEL": "Data e hora de envio", - "DATETIME_PLACEHOLDER": "Selecione data e hora", - "DATETIME_FORMAT": "D [de] MMM, YYYY HH:mm", "ATTACHMENT_LABEL": "Anexo", "ATTACHMENT_ADD": "Anexar arquivo", "ATTACHMENT_CURRENT": "Anexo atual: {filename}", @@ -445,7 +443,18 @@ "TEMPLATE_ACTION": "Agendar mensagem", "CANCEL": "Cancelar", "SAVE_DRAFT": "Salvar como rascunho", - "SCHEDULE": "Agendar" + "SCHEDULE": "Agendar", + "SHORTCUTS": { + "TOMORROW_MORNING": "Amanhã de manhã", + "TOMORROW_AFTERNOON": "Amanhã à tarde", + "MONDAY_MORNING": "Segunda-feira de manhã", + "CUSTOM": "Digitar data e hora" + }, + "CUSTOM_INPUT_PLACEHOLDER": "ex: amanhã às 14h, próxima sexta de manhã", + "CUSTOM_INPUT_HINT": "Tente: amanhã às 15h, próxima segunda, 20 de março às 10h", + "PARSED_DATE_IN_PAST": "Esta data já passou", + "DATEPICKER_TOOLTIP": "Escolher no calendário", + "SCHEDULE_LABEL": "Programar envio" }, "CONFIRM_CLOSE": { "TITLE": "Alterações não salvas", diff --git a/app/javascript/dashboard/routes/dashboard/conversation/scheduledMessages/ScheduleDateShortcuts.vue b/app/javascript/dashboard/routes/dashboard/conversation/scheduledMessages/ScheduleDateShortcuts.vue new file mode 100644 index 000000000..60f3f31f8 --- /dev/null +++ b/app/javascript/dashboard/routes/dashboard/conversation/scheduledMessages/ScheduleDateShortcuts.vue @@ -0,0 +1,245 @@ + + + diff --git a/app/javascript/dashboard/routes/dashboard/conversation/scheduledMessages/ScheduledMessageModal.vue b/app/javascript/dashboard/routes/dashboard/conversation/scheduledMessages/ScheduledMessageModal.vue index 6b280bcd3..173c18a23 100644 --- a/app/javascript/dashboard/routes/dashboard/conversation/scheduledMessages/ScheduledMessageModal.vue +++ b/app/javascript/dashboard/routes/dashboard/conversation/scheduledMessages/ScheduledMessageModal.vue @@ -1,7 +1,6 @@