Explorar o código

Add Feature: Planning Poker

helioguardabaxo %!s(int64=4) %!d(string=hai) anos
pai
achega
fa3d117372

+ 27 - 0
client/components/cards/cardDate.js

@@ -354,3 +354,30 @@ class VoteEndDate extends CardDate {
   }
 }
 VoteEndDate.register('voteEndDate');
+
+class PokerEndDate extends CardDate {
+  onCreated() {
+    super.onCreated();
+    const self = this;
+    self.autorun(() => {
+      self.date.set(moment(self.data().getPokerEnd()));
+    });
+  }
+  classes() {
+    const classes = 'end-date' + ' ';
+    return classes;
+  }
+  showDate() {
+    return this.date.get().format('l LT');
+  }
+  showTitle() {
+    return `${TAPi18n.__('card-end-on')} ${this.date.get().format('LLLL')}`;
+  }
+
+  events() {
+    return super.events().concat({
+      'click .js-edit-date': Popup.open('editPokerEndDate'),
+    });
+  }
+}
+PokerEndDate.register('pokerEndDate');

+ 229 - 0
client/components/cards/cardDetails.jade

@@ -243,6 +243,205 @@ template(name="cardDetails")
             i.fa.fa-thumbs-down
           | {{_ 'vote-against'}}
 
