feat: Implement routines feature with CRUD operations and child assignment
All checks were successful
Chore App Build, Test, and Push Docker Images / build-and-push (push) Successful in 3m0s

- Add backend routines management with add, get, update, delete, and list functionalities.
- Create models for Routine, RoutineItem, RoutineSchedule, and RoutineExtension.
- Develop event types for routine confirmation and modification.
- Implement frontend components for routine assignment, confirmation dialog, and routine management views.
- Add unit tests for routine API and integration tests for routine CRUD flow.
- Create end-to-end test plan for routines feature covering parent and child interactions.
This commit is contained in:
2026-05-05 09:08:19 -04:00
parent 082097b4f9
commit eb775ba7d8
41 changed files with 2847 additions and 29 deletions

View File

@@ -2,17 +2,21 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'
import { mount } from '@vue/test-utils'
import { nextTick } from 'vue'
import ScheduleModal from '../components/shared/ScheduleModal.vue'
import type { ChildTask, ChoreSchedule } from '../common/models'
import type { ChildTask, ChoreSchedule, RoutineSchedule } from '../common/models'
// ---------------------------------------------------------------------------
// Mocks
// ---------------------------------------------------------------------------
const mockSetChoreSchedule = vi.fn()
const mockDeleteChoreSchedule = vi.fn()
const mockSetRoutineSchedule = vi.fn()
const mockDeleteRoutineSchedule = vi.fn()
vi.mock('@/common/api', () => ({
setChoreSchedule: (...args: unknown[]) => mockSetChoreSchedule(...args),
deleteChoreSchedule: (...args: unknown[]) => mockDeleteChoreSchedule(...args),
setRoutineSchedule: (...args: unknown[]) => mockSetRoutineSchedule(...args),
deleteRoutineSchedule: (...args: unknown[]) => mockDeleteRoutineSchedule(...args),
parseErrorResponse: vi.fn().mockResolvedValue({ msg: 'error', code: 'ERR' }),
}))
@@ -39,9 +43,9 @@ const DateInputFieldStub = {
const TASK: ChildTask = { id: 'task-1', name: 'Clean Room', type: 'chore', points: 5, image_id: '' }
const CHILD_ID = 'child-1'
function mountModal(schedule: ChoreSchedule | null = null) {
function mountModal(schedule: ChoreSchedule | RoutineSchedule | null = null) {
return mount(ScheduleModal, {
props: { task: TASK, childId: CHILD_ID, schedule },
props: { entity: TASK, entityType: 'task', childId: CHILD_ID, schedule },
global: {
stubs: {
ModalDialog: ModalDialogStub,
@@ -55,8 +59,12 @@ function mountModal(schedule: ChoreSchedule | null = null) {
beforeEach(() => {
mockSetChoreSchedule.mockReset()
mockDeleteChoreSchedule.mockReset()
mockSetRoutineSchedule.mockReset()
mockDeleteRoutineSchedule.mockReset()
mockSetChoreSchedule.mockResolvedValue({ ok: true })
mockDeleteChoreSchedule.mockResolvedValue({ ok: true })
mockSetRoutineSchedule.mockResolvedValue({ ok: true })
mockDeleteRoutineSchedule.mockResolvedValue({ ok: true })
})
// ---------------------------------------------------------------------------
// Mode toggle
@@ -488,3 +496,39 @@ describe('ScheduleModal backdrop', () => {
expect(w.emitted('cancelled')).toBeFalsy()
})
})
// ---------------------------------------------------------------------------
// Entity type routing
// ---------------------------------------------------------------------------
describe('ScheduleModal entityType routing', () => {
it('uses routine schedule API when entityType is routine', async () => {
const routineEntity = { ...TASK, id: 'routine-1', items: [] }
const w = mount(ScheduleModal, {
props: {
entity: routineEntity,
entityType: 'routine',
childId: CHILD_ID,
schedule: null,
},
global: {
stubs: {
ModalDialog: ModalDialogStub,
TimePickerPopover: TimePickerPopoverStub,
DateInputField: DateInputFieldStub,
},
},
})
await w.findAll('.chip')[1].trigger('click')
await nextTick()
await w.find('.btn-primary').trigger('click')
await nextTick()
expect(mockSetRoutineSchedule).toHaveBeenCalledWith(
CHILD_ID,
'routine-1',
expect.objectContaining({ mode: 'days' }),
)
expect(mockSetChoreSchedule).not.toHaveBeenCalled()
})
})