Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

added(server/api): availability slots "find" and "set" api handlers. #278

Merged
merged 3 commits into from
Jan 15, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
3 changes: 3 additions & 0 deletions packages/atlas/src/atlas.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { User } from "@/user";
import { Auth } from "@/auth";
import { AvailabilitySlot } from "@/availabilitySlot";
import { Backend } from "@litespace/types";
import { Plan } from "@/plan";
import { Coupon } from "@/coupon";
Expand All @@ -22,6 +23,7 @@ import { Session } from "@/session";
export class Atlas {
public readonly user: User;
public readonly auth: Auth;
public readonly availabilitySlot: AvailabilitySlot;
public readonly plan: Plan;
public readonly coupon: Coupon;
public readonly invite: Invite;
Expand All @@ -42,6 +44,7 @@ export class Atlas {
constructor(backend: Backend, token: AuthToken | null) {
this.user = new User(backend, token);
this.auth = new Auth(backend, token);
this.availabilitySlot = new AvailabilitySlot(backend, token);
this.plan = new Plan(backend, token);
this.coupon = new Coupon(backend, token);
this.invite = new Invite(backend, token);
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,
});
}
}
3 changes: 3 additions & 0 deletions packages/headless/src/lessons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,18 +90,21 @@ export function useCreateLesson({
async ({
tutorId,
ruleId,
slotId,
start,
duration,
}: {
tutorId: number;
ruleId: number;
slotId: number;
start: string;
duration: ILesson.Duration;
}) => {
return await atlas.lesson.create({
tutorId,
duration,
ruleId,
slotId,
start,
});
},
Expand Down
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
46 changes: 40 additions & 6 deletions packages/models/fixtures/db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
users,
ratings,
tutors,
availabilitySlots,
} from "@/index";
import {
IInterview,
Expand All @@ -20,18 +21,17 @@ import {
IRating,
IMessage,
ISession,
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";
import { availabilitySlots } from "@/availabilitySlot";
import { randomUUID } from "crypto";

export async function flush() {
await knex.transaction(async (tx) => {
await availabilitySlots.builder(tx).del();
await topics.builder(tx).userTopics.del();
await topics.builder(tx).topics.del();
await messages.builder(tx).del();
Expand All @@ -44,6 +44,7 @@ export async function flush() {
await rules.builder(tx).del();
await ratings.builder(tx).del();
await tutors.builder(tx).del();
await availabilitySlots.builder(tx).del();
await users.builder(tx).del();
});
}
Expand Down Expand Up @@ -102,6 +103,22 @@ export async function rule(payload?: Partial<IRule.CreatePayload>) {
});
}

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");

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 = {
async tutorId(id?: number): Promise<number> {
if (!id) return await tutor().then((tutor) => tutor.id);
Expand All @@ -127,6 +144,10 @@ const or = {
if (!roomId) return await makeRoom();
return roomId;
},
async slotId(id?: number): Promise<number> {
if (!id) return await slot().then((slot) => slot.id);
return id;
},
start(start?: string): string {
if (!start) return faker.date.soon().toISOString();
return start;
Expand Down Expand Up @@ -158,13 +179,18 @@ export async function lesson(
duration: payload?.duration || sample([15, 30]),
price: payload?.price || faker.number.int(500),
rule: await or.ruleId(payload?.rule),
slot: await or.slotId(payload?.slot),
student,
tutor,
tx,
});

if (payload?.canceled)
await lessons.cancel({ canceledBy: tutor, id: data.lesson.id, tx });
await lessons.cancel({
canceledBy: tutor,
ids: [data.lesson.id],
tx,
});

return data;
});
Expand All @@ -176,6 +202,7 @@ export async function interview(payload: Partial<IInterview.CreatePayload>) {
interviewee: await or.tutorId(payload.interviewee),
session: await or.sessionId("interview"),
rule: await or.ruleId(payload.rule),
slot: await or.slotId(payload.slot),
start: or.start(payload.start),
});
}
Expand Down Expand Up @@ -292,7 +319,10 @@ async function makeLessons({
range(0, canceledFutureLessonCount).map(async (i) => {
const info = futureLessons[i];
if (!lesson) throw new Error("invalid future lesson index");
await lessons.cancel({ canceledBy: tutor, id: info.lesson.id });
await lessons.cancel({
canceledBy: tutor,
ids: [info.lesson.id],
});
return info;
})
);
Expand All @@ -302,7 +332,10 @@ async function makeLessons({
range(0, canceledPastLessonCount).map(async (i) => {
const info = pastLessons[i];
if (!info) throw new Error("invalid past lesson index");
await lessons.cancel({ canceledBy: tutor, id: info.lesson.id });
await lessons.cancel({
canceledBy: tutor,
ids: [info.lesson.id],
});
return info;
})
);
Expand Down Expand Up @@ -444,6 +477,7 @@ export default {
flush,
topic,
rule,
slot,
room: makeRoom,
rating: makeRating,
message: makeMessage,
Expand Down
11 changes: 11 additions & 0 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,6 +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,
},
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 @@ -125,6 +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,
},
note: { type: "TEXT", default: null },
level: { type: "INT", default: null },
status: { type: "interview_status", default: "pending" },
Expand Down
Loading
Loading