diff --git a/resources/js/app.js b/resources/js/app.js index c8888c0..46eded2 100644 --- a/resources/js/app.js +++ b/resources/js/app.js @@ -643,6 +643,92 @@ function initNaturalEventParser(root = document) { return null; }; + const weekdayCodeMap = { + su: 'SU', sun: 'SU', sunday: 'SU', sundays: 'SU', + mo: 'MO', mon: 'MO', monday: 'MO', mondays: 'MO', + tu: 'TU', tue: 'TU', tues: 'TU', tuesday: 'TU', tuesdays: 'TU', + we: 'WE', wed: 'WE', wednesday: 'WE', wednesdays: 'WE', + th: 'TH', thu: 'TH', thur: 'TH', thurs: 'TH', thursday: 'TH', thursdays: 'TH', + fr: 'FR', fri: 'FR', friday: 'FR', fridays: 'FR', + sa: 'SA', sat: 'SA', saturday: 'SA', saturdays: 'SA', + }; + + const weekdayNameMap = { + SU: 'Sunday', + MO: 'Monday', + TU: 'Tuesday', + WE: 'Wednesday', + TH: 'Thursday', + FR: 'Friday', + SA: 'Saturday', + }; + + const weekdayCodeFromDate = (date) => ['SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA'][date.getDay()]; + + const parseRecurrenceToken = (text) => { + const repeatMatch = text.match(/\brepeats?\b([\s\S]*)$/i); + if (!repeatMatch) return null; + + const tail = (repeatMatch[1] || '').trim().toLowerCase(); + const recurrence = { + frequency: null, + interval: 1, + weekdays: [], + index: repeatMatch.index ?? 0, + }; + + const everyNumbered = tail.match(/\bevery\s+(\d+)\s*(day|week|month|year)s?\b/i); + if (everyNumbered) { + const count = parseInt(everyNumbered[1], 10); + const unit = everyNumbered[2].toLowerCase(); + if (!Number.isNaN(count) && count > 0) { + recurrence.interval = count; + } + recurrence.frequency = { + day: 'daily', + week: 'weekly', + month: 'monthly', + year: 'yearly', + }[unit] || null; + } else if (/\b(daily|every day)\b/i.test(tail)) { + recurrence.frequency = 'daily'; + } else if (/\b(weekly|every week)\b/i.test(tail)) { + recurrence.frequency = 'weekly'; + } else if (/\b(monthly|every month)\b/i.test(tail)) { + recurrence.frequency = 'monthly'; + } else if (/\b(yearly|every year)\b/i.test(tail)) { + recurrence.frequency = 'yearly'; + } + + if (!recurrence.frequency) { + return null; + } + + if (recurrence.frequency === 'weekly') { + const onMatch = tail.match(/\bon\s+(.+)$/i); + if (onMatch) { + const weekdays = []; + const tokens = onMatch[1] + .replace(/[,/]/g, ' ') + .replace(/\band\b/g, ' ') + .split(/\s+/) + .map((token) => token.trim().toLowerCase().replace(/[^a-z]/g, '')) + .filter(Boolean); + + tokens.forEach((token) => { + const code = weekdayCodeMap[token]; + if (code && !weekdays.includes(code)) { + weekdays.push(code); + } + }); + + recurrence.weekdays = weekdays; + } + } + + return recurrence; + }; + const parseLocationToken = (text) => { const lower = text.toLowerCase(); const index = lower.lastIndexOf(' at '); @@ -656,11 +742,12 @@ function initNaturalEventParser(root = document) { return candidate; }; - const parseTitleToken = (text, dateToken, timeToken, relativeToken, locationToken) => { + const parseTitleToken = (text, dateToken, timeToken, relativeToken, recurrenceToken, locationToken) => { const boundaries = []; if (dateToken) boundaries.push(dateToken.index); if (timeToken) boundaries.push(timeToken.index); if (relativeToken) boundaries.push(relativeToken.index); + if (recurrenceToken) boundaries.push(recurrenceToken.index); const lower = text.toLowerCase(); const durationIndex = lower.indexOf(' for '); @@ -684,11 +771,12 @@ function initNaturalEventParser(root = document) { if (!trimmed) return null; const relativeToken = parseRelativeToken(trimmed, baseDate); + const recurrenceToken = parseRecurrenceToken(trimmed); const dateToken = parseDateToken(trimmed, baseDate); const timeToken = parseTimeToken(trimmed); const durationMinutes = parseDurationMinutes(trimmed); const location = parseLocationToken(trimmed); - const title = parseTitleToken(trimmed, dateToken, timeToken, relativeToken, location); + const title = parseTitleToken(trimmed, dateToken, timeToken, relativeToken, recurrenceToken, location); const allDay = /\ball(?:\s|-)?day\b/i.test(trimmed); return { @@ -697,6 +785,7 @@ function initNaturalEventParser(root = document) { dateToken, timeToken, relativeToken, + recurrenceToken, durationMinutes, allDay, }; @@ -725,6 +814,108 @@ function initNaturalEventParser(root = document) { renderModalAside(dialog, [], true); }; + const clearRecurrenceInputs = (form) => { + const frequencyInput = form.querySelector('select[name="repeat_frequency"]'); + const intervalInput = form.querySelector('input[name="repeat_interval"]'); + const weekdayInputs = Array.from(form.querySelectorAll('input[name="repeat_weekdays[]"]')); + const monthlyModeInputs = Array.from(form.querySelectorAll('input[name="repeat_monthly_mode"]')); + const monthDayInputs = Array.from(form.querySelectorAll('input[name="repeat_month_days[]"]')); + + if (frequencyInput) { + frequencyInput.value = ''; + frequencyInput.dispatchEvent(new Event('change', { bubbles: true })); + } + if (intervalInput) { + intervalInput.value = '1'; + } + weekdayInputs.forEach((input) => { + input.checked = false; + }); + monthlyModeInputs.forEach((input) => { + input.checked = input.value === 'days'; + }); + monthDayInputs.forEach((input) => { + input.checked = false; + }); + }; + + const applyRecurrenceInputs = (form, recurrenceToken, anchorDate) => { + const frequencyInput = form.querySelector('select[name="repeat_frequency"]'); + const intervalInput = form.querySelector('input[name="repeat_interval"]'); + const weekdayInputs = Array.from(form.querySelectorAll('input[name="repeat_weekdays[]"]')); + const monthlyModeInputs = Array.from(form.querySelectorAll('input[name="repeat_monthly_mode"]')); + const monthDayInputs = Array.from(form.querySelectorAll('input[name="repeat_month_days[]"]')); + + if (!frequencyInput || !recurrenceToken?.frequency) { + return; + } + + frequencyInput.value = recurrenceToken.frequency; + frequencyInput.dispatchEvent(new Event('change', { bubbles: true })); + + if (intervalInput) { + intervalInput.value = String(recurrenceToken.interval || 1); + } + + if (recurrenceToken.frequency === 'weekly') { + let weekdays = recurrenceToken.weekdays || []; + if (!weekdays.length && anchorDate) { + weekdays = [weekdayCodeFromDate(anchorDate)]; + } + weekdayInputs.forEach((input) => { + input.checked = weekdays.includes(input.value); + }); + } else { + weekdayInputs.forEach((input) => { + input.checked = false; + }); + } + + if (recurrenceToken.frequency === 'monthly') { + monthlyModeInputs.forEach((input) => { + input.checked = input.value === 'days'; + }); + + const dayOfMonth = anchorDate ? anchorDate.getDate() : null; + monthDayInputs.forEach((input) => { + input.checked = dayOfMonth ? Number(input.value) === dayOfMonth : false; + }); + } else { + monthDayInputs.forEach((input) => { + input.checked = false; + }); + } + }; + + const recurrenceSummary = (recurrenceToken, anchorDate) => { + if (!recurrenceToken?.frequency) return ''; + + const interval = recurrenceToken.interval || 1; + + if (recurrenceToken.frequency === 'daily') { + return interval === 1 ? 'Daily' : `Every ${interval} days`; + } + + if (recurrenceToken.frequency === 'weekly') { + const days = (recurrenceToken.weekdays || []).length + ? recurrenceToken.weekdays + : (anchorDate ? [weekdayCodeFromDate(anchorDate)] : []); + const dayLabels = days.map((code) => weekdayNameMap[code] || code); + const head = interval === 1 ? 'Every week' : `Every ${interval} weeks`; + return dayLabels.length ? `${head} on ${dayLabels.join(' and ')}` : head; + } + + if (recurrenceToken.frequency === 'monthly') { + return interval === 1 ? 'Monthly' : `Every ${interval} months`; + } + + if (recurrenceToken.frequency === 'yearly') { + return interval === 1 ? 'Yearly' : `Every ${interval} years`; + } + + return ''; + }; + const applyParsedData = (input) => { const dialog = input.closest('dialog'); const modal = dialog?.querySelector(SELECTORS.modalContent); @@ -793,6 +984,14 @@ function initNaturalEventParser(root = document) { endInput.value = toLocalDatetimeInputValue(end); } + if (parsed.recurrenceToken) { + applyRecurrenceInputs(form, parsed.recurrenceToken, baseDate); + input.dataset.nlRecurrenceApplied = '1'; + } else if (input.dataset.nlRecurrenceApplied === '1') { + clearRecurrenceInputs(form); + input.dataset.nlRecurrenceApplied = '0'; + } + const summaryItems = []; if (parsed.title) { summaryItems.push({ label: 'Title', value: parsed.title }); @@ -814,6 +1013,12 @@ function initNaturalEventParser(root = document) { summaryItems.push({ label: 'Date', value: summaryDate }); } } + if (parsed.recurrenceToken) { + const repeat = recurrenceSummary(parsed.recurrenceToken, baseDate); + if (repeat) { + summaryItems.push({ label: 'Repeat', value: repeat }); + } + } if (modal?.classList.contains('natural-collapsed')) { renderModalAside(dialog, summaryItems, true);