Initial project scaffold
Full-stack homeschool web app with FastAPI backend, Vue 3 frontend, MySQL database, and Docker Compose orchestration. Includes JWT auth, WebSocket real-time TV dashboard, schedule builder, activity logging, and multi-child support. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
210
frontend/src/views/LogView.vue
Normal file
210
frontend/src/views/LogView.vue
Normal file
@@ -0,0 +1,210 @@
|
||||
<template>
|
||||
<div class="page">
|
||||
<NavBar />
|
||||
<main class="container">
|
||||
<div class="page-header">
|
||||
<h1>Activity Log</h1>
|
||||
<button class="btn-primary" @click="showForm = !showForm">+ Log Activity</button>
|
||||
</div>
|
||||
|
||||
<ChildSelector style="margin-bottom: 1.5rem" />
|
||||
|
||||
<!-- Add form -->
|
||||
<div class="card" v-if="showForm">
|
||||
<h3>Log an Activity</h3>
|
||||
<form @submit.prevent="createLog">
|
||||
<div class="field-row">
|
||||
<div class="field">
|
||||
<label>Child</label>
|
||||
<select v-model="newLog.child_id" required>
|
||||
<option v-for="c in childrenStore.children" :key="c.id" :value="c.id">{{ c.name }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Date</label>
|
||||
<input v-model="newLog.log_date" type="date" required />
|
||||
</div>
|
||||
</div>
|
||||
<div class="field-row">
|
||||
<div class="field">
|
||||
<label>Subject (optional)</label>
|
||||
<select v-model="newLog.subject_id">
|
||||
<option :value="null">None</option>
|
||||
<option v-for="s in subjects" :key="s.id" :value="s.id">{{ s.icon }} {{ s.name }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Duration (minutes)</label>
|
||||
<input v-model.number="newLog.duration_minutes" type="number" min="0" placeholder="e.g. 30" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Notes</label>
|
||||
<textarea v-model="newLog.notes" placeholder="What did they do?" rows="3"></textarea>
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<button type="button" @click="showForm = false">Cancel</button>
|
||||
<button type="submit" class="btn-primary">Save Log</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Filter bar -->
|
||||
<div class="filter-bar">
|
||||
<input v-model="filterDate" type="date" placeholder="Filter by date" />
|
||||
<button v-if="filterDate" class="btn-sm" @click="filterDate = ''">Clear</button>
|
||||
</div>
|
||||
|
||||
<!-- Logs -->
|
||||
<div class="log-list">
|
||||
<div v-for="log in filteredLogs" :key="log.id" class="log-row">
|
||||
<div class="log-date">{{ log.log_date }}</div>
|
||||
<div class="log-content">
|
||||
<div class="log-subject" v-if="log.subject_id">
|
||||
{{ subjectDisplay(log.subject_id) }}
|
||||
</div>
|
||||
<div class="log-notes" v-if="log.notes">{{ log.notes }}</div>
|
||||
<div class="log-meta" v-if="log.duration_minutes">
|
||||
{{ log.duration_minutes }} min
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn-sm btn-danger" @click="deleteLog(log.id)">✕</button>
|
||||
</div>
|
||||
<div v-if="filteredLogs.length === 0" class="empty-state">
|
||||
No activity logs yet.
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useChildrenStore } from '@/stores/children'
|
||||
import api from '@/composables/useApi'
|
||||
import NavBar from '@/components/NavBar.vue'
|
||||
import ChildSelector from '@/components/ChildSelector.vue'
|
||||
|
||||
const childrenStore = useChildrenStore()
|
||||
const logs = ref([])
|
||||
const subjects = ref([])
|
||||
const showForm = ref(false)
|
||||
const filterDate = ref('')
|
||||
|
||||
const today = new Date().toISOString().split('T')[0]
|
||||
const newLog = ref({ child_id: null, subject_id: null, log_date: today, notes: '', duration_minutes: null })
|
||||
|
||||
const filteredLogs = computed(() => {
|
||||
if (!filterDate.value) return logs.value
|
||||
return logs.value.filter((l) => l.log_date === filterDate.value)
|
||||
})
|
||||
|
||||
function subjectDisplay(id) {
|
||||
const s = subjects.value.find((s) => s.id === id)
|
||||
return s ? `${s.icon} ${s.name}` : ''
|
||||
}
|
||||
|
||||
async function loadLogs() {
|
||||
const res = await api.get('/api/logs')
|
||||
logs.value = res.data
|
||||
}
|
||||
|
||||
async function createLog() {
|
||||
await api.post('/api/logs', newLog.value)
|
||||
newLog.value = { child_id: newLog.value.child_id, subject_id: null, log_date: today, notes: '', duration_minutes: null }
|
||||
showForm.value = false
|
||||
await loadLogs()
|
||||
}
|
||||
|
||||
async function deleteLog(id) {
|
||||
if (confirm('Delete this log entry?')) {
|
||||
await api.delete(`/api/logs/${id}`)
|
||||
logs.value = logs.value.filter((l) => l.id !== id)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await childrenStore.fetchChildren()
|
||||
if (childrenStore.activeChild) newLog.value.child_id = childrenStore.activeChild.id
|
||||
const [sRes] = await Promise.all([api.get('/api/subjects'), loadLogs()])
|
||||
subjects.value = sRes.data
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page { min-height: 100vh; background: #0f172a; }
|
||||
.container { max-width: 800px; margin: 0 auto; padding: 2rem; }
|
||||
.page-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 2rem; }
|
||||
h1 { font-size: 1.75rem; font-weight: 700; }
|
||||
|
||||
.card { background: #1e293b; border-radius: 1rem; padding: 1.5rem; margin-bottom: 1.5rem; }
|
||||
.card h3 { margin-bottom: 1.25rem; }
|
||||
|
||||
.field-row { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; }
|
||||
.field { margin-bottom: 1rem; }
|
||||
.field label { display: block; font-size: 0.85rem; color: #94a3b8; margin-bottom: 0.4rem; }
|
||||
.field input, .field select, .field textarea {
|
||||
width: 100%;
|
||||
padding: 0.65rem 0.9rem;
|
||||
background: #0f172a;
|
||||
border: 1px solid #334155;
|
||||
border-radius: 0.5rem;
|
||||
color: #f1f5f9;
|
||||
font-size: 0.9rem;
|
||||
resize: vertical;
|
||||
}
|
||||
.form-actions { display: flex; gap: 0.75rem; justify-content: flex-end; margin-top: 0.5rem; }
|
||||
|
||||
.filter-bar { display: flex; align-items: center; gap: 0.75rem; margin-bottom: 1.5rem; }
|
||||
.filter-bar input {
|
||||
padding: 0.5rem 0.75rem;
|
||||
background: #1e293b;
|
||||
border: 1px solid #334155;
|
||||
border-radius: 0.5rem;
|
||||
color: #f1f5f9;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.log-list { display: flex; flex-direction: column; gap: 0.5rem; }
|
||||
.log-row {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 1rem;
|
||||
background: #1e293b;
|
||||
border-radius: 0.75rem;
|
||||
padding: 1rem 1.25rem;
|
||||
}
|
||||
.log-date { font-size: 0.8rem; color: #64748b; width: 90px; flex-shrink: 0; padding-top: 0.1rem; }
|
||||
.log-content { flex: 1; }
|
||||
.log-subject { font-size: 0.85rem; color: #818cf8; margin-bottom: 0.2rem; }
|
||||
.log-notes { font-size: 0.9rem; }
|
||||
.log-meta { font-size: 0.8rem; color: #64748b; margin-top: 0.25rem; }
|
||||
|
||||
.empty-state { text-align: center; padding: 4rem; color: #64748b; }
|
||||
|
||||
.btn-primary {
|
||||
padding: 0.65rem 1.25rem;
|
||||
background: #4f46e5;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 0.5rem;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
.btn-primary:hover { background: #4338ca; }
|
||||
|
||||
.btn-sm {
|
||||
padding: 0.35rem 0.75rem;
|
||||
border: 1px solid #334155;
|
||||
background: transparent;
|
||||
color: #94a3b8;
|
||||
border-radius: 0.5rem;
|
||||
cursor: pointer;
|
||||
font-size: 0.8rem;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.btn-sm:hover { background: #334155; }
|
||||
.btn-sm.btn-danger { border-color: #7f1d1d; color: #fca5a5; }
|
||||
.btn-sm.btn-danger:hover { background: #7f1d1d; }
|
||||
</style>
|
||||
Reference in New Issue
Block a user