Skip to content

Commit ec7bf5e

Browse files
File changes
1 parent 33b9b12 commit ec7bf5e

File tree

6 files changed

+568
-12
lines changed

6 files changed

+568
-12
lines changed

functions/awardWellnessPoints.ts

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import { createClientFromRequest } from 'npm:@base44/[email protected]';
2+
3+
Deno.serve(async (req) => {
4+
try {
5+
const base44 = createClientFromRequest(req);
6+
const { event, data, old_data } = await req.json();
7+
8+
// Only award points on status change to 'completed'
9+
if (data.status === 'completed' && old_data?.status !== 'completed') {
10+
// Fetch the challenge to get point reward
11+
const challenge = await base44.asServiceRole.entities.WellnessChallenge.filter({
12+
id: data.challenge_id
13+
}).then(r => r[0]);
14+
15+
if (!challenge) {
16+
return Response.json({ error: 'Challenge not found' }, { status: 404 });
17+
}
18+
19+
// Fetch user points
20+
const userPoints = await base44.asServiceRole.entities.UserPoints.filter({
21+
user_email: data.user_email
22+
});
23+
24+
if (userPoints.length > 0) {
25+
// Update existing points
26+
await base44.asServiceRole.entities.UserPoints.update(userPoints[0].id, {
27+
total_points: (userPoints[0].total_points || 0) + challenge.points_reward
28+
});
29+
} else {
30+
// Create new points record
31+
await base44.asServiceRole.entities.UserPoints.create({
32+
user_email: data.user_email,
33+
total_points: challenge.points_reward,
34+
current_level: 1
35+
});
36+
}
37+
38+
// Create notification
39+
await base44.asServiceRole.entities.Notification.create({
40+
user_email: data.user_email,
41+
type: 'wellness_milestone',
42+
title: 'Wellness Goal Completed! 🎉',
43+
message: `You earned ${challenge.points_reward} points for completing ${challenge.title}!`,
44+
read: false
45+
});
46+
47+
// Check for streak milestones
48+
if (data.streak_days && [7, 14, 30].includes(data.streak_days)) {
49+
const bonusPoints = data.streak_days * 10;
50+
await base44.asServiceRole.entities.UserPoints.filter({
51+
user_email: data.user_email
52+
}).then(async (points) => {
53+
if (points[0]) {
54+
await base44.asServiceRole.entities.UserPoints.update(points[0].id, {
55+
total_points: (points[0].total_points || 0) + bonusPoints
56+
});
57+
}
58+
});
59+
60+
await base44.asServiceRole.entities.Notification.create({
61+
user_email: data.user_email,
62+
type: 'streak_milestone',
63+
title: `${data.streak_days}-Day Streak! 🔥`,
64+
message: `Amazing! ${bonusPoints} bonus points for your dedication!`,
65+
read: false
66+
});
67+
}
68+
69+
return Response.json({
70+
success: true,
71+
points_awarded: challenge.points_reward
72+
});
73+
}
74+
75+
return Response.json({ success: true, message: 'No points to award' });
76+
} catch (error) {
77+
return Response.json({ error: error.message }, { status: 500 });
78+
}
79+
});

functions/syncFitbitData.ts

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import { createClientFromRequest } from 'npm:@base44/[email protected]';
2+
3+
Deno.serve(async (req) => {
4+
try {
5+
const base44 = createClientFromRequest(req);
6+
const user = await base44.auth.me();
7+
8+
if (!user) {
9+
return Response.json({ error: 'Unauthorized' }, { status: 401 });
10+
}
11+
12+
// Get Fitbit access token
13+
const accessToken = await base44.asServiceRole.connectors.getAccessToken('fitbit');
14+
15+
// Fetch today's activity from Fitbit API
16+
const today = new Date().toISOString().split('T')[0];
17+
const activityResponse = await fetch(
18+
`https://api.fitbit.com/1/user/-/activities/date/${today}.json`,
19+
{
20+
headers: {
21+
Authorization: `Bearer ${accessToken}`
22+
}
23+
}
24+
);
25+
26+
if (!activityResponse.ok) {
27+
throw new Error('Failed to fetch Fitbit data');
28+
}
29+
30+
const activityData = await activityResponse.json();
31+
const steps = activityData.summary.steps;
32+
33+
// Find active step challenges for this user
34+
const challenges = await base44.asServiceRole.entities.WellnessChallenge.filter({
35+
status: 'active',
36+
challenge_type: 'steps'
37+
});
38+
39+
const userGoals = await base44.asServiceRole.entities.WellnessGoal.filter({
40+
user_email: user.email,
41+
status: 'in_progress'
42+
});
43+
44+
// Log the activity
45+
const log = await base44.asServiceRole.entities.WellnessLog.create({
46+
user_email: user.email,
47+
log_date: today,
48+
activity_type: 'steps',
49+
value: steps,
50+
unit: 'steps',
51+
source: 'fitbit'
52+
});
53+
54+
// Update goal progress
55+
for (const goal of userGoals) {
56+
const challenge = challenges.find(c => c.id === goal.challenge_id);
57+
if (challenge && challenge.challenge_type === 'steps') {
58+
const newProgress = steps;
59+
const progressPercentage = Math.min((newProgress / goal.target_value) * 100, 100);
60+
61+
const updates = {
62+
current_progress: newProgress,
63+
progress_percentage: progressPercentage
64+
};
65+
66+
// Check if goal completed
67+
if (progressPercentage >= 100 && goal.status !== 'completed') {
68+
updates.status = 'completed';
69+
updates.completed_at = new Date().toISOString();
70+
71+
// Award points
72+
await base44.asServiceRole.entities.UserPoints.filter({ user_email: user.email })
73+
.then(async (points) => {
74+
if (points[0]) {
75+
await base44.asServiceRole.entities.UserPoints.update(points[0].id, {
76+
total_points: (points[0].total_points || 0) + challenge.points_reward
77+
});
78+
}
79+
});
80+
}
81+
82+
await base44.asServiceRole.entities.WellnessGoal.update(goal.id, updates);
83+
}
84+
}
85+
86+
return Response.json({
87+
success: true,
88+
steps,
89+
goalsUpdated: userGoals.length
90+
});
91+
} catch (error) {
92+
return Response.json({ error: error.message }, { status: 500 });
93+
}
94+
});

