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;
+  }
+}