+    if getPokerQuestion
+      hr
+      .poker-title
+        div.flex
+          h3
+            i.fa.fa-thumbs-up
+            | {{_ 'poker-question'}}
+          if getPokerEnd
+            +pokerEndDate
+        div.flex
+          .poker-result
+            if expiredPoker
+              unless ($and currentBoard.isPublic pokerAllowNonBoardMembers )
+                .card-label.card-label-gray  {{ pokerCount }} {{_ 'r-of' }} {{ currentBoard.activeMembers.length }}
+      if showPlanningPokerButtons
+        .poker-result
+          .poker-deck
+            .poker-card
+              span.inner.js-poker.js-poker-vote-one(class="{{#if $eq pokerState 'one'}}poker-voted{{/if}}") {{_ 'poker-one'}}
+            if $eq pokerState "one"
+              i.fa.fa-check
+          .poker-deck
+            .poker-card
+              span.inner.js-poker.js-poker-vote-two(class="{{#if $eq pokerState 'two'}}poker-voted{{/if}}") {{_ 'poker-two'}}
+            if $eq pokerState "two"
+              i.fa.fa-check
+          .poker-deck
+            .poker-card
+              span.inner.js-poker.js-poker-vote-three(class="{{#if $eq pokerState 'three'}}poker-voted{{/if}}") {{_ 'poker-three'}}
+            if $eq pokerState "three"
+              i.fa.fa-check
+          .poker-deck
+            .poker-card
+              span.inner.js-poker.js-poker-vote-five(class="{{#if $eq pokerState 'five'}}poker-voted{{/if}}") {{_ 'poker-five'}}
+            if $eq pokerState "five"
+              i.fa.fa-check
+          .poker-deck
+            .poker-card
+              span.inner.js-poker.js-poker-vote-eight(class="{{#if $eq pokerState 'eight'}}poker-voted{{/if}}") {{_ 'poker-eight'}}
+            if $eq pokerState "eight"
+              i.fa.fa-check
+          .poker-deck
+            .poker-card
+              span.inner.js-poker.js-poker-vote-thirteen(class="{{#if $eq pokerState 'thirteen'}}poker-voted{{/if}}") {{_ 'poker-thirteen'}}
+            if $eq pokerState "thirteen"
+              i.fa.fa-check
+          .poker-deck
+            .poker-card
+              span.inner.js-poker.js-poker-vote-twenty(class="{{#if $eq pokerState 'twenty'}}poker-voted{{/if}}") {{_ 'poker-twenty'}}
+            if $eq pokerState "twenty"
+              i.fa.fa-check
+          .poker-deck
+            .poker-card
+              span.inner.js-poker.js-poker-vote-forty(class="{{#if $eq pokerState 'forty'}}poker-voted{{/if}}") {{_ 'poker-forty'}}
+            if $eq pokerState "forty"
+              i.fa.fa-check
+          .poker-deck
+            .poker-card
+              span.inner.js-poker.js-poker-vote-one-hundred(class="{{#if $eq pokerState 'oneHundred'}}poker-voted{{/if}}") {{_ 'poker-oneHundred'}}
+            if $eq pokerState "oneHundred"
+              i.fa.fa-check
+          .poker-deck
+            .poker-card
+              span.inner.js-poker.js-poker-vote-unsure(class="{{#if $eq pokerState 'unsure'}}poker-voted{{/if}}") {{_ 'poker-unsure'}}
+            if $eq pokerState "unsure"
+              i.fa.fa-check
+
+        if currentUser.isBoardAdmin
+          button.card-details-blue.js-poker-finish(class="{{#if $eq voteState false}}poker-voted{{/if}}") {{_ 'poker-finish'}}
+
+      if expiredPoker
+        .poker-table
+          .poker-table-side-left
+            .poker-table-heading-left
+              .poker-table-row
+                .poker-table-cell
+                .poker-table-cell
+                  | {{_ 'poker-result-votes' }}
+                .poker-table-cell.poker-table-cell-who
+                  | {{_ 'poker-result-who' }}
+            .poker-table-body
+              .poker-table-row
+                .poker-table-cell
+                  button.card-details-gray.js-poker.poker-card-result(class="{{#if $eq pokerWinner 1}}winner{{else}}loser{{/if}}") {{_ 'poker-one'}}
+                .poker-table-cell {{ pokerCountOne }}
+                .poker-table-cell.poker-table-cell-who
+                  .poker-result
+                      each m in pokerMemberOne
+                        a.name
+                          +userAvatar(userId=m._id noRemove=true)
+
+              .poker-table-row
+                .poker-table-cell
+                  button.card-details-gray.js-poker.poker-card-result(class="{{#if $eq pokerWinner 2}}winner{{else}}loser{{/if}}") {{_ 'poker-two'}}
+                .poker-table-cell {{ pokerCountTwo }}
+                .poker-table-cell.poker-table-cell-who
+                  .poker-result
+                      each m in pokerMemberTwo
+                        a.name
+                          +userAvatar(userId=m._id noRemove=true)
+
+              .poker-table-row
+                .poker-table-cell
+                  button.card-details-gray.js-poker.poker-card-result(class="{{#if $eq pokerWinner 3}}winner{{else}}loser{{/if}}") {{_ 'poker-three'}}
+                .poker-table-cell {{ pokerCountThree }}
+                .poker-table-cell.poker-table-cell-who
+                  .poker-result
+                      each m in pokerMemberThree
+                        a.name
+                          +userAvatar(userId=m._id noRemove=true)
+
+              .poker-table-row
+                .poker-table-cell
+                  button.card-details-gray.js-poker.poker-card-result(class="{{#if $eq pokerWinner 5}}winner{{else}}loser{{/if}}") {{_ 'poker-five'}}
+                .poker-table-cell {{ pokerCountFive }}
+                .poker-table-cell.poker-table-cell-who
+                  .poker-result
+                      each m in pokerMemberFive
+                        a.name
+                          +userAvatar(userId=m._id noRemove=true)
+
+              .poker-table-row
+                .poker-table-cell
+                  button.card-details-gray.js-poker.poker-card-result(class="{{#if $eq pokerWinner 8}}winner{{else}}loser{{/if}}") {{_ 'poker-eight'}}
+                .poker-table-cell {{ pokerCountEight }}
+                .poker-table-cell.poker-table-cell-who
+                  .poker-result
+                      each m in pokerMemberEight
+                        a.name
+                          +userAvatar(userId=m._id noRemove=true)
+
+          .poker-table-side-right
+            .poker-table-heading-right
+              .poker-table-row
+                .poker-table-cell
+                .poker-table-cell
+                  | {{_ 'poker-result-votes' }}
+                .poker-table-cell.poker-table-cell-who
+                  | {{_ 'poker-result-who' }}
+            .poker-table-body
+              .poker-table-row
+                .poker-table-cell
+                  button.card-details-gray.js-poker.poker-card-result(class="{{#if $eq pokerWinner 13}}winner{{else}}loser{{/if}}") {{_ 'poker-thirteen'}}
+                .poker-table-cell {{ pokerCountThirteen }}
+                .poker-table-cell.poker-table-cell-who
+                  .poker-result
+                      each m in pokerMemberThirteen
+                        a.name
+                          +userAvatar(userId=m._id noRemove=true)
+
+              .poker-table-row
+                .poker-table-cell
+                  button.card-details-gray.js-poker.poker-card-result(class="{{#if $eq pokerWinner 20}}winner{{else}}loser{{/if}}") {{_ 'poker-twenty'}}
+                .poker-table-cell {{ pokerCountTwenty }}
+                .poker-table-cell.poker-table-cell-who
+                  .poker-result
+                      each m in pokerMemberTwenty
+                        a.name
+                          +userAvatar(userId=m._id noRemove=true)
+
+              .poker-table-row
+                .poker-table-cell
+                  button.card-details-gray.js-poker.poker-card-result(class="{{#if $eq pokerWinner 40}}winner{{else}}loser{{/if}}") {{_ 'poker-forty'}}
+                .poker-table-cell {{ pokerCountForty }}
+                .poker-table-cell.poker-table-cell-who
+                  .poker-result
+                      each m in pokerMemberForty
+                        a.name
+                          +userAvatar(userId=m._id noRemove=true)
+
+              .poker-table-row
+                .poker-table-cell
+                  button.card-details-gray.js-poker.poker-card-result(class="{{#if $eq pokerWinner 100}}winner{{else}}loser{{/if}}") {{_ 'poker-oneHundred'}}
+                .poker-table-cell {{ pokerCountOneHundred }}
+                .poker-table-cell.poker-table-cell-who
+                  .poker-result
+                      each m in pokerMemberOneHundred
+                        a.name
+                          +userAvatar(userId=m._id noRemove=true)
+
+              .poker-table-row
+                .poker-table-cell
+                  button.card-details-gray.js-poker.poker-card-result(class="{{#if $eq pokerWinner 'unsure'}}winner{{else}}loser{{/if}}") {{_ 'poker-unsure'}}
+                .poker-table-cell {{ pokerCountUnsure }}
+                .poker-table-cell.poker-table-cell-who
+                  .poker-result
+                      each m in pokerMemberUnsure
+                        a.name
+                          +userAvatar(userId=m._id noRemove=true)
+
+        if currentUser.isBoardAdmin
+          div.estimation-add
+            button.card-details-red.js-poker-replay(class="{{#if $eq voteState false}}voted{{/if}}") {{_ 'poker-replay'}}
+          div.estimation-add
+            button.js-poker-estimation
+              i.fa.fa-plus
+              | {{_ 'set-estimation'}}
+            input(type=text,autofocus value=getPokerEstimation,id="pokerEstimation")
+
     //- XXX We should use "editable" to avoid repetiting ourselves
     if canModifyCard
       unless currentUser.isWorker
