feat: enhance action differentiators, deep video extraction, and full action type parsing
This commit is contained in:
+252
-33
@@ -516,50 +516,257 @@ async function fetchAllActivities(studentId) {
|
|||||||
function matchesSelectedCategories(act) {
|
function matchesSelectedCategories(act) {
|
||||||
const type = (act.activity_type || act.action_type || act.type || '').toLowerCase();
|
const type = (act.activity_type || act.action_type || act.type || '').toLowerCase();
|
||||||
|
|
||||||
if (act.media || act.image_url || act.video_url || type.includes('photo') || type.includes('video')) {
|
if (type.includes('photo') || type.includes('video') || act.media || act.image_url || act.video_url || act.video_stream_url) {
|
||||||
if (el.catPhotos.checked) return true;
|
if (el.catPhotos.checked) return true;
|
||||||
}
|
}
|
||||||
if (type.includes('food') || type.includes('meal') || type.includes('nap') || type.includes('potty') || type.includes('diaper') || type.includes('med')) {
|
if (type.includes('food') || type.includes('meal') || type.includes('nap') || type.includes('potty') || type.includes('diaper') || type.includes('med') || type.includes('bottle')) {
|
||||||
if (el.catCare.checked) return true;
|
if (el.catCare.checked) return true;
|
||||||
}
|
}
|
||||||
if (type.includes('milestone') || type.includes('learn') || type.includes('observation') || type.includes('kudos')) {
|
if (type.includes('milestone') || type.includes('learn') || type.includes('observation') || type.includes('kudos') || type.includes('skill')) {
|
||||||
if (el.catMilestones.checked) return true;
|
if (el.catMilestones.checked) return true;
|
||||||
}
|
}
|
||||||
if (type.includes('checkin') || type.includes('checkout') || type.includes('attendance') || type.includes('health') || type.includes('incident')) {
|
if (type.includes('checkin') || type.includes('checkout') || type.includes('attendance') || type.includes('health') || type.includes('incident') || type.includes('screening') || type.includes('name_to_face')) {
|
||||||
if (el.catAttendance.checked) return true;
|
if (el.catAttendance.checked) return true;
|
||||||
}
|
}
|
||||||
if (type.includes('note') || type.includes('message') || type.includes('announcement')) {
|
if (type.includes('note') || type.includes('message') || type.includes('announcement') || type.includes('reminder') || type.includes('custom')) {
|
||||||
if (el.catMessages.checked) return true;
|
if (el.catMessages.checked) return true;
|
||||||
}
|
}
|
||||||
|
// Default: include all other action types
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Deep recursive media extractor to find high-res photos and videos anywhere in object/array
|
||||||
|
function extractMediaUrls(act) {
|
||||||
|
let imageUrl = null;
|
||||||
|
let videoUrl = null;
|
||||||
|
let thumbUrl = null;
|
||||||
|
|
||||||
|
const isVideoStr = (url) => {
|
||||||
|
if (!url || typeof url !== 'string') return false;
|
||||||
|
const lower = url.toLowerCase();
|
||||||
|
return lower.includes('.mp4') || lower.includes('.mov') || lower.includes('.webm') || lower.includes('/videos/') || lower.includes('video_stream') || lower.includes('transcoded_video') || lower.includes('media_videos');
|
||||||
|
};
|
||||||
|
|
||||||
|
const isImageStr = (url) => {
|
||||||
|
if (!url || typeof url !== 'string') return false;
|
||||||
|
const lower = url.toLowerCase();
|
||||||
|
return lower.includes('.jpg') || lower.includes('.jpeg') || lower.includes('.png') || lower.includes('.webp') || lower.includes('.gif') || lower.includes('/images/') || lower.includes('/photos/') || lower.includes('media_images');
|
||||||
|
};
|
||||||
|
|
||||||
|
function search(obj, depth = 0) {
|
||||||
|
if (!obj || depth > 5) return;
|
||||||
|
|
||||||
|
if (typeof obj === 'string') {
|
||||||
|
if (obj.startsWith('http://') || obj.startsWith('https://')) {
|
||||||
|
if (isVideoStr(obj) && !videoUrl) videoUrl = obj;
|
||||||
|
else if (isImageStr(obj) && !imageUrl) imageUrl = obj;
|
||||||
|
else if (!imageUrl && !videoUrl) {
|
||||||
|
if (act.activity_type === 'ac_video' || act.type === 'video') videoUrl = obj;
|
||||||
|
else imageUrl = obj;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(obj)) {
|
||||||
|
for (const item of obj) search(item, depth + 1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof obj === 'object') {
|
||||||
|
// Check video keys
|
||||||
|
const possibleVideos = [
|
||||||
|
obj.video_url, obj.video_stream_url, obj.video_hd_url, obj.video_transcode_url,
|
||||||
|
obj.transcoded_video_url, obj.original_video_url, obj.stream_url, obj.mp4_url,
|
||||||
|
obj.video, obj.source_url, obj.download_url
|
||||||
|
];
|
||||||
|
for (const v of possibleVideos) {
|
||||||
|
if (typeof v === 'string' && v.startsWith('http')) {
|
||||||
|
if (!videoUrl) videoUrl = v;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check image keys
|
||||||
|
const possibleImages = [
|
||||||
|
obj.image_url, obj.photo_url, obj.full_url, obj.picture_url,
|
||||||
|
obj.original_url, obj.cover_url, obj.large_url
|
||||||
|
];
|
||||||
|
for (const img of possibleImages) {
|
||||||
|
if (typeof img === 'string' && img.startsWith('http')) {
|
||||||
|
if (!imageUrl && img !== videoUrl) imageUrl = img;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check generic url
|
||||||
|
if (typeof obj.url === 'string' && obj.url.startsWith('http')) {
|
||||||
|
if (isVideoStr(obj.url) || act.activity_type === 'ac_video' || act.type === 'video') {
|
||||||
|
if (!videoUrl) videoUrl = obj.url;
|
||||||
|
} else if (!imageUrl) {
|
||||||
|
imageUrl = obj.url;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check thumbnail
|
||||||
|
const possibleThumbs = [obj.thumb_url, obj.thumbnail_url, obj.thumbnail, obj.preview_url, obj.preview_image_url];
|
||||||
|
for (const th of possibleThumbs) {
|
||||||
|
if (typeof th === 'string' && th.startsWith('http') && !thumbUrl) {
|
||||||
|
thumbUrl = th;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recurse into nested common containers
|
||||||
|
if (obj.media) search(obj.media, depth + 1);
|
||||||
|
if (obj.video_data) search(obj.video_data, depth + 1);
|
||||||
|
if (obj.attachments) search(obj.attachments, depth + 1);
|
||||||
|
if (obj.items) search(obj.items, depth + 1);
|
||||||
|
if (obj.photos) search(obj.photos, depth + 1);
|
||||||
|
if (obj.videos) search(obj.videos, depth + 1);
|
||||||
|
if (obj.assets) search(obj.assets, depth + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scan root act and common child objects
|
||||||
|
search(act);
|
||||||
|
|
||||||
|
// If it's an ac_video activity and imageUrl was found but points to video, fix assignment
|
||||||
|
if ((act.activity_type === 'ac_video' || act.type === 'video') && !videoUrl && imageUrl) {
|
||||||
|
videoUrl = imageUrl;
|
||||||
|
imageUrl = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { imageUrl, videoUrl, thumbUrl };
|
||||||
|
}
|
||||||
|
|
||||||
function normalizeActivity(act) {
|
function normalizeActivity(act) {
|
||||||
const mediaObj = act.media || {};
|
const { imageUrl, videoUrl, thumbUrl } = extractMediaUrls(act);
|
||||||
const highResImage = mediaObj.image_url || act.image_url || mediaObj.url || act.url;
|
const actType = (act.activity_type || act.action_type || act.type || 'activity').toLowerCase();
|
||||||
const highResVideo = mediaObj.video_url || act.video_url;
|
const rawSubtype = (act.action_type || act.event_type || act.subtype || act.sub_type || act.action || act.state || act.status || '').toLowerCase();
|
||||||
|
|
||||||
|
// Extract staff / actor name
|
||||||
|
const teacherName = act.actor_name || act.created_by?.name || act.teacher_name || act.staff_name || (act.actor ? `${act.actor.first_name || ''} ${act.actor.last_name || ''}`.trim() : '') || '';
|
||||||
|
|
||||||
|
// Parse Subtypes and Human-readable Summaries for each category
|
||||||
|
let actionSummary = '';
|
||||||
|
let actionSubtype = rawSubtype;
|
||||||
|
let foodType = act.food_type || act.meal_type || act.menu_type || null;
|
||||||
|
let amountEaten = act.amount_eaten || act.quantity || act.amount || null;
|
||||||
|
let napStart = act.start_time || act.nap_start || act.started_at || null;
|
||||||
|
let napEnd = act.end_time || act.nap_end || act.ended_at || null;
|
||||||
|
let durationMinutes = act.duration_minutes || act.duration_in_minutes || act.duration || null;
|
||||||
|
let pottyType = act.potty_type || act.diaper_type || act.potty || act.diaper || null;
|
||||||
|
let milestoneName = act.milestone_name || act.tag_name || act.skill_name || act.milestone || null;
|
||||||
|
let attendanceInfo = '';
|
||||||
|
|
||||||
|
// 1. NAP
|
||||||
|
if (actType.includes('nap')) {
|
||||||
|
if (rawSubtype.includes('start') || (napStart && !napEnd && !durationMinutes)) {
|
||||||
|
actionSubtype = 'start_nap';
|
||||||
|
actionSummary = 'Nap Started';
|
||||||
|
} else if (rawSubtype.includes('end') || rawSubtype.includes('wake') || napEnd || durationMinutes) {
|
||||||
|
actionSubtype = 'end_nap';
|
||||||
|
const durText = durationMinutes ? `${durationMinutes} mins` : '';
|
||||||
|
actionSummary = durText ? `Nap Ended (${durText})` : 'Nap Ended';
|
||||||
|
} else {
|
||||||
|
actionSubtype = 'nap';
|
||||||
|
actionSummary = durationMinutes ? `Nap (${durationMinutes} mins)` : 'Nap Log';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 2. CHECK-IN / ATTENDANCE
|
||||||
|
else if (actType.includes('checkin') || actType.includes('checkout') || actType.includes('attendance')) {
|
||||||
|
const isOut = rawSubtype.includes('out') || rawSubtype.includes('away') || rawSubtype.includes('pickup') || actType.includes('checkout');
|
||||||
|
const signedBy = act.signed_in_by || act.signed_out_by || act.guardian_name || act.actor_name || teacherName || '';
|
||||||
|
if (isOut) {
|
||||||
|
actionSubtype = 'signed_out';
|
||||||
|
actionSummary = signedBy ? `Checked Out / Signed Out (by ${signedBy})` : 'Checked Out / Signed Out';
|
||||||
|
} else {
|
||||||
|
actionSubtype = 'signed_in';
|
||||||
|
actionSummary = signedBy ? `Checked In / Signed In (by ${signedBy})` : 'Checked In / Signed In';
|
||||||
|
}
|
||||||
|
attendanceInfo = signedBy ? `Signed by: ${signedBy}` : '';
|
||||||
|
}
|
||||||
|
// 3. FOOD / MEALS
|
||||||
|
else if (actType.includes('food') || actType.includes('meal')) {
|
||||||
|
if (!foodType && rawSubtype) foodType = rawSubtype;
|
||||||
|
const mealLabel = foodType ? capitalize(foodType) : 'Meal';
|
||||||
|
const eatLabel = amountEaten ? `Ate ${amountEaten}` : '';
|
||||||
|
actionSubtype = foodType || 'meal';
|
||||||
|
actionSummary = eatLabel ? `${mealLabel} (${eatLabel})` : mealLabel;
|
||||||
|
}
|
||||||
|
// 4. POTTY / DIAPER
|
||||||
|
else if (actType.includes('potty') || actType.includes('diaper')) {
|
||||||
|
if (!pottyType && rawSubtype) pottyType = rawSubtype;
|
||||||
|
const pLabel = pottyType ? capitalize(pottyType) : 'Check';
|
||||||
|
actionSubtype = pottyType || 'potty';
|
||||||
|
actionSummary = `Potty / Diaper (${pLabel})`;
|
||||||
|
}
|
||||||
|
// 5. PHOTOS & VIDEOS
|
||||||
|
else if (actType.includes('video') || videoUrl) {
|
||||||
|
actionSubtype = 'video';
|
||||||
|
actionSummary = act.note ? `Video: "${act.note}"` : 'Video Clip';
|
||||||
|
} else if (actType.includes('photo') || imageUrl) {
|
||||||
|
actionSubtype = 'photo';
|
||||||
|
actionSummary = act.note ? `Photo: "${act.note}"` : 'Photo';
|
||||||
|
}
|
||||||
|
// 6. MILESTONES & OBSERVATIONS
|
||||||
|
else if (actType.includes('milestone') || actType.includes('observation') || actType.includes('learn') || actType.includes('kudos')) {
|
||||||
|
actionSubtype = 'milestone';
|
||||||
|
actionSummary = milestoneName ? `Milestone: ${milestoneName}` : (act.title || 'Learning Milestone');
|
||||||
|
}
|
||||||
|
// 7. MEDICATIONS
|
||||||
|
else if (actType.includes('med')) {
|
||||||
|
actionSubtype = 'medication';
|
||||||
|
actionSummary = `Medication Administered: ${act.medication_name || act.title || ''}`;
|
||||||
|
}
|
||||||
|
// 8. INCIDENTS
|
||||||
|
else if (actType.includes('incident')) {
|
||||||
|
actionSubtype = 'incident';
|
||||||
|
actionSummary = `Incident Report: ${act.title || act.incident_type || ''}`;
|
||||||
|
}
|
||||||
|
// 9. HEALTH / SCREENING
|
||||||
|
else if (actType.includes('health') || actType.includes('screening')) {
|
||||||
|
actionSubtype = 'health_screening';
|
||||||
|
actionSummary = act.temperature ? `Health Screening (Temp: ${act.temperature}°)` : 'Health Screening';
|
||||||
|
attendanceInfo = act.temperature ? `Temp: ${act.temperature}°` : '';
|
||||||
|
}
|
||||||
|
// 10. DEFAULT / NOTES
|
||||||
|
else {
|
||||||
|
actionSubtype = rawSubtype || actType;
|
||||||
|
actionSummary = act.title || act.note || capitalize(actType.replace('ac_', ''));
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: act.id || act.object_id || Math.random().toString(36).substring(2),
|
id: act.id || act.object_id || Math.random().toString(36).substring(2),
|
||||||
created_at: act.created_at || act.event_date || new Date().toISOString(),
|
created_at: act.created_at || act.event_date || new Date().toISOString(),
|
||||||
activity_type: act.activity_type || act.action_type || act.type || 'activity',
|
activity_type: act.activity_type || actType,
|
||||||
|
action_subtype: actionSubtype,
|
||||||
|
action_summary: actionSummary,
|
||||||
title: act.title || '',
|
title: act.title || '',
|
||||||
note: act.note || act.description || act.body || '',
|
note: act.note || act.description || act.body || act.comment || '',
|
||||||
teacher_name: act.actor_name || act.created_by?.name || act.teacher_name || '',
|
teacher_name: teacherName,
|
||||||
image_url: highResImage || null,
|
image_url: imageUrl || null,
|
||||||
video_url: highResVideo || null,
|
video_url: videoUrl || null,
|
||||||
thumb_url: mediaObj.thumb_url || act.thumb_url || null,
|
thumb_url: thumbUrl || null,
|
||||||
food_type: act.food_type || act.meal_type || null,
|
food_type: foodType,
|
||||||
amount_eaten: act.amount_eaten || act.quantity || null,
|
amount_eaten: amountEaten,
|
||||||
nap_start: act.start_time || act.nap_start || null,
|
nap_start: napStart,
|
||||||
nap_end: act.end_time || act.nap_end || null,
|
nap_end: napEnd,
|
||||||
duration_minutes: act.duration_minutes || null,
|
duration_minutes: durationMinutes,
|
||||||
potty_type: act.potty_type || act.diaper_type || null,
|
potty_type: pottyType,
|
||||||
milestone_name: act.milestone_name || act.tag_name || null,
|
milestone_name: milestoneName,
|
||||||
media_local_path: null // Populated during ZIP bundling
|
attendance_info: attendanceInfo,
|
||||||
|
media_local_path: null, // Populated during ZIP bundling
|
||||||
|
raw: act // Preserve 100% full raw backend payload
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function capitalize(s) {
|
||||||
|
if (!s || typeof s !== 'string') return '';
|
||||||
|
return s.charAt(0).toUpperCase() + s.slice(1).replace(/_/g, ' ');
|
||||||
|
}
|
||||||
|
|
||||||
// -------------------------------------------------------------
|
// -------------------------------------------------------------
|
||||||
// Media Downloader with Concurrency & Progress
|
// Media Downloader with Concurrency & Progress
|
||||||
// -------------------------------------------------------------
|
// -------------------------------------------------------------
|
||||||
@@ -570,7 +777,7 @@ function extractMediaQueue() {
|
|||||||
state.activities.forEach((act) => {
|
state.activities.forEach((act) => {
|
||||||
const url = act.video_url || act.image_url;
|
const url = act.video_url || act.image_url;
|
||||||
if (url) {
|
if (url) {
|
||||||
const isVideo = !!act.video_url || url.includes('.mp4') || act.activity_type === 'video';
|
const isVideo = !!act.video_url || act.activity_type === 'ac_video' || act.activity_type === 'video' || url.includes('.mp4');
|
||||||
const ext = isVideo ? 'mp4' : 'jpg';
|
const ext = isVideo ? 'mp4' : 'jpg';
|
||||||
const datePrefix = (act.created_at || '').substring(0, 10) || 'unknown_date';
|
const datePrefix = (act.created_at || '').substring(0, 10) || 'unknown_date';
|
||||||
const filename = `${datePrefix}_${act.id.substring(0, 12)}.${ext}`;
|
const filename = `${datePrefix}_${act.id.substring(0, 12)}.${ext}`;
|
||||||
@@ -583,6 +790,7 @@ function extractMediaQueue() {
|
|||||||
state.mediaQueue.push({
|
state.mediaQueue.push({
|
||||||
activityId: act.id,
|
activityId: act.id,
|
||||||
url: url,
|
url: url,
|
||||||
|
isVideo: isVideo,
|
||||||
filename: filename,
|
filename: filename,
|
||||||
localZipPath: localZipPath,
|
localZipPath: localZipPath,
|
||||||
blob: null,
|
blob: null,
|
||||||
@@ -600,6 +808,10 @@ async function downloadAllMediaFiles() {
|
|||||||
const concurrency = 3;
|
const concurrency = 3;
|
||||||
let activeIndex = 0;
|
let activeIndex = 0;
|
||||||
|
|
||||||
|
const videoCount = queue.filter(q => q.isVideo).length;
|
||||||
|
const photoCount = queue.length - videoCount;
|
||||||
|
log(`Media to download: ${photoCount} photos, ${videoCount} videos.`, 'info');
|
||||||
|
|
||||||
updateProgress(35, `Downloading 0 / ${queue.length} media files...`);
|
updateProgress(35, `Downloading 0 / ${queue.length} media files...`);
|
||||||
|
|
||||||
async function worker() {
|
async function worker() {
|
||||||
@@ -622,7 +834,7 @@ async function downloadAllMediaFiles() {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
item.status = 'failed';
|
item.status = 'failed';
|
||||||
state.stats.mediaFailed++;
|
state.stats.mediaFailed++;
|
||||||
log(`Failed to download media ${item.filename}: ${err.message}`, 'warn');
|
log(`Failed to download ${item.isVideo ? 'video' : 'photo'} ${item.filename}: ${err.message}`, 'warn');
|
||||||
}
|
}
|
||||||
|
|
||||||
updateMetrics();
|
updateMetrics();
|
||||||
@@ -643,7 +855,7 @@ async function fetchMediaBlob(url) {
|
|||||||
let retries = 0;
|
let retries = 0;
|
||||||
while (retries < 3) {
|
while (retries < 3) {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(url);
|
const res = await fetch(url, { credentials: 'omit' });
|
||||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||||
return await res.blob();
|
return await res.blob();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -764,26 +976,31 @@ function generateCSV(activities) {
|
|||||||
'Activity ID',
|
'Activity ID',
|
||||||
'Date & Time',
|
'Date & Time',
|
||||||
'Type',
|
'Type',
|
||||||
|
'Event Action / Summary',
|
||||||
|
'Subtype',
|
||||||
'Title',
|
'Title',
|
||||||
'Note / Caption',
|
'Note / Caption',
|
||||||
'Logged By',
|
'Staff / Logged By',
|
||||||
'Media File Path',
|
'Media File Path',
|
||||||
'Food / Meal',
|
'Food / Meal',
|
||||||
'Amount Eaten',
|
'Amount Eaten',
|
||||||
'Nap Start',
|
'Nap Start',
|
||||||
'Nap End',
|
'Nap End',
|
||||||
'Nap Mins',
|
'Nap Duration (Mins)',
|
||||||
'Potty Type',
|
'Potty / Diaper Type',
|
||||||
'Milestone'
|
'Milestone',
|
||||||
|
'Attendance / Health Info'
|
||||||
];
|
];
|
||||||
|
|
||||||
const rows = activities.map(a => [
|
const rows = activities.map(a => [
|
||||||
escapeCSV(a.id),
|
escapeCSV(a.id),
|
||||||
escapeCSV(a.created_at),
|
escapeCSV(a.created_at),
|
||||||
escapeCSV(a.activity_type),
|
escapeCSV(a.activity_type),
|
||||||
escapeCSV(a.title),
|
escapeCSV(a.action_summary || ''),
|
||||||
escapeCSV(a.note),
|
escapeCSV(a.action_subtype || ''),
|
||||||
escapeCSV(a.teacher_name),
|
escapeCSV(a.title || ''),
|
||||||
|
escapeCSV(a.note || ''),
|
||||||
|
escapeCSV(a.teacher_name || ''),
|
||||||
escapeCSV(a.media_local_path || a.video_url || a.image_url || ''),
|
escapeCSV(a.media_local_path || a.video_url || a.image_url || ''),
|
||||||
escapeCSV(a.food_type || ''),
|
escapeCSV(a.food_type || ''),
|
||||||
escapeCSV(a.amount_eaten || ''),
|
escapeCSV(a.amount_eaten || ''),
|
||||||
@@ -791,7 +1008,8 @@ function generateCSV(activities) {
|
|||||||
escapeCSV(a.nap_end || ''),
|
escapeCSV(a.nap_end || ''),
|
||||||
escapeCSV(a.duration_minutes || ''),
|
escapeCSV(a.duration_minutes || ''),
|
||||||
escapeCSV(a.potty_type || ''),
|
escapeCSV(a.potty_type || ''),
|
||||||
escapeCSV(a.milestone_name || '')
|
escapeCSV(a.milestone_name || ''),
|
||||||
|
escapeCSV(a.attendance_info || '')
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return [headers.join(','), ...rows.map(r => r.join(','))].join('\r\n');
|
return [headers.join(','), ...rows.map(r => r.join(','))].join('\r\n');
|
||||||
@@ -810,3 +1028,4 @@ function sanitizeFilename(str) {
|
|||||||
function sleep(ms) {
|
function sleep(ms) {
|
||||||
return new Promise(resolve => setTimeout(resolve, ms));
|
return new Promise(resolve => setTimeout(resolve, ms));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -358,7 +358,7 @@
|
|||||||
}) : '';
|
}) : '';
|
||||||
|
|
||||||
const mediaPath = act.media_local_path || act.video_url || act.image_url;
|
const mediaPath = act.media_local_path || act.video_url || act.image_url;
|
||||||
const isVideo = (act.video_url || mediaPath?.endsWith('.mp4') || act.activity_type === 'video');
|
const isVideo = (act.video_url || mediaPath?.endsWith('.mp4') || act.activity_type === 'ac_video' || act.activity_type === 'video');
|
||||||
|
|
||||||
let mediaHtml = '';
|
let mediaHtml = '';
|
||||||
if (mediaPath) {
|
if (mediaPath) {
|
||||||
@@ -374,7 +374,7 @@
|
|||||||
detailLines.push(`🍽️ <strong>${act.food_type || 'Meal'}</strong>: ${act.amount_eaten || 'Eaten'}`);
|
detailLines.push(`🍽️ <strong>${act.food_type || 'Meal'}</strong>: ${act.amount_eaten || 'Eaten'}`);
|
||||||
}
|
}
|
||||||
if (act.nap_start || act.nap_end || act.duration_minutes) {
|
if (act.nap_start || act.nap_end || act.duration_minutes) {
|
||||||
detailLines.push(`😴 <strong>Nap Duration</strong>: ${act.duration_minutes ? act.duration_minutes + ' mins' : (act.nap_start || '') + ' - ' + (act.nap_end || '')}`);
|
detailLines.push(`😴 <strong>Nap</strong>: ${act.duration_minutes ? act.duration_minutes + ' mins' : (act.nap_start || '') + ' - ' + (act.nap_end || '')}`);
|
||||||
}
|
}
|
||||||
if (act.potty_type) {
|
if (act.potty_type) {
|
||||||
detailLines.push(`🚽 <strong>Potty</strong>: ${act.potty_type}`);
|
detailLines.push(`🚽 <strong>Potty</strong>: ${act.potty_type}`);
|
||||||
@@ -382,16 +382,21 @@
|
|||||||
if (act.milestone_name) {
|
if (act.milestone_name) {
|
||||||
detailLines.push(`🌟 <strong>Milestone</strong>: ${act.milestone_name}`);
|
detailLines.push(`🌟 <strong>Milestone</strong>: ${act.milestone_name}`);
|
||||||
}
|
}
|
||||||
|
if (act.attendance_info) {
|
||||||
|
detailLines.push(`📋 <strong>Attendance</strong>: ${act.attendance_info}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const displayTitle = act.action_summary || act.title || act.activity_type;
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<div class="feed-card">
|
<div class="feed-card">
|
||||||
${mediaHtml}
|
${mediaHtml}
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<span class="type-badge type-${type}">${act.activity_type || type}</span>
|
<span class="type-badge type-${type}">${act.action_subtype || act.activity_type || type}</span>
|
||||||
<span class="card-time">${formattedDate}</span>
|
<span class="card-time">${formattedDate}</span>
|
||||||
</div>
|
</div>
|
||||||
${act.title ? `<div class="card-title">${act.title}</div>` : ''}
|
<div class="card-title">${displayTitle}</div>
|
||||||
${act.note ? `<div class="card-note">${act.note}</div>` : ''}
|
${act.note ? `<div class="card-note">${act.note}</div>` : ''}
|
||||||
${detailLines.length > 0 ? `<div class="card-details">${detailLines.join('<br>')}</div>` : ''}
|
${detailLines.length > 0 ? `<div class="card-details">${detailLines.join('<br>')}</div>` : ''}
|
||||||
<div class="card-footer">
|
<div class="card-footer">
|
||||||
|
|||||||
Reference in New Issue
Block a user