Skip to content

Commit a1544ae

Browse files
committed
feat: gender quota highlighting
1 parent d055bcd commit a1544ae

File tree

12 files changed

+211
-153
lines changed

12 files changed

+211
-153
lines changed

README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
This project aims to allow you to model multi-list works council election representation outcomes given the latest revision of the works constitution act.
2+
3+
We implement the dhondt (aka greatest divisors) method to determine seat distribution for the popular vote, and to measure the gender quotas to determine any additional seat distribution that is needed.
4+
5+
It is likely this still does not work in several edge cases, so please seek a union representative, labor lawyer, etc for proper legal answers to questions about the works constitution act elections.
6+
7+
18
![Demo app with columns](public/demo.png)
29
## Setup Requirements
310
This requires node 18 and `pnpm`

src/components/App/App.css

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ form,
6262

6363
.input-control {
6464
display: flex;
65-
flex-direction: column;
65+
/* flex-direction: column; */
6666
& > label {
6767
width: 100%;
6868
}

src/components/App/App.tsx

Lines changed: 25 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { tallyAndValidateLists } from "../../lib/listData";
1313

1414
import "./App.css";
1515
import { getColor } from "src/utilities/getColor";
16+
import { ElectionResults } from "../Election/ElectionResults";
1617

1718
const CandidateLists = lazy(() =>
1819
import("../CandidateLists").then(({ CandidateLists }) => ({
@@ -127,20 +128,28 @@ export function App({ setLocale }: Props) {
127128

128129
return (
129130
<div className="App">
130-
<h1>
131-
<FormattedMessage id="title" />
132-
<span>
133-
<button onClick={() => setLocale("en")}>en</button>
134-
<button onClick={() => setLocale("de")}>de</button>
135-
<button onClick={() => setLocale("ar")}>ar</button>
136-
</span>
137-
</h1>
138131
<div id="tray">
139132
<div id="workplace-info">
133+
<h1>
134+
<FormattedMessage id="title" />
135+
</h1>
136+
<div>
137+
<button onClick={() => setLocale("en")}>en</button>
138+
<button onClick={() => setLocale("de")}>de</button>
139+
<button onClick={() => setLocale("ar")}>ar</button>
140+
</div>
140141
<h2>
141142
<FormattedMessage id="workplaceInfo.header" />
142143
</h2>
143-
<WorkplaceInfo actions={actions} data={data} />
144+
<WorkplaceInfo
145+
actions={actions}
146+
data={{
147+
numMen: data.numMen,
148+
numNonBinary: data.numNonBinary,
149+
numWomen: data.numWomen,
150+
}}
151+
/>
152+
<ElectionResults data={data} />
144153
</div>
145154
<div id="candidate-lists">
146155
<header>
@@ -152,7 +161,7 @@ export function App({ setLocale }: Props) {
152161
padding: "4px 0px",
153162
}}
154163
>
155-
Candidate Lists&nbsp;
164+
<FormattedMessage id="candidateLists.header" />
156165
</span>
157166

158167
<button
@@ -170,17 +179,13 @@ export function App({ setLocale }: Props) {
170179
vertical
171180
</button>
172181
<span id="legend">
173-
<label>Legend</label>
174182
<span
175-
className="legend-block"
176-
style={{
177-
backgroundColor: getColor({ isPopularlyElected: true }),
178-
}}
179-
/>
180-
<span className="legend-label">
181-
182-
Popularly Elected{" "}
183-
</span>
183+
className="legend-block"
184+
style={{
185+
backgroundColor: getColor({ isPopularlyElected: true }),
186+
}}
187+
/>
188+
<span className="legend-label">Popularly Elected </span>
184189
<span
185190
className="legend-block"
186191
style={{

src/components/CandidateLists/CandidateList.tsx

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,21 @@
11
import { SortableContext } from "@dnd-kit/sortable";
22
import classNames from "classnames";
3-
import { genderArray, GenderEnum, Items, ListDataItem, ListItem } from "../../types";
3+
import {
4+
genderArray,
5+
GenderEnum,
6+
Items,
7+
ListDataItem,
8+
ListItem,
9+
} from "../../types";
410
import { Container } from "../Container";
511
import { SortableItem } from "../SortableItem/SortableItem";
612
import { DroppableContainer } from "../DroppableContainer";
713

814
import { UniqueIdentifier } from "@dnd-kit/core";
915

10-
import { SortingStrategy } from "@dnd-kit/sortable";
16+
import { SortingStrategy } from "@dnd-kit/sortable";
1117

12-
import styles from './CandidateList.module.css'
18+
import styles from "./CandidateList.module.css";
1319
import { ListVotesForm } from "./ListVotesForm";
1420

1521
type CandidateListProps = {
@@ -24,12 +30,12 @@ type CandidateListProps = {
2430
totalWorkers: number;
2531
getMemberIndex: (id: UniqueIdentifier) => number;
2632
handleAddItem: (listId: UniqueIdentifier) => void;
27-
handleRemoveColumn: (listId: UniqueIdentifier) => void
28-
handleRemoveItem: (index: number, listId: UniqueIdentifier) => void
29-
handleRenameList: (listId: UniqueIdentifier, name: string) => void
33+
handleRemoveColumn: (listId: UniqueIdentifier) => void;
34+
handleRemoveItem: (index: number, listId: UniqueIdentifier) => void;
35+
handleRenameList: (listId: UniqueIdentifier, name: string) => void;
3036
wrapperStyle({ index }: { index: number }): React.CSSProperties;
31-
scrollable?: boolean,
32-
columns?: number
37+
scrollable?: boolean;
38+
columns?: number;
3339
};
3440

3541
export function CandidateList({
@@ -49,7 +55,7 @@ export function CandidateList({
4955
handleRemoveItem,
5056
handleRenameList,
5157
getMemberIndex,
52-
wrapperStyle
58+
wrapperStyle,
5359
}: CandidateListProps) {
5460
let data: ListDataItem | null = listData;
5561
return (
@@ -69,7 +75,8 @@ export function CandidateList({
6975
const status = {
7076
isPopularlyElected: data?.popularlyElectedMembers.includes(index),
7177
isOverflowElected: data?.overflowElectedMembers.includes(index),
72-
isGenderQuotaElected: data?.genderOverflowElectedMembers.includes(index)
78+
isGenderQuotaElected:
79+
data?.genderOverflowElectedMembers.includes(index),
7380
};
7481

7582
return (

src/components/Draggable/Draggable.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ export const Draggable = forwardRef<HTMLButtonElement, Props>(
5757
style={
5858
{
5959
...style,
60-
'--translate-x': `${transform?.x ?? 0}px`,
60+
// '--translate-x': `${transform?.x ?? 0}px`,
6161
'--translate-y': `${transform?.y ?? 0}px`,
6262
} as React.CSSProperties
6363
}

src/components/Droppable/Droppable.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ interface Props {
1313

1414
export function Droppable({children, id, dragging}: Props) {
1515
const {isOver, setNodeRef} = useDroppable({
16-
id,
16+
id
1717
});
1818

1919
return (
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import { Tdata } from "../../types";
2+
3+
import { FormattedMessage } from "react-intl";
4+
5+
export function ElectionResults({ data }: { data: Tdata }) {
6+
const {
7+
totalWorkers,
8+
worksCouncilSize,
9+
minorityGender,
10+
workplaceGenderQuota,
11+
totalCandidates,
12+
notEnoughSeats,
13+
moreVotesThanWorkers,
14+
suggestMoreSeats,
15+
suggestedSeats,
16+
totalVotes,
17+
} = data;
18+
19+
const minorityGenderHasMembers = data[`num${minorityGender}`] > 0;
20+
21+
const numMinorityWorkers = workplaceGenderQuota[minorityGender];
22+
const isQuotaDisabled =
23+
worksCouncilSize > 1 && minorityGenderHasMembers && numMinorityWorkers;
24+
25+
return (
26+
<div>
27+
<h2>Election Stats</h2>
28+
<div className="input-control">
29+
<label htmlFor="totalWorkers">
30+
<FormattedMessage
31+
id="label.workerCount"
32+
defaultMessage={"Worker Count"}
33+
/>
34+
</label>
35+
<span className="cell" id="totalWorkers">
36+
{totalWorkers}
37+
</span>
38+
</div>
39+
<div className="input-control">
40+
<label htmlFor="numSeats">
41+
<FormattedMessage
42+
id="label.worksCouncilSeatCount"
43+
defaultMessage={"Works Council Seats"}
44+
/>
45+
</label>
46+
<span className="cell" id="numSeats">
47+
{worksCouncilSize}
48+
</span>
49+
</div>
50+
{/* TODO: how does minority gender work with single member works councils? */}
51+
52+
<div className="input-control">
53+
<label htmlFor="workplaceGenderQuota">
54+
<a href="https://en.wikipedia.org/wiki/D%27Hondt_method">D'Hondt</a>
55+
&nbsp;Gender Quota
56+
</label>
57+
</div>
58+
<div
59+
className={
60+
!data.isGenderQuotaAchieved && isQuotaDisabled ? "error" : "success"
61+
}
62+
id="workplaceGenerQuota"
63+
>
64+
{isQuotaDisabled ? (
65+
<>
66+
{/* todo: replace with ICU pluralisation */}
67+
{`There ${
68+
data.isGenderQuotaAchieved
69+
? numMinorityWorkers > 1
70+
? "are"
71+
: "is"
72+
: "should be"
73+
} at least ${numMinorityWorkers} works council member${
74+
numMinorityWorkers > 1 ? "s" : ""
75+
} for the minority gender (${minorityGender})
76+
`}
77+
</>
78+
) : (
79+
`Minority gender does not apply ${
80+
worksCouncilSize < 5
81+
? "when the works council has less than 5 seats"
82+
: "when the minority proportion is so small that they aren't allocated any seats"
83+
} `
84+
)}
85+
</div>
86+
<div className="input-control">
87+
<label htmlFor="totalVotes">Total Candidates</label>
88+
<span className="cell">{totalCandidates}</span>
89+
</div>
90+
{!notEnoughSeats && suggestMoreSeats && (
91+
<div className="warning">
92+
Note: For a more optimal and fair election, you should have at least{" "}
93+
{suggestedSeats} candidates between available lists.
94+
</div>
95+
)}
96+
97+
{notEnoughSeats && (
98+
<div className="error">
99+
Note: You don't have enough choices (${totalCandidates}) between the
100+
lists below to form the ${worksCouncilSize} person works council board
101+
</div>
102+
)}
103+
104+
{!!totalVotes && (
105+
<div className="input-control">
106+
<label htmlFor="totalVotes">Total Votes</label>
107+
<span className="cell">{totalVotes}</span>
108+
</div>
109+
)}
110+
{moreVotesThanWorkers && (
111+
<div className="error">
112+
You have more votes ({totalVotes}) than workers ({totalWorkers})
113+
</div>
114+
)}
115+
</div>
116+
);
117+
}

0 commit comments

Comments
 (0)