Skip to content

Commit

Permalink
add: find and set handlers for availability slots with unit tests.
Browse files Browse the repository at this point in the history
  • Loading branch information
mmoehabb committed Jan 12, 2025
1 parent e5f4554 commit edee3ad
Show file tree
Hide file tree
Showing 30 changed files with 875 additions and 104 deletions.
5 changes: 4 additions & 1 deletion apps/nova/src/components/Lessons/BookLesson.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,13 +64,16 @@ const BookLesson = ({
const onBook = useCallback(
({
ruleId,
slotId,
start,
duration,
}: {
ruleId: number;
slotId: number;
start: string;
duration: ILesson.Duration;
}) => bookLessonMutation.mutate({ tutorId, ruleId, start, duration }),
}) =>
bookLessonMutation.mutate({ tutorId, ruleId, slotId, start, duration }),
[bookLessonMutation, tutorId]
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@ const ScheduleInterview: React.FC<{
mutation.mutate({
interviewerId: interviewer.data.id,
ruleId: selectedRule.id,
slotId: selectedRule.id,
start: selectedRule.start,
});
}}
Expand Down
22 changes: 22 additions & 0 deletions packages/atlas/src/availabilitySlot.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { Base } from "@/base";
import { IAvailabilitySlot } from "@litespace/types";