@@ -362,6 +561,10 @@ template(name="cardDetailsActionsPopup")
           a.js-start-voting
             i.fa.fa-thumbs-up
             | {{_ 'card-edit-voting'}}
+        li
+          a.js-start-planning-poker
+            i.fa.fa-thumbs-up
+            | {{_ 'card-edit-planning-poker'}}
         if currentUser.isBoardAdmin
           li
             a.js-custom-fields
@@ -621,3 +824,29 @@ template(name="negativeVoteMembersPopup")
           span.full-name
             = m.profile.fullname
             | (<span class="username">{{ m.username }}</span>)
+
+template(name="deletePokerPopup")
+  p {{_ "poker-delete-pop"}}
+  button.js-confirm.negate.full(type="submit") {{_ 'delete'}}
+
+template(name="cardStartPlanningPokerPopup")
+  form.edit-poker-question
+    .fields
+      .check-div
+        a.flex(class="{{#if getPokerQuestion}}is-disabled{{else}}js-toggle-poker-allow-non-members{{/if}}")
+          .materialCheckBox#poker-allow-non-members(name="poker-allow-non-members" class="{{#if pokerAllowNonBoardMembers}}is-checked{{/if}}")
+          span {{_ 'allowNonBoardMembers'}}
+      .check-div.flex
+        i.fa.fa-hourglass-end
+        a.js-end-date
+          span
+            | {{_ 'card-end'}}
+            unless getPokerEnd
+              i.fa.fa-plus
+        if getPokerEnd
+          +pokerEndDate
+
+    button.primary.js-submit {{_ 'save'}}
+    if getPokerQuestion
+      if currentUser.isBoardAdmin
+        button.js-remove-poker.negate.wide.right {{_ 'delete'}}

+ 174 - 0
client/components/cards/cardDetails.js

@@ -140,6 +140,15 @@ BlazeComponent.extendComponent({
     );
   },
 
+  showPlanningPokerButtons() {
+    const card = this.currentData();
+    return (
+      (currentUser.isBoardMember() ||
+        (currentUser && card.pokerAllowNonBoardMembers())) &&
+      !card.expiredPoker()
+    );
+  },
+
   onRendered() {
     if (Meteor.settings.public.CARD_OPENED_WEBHOOK_ENABLED) {
       // Send Webhook but not create Activities records ---
@@ -407,6 +416,80 @@ BlazeComponent.extendComponent({
           }
           this.data().setVote(Meteor.userId(), newState);
         },
+        'click .js-poker'(e) {
+          let newState = null;
+          if ($(e.target).hasClass('js-poker-vote-one')) {
+            newState = 'one';
+            this.data().setPoker(Meteor.userId(), newState);
+          }
+          if ($(e.target).hasClass('js-poker-vote-two')) {
+            newState = 'two';
+            this.data().setPoker(Meteor.userId(), newState);
+          }
+          if ($(e.target).hasClass('js-poker-vote-three')) {
+            newState = 'three';
+            this.data().setPoker(Meteor.userId(), newState);
+          }
+          if ($(e.target).hasClass('js-poker-vote-five')) {
+            newState = 'five';
+            this.data().setPoker(Meteor.userId(), newState);
+          }
+          if ($(e.target).hasClass('js-poker-vote-eight')) {
+            newState = 'eight';
+            this.data().setPoker(Meteor.userId(), newState);
+          }
+          if ($(e.target).hasClass('js-poker-vote-thirteen')) {
+            newState = 'thirteen';
+            this.data().setPoker(Meteor.userId(), newState);
+          }
+          if ($(e.target).hasClass('js-poker-vote-twenty')) {
+            newState = 'twenty';
+            this.data().setPoker(Meteor.userId(), newState);
+          }
+          if ($(e.target).hasClass('js-poker-vote-forty')) {
+            newState = 'forty';
+            this.data().setPoker(Meteor.userId(), newState);
+          }
+          if ($(e.target).hasClass('js-poker-vote-one-hundred')) {
+            newState = 'oneHundred';
+            this.data().setPoker(Meteor.userId(), newState);
+          }
+          if ($(e.target).hasClass('js-poker-vote-unsure')) {
+            newState = 'unsure';
+            this.data().setPoker(Meteor.userId(), newState);
+          }
+        },
+        'click .js-poker-finish'(e) {
+          if ($(e.target).hasClass('js-poker-finish')) {
+            e.preventDefault();
+            const now = moment().format('YYYY-MM-DD HH:mm');
+            this.data().setPokerEnd(now);
+          }
+        },
+
+        'click .js-poker-replay'(e) {
+          if ($(e.target).hasClass('js-poker-replay')) {
+            e.preventDefault();
+            this.currentCard = this.currentData();
+            this.currentCard.replayPoker();
+            this.data().unsetPokerEnd();
+            this.data().unsetPokerEstimation();
+          }
+        },
+        'click .js-poker-estimation'(event) {
+          event.preventDefault();
+
+          const ruleTitle = this.find('#pokerEstimation').value;
+          if (ruleTitle !== undefined && ruleTitle !== '') {
+            this.find('#pokerEstimation').value = '';
+
+            if (ruleTitle) {
+              this.data().setPokerEstimation(parseInt(ruleTitle, 10));
+            } else {
+              this.data().setPokerEstimation('');
+            }
+          }
+        },
       },
     ];
   },