src/Layout.jsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@ export default function Layout({ children, currentPageName }) {
128128
{ name: 'Milestones', icon: Cake, page: 'Milestones' },
129129
{ name: 'Analytics', icon: BarChart3, page: 'Analytics' },
130130
{ name: 'Wellness Admin', icon: Activity, page: 'WellnessAdmin' },
131+
{ name: 'Wellness Analytics', icon: BarChart3, page: 'WellnessAnalyticsReport' },
131132
{ name: 'Report Builder', icon: FileText, page: 'ReportBuilder' },
132133
{ name: 'Predictive Analytics', icon: Brain, page: 'PredictiveAnalytics' },
133134
{ name: 'Audit Log', icon: Shield, page: 'AuditLog' },
Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
import { useQuery } from '@tanstack/react-query';
2+
import { base44 } from '@/api/base44Client';
3+
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
4+
import { Badge } from '@/components/ui/badge';
5+
import { Avatar } from '@/components/ui/avatar';
6+
import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs';
7+
import { Trophy, TrendingUp, Users, Award } from 'lucide-react';
8+
import LoadingSpinner from '@/components/common/LoadingSpinner';
9+
10+
export default function TeamWellnessLeaderboard({ challengeId }) {
11+
const { data: challenge } = useQuery({
12+
queryKey: ['wellnessChallenge', challengeId],
13+
queryFn: () => base44.entities.WellnessChallenge.filter({ id: challengeId }).then(r => r[0])
14+
});
15+
16+
const { data: leaderboard, isLoading } = useQuery({
17+
queryKey: ['teamWellnessLeaderboard', challengeId],
18+
queryFn: async () => {
19+
const goals = await base44.entities.WellnessGoal.filter({ challenge_id: challengeId });
20+
21+
// Aggregate by team
22+
const teamStats = {};
23+
24+
for (const goal of goals) {
25+
const memberships = await base44.entities.TeamMembership.filter({
26+
user_email: goal.user_email
27+
});
28+
29+
for (const membership of memberships) {
30+
if (!teamStats[membership.team_id]) {
31+
const team = await base44.entities.Team.filter({ id: membership.team_id }).then(r => r[0]);
32+
teamStats[membership.team_id] = {
33+
teamId: membership.team_id,
34+
teamName: team?.name || 'Unknown Team',
35+
totalProgress: 0,
36+
memberCount: 0,
37+
completedGoals: 0
38+
};
39+
}
40+
41+
teamStats[membership.team_id].totalProgress += goal.progress_percentage || 0;
42+
teamStats[membership.team_id].memberCount += 1;
43+
if (goal.status === 'completed') {
44+
teamStats[membership.team_id].completedGoals += 1;
45+
}
46+
}
47+
}
48+
49+
// Calculate averages and sort
50+
const teams = Object.values(teamStats).map(team => ({
51+
...team,
52+
avgProgress: team.memberCount > 0 ? team.totalProgress / team.memberCount : 0
53+
})).sort((a, b) => b.avgProgress - a.avgProgress);
54+
55+
return teams;
56+
},
57+
enabled: !!challengeId
58+
});
59+
60+
const { data: individualLeaderboard } = useQuery({
61+
queryKey: ['individualWellnessLeaderboard', challengeId],
62+
queryFn: async () => {
63+
const goals = await base44.entities.WellnessGoal.filter({ challenge_id: challengeId });
64+
const profiles = await base44.entities.UserProfile.filter({});
65+
66+
return goals
67+
.map(goal => ({
68+
...goal,
69+
profile: profiles.find(p => p.user_email === goal.user_email)
70+
}))
71+
.sort((a, b) => (b.progress_percentage || 0) - (a.progress_percentage || 0))
72+
.slice(0, 10);
73+
},
74+
enabled: !!challengeId
75+
});
76+
77+
if (isLoading) return <LoadingSpinner />;
78+
79+
const getRankColor = (index) => {
80+
if (index === 0) return 'text-yellow-600';
81+
if (index === 1) return 'text-slate-400';
82+
if (index === 2) return 'text-amber-600';
83+
return 'text-slate-600';
84+
};
85+
86+
const getRankBg = (index) => {
87+
if (index === 0) return 'bg-gradient-to-br from-yellow-400 to-yellow-600';
88+
if (index === 1) return 'bg-gradient-to-br from-slate-300 to-slate-400';
89+
if (index === 2) return 'bg-gradient-to-br from-amber-400 to-amber-600';
90+
return 'bg-slate-100';
91+
};
92+
93+
return (
94+
<Card>
95+
<CardHeader>
96+
<CardTitle className="flex items-center gap-2">
97+
<Trophy className="h-5 w-5 text-int-gold" />
98+
Wellness Leaderboard
99+
</CardTitle>
100+
<CardDescription>
101+
{challenge?.title || 'Challenge'} - Team & Individual Rankings
102+
</CardDescription>
103+
</CardHeader>
104+
<CardContent>
105+
<Tabs defaultValue="teams">
106+
<TabsList className="w-full">
107+
<TabsTrigger value="teams" className="flex-1">
108+
<Users className="h-4 w-4 mr-2" />
109+
Teams
110+
</TabsTrigger>
111+
<TabsTrigger value="individual" className="flex-1">
112+
<Award className="h-4 w-4 mr-2" />
113+
Individual
114+
</TabsTrigger>
115+
</TabsList>
116+
117+
<TabsContent value="teams" className="space-y-3 mt-4">
118+
{leaderboard?.length === 0 ? (
119+
<p className="text-center text-slate-500 py-8">No team data yet</p>
120+
) : (
121+
leaderboard?.map((team, index) => (
122+
<div
123+
key={team.teamId}
124+
className={`flex items-center gap-3 p-4 rounded-lg border ${
125+
index < 3 ? 'border-int-gold/30 bg-int-gold/5' : 'border-slate-200'
126+
}`}
127+
>
128+
<div className={`h-10 w-10 rounded-full ${getRankBg(index)} flex items-center justify-center font-bold text-white`}>
129+
{index + 1}
130+
</div>
131+
<div className="flex-1">
132+
<p className="font-semibold">{team.teamName}</p>
133+
<p className="text-sm text-slate-500">
134+
{team.memberCount} members • {team.completedGoals} completed
135+
</p>
136+
</div>
137+
<div className="text-right">
138+
<p className="text-2xl font-bold text-int-orange">
139+
{Math.round(team.avgProgress)}%
140+
</p>
141+
<p className="text-xs text-slate-500">Avg Progress</p>
142+
</div>
143+
</div>
144+
))
145+
)}
146+
</TabsContent>
147+
148+
<TabsContent value="individual" className="space-y-3 mt-4">
149+
{individualLeaderboard?.length === 0 ? (
150+
<p className="text-center text-slate-500 py-8">No participants yet</p>
151+
) : (
152+
individualLeaderboard?.map((goal, index) => (
153+
<div
154+
key={goal.id}
155+
className={`flex items-center gap-3 p-3 rounded-lg border ${
156+
index < 3 ? 'border-int-gold/30 bg-int-gold/5' : 'border-slate-200'
157+
}`}
158+
>
159+
<div className={`h-8 w-8 rounded-full ${getRankBg(index)} flex items-center justify-center font-bold text-white text-sm`}>
160+
{index + 1}
161+
</div>
162+
<div className="flex-1">
163+
<p className="font-medium text-sm">{goal.user_email}</p>
164+
{goal.status === 'completed' && (
165+
<Badge className="text-xs bg-green-500 text-white mt-1">Completed</Badge>
166+
)}
167+
</div>
168+
<div className="text-right">
169+
<p className="font-bold text-int-orange">
170+
{Math.round(goal.progress_percentage || 0)}%
171+
</p>
172+
</div>
173+
</div>
174+
))
175+
)}
176+
</TabsContent>
177+
</Tabs>
178+
</CardContent>
179+
</Card>
180+
);
181+
}

0 commit comments

Comments
 (0)