diff --git a/data/test.db b/data/test.db index e4f37d29268ba4499279d30c1322d4f75cac4977..2f2f142d93f55b5661c089395d795c6bd40d1d10 100644 Binary files a/data/test.db and b/data/test.db differ diff --git a/web/components/visualization/results-section.js b/web/components/visualization/results-section.js index cbee50f3b1aef0488a2e739bacef16c9ae8ba59e..b27d99cc7d7c5e329a7fe4f7400511e4f3f0df2e 100644 --- a/web/components/visualization/results-section.js +++ b/web/components/visualization/results-section.js @@ -1,8 +1,10 @@ // Imports +import * as results from "/services/election/calculate-election-generic.js"; export async function mount(where, round) { const resultComponent = new ResultComponent(round); await resultComponent.mount(where); + await resultComponent.calculateResults(); } class ResultComponent { @@ -29,15 +31,15 @@ class ResultComponent { </div> <div class="control filter"> <label class="radio"> - <input type="radio" name="answer" checked /> + <input type="radio" name="filter" value="partial" checked /> Partiel </label> <label class="radio"> - <input type="radio" name="answer" /> + <input type="radio" name="filter" value="completed" /> Complété </label> <label class="radio"> - <input type="radio" name="answer" /> + <input type="radio" name="filter" value="validated" /> Validé </label> </div> @@ -100,6 +102,7 @@ class ResultComponent { `; this.handleDom(); document.getElementById("areas").click(); + this.calculator = await results.mountCalculator(this.round); } handleDom() { @@ -126,6 +129,13 @@ class ResultComponent { .addEventListener("click", function () { resultHandler.zoomResults(); }); + + let radioButtons = document.getElementsByName("filter"); + for (var i = 0; i < radioButtons.length; i++) { + radioButtons[i].addEventListener("click", (e) => { + this.calculateResults(); + }); + } } zoomMap() { @@ -152,7 +162,7 @@ class ResultComponent { }); } - zoomResults(){ + zoomResults() { let resultHandler = this; document.getElementById("results-section").parentElement.className = "column is-full"; @@ -179,4 +189,9 @@ class ResultComponent { this.handleDom(); } + + async calculateResults() { + let filter = document.querySelector('input[name="filter"]:checked').value; + await this.calculator.calculateResults(filter); + } } diff --git a/web/services/election/calculate-election-generic.js b/web/services/election/calculate-election-generic.js new file mode 100644 index 0000000000000000000000000000000000000000..5a6ccb64a13a37f23e93f157212db4a01fd6b498 --- /dev/null +++ b/web/services/election/calculate-election-generic.js @@ -0,0 +1,424 @@ +import * as Auth from "/services/auth/auth.js"; +import * as ElectionModel from "/services/model/election-model.js"; +import * as RoundModel from "/services/model/round-model.js"; +import * as AreaModel from "/services/model/area-model.js"; +import * as SectionModel from "/services/model/section-model.js"; +import * as DeskModel from "/services/model/desk-model.js"; +import * as PartyModel from "/services/model/party-model.js"; +import * as DeskRoundModel from "/services/model/deskRound-model.js"; +import * as VoteModel from "/services/model/vote-model.js"; +import * as CandidateListModel from "/services/model/candidateList-model.js"; + +export async function mountCalculator(round) { + const directMetropolitanCalculator = new DirectMetropolitanCalculator(round); + await directMetropolitanCalculator.initModel(); + return directMetropolitanCalculator; +} + +class DirectMetropolitanCalculator { + constructor(round, filter) { + this.round = round; + this.ElectionModel = ElectionModel.getElectionModel(); + this.RoundModel = RoundModel.getRoundModel(); + this.AreaModel = AreaModel.getAreaModel(); + this.SectionModel = SectionModel.getSectionModel(); + this.DeskModel = DeskModel.getDeskModel(); + this.PartyModel = PartyModel.getPartyModel(); + this.DeskRoundModel = DeskRoundModel.getDeskRoundModel(); + this.VoteModel = VoteModel.getVoteModel(); + this.CandidateListModel = CandidateListModel.getCandidateListModel(); + } + + async initModel() { + this.ElectionModel.current_user = await Auth.GetUser(); + this.RoundModel.current_user = await Auth.GetUser(); + this.AreaModel.current_user = await Auth.GetUser(); + this.SectionModel.current_user = await Auth.GetUser(); + this.DeskModel.current_user = await Auth.GetUser(); + this.PartyModel.current_user = await Auth.GetUser(); + this.DeskRoundModel.current_user = await Auth.GetUser(); + this.VoteModel.current_user = await Auth.GetUser(); + this.CandidateListModel.current_user = await Auth.GetUser(); + } + + async calculateResults(filter) { + this.CandidateListModel.refreshCandidateLists(); + let calculator = this; + this.filter = filter; + this.deskRounds = await this.DeskRoundModel.getDeskRounds(); + this.deskRounds = this.deskRounds.filter(function (deskRound) { + return deskRound.RoundID === calculator.round.ID; + }); + this.candidateLists = await this.CandidateListModel.getCandidateLists(); + this.candidateLists = this.candidateLists.filter((candidateList) => { + return candidateList.RoundID === calculator.round.ID; + }); + + this.stats = await this.calculateStats(this.deskRounds); + + let flag = true; + switch (this.filter) { + case "partial": + if (this.stats.VotesExpressed === 0) { + this.status = "no_results"; + } else { + this.roundResults = await this.calculateRoundResults(); + this.status = "partial"; + } + break; + case "completed": + flag = true; + this.deskRounds.forEach((deskRound) => { + if (!deskRound.Completed) flag = false; + }); + if (flag) { + this.roundResults = await this.calculateRoundResults(); + this.stats = await this.calculateStats(this.deskRounds); + this.status = "completed"; + } else { + this.roundResults = null; + this.stats = null; + this.status = "incompleted"; + } + break; + case "validated": + flag = true; + this.deskRounds.forEach((deskRound) => { + if (!deskRound.Validated) flag = false; + }); + if (flag) { + this.roundResults = await this.calculateRoundResults(); + this.stats = await this.calculateStats(this.deskRounds); + this.status = "validated"; + } else { + this.roundResults = null; + this.stats = null; + this.status = "not validated"; + } + } + + this.areasResults = await this.calculateAreasResults(); + console.log(this); + } + + async calculateRoundResults() { + let partiesIDToKeep = []; + this.candidateLists.forEach((candidateList) => { + partiesIDToKeep.push(candidateList.PartyID); + }); + partiesIDToKeep = partiesIDToKeep.filter(function (item, index) { + return partiesIDToKeep.indexOf(item) >= index; + }); + + let parties = await this.PartyModel.getParties(); + parties = parties.filter((party) => partiesIDToKeep.includes(party.ID)); + parties.forEach((party) => { + party.VoiceNumber = 0; + let currentParty = party; + this.candidateLists.forEach((candidateList) => { + if (candidateList.PartyID == currentParty.ID) { + currentParty.VoiceNumber = candidateList.Votes.reduce( + (voiceNumber, vote) => { + return voiceNumber + vote.VoiceNumber; + }, + currentParty.VoiceNumber + ); + } + }); + party = currentParty; + }); + parties.sort((a, b) => { + return b.VoiceNumber - a.VoiceNumber; + }); + return parties; + } + + async calculateAreasResults() { + let calculator = this; + this.AreaModel.refreshAreas(); + let areas = await this.AreaModel.getAreas(); + areas = areas.filter(function (area) { + return area.ElectionID == calculator.round.ElectionID; + }); + + let areasCalculated = []; + for (let i in areas) { + areasCalculated.push(await this.calculateAreaResults(areas[i])); + } + areas = areasCalculated; + return areas; + } + + async calculateAreaResults(area) { + let deskRounds = []; + for (let i in area.Sections) { + let section = await this.SectionModel.getSection(area.Sections[i].ID); + for (let j in section.Desks) { + for (let k in this.deskRounds) { + if (section.Desks[j].ID == this.deskRounds[k].DeskID) + deskRounds.push(this.deskRounds[k]); + } + } + } + + let candidateListToKeep = this.candidateLists.filter((candidateList) => { + return candidateList.AreaID === area.ID; + }); + + let sections = []; + for (let i in area.Sections) { + sections.push( + await this.calculateSectionResults( + area.Sections[i], + candidateListToKeep.map((a) => ({ ...a })) + ) + ); + } + area.Sections = sections; + + area.stats = await this.calculateStats(deskRounds); + + let flag = true; + switch (this.filter) { + case "partial": + if (area.stats.VotesExpressed === 0) { + area.status = "no_results"; + return; + } + area.status = "partial"; + break; + case "completed": + flag = true; + deskRounds.forEach((deskRound) => { + if (!deskRound.Completed) flag = false; + }); + if (flag) { + area.status = "completed"; + } else { + area.status = "incompleted"; + return; + } + break; + case "validated": + flag = true; + deskRounds.forEach((deskRound) => { + if (!deskRound.Validated) flag = false; + }); + if (flag) { + area.status = "validated"; + } else { + area.status = "not validated"; + return; + } + } + + candidateListToKeep.forEach((candidateList) => { + candidateList.VoiceNumber = candidateList.Votes.reduce( + (voiceNumber, vote) => { + return voiceNumber + vote.VoiceNumber; + }, + 0 + ); + candidateList.Percentage = + (candidateList.VoiceNumber / area.stats.VotesExpressed) * 100; + }); + area.candidateLists = candidateListToKeep; + area.candidateLists.sort((a, b) => { + return b.VoiceNumber - a.VoiceNumber; + }); + + area.Electeds = this.getElecteds(area); + + area.candidateLists.sort((a, b) => { + return b.VoiceNumber - a.VoiceNumber; + }); + + return area; + } + + async calculateSectionResults(section, candidateLists) { + section = await this.SectionModel.getSection(section.ID); + let deskRounds = []; + for (let i in section.Desks) { + for (let j in this.deskRounds) { + if (section.Desks[i].ID == this.deskRounds[j].DeskID) + deskRounds.push(this.deskRounds[j]); + } + } + + section.stats = await this.calculateStats(deskRounds); + let flag = true; + switch (this.filter) { + case "partial": + if (section.stats.VotesExpressed === 0) { + section.status = "no_results"; + return; + } + section.status = "partial"; + break; + case "completed": + flag = true; + deskRounds.forEach((deskRound) => { + if (!deskRound.Completed) flag = false; + }); + if (flag) { + section.status = "completed"; + } else { + section.status = "incompleted"; + return; + } + break; + case "validated": + flag = true; + deskRounds.forEach((deskRound) => { + if (!deskRound.Validated) flag = false; + }); + if (flag) { + section.status = "validated"; + } else { + section.status = "not validated"; + return; + } + } + + candidateLists.forEach((candidateList) => { + candidateList.Votes = candidateList.Votes.filter((vote) => { + return deskRounds + .map((deskRound) => deskRound.ID) + .includes(vote.DeskRoundID); + }); + candidateList.VoiceNumber = candidateList.Votes.reduce( + (voiceNumber, vote) => { + return voiceNumber + vote.VoiceNumber; + }, + 0 + ); + candidateList.Percentage = + (candidateList.VoiceNumber / section.stats.VotesExpressed) * 100; + }); + section.candidateLists = candidateLists; + section.candidateLists.sort((a, b) => { + return b.VoiceNumber - a.VoiceNumber; + }); + return section; + } + + async calculateStats(deskRounds) { + let subscribed = 0; + let blank = 0; + let nullVote = 0; + let totalVotes = 0; + let VotesExpressed = 0; + + for (let i in deskRounds) { + let desk = await this.DeskModel.getDesk(deskRounds[i].DeskID); + subscribed += desk.Subscribed; + deskRounds[i].Votes.forEach((vote) => { + totalVotes += vote.VoiceNumber; + if (vote.Blank) blank += vote.VoiceNumber; + else if (vote.NullVote) nullVote += vote.VoiceNumber; + else VotesExpressed += vote.VoiceNumber; + }); + } + return { + Abstention: Number( + ((subscribed - totalVotes) / subscribed) * 100 + ).toFixed(2), + Blank: Number((blank / totalVotes) * 100).toFixed(2), + NullVote: Number((nullVote / totalVotes) * 100).toFixed(2), + VotesExpressed: VotesExpressed, + }; + } + + getElecteds(area) { + let electeds = []; + + // order candidates by rank and remove refused or removed candidates + area.candidateLists.forEach((candidateList) => { + candidateList.Candidates.sort(function (a, b) { + return a.Rank - b.Rank; + }); + for (let i = 0; i < candidateList.Candidates.length; i++) { + if ( + candidateList.Candidates[i].Refused || + candidateList.Candidates[i].Removed + ) { + candidateList.Candidates.splice(i, 1); + } + } + }); + + // première étape + let seatForFirst = parseInt(area.SeatNumber / 2); + if ((parseInt(area.seatNumber) / 2) % 2 != 0) seatForFirst += 1; + electeds = electeds.concat( + area.candidateLists[0].Candidates.splice(0, seatForFirst) + ); + + // deuxième étape + let leftSeats = area.SeatNumber - seatForFirst; + let electoralQuotien = area.stats.VotesExpressed / leftSeats; + + area.candidateLists.forEach((candidateList) => { + let seatsAttributed = parseInt( + candidateList.VoiceNumber / electoralQuotien + ); + candidateList.SeatsAttributed = seatsAttributed; + leftSeats -= seatsAttributed; + if (seatsAttributed > 0) { + electeds = electeds.concat( + candidateList.Candidates.splice(0, seatsAttributed) + ); + } + }); + + // //troisème étape + var day = new Date(); + while (leftSeats > 0) { + area.candidateLists.forEach((candidateList) => { + candidateList.Average = + candidateList.VoiceNumber / + (parseInt(candidateList.SeatsAttributed) + 1); + }); + area.candidateLists.sort(function (a, b) { + return b.Average - a.Average; + }); + if (area.candidateLists[0].Average === area.candidateLists[1].Average) { + if (area.candidateLists[1].vote > area.candidateLists[0].vote) + [area.candidateLists[0], area.candidateLists[1]] = [ + area.candidateLists[1], + area.candidateLists[0], + ]; + if (area.candidateLists[0].vote == area.candidateLists[1].vote) { + if ( + area.candidateLists[0].Candidates[0].Birthdate == "" || + area.candidateLists[1].Candidates[0].Birthdate == "" + ) { + area.errorAgeAverage = true; + } + ageCandidate1 = ageCount( + day, + new Date(area.candidateLists[0].Candidates[0].Birthdate) + ); + ageCandidate2 = ageCount( + day, + new Date(area.candidateLists[1].Candidates[0].Birthdate) + ); + if (ageCandidate2 > ageCandidate1) { + [area.candidateLists[0], area.candidateLists[1]] = [ + area.candidateLists[1], + area.candidateLists[0], + ]; + } + } + } + electeds = electeds.concat( + area.candidateLists[0].Candidates.splice(0, 1) + ); + area.candidateLists[0].SeatsAttributed += 1; + leftSeats -= 1; + } + + return electeds; + } +}