Skip to content

Commit

Permalink
Replace date logic with weekly-availabilities, improve typescript setup
Browse files Browse the repository at this point in the history
This codebase is still a complete mess, but this at least takes it a little closer to sanity
  • Loading branch information
domdomegg committed Apr 7, 2024
1 parent b8fc399 commit 3aecdc6
Show file tree
Hide file tree
Showing 22 changed files with 311 additions and 574 deletions.
71 changes: 34 additions & 37 deletions frontend/algorithm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,20 +11,19 @@ import {
useGlobalConfig
} from "@airtable/blocks/ui";
import React, { useCallback, useEffect, useState } from "react";
import { MINUTES_IN_UNIT } from "../lib/constants";
import { getDateFromCoord } from "../lib/date";
import { parseTimeAvString, unparseNumber } from "../lib/parse";
import { Cohort, PersonType, SchedulerInput, solve } from "../lib/scheduler";
import { Cohort, PersonType as SchedulerPersonType, SchedulerInput, solve } from "../lib/scheduler";
import { wait } from "../lib/util";
import { PersonBlob } from "./components/Blobs";
import { CollapsibleSection } from "./components/CollapsibleSection";
import { Preset } from "./index";
import { PersonType as SetupPersonType } from "./setup";
import { ViewCohort } from "./view";
import { parseIntervals, toDate } from "weekly-availabilities";
import { MINUTES_IN_UNIT } from "../lib/constants";
import { expectInteger } from "../lib/expectInteger";

interface SolutionProps {
solution: Cohort[],
personTypes: PersonType[],
personTypes: SchedulerPersonType[],
}

