api: add today endpoint + PATCH /users/me preferences
`GET /medicine-regimens/today` returns active regimens with today's MedicineLogs per regimen. PATCH /users/me validates and persists notificationsEnabled + dailyReminderTime, then syncs the daily reminder BullMQ repeatable job
Parents:
6c0f1d07 file(s) changed
- api/src/controllers/medicineRegimens.ts +11 -0
- api/src/controllers/users.ts +23 -1
- api/src/routes/medicineRegimens.ts +1 -0
- api/src/routes/users.ts +1 -0
- api/src/schemas/index.ts +2 -0
- api/src/schemas/user.schema.ts +7 -0
- api/src/services/medicineRegimen.service.ts +34 -0
api/src/controllers/medicineRegimens.ts
1 1
import { Response, NextFunction } from 'express';
2 2
import { AuthenticatedRequest } from '@app/middleware/auth';
3 3
import { CreateMedicineRegimenSchema, UpdateMedicineRegimenSchema } from '@app/schemas';
4 4
import {
5 5
getMedicineRegimensByUserId,
6 6
createMedicineRegimen,
7 7
updateMedicineRegimen,
8 8
deactivateMedicineRegimen,
9 +
getTodayMedicineRegimens,
9 10
} from '@app/services/medicineRegimen.service';
10 11
11 12
export const MedicineRegimensController = {
12 13
index: async (req: AuthenticatedRequest, res: Response, next: NextFunction) => {
13 14
try {
14 15
const regimens = await getMedicineRegimensByUserId(Number(req.userId));
16 +
17 +
return res.status(200).json({ regimens });
18 +
} catch (err) {
19 +
next(err);
20 +
}
21 +
},
22 +
23 +
today: async (req: AuthenticatedRequest, res: Response, next: NextFunction) => {
24 +
try {
25 +
const regimens = await getTodayMedicineRegimens(req.userId!);
15 26
16 27
return res.status(200).json({ regimens });
17 28
} catch (err) {
18 29
next(err);
19 30
}
▸ 43 unchanged lines
63 74
} catch (err) {
64 75
next(err);
65 76
}
66 77
},
67 78
};
api/src/controllers/users.ts
1 1
import { Request, Response, NextFunction } from 'express';
2 2
import {
3 3
generateToken
4 4
} from '@app/services/auth.service';
5 5
6 6
import { getQueue, MailJobName } from '@app/lib/queue';
7 7
8 8
import {
9 9
CreateUserSchema,
10 -
UpdateUserSchema
10 +
UpdateUserSchema,
11 +
UpdateUserPreferencesSchema,
11 12
} from '@app/schemas';
13 +
import { syncDailyReminderJob } from '@app/services/dailyReminder.sync';
12 14
13 15
import {
14 16
createUser,
15 17
findUserById,
16 18
findUserByEmail,
▸ 113 unchanged lines
130 132
key: req.file.key,
131 133
filename: req.file.originalname,
132 134
size: req.file.size,
133 135
user: user
134 136
});
137 +
} catch (err) {
138 +
next(err);
139 +
}
140 +
},
141 +
142 +
updatePreferences: async (req: AuthenticatedRequest, res: Response, next: NextFunction) => {
143 +
try {
144 +
const parsed = UpdateUserPreferencesSchema.safeParse(req.body.user);
145 +
if (!parsed.success) {
146 +
return res.status(400).json({ errors: parsed.error!.issues });
147 +
}
148 +
149 +
const user = await prisma.user.update({
150 +
where: { id: req.userId },
151 +
data: parsed.data,
152 +
});
153 +
154 +
await syncDailyReminderJob(user);
155 +
156 +
return res.status(200).json({ user });
135 157
} catch (err) {
136 158
next(err);
137 159
}
138 160
},
139 161
▸ 22 unchanged lines
162 184
return res.json({
163 185
user
164 186
})
165 187
}
166 188
}
api/src/routes/medicineRegimens.ts
1 1
import { Router } from 'express';
2 2
import { MedicineRegimensController } from '@app/controllers/medicineRegimens';
3 3
import { requireAuth } from '@app/middleware/auth';
4 4
5 5
const router = Router();
6 6
7 7
router.get('/', requireAuth, MedicineRegimensController.index);
8 +
router.get('/today', requireAuth, MedicineRegimensController.today);
8 9
router.post('/', requireAuth, MedicineRegimensController.create);
9 10
router.patch('/:id', requireAuth, MedicineRegimensController.update);
10 11
router.delete('/:id', requireAuth, MedicineRegimensController.destroy);
11 12
12 13
export default router;
api/src/routes/users.ts
1 1
import { Router } from "express";
2 2
import { UsersController } from '@app/controllers/users';
3 3
import multer from '@app/services/avatar';
4 4
import { requireAuth } from '@app/middleware/auth';
5 5
6 6
const router = Router();
7 7
8 8
router.post('/', UsersController.create);
9 +
router.patch('/me', requireAuth, UsersController.updatePreferences);
9 10
router.put('/:id', UsersController.update);
10 11
router.post('/me/avatar', requireAuth, multer.single('avatar'), UsersController.updateAvatar);
11 12
router.delete('/me/avatar', requireAuth, UsersController.removeAvatar);
12 13
13 14
export default router;
api/src/schemas/index.ts
1 1
export {
2 2
CreateMoodSchema,
3 3
MoodComponentSchema,
4 4
} from "@app/schemas/mood.schema";
5 5
6 6
export type { CreateMoodInput } from "@app/schemas/mood.schema";
7 7
8 8
export {
9 9
CreateUserSchema,
10 10
UpdateUserSchema,
11 +
UpdateUserPreferencesSchema,
11 12
type CreateUserInput,
12 13
type UpdateUserInput,
14 +
type UpdateUserPreferencesInput,
13 15
} from "@app/schemas/user.schema";
14 16
15 17
export {
16 18
LoginSchema,
17 19
PasswordResetRequestSchema,
▸ 40 unchanged lines
58 60
CreateMedicineRegimenSchema,
59 61
type CreateMedicineRegimenInput,
60 62
UpdateMedicineRegimenSchema,
61 63
type UpdateMedicineRegimenInput,
62 64
} from "@app/schemas/medicineRegimen.schema";
api/src/schemas/user.schema.ts
1 1
import { z } from "zod";
2 2
3 3
export const CreateUserSchema = z.object({
4 4
firstName: z.string(),
5 5
lastName: z.string().optional(),
▸ 6 unchanged lines
12 12
id: z.number(),
13 13
});
14 14
15 15
export type CreateUserInput = z.infer<typeof CreateUserSchema>;
16 16
export type UpdateUserInput = z.infer<typeof UpdateUserSchema>;
17 +
18 +
export const UpdateUserPreferencesSchema = z.object({
19 +
notificationsEnabled: z.boolean().optional(),
20 +
dailyReminderTime: z.string().regex(/^\d{2}:\d{2}$/).nullable().optional(),
21 +
});
22 +
23 +
export type UpdateUserPreferencesInput = z.infer<typeof UpdateUserPreferencesSchema>;
api/src/services/medicineRegimen.service.ts
1 1
import { prisma } from '@app/lib/prisma';
2 2
import { MedicineRegimen } from '@prisma/client';
3 3
import { CreateMedicineRegimenInput, UpdateMedicineRegimenInput } from '@app/schemas';
4 4
import { syncMedicineReminderJobs } from '@app/services/medicineReminder.sync';
5 5
6 +
type TodayEntry = {
7 +
regimen: Pick<MedicineRegimen, 'id' | 'name' | 'dosage' | 'scheduledAt'>;
8 +
logs: { id: number; takenAt: Date }[];
9 +
};
10 +
6 11
export const getMedicineRegimensByUserId = async (
7 12
userId: number,
8 13
): Promise<MedicineRegimen[]> => {
9 14
return prisma.medicineRegimen.findMany({
10 15
where: { userId, active: true },
▸ 21 unchanged lines
32 37
where: { id, userId },
33 38
data: input,
34 39
});
35 40
await syncMedicineReminderJobs(regimen);
36 41
return regimen;
42 +
};
43 +
44 +
export const getTodayMedicineRegimens = async (
45 +
userId: number,
46 +
): Promise<TodayEntry[]> => {
47 +
const now = new Date();
48 +
const startOfDay = new Date(now);
49 +
startOfDay.setUTCHours(0, 0, 0, 0);
50 +
const endOfDay = new Date(now);
51 +
endOfDay.setUTCHours(23, 59, 59, 999);
52 +
53 +
const regimens = await prisma.medicineRegimen.findMany({
54 +
where: { userId, active: true },
55 +
select: { id: true, name: true, dosage: true, scheduledAt: true },
56 +
orderBy: { createdAt: 'desc' },
57 +
});
58 +
59 +
return Promise.all(
60 +
regimens.map(async (regimen) => {
61 +
const logs = await prisma.medicineLog.findMany({
62 +
where: {
63 +
regimenId: regimen.id,
64 +
takenAt: { gte: startOfDay, lte: endOfDay },
65 +
},
66 +
select: { id: true, takenAt: true },
67 +
});
68 +
return { regimen, logs };
69 +
})
70 +
);
37 71
};
38 72
39 73
export const deactivateMedicineRegimen = async (
40 74
userId: number,
41 75
id: number,
▸ 3 unchanged lines
45 79
data: { active: false },
46 80
});
47 81
await syncMedicineReminderJobs(regimen);
48 82
return regimen;
49 83
};