248 lines
5.9 KiB
Vue
248 lines
5.9 KiB
Vue
<template>
|
|
<div class="child-edit-view">
|
|
<h2>{{ isEdit ? 'Edit Child' : 'Create Child' }}</h2>
|
|
<div v-if="loading" class="loading-message">Loading child...</div>
|
|
<form v-else @submit.prevent="submit" class="form">
|
|
<div class="form-group">
|
|
<label for="child-name">Name</label>
|
|
<input id="child-name" ref="nameInput" v-model="name" required maxlength="64" />
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="child-age">Age</label>
|
|
<input id="child-age" v-model.number="age" type="number" min="0" max="120" required />
|
|
</div>
|
|
<div class="form-group image-picker-group">
|
|
<label for="child-image">Image</label>
|
|
<ImagePicker
|
|
id="child-image"
|
|
v-model="selectedImageId"
|
|
:image-type="1"
|
|
@add-image="onAddImage"
|
|
/>
|
|
</div>
|
|
<div v-if="error" class="error">{{ error }}</div>
|
|
<div class="actions">
|
|
<button type="button" class="btn cancel" @click="onCancel" :disabled="loading">
|
|
Cancel
|
|
</button>
|
|
<button type="submit" class="btn save" :disabled="loading">
|
|
{{ isEdit ? 'Save' : 'Create' }}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ref, onMounted, computed, nextTick } from 'vue'
|
|
import { useRoute, useRouter } from 'vue-router'
|
|
import ImagePicker from '../ImagePicker.vue'
|
|
|
|
const route = useRoute()
|
|
const router = useRouter()
|
|
|
|
// Accept id as a prop for edit mode
|
|
const props = defineProps<{ id?: string }>()
|
|
|
|
const isEdit = computed(() => !!props.id)
|
|
const name = ref('')
|
|
const age = ref<number | null>(null)
|
|
const selectedImageId = ref<string | null>(null)
|
|
const localImageFile = ref<File | null>(null)
|
|
const nameInput = ref<HTMLInputElement | null>(null)
|
|
const loading = ref(false)
|
|
const error = ref<string | null>(null)
|
|
|
|
onMounted(async () => {
|
|
if (isEdit.value && props.id) {
|
|
loading.value = true
|
|
try {
|
|
const resp = await fetch(`/api/child/${props.id}`)
|
|
if (!resp.ok) throw new Error('Failed to load child')
|
|
const data = await resp.json()
|
|
name.value = data.name ?? ''
|
|
age.value = Number(data.age) ?? null
|
|
selectedImageId.value = data.image_id ?? null
|
|
} catch (e) {
|
|
error.value = 'Could not load child.'
|
|
} finally {
|
|
loading.value = false
|
|
await nextTick()
|
|
nameInput.value?.focus()
|
|
}
|
|
} else {
|
|
await nextTick()
|
|
nameInput.value?.focus()
|
|
}
|
|
})
|
|
|
|
function onAddImage({ id, file }: { id: string; file: File }) {
|
|
if (id === 'local-upload') {
|
|
localImageFile.value = file
|
|
}
|
|
}
|
|
|
|
const submit = async () => {
|
|
let imageId = selectedImageId.value
|
|
error.value = null
|
|
if (!name.value.trim()) {
|
|
error.value = 'Child name is required.'
|
|
return
|
|
}
|
|
if (age.value === null || age.value < 0) {
|
|
error.value = 'Age must be a non-negative number.'
|
|
return
|
|
}
|
|
loading.value = true
|
|
|
|
// If the selected image is a local upload, upload it first
|
|
if (imageId === 'local-upload' && localImageFile.value) {
|
|
const formData = new FormData()
|
|
formData.append('file', localImageFile.value)
|
|
formData.append('type', '1')
|
|
formData.append('permanent', 'false')
|
|
try {
|
|
const resp = await fetch('/api/image/upload', {
|
|
method: 'POST',
|
|
body: formData,
|
|
})
|
|
if (!resp.ok) throw new Error('Image upload failed')
|
|
const data = await resp.json()
|
|
imageId = data.id
|
|
} catch (err) {
|
|
alert('Failed to upload image.')
|
|
loading.value = false
|
|
return
|
|
}
|
|
}
|
|
|
|
// Now update or create the child
|
|
try {
|
|
let resp
|
|
if (isEdit.value && props.id) {
|
|
resp = await fetch(`/api/child/${props.id}/edit`, {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
name: name.value,
|
|
age: age.value,
|
|
image_id: imageId,
|
|
}),
|
|
})
|
|
} else {
|
|
resp = await fetch('/api/child/add', {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
name: name.value,
|
|
age: age.value,
|
|
image_id: imageId,
|
|
}),
|
|
})
|
|
}
|
|
if (!resp.ok) throw new Error('Failed to save child')
|
|
await router.push({ name: 'ParentChildrenListView' })
|
|
} catch (err) {
|
|
alert('Failed to save child.')
|
|
}
|
|
loading.value = false
|
|
}
|
|
|
|
function onCancel() {
|
|
router.back()
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
.child-edit-view {
|
|
max-width: 400px;
|
|
margin: 2rem auto;
|
|
background: #fff;
|
|
border-radius: 12px;
|
|
box-shadow: 0 4px 24px #667eea22;
|
|
padding: 2rem 2.2rem 1.5rem 2.2rem;
|
|
}
|
|
h2 {
|
|
margin-bottom: 1.2rem;
|
|
font-size: 1.15rem;
|
|
color: #667eea;
|
|
font-weight: 700;
|
|
text-align: center;
|
|
}
|
|
.form {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 1.1rem;
|
|
}
|
|
.form-group {
|
|
width: 100%;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: flex-start;
|
|
gap: 0.35rem;
|
|
box-sizing: border-box;
|
|
}
|
|
label {
|
|
font-weight: 600;
|
|
color: #333;
|
|
font-size: 1rem;
|
|
}
|
|
input[type='text'],
|
|
input[type='number'] {
|
|
width: 100%;
|
|
min-width: 0;
|
|
box-sizing: border-box;
|
|
padding: 0.5rem 0.7rem;
|
|
border-radius: 8px;
|
|
border: 1px solid #e6e6e6;
|
|
font-size: 1rem;
|
|
background: #fafbff;
|
|
color: #222;
|
|
transition: border 0.2s;
|
|
}
|
|
input:focus {
|
|
outline: none;
|
|
border: 1.5px solid #667eea;
|
|
}
|
|
.actions {
|
|
display: flex;
|
|
gap: 0.7rem;
|
|
justify-content: center;
|
|
margin-top: 0.5rem;
|
|
}
|
|
.btn {
|
|
padding: 0.5rem 1.1rem;
|
|
border-radius: 8px;
|
|
border: 0;
|
|
cursor: pointer;
|
|
font-weight: 700;
|
|
font-size: 1rem;
|
|
transition: background 0.18s;
|
|
}
|
|
.btn.cancel {
|
|
background: #f3f3f3;
|
|
color: #666;
|
|
}
|
|
.btn.save {
|
|
background: #667eea;
|
|
color: white;
|
|
}
|
|
.btn.save:hover {
|
|
background: #5a67d8;
|
|
}
|
|
.form-group.image-picker-group {
|
|
display: block;
|
|
text-align: left;
|
|
}
|
|
.error {
|
|
color: #e53e3e;
|
|
margin-top: 0.7rem;
|
|
text-align: center;
|
|
}
|
|
.loading-message {
|
|
text-align: center;
|
|
color: #666;
|
|
margin-bottom: 1.2rem;
|
|
}
|
|
</style>
|