@@ -477,6 +560,7 @@ Template.cardDetailsActionsPopup.events({
   'click .js-labels': Popup.open('cardLabels'),
   'click .js-attachments': Popup.open('cardAttachments'),
   'click .js-start-voting': Popup.open('cardStartVoting'),
+  'click .js-start-planning-poker': Popup.open('cardStartPlanningPoker'),
   'click .js-custom-fields': Popup.open('cardCustomFields'),
   'click .js-received-date': Popup.open('editCardReceivedDate'),
   'click .js-start-date': Popup.open('editCardStartDate'),
@@ -976,6 +1060,96 @@ BlazeComponent.extendComponent({
   }
 }.register('editVoteEndDatePopup'));
 
+BlazeComponent.extendComponent({
+  onCreated() {
+    this.currentCard = this.currentData();
+    this.pokerQuestion = new ReactiveVar(this.currentCard.pokerQuestion);
+  },
+
+  events() {
+    return [
+      {
+        'click .js-end-date': Popup.open('editPokerEndDate'),
+        'submit .edit-poker-question'(evt) {
+          evt.preventDefault();
+          const pokerQuestion = true;
+          const allowNonBoardMembers = $('#poker-allow-non-members').hasClass(
+            'is-checked',
+          );
+          const endString = this.currentCard.getPokerEnd();
+
+          this.currentCard.setPokerQuestion(
+            pokerQuestion,
+            allowNonBoardMembers,
+          );
+          if (endString) {
+            this.currentCard.setPokerEnd(endString);
+          }
+          Popup.close();
+        },
+        'click .js-remove-poker': Popup.afterConfirm('deletePoker', () => {
+          event.preventDefault();
+          this.currentCard.unsetPoker();
+          Popup.close();
+        }),
+        'click a.js-toggle-poker-allow-non-members'(event) {
+          event.preventDefault();
+          $('#poker-allow-non-members').toggleClass('is-checked');
+        },
+      },
+    ];
+  },
+}).register('cardStartPlanningPokerPopup');
+
+// editPokerEndDatePopup
+(class extends DatePicker {
+  onCreated() {
+    super.onCreated(moment().format('YYYY-MM-DD HH:mm'));
+    this.data().getPokerEnd() && this.date.set(moment(this.data().getPokerEnd()));
+  }
+  events() {
+    return [
+      {
+        'submit .edit-date'(evt) {
+          evt.preventDefault();
+
+          // if no time was given, init with 12:00
+          const time =
+            evt.target.time.value ||
+            moment(new Date().setHours(12, 0, 0)).format('LT');
+
+          const dateString = `${evt.target.date.value} ${time}`;
+          const newDate = moment(dateString, 'L LT', true);
+          if (newDate.isValid()) {
+            // if active poker -  store it
+            if (this.currentData().getPokerQuestion()) {
+              this._storeDate(newDate.toDate());
+              Popup.close();
+            } else {
+              this.currentData().poker = { end: newDate.toDate() }; // set poker end temp
+              Popup.back();
+            }
+          } else {
+            this.error.set('invalid-date');
+            evt.target.date.focus();
+          }
+        },
+        'click .js-delete-date'(evt) {
+          evt.preventDefault();
+          this._deleteDate();
+          Popup.close();
+        },
+      },
+    ];
+  }
+  _storeDate(newDate) {
+    this.card.setPokerEnd(newDate);
+  }
+  _deleteDate() {
+    this.card.unsetPokerEnd();
+  }
+}.register('editPokerEndDatePopup'));
+
 // Close the card details pane by pressing escape
 EscapeActions.register(
   'detailsPane',

+ 128 - 0
client/components/cards/cardDetails.styl

@@ -357,3 +357,131 @@ card-details-color(background, color...)
   display: flex
 .js-show-positive-votes
   cursor: pointer
+
+.poker-voted
+  opacity: .7
+
+.poker-title
+  display: flex
+  justify-content: space-between
+
+  .js-edit-date
+    align-self: baseline
+    margin-left: 5px
+
+.poker-result
+  display: flex
+  flex-flow: row wrap
+.js-show-positive-poker-votes
+  cursor: pointer
+
+.poker-deck
+  display: grid
+  flex-direction: column
+  text-align: center
+
+.poker-card-result
+  width: 32px
+  font-size: 1em
+  font-weight: bold
+  padding: 4px 2px 4px 2px
+  cursor: default
+
+.winner
+  font-weight: bold
+  outline: #2d2d2d solid 2px
+
+.loser
+  opacity: .5
+
+.responsive-table
+  overflow-x: auto
+
+.poker-table
+  display: table
+  width: 100%
+  padding-top: 10px
+
+.poker-table-row
+  display: table-row
+
+.poker-table-heading
+  background-color: #EEE
+  display: table-header-group
+
+.poker-table-cell
+  display: table-cell
+  padding: 0 0 5px 2px
+  border-bottom: 1px solid #d2d0d0
+  text-align: center
+  min-width: 45px
+
+.poker-table-cell-who
+  width: 150px
+  vertical-align: middle
+
+.poker-table-heading-left,
+.poker-table-heading-right
+  display: table-header-group
+  font-weight: bold
+  border-top: 1px solid #808080
+
+@media (max-width: 400px)
+  .poker-table-heading-right
+    display: none
+
+.poker-table-body
+  display: table-row-group
+
+.poker-table-side-left,
+.poker-table-side-right
+  display: inline-block
+
+.poker-table-side-right
+  padding-left: 10px
+
+@media (max-width: 400px)
+  .poker-table-side-right
+    padding-left: 0px
+
+.estimation-add
+  display: block
+  overflow: auto
+  margin-top: 15px
+  margin-bottom: 5px
+  input
+    display: inline-block
+    float: right
+    margin: auto
+    margin-right: 10px
+    width: 100px
+  button
+    display: inline-block
+    float: right
+    margin: auto
+
+.poker-card
+  width:48px
+  height:72px
+  float:left
+  background:#fff
+  border-radius:5px
+  display:table
+  box-sizing:border-box
+  padding:5px
+  margin:3px
+  font-size:20px
+  font-weight: bold
+  text-shadow: #2d2d2d 1px 1px 0
+  box-shadow:0 0 5px #aaaaaa
+  text-align:center
+  position:relative
+  cursor: pointer
+
+  .inner
+    display:table-cell
+    vertical-align:middle
+    border-radius:5px
+    overflow:hidden
+    background-color: #cecece
+

+ 5 - 0
client/components/cards/minicard.jade

@@ -121,6 +121,11 @@ template(name="minicard")
           span.badge-text {{ voteCountPositive }}
           span.badge-icon.fa.fa-thumbs-down(class="{{#if $eq voteState false}}text-red{{/if}}")
           span.badge-text {{ voteCountNegative }}
+      if getPokerQuestion
+        .badge.badge-state-image-only(title=getPokerQuestion)
+          span.badge-icon.fa.fa-check(class="{{#if pokerState}}text-green{{/if}}")
+          if expiredPoker
+            span.badge-text {{ getPokerEstimation }}
       if attachments.count
         .badge
           span.badge-icon.fa.fa-paperclip

+ 22 - 0
i18n/en.i18n.json

@@ -178,6 +178,27 @@
   "vote-against": "against",
   "deleteVotePopup-title": "Delete vote?",
   "vote-delete-pop": "Deleting is permanent. You will lose all actions associated with this vote.",
+  "cardStartPlanningPokerPopup-title": "Start a Planning Poker",
+  "card-edit-planning-poker": "Edit Planning Poker",
+  "editPokerEndDatePopup-title": "Change Planning Poker vote end date",
+  "poker-question": "Planning Poker",
+  "poker-one": "1",
+  "poker-two": "2",
+  "poker-three": "3",
+  "poker-five": "5",
+  "poker-eight": "8",
+  "poker-thirteen": "13",
+  "poker-twenty": "20",
+  "poker-forty": "40",
+  "poker-oneHundred": "100",
+  "poker-unsure": "?",
+  "poker-finish": "Finish",
+  "poker-result-votes": "Votes",
+  "poker-result-who": "Who",
+  "poker-replay": "Replay",
+  "set-estimation": "Set Estimation",
+  "deletePokerPopup-title": "Delete planning poker?",
+  "poker-delete-pop": "Deleting is permanent. You will lose all actions associated with this planning poker.",
   "cardDeletePopup-title": "Delete Card?",
   "cardDetailsActionsPopup-title": "Card Actions",
   "cardLabelsPopup-title": "Labels",
@@ -186,6 +207,7 @@
   "cardTemplatePopup-title": "Create template",
   "cards": "Cards",
   "cards-count": "Cards",
+  "cards-count-one": "Card",
   "casSignIn": "Sign In with CAS",
   "cardType-card": "Card",
   "cardType-linkedCard": "Linked Card",

+ 688 - 0
models/cards.js

@@ -338,6 +338,113 @@ Cards.attachSchema(
       type: Boolean,
       defaultValue: false,
     },
+    poker: {
+      /**
+       * poker object, see below
+       */
+      type: Object,
+      optional: true,
+    },
+    'poker.question': {
+      type: Boolean,
+      defaultValue: false,
+    },
+    'poker.one': {
+      /**
+       * poker card one
+       */
+      type: [String],
+      optional: true,
+      defaultValue: [],
+    },
+    'poker.two': {
+      /**
+       * poker card two
+       */
+      type: [String],
+      optional: true,
+      defaultValue: [],
+    },
+    'poker.three': {
+      /**
+       * poker card three
+       */
+      type: [String],
+      optional: true,
+      defaultValue: [],
+    },
+    'poker.five': {
+      /**
+       * poker card five
+       */
+      type: [String],
+      optional: true,
+      defaultValue: [],
+    },
+    'poker.eight': {
+      /**
+       * poker card eight
+       */
+      type: [String],
+      optional: true,
+      defaultValue: [],
+    },
+    'poker.thirteen': {
+      /**
+       * poker card thirteen
+       */
+      type: [String],
+      optional: true,
+      defaultValue: [],
+    },
+    'poker.twenty': {
+      /**
+       * poker card twenty
+       */
+      type: [String],
+      optional: true,
+      defaultValue: [],
+    },
+    'poker.forty': {
+      /**
+       * poker card forty
+       */
+      type: [String],
+      optional: true,
+      defaultValue: [],
+    },
+    'poker.oneHundred': {
+      /**
+       * poker card oneHundred
+       */
+      type: [String],
+      optional: true,
+      defaultValue: [],
+    },
+    'poker.unsure': {
+      /**
+       * poker card unsure
+       */
+      type: [String],
+      optional: true,
+      defaultValue: [],
+    },
+    'poker.end': {
+      type: Date,
+      optional: true,
+      defaultValue: null,
+    },
+    'poker.allowNonBoardMembers': {
+      type: Boolean,
+      defaultValue: false,
+    },
+    'poker.estimation': {
+      /**
+       * poker estimation value
+       */
+      type: Number,
+      optional: true,
+    },
   }),
 );
 
