eletrotupi / tcc/ commit / e12acd9

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
Pedro Lucas Porcellis porcellis@eletrotupi.com 1 month ago e12acd9f8e7d69f1d862f496e3136e60c63de5c0
Parents: 6c0f1d0
7 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
};