export class AvailabilitySlot extends Base {
async find(
params: IAvailabilitySlot.FindAvailabilitySlotsApiQuery
): Promise<IAvailabilitySlot.FindAvailabilitySlotsApiResponse> {
return await this.get({
route: `/api/v1/availability-slot/`,
params,
});
}

async set(
payload: IAvailabilitySlot.SetAvailabilitySlotsApiPayload
): Promise<IAvailabilitySlot.FindAvailabilitySlotsApiResponse> {
return await this.post({
route: `/api/v1/availability-slot/`,
payload,
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -120,10 +120,12 @@ export const BookLessonDialog: React.FC<{
notice: number | null;
onBook: ({
ruleId,
slotId,
start,
duration,
}: {
ruleId: number;
slotId: number;
start: string;
duration: ILesson.Duration;
}) => void;
Expand All @@ -145,6 +147,7 @@ export const BookLessonDialog: React.FC<{
const [duration, setDuration] = useState<number>(15);
const [start, setStart] = useState<string | null>(null);
const [ruleId, setRuleId] = useState<number | null>(null);
const [slotId, setSlotId] = useState<number | null>(null);
const [date, setDate] = useState<Dayjs>(dayjs());

const dateBounds = useMemo(() => {
Expand Down Expand Up @@ -176,6 +179,7 @@ export const BookLessonDialog: React.FC<{
daySlots.map((rule) => splitRuleEvent(rule, duration))
).map((event) => ({
ruleId: event.id,
slotId: event.id,
start: event.start,
end: event.end,
bookable: dayjs(event.start).isAfter(
Expand Down Expand Up @@ -205,6 +209,7 @@ export const BookLessonDialog: React.FC<{
const bookedSlots: AttributedSlot[] = slots
.map((slot) => ({
ruleId: slot.ruleId,
slotId: slot.ruleId,
start: slot.start,
end: dayjs(slot.start).add(slot.duration, "minutes").toISOString(),
bookable: false,
Expand Down Expand Up @@ -290,9 +295,11 @@ export const BookLessonDialog: React.FC<{
slots={allSlots}
start={start}
ruleId={ruleId}
select={({ ruleId, start }) => {
slotId={ruleId}
select={({ slotId, ruleId, start }) => {
setStart(start);
setRuleId(ruleId);
setSlotId(slotId);
}}
/>
</Animation>
Expand All @@ -302,6 +309,7 @@ export const BookLessonDialog: React.FC<{
step === "confirmation" &&
start &&
ruleId &&
slotId &&
!loading ? (
<Animation key="confimration" id="confirmation">
<div className="tw-px-6">
Expand All @@ -312,7 +320,7 @@ export const BookLessonDialog: React.FC<{
start={start}
confirmationLoading={confirmationLoading}
duration={duration}
onConfrim={() => onBook({ ruleId, start, duration })}
onConfrim={() => onBook({ ruleId, slotId, start, duration })}
onEdit={() => {
setStep("date-selection");
}}
Expand Down
19 changes: 15 additions & 4 deletions packages/luna/src/components/Lessons/BookLesson/TimeSelection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@ export const TimeSelection: React.FC<{
slots: AttributedSlot[];
start: string | null;
ruleId: number | null;
select: (payload: { start: string; ruleId: number }) => void;
}> = ({ slots, start, ruleId, select }) => {
slotId: number | null;
select: (payload: { start: string; ruleId: number; slotId: number }) => void;
}> = ({ slots, start, ruleId, slotId, select }) => {
return (
<div className="tw-px-5">
<div
Expand All @@ -26,8 +27,18 @@ export const TimeSelection: React.FC<{
whileTap={{ scale: 0.95 }}
type="button"
key={slot.start}
onClick={() => select({ ruleId: slot.ruleId, start: slot.start })}
data-selected={slot.start === start && slot.ruleId === ruleId}
onClick={() =>
select({
ruleId: slot.ruleId,
slotId: slot.ruleId,
start: slot.start,
})
}
data-selected={
slot.start === start &&
slot.ruleId === ruleId &&
slot.ruleId === slotId
}
disabled={!slot.bookable}
className={cn(
"tw-bg-natural-50 tw-border tw-border-natural-800 tw-shadow-time-selection-item",
Expand Down
21 changes: 15 additions & 6 deletions packages/models/fixtures/db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import {
IAvailabilitySlot,
} from "@litespace/types";
import { faker } from "@faker-js/faker/locale/ar";
import { entries, range, sample } from "lodash";
import { entries, first, range, sample } from "lodash";
import { Knex } from "knex";
import dayjs from "@/lib/dayjs";
import { Time } from "@litespace/sol/time";
Expand Down Expand Up @@ -107,11 +107,16 @@ export async function slot(payload?: Partial<IAvailabilitySlot.CreatePayload>) {
const start = dayjs.utc(payload?.start || faker.date.future());
const end = start.add(faker.number.int(8), "hours");

return (await availabilitySlots.create([{
userId: await or.tutorId(payload?.userId),
start: start.toISOString(),
end: end.toISOString(),
}]))[0];
const slots = await availabilitySlots.create([
{
userId: await or.tutorId(payload?.userId),
start: start.toISOString(),
end: end.toISOString(),
},
])
const slot = first(slots);
if (!slot) throw new Error('Slot not found; should never happen');
return slot;
}

const or = {
Expand All @@ -135,6 +140,10 @@ const or = {
if (!id) return await rule().then((rule) => rule.id);
return id;
},
async roomId(roomId?: number): Promise<number> {
if (!roomId) return await makeRoom();
return roomId;
},
async slotId(id?: number): Promise<number> {
if (!id) return await slot().then((slot) => slot.id);
return id;
Expand Down
13 changes: 11 additions & 2 deletions packages/models/migrations/1716146586880_setup.js
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ exports.up = (pgm) => {
user_id: { type: "INT", notNull: true, references: "users(id)" },
start: { type: "TIMESTAMP", notNull: true },
end: { type: "TIMESTAMP", notNull: true },
deleted: { type: "BOOLEAN", notNull: true, default: false },
created_at: { type: "TIMESTAMP", notNull: true },
updated_at: { type: "TIMESTAMP", notNull: true },
});
Expand All @@ -104,7 +105,11 @@ exports.up = (pgm) => {
duration: { type: "SMALLINT", notNull: true },
price: { type: "INT", notNull: true },
rule_id: { type: "SERIAL", references: "rules(id)", notNull: true },
slot_id: { type: "SERIAL", references: "availability_slots(id)", notNull: true },
slot_id: {
type: "SERIAL",
references: "availability_slots(id)",
notNull: true,
},
session_id: { type: "VARCHAR(50)", notNull: true, primaryKey: true },
canceled_by: { type: "INT", references: "users(id)", default: null },
canceled_at: { type: "TIMESTAMP", default: null },
Expand All @@ -126,7 +131,11 @@ exports.up = (pgm) => {
interviewee_feedback: { type: "TEXT", default: null },
session_id: { type: "TEXT", notNull: true, primaryKey: true },
rule_id: { type: "SERIAL", references: "rules(id)", notNull: true },
slot_id: { type: "SERIAL", references: "availability_slots(id)", notNull: true },
slot_id: {
type: "SERIAL",
references: "availability_slots(id)",
notNull: true,
},
note: { type: "TEXT", default: null },
level: { type: "INT", default: null },
status: { type: "interview_status", default: "pending" },
Expand Down
54 changes: 34 additions & 20 deletions packages/models/scripts/seed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -288,13 +288,13 @@ async function main(): Promise<void> {
});

// seeding slots
await availabilitySlots.create(addedTutors.map(tutor =>
({
await availabilitySlots.create(
addedTutors.map((tutor) => ({
userId: tutor.id,
start: dayjs.utc().startOf("day").toISOString(),
end: dayjs.utc().startOf("day").add(30, "days").toISOString(),
})
));
}))
);

const times = range(0, 24).map((hour) =>
[hour.toString().padStart(2, "0"), "00"].join(":")
Expand Down Expand Up @@ -398,7 +398,9 @@ async function main(): Promise<void> {
let start = dayjs().utc().startOf("day");
for (const tutor of addedTutors) {
// create chat room tutor-student
await rooms.create([tutor.id, student.id]);
await knex.transaction(async (tx) =>
rooms.create([tutor.id, student.id], tx)
);

for (const _ of range(1, 100)) {
await createRandomLesson({
Expand All @@ -414,11 +416,15 @@ async function main(): Promise<void> {
);
}

const slot = (await availabilitySlots.create([{
userId: tutorManager.id,
start: dayjs.utc().startOf("day").toISOString(),
end: dayjs.utc().startOf("day").add(1, "days").toISOString(),
}]))[0];
const slot = (
await availabilitySlots.create([
{
userId: tutorManager.id,
start: dayjs.utc().startOf("day").toISOString(),
end: dayjs.utc().startOf("day").add(1, "days").toISOString(),
},
])
)[0];

for (const tutor of addedTutors) {
await knex.transaction(async (tx: Knex.Transaction) => {
Expand Down Expand Up @@ -576,18 +582,26 @@ async function main(): Promise<void> {
reportId: 1,
});

const roomId = await rooms.create([tutor.id, tutorManager.id]);
await knex.transaction(async (tx) => {
const roomId = await rooms.create([tutor.id, tutorManager.id], tx);

await messages.create({
userId: tutor.id,
text: "Hello!",
roomId,
});
await messages.create(
{
userId: tutor.id,
text: "Hello!",
roomId,
},
tx
);

await messages.create({
userId: tutorManager.id,
text: "Nice to meet you!",
roomId,
await messages.create(
{
userId: tutorManager.id,
text: "Nice to meet you!",
roomId,
},
tx
);
});

const lessonsTotal = await lessons.sumPrice({});
Expand Down
Loading

0 comments on commit edee3ad

Please sign in to comment.