Skip to content

Write initial stories #2

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

Merged
merged 11 commits into from
May 10, 2024
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
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ on:
push:
tags:
- v*.**
pull_request:
branches: main

jobs:
build_and_deploy_job:
Expand Down
1 change: 1 addition & 0 deletions Bylaws/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
Sample](https://drive.google.com/file/d/0B7aFfmk4wQfbWVRPQzFkU1ZHLVE/view?resourcekey=0-Ldj_CiYRwRyAUyaGo6ThXg)

Alternatives:
- [REI](https://www.rei.com/assets/about-rei/governance/rei-bylaws/live.pdf)
- [Boston Techcollective
Cooperative](https://drive.google.com/file/d/0B7aFfmk4wQfbbWxoYjNPVEVGbjg/view?resourcekey=0-ejmpEn33O9JxmiOXgF-tMA)
- [Sample with Two Classes at page
Expand Down
126 changes: 126 additions & 0 deletions astro/src/business-logic/patronage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import { parseISO, addDays } from 'date-fns';

// One per day of the week
type DayOfWeek = 0 | 1 | 2 | 3 | 4 | 5 | 6;
type TenureSchedule = Record<DayOfWeek, number>;
export const noWorkSchedule: TenureSchedule = [0,0,0,0,0,0,0];
// TODO: it's unclear in the bylaws when Tenure is allocated; what happens for
// partial weeks at beginning/end of the year? This code allows for a tenure
// schedule array.
export const fullWorkWeekSchedule: TenureSchedule = [0, 2, 1, 1, 1, 0, 0];

export interface PatronageEvents {
addMember: { name: string; tenureSchedule: TenureSchedule };
tenureRate: { name: string; tenureSchedule: TenureSchedule };
patronage: { name: string; additional: number; comment: string };
}

export type PatronageEvent<T extends keyof PatronageEvents = keyof PatronageEvents> =
& { date: Date }
& {
[P in keyof PatronageEvents]: { type: P } & PatronageEvents[P]
}[T];

export type PatronageEntry = {
memberDate: Date;
weeksYTD: number;
tenureSchedule: TenureSchedule;
tenure: number;
tenureYTD: number;
patronageScheduleYTD: number;
patronageYTD: number;
};

export type PatronageSnapshot = {
date: Date;
details: Record<string, PatronageEntry>;
}

function normalizeDate(date: Date | string): Date {
return typeof date === 'string' ? parseISO(date) : date;
}

function newPatronageEntry(memberDate: Date): PatronageEntry {
return {
memberDate,
weeksYTD: 0,
tenureSchedule: noWorkSchedule,
tenure: 0,
tenureYTD: 0,
patronageScheduleYTD: 0,
patronageYTD: 0,
};
}

export class PatronageRecord {
private events: PatronageEvent[];
constructor(events?: PatronageEvent[]) {
this.events = events ? [...events] : [];
}

add<const P extends keyof PatronageEvents>(date: Date | string, type: P, data: PatronageEvents[P]): this {
date = normalizeDate(date);
const after = this.events.findIndex(e => e.date > date);
const newEvent = { date, type, ...data } as PatronageEvent;
if (after === -1)
this.events.push(newEvent);
else
this.events.splice(after, 0, newEvent);
return this;
}

getDetailsAt(date: Date | string): PatronageSnapshot {
date = normalizeDate(date);
const results: Record<string, PatronageEntry> = {};
if (!this.events[0]) return { date, details: results };
let nextEvent: PatronageEvent | undefined;
for (let current = this.events[0].date, nextEventIndex = 0; current <= date; current = addDays(current, 1)) {
// reset from end of year
if (current.getMonth() === 0 && current.getDate() === 1)
for (const entry of Object.values(results)) {
entry.patronageScheduleYTD = 0;
entry.patronageYTD = 0;
entry.tenureYTD = 0;
}

// process events
while ((nextEvent = this.events[nextEventIndex]) && nextEvent.date <= current) {
nextEventIndex++;
switch (nextEvent.type) {
case 'addMember':
const newMember = newPatronageEntry(current);
newMember.tenureSchedule = nextEvent.tenureSchedule;
results[nextEvent.name] = newMember;
break;
case 'patronage':
results[nextEvent.name]!.patronageScheduleYTD += nextEvent.additional;
break;
case 'tenureRate':
results[nextEvent.name]!.tenureSchedule = nextEvent.tenureSchedule;
break;
}
}

// process tenure
const dayOfWeek = current.getDay() as DayOfWeek;
for (const entry of Object.values(results)) {
if (dayOfWeek === 6 /* Saturday */)
entry.weeksYTD += 1;
entry.tenure += entry.tenureSchedule[dayOfWeek];
entry.tenureYTD += entry.tenureSchedule[dayOfWeek];
}
}

for (const entry of Object.values(results)) {
const base = entry.tenureYTD + entry.patronageScheduleYTD;
entry.patronageYTD = base + Math.min(entry.tenure, base * 2);
}
return { date, details: results };
}

get allEvents() { return [...this.events]; }

clone(): PatronageRecord {
return new PatronageRecord(this.events);
}
}
82 changes: 82 additions & 0 deletions astro/src/components/patronage/point-in-time.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { compareAsc, format } from 'date-fns';
import type { PatronageSnapshot } from "../../business-logic/patronage";
const { amber, blue, cyan, emerald, orange, purple, rose } = (await import('tailwindcss/colors')).default;

const colorIndices = [
500,
900,
100,
700,
300,
] as const;
type ColorIndices = typeof colorIndices[number];

const colorMaps: { [P in ColorIndices]: string }[] = [
amber,
emerald,
blue,
orange,
cyan,
purple,
rose
];

const colors = (Array(colorIndices.length * colorMaps.length).fill(0).map((_, index) => {
const colorRange = colorMaps[index % colorMaps.length]!;
return colorRange[colorIndices[index % colorIndices.length]!];
}))

export function PointInTime({ data: { date, details: data}, membersOrder }: { data: PatronageSnapshot; membersOrder?: string[] }) {
const allEntries = Object.entries(data);
const order = membersOrder ?? allEntries.sort((a, b) => compareAsc(a[1].memberDate, b[1].memberDate)).map(([name]) => name);
return (
<>
<span>{format(date, 'MMM d, yyyy')}</span>
<div className="flex gap-1">
{order.map((name, index) =>
<div className="h-4" key={name} style={{ background: colors[index], flexBasis: 0, flexGrow: data[name]?.patronageYTD }}></div>
)}
</div>
<details>
<summary className="cursor-pointer">
<div className="inline-flex gap-1 flex-wrap">
{order.map((name, index) =>
<div key={name}>
<span className="inline-block h-4 w-4" style={{ background: colors[index] }}></span>
{' '}{name}
</div>
)}
</div>
</summary>
<table className="w-full text-center">
<thead>
<tr>
<th>Name</th>
<th>Member Date</th>
<th>Weeks YTD</th>
<th>Tenure (YTD/total)</th>
<th>Patronage Schedule (YTD)</th>
<th>Patronage (YTD)</th>
</tr>
</thead>
<tbody>
{order.map((name, index) =>
{
const current = data[name]!;
return <tr key={name}>
<td><span className="inline-block h-4 w-4" style={{ background: colors[index] }}></span>
{' '}{name}</td>
<td>{format(current.memberDate, 'MMM d, yyyy')}</td>
<td>{current.weeksYTD.toFixed(0)}</td>
<td>{current.tenureYTD.toFixed(0)} / {current.tenure.toFixed(0)}</td>
<td>{current.patronageScheduleYTD.toFixed(0)}</td>
<td>{current.patronageYTD.toFixed(0)}</td>
</tr>;
}
)}
</tbody>
</table>
</details>
</>
)
}
34 changes: 31 additions & 3 deletions astro/src/components/sets/documents/legal-styles.module.css
Original file line number Diff line number Diff line change
@@ -1,7 +1,35 @@
@import url(./md-styles.module.css);

.container {
composes: container from './md-styles.module.css';
.container :global h1 {
@apply text-lg font-bold text-center;
font-variant: small-caps;
}

.container :global h2 {
@apply font-bold text-center;
counter-increment: article;
font-variant: small-caps;
}

.container :global a {
@apply text-blue-700 dark:text-blue-400 underline;
}
.container :global p {
@apply my-4;
}
.container :global ol {
@apply my-4;
--indentSize: 4rem;
counter-reset: item;
margin-left: var(--indentSize);
}
.container :global li {
@apply my-4;
}
.container :global ul {
@apply my-4 ml-6 list-disc;
}
.container :global blockquote {
@apply my-4 px-4 border-l-emerald-500 border-l-4;
}

.container :global h2:before {
Expand Down
17 changes: 9 additions & 8 deletions astro/src/components/sets/documents/md-styles.module.css
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@

.container :global h1 {
@apply text-lg font-bold text-center;
@apply text-2xl font-bold mt-8 mb-4;
font-variant: small-caps;
}

.container :global h2 {
@apply font-bold text-center;
counter-increment: article;
@apply text-xl font-bold mt-8 mb-4;
font-variant: small-caps;
}

.container :global h3 {
@apply text-lg font-bold mt-8 mb-4;
font-variant: small-caps;
}

Expand All @@ -17,10 +21,7 @@
@apply my-4;
}
.container :global ol {
@apply my-4;
--indentSize: 4rem;
counter-reset: item;
margin-left: var(--indentSize);
@apply my-4 ml-6 list-decimal;
}
.container :global li {
@apply my-4;
Expand All @@ -29,5 +30,5 @@
@apply my-4 ml-6 list-disc;
}
.container :global blockquote {
@apply my-4 px-4 border-l-emerald-500 border-l-4;
@apply my-4 px-4 border-l-slate-500 border-l-4;
}
12 changes: 12 additions & 0 deletions astro/src/content/config.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// Import utilities from `astro:content`
import { z, defineCollection } from "astro:content";

// Define a `type` and `schema` for each collection
const documentsCollection = defineCollection({
type: 'content',
Expand All @@ -8,7 +9,18 @@ const documentsCollection = defineCollection({
description: z.string(),
})
});

const storiesCollection = defineCollection({
type: 'content',
schema: z.object({
title: z.string(),
description: z.string(),
order: z.number(),
})
});

// Export a single `collections` object to register your collection(s)
export const collections = {
documents: documentsCollection,
stories: storiesCollection,
};
9 changes: 7 additions & 2 deletions astro/src/content/documents/bylaws.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ https://drive.google.com/file/d/0B7aFfmk4wQfbWVRPQzFkU1ZHLVE/view?resourcekey=0-
responsible for any tax liability incurred as a result of Patronage with the
Cooperative, as defined in <Patronage />. Each Member shall indemnify and
forever hold harmless the Cooperative from any claims of any kind arising out
of their patronage or their Membership of the Cooperative.
of their Patronage or their Membership of the Cooperative.

9. Record of Members. A record of the Members and their full names, addresses,
and, if required for tax reporting purposes, social security or tax
Expand Down Expand Up @@ -732,6 +732,9 @@ https://drive.google.com/file/d/0B7aFfmk4wQfbWVRPQzFkU1ZHLVE/view?resourcekey=0-
* 4 if at least 24 hours were worked for the Cooperative.
* 5 if at least 32 hours were worked for the Cooperative.

> TODO: Make it clear that tenuer is earned at the completion of 1 hour,
> 8 hours, etc. rather than at the end of the week.

8. "Patronage" shall be calculated per Member within a fiscal year as the
sum of the following:

Expand Down Expand Up @@ -769,7 +772,7 @@ https://drive.google.com/file/d/0B7aFfmk4wQfbWVRPQzFkU1ZHLVE/view?resourcekey=0-
amounts of Patronage may be allocated to Members.
2. The Patronage Schedule shall have an effective date no earlier than 120
days after approval by the Board or a committee appointed by the board to
handle patronage.
handle Patronage.
3. Within 30 days of the approval of a new Patronage Schedule, the President
or Secretary shall cause notice to be given to the Members including the
new Patronage Schedule.
Expand Down Expand Up @@ -810,6 +813,8 @@ https://drive.google.com/file/d/0B7aFfmk4wQfbWVRPQzFkU1ZHLVE/view?resourcekey=0-
2. Patronage Dividends may be by qualified or non-qualified written notices
of allocation or a combination of the two.

> TODO: When are dividends allocated/distributed?

6. Distributions of Interest on Member Accounts. The Cooperative may, by a
decision of the Board, pay interest to Members on the Members Accounts. The
interest may be paid in cash or as an additional credit to the Member
Expand Down
Loading
Loading