diff --git a/app/composables/useAdmin.ts b/app/composables/useAdmin.ts index f5c76eb9..dae4acde 100644 --- a/app/composables/useAdmin.ts +++ b/app/composables/useAdmin.ts @@ -1,9 +1,5 @@ -// composables/useAdmin.ts -// Place this at: app/composables/useAdmin.ts -import dayjs from 'dayjs' -import utc from 'dayjs/plugin/utc' +import * as dateLogic from '../utils/dateLogic' -dayjs.extend(utc) export const useAdmin = () => { const callFormApi = async (method: 'GET' | 'POST' | 'PUT' | 'DELETE', params: Record = {}, body?: Record): Promise => { const queryString = method === 'GET' || method === 'DELETE' @@ -22,40 +18,6 @@ export const useAdmin = () => { }) } - const parseLocalDate = (value: string) => { - if (!value) return null - - const match = value.match(/^(\d{4})-(\d{2})-(\d{2})$/) - if (!match) return null - - const [, year, month, day] = match - const parsed = new Date(Number(year), Number(month) - 1, Number(day)) - - return Number.isNaN(parsed.getTime()) ? null : parsed - } - - 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}` - } - - const parseDateToYmd = (value: string) => { - const parsed = parseLocalDate(value) - - if (parsed) { - return formatYmdLocal(parsed) - } - - const fallback = new Date(value) - if (Number.isNaN(fallback.getTime())) { - return '' - } - - return formatYmdLocal(fallback) - } - const buildQuestionOptions = (question: any) => ({ textEs: question.textEs ?? '', reference: question.reference ?? '', @@ -78,13 +40,15 @@ export const useAdmin = () => { const mapApiFormToUi = (form: any) => { const questionList = Array.isArray(form.questions) ? form.questions : [] + const formDate = dateLogic.parseDateToYmd(form.startDate || form.weekStart || '') return { id: Number(form.id), - weekStart: parseDateToYmd(form.weekStart || form.startDate || ''), + weekStart: dateLogic.parseDateToYmd(form.weekStart || form.startDate || ''), day: form.day || 'Monday', title: form.title || `Form ${form.id}`, - date: form.date || formatDate(form.startDate || ''), + startDate: formDate, + date: form.date || dateLogic.formatYmdUtcDateString(form.startDate || ''), status: form.status || (form.published ? 'Active' : 'Unpublished'), questions: questionList.map((question: any, index: number) => ({ id: Number(question.id ?? index + 1), @@ -110,15 +74,29 @@ export const useAdmin = () => { const dayOff = (todayDate.getDay() + 6) % 7 const mon = new Date(todayDate) mon.setDate(todayDate.getDate() - dayOff) - const monStr = formatYmdLocal(mon) + const monStr = dateLogic.formatYmdLocal(mon) 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)) { @@ -129,53 +107,7 @@ 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', - }) - } const defaultQuestions = (): any[] => [ { id: Date.now(), type: 'video', text: '', textEs: '', reference: '', referenceEs: '', url: '' }, @@ -200,17 +132,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 }) ) @@ -233,8 +192,8 @@ export const useAdmin = () => { return } - const fallbackWeekStart = getLastMonday(historyWeekStart.value) - const fallbackWeekEnd = dayjs.utc(fallbackWeekStart).add(6, 'day').format('YYYY-MM-DD') + const fallbackWeekStart = dateLogic.startOfWeekString(historyWeekStart.value) + const fallbackWeekEnd = dateLogic.endOfWeekString(fallbackWeekStart) try { const result = await callFormApi<{ @@ -252,8 +211,8 @@ export const useAdmin = () => { return } - historyGroupStartDate.value = parseDateToYmd(result.startDate || '') - historyGroupEndDate.value = parseDateToYmd(result.endDate || '') || fallbackWeekEnd + historyGroupStartDate.value = dateLogic.parseDateToYmd(result.startDate || '') + historyGroupEndDate.value = dateLogic.parseDateToYmd(result.endDate || '') || fallbackWeekEnd } catch (error) { console.error('Failed to resolve form group range', error) historyGroupStartDate.value = fallbackWeekStart @@ -300,110 +259,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 } - - try { - const weekStart = getLastMonday(formWeekStart.value || '') - const days = ['Monday','Tuesday','Wednesday','Thursday','Friday','Saturday','Sunday'] + 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 } - if (editingFormId.value) { - const targetDay = formDays.value[0] || 'Monday' - const dayIndex = days.indexOf(targetDay) + const weekStart = dateLogic.startOfWeekString(formWeekStart.value || '') + 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 = dateLogic.add(weekStart, 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: dateLogic.formatYmdLocal(startDate), + 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: dateLogic.startOfWeekString(weekStart), + 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 || dateLogic.startOfCurrentWeekString(), + 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 publishedDates: string[] = [] - const createdForm = createdFormResponse?.data + if (published) { + for (const day of formDays.value) { + const dayIndex = days.indexOf(day) - if (!createdForm?.id) { + if (dayIndex === -1) { continue } + const startDate = dateLogic.add(weekStart, dayIndex, 'day') + publishedDates.push(dateLogic.formatDayMDY(startDate)) + + await callFormApi('PUT', {}, { + action: 'updateForm', + id: createdForm.id, + startDate: dateLogic.formatYmdLocal(startDate), + published: true, + title: formTitle.value, + }) + for (let index = 0; index < questions.value.length; index++) { const question = questions.value[index] @@ -417,15 +390,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: dateLogic.startOfWeekString(weekStart), + 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 @@ -448,16 +455,39 @@ export const useAdmin = () => { }) } + 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 } // ── Students / Progress ── - const students = useState('adminStudents', () => [ - { id: 1, name: 'Aiden Smith', initials: 'AS', email: 'aiden@school.edu', tickets: 12, streak: 4, lastActive: '2 hours ago' }, - { id: 2, name: 'Nevin Kumar', initials: 'NK', email: 'nevin@school.edu', tickets: 14, streak: 5, lastActive: 'Just now' }, - { id: 3, name: 'Swarna Jay', initials: 'SJ', email: 'swarna@school.edu',tickets: 8, streak: 2, lastActive: 'Yesterday' }, - ]) + const students = useState('adminStudents', () => []) const searchStudent = useState('searchStudent', () => '') const sortStudent = useState('sortStudent', () => 'tickets') @@ -479,13 +509,7 @@ export const useAdmin = () => { // ── Announcements ── const announcementSubTab = useState<'creation' | 'history'>('announcementSubTab', () => 'creation') - const announcements = useState('announcements', () => [ - { id: 1, title: 'Summer Reading Challenge!', content: 'Log 20 books this month to win a Super Sage badge!', icon: '🌟', startDate: '2026-03-01', endDate: '2026-03-31', weekStart: '2026-03-02', day: 'Monday' }, - { id: 2, title: 'New Badges Available', content: 'Check the shop for new limited edition themes.', icon: '🎉', startDate: '2026-03-05', endDate: '', weekStart: '2026-03-02', day: 'Thursday' }, - { id: 3, title: 'Friday Game Night', content: 'Join us in the library for board games and snacks!', icon: '🎲', startDate: '2026-03-06', endDate: '', weekStart: '2026-03-02', day: 'Friday' }, - { id: 4, title: 'Week 10 Progress', content: 'You are doing amazing! Keep up the streak.', icon: '📈', startDate: '2026-03-09', endDate: '', weekStart: '2026-03-09', day: 'Monday' }, - { id: 5, title: 'Author Visit', content: 'Virtual session this Wednesday at 10 AM.', icon: '✍️', startDate: '2026-03-11', endDate: '', weekStart: '2026-03-09', day: 'Wednesday' }, - ]) + const announcements = useState('announcements', () => []) const newAnnouncement = useState('newAnnouncement', () => ({ title: '', content: '', icon: '🌟', @@ -527,12 +551,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, 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 1e7277f6..4a502a11 100644 --- a/app/pages/admin/builder.vue +++ b/app/pages/admin/builder.vue @@ -1,22 +1,48 @@