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')
|
DB_ENV = os.environ.get('DB_ENV', 'prod')
|
||||||
base_dir = os.path.dirname(__file__)
|
base_dir = os.path.dirname(__file__)
|
||||||
|
|
||||||
|
|
||||||
class LockedTable:
|
class LockedTable:
|
||||||
"""
|
"""
|
||||||
Thread-safe wrapper around a TinyDB table. All callable attribute access
|
Thread-safe wrapper around a TinyDB table. All callable attribute access
|
||||||
@@ -99,3 +100,4 @@ if DB_ENV == 'test':
|
|||||||
reward_db.truncate()
|
reward_db.truncate()
|
||||||
image_db.truncate()
|
image_db.truncate()
|
||||||
pending_reward_db.truncate()
|
pending_reward_db.truncate()
|
||||||
|
|
||||||
|
|||||||
@@ -82,5 +82,28 @@ def populate_default_data():
|
|||||||
"rewards": rewards
|
"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__":
|
if __name__ == "__main__":
|
||||||
result = populate_default_data()
|
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 api.task_api import task_api
|
||||||
from events.broadcaster import Broadcaster
|
from events.broadcaster import Broadcaster
|
||||||
from events.sse import sse_response_for_user, send_to_user
|
from events.sse import sse_response_for_user, send_to_user
|
||||||
|
from db.default import initializeImages
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
#CORS(app, resources={r"/api/*": {"origins": ["http://localhost:3000", "http://localhost:5173"]}})
|
#CORS(app, resources={r"/api/*": {"origins": ["http://localhost:3000", "http://localhost:5173"]}})
|
||||||
@@ -48,4 +49,5 @@ start_background_threads()
|
|||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
initializeImages()
|
||||||
app.run(debug=False, host='0.0.0.0', port=5000, threaded=True)
|
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
|
activeMenuFor.value = childId
|
||||||
}
|
}
|
||||||
const closeMenu = () => {
|
const closeMenu = () => {
|
||||||
console.log('Closing menu')
|
|
||||||
activeMenuFor.value = null
|
activeMenuFor.value = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -30,6 +30,11 @@ const submit = () => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (pin.value !== '1179') {
|
||||||
|
error.value = 'Incorrect PIN'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Authenticate parent and navigate
|
// Authenticate parent and navigate
|
||||||
authenticateParent()
|
authenticateParent()
|
||||||
close()
|
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 () => {
|
onMounted(async () => {
|
||||||
try {
|
try {
|
||||||
eventBus.on('child_task_triggered', handleTaskTriggered)
|
eventBus.on('child_task_triggered', handleTaskTriggered)
|
||||||
@@ -269,6 +289,8 @@ onMounted(async () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
setupInactivityListeners()
|
||||||
|
resetInactivityTimer()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error in onMounted:', err)
|
console.error('Error in onMounted:', err)
|
||||||
}
|
}
|
||||||
@@ -283,6 +305,7 @@ onUnmounted(() => {
|
|||||||
eventBus.off('reward_modified', handleRewardModified)
|
eventBus.off('reward_modified', handleRewardModified)
|
||||||
eventBus.off('child_modified', handleChildModified)
|
eventBus.off('child_modified', handleChildModified)
|
||||||
eventBus.off('child_reward_request', handleRewardRequest)
|
eventBus.off('child_reward_request', handleRewardRequest)
|
||||||
|
removeInactivityListeners()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -53,7 +53,6 @@ function handleRewardTriggered(event: Event) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleChildTaskSet(event: Event) {
|
function handleChildTaskSet(event: Event) {
|
||||||
console.log('handleChildTaskSet called')
|
|
||||||
const payload = event.payload as ChildTasksSetEventPayload
|
const payload = event.payload as ChildTasksSetEventPayload
|
||||||
if (child.value && payload.child_id == child.value.id) {
|
if (child.value && payload.child_id == child.value.id) {
|
||||||
tasks.value = payload.task_ids
|
tasks.value = payload.task_ids
|
||||||
@@ -235,7 +234,6 @@ const confirmTriggerTask = async () => {
|
|||||||
})
|
})
|
||||||
if (!resp.ok) return
|
if (!resp.ok) return
|
||||||
const data = await resp.json()
|
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
|
if (child.value && child.value.id === data.id) child.value.points = data.points
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to trigger task:', err)
|
console.error('Failed to trigger task:', err)
|
||||||
@@ -246,7 +244,6 @@ const confirmTriggerTask = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const triggerReward = (reward: Reward, redeemable: boolean) => {
|
const triggerReward = (reward: Reward, redeemable: boolean) => {
|
||||||
console.log('Handle trigger reward:', reward, redeemable)
|
|
||||||
if (!redeemable) return
|
if (!redeemable) return
|
||||||
selectedReward.value = reward
|
selectedReward.value = reward
|
||||||
showRewardConfirm.value = true
|
showRewardConfirm.value = true
|
||||||
|
|||||||
@@ -1,10 +1,24 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="reward-assign-view">
|
<div class="reward-assign-view">
|
||||||
<h2>Assign Rewards</h2>
|
<h2>Assign Rewards</h2>
|
||||||
<div class="reward-list-scroll">
|
<div class="reward-view">
|
||||||
<RewardList ref="rewardListRef" :child-id="childId" :selectable="true" />
|
<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>
|
||||||
|
<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>
|
||||||
<div class="actions">
|
<div class="actions" v-if="rewardCountRef != 0">
|
||||||
<button class="btn cancel" @click="onCancel">Cancel</button>
|
<button class="btn cancel" @click="onCancel">Cancel</button>
|
||||||
<button class="btn submit" @click="onSubmit">Submit</button>
|
<button class="btn submit" @click="onSubmit">Submit</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -21,6 +35,11 @@ const router = useRouter()
|
|||||||
const childId = route.params.id
|
const childId = route.params.id
|
||||||
|
|
||||||
const rewardListRef = ref()
|
const rewardListRef = ref()
|
||||||
|
const rewardCountRef = ref(-1)
|
||||||
|
|
||||||
|
function goToCreateReward() {
|
||||||
|
router.push({ name: 'CreateReward' })
|
||||||
|
}
|
||||||
|
|
||||||
async function onSubmit() {
|
async function onSubmit() {
|
||||||
const selectedIds = rewardListRef.value?.selectedRewards ?? []
|
const selectedIds = rewardListRef.value?.selectedRewards ?? []
|
||||||
@@ -91,4 +110,47 @@ h2 {
|
|||||||
.btn.submit:hover {
|
.btn.submit:hover {
|
||||||
background: #5a67d8;
|
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>
|
</style>
|
||||||
|
|||||||
@@ -1,15 +1,25 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="task-assign-view">
|
<div class="task-assign-view">
|
||||||
<h2>Assign Tasks</h2>
|
<h2>Assign Tasks</h2>
|
||||||
<div class="task-list-scroll">
|
<div class="task-view">
|
||||||
<TaskList
|
<div v-if="taskCountRef == 0" class="no-tasks-message">
|
||||||
ref="taskListRef"
|
<div>No tasks available</div>
|
||||||
:child-id="childId"
|
<div class="sub-message">
|
||||||
:selectable="true"
|
<button class="create-btn" @click="goToCreateTask">Create</button> a task
|
||||||
:type-filter="typeFilter"
|
</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>
|
</div>
|
||||||
<div class="actions">
|
<div class="actions" v-if="taskCountRef > 0">
|
||||||
<button class="btn cancel" @click="onCancel">Cancel</button>
|
<button class="btn cancel" @click="onCancel">Cancel</button>
|
||||||
<button class="btn submit" @click="onSubmit">Submit</button>
|
<button class="btn submit" @click="onSubmit">Submit</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -26,6 +36,7 @@ const router = useRouter()
|
|||||||
const childId = route.params.id
|
const childId = route.params.id
|
||||||
|
|
||||||
const taskListRef = ref()
|
const taskListRef = ref()
|
||||||
|
const taskCountRef = ref(-1)
|
||||||
|
|
||||||
const typeFilter = computed(() => {
|
const typeFilter = computed(() => {
|
||||||
if (route.params.type === 'good') return 'good'
|
if (route.params.type === 'good') return 'good'
|
||||||
@@ -33,6 +44,10 @@ const typeFilter = computed(() => {
|
|||||||
return 'all'
|
return 'all'
|
||||||
})
|
})
|
||||||
|
|
||||||
|
function goToCreateTask() {
|
||||||
|
router.push({ name: 'CreateTask' })
|
||||||
|
}
|
||||||
|
|
||||||
async function onSubmit() {
|
async function onSubmit() {
|
||||||
const selectedIds = taskListRef.value?.selectedTasks ?? []
|
const selectedIds = taskListRef.value?.selectedTasks ?? []
|
||||||
try {
|
try {
|
||||||
@@ -102,4 +117,48 @@ h2 {
|
|||||||
.btn.submit:hover {
|
.btn.submit:hover {
|
||||||
background: #5a67d8;
|
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>
|
</style>
|
||||||
|
|||||||
@@ -105,6 +105,7 @@ const fetchRewards = async () => {
|
|||||||
rewards.value = []
|
rewards.value = []
|
||||||
if (props.selectable) selectedRewards.value = []
|
if (props.selectable) selectedRewards.value = []
|
||||||
} finally {
|
} finally {
|
||||||
|
emit('loading-complete', rewards.value.length)
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="task-view">
|
<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>No Rewards</div>
|
||||||
<div class="sub-message">
|
<div class="sub-message">
|
||||||
<button class="create-btn" @click="createReward">Create</button> a reward
|
<button class="create-btn" @click="createReward">Create</button> a reward
|
||||||
@@ -47,7 +47,7 @@ const $router = useRouter()
|
|||||||
const showConfirm = ref(false)
|
const showConfirm = ref(false)
|
||||||
const rewardToDelete = ref<string | null>(null)
|
const rewardToDelete = ref<string | null>(null)
|
||||||
const rewardListRef = ref()
|
const rewardListRef = ref()
|
||||||
const rewardCountRef = ref<number>(0)
|
const rewardCountRef = ref<number>(-1)
|
||||||
|
|
||||||
function confirmDeleteReward(rewardId: string) {
|
function confirmDeleteReward(rewardId: string) {
|
||||||
rewardToDelete.value = rewardId
|
rewardToDelete.value = rewardId
|
||||||
@@ -63,7 +63,6 @@ const deleteReward = async () => {
|
|||||||
if (!resp.ok) throw new Error(`HTTP ${resp.status}`)
|
if (!resp.ok) throw new Error(`HTTP ${resp.status}`)
|
||||||
// Refresh the reward list after successful delete
|
// Refresh the reward list after successful delete
|
||||||
rewardListRef.value?.refresh()
|
rewardListRef.value?.refresh()
|
||||||
console.log(`Reward ${rewardToDelete.value} deleted successfully`)
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to delete reward:', err)
|
console.error('Failed to delete reward:', err)
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -79,6 +79,7 @@ const fetchTasks = async () => {
|
|||||||
if (props.selectable) selectedTasks.value = []
|
if (props.selectable) selectedTasks.value = []
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
|
emit('loading-complete', filteredTasks.value.length)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
url = '/api/task/list'
|
url = '/api/task/list'
|
||||||
@@ -105,7 +106,7 @@ const fetchTasks = async () => {
|
|||||||
tasks.value = []
|
tasks.value = []
|
||||||
if (props.selectable) selectedTasks.value = []
|
if (props.selectable) selectedTasks.value = []
|
||||||
} finally {
|
} finally {
|
||||||
emit('loading-complete', tasks.value.length)
|
emit('loading-complete', filteredTasks.value.length)
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ const $router = useRouter()
|
|||||||
const showConfirm = ref(false)
|
const showConfirm = ref(false)
|
||||||
const taskToDelete = ref<string | null>(null)
|
const taskToDelete = ref<string | null>(null)
|
||||||
const taskListRef = ref()
|
const taskListRef = ref()
|
||||||
const taskCountRef = ref<number>(0)
|
const taskCountRef = ref<number>(-1)
|
||||||
|
|
||||||
function confirmDeleteTask(taskId: string) {
|
function confirmDeleteTask(taskId: string) {
|
||||||
taskToDelete.value = taskId
|
taskToDelete.value = taskId
|
||||||
@@ -62,7 +62,6 @@ const deleteTask = async () => {
|
|||||||
if (!resp.ok) throw new Error(`HTTP ${resp.status}`)
|
if (!resp.ok) throw new Error(`HTTP ${resp.status}`)
|
||||||
// Refresh the task list after successful delete
|
// Refresh the task list after successful delete
|
||||||
taskListRef.value?.refresh()
|
taskListRef.value?.refresh()
|
||||||
console.log(`Task ${taskToDelete.value} deleted successfully`)
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to delete task:', err)
|
console.error('Failed to delete task:', err)
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ const showBack = computed(
|
|||||||
'ChildEditView',
|
'ChildEditView',
|
||||||
'CreateChild',
|
'CreateChild',
|
||||||
'TaskAssignView',
|
'TaskAssignView',
|
||||||
|
'RewardAssignView',
|
||||||
].includes(String(route.name)),
|
].includes(String(route.name)),
|
||||||
}"
|
}"
|
||||||
@click="router.push({ name: 'ParentChildrenListView' })"
|
@click="router.push({ name: 'ParentChildrenListView' })"
|
||||||
|
|||||||
Reference in New Issue
Block a user