round 5
This commit is contained in:
2
db/db.py
2
db/db.py
@@ -6,6 +6,7 @@ from tinydb import TinyDB
|
||||
DB_ENV = os.environ.get('DB_ENV', 'prod')
|
||||
base_dir = os.path.dirname(__file__)
|
||||
|
||||
|
||||
class LockedTable:
|
||||
"""
|
||||
Thread-safe wrapper around a TinyDB table. All callable attribute access
|
||||
@@ -99,3 +100,4 @@ if DB_ENV == 'test':
|
||||
reward_db.truncate()
|
||||
image_db.truncate()
|
||||
pending_reward_db.truncate()
|
||||
|
||||
|
||||
@@ -82,5 +82,28 @@ def populate_default_data():
|
||||
"rewards": rewards
|
||||
}
|
||||
|
||||
def initializeImages():
|
||||
"""Initialize the image database with default images if empty."""
|
||||
if len(image_db.all()) == 0:
|
||||
image_defs = [
|
||||
('computer-game', IMAGE_TYPE_ICON, '.png', True),
|
||||
('ice-cream', IMAGE_TYPE_ICON, '.png', True),
|
||||
('meal', IMAGE_TYPE_ICON, '.png', True),
|
||||
('playground', IMAGE_TYPE_ICON, '.png', True),
|
||||
('tablet', IMAGE_TYPE_ICON, '.png', True),
|
||||
('boy01', IMAGE_TYPE_PROFILE, '.png', True),
|
||||
('girl01', IMAGE_TYPE_PROFILE, '.png', True),
|
||||
('girl02', IMAGE_TYPE_PROFILE, '.png', True),
|
||||
('boy02', IMAGE_TYPE_PROFILE, '.png', True),
|
||||
('boy03', IMAGE_TYPE_PROFILE, '.png', True),
|
||||
('girl03', IMAGE_TYPE_PROFILE, '.png', True),
|
||||
('boy04', IMAGE_TYPE_PROFILE, '.png', True),
|
||||
('girl04', IMAGE_TYPE_PROFILE, '.png', True),
|
||||
]
|
||||
for _id, _type, ext, perm in image_defs:
|
||||
img = Image(type=_type, extension=ext, permanent=perm)
|
||||
img.id = _id
|
||||
image_db.insert(img.to_dict())
|
||||
|
||||
if __name__ == "__main__":
|
||||
result = populate_default_data()
|
||||
|
||||
2
main.py
2
main.py
@@ -7,6 +7,7 @@ from api.reward_api import reward_api
|
||||
from api.task_api import task_api
|
||||
from events.broadcaster import Broadcaster
|
||||
from events.sse import sse_response_for_user, send_to_user
|
||||
from db.default import initializeImages
|
||||
|
||||
app = Flask(__name__)
|
||||
#CORS(app, resources={r"/api/*": {"origins": ["http://localhost:3000", "http://localhost:5173"]}})
|
||||
@@ -48,4 +49,5 @@ start_background_threads()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
initializeImages()
|
||||
app.run(debug=False, host='0.0.0.0', port=5000, threaded=True)
|
||||
@@ -211,7 +211,6 @@ const openMenu = (childId: string | number, evt?: Event) => {
|
||||
activeMenuFor.value = childId
|
||||
}
|
||||
const closeMenu = () => {
|
||||
console.log('Closing menu')
|
||||
activeMenuFor.value = null
|
||||
}
|
||||
|
||||
|
||||
@@ -30,6 +30,11 @@ const submit = () => {
|
||||
return
|
||||
}
|
||||
|
||||
if (pin.value !== '1179') {
|
||||
error.value = 'Incorrect PIN'
|
||||
return
|
||||
}
|
||||
|
||||
// Authenticate parent and navigate
|
||||
authenticateParent()
|
||||
close()
|
||||
|
||||
@@ -246,6 +246,26 @@ async function fetchChildData(id: string | number) {
|
||||
}
|
||||
}
|
||||
|
||||
let inactivityTimer: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
function resetInactivityTimer() {
|
||||
if (inactivityTimer) clearTimeout(inactivityTimer)
|
||||
inactivityTimer = setTimeout(() => {
|
||||
router.push({ name: 'ChildrenListView' })
|
||||
}, 15000) // 15 seconds
|
||||
}
|
||||
|
||||
function setupInactivityListeners() {
|
||||
const events = ['mousemove', 'mousedown', 'keydown', 'touchstart']
|
||||
events.forEach((evt) => window.addEventListener(evt, resetInactivityTimer))
|
||||
}
|
||||
|
||||
function removeInactivityListeners() {
|
||||
const events = ['mousemove', 'mousedown', 'keydown', 'touchstart']
|
||||
events.forEach((evt) => window.removeEventListener(evt, resetInactivityTimer))
|
||||
if (inactivityTimer) clearTimeout(inactivityTimer)
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
eventBus.on('child_task_triggered', handleTaskTriggered)
|
||||
@@ -269,6 +289,8 @@ onMounted(async () => {
|
||||
})
|
||||
}
|
||||
}
|
||||
setupInactivityListeners()
|
||||
resetInactivityTimer()
|
||||
} catch (err) {
|
||||
console.error('Error in onMounted:', err)
|
||||
}
|
||||
@@ -283,6 +305,7 @@ onUnmounted(() => {
|
||||
eventBus.off('reward_modified', handleRewardModified)
|
||||
eventBus.off('child_modified', handleChildModified)
|
||||
eventBus.off('child_reward_request', handleRewardRequest)
|
||||
removeInactivityListeners()
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
@@ -53,7 +53,6 @@ function handleRewardTriggered(event: Event) {
|
||||
}
|
||||
|
||||
function handleChildTaskSet(event: Event) {
|
||||
console.log('handleChildTaskSet called')
|
||||
const payload = event.payload as ChildTasksSetEventPayload
|
||||
if (child.value && payload.child_id == child.value.id) {
|
||||
tasks.value = payload.task_ids
|
||||
@@ -235,7 +234,6 @@ const confirmTriggerTask = async () => {
|
||||
})
|
||||
if (!resp.ok) return
|
||||
const data = await resp.json()
|
||||
console.log('Trigger task response data:', child.value.id, data.id)
|
||||
if (child.value && child.value.id === data.id) child.value.points = data.points
|
||||
} catch (err) {
|
||||
console.error('Failed to trigger task:', err)
|
||||
@@ -246,7 +244,6 @@ const confirmTriggerTask = async () => {
|
||||
}
|
||||
|
||||
const triggerReward = (reward: Reward, redeemable: boolean) => {
|
||||
console.log('Handle trigger reward:', reward, redeemable)
|
||||
if (!redeemable) return
|
||||
selectedReward.value = reward
|
||||
showRewardConfirm.value = true
|
||||
|
||||
@@ -1,10 +1,24 @@
|
||||
<template>
|
||||
<div class="reward-assign-view">
|
||||
<h2>Assign Rewards</h2>
|
||||
<div class="reward-list-scroll">
|
||||
<RewardList ref="rewardListRef" :child-id="childId" :selectable="true" />
|
||||
<div class="reward-view">
|
||||
<div v-if="rewardCountRef == 0" class="no-rewards-message">
|
||||
<div>No rewards available</div>
|
||||
<div class="sub-message">
|
||||
<button class="create-btn" @click="goToCreateReward">Create</button> a reward
|
||||
</div>
|
||||
<div class="actions">
|
||||
</div>
|
||||
<div class="reward-list-scroll">
|
||||
<RewardList
|
||||
v-if="rewardCountRef != 0"
|
||||
ref="rewardListRef"
|
||||
:child-id="childId"
|
||||
:selectable="true"
|
||||
@loading-complete="(count) => (rewardCountRef = count)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="actions" v-if="rewardCountRef != 0">
|
||||
<button class="btn cancel" @click="onCancel">Cancel</button>
|
||||
<button class="btn submit" @click="onSubmit">Submit</button>
|
||||
</div>
|
||||
@@ -21,6 +35,11 @@ const router = useRouter()
|
||||
const childId = route.params.id
|
||||
|
||||
const rewardListRef = ref()
|
||||
const rewardCountRef = ref(-1)
|
||||
|
||||
function goToCreateReward() {
|
||||
router.push({ name: 'CreateReward' })
|
||||
}
|
||||
|
||||
async function onSubmit() {
|
||||
const selectedIds = rewardListRef.value?.selectedRewards ?? []
|
||||
@@ -91,4 +110,47 @@ h2 {
|
||||
.btn.submit:hover {
|
||||
background: #5a67d8;
|
||||
}
|
||||
.reward-view {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
flex: 1 1 auto;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 0;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.no-rewards-message {
|
||||
margin: 2rem 0;
|
||||
font-size: 1.15rem;
|
||||
font-weight: 600;
|
||||
text-align: center;
|
||||
color: #fdfdfd;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.sub-message {
|
||||
margin-top: 0.3rem;
|
||||
font-size: 1rem;
|
||||
font-weight: 400;
|
||||
color: #b5ccff;
|
||||
}
|
||||
.create-btn {
|
||||
background: #fff;
|
||||
color: #2563eb;
|
||||
border: 2px solid #2563eb;
|
||||
border-radius: 6px;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
padding: 0.2rem 0.5rem;
|
||||
margin-right: 0.1rem;
|
||||
cursor: pointer;
|
||||
transition:
|
||||
background 0.18s,
|
||||
color 0.18s;
|
||||
}
|
||||
.create-btn:hover {
|
||||
background: #2563eb;
|
||||
color: #fff;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,15 +1,25 @@
|
||||
<template>
|
||||
<div class="task-assign-view">
|
||||
<h2>Assign Tasks</h2>
|
||||
<div class="task-view">
|
||||
<div v-if="taskCountRef == 0" class="no-tasks-message">
|
||||
<div>No tasks available</div>
|
||||
<div class="sub-message">
|
||||
<button class="create-btn" @click="goToCreateTask">Create</button> a task
|
||||
</div>
|
||||
</div>
|
||||
<div class="task-list-scroll">
|
||||
<TaskList
|
||||
v-if="taskCountRef != 0"
|
||||
ref="taskListRef"
|
||||
:child-id="childId"
|
||||
:selectable="true"
|
||||
:type-filter="typeFilter"
|
||||
@loading-complete="(count) => (taskCountRef = count)"
|
||||
/>
|
||||
</div>
|
||||
<div class="actions">
|
||||
</div>
|
||||
<div class="actions" v-if="taskCountRef > 0">
|
||||
<button class="btn cancel" @click="onCancel">Cancel</button>
|
||||
<button class="btn submit" @click="onSubmit">Submit</button>
|
||||
</div>
|
||||
@@ -26,6 +36,7 @@ const router = useRouter()
|
||||
const childId = route.params.id
|
||||
|
||||
const taskListRef = ref()
|
||||
const taskCountRef = ref(-1)
|
||||
|
||||
const typeFilter = computed(() => {
|
||||
if (route.params.type === 'good') return 'good'
|
||||
@@ -33,6 +44,10 @@ const typeFilter = computed(() => {
|
||||
return 'all'
|
||||
})
|
||||
|
||||
function goToCreateTask() {
|
||||
router.push({ name: 'CreateTask' })
|
||||
}
|
||||
|
||||
async function onSubmit() {
|
||||
const selectedIds = taskListRef.value?.selectedTasks ?? []
|
||||
try {
|
||||
@@ -102,4 +117,48 @@ h2 {
|
||||
.btn.submit:hover {
|
||||
background: #5a67d8;
|
||||
}
|
||||
|
||||
.task-view {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
flex: 1 1 auto;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 0;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.no-tasks-message {
|
||||
margin: 2rem 0;
|
||||
font-size: 1.15rem;
|
||||
font-weight: 600;
|
||||
text-align: center;
|
||||
color: #fdfdfd;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.sub-message {
|
||||
margin-top: 0.3rem;
|
||||
font-size: 1rem;
|
||||
font-weight: 400;
|
||||
color: #b5ccff;
|
||||
}
|
||||
.create-btn {
|
||||
background: #fff;
|
||||
color: #2563eb;
|
||||
border: 2px solid #2563eb;
|
||||
border-radius: 6px;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
padding: 0.2rem 0.5rem;
|
||||
margin-right: 0.1rem;
|
||||
cursor: pointer;
|
||||
transition:
|
||||
background 0.18s,
|
||||
color 0.18s;
|
||||
}
|
||||
.create-btn:hover {
|
||||
background: #2563eb;
|
||||
color: #fff;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -105,6 +105,7 @@ const fetchRewards = async () => {
|
||||
rewards.value = []
|
||||
if (props.selectable) selectedRewards.value = []
|
||||
} finally {
|
||||
emit('loading-complete', rewards.value.length)
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="task-view">
|
||||
<div v-if="rewardCountRef === 0" class="no-rewards-message">
|
||||
<div v-if="rewardCountRef == 0" class="no-rewards-message">
|
||||
<div>No Rewards</div>
|
||||
<div class="sub-message">
|
||||
<button class="create-btn" @click="createReward">Create</button> a reward
|
||||
@@ -47,7 +47,7 @@ const $router = useRouter()
|
||||
const showConfirm = ref(false)
|
||||
const rewardToDelete = ref<string | null>(null)
|
||||
const rewardListRef = ref()
|
||||
const rewardCountRef = ref<number>(0)
|
||||
const rewardCountRef = ref<number>(-1)
|
||||
|
||||
function confirmDeleteReward(rewardId: string) {
|
||||
rewardToDelete.value = rewardId
|
||||
@@ -63,7 +63,6 @@ const deleteReward = async () => {
|
||||
if (!resp.ok) throw new Error(`HTTP ${resp.status}`)
|
||||
// Refresh the reward list after successful delete
|
||||
rewardListRef.value?.refresh()
|
||||
console.log(`Reward ${rewardToDelete.value} deleted successfully`)
|
||||
} catch (err) {
|
||||
console.error('Failed to delete reward:', err)
|
||||
} finally {
|
||||
|
||||
@@ -79,6 +79,7 @@ const fetchTasks = async () => {
|
||||
if (props.selectable) selectedTasks.value = []
|
||||
} finally {
|
||||
loading.value = false
|
||||
emit('loading-complete', filteredTasks.value.length)
|
||||
}
|
||||
} else {
|
||||
url = '/api/task/list'
|
||||
@@ -105,7 +106,7 @@ const fetchTasks = async () => {
|
||||
tasks.value = []
|
||||
if (props.selectable) selectedTasks.value = []
|
||||
} finally {
|
||||
emit('loading-complete', tasks.value.length)
|
||||
emit('loading-complete', filteredTasks.value.length)
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ const $router = useRouter()
|
||||
const showConfirm = ref(false)
|
||||
const taskToDelete = ref<string | null>(null)
|
||||
const taskListRef = ref()
|
||||
const taskCountRef = ref<number>(0)
|
||||
const taskCountRef = ref<number>(-1)
|
||||
|
||||
function confirmDeleteTask(taskId: string) {
|
||||
taskToDelete.value = taskId
|
||||
@@ -62,7 +62,6 @@ const deleteTask = async () => {
|
||||
if (!resp.ok) throw new Error(`HTTP ${resp.status}`)
|
||||
// Refresh the task list after successful delete
|
||||
taskListRef.value?.refresh()
|
||||
console.log(`Task ${taskToDelete.value} deleted successfully`)
|
||||
} catch (err) {
|
||||
console.error('Failed to delete task:', err)
|
||||
} finally {
|
||||
|
||||
@@ -40,6 +40,7 @@ const showBack = computed(
|
||||
'ChildEditView',
|
||||
'CreateChild',
|
||||
'TaskAssignView',
|
||||
'RewardAssignView',
|
||||
].includes(String(route.name)),
|
||||
}"
|
||||
@click="router.push({ name: 'ParentChildrenListView' })"
|
||||
|
||||
Reference in New Issue
Block a user