const Solution = ({ solution, personTypes }: SolutionProps) => {
Expand Down Expand Up @@ -94,7 +93,7 @@ const Solution = ({ solution, personTypes }: SolutionProps) => {
onClick={() => setViewedCohortIndex(i)}
>
{Object.keys(cohort.people).map((personTypeName) => {
const personType = personTypes.find((pt: PersonType) => pt.name === personTypeName);
const personType = personTypes.find((pt: SchedulerPersonType) => pt.name === personTypeName);
if (!personType) throw new Error('Person type in cohort but not configured');

const avgSize = 100 * (personType.min + personType.max) / 2;
Expand All @@ -105,11 +104,11 @@ const Solution = ({ solution, personTypes }: SolutionProps) => {
className="flex flex-wrap gap-1 px-1"
style={{ flex: `0 1 ${avgSize}%`, minWidth: '80px' }}
>
{cohort.people[personTypeName].map((personID) => {
{cohort.people[personTypeName]!.map((personId) => {
return (
<PersonBlob
key={personID}
name={personType.people.find((person) => person.id === personID)?.name}
key={personId}
name={personType.people.find((person) => person.id === personId)?.name!}
/>
);
})}
Expand Down Expand Up @@ -177,7 +176,7 @@ const Solution = ({ solution, personTypes }: SolutionProps) => {
</div>
<Dialog.CloseButton />
</div>
<ViewCohort cohort={solution[viewedCohortIndex]} />
<ViewCohort cohort={solution[viewedCohortIndex]!} />
</Dialog>
)}
{isAcceptDialogOpen && (
Expand Down Expand Up @@ -205,22 +204,19 @@ const Solution = ({ solution, personTypes }: SolutionProps) => {
onClick={async () => {
setSaving(true);
const records = solution.map((cohort) => {
const start = cohort.time;
const end = cohort.time + preset.lengthOfMeeting / MINUTES_IN_UNIT;
const fields: Record<FieldId, unknown> = {
[preset.cohortsTableStartDateField]: getDateFromCoord(
unparseNumber(start),
[preset.cohortsTableStartDateField!]: toDate(
cohort.startTime,
new Date(preset.firstWeek)
),
[preset.cohortsTableEndDateField]: getDateFromCoord(
unparseNumber(end),
[preset.cohortsTableEndDateField!]: toDate(
cohort.endTime,
new Date(preset.firstWeek)
),
};

for (const personTypeID of Object.keys(preset.personTypes)) {
const personType = preset.personTypes[personTypeID];
fields[personType.cohortsTableField] = cohort.people[personType.name].map((id) => ({ id }));
for (const personType of Object.values(preset.personTypes)) {
fields[personType.cohortsTableField!] = cohort.people[personType.name]!.map((id) => ({ id }));
}
return { fields };
});
Expand Down Expand Up @@ -270,11 +266,9 @@ const AlgorithmPage = () => {
useEffect(() => {
const generateGrandInput = async () => {
try {
const personTypes: PersonType[] = [];

for (const key of Object.keys(preset.personTypes)) {
const personType: SetupPersonType = preset.personTypes[key];
const personTypes: SchedulerPersonType[] = [];

for (const personType of Object.values(preset.personTypes)) {
const table = personType.sourceTable
? base.getTableByIdIfExists(personType.sourceTable)
: null;
Expand Down Expand Up @@ -312,10 +306,15 @@ const AlgorithmPage = () => {
max: personType.howManyTypePerCohort[1],
people: peopleRecords.map((record) => {
try {
const timeAvMins = parseIntervals(record.getCellValueAsString(personType.timeAvField!));
return {
id: record.id,
name: record.getCellValueAsString(table.primaryField.id),
timeAv: parseTimeAvString(record.getCellValueAsString(personType.timeAvField!)),
timeAvMins,
timeAvUnits: timeAvMins.map(([s, e]) => [
expectInteger(s / MINUTES_IN_UNIT, 'Expected time availability to be aligned to 30 minute blocks'),
expectInteger(e / MINUTES_IN_UNIT, 'Expected time availability to be aligned to 30 minute blocks')
]),
howManyCohorts:
typeof personType.howManyCohortsPerType === "string"
? record.getCellValue(personType.howManyCohortsPerType) as number
Expand All @@ -332,7 +331,7 @@ const AlgorithmPage = () => {
});
}
setGrandInput({
lengthOfMeeting: preset.lengthOfMeeting / MINUTES_IN_UNIT,
lengthOfMeetingMins: preset.lengthOfMeeting,
personTypes,
});
} catch (err) {
Expand All @@ -354,18 +353,17 @@ const AlgorithmPage = () => {
useEffect(() => {
if (solution) {
const checkSolution = (async () => {
if (!solution) return;
return solution.every((cohort) => {
const t = cohort.time;

if (!solution || !grandInput) return;
return solution.every((cohort) => {
return Object.keys(cohort.people).every((personTypeName) => {
const personType = grandInput?.personTypes.find(
const personType = grandInput.personTypes.find(
(pt) => pt.name === personTypeName
);

return cohort.people[personTypeName].every((personID) => {
const timeAv = personType?.people?.find((person) => person.id === personID)?.timeAv ?? [];
return grandInput && timeAv.some(([b, e]) => b <= t && t <= e - grandInput.lengthOfMeeting);

// Check that for every person in the cohort, they have a slot in their time availability which includes the cohort
return cohort.people[personTypeName]!.every((personID) => {
const timeAv = personType?.people?.find((person) => person.id === personID)?.timeAvMins ?? [];
return timeAv.some(([b, e]) => b <= cohort.startTime && cohort.startTime <= e - grandInput.lengthOfMeetingMins);
});
});
});
Expand All @@ -381,7 +379,7 @@ const AlgorithmPage = () => {
})
);
}
}, [grandInput, grandInput?.lengthOfMeeting, grandInput?.personTypes, solution]);
}, [grandInput, grandInput?.lengthOfMeetingMins, grandInput?.personTypes, solution]);

return (
<div>
Expand All @@ -402,7 +400,6 @@ const AlgorithmPage = () => {
return (
<CollapsibleSection
key={personType.name}
size="xsmall"
title={`${personType.name} (${personType.people.length})`}
>
<div className="flex flex-wrap w-full bg-white border p-1 rounded-sm">
Expand Down
9 changes: 7 additions & 2 deletions frontend/components/Blobs.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
import React from "react";

export const PersonBlob = (props) => {
interface BlobProps {
name: string,
className?: string,
}

export const PersonBlob: React.FC<BlobProps> = (props) => {
return (
<div className={"rounded px-1 bg-blue-50 " + props.className}>
{props.name}
</div>
);
};

export const CohortBlob = (props) => {
export const CohortBlob: React.FC<BlobProps> = (props) => {
return (
<div className={"rounded px-1 bg-purple-50 " + props.className}>
{props.name}
Expand Down
13 changes: 9 additions & 4 deletions frontend/components/CollapsibleSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,18 @@ import { Heading, Icon } from "@airtable/blocks/ui";
import { Disclosure, Transition } from "@headlessui/react";
import React from "react";

export const CollapsibleSection = (props) => {
interface CollapsibleSectionProps {
defaultOpen?: boolean,
title: string,
}

export const CollapsibleSection: React.FC<CollapsibleSectionProps> = (props) => {
return (
<Disclosure defaultOpen={props.startOpen}>
<Disclosure defaultOpen={props.defaultOpen ?? false}>
{({ open }) => (
<div>
<Disclosure.Button>
<Heading size={props.size} className="flex">
<Heading size="xsmall" className="flex">
<div className="flex items-center">
<Icon
name="caret"
Expand All @@ -27,7 +32,7 @@ export const CollapsibleSection = (props) => {
leaveFrom="transform scale-100 opacity-100"
leaveTo="transform scale-95 opacity-0"
>
<Disclosure.Panel className={"pl-6 " + props.className}>
<Disclosure.Panel className={"pl-6"}>
{props.children}
</Disclosure.Panel>
</Transition>
Expand Down
7 changes: 6 additions & 1 deletion frontend/components/FixedNumberInput.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import { Icon } from "@airtable/blocks/ui";
import React from "react";

export const FixedNumberInput = ({ value, increment, decrement, render }) => {
export const FixedNumberInput = ({ value, increment, decrement, render }: {
value: number,
increment: () => void,
decrement: () => void,
render: (value: number) => React.ReactNode,
}) => {
return (
<div className="flex items-center">
<div className="flex flex-col">
Expand Down
53 changes: 25 additions & 28 deletions frontend/components/TimeAvWidget.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,16 @@
import React from "react";
import { MINUTE_IN_HOUR, MINUTES_IN_UNIT } from "../../lib/constants";
import { prettyPrintTime } from "../../lib/format";
import { Interval, unparseNumber } from "../../lib/parse";
import { isWithin } from "../../lib/util";
import { MINUTES_IN_UNIT } from "../../lib/constants";
import { Interval, WeeklyTime, format, isInInterval } from "weekly-availabilities";

const dayLabels = {
0: "Mon",
1: "Tue",
2: "Wed",
3: "Thu",
4: "Fri",
5: "Sat",
6: "Sun",
};
const dayLabels = [
"Mon",
"Tue",
"Wed",
"Thu",
"Fri",
"Sat",
"Sun",
];

/**
* @returns [0, 1, 2, 3, ..., n - 1]
Expand All @@ -28,44 +26,43 @@ export interface TimeAvWidgetProps {
}

export function TimeAvWidget({ availabilities }: TimeAvWidgetProps) {
const multiplier = MINUTE_IN_HOUR / MINUTES_IN_UNIT;

const allNumbers = zeroUntilN(7 * 24 * multiplier);
const unitsPerHour = 60 / MINUTES_IN_UNIT;
const unitIndexes = zeroUntilN(7 * 24 * unitsPerHour);

const cellHeight = 2;
const leftColumnWidth = 12;
const labelFreq = 2;
const unitsPerLabel = 2;

return (
<div>
<div className="flex">
<div className={"w-" + leftColumnWidth}></div>
<div className="grid w-full text-sm grid-cols-7">
{zeroUntilN(7).map((d) => {
{dayLabels.map((d) => {
return (
<div key={d} className="h-8 mx-auto">
{dayLabels[d]}
{d}
</div>
);
})}
</div>
</div>
<div className="flex text-xs">
<div className={"w-" + leftColumnWidth}>
{zeroUntilN(24 * multiplier + 1)
{zeroUntilN(24 * unitsPerHour + 1)
.filter((value) => {
return value % labelFreq == 0;
return value % unitsPerLabel == 0;
})
.map((time) => {
.map((unitTime) => {
return (
<div
key={time}
key={unitTime}
className={
"flex justify-end px-1 h-" + labelFreq * cellHeight
"flex justify-end px-1 h-" + unitsPerLabel * cellHeight
}
>
<div className="-translate-y-2">
{prettyPrintTime(unparseNumber(time))}
{format((unitTime * MINUTES_IN_UNIT) as WeeklyTime).slice(1)}
</div>
</div>
);
Expand All @@ -76,12 +73,12 @@ export function TimeAvWidget({ availabilities }: TimeAvWidgetProps) {
className="grid grid-flow-col border-t border-l border-solid border-gray-800"
style={{ gridTemplateRows: "repeat(48, minmax(0, 1fr))" }}
>
{allNumbers.map((number) => {
{unitIndexes.map((number) => {
const relevantAvailabilities = availabilities.filter(a =>
a.intervals.some(interval => isWithin(interval, number))
a.intervals.some(interval => isInInterval(interval, number * MINUTES_IN_UNIT as WeeklyTime))
)

const isEven = Math.floor(number) % labelFreq == 0;
const isEven = Math.floor(number) % unitsPerLabel == 0;
return (
<div
key={number}
Expand Down
Loading

0 comments on commit 3aecdc6

Please sign in to comment.