diff --git a/app/composables/useAdmin.ts b/app/composables/useAdmin.ts index 685b6bb..8e20b74 100644 --- a/app/composables/useAdmin.ts +++ b/app/composables/useAdmin.ts @@ -1,8 +1,10 @@ // composables/useAdmin.ts // Place this at: app/composables/useAdmin.ts import dayjs from 'dayjs' +import isoWeek from 'dayjs/plugin/isoWeek' import utc from 'dayjs/plugin/utc' +dayjs.extend(isoWeek) dayjs.extend(utc) export const useAdmin = () => { const callFormApi = async (method: 'GET' | 'POST' | 'PUT' | 'DELETE', params: Record = {}, body?: Record): Promise => { @@ -35,10 +37,7 @@ export const useAdmin = () => { } const formatYmdLocal = (date: Date) => { - const year = date.getFullYear() - const month = String(date.getMonth() + 1).padStart(2, '0') - const day = String(date.getDate()).padStart(2, '0') - return `${year}-${month}-${day}` + return dayjs(date).format('YYYY-MM-DD') } const parseDateToYmd = (value: string) => { @@ -82,12 +81,14 @@ export const useAdmin = () => { const mapApiFormToUi = (form: any) => { const questionList = Array.isArray(form.questions) ? form.questions : [] + const formDate = parseDateToYmd(form.startDate || form.weekStart || '') return { id: Number(form.id), weekStart: parseDateToYmd(form.weekStart || form.startDate || ''), day: form.day || 'Monday', title: form.title || `Form ${form.id}`, + startDate: formDate, date: form.date || formatDate(form.startDate || ''), status: form.status || (form.published ? 'Active' : 'Unpublished'), questions: questionList.map((question: any, index: number) => ({ @@ -119,10 +120,24 @@ export const useAdmin = () => { const formWeekStart = useState('formWeekStart', () => monStr) const formDays = useState('formDays', () => ['Monday']) const historyWeekStart = useState('historyWeekStart', () => '') + const historyKeywordQuery = useState('historyKeywordQuery', () => '') + const historyAdvancedFiltersOpen = useState('historyAdvancedFiltersOpen', () => false) const historyStatusSelection = useState>('historyStatusSelection', () => ['published', 'unpublished']) const historyGroupStartDate = useState('historyGroupStartDate', () => '') const historyGroupEndDate = useState('historyGroupEndDate', () => '') + const emptyFormPromptOpen = useState('emptyFormPromptOpen', () => false) const selectedFormDetails = useState('selectedFormDetails', () => null) + const publishSuccessInfo = useState('publishSuccessInfo', () => null) + + const resetHistoryAdvancedFilters = () => { + historyStatusSelection.value = ['published', 'unpublished'] + historyGroupStartDate.value = '' + historyGroupEndDate.value = '' + } + const resetHistoryFilters = () => { + historyKeywordQuery.value = '' + historyWeekStart.value = '' + } const toggleHistoryStatus = (value: 'published' | 'unpublished') => { if (historyStatusSelection.value.includes(value)) { @@ -133,52 +148,10 @@ export const useAdmin = () => { historyStatusSelection.value = [...historyStatusSelection.value, value] } - // ── Helpers ── - const getCalculatedDate = (weekStartStr: string, dayName: string): string => { - if (!weekStartStr) return '' - - const days = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'] - const idx = days.indexOf(dayName) - if (idx === -1) return '' - - const base = parseLocalDate(weekStartStr) - if (!base) return '' - - const d = new Date(base) - d.setDate(base.getDate() + idx) - - return d.toLocaleDateString('en-US', { - weekday: 'long', - month: 'short', - day: 'numeric', - year: 'numeric', - }) - } - - const getLastMonday = (dateStr: string) => { - const d = parseLocalDate(dateStr) - if (!d) return '' - - const day = d.getDay() - const diffToMonday = day === 0 ? -6 : 1 - day - - const monday = new Date(d) - monday.setDate(d.getDate() + diffToMonday) - - return formatYmdLocal(monday) - } - const formatDate = (dateStr: string): string => { if (!dateStr) return '' - const parsed = parseLocalDate(dateStr) - if (!parsed) return '' - - return parsed.toLocaleDateString('en-US', { - month: 'short', - day: 'numeric', - year: 'numeric', - }) + return dayjs.utc(dateStr).format('MMM D, YYYY') } const defaultQuestions = (): any[] => [ @@ -204,17 +177,44 @@ export const useAdmin = () => { const matchesUnpublished = historyStatusSelection.value.includes('unpublished') && !isActive const matchesStatus = matchesPublished || matchesUnpublished + const normalizedKeyword = historyKeywordQuery.value.trim().toLowerCase() + const searchableFields = [ + form.title, + form.day, + form.weekStart, + form.date, + form.status, + ] + + for (const question of form.questions ?? []) { + searchableFields.push( + question.type, + question.text, + question.textEs, + question.reference, + question.referenceEs, + question.url, + ) + + for (const choice of question.choices ?? []) { + searchableFields.push(choice.text) + } + } + + const matchesKeyword = + !normalizedKeyword || searchableFields.filter(Boolean).join(' ').toLowerCase().includes(normalizedKeyword) + const matchesGroupStart = !historyGroupStartDate.value || - !form.weekStart || - form.weekStart >= historyGroupStartDate.value + !form.startDate || + form.startDate >= historyGroupStartDate.value const matchesGroupEnd = !historyGroupEndDate.value || - !form.weekStart || - form.weekStart <= historyGroupEndDate.value + !form.startDate || + form.startDate <= historyGroupEndDate.value - return matchesStatus && matchesGroupStart && matchesGroupEnd + return matchesStatus && matchesKeyword && matchesGroupStart && matchesGroupEnd }) ) @@ -237,8 +237,8 @@ export const useAdmin = () => { return } - const fallbackWeekStart = getLastMonday(historyWeekStart.value) - const fallbackWeekEnd = dayjs.utc(fallbackWeekStart).add(6, 'day').format('YYYY-MM-DD') + const fallbackWeekStart = dayjs.utc(historyWeekStart.value).startOf('isoWeek').format('YYYY-MM-DD') + const fallbackWeekEnd = dayjs.utc(fallbackWeekStart).endOf('isoWeek').format('YYYY-MM-DD') try { const result = await callFormApi<{ @@ -304,110 +304,124 @@ export const useAdmin = () => { questions.value.push(q) } - const publishForm = async () => { - if (!formTitle.value) { alert('Please enter a title!'); return } - if (!formDays.value.length) { alert('Please select at least one day!'); return } + const persistForm = async (published: boolean) => { + if (!formTitle.value) { alert('Please enter a title!'); return false } + if (!formDays.value.length) { alert('Please select at least one day!'); return false } - try { - const weekStart = getLastMonday(formWeekStart.value || '') - const days = ['Monday','Tuesday','Wednesday','Thursday','Friday','Saturday','Sunday'] - - if (editingFormId.value) { - const targetDay = formDays.value[0] || 'Monday' - const dayIndex = days.indexOf(targetDay) + const weekStart = dayjs.utc(formWeekStart.value || '').startOf('isoWeek').format('YYYY-MM-DD') + const days = ['Monday','Tuesday','Wednesday','Thursday','Friday','Saturday','Sunday'] - if (dayIndex === -1) { - throw new Error('Invalid day selected for update') - } + if (editingFormId.value) { + const targetDay = formDays.value[0] || 'Monday' + const dayIndex = days.indexOf(targetDay) - const startDate = parseLocalDate(weekStart) - if (!startDate) throw new Error('Invalid week start date') - startDate.setDate(startDate.getDate() + dayIndex) + if (dayIndex === -1) { + throw new Error('Invalid day selected for update') + } - await callFormApi('PUT', {}, { - action: 'updateForm', - id: editingFormId.value, - startDate: formatYmdLocal(startDate), - published: true, - title: formTitle.value, - }) + const startDate = dayjs.utc(weekStart).add(dayIndex, 'day') - const existingForm = publishedForms.value.find((form) => Number(form.id) === Number(editingFormId.value)) - const existingComponentIds = new Set( - (existingForm?.questions ?? []) - .map((question: any) => Number(question.id)) - .filter((questionId: number) => Number.isInteger(questionId) && questionId > 0) - ) + await callFormApi('PUT', {}, { + action: 'updateForm', + id: editingFormId.value, + startDate: startDate.format('YYYY-MM-DD'), + published, + title: formTitle.value, + }) - for (let index = 0; index < questions.value.length; index++) { - const question = questions.value[index] - const numericQuestionId = Number(question.id) - const isExistingComponent = Number.isInteger(numericQuestionId) && existingComponentIds.has(numericQuestionId) - - if (isExistingComponent) { - await callFormApi('PUT', {}, { - action: 'updateComponent', - id: numericQuestionId, - order: index, - questionType: question.type, - questionText: toApiQuestionText(question, formTitle.value), - questionOptions: buildQuestionOptions(question), - }) - - existingComponentIds.delete(numericQuestionId) - } else { - await callFormApi('POST', {}, { - action: 'createComponent', - form: editingFormId.value, - order: index, - questionType: question.type, - questionText: toApiQuestionText(question, formTitle.value), - questionOptions: buildQuestionOptions(question), - }) - } - } + const existingForm = publishedForms.value.find((form) => Number(form.id) === Number(editingFormId.value)) + const existingComponentIds = new Set( + (existingForm?.questions ?? []) + .map((question: any) => Number(question.id)) + .filter((questionId: number) => Number.isInteger(questionId) && questionId > 0) + ) + + for (let index = 0; index < questions.value.length; index++) { + const question = questions.value[index] + const numericQuestionId = Number(question.id) + const isExistingComponent = Number.isInteger(numericQuestionId) && existingComponentIds.has(numericQuestionId) + + if (isExistingComponent) { + await callFormApi('PUT', {}, { + action: 'updateComponent', + id: numericQuestionId, + order: index, + questionType: question.type, + questionText: toApiQuestionText(question, formTitle.value), + questionOptions: buildQuestionOptions(question), + }) - for (const removedId of existingComponentIds) { - await callFormApi('DELETE', {}, { - action: 'deleteComponent', - id: removedId, + existingComponentIds.delete(numericQuestionId) + } else { + await callFormApi('POST', {}, { + action: 'createComponent', + form: editingFormId.value, + order: index, + questionType: question.type, + questionText: toApiQuestionText(question, formTitle.value), + questionOptions: buildQuestionOptions(question), }) } + } - await loadPublishedForms() - editingFormId.value = null - builderSubTab.value = 'history' - alert('Form updated successfully') - return + for (const removedId of existingComponentIds) { + await callFormApi('DELETE', {}, { + action: 'deleteComponent', + id: removedId, + }) } - for (const day of formDays.value) { - const dayIndex = days.indexOf(day) + await loadPublishedForms() - if (dayIndex === -1) { - continue + if (published) { + publishSuccessInfo.value = { + title: formTitle.value, + days: formDays.value, + weekStart: dayjs.utc(weekStart).startOf('isoWeek').format('YYYY-MM-DD'), + isUpdate: true, + questionCount: questions.value.length, } + } - const startDate = parseLocalDate(weekStart) - if (!startDate) { - continue - } + editingFormId.value = null + builderSubTab.value = 'history' + return true + } - startDate.setDate(startDate.getDate() + dayIndex) + const createdFormResponse = await callFormApi('POST', {}, { + action: 'createForm', + startDate: weekStart || dayjs.utc().startOf('isoWeek').format('YYYY-MM-DD'), + published, + title: formTitle.value, + }) - const createdFormResponse = await callFormApi('POST', {}, { - action: 'createForm', - startDate: formatYmdLocal(startDate), - published: true, - title: formTitle.value, - }) + const createdForm = createdFormResponse?.data + + if (!createdForm?.id) { + return false + } - const createdForm = createdFormResponse?.data + const publishedDates: string[] = [] + + if (published) { + for (const day of formDays.value) { + const dayIndex = days.indexOf(day) - if (!createdForm?.id) { + if (dayIndex === -1) { continue } + const startDate = dayjs.utc(weekStart).add(dayIndex, 'day') + publishedDates.push(dayjs.utc(weekStart).add(dayIndex, 'day').format('dddd, MMM D, YYYY')) + + await callFormApi('PUT', {}, { + action: 'updateForm', + id: createdForm.id, + startDate: startDate.format('YYYY-MM-DD'), + published: true, + title: formTitle.value, + }) + for (let index = 0; index < questions.value.length; index++) { const question = questions.value[index] @@ -421,15 +435,49 @@ export const useAdmin = () => { }) } } + } - await loadPublishedForms() - alert(`Published for: ${formDays.value.join(', ')}`) + await loadPublishedForms() + + if (published) { + publishSuccessInfo.value = { + title: formTitle.value, + days: formDays.value, + weekStart: dayjs.utc(weekStart).startOf('isoWeek').format('YYYY-MM-DD'), + publishedDates, + questionCount: questions.value.length, + isUpdate: false, + } + } + + builderSubTab.value = 'history' + return true + } + + const publishForm = async () => { + if (questions.value.length === 0) { + emptyFormPromptOpen.value = true + return + } + + try { + await persistForm(true) } catch (error) { console.error('Failed to publish form', error) alert('Failed to publish form. Please try again.') } } + const saveEmptyFormDraft = async () => { + try { + await persistForm(false) + emptyFormPromptOpen.value = false + } catch (error) { + console.error('Failed to save empty form', error) + alert('Failed to save form. Please try again.') + } + } + const editPublishedForm = (form: any) => { formTitle.value = form.title formWeekStart.value = form.weekStart || formWeekStart.value @@ -444,6 +492,33 @@ export const useAdmin = () => { form.status = form.status === 'Active' ? 'Unpublished' : 'Active' } + const deleteStoredForm = async (form: any, skipConfirm = false) => { + const id = Number(form?.id) + + if (!Number.isInteger(id)) { + return + } + + if (!skipConfirm && !confirm(`Delete "${form.title}"? This cannot be undone.`)) { + return + } + + await callFormApi('DELETE', {}, { + action: 'deleteForm', + id, + }) + + if (selectedFormDetails.value?.id === id) { + selectedFormDetails.value = null + } + + if (editingFormId.value === id) { + editingFormId.value = null + } + + await loadPublishedForms() + } + const viewFormDetails = (form: any) => { selectedFormDetails.value = form } @@ -523,12 +598,13 @@ export const useAdmin = () => { return { // builder builderSubTab, formTitle, editingFormId, questions, - formWeekStart, formDays, historyWeekStart, historyStatusSelection, historyGroupStartDate, historyGroupEndDate, toggleHistoryStatus, getLastMonday, - getCalculatedDate, formatDate, defaultQuestions, + formWeekStart, formDays, historyWeekStart, historyKeywordQuery, historyAdvancedFiltersOpen, + historyStatusSelection, historyGroupStartDate, historyGroupEndDate, emptyFormPromptOpen, + toggleHistoryStatus, resetHistoryAdvancedFilters, resetHistoryFilters, formatDate, defaultQuestions, publishedForms, filteredPublishedForms, - selectedFormDetails, viewFormDetails, + selectedFormDetails, viewFormDetails, publishSuccessInfo, draggedIdx, dragStart, onDrop, - addQuestion, publishForm, editPublishedForm, toggleFormPublish, + addQuestion, publishForm, saveEmptyFormDraft, editPublishedForm, toggleFormPublish, deleteStoredForm, loadPublishedForms, // progress students, searchStudent, sortStudent, filteredAndSortedStudents, diff --git a/app/pages/admin/builder.vue b/app/pages/admin/builder.vue index 0a8aaab..e1168f6 100644 --- a/app/pages/admin/builder.vue +++ b/app/pages/admin/builder.vue @@ -1,22 +1,48 @@