feat: enhance child edit and view components with improved form handling and validation
All checks were successful
Chore App Build and Push Docker Images / build-and-push (push) Successful in 1m4s

- Added `requireDirty` prop to `EntityEditForm` for dirty state management.
- Updated `ChildEditView` to handle initial data loading and image selection more robustly.
- Refactored `ChildView` to remove unused reward dialog logic and prevent API calls in child mode.
- Improved type definitions for form fields and initial data in `ChildEditView`.
- Enhanced error handling in form submissions across components.
- Implemented cross-tab logout synchronization on password reset in the auth store.
- Added tests for login and entity edit form functionalities to ensure proper behavior.
- Introduced global fetch interceptor for handling unauthorized responses.
- Documented password reset flow and its implications on session management.
This commit is contained in:
2026-02-17 17:18:03 -05:00
parent 5e22e5e0ee
commit 31ea76f013
29 changed files with 1000 additions and 164 deletions

View File

@@ -1,6 +1,5 @@
<script setup lang="ts">
import { ref, onMounted, onUnmounted, computed } from 'vue'
import ModalDialog from '../shared/ModalDialog.vue'
import { useRoute, useRouter } from 'vue-router'
import ChildDetailCard from './ChildDetailCard.vue'
import ScrollingList from '../shared/ScrollingList.vue'
@@ -12,7 +11,6 @@ import type {
Child,
Event,
Task,
Reward,
RewardStatus,
ChildTaskTriggeredEventPayload,
ChildRewardTriggeredEventPayload,
@@ -32,9 +30,6 @@ const tasks = ref<string[]>([])
const rewards = ref<string[]>([])
const loading = ref(true)
const error = ref<string | null>(null)
const showRewardDialog = ref(false)
const showCancelDialog = ref(false)
const dialogReward = ref<Reward | null>(null)
const childRewardListRef = ref()
function handleTaskTriggered(event: Event) {
@@ -179,21 +174,7 @@ const triggerTask = async (task: Task) => {
}
}
// Trigger the task via API
if (child.value?.id && task.id) {
try {
const resp = await fetch(`/api/child/${child.value.id}/trigger-task`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ task_id: task.id }),
})
if (!resp.ok) {
console.error('Failed to trigger task')
}
} catch (err) {
console.error('Error triggering task:', err)
}
}
// Child mode is speech-only; point changes are handled in parent mode.
}
const triggerReward = (reward: RewardStatus) => {
@@ -211,60 +192,6 @@ const triggerReward = (reward: RewardStatus) => {
utter.volume = 1.0
window.speechSynthesis.speak(utter)
}
if (reward.redeeming) {
dialogReward.value = reward
showCancelDialog.value = true
return // Do not allow redeeming if already pending
}
if (reward.points_needed <= 0) {
dialogReward.value = reward
showRewardDialog.value = true
}
}
}
async function cancelPendingReward() {
if (!child.value?.id || !dialogReward.value) return
try {
const resp = await fetch(`/api/child/${child.value.id}/cancel-request-reward`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ reward_id: dialogReward.value.id }),
})
if (!resp.ok) throw new Error('Failed to cancel pending reward')
} catch (err) {
console.error('Failed to cancel pending reward:', err)
} finally {
showCancelDialog.value = false
dialogReward.value = null
}
}
function cancelRedeemReward() {
showRewardDialog.value = false
dialogReward.value = null
}
function closeCancelDialog() {
showCancelDialog.value = false
dialogReward.value = null
}
async function confirmRedeemReward() {
if (!child.value?.id || !dialogReward.value) return
try {
const resp = await fetch(`/api/child/${child.value.id}/request-reward`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ reward_id: dialogReward.value.id }),
})
if (!resp.ok) return
} catch (err) {
console.error('Failed to redeem reward:', err)
} finally {
showRewardDialog.value = false
dialogReward.value = null
}
}
@@ -464,35 +391,6 @@ onUnmounted(() => {
</ScrollingList>
</div>
</div>
<ModalDialog
v-if="showRewardDialog && dialogReward"
:imageUrl="dialogReward?.image_url"
:title="dialogReward.name"
:subtitle="`${dialogReward.cost} pts`"
>
<div class="modal-message">Would you like to redeem this reward?</div>
<div class="modal-actions">
<button @click="confirmRedeemReward" class="btn btn-primary">Yes</button>
<button @click="cancelRedeemReward" class="btn btn-secondary">No</button>
</div>
</ModalDialog>
<ModalDialog
v-if="showCancelDialog && dialogReward"
:imageUrl="dialogReward?.image_url"
:title="dialogReward.name"
:subtitle="`${dialogReward.cost} pts`"
>
<div class="modal-message">
This reward is pending.<br />
Would you like to cancel the pending reward request?
</div>
<div class="modal-actions">
<button @click="cancelPendingReward" class="btn btn-primary">Yes</button>
<button @click="closeCancelDialog" class="btn btn-secondary">No</button>
</div>
</ModalDialog>
</div>
</template>