@@ -1279,6 +1386,191 @@ Cards.helpers({
     return null;
   },
 
+  getPokerQuestion() {
+    if (this.isLinkedCard()) {
+      const card = Cards.findOne({ _id: this.linkedId });
+      if (card === undefined) {
+        return null;
+      } else if (card && card.poker) {
+        return card.poker.question;
+      } else {
+        return null;
+      }
+    } else if (this.isLinkedBoard()) {
+      const board = Boards.findOne({ _id: this.linkedId });
+      if (board === undefined) {
+        return null;
+      } else if (board && board.poker) {
+        return board.poker.question;
+      } else {
+        return null;
+      }
+    } else if (this.poker) {
+      return this.poker.question;
+    } else {
+      return null;
+    }
+  },
+
+  getPokerEstimation() {
+    if (this.poker) {
+      return this.poker.estimation;
+    } else {
+      return null;
+    }
+  },
+
+  getPokerEnd() {
+    if (this.isLinkedCard()) {
+      const card = Cards.findOne({ _id: this.linkedId });
+      if (card === undefined) {
+        return null;
+      } else if (card && card.poker) {
+        return card.poker.end;
+      } else {
+        return null;
+      }
+    } else if (this.isLinkedBoard()) {
+      const board = Boards.findOne({ _id: this.linkedId });
+      if (board === undefined) {
+        return null;
+      } else if (board && board.poker) {
+        return board.poker.end;
+      } else {
+        return null;
+      }
+    } else if (this.poker) {
+      return this.poker.end;
+    } else {
+      return null;
+    }
+  },
+  expiredPoker() {
+    let end = this.getPokerEnd();
+    if (end) {
+      end = moment(end);
+      return end.isBefore(new Date());
+    }
+    return false;
+  },
+  pokerMemberOne() {
+    if (this.poker && this.poker.one)
+      return Users.find({ _id: { $in: this.poker.one } });
+    return [];
+  },
+  pokerMemberTwo() {
+    if (this.poker && this.poker.two)
+      return Users.find({ _id: { $in: this.poker.two } });
+    return [];
+  },
+  pokerMemberThree() {
+    if (this.poker && this.poker.three)
+      return Users.find({ _id: { $in: this.poker.three } });
+    return [];
+  },
+  pokerMemberFive() {
+    if (this.poker && this.poker.five)
+      return Users.find({ _id: { $in: this.poker.five } });
+    return [];
+  },
+  pokerMemberEight() {
+    if (this.poker && this.poker.eight)
+      return Users.find({ _id: { $in: this.poker.eight } });
+    return [];
+  },
+  pokerMemberThirteen() {
+    if (this.poker && this.poker.thirteen)
+      return Users.find({ _id: { $in: this.poker.thirteen } });
+    return [];
+  },
+  pokerMemberTwenty() {
+    if (this.poker && this.poker.twenty)
+      return Users.find({ _id: { $in: this.poker.twenty } });
+    return [];
+  },
+  pokerMemberForty() {
+    if (this.poker && this.poker.forty)
+      return Users.find({ _id: { $in: this.poker.forty } });
+    return [];
+  },
+  pokerMemberOneHundred() {
+    if (this.poker && this.poker.oneHundred)
+      return Users.find({ _id: { $in: this.poker.oneHundred } });
+    return [];
+  },
+  pokerMemberUnsure() {
+    if (this.poker && this.poker.unsure)
+      return Users.find({ _id: { $in: this.poker.unsure } });
+    return [];
+  },
+  pokerState() {
+    const userId = Meteor.userId();
+    let state;
+    if (this.poker) {
+      if (this.poker.one) {
+        state = _.contains(this.poker.one, userId);
+        if (state === true) {
+          return 'one';
+        }
+      }
+      if (this.poker.two) {
+        state = _.contains(this.poker.two, userId);
+        if (state === true) {
+          return 'two';
+        }
+      }
+      if (this.poker.three) {
+        state = _.contains(this.poker.three, userId);
+        if (state === true) {
+          return 'three';
+        }
+      }
+      if (this.poker.five) {
+        state = _.contains(this.poker.five, userId);
+        if (state === true) {
+          return 'five';
+        }
+      }
+      if (this.poker.eight) {
+        state = _.contains(this.poker.eight, userId);
+        if (state === true) {
+          return 'eight';
+        }
+      }
+      if (this.poker.thirteen) {
+        state = _.contains(this.poker.thirteen, userId);
+        if (state === true) {
+          return 'thirteen';
+        }
+      }
+      if (this.poker.twenty) {
+        state = _.contains(this.poker.twenty, userId);
+        if (state === true) {
+          return 'twenty';
+        }
+      }
+      if (this.poker.forty) {
+        state = _.contains(this.poker.forty, userId);
+        if (state === true) {
+          return 'forty';
+        }
+      }
+      if (this.poker.oneHundred) {
+        state = _.contains(this.poker.oneHundred, userId);
+        if (state === true) {
+          return 'oneHundred';
+        }
+      }
+      if (this.poker.unsure) {
+        state = _.contains(this.poker.unsure, userId);
+        if (state === true) {
+          return 'unsure';
+        }
+      }
+    }
+    return null;
+  },
+
   getId() {
     if (this.isLinked()) {
       return this.linkedId;
@@ -1433,6 +1725,101 @@ Cards.helpers({
   voteCount() {
     return this.voteCountPositive() + this.voteCountNegative();
   },
+
+  pokerAllowNonBoardMembers() {
+    if (this.poker) return this.poker.allowNonBoardMembers;
+    return null;
+  },
+  pokerCountOne() {
+    if (this.poker && this.poker.one) return this.poker.one.length;
+    return null;
+  },
+  pokerCountTwo() {
+    if (this.poker && this.poker.two) return this.poker.two.length;
+    return null;
+  },
+  pokerCountThree() {
+    if (this.poker && this.poker.three) return this.poker.three.length;
+    return null;
+  },
+  pokerCountFive() {
+    if (this.poker && this.poker.five) return this.poker.five.length;
+    return null;
+  },
+  pokerCountEight() {
+    if (this.poker && this.poker.eight) return this.poker.eight.length;
+    return null;
+  },
+  pokerCountThirteen() {
+    if (this.poker && this.poker.thirteen) return this.poker.thirteen.length;
+    return null;
+  },
+  pokerCountTwenty() {
+    if (this.poker && this.poker.twenty) return this.poker.twenty.length;
+    return null;
+  },
+  pokerCountForty() {
+    if (this.poker && this.poker.forty) return this.poker.forty.length;
+    return null;
+  },
+  pokerCountOneHundred() {
+    if (this.poker && this.poker.oneHundred) return this.poker.oneHundred.length;
+    return null;
+  },
+  pokerCountUnsure() {
+    if (this.poker && this.poker.unsure) return this.poker.unsure.length;
+    return null;
+  },
+  pokerCount() {
+    return (
+      this.pokerCountOne() +
+      this.pokerCountTwo() +
+      this.pokerCountThree() +
+      this.pokerCountFive() +
+      this.pokerCountEight() +
+      this.pokerCountThirteen() +
+      this.pokerCountTwenty() +
+      this.pokerCountForty() +
+      this.pokerCountOneHundred() +
+      this.pokerCountUnsure()
+    );
+  },
+  pokerWinner() {
+    const pokerListMaps = [];
+    let pokerWinnersListMap = [];
+    if (this.expiredPoker()) {
+      const one = { count: this.pokerCountOne(), pokerCard: 1 };
+      const two = { count: this.pokerCountTwo(), pokerCard: 2 };
+      const three = { count: this.pokerCountThree(), pokerCard: 3 };
+      const five = { count: this.pokerCountFive(), pokerCard: 5 };
+      const eight = { count: this.pokerCountEight(), pokerCard: 8 };
+      const thirteen = { count: this.pokerCountThirteen(), pokerCard: 13 };
+      const twenty = { count: this.pokerCountTwenty(), pokerCard: 20 };
+      const forty = { count: this.pokerCountForty(), pokerCard: 40 };
+      const oneHundred = { count: this.pokerCountOneHundred(), pokerCard: 100 };
+      const unsure = { count: this.pokerCountUnsure(), pokerCard: 'Unsure' };
+      pokerListMaps.push(one);
+      pokerListMaps.push(two);
+      pokerListMaps.push(three);
+      pokerListMaps.push(five);
+      pokerListMaps.push(eight);
+      pokerListMaps.push(thirteen);
+      pokerListMaps.push(twenty);
+      pokerListMaps.push(forty);
+      pokerListMaps.push(oneHundred);
+      pokerListMaps.push(unsure);
+
+      pokerListMaps.sort(function(a, b) {
+        return b.count - a.count;
+      });
+      const max = pokerListMaps[0].count;
+      pokerWinnersListMap = pokerListMaps.filter(task => task.count >= max);
+      pokerWinnersListMap.sort(function(a, b) {
+        return b.pokerCard - a.pokerCard;
+      });
+    }
+    return pokerWinnersListMap[0].pokerCard;
+  },
 });
 
 Cards.mutations({
@@ -1870,6 +2257,279 @@ Cards.mutations({
         };
     }
   },
+
+  setPokerQuestion(question, allowNonBoardMembers) {
+    return {
+      $set: {
+        poker: {
+          question,
+          allowNonBoardMembers,
+          one: [],
+          two: [],
+          three: [],
+          five: [],
+          eight: [],
+          thirteen: [],
+          twenty: [],
+          forty: [],
+          oneHundred: [],
+          unsure: [],
+        },
+      },
+    };
+  },
+  setPokerEstimation(estimation) {
+    return {
+      $set: { 'poker.estimation': estimation },
+    };
+  },
+  unsetPokerEstimation() {
+    return {
+      $unset: { 'poker.estimation': '' },
+    };
+  },
+  unsetPoker() {
+    return {
+      $unset: {
+        poker: '',
+      },
+    };
+  },
+  setPokerEnd(end) {
+    return {
+      $set: { 'poker.end': end },
+    };
+  },
+  unsetPokerEnd() {
+    return {
+      $unset: { 'poker.end': '' },
+    };
+  },
+  setPoker(userId, state) {
+    switch (state) {
+      case 'one':
+        // poker one
+        return {
+          $pull: {
+            'poker.two': userId,
+            'poker.three': userId,
+            'poker.five': userId,
+            'poker.eight': userId,
+            'poker.thirteen': userId,
+            'poker.twenty': userId,
+            'poker.forty': userId,
+            'poker.oneHundred': userId,
+            'poker.unsure': userId,
+          },
+          $addToSet: {
+            'poker.one': userId,
+          },
+        };
+      case 'two':
+        // poker two
+        return {
+          $pull: {
+            'poker.one': userId,
+            'poker.three': userId,
+            'poker.five': userId,
+            'poker.eight': userId,
+            'poker.thirteen': userId,
+            'poker.twenty': userId,
+            'poker.forty': userId,
+            'poker.oneHundred': userId,
+            'poker.unsure': userId,
+          },
+          $addToSet: {
+            'poker.two': userId,
+          },
+        };
+
+      case 'three':
+        // poker three
+        return {
+          $pull: {
+            'poker.one': userId,
+            'poker.two': userId,
+            'poker.five': userId,
+            'poker.eight': userId,
+            'poker.thirteen': userId,
+            'poker.twenty': userId,
+            'poker.forty': userId,
+            'poker.oneHundred': userId,
+            'poker.unsure': userId,
+          },
+          $addToSet: {
+            'poker.three': userId,
+          },
+        };
+
+      case 'five':
+        // poker five
+        return {
+          $pull: {
+            'poker.one': userId,
+            'poker.two': userId,
+            'poker.three': userId,
+            'poker.eight': userId,
+            'poker.thirteen': userId,
+            'poker.twenty': userId,
+            'poker.forty': userId,
+            'poker.oneHundred': userId,
+            'poker.unsure': userId,
+          },
+          $addToSet: {
+            'poker.five': userId,
+          },
+        };
+
+      case 'eight':
+        // poker eight
+        return {
+          $pull: {
+            'poker.one': userId,
+            'poker.two': userId,
+            'poker.three': userId,
+            'poker.five': userId,
+            'poker.thirteen': userId,
+            'poker.twenty': userId,
+            'poker.forty': userId,
+            'poker.oneHundred': userId,
+            'poker.unsure': userId,
+          },
+          $addToSet: {
+            'poker.eight': userId,
+          },
+        };
+
+      case 'thirteen':
+        // poker thirteen
+        return {
+          $pull: {
+            'poker.one': userId,
+            'poker.two': userId,
+            'poker.three': userId,
+            'poker.five': userId,
+            'poker.eight': userId,
+            'poker.twenty': userId,
+            'poker.forty': userId,
+            'poker.oneHundred': userId,
+            'poker.unsure': userId,
+          },
+          $addToSet: {
+            'poker.thirteen': userId,
+          },
+        };
+
+      case 'twenty':
+        // poker twenty
+        return {
+          $pull: {
+            'poker.one': userId,
+            'poker.two': userId,
+            'poker.three': userId,
+            'poker.five': userId,
+            'poker.eight': userId,
+            'poker.thirteen': userId,
+            'poker.forty': userId,
+            'poker.oneHundred': userId,
+            'poker.unsure': userId,
+          },
+          $addToSet: {
+            'poker.twenty': userId,
+          },
+        };
+
+      case 'forty':
+        // poker forty
+        return {
+          $pull: {
+            'poker.one': userId,
+            'poker.two': userId,
+            'poker.three': userId,
+            'poker.five': userId,
+            'poker.eight': userId,
+            'poker.thirteen': userId,
+            'poker.twenty': userId,
+            'poker.oneHundred': userId,
+            'poker.unsure': userId,
+          },
+          $addToSet: {
+            'poker.forty': userId,
+          },
+        };
+
+      case 'oneHundred':
+        // poker one hundred
+        return {
+          $pull: {
+            'poker.one': userId,
+            'poker.two': userId,
+            'poker.three': userId,
+            'poker.five': userId,
+            'poker.eight': userId,
+            'poker.thirteen': userId,
+            'poker.twenty': userId,
+            'poker.forty': userId,
+            'poker.unsure': userId,
+          },
+          $addToSet: {
+            'poker.oneHundred': userId,
+          },
+        };
+
+      case 'unsure':
+        // poker unsure
+        return {
+          $pull: {
+            'poker.one': userId,
+            'poker.two': userId,
+            'poker.three': userId,
+            'poker.five': userId,
+            'poker.eight': userId,
+            'poker.thirteen': userId,
+            'poker.twenty': userId,
+            'poker.forty': userId,
+            'poker.oneHundred': userId,
+          },
+          $addToSet: {
+            'poker.unsure': userId,
+          },
+        };
+
+      default:
+        // Remove pokers
+        return {
+          $pull: {
+            'poker.one': userId,
+            'poker.two': userId,
+            'poker.three': userId,
+            'poker.five': userId,
+            'poker.eight': userId,
+            'poker.thirteen': userId,
+            'poker.twenty': userId,
+            'poker.forty': userId,
+            'poker.oneHundred': userId,
+            'poker.unsure': userId,
+          },
+        };
+    }
+  },
+  replayPoker() {
+    return {
+      $set: {
+        'poker.one': [],
+        'poker.two': [],
+        'poker.three': [],
+        'poker.five': [],
+        'poker.eight': [],
+        'poker.thirteen': [],
+        'poker.twelve': [],
+        'poker.forty': [],
+        'poker.oneHundred': [],
+        'poker.unsure': [],
+      },
+    };
+  },
 });
 
 //FUNCTIONS FOR creation of Activities
@@ -2593,6 +3253,9 @@ if (Meteor.isServer) {
    * @param {string} vote.question the vote question
    * @param {boolean} vote.public show who voted what
    * @param {boolean} vote.allowNonBoardMembers allow all logged in users to vote?
+   * @param {Object} [poker] the poker object
+   * @param {string} poker.question the vote question
+   * @param {boolean} poker.allowNonBoardMembers allow all logged in users to vote?
    * @return_type {_id: string}
    */
   JsonRoutes.add(
@@ -2698,6 +3361,31 @@ if (Meteor.isServer) {
           { $set: { vote: newVote } },
         );
       }
+      if (req.body.hasOwnProperty('poker')) {
+        const newPoker = req.body.poker;
+        newPoker.one = [];
+        newPoker.two = [];
+        newPoker.three = [];
+        newPoker.five = [];
+        newPoker.eight = [];
+        newPoker.thirteen = [];
+        newPoker.twenty = [];
+        newPoker.forty = [];
+        newPoker.oneHundred = [];
+        newPoker.unsure = [];
+        if (!newPoker.hasOwnProperty('allowNonBoardMembers'))
+          newPoker.allowNonBoardMembers = false;
+
+        Cards.direct.update(
+          {
+            _id: paramCardId,
+            listId: paramListId,
+            boardId: paramBoardId,
+            archived: false,
+          },
+          { $set: { poker: newPoker } },
+        );
+      }
       if (req.body.hasOwnProperty('labelIds')) {
         let newlabelIds = req.body.labelIds;
         if (_.isString(newlabelIds)) {