From c0afedd2800bc605d3c7136b87da63057f3bf16f Mon Sep 17 00:00:00 2001 From: Rob Misiorowski Date: Thu, 15 Jun 2017 16:34:13 -0700 Subject: [PATCH 01/38] WIP dispute order form --- js/languages/en-US.json | 12 +++ js/models/order/DisputeOrder.js | 36 ++++++++ .../modals/orderDetail/actionBar.html | 6 ++ .../modals/orderDetail/disputeOrder.html | 39 +++++++++ .../modals/orderDetail/orderDetail.html | 3 +- js/views/modals/orderDetail/ActionBar.js | 67 +++++++++++++++ js/views/modals/orderDetail/DisputeOrder.js | 86 +++++++++++++++++++ js/views/modals/orderDetail/OrderDetail.js | 54 +++++++++++- .../modals/orderDetail/summaryTab/Summary.js | 2 - styles/modules/modals/_orderDetail.scss | 15 +++- 10 files changed, 312 insertions(+), 8 deletions(-) create mode 100644 js/models/order/DisputeOrder.js create mode 100644 js/templates/modals/orderDetail/actionBar.html create mode 100644 js/templates/modals/orderDetail/disputeOrder.html create mode 100644 js/views/modals/orderDetail/ActionBar.js create mode 100644 js/views/modals/orderDetail/DisputeOrder.js diff --git a/js/languages/en-US.json b/js/languages/en-US.json index d100974a9..bb8ba84e9 100644 --- a/js/languages/en-US.json +++ b/js/languages/en-US.json @@ -1087,6 +1087,18 @@ "noteHelperTextDigital": "The buyer will receive a notification containing the file information", "btnCancel": "Cancel", "btnSubmit": "Submit" + }, + "disputeOrderTab": { + "heading": "Dispute Order", + "moderatorLabel": "Moderator", + "reasonLabel": "Reason", + "reasonPlaceholder": "Please explain your reason for opening a dispute…", + "reasonHelperText": "Opening a dispute will notify and invite the moderator into the order to help resolve any conflicts", + "btnCancel": "Cancel", + "btnSubmit": "Submit" + }, + "actionBar": { + "disputeOrderBtn": "Dispute Order" } }, "orderUtil": { diff --git a/js/models/order/DisputeOrder.js b/js/models/order/DisputeOrder.js new file mode 100644 index 000000000..24cf6714b --- /dev/null +++ b/js/models/order/DisputeOrder.js @@ -0,0 +1,36 @@ +import app from '../../app'; +import BaseModel from '../BaseModel'; + +export default class extends BaseModel { + // constructor(attrs = {}, options = {}) { + // super(attrs, options); + // } + + defaults() { + return { + claim: '', + }; + } + + url() { + return app.getServerUrl('ob/opendispute/'); + } + + get idAttribute() { + return 'orderId'; + } + + // validate() { + // const errObj = this.mergeInNestedErrors(); + // if (Object.keys(errObj).length) return errObj; + // return undefined; + // } + + sync(method, model, options) { + if (method === 'create' || method === 'update') { + options.type = 'POST'; + } + + return super.sync(method, model, options); + } +} diff --git a/js/templates/modals/orderDetail/actionBar.html b/js/templates/modals/orderDetail/actionBar.html new file mode 100644 index 000000000..5d17e37ed --- /dev/null +++ b/js/templates/modals/orderDetail/actionBar.html @@ -0,0 +1,6 @@ +<% if (ob.showDisputeOrderButton) { %> + <%= ob.processingButton({ + className: `flex btn clrErr clrBrDec1 clrTOnEmph js-openDispute ${ob.disputeOrderInProgress ? 'processing' : ''}`, + btnText: ob.polyT('orderDetail.actionBar.disputeOrderBtn'), + }) %> +<% } %> \ No newline at end of file diff --git a/js/templates/modals/orderDetail/disputeOrder.html b/js/templates/modals/orderDetail/disputeOrder.html new file mode 100644 index 000000000..c64fb44a6 --- /dev/null +++ b/js/templates/modals/orderDetail/disputeOrder.html @@ -0,0 +1,39 @@ +
+ +
<%= ob.polyT(`orderDetail.disputeOrderTab.heading`) %>
+
+
+
+
+
+
<%= ob.polyT(`orderDetail.disputeOrderTab.moderatorLabel`) %>
+
+
+ <% ob.moderator = {}; ob.moderator.avatarHashes = {} %> +
+
+
Mod God @modgod27
+
+
+
+
+
+ +
+
+ <% if (ob.errors['claim']) print(ob.formErrorTmpl({ errors: ob.errors['claim'] })) %> + +
<%= ob.polyT(`orderDetail.disputeOrderTab.reasonHelperText`) %>
+
+
+
+
+
+ <%= ob.polyT(`orderDetail.fulfillOrderTab.btnCancel`) %> + <%= ob.processingButton({ + className: `btn clrBAttGrad clrBrDec1 clrTOnEmph js-submit ${ob.fulfillingOrder ? 'processing' : ''}`, + btnText: ob.polyT(`orderDetail.fulfillOrderTab.btnSubmit`), + }) %> +
\ No newline at end of file diff --git a/js/templates/modals/orderDetail/orderDetail.html b/js/templates/modals/orderDetail/orderDetail.html index 5859b9321..98afc61bc 100644 --- a/js/templates/modals/orderDetail/orderDetail.html +++ b/js/templates/modals/orderDetail/orderDetail.html @@ -33,6 +33,7 @@

<%= ob.polyT('tabMenuHeading') %>

btnText: 'Accept Order', }) %> +
@@ -52,5 +53,5 @@

<%= ob.polyT('tabMenuHeading') %>

<% } %>
-
+ \ No newline at end of file diff --git a/js/views/modals/orderDetail/ActionBar.js b/js/views/modals/orderDetail/ActionBar.js new file mode 100644 index 000000000..6b66b6be5 --- /dev/null +++ b/js/views/modals/orderDetail/ActionBar.js @@ -0,0 +1,67 @@ +import _ from 'underscore'; +// import { +// fulfillingOrder, +// refundingOrder, +// refundOrder, +// events as orderEvents, +// } from '../../../../utils/order'; +import loadTemplate from '../../../utils/loadTemplate'; +import BaseVw from '../../baseVw'; + +export default class extends BaseVw { + constructor(options = {}) { + super(options); + + if (!options.orderId) { + throw new Error('Please provide the order id.'); + } + + this.orderId = options.orderId; + this._state = { + showDisputeOrderButton: false, + ...options.initialState || {}, + }; + } + + className() { + return 'actionBar gutterV'; + } + + events() { + return { + // 'click .js-fulfillOrder': 'onClickFulfillOrder', + }; + } + + getState() { + return this._state; + } + + setState(state, replace = false, renderOnChange = true) { + let newState; + + if (replace) { + this._state = {}; + } else { + newState = _.extend({}, this._state, state); + } + + if (renderOnChange && !_.isEqual(this._state, newState)) { + this._state = newState; + this.render(); + } + + return this; + } + + render() { + loadTemplate('modals/orderDetail/actionBar.html', (t) => { + this.$el.html(t({ + ...this._state, + // disputeOrderInProgress: fulfillingOrder(this.orderId), + })); + }); + + return this; + } +} diff --git a/js/views/modals/orderDetail/DisputeOrder.js b/js/views/modals/orderDetail/DisputeOrder.js new file mode 100644 index 000000000..ce25709d9 --- /dev/null +++ b/js/views/modals/orderDetail/DisputeOrder.js @@ -0,0 +1,86 @@ +// import { +// fulfillingOrder, +// fulfillOrder, +// events as orderEvents, +// } from '../../../utils/order'; +import loadTemplate from '../../../utils/loadTemplate'; +import BaseVw from '../../baseVw'; + +export default class extends BaseVw { + constructor(options = {}) { + super(options); + + if (!this.model) { + throw new Error('Please provide an DisputeOrder model.'); + } + + // this.listenTo(orderEvents, 'fulfillingOrder', this.onFulfillingOrder); + // this.listenTo(orderEvents, 'fulfillOrderComplete, fulfillOrderFail', + // this.onFulfillOrderAlways); + } + + className() { + return 'disputeOrderTab'; + } + + events() { + return { + 'click .js-backToSummary': 'onClickBackToSummary', + 'click .js-cancel': 'onClickCancel', + 'click .js-submit': 'onClickSubmit', + }; + } + + onClickBackToSummary() { + this.trigger('clickBackToSummary'); + } + + onClickCancel() { + const id = this.model.id; + this.model.reset(); + // restore the id reset blew away + this.model.set({ orderId: id }); + this.render(); + this.trigger('clickCancel'); + } + + onClickSubmit() { + const formData = this.getFormData(); + this.model.set(formData); + this.model.set({}, { validate: true }); + + if (!this.model.validationError) { + // fulfillOrder(this.model.id, this.model.toJSON()); + } + + this.render(); + const $firstErr = this.$('.errorList:first'); + if ($firstErr.length) $firstErr[0].scrollIntoViewIfNeeded(); + } + + // onFulfillingOrder(e) { + // if (e.id === this.model.id) { + // this.$btnSubmit.addClass('processing'); + // this.$btnCancel.addClass('disabled'); + // } + // } + + // onFulfillOrderAlways(e) { + // if (e.id === this.model.id) { + // this.$btnSubmit.removeClass('processing'); + // this.$btnCancel.removeClass('disabled'); + // } + // } + + render() { + loadTemplate('modals/orderDetail/disputeOrder.html', (t) => { + this.$el.html(t({ + ...this.model.toJSON(), + errors: this.model.validationError || {}, + // fulfillingOrder: fulfillingOrder(this.model.id), + })); + }); + + return this; + } +} diff --git a/js/views/modals/orderDetail/OrderDetail.js b/js/views/modals/orderDetail/OrderDetail.js index 121d4efc4..f0dd59ced 100644 --- a/js/views/modals/orderDetail/OrderDetail.js +++ b/js/views/modals/orderDetail/OrderDetail.js @@ -7,12 +7,15 @@ import { events as orderEvents } from '../../../utils/order'; import loadTemplate from '../../../utils/loadTemplate'; import Case from '../../../models/order/Case'; import OrderFulfillment from '../../../models/order/orderFulfillment/OrderFulfillment'; +import DisputeOrder from '../../../models/order/DisputeOrder'; import BaseModal from '../BaseModal'; import ProfileBox from './ProfileBox'; import Summary from './summaryTab/Summary'; import Discussion from './Discussion'; import Contract from './Contract'; import FulfillOrder from './FulfillOrder'; +import DisputeOrderTab from './DisputeOrder'; +import ActionBar from './ActionBar.js'; export default class extends BaseModal { constructor(options = {}) { @@ -22,7 +25,8 @@ export default class extends BaseModal { fetchFailed: false, fetchError: '', }, - initialTab: 'summary', + // initialTab: 'summary', + initialTab: 'disputeOrder', ...options, }; @@ -50,6 +54,11 @@ export default class extends BaseModal { this.listenTo(orderEvents, 'fulfillOrderComplete', () => { if (this.activeTab === 'fulfillOrder') this.selectTab('summary'); }); + this.listenTo(this.model, 'change:state', () => { + if (this.actionBar) { + this.actionBar.setState(this.actionBarButtonState); + } + }); const socket = getSocket(); @@ -362,6 +371,21 @@ export default class extends BaseModal { return view; } + createDisputeOrderTabView() { + const contractType = this.model.get('contract').type; + + const model = new DisputeOrder({ orderId: this.model.id }); + + const view = this.createChild(DisputeOrderTab, { + model, + contractType, + }); + + this.listenTo(view, 'clickBackToSummary clickCancel', () => this.selectTab('summary')); + + return view; + } + setUnreadChatMessagesBadge() { this.$unreadChatMessagesBadge.text(this.getUnreadChatMessagesText()); } @@ -373,6 +397,27 @@ export default class extends BaseModal { return count; } + /** + * Returns whether different action bar buttons should be displayed or not + * based upon the order state. + */ + get actionBarButtonState() { + const orderState = this.model.get('state'); + let showDisputeOrderButton = false; + + if (this.buyerId === app.profile.id) { + showDisputeOrderButton = this.moderatorId && + ['AWAITING_FULFILLMENT', 'PENDING', 'FULFILLED'].indexOf(orderState) > -1; + } else if (this.vendorId === app.profile.id) { + showDisputeOrderButton = this.moderatorId && + ['AWAITING_FULFILLMENT', 'FULFILLED'].indexOf(orderState) > -1; + } + + return { + showDisputeOrderButton, + }; + } + get $unreadChatMessagesBadge() { return this._$unreadChatMessagesBadge || (this._$unreadChatMessagesBadge = this.$('.js-unreadChatMessagesBadge')); @@ -406,6 +451,13 @@ export default class extends BaseModal { if (!state.isFetching && !state.fetchError) { this.selectTab(this.activeTab); + + if (this.actionBar) this.actionBar.remove(); + this.actionBar = this.createChild(ActionBar, { + orderId: this.model.id, + initialState: this.actionBarButtonState, + }); + this.$('.js-actionBarContainer').html(this.actionBar.render().el); } }); diff --git a/js/views/modals/orderDetail/summaryTab/Summary.js b/js/views/modals/orderDetail/summaryTab/Summary.js index ca874ba94..c8c5da70f 100644 --- a/js/views/modals/orderDetail/summaryTab/Summary.js +++ b/js/views/modals/orderDetail/summaryTab/Summary.js @@ -505,8 +505,6 @@ export default class extends BaseVw { const sections = document.createDocumentFragment(); const $sections = $(sections).append(this.fulfilled.render().el); - console.log(`renderin fulfilled: ${this.model.get('state')}`); - // If the order is not complete and this is the buyer, we'll // render a complete order form. if (['FULFILLED', 'RESOLVED'].indexOf(this.model.get('state')) > -1 && diff --git a/styles/modules/modals/_orderDetail.scss b/styles/modules/modals/_orderDetail.scss index e5601aa48..0bc96aa74 100644 --- a/styles/modules/modals/_orderDetail.scss +++ b/styles/modules/modals/_orderDetail.scss @@ -5,8 +5,8 @@ padding: $padSm; .avatar { - width: 24px; - height: 24px; + width: $thumbTn; + height: $thumbTn; float: left; } } @@ -377,14 +377,21 @@ } } - .fulfillOrderTab { + .fulfillOrderTab, .disputeOrderTab { hr { margin-left: $padLg; margin-right: $padLg; } - + .buttonBar { padding: $padMd $padLg $padLg; } } + + .disputeOrderTab { + .avatar { + width: $thumbTn; + height: $thumbTn; + } + } } \ No newline at end of file From eba2fd76aca1a39c15cf31740c2e7295932202a7 Mon Sep 17 00:00:00 2001 From: Rob Misiorowski Date: Fri, 16 Jun 2017 09:53:47 -0700 Subject: [PATCH 02/38] seperated out the order details moderator fragment into its own view --- .../modals/orderDetail/disputeOrder.html | 6 +-- .../modals/orderDetail/modFragment.html | 11 +++++ .../orderDetail/summaryTab/orderComplete.html | 1 - .../orderDetail/summaryTab/orderDetails.html | 6 ++- js/utils/order.js | 8 ++-- js/views/modals/orderDetail/DisputeOrder.js | 42 +++++++++++++++++ js/views/modals/orderDetail/ModFragment.js | 47 +++++++++++++++++++ js/views/modals/orderDetail/OrderDetail.js | 4 ++ .../orderDetail/summaryTab/OrderDetails.js | 46 +++++++++--------- .../modals/orderDetail/summaryTab/Summary.js | 4 +- 10 files changed, 138 insertions(+), 37 deletions(-) create mode 100644 js/templates/modals/orderDetail/modFragment.html create mode 100644 js/views/modals/orderDetail/ModFragment.js diff --git a/js/templates/modals/orderDetail/disputeOrder.html b/js/templates/modals/orderDetail/disputeOrder.html index c64fb44a6..3411a7663 100644 --- a/js/templates/modals/orderDetail/disputeOrder.html +++ b/js/templates/modals/orderDetail/disputeOrder.html @@ -11,11 +11,7 @@
<%= ob.polyT(`orderDetail.disputeOrderTab.moderatorLabel`) %>
- <% ob.moderator = {}; ob.moderator.avatarHashes = {} %> -
-
-
Mod God @modgod27
-
+
diff --git a/js/templates/modals/orderDetail/modFragment.html b/js/templates/modals/orderDetail/modFragment.html new file mode 100644 index 000000000..ff2789401 --- /dev/null +++ b/js/templates/modals/orderDetail/modFragment.html @@ -0,0 +1,11 @@ +<% + let modLink = + `${ob.handle && `@${ob.handle}` || `${ob.peerId.slice(0, ob.maxPeerIdLength)}…`}`; +%> + +
+ <% if (ob.showAvatar) { %> +
+ <% } %> +
<%= ob.name %><% print(modLink ? ` ${modLink}` : '') %>
+
diff --git a/js/templates/modals/orderDetail/summaryTab/orderComplete.html b/js/templates/modals/orderDetail/summaryTab/orderComplete.html index d54deef11..697aa8a7c 100644 --- a/js/templates/modals/orderDetail/summaryTab/orderComplete.html +++ b/js/templates/modals/orderDetail/summaryTab/orderComplete.html @@ -1,4 +1,3 @@ -<% console.log('sugar'); window.sugar = ob %>

<%= ob.polyT('orderDetail.summaryTab.orderComplete.heading') %>

<%= ob.moment(ob.timestamp).format('lll') %>
diff --git a/js/templates/modals/orderDetail/summaryTab/orderDetails.html b/js/templates/modals/orderDetail/summaryTab/orderDetails.html index 6ca5f3ea3..20e8cd5d2 100644 --- a/js/templates/modals/orderDetail/summaryTab/orderDetails.html +++ b/js/templates/modals/orderDetail/summaryTab/orderDetails.html @@ -95,7 +95,11 @@

<%= ob.polyT('orderDetail.summaryTab.orderDetails.headin
<%= ob.polyT('orderDetail.summaryTab.orderDetails.moderatorHeading') %>
-
+ <% if (ob.isModerated) { %> +
+ <% } else { %> + <%= ob.polyT('orderDetail.summaryTab.notApplicable') %> + <% } %>
<%= ob.polyT('orderDetail.summaryTab.orderDetails.totalHeading') %>
diff --git a/js/utils/order.js b/js/utils/order.js index 745a02d73..424eeca1d 100644 --- a/js/utils/order.js +++ b/js/utils/order.js @@ -251,7 +251,7 @@ export function refundOrder(orderId) { /** * If the order with the given id is in the process of being completed, this method - * will return an object containing the post xhr and the model that's being save. + * will return an object containing the post xhr and the data that's being saved. */ export function completingOrder(orderId) { return completePosts[orderId] || false; @@ -262,9 +262,7 @@ export function completeOrder(orderId, data = {}) { throw new Error('Please provide an orderId'); } - const completeObject = completePosts[orderId]; - - if (!completeObject) { + if (!completePosts[orderId]) { const model = new OrderCompletion(data); const save = model.save(); @@ -297,7 +295,7 @@ export function completeOrder(orderId, data = {}) { completePosts[orderId] = { xhr: save, - model, + data: model.toJSON(), }; } diff --git a/js/views/modals/orderDetail/DisputeOrder.js b/js/views/modals/orderDetail/DisputeOrder.js index ce25709d9..bcd081497 100644 --- a/js/views/modals/orderDetail/DisputeOrder.js +++ b/js/views/modals/orderDetail/DisputeOrder.js @@ -5,6 +5,7 @@ // } from '../../../utils/order'; import loadTemplate from '../../../utils/loadTemplate'; import BaseVw from '../../baseVw'; +import ModFragment from './ModFragment'; export default class extends BaseVw { constructor(options = {}) { @@ -14,6 +15,34 @@ export default class extends BaseVw { throw new Error('Please provide an DisputeOrder model.'); } + const isValidParticipantObject = (participant) => { + let isValid = true; + if (!participant.id) isValid = false; + if (typeof participant.getProfile !== 'function') isValid = false; + return isValid; + }; + + const getInvalidParticpantError = (type = '') => + (`The ${type} object is not valid. It should have an id ` + + 'as well as a getProfile function that returns a promise that ' + + 'resolves with a profile model.'); + + if (!options.moderator) { + throw new Error('Please provide a moderator object.'); + } + + if (!isValidParticipantObject(options.moderator)) { + throw new Error(getInvalidParticpantError('moderator')); + } + + options.moderator.getProfile() + .done((modProfile) => { + this.modProfile = modProfile; + if (this.moderatorVw) this.moderatorVw.setState({ ...modProfile.toJSON() }); + }); + + this.options = options; + // this.listenTo(orderEvents, 'fulfillingOrder', this.onFulfillingOrder); // this.listenTo(orderEvents, 'fulfillOrderComplete, fulfillOrderFail', // this.onFulfillOrderAlways); @@ -79,6 +108,19 @@ export default class extends BaseVw { errors: this.model.validationError || {}, // fulfillingOrder: fulfillingOrder(this.model.id), })); + + const moderatorState = { + peerId: this.options.moderator.id, + showAvatar: true, + ...(this.modProfile && this.modProfile.toJSON() || {}), + }; + + if (this.moderatorVw) this.moderatorVw.remove(); + this.moderatorVw = this.createChild(ModFragment, { + initialState: moderatorState, + }); + + this.$('.js-modContainer').html(this.moderatorVw.render().el); }); return this; diff --git a/js/views/modals/orderDetail/ModFragment.js b/js/views/modals/orderDetail/ModFragment.js new file mode 100644 index 000000000..277eca545 --- /dev/null +++ b/js/views/modals/orderDetail/ModFragment.js @@ -0,0 +1,47 @@ +import _ from 'underscore'; +import loadTemplate from '../../../utils/loadTemplate'; +import BaseVw from '../../baseVw'; + +export default class extends BaseVw { + constructor(options = {}) { + super(options); + this.options = options; + + this._state = { + maxPeerIdLength: 8, + showAvatar: false, + ...options.initialState || {}, + }; + } + + getState() { + return this._state; + } + + setState(state, replace = false) { + let newState; + + if (replace) { + this._state = {}; + } else { + newState = _.extend({}, this._state, state); + } + + if (!_.isEqual(this._state, newState)) { + this._state = newState; + this.render(); + } + + return this; + } + + render() { + loadTemplate('modals/orderDetail/modFragment.html', t => { + this.$el.html(t({ + ...this._state, + })); + }); + + return this; + } +} diff --git a/js/views/modals/orderDetail/OrderDetail.js b/js/views/modals/orderDetail/OrderDetail.js index f0dd59ced..6bc8ab314 100644 --- a/js/views/modals/orderDetail/OrderDetail.js +++ b/js/views/modals/orderDetail/OrderDetail.js @@ -379,6 +379,10 @@ export default class extends BaseModal { const view = this.createChild(DisputeOrderTab, { model, contractType, + moderator: { + id: this.moderatorId, + getProfile: this.getModeratorProfile.bind(this), + }, }); this.listenTo(view, 'clickBackToSummary clickCancel', () => this.selectTab('summary')); diff --git a/js/views/modals/orderDetail/summaryTab/OrderDetails.js b/js/views/modals/orderDetail/summaryTab/OrderDetails.js index a5b530e4c..0dd58245a 100644 --- a/js/views/modals/orderDetail/summaryTab/OrderDetails.js +++ b/js/views/modals/orderDetail/summaryTab/OrderDetails.js @@ -6,7 +6,7 @@ import { convertAndFormatCurrency } from '../../../../utils/currency'; import { clipboard } from 'electron'; import '../../../../utils/velocity'; import loadTemplate from '../../../../utils/loadTemplate'; -import Moderator from './OrderDetailsModerator'; +import ModFragment from '../ModFragment'; import BaseVw from '../../../baseVw'; export default class extends BaseVw { @@ -29,7 +29,7 @@ export default class extends BaseVw { 'as well as a getProfile function that returns a promise that ' + 'resolves with a profile model.'); - if (this.model.get('buyerOrder').payment.moderator) { + if (this.isModerated()) { if (!options.moderator) { throw new Error('Please provide a moderator object.'); } @@ -37,6 +37,12 @@ export default class extends BaseVw { if (!isValidParticipantObject(options.moderator)) { throw new Error(getInvalidParticpantError('moderator')); } + + options.moderator.getProfile() + .done((modProfile) => { + this.modProfile = modProfile; + if (this.moderatorVw) this.moderatorVw.setState({ ...modProfile.toJSON() }); + }); } this.options = options; @@ -66,6 +72,10 @@ export default class extends BaseVw { }); } + isModerated() { + return !!this.model.get('buyerOrder').payment.moderator; + } + get $copiedToClipboard() { return this._$copiedToClipboard || (this._$copiedToClipboard = this.$('.js-orderDetailsCopiedToClipboard')); @@ -80,34 +90,24 @@ export default class extends BaseVw { convertAndFormatCurrency, userCurrency: app.settings.get('localCurrency'), moment, - moderator: this.modProfile && this.modProfile.toJSON() || null, + isModerated: this.isModerated(), })); this._$copiedToClipboard = null; - const moderatorState = {}; + if (this.isModerated()) { + const moderatorState = { + peerId: this.options.moderator.id, + ...(this.modProfile && this.modProfile.toJSON() || {}), + }; - if (this.options.moderator) { - moderatorState.peerId = this.options.moderator.id; - } + if (this.moderatorVw) this.moderatorVw.remove(); + this.moderatorVw = this.createChild(ModFragment, { + initialState: moderatorState, + }); - if (this.moderatorVw) this.moderatorVw.remove(); - this.moderatorVw = this.createChild(Moderator, { - initialState: moderatorState, - }); - - if (!this.modProfile && this.options.moderator) { - this.options.moderator.getProfile() - .done((modProfile) => { - this.modProfile = modProfile; - this.moderatorVw.setState({ - name: modProfile.get('name'), - handle: modProfile.get('handle'), - }); - }); + this.$('.js-moderatorContainer').html(this.moderatorVw.render().el); } - - this.$('.js-moderatorContainer').html(this.moderatorVw.render().el); }); return this; diff --git a/js/views/modals/orderDetail/summaryTab/Summary.js b/js/views/modals/orderDetail/summaryTab/Summary.js index c8c5da70f..6ba86540a 100644 --- a/js/views/modals/orderDetail/summaryTab/Summary.js +++ b/js/views/modals/orderDetail/summaryTab/Summary.js @@ -510,8 +510,8 @@ export default class extends BaseVw { if (['FULFILLED', 'RESOLVED'].indexOf(this.model.get('state')) > -1 && this.buyer.id === app.profile.id) { const completingObject = completingOrder(this.model.id); - const model = completingObject ? - completingObject.model : new OrderCompletion({ orderId: this.model.id }); + const model = new OrderCompletion( + completingObject ? completingObject.data : { orderId: this.model.id }); if (this.completeOrderForm) this.completeOrderForm.remove(); this.completeOrderForm = this.createChild(CompleteOrderForm, { model, From 8fcd8a696cdebb39f8326f3c107adb0714ad3086 Mon Sep 17 00:00:00 2001 From: Rob Misiorowski Date: Fri, 16 Jun 2017 09:54:11 -0700 Subject: [PATCH 03/38] removing some files --- .../summaryTab/orderDetailsModerator.html | 9 ---- .../summaryTab/OrderDetailsModerator.js | 47 ------------------- 2 files changed, 56 deletions(-) delete mode 100644 js/templates/modals/orderDetail/summaryTab/orderDetailsModerator.html delete mode 100644 js/views/modals/orderDetail/summaryTab/OrderDetailsModerator.js diff --git a/js/templates/modals/orderDetail/summaryTab/orderDetailsModerator.html b/js/templates/modals/orderDetail/summaryTab/orderDetailsModerator.html deleted file mode 100644 index 4a76dd7ec..000000000 --- a/js/templates/modals/orderDetail/summaryTab/orderDetailsModerator.html +++ /dev/null @@ -1,9 +0,0 @@ -<% if (ob.peerId) { %> - <% - let modLink = - `${ob.handle || `${ob.peerId.slice(0, 8)}…`}`; - %> -
<%= ob.name %><% print(modLink ? ` ${modLink}` : '') %>
-<% } else { %> - <%= ob.polyT('orderDetail.summaryTab.notApplicable') %> -<% } %> \ No newline at end of file diff --git a/js/views/modals/orderDetail/summaryTab/OrderDetailsModerator.js b/js/views/modals/orderDetail/summaryTab/OrderDetailsModerator.js deleted file mode 100644 index 083399b57..000000000 --- a/js/views/modals/orderDetail/summaryTab/OrderDetailsModerator.js +++ /dev/null @@ -1,47 +0,0 @@ -import _ from 'underscore'; -import loadTemplate from '../../../../utils/loadTemplate'; -import BaseVw from '../../../baseVw'; - -export default class extends BaseVw { - constructor(options = {}) { - super(options); - this.options = options; - - if (this.model) this.setModel(this.model); - - this._state = { - ...options.initialState || {}, - }; - } - - getState() { - return this._state; - } - - setState(state, replace = false) { - let newState; - - if (replace) { - this._state = {}; - } else { - newState = _.extend({}, this._state, state); - } - - if (!_.isEqual(this._state, newState)) { - this._state = newState; - this.render(); - } - - return this; - } - - render() { - loadTemplate('modals/orderDetail/summaryTab/orderDetailsModerator.html', t => { - this.$el.html(t({ - ...this._state, - })); - }); - - return this; - } -} From ca54ecc47d36ac4fb365e6b59ed2cc7c982f380c Mon Sep 17 00:00:00 2001 From: Rob Misiorowski Date: Fri, 16 Jun 2017 12:16:11 -0700 Subject: [PATCH 04/38] WIP - functionlity to open a dispute --- js/languages/en-US.json | 3 +- js/models/order/OrderDispute.js | 36 +++++++++++ .../modals/orderDetail/actionBar.html | 2 +- .../modals/orderDetail/disputeOrder.html | 4 +- js/utils/order.js | 62 +++++++++++++++++++ js/views/modals/orderDetail/ActionBar.js | 13 ++-- js/views/modals/orderDetail/DisputeOrder.js | 46 +++++++------- js/views/modals/orderDetail/OrderDetail.js | 11 ++-- js/views/transactions/Transactions.js | 20 +++--- 9 files changed, 151 insertions(+), 46 deletions(-) create mode 100644 js/models/order/OrderDispute.js diff --git a/js/languages/en-US.json b/js/languages/en-US.json index bb8ba84e9..6764aaba9 100644 --- a/js/languages/en-US.json +++ b/js/languages/en-US.json @@ -1107,7 +1107,8 @@ "failedCancelHeading": "There was an error canceling the order.", "failedFulfillHeading": "There was an error fulfilling the order.", "failedRefundHeading": "There was an error refunding the order.", - "failedCompleteHeading": "There was an error completing the order." + "failedCompleteHeading": "There was an error completing the order.", + "failedOpenDisputeHeading": "There was an error completing the order." }, "exchangeRatesSyncer": { "fetchingRatesStatusMsg": "Fetching exchange rates…", diff --git a/js/models/order/OrderDispute.js b/js/models/order/OrderDispute.js new file mode 100644 index 000000000..24cf6714b --- /dev/null +++ b/js/models/order/OrderDispute.js @@ -0,0 +1,36 @@ +import app from '../../app'; +import BaseModel from '../BaseModel'; + +export default class extends BaseModel { + // constructor(attrs = {}, options = {}) { + // super(attrs, options); + // } + + defaults() { + return { + claim: '', + }; + } + + url() { + return app.getServerUrl('ob/opendispute/'); + } + + get idAttribute() { + return 'orderId'; + } + + // validate() { + // const errObj = this.mergeInNestedErrors(); + // if (Object.keys(errObj).length) return errObj; + // return undefined; + // } + + sync(method, model, options) { + if (method === 'create' || method === 'update') { + options.type = 'POST'; + } + + return super.sync(method, model, options); + } +} diff --git a/js/templates/modals/orderDetail/actionBar.html b/js/templates/modals/orderDetail/actionBar.html index 5d17e37ed..9c9e6c887 100644 --- a/js/templates/modals/orderDetail/actionBar.html +++ b/js/templates/modals/orderDetail/actionBar.html @@ -1,6 +1,6 @@ <% if (ob.showDisputeOrderButton) { %> <%= ob.processingButton({ - className: `flex btn clrErr clrBrDec1 clrTOnEmph js-openDispute ${ob.disputeOrderInProgress ? 'processing' : ''}`, + className: 'flex btn clrErr clrBrDec1 clrTOnEmph js-openDispute', btnText: ob.polyT('orderDetail.actionBar.disputeOrderBtn'), }) %> <% } %> \ No newline at end of file diff --git a/js/templates/modals/orderDetail/disputeOrder.html b/js/templates/modals/orderDetail/disputeOrder.html index 3411a7663..b11eb682c 100644 --- a/js/templates/modals/orderDetail/disputeOrder.html +++ b/js/templates/modals/orderDetail/disputeOrder.html @@ -27,9 +27,9 @@
- <%= ob.polyT(`orderDetail.fulfillOrderTab.btnCancel`) %> + <%= ob.polyT(`orderDetail.fulfillOrderTab.btnCancel`) %> <%= ob.processingButton({ - className: `btn clrBAttGrad clrBrDec1 clrTOnEmph js-submit ${ob.fulfillingOrder ? 'processing' : ''}`, + className: `btn clrBAttGrad clrBrDec1 clrTOnEmph js-submit ${ob.openingDispute ? 'processing' : ''}`, btnText: ob.polyT(`orderDetail.fulfillOrderTab.btnSubmit`), }) %>
\ No newline at end of file diff --git a/js/utils/order.js b/js/utils/order.js index 424eeca1d..ad65879db 100644 --- a/js/utils/order.js +++ b/js/utils/order.js @@ -4,6 +4,7 @@ import { Events } from 'backbone'; import OrderFulfillment from '../models/order/orderFulfillment/OrderFulfillment'; import { openSimpleMessage } from '../views/modals/SimpleMessage'; import OrderCompletion from '../models/order/orderCompletion/OrderCompletion'; +import OrderDispute from '../models/order/OrderDispute'; const events = { ...Events, @@ -15,6 +16,7 @@ const cancelPosts = {}; const fulfillPosts = {}; const refundPosts = {}; const completePosts = {}; +const openDisputePosts = {}; function confirmOrder(orderId, reject = false) { if (!orderId) { @@ -307,3 +309,63 @@ export function completeOrder(orderId, data = {}) { return completePosts[orderId].xhr; } + +/** + * If the order with the given id is in the process of a dispute being opened, + * this method will return an object containing the post xhr and the data + * that's being saved. + */ +export function openingDispute(orderId) { + return openDisputePosts[orderId] || false; +} + +export function openDispute(orderId, data = {}) { + if (!orderId) { + throw new Error('Please provide an orderId'); + } + + if (!openDisputePosts[orderId]) { + const model = new OrderDispute(data); + const save = model.save(); + + if (!save) { + Object.keys(model.validationError) + .forEach(errorKey => { + throw new Error(`${errorKey}: ${model.validationError[errorKey][0]}`); + }); + } else { + save.always(() => { + delete openDisputePosts[orderId]; + }).done(() => { + events.trigger('openDisputeComplete', { + id: orderId, + xhr: save, + }); + }) + .fail(xhr => { + events.trigger('openDisputeFail', { + id: orderId, + xhr: save, + }); + + const failReason = xhr.responseJSON && xhr.responseJSON.reason || ''; + openSimpleMessage( + app.polyglot.t('orderUtil.failedOpenDisputeHeading'), + failReason + ); + }); + + openDisputePosts[orderId] = { + xhr: save, + data: model.toJSON(), + }; + } + + events.trigger('openingDisputeOrder', { + id: orderId, + xhr: save, + }); + } + + return openDisputePosts[orderId].xhr; +} diff --git a/js/views/modals/orderDetail/ActionBar.js b/js/views/modals/orderDetail/ActionBar.js index 6b66b6be5..bcf5d9751 100644 --- a/js/views/modals/orderDetail/ActionBar.js +++ b/js/views/modals/orderDetail/ActionBar.js @@ -1,10 +1,8 @@ import _ from 'underscore'; // import { -// fulfillingOrder, -// refundingOrder, -// refundOrder, +// openingDispute, // events as orderEvents, -// } from '../../../../utils/order'; +// } from '../../../utils/order'; import loadTemplate from '../../../utils/loadTemplate'; import BaseVw from '../../baseVw'; @@ -29,10 +27,14 @@ export default class extends BaseVw { events() { return { - // 'click .js-fulfillOrder': 'onClickFulfillOrder', + 'click .js-openDispute': 'onClickOpenDispute', }; } + onClickOpenDispute() { + this.trigger('clickOpenDispute'); + } + getState() { return this._state; } @@ -58,7 +60,6 @@ export default class extends BaseVw { loadTemplate('modals/orderDetail/actionBar.html', (t) => { this.$el.html(t({ ...this._state, - // disputeOrderInProgress: fulfillingOrder(this.orderId), })); }); diff --git a/js/views/modals/orderDetail/DisputeOrder.js b/js/views/modals/orderDetail/DisputeOrder.js index bcd081497..501290dac 100644 --- a/js/views/modals/orderDetail/DisputeOrder.js +++ b/js/views/modals/orderDetail/DisputeOrder.js @@ -1,8 +1,8 @@ -// import { -// fulfillingOrder, -// fulfillOrder, -// events as orderEvents, -// } from '../../../utils/order'; +import { + openingDispute, + openDispute, + events as orderEvents, +} from '../../../utils/order'; import loadTemplate from '../../../utils/loadTemplate'; import BaseVw from '../../baseVw'; import ModFragment from './ModFragment'; @@ -43,9 +43,9 @@ export default class extends BaseVw { this.options = options; - // this.listenTo(orderEvents, 'fulfillingOrder', this.onFulfillingOrder); - // this.listenTo(orderEvents, 'fulfillOrderComplete, fulfillOrderFail', - // this.onFulfillOrderAlways); + this.listenTo(orderEvents, 'openingDispute', this.onOpeningDispute); + this.listenTo(orderEvents, 'openDisputeComplete, openDisputeFail', + this.onOpenDisputeAlways); } className() { @@ -79,7 +79,7 @@ export default class extends BaseVw { this.model.set({}, { validate: true }); if (!this.model.validationError) { - // fulfillOrder(this.model.id, this.model.toJSON()); + openDispute(this.model.id, this.model.toJSON()); } this.render(); @@ -87,26 +87,28 @@ export default class extends BaseVw { if ($firstErr.length) $firstErr[0].scrollIntoViewIfNeeded(); } - // onFulfillingOrder(e) { - // if (e.id === this.model.id) { - // this.$btnSubmit.addClass('processing'); - // this.$btnCancel.addClass('disabled'); - // } - // } + onOpeningDisputeOrder(e) { + if (e.id === this.model.id) { + this.getCachedEl('.js-submit').addClass('processing'); + this.getCachedEl('.js-cancel').addClass('disabled'); + } + } - // onFulfillOrderAlways(e) { - // if (e.id === this.model.id) { - // this.$btnSubmit.removeClass('processing'); - // this.$btnCancel.removeClass('disabled'); - // } - // } + onOpenDisputeAlways(e) { + if (e.id === this.model.id) { + this.getCachedEl('.js-submit').removeClass('processing'); + this.getCachedEl('.js-cancel').removeClass('disabled'); + } + } render() { + super.render(); + loadTemplate('modals/orderDetail/disputeOrder.html', (t) => { this.$el.html(t({ ...this.model.toJSON(), errors: this.model.validationError || {}, - // fulfillingOrder: fulfillingOrder(this.model.id), + openingDispute: !!openingDispute(this.model.id), })); const moderatorState = { diff --git a/js/views/modals/orderDetail/OrderDetail.js b/js/views/modals/orderDetail/OrderDetail.js index 6bc8ab314..b05aaf2f8 100644 --- a/js/views/modals/orderDetail/OrderDetail.js +++ b/js/views/modals/orderDetail/OrderDetail.js @@ -7,7 +7,7 @@ import { events as orderEvents } from '../../../utils/order'; import loadTemplate from '../../../utils/loadTemplate'; import Case from '../../../models/order/Case'; import OrderFulfillment from '../../../models/order/orderFulfillment/OrderFulfillment'; -import DisputeOrder from '../../../models/order/DisputeOrder'; +import OrderDispute from '../../../models/order/OrderDispute'; import BaseModal from '../BaseModal'; import ProfileBox from './ProfileBox'; import Summary from './summaryTab/Summary'; @@ -25,8 +25,7 @@ export default class extends BaseModal { fetchFailed: false, fetchError: '', }, - // initialTab: 'summary', - initialTab: 'disputeOrder', + initialTab: 'summary', ...options, }; @@ -59,6 +58,9 @@ export default class extends BaseModal { this.actionBar.setState(this.actionBarButtonState); } }); + this.listenTo(orderEvents, 'openDisputeComplete', () => { + if (this.activeTab === 'disputeOrder') this.selectTab('summary'); + }); const socket = getSocket(); @@ -374,7 +376,7 @@ export default class extends BaseModal { createDisputeOrderTabView() { const contractType = this.model.get('contract').type; - const model = new DisputeOrder({ orderId: this.model.id }); + const model = new OrderDispute({ orderId: this.model.id }); const view = this.createChild(DisputeOrderTab, { model, @@ -462,6 +464,7 @@ export default class extends BaseModal { initialState: this.actionBarButtonState, }); this.$('.js-actionBarContainer').html(this.actionBar.render().el); + this.listenTo(this.actionBar, 'clickOpenDispute', () => this.selectTab('disputeOrder')); } }); diff --git a/js/views/transactions/Transactions.js b/js/views/transactions/Transactions.js index 459b4b953..89cc81229 100644 --- a/js/views/transactions/Transactions.js +++ b/js/views/transactions/Transactions.js @@ -197,37 +197,37 @@ export default class extends baseVw { { id: 'filterRefunded', text: app.polyglot.t('transactions.filters.refunded'), - checked: this.salesPurchasesDefaultFilter.states.indexOf(8) > -1, + checked: this.salesPurchasesDefaultFilter.states.indexOf(9) > -1, className: 'filter', targetState: [9], }, { id: 'filterDisputeOpen', text: app.polyglot.t('transactions.filters.disputeOpen'), - checked: this.salesPurchasesDefaultFilter.states.indexOf(5) > -1, + checked: this.salesPurchasesDefaultFilter.states.indexOf(10) > -1, className: 'filter', targetState: [10], }, { id: 'filterDisputePending', text: app.polyglot.t('transactions.filters.disputePending'), - checked: this.salesPurchasesDefaultFilter.states.indexOf(6) > -1, + checked: this.salesPurchasesDefaultFilter.states.indexOf(11) > -1, className: 'filter', targetState: [11], }, { id: 'filterDisputeClosed', text: app.polyglot.t('transactions.filters.disputeClosed'), - checked: this.salesPurchasesDefaultFilter.states.indexOf(7) > -1, + checked: this.salesPurchasesDefaultFilter.states.indexOf(12) > -1, className: 'filter', targetState: [12], }, { id: 'filterCompleted', text: app.polyglot.t('transactions.filters.completed'), - checked: this.salesPurchasesDefaultFilter.states.indexOf(4) > -1 || - this.salesPurchasesDefaultFilter.states.indexOf(9) > -1 || - this.salesPurchasesDefaultFilter.states.indexOf(10) > -1, + checked: this.salesPurchasesDefaultFilter.states.indexOf(6) > -1 || + this.salesPurchasesDefaultFilter.states.indexOf(7) > -1 || + this.salesPurchasesDefaultFilter.states.indexOf(8) > -1, className: 'filter', targetState: [6, 7, 8], }, @@ -247,21 +247,21 @@ export default class extends baseVw { { id: 'filterDisputeOpen', text: app.polyglot.t('transactions.filters.disputeOpen'), - checked: this.salesPurchasesDefaultFilter.states.indexOf(5) > -1, + checked: this.salesPurchasesDefaultFilter.states.indexOf(10) > -1, className: 'filter', targetState: [10], }, { id: 'filterDisputePending', text: app.polyglot.t('transactions.filters.disputePending'), - checked: this.salesPurchasesDefaultFilter.states.indexOf(6) > -1, + checked: this.salesPurchasesDefaultFilter.states.indexOf(11) > -1, className: 'filter', targetState: [11], }, { id: 'filterDisputeClosed', text: app.polyglot.t('transactions.filters.disputeClosed'), - checked: this.salesPurchasesDefaultFilter.states.indexOf(7) > -1, + checked: this.salesPurchasesDefaultFilter.states.indexOf(12) > -1, className: 'filter', targetState: [12], }, From b5c5f8c0204ff9e510e65d622af639b56c986828 Mon Sep 17 00:00:00 2001 From: Rob Misiorowski Date: Fri, 16 Jun 2017 12:16:34 -0700 Subject: [PATCH 05/38] removed file from git --- js/models/order/DisputeOrder.js | 36 --------------------------------- 1 file changed, 36 deletions(-) delete mode 100644 js/models/order/DisputeOrder.js diff --git a/js/models/order/DisputeOrder.js b/js/models/order/DisputeOrder.js deleted file mode 100644 index 24cf6714b..000000000 --- a/js/models/order/DisputeOrder.js +++ /dev/null @@ -1,36 +0,0 @@ -import app from '../../app'; -import BaseModel from '../BaseModel'; - -export default class extends BaseModel { - // constructor(attrs = {}, options = {}) { - // super(attrs, options); - // } - - defaults() { - return { - claim: '', - }; - } - - url() { - return app.getServerUrl('ob/opendispute/'); - } - - get idAttribute() { - return 'orderId'; - } - - // validate() { - // const errObj = this.mergeInNestedErrors(); - // if (Object.keys(errObj).length) return errObj; - // return undefined; - // } - - sync(method, model, options) { - if (method === 'create' || method === 'update') { - options.type = 'POST'; - } - - return super.sync(method, model, options); - } -} From 41432a1bdf916906a41d2199c7f6f8dd65890c93 Mon Sep 17 00:00:00 2001 From: Rob Misiorowski Date: Fri, 16 Jun 2017 14:16:47 -0700 Subject: [PATCH 06/38] dispute open form --- js/languages/en-US.json | 6 ++ .../summaryTab/disputeStarted.html | 27 ++++++ .../summaryTab/stateProgressBar.html | 2 +- .../orderDetail/summaryTab/DisputeStarted.js | 86 +++++++++++++++++++ .../modals/orderDetail/summaryTab/Summary.js | 48 ++++++++++- styles/_theme.scss | 6 ++ styles/components/_stateProgressBar.scss | 8 +- styles/modules/modals/_orderDetail.scss | 10 ++- 8 files changed, 189 insertions(+), 4 deletions(-) create mode 100644 js/templates/modals/orderDetail/summaryTab/disputeStarted.html create mode 100644 js/views/modals/orderDetail/summaryTab/DisputeStarted.js diff --git a/js/languages/en-US.json b/js/languages/en-US.json index 6764aaba9..e6fcba3bc 100644 --- a/js/languages/en-US.json +++ b/js/languages/en-US.json @@ -1046,6 +1046,12 @@ "heading": "Order Complete", "reviewLabel": "%{name}'s Review:" }, + "disputeStarted": { + "heading": "Dispute Started", + "partyIsDisputing": "%{name} is disputing the order:", + "resolveBtn": "Resolve Dispute", + "genericIsDisputed": "The order is being disputed:" + }, "orderDetails": { "progressBarStates": { "paid": "Paid", diff --git a/js/templates/modals/orderDetail/summaryTab/disputeStarted.html b/js/templates/modals/orderDetail/summaryTab/disputeStarted.html new file mode 100644 index 000000000..a027c362f --- /dev/null +++ b/js/templates/modals/orderDetail/summaryTab/disputeStarted.html @@ -0,0 +1,27 @@ +

<%= ob.polyT('orderDetail.summaryTab.disputeStarted.heading') %>

+<% if (ob.timestamp) { %> +<%= ob.moment(ob.timestamp).format('lll') %> +<% } %> + +
+
+
+
+ <% + let introLine = ob.disputerName ? + ob.polyT('orderDetail.summaryTab.disputeStarted.partyIsDisputing', { name: ob.disputerName }) : + ob.polyT('orderDetail.summaryTab.disputeStarted.genericIsDisputed'); + %> +
<%= introLine %>
+
<%= ob.claim %>
+
+
+ <% if (ob.showResolveButton) { %> + <%= ob.processingButton({ + className: `btn clrBAttGrad clrBrDec1 clrTOnEmph tx5b js-resolveDispute ${ob.resolveInProgress ? 'processing' : ''}`, + btnText: ob.polyT('orderDetail.summaryTab.disputeStarted.resolveBtn') + }) %> + <% } %> +
+
+
\ No newline at end of file diff --git a/js/templates/modals/orderDetail/summaryTab/stateProgressBar.html b/js/templates/modals/orderDetail/summaryTab/stateProgressBar.html index 2c88826d7..b3dcb4756 100644 --- a/js/templates/modals/orderDetail/summaryTab/stateProgressBar.html +++ b/js/templates/modals/orderDetail/summaryTab/stateProgressBar.html @@ -12,7 +12,7 @@
<%= state %>
<% if (ob.disputeState === index + 1) { %>
- +
<% } %>
diff --git a/js/views/modals/orderDetail/summaryTab/DisputeStarted.js b/js/views/modals/orderDetail/summaryTab/DisputeStarted.js new file mode 100644 index 000000000..d8e1aa8c1 --- /dev/null +++ b/js/views/modals/orderDetail/summaryTab/DisputeStarted.js @@ -0,0 +1,86 @@ +// import $ from 'jquery'; +import _ from 'underscore'; +import moment from 'moment'; +// import { +// fulfillingOrder, +// refundingOrder, +// refundOrder, +// events as orderEvents, +// } from '../../../../utils/order'; +import loadTemplate from '../../../../utils/loadTemplate'; +import BaseVw from '../../../baseVw'; + +export default class extends BaseVw { + constructor(options = {}) { + super(options); + + this.orderId = options.orderId; + this._state = { + disputerName: '', + claim: '', + showResolveButton: true, + resolveInProgress: false, + ...options.initialState || {}, + }; + + // this.listenTo(orderEvents, 'fulfillingOrder', e => { + // if (e.id === this.orderId) { + // this.setState({ fulfillInProgress: true }); + // } + // }); + + // this.listenTo(orderEvents, 'fulfillOrderComplete fulfillOrderFail', e => { + // if (e.id === this.orderId) { + // this.setState({ fulfillInProgress: false }); + // } + // }); + } + + className() { + return 'disputeStartedEvent rowLg'; + } + + events() { + return { + 'click .js-fulfillOrder': 'onClickFulfillOrder', + }; + } + + onClickRefundConfirmed() { + this.setState({ refundConfirmOn: false }); + // refundOrder(this.orderId); + } + + getState() { + return this._state; + } + + setState(state, replace = false, renderOnChange = true) { + let newState; + + if (replace) { + this._state = {}; + } else { + newState = _.extend({}, this._state, state); + } + + if (renderOnChange && !_.isEqual(this._state, newState)) { + this._state = newState; + this.render(); + } + + return this; + } + + render() { + loadTemplate('modals/orderDetail/summaryTab/disputeStarted.html', (t) => { + this.$el.html(t({ + ...this._state, + moment, + // resolveInProgress: fulfillingOrder(this.orderId), + })); + }); + + return this; + } +} diff --git a/js/views/modals/orderDetail/summaryTab/Summary.js b/js/views/modals/orderDetail/summaryTab/Summary.js index 6ba86540a..ccc6f6252 100644 --- a/js/views/modals/orderDetail/summaryTab/Summary.js +++ b/js/views/modals/orderDetail/summaryTab/Summary.js @@ -19,6 +19,7 @@ import Refunded from './Refunded'; import OrderDetails from './OrderDetails'; import CompleteOrderForm from './CompleteOrderForm'; import OrderComplete from './OrderComplete'; +import DisputeStarted from './DisputeStarted'; export default class extends BaseVw { constructor(options = {}) { @@ -374,7 +375,8 @@ export default class extends BaseVw { let bool = false; // Show the accepted section if the order has been accepted and its fully funded. - if (this.contract.get('vendorOrderConfirmation') && this.getBalanceRemaining() <= 0) { + if (this.contract.get('vendorOrderConfirmation') + && (this.isCase() || this.getBalanceRemaining() <= 0)) { bool = true; } @@ -543,6 +545,38 @@ export default class extends BaseVw { this.$subSections.prepend(this.orderComplete.render().el); } + renderDisputeStartedView() { + const data = this.isCase() ? { + timestamp: this.model.get('timestamp'), + claim: this.model.get('claim'), + } : this.contract.get('dispute'); + + if (!data) { + throw new Error('Unable to create the Dispute Started view because the dispute ' + + 'data object has not been set.'); + } + + if (this.disputeStarted) this.disputeStarted.remove(); + this.disputeStarted = this.createChild(DisputeStarted, { + initialState: { + ...data, + showResolveButton: this.model.get('state') === 'DISPUTED' && + this.moderator.id === app.profile.id, + }, + }); + + // this is only set on the Case. + const buyerOpened = this.model.get('buyerOpened'); + if (typeof buyerOpened !== undefined) { + const disputeOpener = buyerOpened ? this.buyer : this.vendor; + disputeOpener.getProfile() + .done(profile => + this.disputeStarted.setState({ disputerName: profile.get('name') })); + } + + this.$subSections.prepend(this.disputeStarted.render().el); + } + /** * Will render sub-sections in order based on their timestamp. Exempt from * this are the Order Details, Payment Details and Accepted sections which @@ -575,6 +609,18 @@ export default class extends BaseVw { }); } + if (this.contract.get('dispute') || this.isCase()) { + const timestamp = this.isCase() ? + this.model.get('timestamp') : + this.contract.get('dispute').timestamp; + + sections.push({ + function: this.renderDisputeStartedView, + timestamp: + (new Date(timestamp)).getTime(), + }); + } + sections.sort((a, b) => (a.timestamp - b.timestamp)) .forEach(section => { if (typeof section.function === 'function') { diff --git a/styles/_theme.scss b/styles/_theme.scss index 6de64f60f..371f83300 100644 --- a/styles/_theme.scss +++ b/styles/_theme.scss @@ -418,4 +418,10 @@ input[type=range][class~="clrS"] { color: #64452c; } } + + .disputeOpenedBadge, .statusIconCol { + .ion-alert-circled { + color: #ed732a; + } + } } \ No newline at end of file diff --git a/styles/components/_stateProgressBar.scss b/styles/components/_stateProgressBar.scss index 7225fbc38..d3fc4c5a5 100644 --- a/styles/components/_stateProgressBar.scss +++ b/styles/components/_stateProgressBar.scss @@ -64,8 +64,14 @@ $borderWidth: 1px; border-width: 1px; border-radius: 50%; font-size: 16px; - padding: 5px; z-index: 1; + width: 25px; + height: 25px; + text-align: center; + + .ion-alert-circled::before { + padding-top: 3px; + } } &.active { diff --git a/styles/modules/modals/_orderDetail.scss b/styles/modules/modals/_orderDetail.scss index 0bc96aa74..9df56f13b 100644 --- a/styles/modules/modals/_orderDetail.scss +++ b/styles/modules/modals/_orderDetail.scss @@ -298,7 +298,15 @@ &::before { padding-top: 7px; - } + } + } + + &.ion-alert-circled { + font-size: 17px; + + &::before { + padding-top: 8px; + } } } } From 1fee72e0c9b37ff5999efe0066e93abc61047123 Mon Sep 17 00:00:00 2001 From: Rob Misiorowski Date: Fri, 16 Jun 2017 15:41:54 -0700 Subject: [PATCH 07/38] wiring in updating order details based on the disputeOpen notification --- .../summaryTab/disputeStarted.html | 1 + .../orderDetail/summaryTab/DisputeStarted.js | 20 ------------------- .../modals/orderDetail/summaryTab/Summary.js | 19 ++++++++++++++---- 3 files changed, 16 insertions(+), 24 deletions(-) diff --git a/js/templates/modals/orderDetail/summaryTab/disputeStarted.html b/js/templates/modals/orderDetail/summaryTab/disputeStarted.html index a027c362f..13dc34538 100644 --- a/js/templates/modals/orderDetail/summaryTab/disputeStarted.html +++ b/js/templates/modals/orderDetail/summaryTab/disputeStarted.html @@ -1,3 +1,4 @@ +<% console.log('milly'); window.milly = ob %>

<%= ob.polyT('orderDetail.summaryTab.disputeStarted.heading') %>

<% if (ob.timestamp) { %> <%= ob.moment(ob.timestamp).format('lll') %> diff --git a/js/views/modals/orderDetail/summaryTab/DisputeStarted.js b/js/views/modals/orderDetail/summaryTab/DisputeStarted.js index d8e1aa8c1..50e1ae904 100644 --- a/js/views/modals/orderDetail/summaryTab/DisputeStarted.js +++ b/js/views/modals/orderDetail/summaryTab/DisputeStarted.js @@ -1,12 +1,5 @@ -// import $ from 'jquery'; import _ from 'underscore'; import moment from 'moment'; -// import { -// fulfillingOrder, -// refundingOrder, -// refundOrder, -// events as orderEvents, -// } from '../../../../utils/order'; import loadTemplate from '../../../../utils/loadTemplate'; import BaseVw from '../../../baseVw'; @@ -22,18 +15,6 @@ export default class extends BaseVw { resolveInProgress: false, ...options.initialState || {}, }; - - // this.listenTo(orderEvents, 'fulfillingOrder', e => { - // if (e.id === this.orderId) { - // this.setState({ fulfillInProgress: true }); - // } - // }); - - // this.listenTo(orderEvents, 'fulfillOrderComplete fulfillOrderFail', e => { - // if (e.id === this.orderId) { - // this.setState({ fulfillInProgress: false }); - // } - // }); } className() { @@ -77,7 +58,6 @@ export default class extends BaseVw { this.$el.html(t({ ...this._state, moment, - // resolveInProgress: fulfillingOrder(this.orderId), })); }); diff --git a/js/views/modals/orderDetail/summaryTab/Summary.js b/js/views/modals/orderDetail/summaryTab/Summary.js index ccc6f6252..359c28ed6 100644 --- a/js/views/modals/orderDetail/summaryTab/Summary.js +++ b/js/views/modals/orderDetail/summaryTab/Summary.js @@ -187,6 +187,18 @@ export default class extends BaseVw { } }); + this.listenTo(orderEvents, 'openDisputeComplete', e => { + if (e.id === this.model.id) { + this.model.set('state', 'DISPUTED'); + this.model.fetch(); + } + }); + + if (!this.isCase()) { + this.listenTo(this.contract, 'change:dispute', + () => this.renderDisputeStartedView()); + } + const serverSocket = getSocket(); if (serverSocket) { @@ -272,10 +284,9 @@ export default class extends BaseVw { ], }; - // TODO: add in completed check with determination of whether a dispute - // had been opened. if (orderState === 'DISPUTED' || orderState === 'DECIDED' || - orderState === 'RESOLVED') { + orderState === 'RESOLVED' || + (orderState === 'COMPLETED' && this.model.get('dispute') !== undefined)) { if (!this.isCase()) { state.states = [ app.polyglot.t('orderDetail.summaryTab.orderDetails.progressBarStates.disputed'), @@ -567,7 +578,7 @@ export default class extends BaseVw { // this is only set on the Case. const buyerOpened = this.model.get('buyerOpened'); - if (typeof buyerOpened !== undefined) { + if (typeof buyerOpened !== 'undefined') { const disputeOpener = buyerOpened ? this.buyer : this.vendor; disputeOpener.getProfile() .done(profile => From 87b94209ce1476229584789ee75d117a6df10270 Mon Sep 17 00:00:00 2001 From: Rob Misiorowski Date: Fri, 16 Jun 2017 16:58:24 -0700 Subject: [PATCH 08/38] WIP - starting on the Resolve Dispute form --- js/languages/en-US.json | 6 +- .../modals/orderDetail/resolveDispute.html | 35 ++++++++ .../summaryTab/disputeStarted.html | 5 +- js/views/modals/orderDetail/OrderDetail.js | 24 ++++- js/views/modals/orderDetail/ResolveDispute.js | 89 +++++++++++++++++++ .../modals/orderDetail/summaryTab/Summary.js | 6 ++ styles/modules/modals/_orderDetail.scss | 2 +- 7 files changed, 159 insertions(+), 8 deletions(-) create mode 100644 js/templates/modals/orderDetail/resolveDispute.html create mode 100644 js/views/modals/orderDetail/ResolveDispute.js diff --git a/js/languages/en-US.json b/js/languages/en-US.json index e6fcba3bc..0b74e0908 100644 --- a/js/languages/en-US.json +++ b/js/languages/en-US.json @@ -1050,7 +1050,8 @@ "heading": "Dispute Started", "partyIsDisputing": "%{name} is disputing the order:", "resolveBtn": "Resolve Dispute", - "genericIsDisputed": "The order is being disputed:" + "genericIsDisputed": "The order is being disputed:", + "noReasonProvided": "No reason was provided." }, "orderDetails": { "progressBarStates": { @@ -1094,6 +1095,9 @@ "btnCancel": "Cancel", "btnSubmit": "Submit" }, + "resolveDisputeTab": { + "heading": "Resolve Dispute" + }, "disputeOrderTab": { "heading": "Dispute Order", "moderatorLabel": "Moderator", diff --git a/js/templates/modals/orderDetail/resolveDispute.html b/js/templates/modals/orderDetail/resolveDispute.html new file mode 100644 index 000000000..77cc72501 --- /dev/null +++ b/js/templates/modals/orderDetail/resolveDispute.html @@ -0,0 +1,35 @@ +
+ +
<%= ob.polyT(`orderDetail.resolveDisputeTab.heading`) %>
+
+
+
+
+
+ +
+
+ <% if (ob.errors['physicalDelivery.shipper']) print(ob.formErrorTmpl({ errors: ob.errors['physicalDelivery.shipper'] })) %> + +
+
+
+
+ +
+
+ <% if (ob.errors['physicalDelivery.trackingNumber']) print(ob.formErrorTmpl({ errors: ob.errors['physicalDelivery.trackingNumber'] })) %> + +
+
+
+
+
+ <%= ob.polyT(`orderDetail.fulfillOrderTab.btnCancel`) %> + <%= ob.processingButton({ + className: `btn clrBAttGrad clrBrDec1 clrTOnEmph js-submit ${ob.fulfillingOrder ? 'processing' : ''}`, + btnText: ob.polyT(`orderDetail.fulfillOrderTab.btnSubmit`), + }) %> +
\ No newline at end of file diff --git a/js/templates/modals/orderDetail/summaryTab/disputeStarted.html b/js/templates/modals/orderDetail/summaryTab/disputeStarted.html index 13dc34538..3cd9aa372 100644 --- a/js/templates/modals/orderDetail/summaryTab/disputeStarted.html +++ b/js/templates/modals/orderDetail/summaryTab/disputeStarted.html @@ -1,11 +1,10 @@ -<% console.log('milly'); window.milly = ob %>

<%= ob.polyT('orderDetail.summaryTab.disputeStarted.heading') %>

<% if (ob.timestamp) { %> <%= ob.moment(ob.timestamp).format('lll') %> <% } %>
-
+
<% @@ -14,7 +13,7 @@

<%= ob.polyT('orderDetail.summaryTab.disputeStarted.head ob.polyT('orderDetail.summaryTab.disputeStarted.genericIsDisputed'); %>
<%= introLine %>
-
<%= ob.claim %>
+
<%= ob.claim || ob.polyT('orderDetail.summaryTab.disputeStarted.noReasonProvided') %>

<% if (ob.showResolveButton) { %> diff --git a/js/views/modals/orderDetail/OrderDetail.js b/js/views/modals/orderDetail/OrderDetail.js index b05aaf2f8..028720635 100644 --- a/js/views/modals/orderDetail/OrderDetail.js +++ b/js/views/modals/orderDetail/OrderDetail.js @@ -14,7 +14,8 @@ import Summary from './summaryTab/Summary'; import Discussion from './Discussion'; import Contract from './Contract'; import FulfillOrder from './FulfillOrder'; -import DisputeOrderTab from './DisputeOrder'; +import DisputeOrder from './DisputeOrder'; +import ResolveDispute from './ResolveDispute'; import ActionBar from './ActionBar.js'; export default class extends BaseModal { @@ -25,7 +26,8 @@ export default class extends BaseModal { fetchFailed: false, fetchError: '', }, - initialTab: 'summary', + // initialTab: 'summary', + initialTab: 'resolveDispute', ...options, }; @@ -378,7 +380,7 @@ export default class extends BaseModal { const model = new OrderDispute({ orderId: this.model.id }); - const view = this.createChild(DisputeOrderTab, { + const view = this.createChild(DisputeOrder, { model, contractType, moderator: { @@ -392,6 +394,22 @@ export default class extends BaseModal { return view; } + createResolveDisputeTabView() { + const model = new OrderDispute({ orderId: this.model.id }); + + const view = this.createChild(ResolveDispute, { + model, + // moderator: { + // id: this.moderatorId, + // getProfile: this.getModeratorProfile.bind(this), + // }, + }); + + this.listenTo(view, 'clickBackToSummary clickCancel', () => this.selectTab('summary')); + + return view; + } + setUnreadChatMessagesBadge() { this.$unreadChatMessagesBadge.text(this.getUnreadChatMessagesText()); } diff --git a/js/views/modals/orderDetail/ResolveDispute.js b/js/views/modals/orderDetail/ResolveDispute.js new file mode 100644 index 000000000..c87460d8d --- /dev/null +++ b/js/views/modals/orderDetail/ResolveDispute.js @@ -0,0 +1,89 @@ +// import { +// fulfillingOrder, +// fulfillOrder, +// events as orderEvents, +// } from '../../../utils/order'; +import loadTemplate from '../../../utils/loadTemplate'; +import BaseVw from '../../baseVw'; + +export default class extends BaseVw { + constructor(options = {}) { + super(options); + + // if (!this.model) { + // throw new Error('Please provide an OrderFulfillment model.'); + // } + + // this.listenTo(orderEvents, 'fulfillingOrder', this.onFulfillingOrder); + // this.listenTo(orderEvents, 'fulfillOrderComplete, fulfillOrderFail', + // this.onFulfillOrderAlways); + } + + className() { + return 'resolveDisputeTab'; + } + + events() { + return { + 'click .js-backToSummary': 'onClickBackToSummary', + 'click .js-cancel': 'onClickCancel', + 'click .js-submit': 'onClickSubmit', + }; + } + + onClickBackToSummary() { + this.trigger('clickBackToSummary'); + } + + onClickCancel() { + const id = this.model.id; + this.model.reset(); + // restore the id reset blew away + this.model.set({ orderId: id }); + this.render(); + this.trigger('clickCancel'); + } + + onClickSubmit() { + const formData = this.getFormData(); + this.model.set(formData); + this.model.set({}, { validate: true }); + + if (!this.model.validationError) { + // fulfillOrder(this.model.id, this.model.toJSON()); + } + + this.render(); + const $firstErr = this.$('.errorList:first'); + if ($firstErr.length) $firstErr[0].scrollIntoViewIfNeeded(); + } + + // onFulfillingOrder(e) { + // if (e.id === this.model.id) { + // this.$btnSubmit.addClass('processing'); + // this.$btnCancel.addClass('disabled'); + // } + // } + + // onFulfillOrderAlways(e) { + // if (e.id === this.model.id) { + // this.$btnSubmit.removeClass('processing'); + // this.$btnCancel.removeClass('disabled'); + // } + // } + + render() { + loadTemplate('modals/orderDetail/resolveDispute.html', (t) => { + this.$el.html(t({ + ...this.model.toJSON(), + errors: this.model.validationError || {}, + // fulfillingOrder: fulfillingOrder(this.model.id), + })); + + this._$btnCancel = null; + this._$btnSubmit = null; + }); + + return this; + } +} diff --git a/js/views/modals/orderDetail/summaryTab/Summary.js b/js/views/modals/orderDetail/summaryTab/Summary.js index 359c28ed6..e82da1276 100644 --- a/js/views/modals/orderDetail/summaryTab/Summary.js +++ b/js/views/modals/orderDetail/summaryTab/Summary.js @@ -235,6 +235,12 @@ export default class extends BaseVw { e.jsonData.notification.orderCompletion.orderId === this.model.id) { // A notification the vendor will get when the buyer has completed an order. this.model.fetch(); + } else if (e.jsonData.notification.disputeOpen && + e.jsonData.notification.disputeOpen.orderId === this.model.id) { + // When a party opens a dispute the mod and the other party will get this + // notification + console.log('what what'); + this.model.fetch(); } } }); diff --git a/styles/modules/modals/_orderDetail.scss b/styles/modules/modals/_orderDetail.scss index 9df56f13b..33bccc2ca 100644 --- a/styles/modules/modals/_orderDetail.scss +++ b/styles/modules/modals/_orderDetail.scss @@ -385,7 +385,7 @@ } } - .fulfillOrderTab, .disputeOrderTab { + .fulfillOrderTab, .disputeOrderTab, .resolveDisputeTab { hr { margin-left: $padLg; margin-right: $padLg; From daaae5ffe69d0d3bf1394efd7f2d87ec8a3093b0 Mon Sep 17 00:00:00 2001 From: Rob Misiorowski Date: Mon, 19 Jun 2017 16:45:19 -0700 Subject: [PATCH 09/38] additional work on the resolve dispute form --- js/languages/en-US.json | 8 ++++- .../modals/orderDetail/resolveDispute.html | 33 ++++++++++++------ js/views/modals/orderDetail/OrderDetail.js | 17 ++++++---- js/views/modals/orderDetail/ResolveDispute.js | 34 +++++++++++++++++-- .../orderDetail/summaryTab/DisputeStarted.js | 7 ++-- .../modals/orderDetail/summaryTab/Summary.js | 22 ++++++------ 6 files changed, 84 insertions(+), 37 deletions(-) diff --git a/js/languages/en-US.json b/js/languages/en-US.json index 0b74e0908..4f10dda06 100644 --- a/js/languages/en-US.json +++ b/js/languages/en-US.json @@ -1096,7 +1096,13 @@ "btnSubmit": "Submit" }, "resolveDisputeTab": { - "heading": "Resolve Dispute" + "heading": "Resolve Dispute", + "buyerAmountLabel": "Buyer Amount", + "vendorAmountLabel": "Vendor Amount", + "commentLabel": "Comment", + "commentPlaceholder": "Explain your decision...", + "btnCancel": "Cancel", + "btnSubmit": "Submit" }, "disputeOrderTab": { "heading": "Dispute Order", diff --git a/js/templates/modals/orderDetail/resolveDispute.html b/js/templates/modals/orderDetail/resolveDispute.html index 77cc72501..5650ee1f4 100644 --- a/js/templates/modals/orderDetail/resolveDispute.html +++ b/js/templates/modals/orderDetail/resolveDispute.html @@ -8,28 +8,39 @@
- + +
-
- <% if (ob.errors['physicalDelivery.shipper']) print(ob.formErrorTmpl({ errors: ob.errors['physicalDelivery.shipper'] })) %> - +
+ <% if (ob.errors['buyerPercentage']) print(ob.formErrorTmpl({ errors: ob.errors['buyerPercentage'] })) %> +
- + +
-
- <% if (ob.errors['physicalDelivery.trackingNumber']) print(ob.formErrorTmpl({ errors: ob.errors['physicalDelivery.trackingNumber'] })) %> - +
+ <% if (ob.errors['vendorPercentage']) print(ob.formErrorTmpl({ errors: ob.errors['vendorPercentage'] })) %> +
+
+
+ +
+
+ <% if (ob.errors['resolution']) print(ob.formErrorTmpl({ errors: ob.errors['resolution'] })) %> + +
+

- <%= ob.polyT(`orderDetail.fulfillOrderTab.btnCancel`) %> + <%= ob.polyT(`orderDetail.resolveDisputeTab.btnCancel`) %> <%= ob.processingButton({ - className: `btn clrBAttGrad clrBrDec1 clrTOnEmph js-submit ${ob.fulfillingOrder ? 'processing' : ''}`, - btnText: ob.polyT(`orderDetail.fulfillOrderTab.btnSubmit`), + className: `btn clrBAttGrad clrBrDec1 clrTOnEmph js-submit ${ob.resolvingDispute ? 'processing' : ''}`, + btnText: ob.polyT(`orderDetail.resolveDisputeTab.btnSubmit`), }) %>
\ No newline at end of file diff --git a/js/views/modals/orderDetail/OrderDetail.js b/js/views/modals/orderDetail/OrderDetail.js index 028720635..80d371437 100644 --- a/js/views/modals/orderDetail/OrderDetail.js +++ b/js/views/modals/orderDetail/OrderDetail.js @@ -26,8 +26,7 @@ export default class extends BaseModal { fetchFailed: false, fetchError: '', }, - // initialTab: 'summary', - initialTab: 'resolveDispute', + initialTab: 'summary', ...options, }; @@ -313,6 +312,8 @@ export default class extends BaseModal { const view = this.createChild(Summary, viewData); this.listenTo(view, 'clickFulfillOrder', () => this.selectTab('fulfillOrder')); + this.listenTo(view, 'clickResolveDispute', + () => this.selectTab('resolveDispute')); return view; } @@ -399,10 +400,14 @@ export default class extends BaseModal { const view = this.createChild(ResolveDispute, { model, - // moderator: { - // id: this.moderatorId, - // getProfile: this.getModeratorProfile.bind(this), - // }, + vendor: { + id: this.vendorId, + getProfile: this.getVendorProfile.bind(this), + }, + buyer: { + id: this.buyerId, + getProfile: this.getBuyerProfile.bind(this), + }, }); this.listenTo(view, 'clickBackToSummary clickCancel', () => this.selectTab('summary')); diff --git a/js/views/modals/orderDetail/ResolveDispute.js b/js/views/modals/orderDetail/ResolveDispute.js index c87460d8d..2db05bdc5 100644 --- a/js/views/modals/orderDetail/ResolveDispute.js +++ b/js/views/modals/orderDetail/ResolveDispute.js @@ -10,9 +10,37 @@ export default class extends BaseVw { constructor(options = {}) { super(options); - // if (!this.model) { - // throw new Error('Please provide an OrderFulfillment model.'); - // } + if (!this.model) { + throw new Error('Please provide an OrderFulfillment model.'); + } + + const isValidParticipantObject = (participant) => { + let isValid = true; + if (!participant.id) isValid = false; + if (typeof participant.getProfile !== 'function') isValid = false; + return isValid; + }; + + const getInvalidParticpantError = (type = '') => + (`The ${type} object is not valid. It should have an id ` + + 'as well as a getProfile function that returns a promise that ' + + 'resolves with a profile model.'); + + if (!options.vendor) { + throw new Error('Please provide a vendor object.'); + } + + if (!isValidParticipantObject(options.vendor)) { + throw new Error(getInvalidParticpantError('vendor')); + } + + if (!options.buyer) { + throw new Error('Please provide a buyer object.'); + } + + if (!isValidParticipantObject(options.buyer)) { + throw new Error(getInvalidParticpantError('buyer')); + } // this.listenTo(orderEvents, 'fulfillingOrder', this.onFulfillingOrder); // this.listenTo(orderEvents, 'fulfillOrderComplete, fulfillOrderFail', diff --git a/js/views/modals/orderDetail/summaryTab/DisputeStarted.js b/js/views/modals/orderDetail/summaryTab/DisputeStarted.js index 50e1ae904..27082f4ab 100644 --- a/js/views/modals/orderDetail/summaryTab/DisputeStarted.js +++ b/js/views/modals/orderDetail/summaryTab/DisputeStarted.js @@ -23,13 +23,12 @@ export default class extends BaseVw { events() { return { - 'click .js-fulfillOrder': 'onClickFulfillOrder', + 'click .js-resolveDispute': 'onClickResolveDispute', }; } - onClickRefundConfirmed() { - this.setState({ refundConfirmOn: false }); - // refundOrder(this.orderId); + onClickResolveDispute() { + this.trigger('clickResolveDispute'); } getState() { diff --git a/js/views/modals/orderDetail/summaryTab/Summary.js b/js/views/modals/orderDetail/summaryTab/Summary.js index e82da1276..efe5c078a 100644 --- a/js/views/modals/orderDetail/summaryTab/Summary.js +++ b/js/views/modals/orderDetail/summaryTab/Summary.js @@ -23,11 +23,7 @@ import DisputeStarted from './DisputeStarted'; export default class extends BaseVw { constructor(options = {}) { - const opts = { - ...options, - }; - - super(opts); + super(options); if (!this.model) { throw new Error('Please provide a model.'); @@ -57,7 +53,7 @@ export default class extends BaseVw { 'as well as a getProfile function that returns a promise that ' + 'resolves with a profile model.'); - if (!opts.vendor) { + if (!options.vendor) { throw new Error('Please provide a vendor object.'); } @@ -65,7 +61,7 @@ export default class extends BaseVw { throw new Error(getInvalidParticpantError('vendor')); } - if (!opts.buyer) { + if (!options.buyer) { throw new Error('Please provide a buyer object.'); } @@ -83,10 +79,10 @@ export default class extends BaseVw { } } - this.options = opts || {}; - this.vendor = opts.vendor; - this.buyer = opts.buyer; - this.moderator = opts.moderator; + this.options = options || {}; + this.vendor = options.vendor; + this.buyer = options.buyer; + this.moderator = options.moderator; this.listenTo(this.model, 'change:state', (md, state) => { this.stateProgressBar.setState(this.progressBarState); @@ -239,7 +235,6 @@ export default class extends BaseVw { e.jsonData.notification.disputeOpen.orderId === this.model.id) { // When a party opens a dispute the mod and the other party will get this // notification - console.log('what what'); this.model.fetch(); } } @@ -591,6 +586,9 @@ export default class extends BaseVw { this.disputeStarted.setState({ disputerName: profile.get('name') })); } + this.listenTo(this.disputeStarted, 'clickResolveDispute', + () => this.trigger('clickResolveDispute')); + this.$subSections.prepend(this.disputeStarted.render().el); } From 66e7c02a342281403800a1bec16e9218d5662cfd Mon Sep 17 00:00:00 2001 From: Rob Misiorowski Date: Tue, 20 Jun 2017 16:42:59 -0700 Subject: [PATCH 10/38] Completed the resolve dispute form --- js/languages/en-US.json | 19 ++- js/models/order/Case.js | 9 ++ js/models/order/Order.js | 9 ++ js/models/order/OrderDispute.js | 10 -- js/models/order/ResolveDispute.js | 83 ++++++++++++++ .../modals/orderDetail/resolveDispute.html | 43 ++++--- js/utils/order.js | 62 ++++++++++ js/views/modals/orderDetail/OrderDetail.js | 26 ++++- js/views/modals/orderDetail/ResolveDispute.js | 108 +++++++++++++----- .../modals/orderDetail/summaryTab/Summary.js | 43 +++++++ js/views/transactions/Transactions.js | 3 +- styles/components/_containers.scss | 11 ++ styles/modules/modals/_orderDetail.scss | 39 ++++++- 13 files changed, 410 insertions(+), 55 deletions(-) create mode 100644 js/models/order/ResolveDispute.js diff --git a/js/languages/en-US.json b/js/languages/en-US.json index 4f10dda06..f1fa5e4d2 100644 --- a/js/languages/en-US.json +++ b/js/languages/en-US.json @@ -1102,7 +1102,13 @@ "commentLabel": "Comment", "commentPlaceholder": "Explain your decision...", "btnCancel": "Cancel", - "btnSubmit": "Submit" + "btnSubmit": "Submit", + "resolveConfirm": { + "title": "Are you sure?", + "body": "Please double check everything looks good", + "btnCancel": "Cancel", + "btnSubmit": "Submit" + } }, "disputeOrderTab": { "heading": "Dispute Order", @@ -1124,7 +1130,8 @@ "failedFulfillHeading": "There was an error fulfilling the order.", "failedRefundHeading": "There was an error refunding the order.", "failedCompleteHeading": "There was an error completing the order.", - "failedOpenDisputeHeading": "There was an error completing the order." + "failedOpenDisputeHeading": "There was an error completing the order.", + "failedResolveHeading": "There was an error resolving the order." }, "exchangeRatesSyncer": { "fetchingRatesStatusMsg": "Fetching exchange rates…", @@ -1271,6 +1278,14 @@ "orderCompletionModelErrors": { "provideReview": "Please provide a review.", "provideRating": "Please select a rating." + }, + "resolveDisputeModelErrors": { + "provideAmount": "Please provide an amount.", + "percentageOutOfRange": "The amount must be between 0 and 100.", + "providePercentageAsNumber": "Please provide a vendor amount as a number.", + "totalPercentageOutOfRange": "The sum of the buyer and vendor amounts cannot exceed 100.", + "totalPercentageTooLow": "The sum of the buyer and vendor amounts must add up to 100.", + "provideResolution": "Please explain your decision." }, "bitcoinCurrencyUnits": { "BTC": "BTC", diff --git a/js/models/order/Case.js b/js/models/order/Case.js index a9dc83cae..cdf66c8e6 100644 --- a/js/models/order/Case.js +++ b/js/models/order/Case.js @@ -40,6 +40,15 @@ export default class extends BaseModel { // convert price fields response.vendorContract.buyerOrder.payment.amount = integerToDecimal(response.vendorContract.buyerOrder.payment.amount, true); + + // if (response.resolution) { + // response.resolution.payout.buyerOutput.amount = + // integerToDecimal(response.resolution.payout.buyerOutput.amount, true); + // response.resolution.buyerOutput.amount = + // integerToDecimal(response.resolution.payout.vendorOutput.amount, true); + // response.resolution.buyerOutput.amount = + // integerToDecimal(response.resolution.payout.moderatorOutput.amount, true); + // } } return response; diff --git a/js/models/order/Order.js b/js/models/order/Order.js index 7478a8844..b9d073fc4 100644 --- a/js/models/order/Order.js +++ b/js/models/order/Order.js @@ -47,6 +47,15 @@ export default class extends BaseModel { // convert price fields response.contract.buyerOrder.payment.amount = integerToDecimal(response.contract.buyerOrder.payment.amount, true); + + // if (response.contract.disputeResolution) { + // response.contract.disputeResolution.payout.buyerOutput.amount = + // integerToDecimal(response.contract.disputeResolution.payout.buyerOutput.amount, true); + // response.contract.disputeResolution.payout.buyerOutput.amount = + // integerToDecimal(response.contract.disputeResolution.payout.vendorOutput.amount, true); + // response.contract.disputeResolution.payout.buyerOutput.amount = + // integerToDecimal(response.contract.disputeResolution.payout.moderatorOutput.amount, true); + // } } response.paymentAddressTransactions = response.paymentAddressTransactions || []; diff --git a/js/models/order/OrderDispute.js b/js/models/order/OrderDispute.js index 24cf6714b..7b97ae06f 100644 --- a/js/models/order/OrderDispute.js +++ b/js/models/order/OrderDispute.js @@ -2,10 +2,6 @@ import app from '../../app'; import BaseModel from '../BaseModel'; export default class extends BaseModel { - // constructor(attrs = {}, options = {}) { - // super(attrs, options); - // } - defaults() { return { claim: '', @@ -20,12 +16,6 @@ export default class extends BaseModel { return 'orderId'; } - // validate() { - // const errObj = this.mergeInNestedErrors(); - // if (Object.keys(errObj).length) return errObj; - // return undefined; - // } - sync(method, model, options) { if (method === 'create' || method === 'update') { options.type = 'POST'; diff --git a/js/models/order/ResolveDispute.js b/js/models/order/ResolveDispute.js new file mode 100644 index 000000000..50c454bda --- /dev/null +++ b/js/models/order/ResolveDispute.js @@ -0,0 +1,83 @@ +import app from '../../app'; +import BaseModel from '../BaseModel'; + +export default class extends BaseModel { + defaults() { + return { + resolution: '', + }; + } + + url() { + return app.getServerUrl('ob/closedispute/'); + } + + get idAttribute() { + return 'orderId'; + } + + validate(attrs) { + const errObj = {}; + + const addError = (fieldName, error) => { + errObj[fieldName] = errObj[fieldName] || []; + errObj[fieldName].push(error); + }; + + let vendorPercentageOk = false; + let buyerPercentageOk = false; + + if (typeof attrs.vendorPercentage === 'undefined' || attrs.vendorPercentage === '') { + addError('vendorPercentage', + app.polyglot.t('resolveDisputeModelErrors.provideAmount')); + } else if (typeof attrs.vendorPercentage !== 'number') { + addError('vendorPercentage', + app.polyglot.t('resolveDisputeModelErrors.providePercentageAsNumber')); + } else if (attrs.vendorPercentage < 0 || attrs.vendorPercentage > 100) { + addError('vendorPercentage', + app.polyglot.t('resolveDisputeModelErrors.vendorPercentageOutOfRange')); + } else { + vendorPercentageOk = true; + } + + if (typeof attrs.buyerPercentage === 'undefined' || attrs.buyerPercentage === '') { + addError('buyerPercentage', + app.polyglot.t('resolveDisputeModelErrors.provideAmount')); + } else if (typeof attrs.buyerPercentage !== 'number') { + addError('buyerPercentage', + app.polyglot.t('resolveDisputeModelErrors.providePercentageAsNumber')); + } else if (attrs.buyerPercentage < 0 || attrs.buyerPercentage > 100) { + addError('buyerPercentage', + app.polyglot.t('resolveDisputeModelErrors.buyerPercentageOutOfRange')); + } else { + buyerPercentageOk = true; + } + + if (vendorPercentageOk && buyerPercentageOk) { + if (attrs.buyerPercentage + attrs.vendorPercentage > 100) { + addError('buyerPercentage', + app.polyglot.t('resolveDisputeModelErrors.totalPercentageOutOfRange')); + } else if (attrs.buyerPercentage + attrs.vendorPercentage < 100) { + addError('buyerPercentage', + app.polyglot.t('resolveDisputeModelErrors.totalPercentageTooLow')); + } + } + + if (!attrs.resolution) { + addError('resolution', + app.polyglot.t('resolveDisputeModelErrors.provideResolution')); + } + + if (Object.keys(errObj).length) return errObj; + + return undefined; + } + + sync(method, model, options) { + if (method === 'create' || method === 'update') { + options.type = 'POST'; + } + + return super.sync(method, model, options); + } +} diff --git a/js/templates/modals/orderDetail/resolveDispute.html b/js/templates/modals/orderDetail/resolveDispute.html index 5650ee1f4..ce94a490a 100644 --- a/js/templates/modals/orderDetail/resolveDispute.html +++ b/js/templates/modals/orderDetail/resolveDispute.html @@ -8,22 +8,28 @@
- -
+ +
<%= ob.buyerName %>
-
+
<% if (ob.errors['buyerPercentage']) print(ob.formErrorTmpl({ errors: ob.errors['buyerPercentage'] })) %> - +
+ +
+
- -
+ +
<%= ob.vendorName %>
-
+
<% if (ob.errors['vendorPercentage']) print(ob.formErrorTmpl({ errors: ob.errors['vendorPercentage'] })) %> - +
+ +
+
@@ -34,13 +40,24 @@ <% if (ob.errors['resolution']) print(ob.formErrorTmpl({ errors: ob.errors['resolution'] })) %>
-
+

<%= ob.polyT(`orderDetail.resolveDisputeTab.btnCancel`) %> - <%= ob.processingButton({ - className: `btn clrBAttGrad clrBrDec1 clrTOnEmph js-submit ${ob.resolvingDispute ? 'processing' : ''}`, - btnText: ob.polyT(`orderDetail.resolveDisputeTab.btnSubmit`), - }) %> +
+ <%= ob.processingButton({ + className: `btn clrBAttGrad clrBrDec1 clrTOnEmph js-submit ${ob.resolvingDispute ? 'processing' : ''}`, + btnText: ob.polyT(`orderDetail.resolveDisputeTab.btnSubmit`), + }) %> +
+
<%= ob.polyT('orderDetail.resolveDisputeTab.resolveConfirm.title') %>
+

<%= ob.polyT('orderDetail.resolveDisputeTab.resolveConfirm.body') %>

+
+ +
+
\ No newline at end of file diff --git a/js/utils/order.js b/js/utils/order.js index ad65879db..f8e1c43e3 100644 --- a/js/utils/order.js +++ b/js/utils/order.js @@ -5,6 +5,7 @@ import OrderFulfillment from '../models/order/orderFulfillment/OrderFulfillment' import { openSimpleMessage } from '../views/modals/SimpleMessage'; import OrderCompletion from '../models/order/orderCompletion/OrderCompletion'; import OrderDispute from '../models/order/OrderDispute'; +import ResolveDispute from '../models/order/ResolveDispute'; const events = { ...Events, @@ -17,6 +18,7 @@ const fulfillPosts = {}; const refundPosts = {}; const completePosts = {}; const openDisputePosts = {}; +const resolvePosts = {}; function confirmOrder(orderId, reject = false) { if (!orderId) { @@ -369,3 +371,63 @@ export function openDispute(orderId, data = {}) { return openDisputePosts[orderId].xhr; } + +/** + * If the order with the given id is in the process of its dispute being resolved, + * this method will return an object containing the post xhr and the data that's + * being saved. + */ +export function resolvingDispute(orderId) { + return resolvePosts[orderId] || false; +} + +export function resolveDispute(orderId, data = {}) { + if (!orderId) { + throw new Error('Please provide an orderId'); + } + + if (!resolvePosts[orderId]) { + const model = new ResolveDispute(data); + const save = model.save(); + + if (!save) { + Object.keys(model.validationError) + .forEach(errorKey => { + throw new Error(`${errorKey}: ${model.validationError[errorKey][0]}`); + }); + } else { + save.always(() => { + delete resolvePosts[orderId]; + }).done(() => { + events.trigger('resolveDisputeComplete', { + id: orderId, + xhr: save, + }); + }) + .fail(xhr => { + events.trigger('resolveDisputeFail', { + id: orderId, + xhr: save, + }); + + const failReason = xhr.responseJSON && xhr.responseJSON.reason || ''; + openSimpleMessage( + app.polyglot.t('orderUtil.failedResolveHeading'), + failReason + ); + }); + + resolvePosts[orderId] = { + xhr: save, + data: model.toJSON(), + }; + } + + events.trigger('resolvingDispute', { + id: orderId, + xhr: save, + }); + } + + return resolvePosts[orderId].xhr; +} diff --git a/js/views/modals/orderDetail/OrderDetail.js b/js/views/modals/orderDetail/OrderDetail.js index 80d371437..612d6737c 100644 --- a/js/views/modals/orderDetail/OrderDetail.js +++ b/js/views/modals/orderDetail/OrderDetail.js @@ -3,11 +3,15 @@ import $ from 'jquery'; import app from '../../../app'; import { capitalize } from '../../../utils/string'; import { getSocket } from '../../../utils/serverConnect'; -import { events as orderEvents } from '../../../utils/order'; +import { + resolvingDispute, + events as orderEvents, +} from '../../../utils/order'; import loadTemplate from '../../../utils/loadTemplate'; import Case from '../../../models/order/Case'; import OrderFulfillment from '../../../models/order/orderFulfillment/OrderFulfillment'; import OrderDispute from '../../../models/order/OrderDispute'; +import ResolveDisputeMd from '../../../models/order/ResolveDispute'; import BaseModal from '../BaseModal'; import ProfileBox from './ProfileBox'; import Summary from './summaryTab/Summary'; @@ -51,18 +55,25 @@ export default class extends BaseModal { this.listenToOnce(this.model, 'sync', this.onFirstOrderSync); this.listenTo(this.model, 'change:unreadChatMessages', () => this.setUnreadChatMessagesBadge()); + this.listenTo(orderEvents, 'fulfillOrderComplete', () => { if (this.activeTab === 'fulfillOrder') this.selectTab('summary'); }); + this.listenTo(this.model, 'change:state', () => { if (this.actionBar) { this.actionBar.setState(this.actionBarButtonState); } }); + this.listenTo(orderEvents, 'openDisputeComplete', () => { if (this.activeTab === 'disputeOrder') this.selectTab('summary'); }); + this.listenTo(orderEvents, 'resolveDisputeComplete', () => { + if (this.activeTab === 'resolveDispute') this.selectTab('summary'); + }); + const socket = getSocket(); if (socket) { @@ -396,8 +407,19 @@ export default class extends BaseModal { } createResolveDisputeTabView() { - const model = new OrderDispute({ orderId: this.model.id }); + let modelAttrs = { orderId: this.model.id }; + const isResolvingDispute = resolvingDispute(this.model.id); + + // If this order is in the process of the dispute being resolved, we'll + // populate the model with the data that was posted to the server. + if (isResolvingDispute) { + modelAttrs = { + ...modelAttrs, + ...isResolvingDispute.data, + }; + } + const model = new ResolveDisputeMd(modelAttrs); const view = this.createChild(ResolveDispute, { model, vendor: { diff --git a/js/views/modals/orderDetail/ResolveDispute.js b/js/views/modals/orderDetail/ResolveDispute.js index 2db05bdc5..cf1113e4e 100644 --- a/js/views/modals/orderDetail/ResolveDispute.js +++ b/js/views/modals/orderDetail/ResolveDispute.js @@ -1,8 +1,10 @@ -// import { -// fulfillingOrder, -// fulfillOrder, -// events as orderEvents, -// } from '../../../utils/order'; +import $ from 'jquery'; +import { getAvatarBgImage } from '../../../utils/responsive'; +import { + resolvingDispute, + resolveDispute, + events as orderEvents, +} from '../../../utils/order'; import loadTemplate from '../../../utils/loadTemplate'; import BaseVw from '../../baseVw'; @@ -42,9 +44,30 @@ export default class extends BaseVw { throw new Error(getInvalidParticpantError('buyer')); } - // this.listenTo(orderEvents, 'fulfillingOrder', this.onFulfillingOrder); - // this.listenTo(orderEvents, 'fulfillOrderComplete, fulfillOrderFail', - // this.onFulfillOrderAlways); + options.buyer.getProfile().done(profile => { + this.buyerProfile = profile; + if (this.rendered) { + this.getCachedEl('.js-buyerName').text(this.buyerProfile.get('name')); + this.getCachedEl('.js-buyerAvatar') + .attr('style', getAvatarBgImage(profile.get('avatarHashes').toJSON())); + } + }); + + options.vendor.getProfile().done(profile => { + this.vendorProfile = profile; + if (this.rendered) { + this.getCachedEl('.js-vendorName').text(this.vendorProfile.get('name')); + this.getCachedEl('.js-vendorAvatar') + .attr('style', getAvatarBgImage(profile.get('avatarHashes').toJSON())); + } + }); + + this.listenTo(orderEvents, 'resolvingDispute', this.onResolvingDispute); + this.listenTo(orderEvents, 'resolveDisputeComplete resolveDisputeFail', + this.onResolveDisputeAlways); + + this.boundOnDocClick = this.onDocumentClick.bind(this); + $(document).on('click', this.boundOnDocClick); } className() { @@ -56,9 +79,26 @@ export default class extends BaseVw { 'click .js-backToSummary': 'onClickBackToSummary', 'click .js-cancel': 'onClickCancel', 'click .js-submit': 'onClickSubmit', + 'click .js-resolveConfirmed': 'onClickConfirmedSubmit', + 'click .js-resolveConfirm': 'onClickResolveConfirmBox', + 'click .js-resolveConfirmCancel': 'onClickCancelConfirm', }; } + onClickResolveConfirmBox() { + // ensure event doesn't bubble so onDocumentClick doesn't + // close the confirmBox. + return false; + } + + onClickCancelConfirm() { + this.getCachedEl('.js-resolveConfirm').addClass('hide'); + } + + onDocumentClick() { + this.getCachedEl('.js-resolveConfirm').addClass('hide'); + } + onClickBackToSummary() { this.trigger('clickBackToSummary'); } @@ -73,12 +113,17 @@ export default class extends BaseVw { } onClickSubmit() { + this.getCachedEl('.js-resolveConfirm').removeClass('hide'); + return false; + } + + onClickConfirmedSubmit() { const formData = this.getFormData(); this.model.set(formData); this.model.set({}, { validate: true }); if (!this.model.validationError) { - // fulfillOrder(this.model.id, this.model.toJSON()); + resolveDispute(this.model.id, this.model.toJSON()); } this.render(); @@ -86,30 +131,41 @@ export default class extends BaseVw { if ($firstErr.length) $firstErr[0].scrollIntoViewIfNeeded(); } - // onFulfillingOrder(e) { - // if (e.id === this.model.id) { - // this.$btnSubmit.addClass('processing'); - // this.$btnCancel.addClass('disabled'); - // } - // } + onResolvingDispute(e) { + if (e.id === this.model.id) { + this.getCachedEl('.js-submit').addClass('processing'); + this.getCachedEl('.js-cancel').addClass('disabled'); + } + } - // onFulfillOrderAlways(e) { - // if (e.id === this.model.id) { - // this.$btnSubmit.removeClass('processing'); - // this.$btnCancel.removeClass('disabled'); - // } - // } + onResolveDisputeAlways(e) { + if (e.id === this.model.id) { + this.getCachedEl('.js-submit').removeClass('processing'); + this.getCachedEl('.js-cancel').removeClass('disabled'); + } + } render() { + super.render(); loadTemplate('modals/orderDetail/resolveDispute.html', (t) => { - this.$el.html(t({ + const templateData = { ...this.model.toJSON(), errors: this.model.validationError || {}, - // fulfillingOrder: fulfillingOrder(this.model.id), - })); + resolvingDispute: resolvingDispute(this.model.id), + }; + + if (this.buyerProfile) { + templateData.buyerAvatarHashes = this.buyerProfile.get('avatarHashes').toJSON(); + templateData.buyerName = this.buyerProfile.get('name'); + } + + if (this.vendorProfile) { + templateData.vendorAvatarHashes = this.vendorProfile.get('avatarHashes').toJSON(); + templateData.vendorName = this.vendorProfile.get('name'); + } - this._$btnCancel = null; - this._$btnSubmit = null; + this.$el.html(t(templateData)); + this.rendered = true; }); return this; diff --git a/js/views/modals/orderDetail/summaryTab/Summary.js b/js/views/modals/orderDetail/summaryTab/Summary.js index efe5c078a..8df878c36 100644 --- a/js/views/modals/orderDetail/summaryTab/Summary.js +++ b/js/views/modals/orderDetail/summaryTab/Summary.js @@ -193,6 +193,16 @@ export default class extends BaseVw { if (!this.isCase()) { this.listenTo(this.contract, 'change:dispute', () => this.renderDisputeStartedView()); + } else { + // this.listenTo(this.contract, 'change:dispute', + // () => this.renderDisputeStartedView()); + + this.listenTo(orderEvents, 'resolveDisputeComplete', e => { + if (e.id === this.model.id) { + this.model.set('state', 'RESOLVED'); + this.model.fetch(); + } + }); } const serverSocket = getSocket(); @@ -592,6 +602,39 @@ export default class extends BaseVw { this.$subSections.prepend(this.disputeStarted.render().el); } + renderDisputePayoutView() { + const data = this.isCase() ? this.model.get('resolution') : + this.contract.get('disputeResolution'); + + if (!data) { + throw new Error('Unable to create the Dispute Payout view because the resolution ' + + 'data object has not been set.'); + } + + if (this.disputePayout) this.disputePayout.remove(); + this.disputePayout = this.createChild(DisputePayout, { + initialState: { + ...data, + showAcceptButton: !this.isCase() && this.model.get('state') === 'DECIDED' && + data.proposedBy !== app.profile.id, + }, + }); + + ['buyer', 'vendor', 'moderator'].forEach(type => { + this[type].getProfile().done(profile => { + const state = {}; + state[`${type}Name`] = profile.get('name'); + state[`${type}AvatarHashes`] = profile.get('avatarHashes'); + this.disputePayout.setState(state); + }); + }); + + this.listenTo(this.disputeStarted, 'clickResolveDispute', + () => this.trigger('clickResolveDispute')); + + this.$subSections.prepend(this.disputePayout.render().el); + } + /** * Will render sub-sections in order based on their timestamp. Exempt from * this are the Order Details, Payment Details and Accepted sections which diff --git a/js/views/transactions/Transactions.js b/js/views/transactions/Transactions.js index 89cc81229..aca217626 100644 --- a/js/views/transactions/Transactions.js +++ b/js/views/transactions/Transactions.js @@ -415,7 +415,8 @@ export default class extends baseVw { if (this.socket) { this.listenTo(this.socket, 'message', (e) => { if (e.jsonData.id === data.id) { - this.profileDeferreds[e.jsonData.peerId].resolve(new Profile(e.jsonData.profile)); + this.profileDeferreds[e.jsonData.peerId].resolve(new Profile(e.jsonData.profile, + { parse: true })); } }); } diff --git a/styles/components/_containers.scss b/styles/components/_containers.scss index d664d965f..ef1449fb7 100644 --- a/styles/components/_containers.scss +++ b/styles/components/_containers.scss @@ -271,6 +271,17 @@ } } +.arrowBoxBottom { + @extend .arrowBox; + + &::before { + bottom: 1px; + left: 50%; + transform: translate(-25%, 50%) rotate(-135deg); + -webkit-clip-path: polygon(0 0, 0 100%, 100% 0); + } +} + .toolTip { position: relative; cursor: pointer; diff --git a/styles/modules/modals/_orderDetail.scss b/styles/modules/modals/_orderDetail.scss index 33bccc2ca..ede3a7a24 100644 --- a/styles/modules/modals/_orderDetail.scss +++ b/styles/modules/modals/_orderDetail.scss @@ -396,10 +396,47 @@ } } - .disputeOrderTab { + .disputeOrderTab, .resolveDisputeTab { .avatar { width: $thumbTn; height: $thumbTn; } } + + .resolveDisputeTab { + .resolveConfirm { + bottom: 45px; + left: 50%; + transform: translateX(-50%); + } + + .inputBuyerAmountWrap, .inputVendorAmountWrap { + position: relative; + max-width: 175px; + + &::after { + content: '%'; + position: absolute; + right: 15px; + top: 20px; + transform: translateY(-50%); + } + + input { + padding-left: 45px; + padding-right: 40px; + } + + .avatar { + position: absolute; + top: 10px; + left: 13px; + } + } + + label[for=resolveDisputeBuyerAmount], + label[for=resolveDisputeVendorAmount] { + line-height: 2rem; + } + } } \ No newline at end of file From 1198c4bbab6de2e48570ac2b5f8f7ab992cafe08 Mon Sep 17 00:00:00 2001 From: Rob Misiorowski Date: Wed, 21 Jun 2017 12:51:50 -0700 Subject: [PATCH 11/38] implemented the resolve dispute section --- js/languages/en-US.json | 17 ++- .../orderDetail/summaryTab/disputePayout.html | 72 ++++++++++ js/utils/order.js | 51 +++++++ .../modals/orderDetail/summaryTab/Accepted.js | 5 + .../orderDetail/summaryTab/DisputePayout.js | 127 ++++++++++++++++++ .../orderDetail/summaryTab/DisputeStarted.js | 1 - .../modals/orderDetail/summaryTab/Payment.js | 5 + .../modals/orderDetail/summaryTab/Summary.js | 28 +++- styles/modules/modals/_orderDetail.scss | 21 ++- 9 files changed, 313 insertions(+), 14 deletions(-) create mode 100644 js/templates/modals/orderDetail/summaryTab/disputePayout.html create mode 100644 js/views/modals/orderDetail/summaryTab/DisputePayout.js diff --git a/js/languages/en-US.json b/js/languages/en-US.json index f1fa5e4d2..5e685ec4a 100644 --- a/js/languages/en-US.json +++ b/js/languages/en-US.json @@ -1053,6 +1053,20 @@ "genericIsDisputed": "The order is being disputed:", "noReasonProvided": "No reason was provided." }, + "disputePayout": { + "heading": "Dispute Payout", + "buyerHeading": "%{name} (buyer)", + "vendorHeading": "%{name} (vendor)", + "modHeading": "%{name} (moderator)", + "noteFromHeading": "Note from %{name}:", + "btnAcceptPayout": "Accept Payout", + "acceptPayoutConfirm": { + "title": "Are you sure?", + "body": "Once accepted, the payout will process immediately", + "btnCancel": "Cancel", + "btnConfirm": "Yes, Accept" + } + }, "orderDetails": { "progressBarStates": { "paid": "Paid", @@ -1131,7 +1145,8 @@ "failedRefundHeading": "There was an error refunding the order.", "failedCompleteHeading": "There was an error completing the order.", "failedOpenDisputeHeading": "There was an error completing the order.", - "failedResolveHeading": "There was an error resolving the order." + "failedResolveHeading": "There was an error resolving the order.", + "failedAcceptPayoutHeading": "There was an error accepting the payout." }, "exchangeRatesSyncer": { "fetchingRatesStatusMsg": "Fetching exchange rates…", diff --git a/js/templates/modals/orderDetail/summaryTab/disputePayout.html b/js/templates/modals/orderDetail/summaryTab/disputePayout.html new file mode 100644 index 000000000..7ebf688f3 --- /dev/null +++ b/js/templates/modals/orderDetail/summaryTab/disputePayout.html @@ -0,0 +1,72 @@ +<% + ob.payout.buyerOutput.amount = 0.1; + ob.payout.vendorOutput.amount = 0.05; + ob.payout.moderatorOutput.amount = 0.0001; + + let priceLines = {}; + + ['buyer', 'vendor', 'moderator'].forEach(type => { + priceLines[type] = ob.convertAndFormatCurrency(ob.payout[`${type}Output`].amount, + 'BTC', ob.userCurrency, { useBtcSymbol: false }); + + if (ob.userCurrency !== 'BTC') { + priceLines[type] = ob.polyT('fiatBtcPairing', { + fiatAmount: priceLines[type], + btcAmount: ob.formatCurrency(ob.payout[`${type}Output`].amount, 'BTC', { useBtcSymbol: false }), + }); + } + }); +%> +

<%= ob.polyT('orderDetail.summaryTab.disputePayout.heading') %>

+<% if (ob.timestamp) { %> +<%= ob.moment(ob.timestamp).format('lll') %> +<% } %> + +
+
+
+
+
+
+
<%= ob.polyT('orderDetail.summaryTab.disputePayout.buyerHeading', { name: ob.buyerName }) %>
+
<%= priceLines.buyer %>
+
+
+
+
+
+
<%= ob.polyT('orderDetail.summaryTab.disputePayout.vendorHeading', { name: ob.vendorName }) %>
+
<%= priceLines.vendor %>
+
+
+
+
+
+
<%= ob.polyT('orderDetail.summaryTab.disputePayout.modHeading', { name: ob.moderatorName }) %>
+
<%= priceLines.moderator %>
+
+
+
+
+
+ <% if (ob.showAcceptButton) { %> + <%= ob.processingButton({ + className: `btn clrBAttGrad clrBrDec1 clrTOnEmph tx5b js-acceptPayout ${ob.acceptInProgress ? 'processing' : ''} ${ob.acceptConfirmOn ? 'disabled' : '' }`, + btnText: ob.polyT('orderDetail.summaryTab.disputePayout.btnAcceptPayout') + }) %> + <% } %> + <% if (ob.acceptConfirmOn) { %> +
+
<%= ob.polyT('orderDetail.summaryTab.disputePayout.acceptPayoutConfirm.title') %>
+

<%= ob.polyT('orderDetail.summaryTab.disputePayout.acceptPayoutConfirm.body') %>

+
+ +
+ <% } %> +
+
+
+
\ No newline at end of file diff --git a/js/utils/order.js b/js/utils/order.js index f8e1c43e3..34b0876e9 100644 --- a/js/utils/order.js +++ b/js/utils/order.js @@ -19,6 +19,7 @@ const refundPosts = {}; const completePosts = {}; const openDisputePosts = {}; const resolvePosts = {}; +const acceptPayoutPosts = {}; function confirmOrder(orderId, reject = false) { if (!orderId) { @@ -431,3 +432,53 @@ export function resolveDispute(orderId, data = {}) { return resolvePosts[orderId].xhr; } + +export function acceptingPayout(orderId) { + return acceptPayoutPosts[orderId] || false; +} + +export function acceptPayout(orderId) { + if (!orderId) { + throw new Error('Please provide an orderId'); + } + + let post = acceptPayoutPosts[orderId]; + + if (!post) { + post = $.post({ + url: app.getServerUrl('ob/releasefunds'), + data: JSON.stringify({ + orderId, + }), + dataType: 'json', + contentType: 'application/json', + }).always(() => { + delete acceptPayoutPosts[orderId]; + }).done(() => { + events.trigger('acceptPayoutComplete', { + id: orderId, + xhr: post, + }); + }) + .fail(xhr => { + events.trigger('acceptPayoutFail', { + id: orderId, + xhr: post, + }); + + const failReason = xhr.responseJSON && xhr.responseJSON.reason || ''; + openSimpleMessage( + app.polyglot.t('orderUtil.failedAcceptPayoutHeading'), + failReason + ); + }); + + acceptPayoutPosts[orderId] = post; + events.trigger('acceptingPayout', { + id: orderId, + xhr: post, + }); + } + + return post; +} diff --git a/js/views/modals/orderDetail/summaryTab/Accepted.js b/js/views/modals/orderDetail/summaryTab/Accepted.js index 334fcde36..6ed50a7fc 100644 --- a/js/views/modals/orderDetail/summaryTab/Accepted.js +++ b/js/views/modals/orderDetail/summaryTab/Accepted.js @@ -119,6 +119,11 @@ export default class extends BaseVw { return this; } + remove() { + $(document).off('click', this.boundOnDocClick); + super.remove(); + } + render() { loadTemplate('modals/orderDetail/summaryTab/accepted.html', (t) => { this.$el.html(t({ diff --git a/js/views/modals/orderDetail/summaryTab/DisputePayout.js b/js/views/modals/orderDetail/summaryTab/DisputePayout.js new file mode 100644 index 000000000..a53b3e0b5 --- /dev/null +++ b/js/views/modals/orderDetail/summaryTab/DisputePayout.js @@ -0,0 +1,127 @@ +import $ from 'jquery'; +import app from '../../../../app'; +import _ from 'underscore'; +import moment from 'moment'; +import { + acceptingPayout, + acceptPayout, + events as orderEvents, +} from '../../../../utils/order'; +import loadTemplate from '../../../../utils/loadTemplate'; +import BaseVw from '../../../baseVw'; + +export default class extends BaseVw { + constructor(options = {}) { + super(options); + + if (!options.orderId) { + throw new Error('Please provide the orderId'); + } + + this.orderId = options.orderId; + + this._state = { + userCurrency: app.settings.get('localCurrency') || 'USD', + showAcceptButton: false, + acceptConfirmOn: false, + ...options.initialState || {}, + }; + + this.boundOnDocClick = this.onDocumentClick.bind(this); + $(document).on('click', this.boundOnDocClick); + + this.listenTo(orderEvents, 'acceptingPayout', e => { + if (e.id === this.orderId) { + this.setState({ acceptInProgress: true }); + } + }); + + this.listenTo(orderEvents, 'acceptPayoutComplete acceptPayoutFail', e => { + if (e.id === this.orderId) { + this.setState({ acceptInProgress: false }); + } + }); + + this.listenTo(orderEvents, 'acceptPayoutComplete', e => { + if (e.id === this.orderId) { + this.setState({ showAcceptButton: false }); + } + }); + } + + className() { + return 'disputePayoutEvent rowLg'; + } + + events() { + return { + 'click .js-acceptPayout': 'onClickAcceptPayout', + 'click .js-acceptPayoutConfirmBox': 'onClickAcceptPayoutConfirmedBox', + 'click .js-acceptPayoutConfirmed': 'onClickAcceptPayoutConfirmed', + 'click .js-acceptPayoutConfirmCancel': 'onClickAcceptPayoutConfirmCancel', + }; + } + + onDocumentClick() { + this.setState({ acceptConfirmOn: false }); + } + + onClickAcceptPayout() { + this.setState({ acceptConfirmOn: true }); + return false; + } + + onClickAcceptPayoutConfirmedBox() { + // ensure event doesn't bubble so onDocumentClick doesn't + // close the confirmBox. + return false; + } + + onClickAcceptPayoutConfirmCancel() { + this.setState({ acceptConfirmOn: false }); + } + + onClickAcceptPayoutConfirmed() { + this.setState({ acceptConfirmOn: false }); + acceptPayout(this.orderId); + } + + getState() { + return this._state; + } + + setState(state, replace = false, renderOnChange = true) { + let newState; + + if (replace) { + this._state = {}; + } else { + newState = _.extend({}, this._state, state); + } + + if (renderOnChange && !_.isEqual(this._state, newState)) { + this._state = newState; + this.render(); + } + + return this; + } + + remove() { + $(document).off('click', this.boundOnDocClick); + super.remove(); + } + + render() { + loadTemplate('modals/orderDetail/summaryTab/disputePayout.html', (t) => { + this.$el.html(t({ + ...this._state, + moment, + showAcceptButton: true, + acceptInProgress: acceptingPayout(this.orderId), + })); + }); + + return this; + } +} diff --git a/js/views/modals/orderDetail/summaryTab/DisputeStarted.js b/js/views/modals/orderDetail/summaryTab/DisputeStarted.js index 27082f4ab..06fb851ef 100644 --- a/js/views/modals/orderDetail/summaryTab/DisputeStarted.js +++ b/js/views/modals/orderDetail/summaryTab/DisputeStarted.js @@ -7,7 +7,6 @@ export default class extends BaseVw { constructor(options = {}) { super(options); - this.orderId = options.orderId; this._state = { disputerName: '', claim: '', diff --git a/js/views/modals/orderDetail/summaryTab/Payment.js b/js/views/modals/orderDetail/summaryTab/Payment.js index 8874de0d2..c72b0e3c6 100644 --- a/js/views/modals/orderDetail/summaryTab/Payment.js +++ b/js/views/modals/orderDetail/summaryTab/Payment.js @@ -101,6 +101,11 @@ export default class extends BaseVw { return this; } + remove() { + $(document).off('click', this.boundOnDocClick); + super.remove(); + } + render() { loadTemplate('modals/orderDetail/summaryTab/payment.html', (t) => { this.$el.html(t({ diff --git a/js/views/modals/orderDetail/summaryTab/Summary.js b/js/views/modals/orderDetail/summaryTab/Summary.js index 8df878c36..cc65ea2d5 100644 --- a/js/views/modals/orderDetail/summaryTab/Summary.js +++ b/js/views/modals/orderDetail/summaryTab/Summary.js @@ -20,6 +20,7 @@ import OrderDetails from './OrderDetails'; import CompleteOrderForm from './CompleteOrderForm'; import OrderComplete from './OrderComplete'; import DisputeStarted from './DisputeStarted'; +import DisputePayout from './DisputePayout'; export default class extends BaseVw { constructor(options = {}) { @@ -613,10 +614,10 @@ export default class extends BaseVw { if (this.disputePayout) this.disputePayout.remove(); this.disputePayout = this.createChild(DisputePayout, { + orderId: this.model.id, initialState: { ...data, - showAcceptButton: !this.isCase() && this.model.get('state') === 'DECIDED' && - data.proposedBy !== app.profile.id, + showAcceptButton: !this.isCase() && this.model.get('state') === 'DECIDED', }, }); @@ -624,7 +625,7 @@ export default class extends BaseVw { this[type].getProfile().done(profile => { const state = {}; state[`${type}Name`] = profile.get('name'); - state[`${type}AvatarHashes`] = profile.get('avatarHashes'); + state[`${type}AvatarHashes`] = profile.get('avatarHashes').toJSON(); this.disputePayout.setState(state); }); }); @@ -647,7 +648,7 @@ export default class extends BaseVw { sections.push({ function: this.renderRefundView, timestamp: - (new Date(this.model.get('refundAddressTransaction').timestamp)).getTime(), + (new Date(this.model.get('refundAddressTransaction').timestamp)), }); } @@ -655,7 +656,7 @@ export default class extends BaseVw { sections.push({ function: this.renderFulfilledView, timestamp: - (new Date(this.contract.get('vendorOrderFulfillment')[0].timestamp)).getTime(), + (new Date(this.contract.get('vendorOrderFulfillment')[0].timestamp)), }); } @@ -663,7 +664,7 @@ export default class extends BaseVw { sections.push({ function: this.renderOrderCompleteView, timestamp: - (new Date(this.contract.get('buyerOrderCompletion').timestamp)).getTime(), + (new Date(this.contract.get('buyerOrderCompletion').timestamp)), }); } @@ -675,7 +676,20 @@ export default class extends BaseVw { sections.push({ function: this.renderDisputeStartedView, timestamp: - (new Date(timestamp)).getTime(), + (new Date(timestamp)), + }); + } + + if (this.contract.get('disputeResolution') || + (this.isCase() && this.model.get('resolution'))) { + const timestamp = this.isCase() ? + this.model.get('resolution').timestamp : + this.contract.get('disputeResolution').timestamp; + + sections.push({ + function: this.renderDisputePayoutView, + timestamp: + (new Date(timestamp)), }); } diff --git a/styles/modules/modals/_orderDetail.scss b/styles/modules/modals/_orderDetail.scss index ede3a7a24..02a8133fb 100644 --- a/styles/modules/modals/_orderDetail.scss +++ b/styles/modules/modals/_orderDetail.scss @@ -313,12 +313,10 @@ .avatarCol { flex-shrink: 0; - width: 35px; - height: 35px; + width: 38px; + height: 38px; border-width: 2px; border-style: solid; - width: 38px; - height: 38px; } .orderDetails { @@ -345,7 +343,7 @@ .refundConfirm { left: 50%; transform: translateX(-50%); - top: 39px; + top: 39px; } } @@ -383,6 +381,19 @@ } } } + + .disputePayoutEvent { + .avatarCol { + position: relative; + top: -2px; + } + + .acceptPayoutConfirm { + left: 50%; + transform: translateX(-50%); + top: 45px; + } + } } .fulfillOrderTab, .disputeOrderTab, .resolveDisputeTab { From 1d6f164b9d5ce9c342cab105d3d02be554102bbf Mon Sep 17 00:00:00 2001 From: Rob Misiorowski Date: Wed, 21 Jun 2017 13:41:22 -0700 Subject: [PATCH 12/38] handling issue where payout amount not set for a party if the other party received 100% --- .../orderDetail/summaryTab/disputePayout.html | 17 ++++++++++++++--- .../modals/orderDetail/summaryTab/Summary.js | 11 ++++++++--- 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/js/templates/modals/orderDetail/summaryTab/disputePayout.html b/js/templates/modals/orderDetail/summaryTab/disputePayout.html index 7ebf688f3..2ff8f7e6b 100644 --- a/js/templates/modals/orderDetail/summaryTab/disputePayout.html +++ b/js/templates/modals/orderDetail/summaryTab/disputePayout.html @@ -1,7 +1,18 @@ <% - ob.payout.buyerOutput.amount = 0.1; - ob.payout.vendorOutput.amount = 0.05; - ob.payout.moderatorOutput.amount = 0.0001; + ob.payout.buyerOutput = { + ...ob.payout.buyerOutput || {}, + amount: 0.1, + }; + + ob.payout.vendorOutput = { + ...ob.payout.vendorOutput || {}, + amount: 0.05, + }; + + ob.payout.moderatorOutput = { + ...ob.payout.moderatorOutput || {}, + amount: 0.0001, + }; let priceLines = {}; diff --git a/js/views/modals/orderDetail/summaryTab/Summary.js b/js/views/modals/orderDetail/summaryTab/Summary.js index cc65ea2d5..568e12363 100644 --- a/js/views/modals/orderDetail/summaryTab/Summary.js +++ b/js/views/modals/orderDetail/summaryTab/Summary.js @@ -195,15 +195,15 @@ export default class extends BaseVw { this.listenTo(this.contract, 'change:dispute', () => this.renderDisputeStartedView()); } else { - // this.listenTo(this.contract, 'change:dispute', - // () => this.renderDisputeStartedView()); - this.listenTo(orderEvents, 'resolveDisputeComplete', e => { if (e.id === this.model.id) { this.model.set('state', 'RESOLVED'); this.model.fetch(); } }); + + this.listenTo(this.model, 'change:resolution', + () => this.renderDisputePayoutView()); } const serverSocket = getSocket(); @@ -247,6 +247,11 @@ export default class extends BaseVw { // When a party opens a dispute the mod and the other party will get this // notification this.model.fetch(); + } else if (e.jsonData.notification.disputeClose && + e.jsonData.notification.disputeClose.orderId === this.model.id) { + // Notification to the vendor and buyer when a mod has made a decision + // on an open dispute. + this.model.fetch(); } } }); From c6f3af0acd14d0bf741b50a71ce280dc81e417c9 Mon Sep 17 00:00:00 2001 From: Rob Misiorowski Date: Wed, 21 Jun 2017 13:52:08 -0700 Subject: [PATCH 13/38] tweak to the paymount amount logic in the disputePayout template --- .../orderDetail/summaryTab/disputePayout.html | 20 ++++++------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/js/templates/modals/orderDetail/summaryTab/disputePayout.html b/js/templates/modals/orderDetail/summaryTab/disputePayout.html index 2ff8f7e6b..6f6b48150 100644 --- a/js/templates/modals/orderDetail/summaryTab/disputePayout.html +++ b/js/templates/modals/orderDetail/summaryTab/disputePayout.html @@ -1,18 +1,10 @@ <% - ob.payout.buyerOutput = { - ...ob.payout.buyerOutput || {}, - amount: 0.1, - }; - - ob.payout.vendorOutput = { - ...ob.payout.vendorOutput || {}, - amount: 0.05, - }; - - ob.payout.moderatorOutput = { - ...ob.payout.moderatorOutput || {}, - amount: 0.0001, - }; + ob.payout.buyerOutput = ob.payout.buyerOutput || {}; + ob.payout.buyerOutput.amount = 0.1; + ob.payout.vendorOutput = ob.payout.vendorOutput || {}; + ob.payout.vendorOutput.amount = 0.05; + ob.payout.moderatorOutput = ob.payout.moderatorOutput || {}; + ob.payout.moderatorOutput.amount = 0.0001; let priceLines = {}; From 376dc9d58ee4a23093d129ea3d5d8999077d76bc Mon Sep 17 00:00:00 2001 From: Rob Misiorowski Date: Wed, 21 Jun 2017 16:49:49 -0700 Subject: [PATCH 14/38] cleanup related to the dispute payout section --- js/languages/en-US.json | 12 +++++--- .../orderDetail/summaryTab/disputePayout.html | 30 +++++++++++++++---- .../summaryTab/disputeStarted.html | 2 +- .../orderDetail/summaryTab/DisputePayout.js | 1 - .../orderDetail/summaryTab/DisputeStarted.js | 10 ++++++- .../modals/orderDetail/summaryTab/Summary.js | 24 +++++++++++++-- 6 files changed, 63 insertions(+), 16 deletions(-) diff --git a/js/languages/en-US.json b/js/languages/en-US.json index 5e685ec4a..b8808ba25 100644 --- a/js/languages/en-US.json +++ b/js/languages/en-US.json @@ -1055,10 +1055,14 @@ }, "disputePayout": { "heading": "Dispute Payout", - "buyerHeading": "%{name} (buyer)", - "vendorHeading": "%{name} (vendor)", - "modHeading": "%{name} (moderator)", - "noteFromHeading": "Note from %{name}:", + "buyerHeading": "Buyer", + "buyerHeadingWithName": "%{name} (buyer)", + "vendorHeading": "Vendor", + "vendorHeadingWithName": "%{name} (vendor)", + "moderatorHeading": "Moderator", + "moderatorHeadingWithName": "%{name} (moderator)", + "noteFromHeading": "Note from moderator:", + "noteFromHeadingWithName": "Note from %{name}:", "btnAcceptPayout": "Accept Payout", "acceptPayoutConfirm": { "title": "Are you sure?", diff --git a/js/templates/modals/orderDetail/summaryTab/disputePayout.html b/js/templates/modals/orderDetail/summaryTab/disputePayout.html index 6f6b48150..db0a549dc 100644 --- a/js/templates/modals/orderDetail/summaryTab/disputePayout.html +++ b/js/templates/modals/orderDetail/summaryTab/disputePayout.html @@ -7,6 +7,7 @@ ob.payout.moderatorOutput.amount = 0.0001; let priceLines = {}; + let partyHeadings = {}; ['buyer', 'vendor', 'moderator'].forEach(type => { priceLines[type] = ob.convertAndFormatCurrency(ob.payout[`${type}Output`].amount, @@ -18,6 +19,10 @@ btcAmount: ob.formatCurrency(ob.payout[`${type}Output`].amount, 'BTC', { useBtcSymbol: false }), }); } + + partyHeadings[type] = ob[`${type}Name`] ? + ob.polyT(`orderDetail.summaryTab.disputePayout.${type}HeadingWithName`, { name: ob[`${type}Name`] }) : + ob.polyT(`orderDetail.summaryTab.disputePayout.${type}Heading`); }); %>

<%= ob.polyT('orderDetail.summaryTab.disputePayout.heading') %>

@@ -26,26 +31,26 @@

<%= ob.polyT('orderDetail.summaryTab.disputePayout.headi <% } %>
-
+
-
+
-
<%= ob.polyT('orderDetail.summaryTab.disputePayout.buyerHeading', { name: ob.buyerName }) %>
+
<%= partyHeadings.buyer %>
<%= priceLines.buyer %>
-
<%= ob.polyT('orderDetail.summaryTab.disputePayout.vendorHeading', { name: ob.vendorName }) %>
+
<%= partyHeadings.vendor %>
<%= priceLines.vendor %>
-
<%= ob.polyT('orderDetail.summaryTab.disputePayout.modHeading', { name: ob.moderatorName }) %>
+
<%= partyHeadings.moderator %>
<%= priceLines.moderator %>
@@ -64,7 +69,7 @@

<%= ob.polyT('orderDetail.summaryTab.disputePayout.headi

<%= ob.polyT('orderDetail.summaryTab.disputePayout.acceptPayoutConfirm.body') %>


@@ -72,4 +77,17 @@

<%= ob.polyT('orderDetail.summaryTab.disputePayout.headi

+
+ + +
+ <% + const noteFromHeading = ob.moderatorName ? + ob.polyT('orderDetail.summaryTab.disputePayout.noteFromHeadingWithName', { name: ob.moderatorName }) : + ob.polyT('orderDetail.summaryTab.disputePayout.noteFromHeading'); + %> +
<%= noteFromHeading %>
+
<%= ob.resolution %>
+
+

\ No newline at end of file diff --git a/js/templates/modals/orderDetail/summaryTab/disputeStarted.html b/js/templates/modals/orderDetail/summaryTab/disputeStarted.html index 3cd9aa372..4b0005f57 100644 --- a/js/templates/modals/orderDetail/summaryTab/disputeStarted.html +++ b/js/templates/modals/orderDetail/summaryTab/disputeStarted.html @@ -18,7 +18,7 @@

<%= ob.polyT('orderDetail.summaryTab.disputeStarted.head
<% if (ob.showResolveButton) { %> <%= ob.processingButton({ - className: `btn clrBAttGrad clrBrDec1 clrTOnEmph tx5b js-resolveDispute ${ob.resolveInProgress ? 'processing' : ''}`, + className: `btn clrBAttGrad clrBrDec1 clrTOnEmph tx5b js-resolveDispute`, btnText: ob.polyT('orderDetail.summaryTab.disputeStarted.resolveBtn') }) %> <% } %> diff --git a/js/views/modals/orderDetail/summaryTab/DisputePayout.js b/js/views/modals/orderDetail/summaryTab/DisputePayout.js index a53b3e0b5..4e6b8fc61 100644 --- a/js/views/modals/orderDetail/summaryTab/DisputePayout.js +++ b/js/views/modals/orderDetail/summaryTab/DisputePayout.js @@ -117,7 +117,6 @@ export default class extends BaseVw { this.$el.html(t({ ...this._state, moment, - showAcceptButton: true, acceptInProgress: acceptingPayout(this.orderId), })); }); diff --git a/js/views/modals/orderDetail/summaryTab/DisputeStarted.js b/js/views/modals/orderDetail/summaryTab/DisputeStarted.js index 06fb851ef..89838b5c4 100644 --- a/js/views/modals/orderDetail/summaryTab/DisputeStarted.js +++ b/js/views/modals/orderDetail/summaryTab/DisputeStarted.js @@ -1,5 +1,8 @@ import _ from 'underscore'; import moment from 'moment'; +import { + events as orderEvents, +} from '../../../../utils/order'; import loadTemplate from '../../../../utils/loadTemplate'; import BaseVw from '../../../baseVw'; @@ -11,9 +14,14 @@ export default class extends BaseVw { disputerName: '', claim: '', showResolveButton: true, - resolveInProgress: false, ...options.initialState || {}, }; + + this.listenTo(orderEvents, 'resolveDisputeComplete', () => { + this.setState({ + showResolveButton: false, + }); + }); } className() { diff --git a/js/views/modals/orderDetail/summaryTab/Summary.js b/js/views/modals/orderDetail/summaryTab/Summary.js index 568e12363..316b6042d 100644 --- a/js/views/modals/orderDetail/summaryTab/Summary.js +++ b/js/views/modals/orderDetail/summaryTab/Summary.js @@ -194,6 +194,24 @@ export default class extends BaseVw { if (!this.isCase()) { this.listenTo(this.contract, 'change:dispute', () => this.renderDisputeStartedView()); + + this.listenTo(this.contract, 'change:disputeResolution', () => { + // Only render the dispute payout the first time we receive it + // (it changes from undefined to an object with data). It shouldn't + // be changing after that, but for some reason it is. + if (!this.contract.previous('disputeResolution')) { + // The timeout is needed in the handler so the updated + // order state is available. + setTimeout(() => this.renderDisputePayoutView()); + } + }); + + this.listenTo(orderEvents, 'acceptPayoutComplete', e => { + if (e.id === this.model.id) { + this.model.set('state', 'RESOLVED'); + this.model.fetch(); + } + }); } else { this.listenTo(orderEvents, 'resolveDisputeComplete', e => { if (e.id === this.model.id) { @@ -314,15 +332,15 @@ export default class extends BaseVw { switch (orderState) { case 'DECIDED': - state.currentState = 1; + state.currentState = 2; state.disputeState = 0; break; case 'RESOLVED': - state.currentState = 2; + state.currentState = 3; state.disputeState = 0; break; case 'COMPLETE': - state.currentState = 3; + state.currentState = 4; state.disputeState = 0; break; default: From b2704f9a38bca49e3ca5a3811818e36871804cf9 Mon Sep 17 00:00:00 2001 From: Rob Misiorowski Date: Wed, 21 Jun 2017 16:52:40 -0700 Subject: [PATCH 15/38] removing the pending filter from the cases tab --- js/views/transactions/Transactions.js | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/js/views/transactions/Transactions.js b/js/views/transactions/Transactions.js index aca217626..489b53c3d 100644 --- a/js/views/transactions/Transactions.js +++ b/js/views/transactions/Transactions.js @@ -238,7 +238,7 @@ export default class extends baseVw { return { search: '', sortBy: 'UNREAD', - states: [10, 11, 12], + states: [10, 12], }; } @@ -251,13 +251,6 @@ export default class extends baseVw { className: 'filter', targetState: [10], }, - { - id: 'filterDisputePending', - text: app.polyglot.t('transactions.filters.disputePending'), - checked: this.salesPurchasesDefaultFilter.states.indexOf(11) > -1, - className: 'filter', - targetState: [11], - }, { id: 'filterDisputeClosed', text: app.polyglot.t('transactions.filters.disputeClosed'), From 8e21de9d4e7509419a004c60e3982d2384f9d11c Mon Sep 17 00:00:00 2001 From: Rob Misiorowski Date: Thu, 22 Jun 2017 08:58:37 -0700 Subject: [PATCH 16/38] managaing the complet order form when a diuspute is opened and closed --- .../modals/orderDetail/summaryTab/Summary.js | 51 +++++++++++++------ 1 file changed, 36 insertions(+), 15 deletions(-) diff --git a/js/views/modals/orderDetail/summaryTab/Summary.js b/js/views/modals/orderDetail/summaryTab/Summary.js index 316b6042d..0dad6de08 100644 --- a/js/views/modals/orderDetail/summaryTab/Summary.js +++ b/js/views/modals/orderDetail/summaryTab/Summary.js @@ -1,4 +1,3 @@ -import $ from 'jquery'; import app from '../../../../app'; import { clipboard } from 'electron'; import '../../../../utils/velocity'; @@ -105,6 +104,11 @@ export default class extends BaseVw { if (state === 'COMPLETED' && this.completeOrderForm) { this.completeOrderForm.remove(); } + + if (this.completeOrderForm && + ['FULFILLED', 'RESOLVED'].indexOf(state) === -1) { + this.completeOrderForm.remove(); + } }); if (!this.isCase()) { @@ -203,6 +207,16 @@ export default class extends BaseVw { // The timeout is needed in the handler so the updated // order state is available. setTimeout(() => this.renderDisputePayoutView()); + } else { + // NOTE!!!!!!! + // NOTE!!!!!!! + // NOTE!!!!!!! + // Temporarily putting this here. Once the server issue is + // fixed and a disputeClosed event comes, then this should + // go in the change handler of that event. + if (this.buyer.id === app.profile.id) { + this.renderCompleteOrderForm(); + } } }); @@ -529,6 +543,25 @@ export default class extends BaseVw { this.$subSections.prepend(this.refunded.render().el); } + renderCompleteOrderForm() { + if (['FULFILLED', 'RESOLVED'].indexOf(this.model.get('state')) > -1 && + this.buyer.id === app.profile.id) { + throw new Error('The complete order form should only be showed for the buyer and ' + + 'when the order is in a state of FULFILLED or REOLVED'); + } + + const completingObject = completingOrder(this.model.id); + const model = new OrderCompletion( + completingObject ? completingObject.data : { orderId: this.model.id }); + if (this.completeOrderForm) this.completeOrderForm.remove(); + this.completeOrderForm = this.createChild(CompleteOrderForm, { + model, + slug: this.contract.get('vendorListings').at(0).get('slug'), + }); + + this.$subSections.prepend(this.completeOrderForm.render().el); + } + renderFulfilledView() { const data = this.contract.get('vendorOrderFulfillment'); @@ -550,26 +583,14 @@ export default class extends BaseVw { .done(profile => this.fulfilled.setState({ storeName: profile.get('name') })); - const sections = document.createDocumentFragment(); - const $sections = $(sections).append(this.fulfilled.render().el); + this.$subSections.prepend(this.fulfilled.render().el); // If the order is not complete and this is the buyer, we'll // render a complete order form. if (['FULFILLED', 'RESOLVED'].indexOf(this.model.get('state')) > -1 && this.buyer.id === app.profile.id) { - const completingObject = completingOrder(this.model.id); - const model = new OrderCompletion( - completingObject ? completingObject.data : { orderId: this.model.id }); - if (this.completeOrderForm) this.completeOrderForm.remove(); - this.completeOrderForm = this.createChild(CompleteOrderForm, { - model, - slug: this.contract.get('vendorListings').at(0).get('slug'), - }); - - $sections.prepend(this.completeOrderForm.render().el); + this.renderCompleteOrderForm(); } - - this.$subSections.prepend($sections); } renderOrderCompleteView() { From a3ed8697e53978f427126f39f4f9787e5e4216e3 Mon Sep 17 00:00:00 2001 From: Rob Misiorowski Date: Thu, 22 Jun 2017 09:51:33 -0700 Subject: [PATCH 17/38] adjusting the accepted event state when a dispute is opened --- .../modals/orderDetail/summaryTab/Summary.js | 26 +++++++++++-------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/js/views/modals/orderDetail/summaryTab/Summary.js b/js/views/modals/orderDetail/summaryTab/Summary.js index 0dad6de08..e9e37982f 100644 --- a/js/views/modals/orderDetail/summaryTab/Summary.js +++ b/js/views/modals/orderDetail/summaryTab/Summary.js @@ -93,21 +93,25 @@ export default class extends BaseVw { if (this.accepted) this.accepted.remove(); } - if (state === 'REFUNDED' || state === 'FULFILLED' && this.accepted) { - this.accepted.setState({ - showRefundButton: false, + if ( + ['REFUNDED', 'FULFILLED', 'DISPUTED', 'DECIDED', 'RESOLVED', 'COMPLETE'].indexOf(state) > -1 + && this.accepted) { + const acceptedState = { showFulfillButton: false, infoText: app.polyglot.t('orderDetail.summaryTab.accepted.vendorReceived'), - }); - } + }; - if (state === 'COMPLETED' && this.completeOrderForm) { - this.completeOrderForm.remove(); + if (state !== 'DISPUTED') { + acceptedState.showRefundButton = false; + } + + this.accepted.setState(acceptedState); } if (this.completeOrderForm && - ['FULFILLED', 'RESOLVED'].indexOf(state) === -1) { + ['FULFILLED', 'RESOLVED'].indexOf(state) > -1) { this.completeOrderForm.remove(); + this.completeOrderForm = null; } }); @@ -544,9 +548,9 @@ export default class extends BaseVw { } renderCompleteOrderForm() { - if (['FULFILLED', 'RESOLVED'].indexOf(this.model.get('state')) > -1 && - this.buyer.id === app.profile.id) { - throw new Error('The complete order form should only be showed for the buyer and ' + + if (['FULFILLED', 'RESOLVED'].indexOf(this.model.get('state')) === -1 || + this.buyer.id !== app.profile.id) { + throw new Error('The complete order form should only be shown for the buyer and ' + 'when the order is in a state of FULFILLED or REOLVED'); } From b0d6f6e7958e38c143abaadc29666d61fe70e9ca Mon Sep 17 00:00:00 2001 From: Rob Misiorowski Date: Thu, 22 Jun 2017 12:42:58 -0700 Subject: [PATCH 18/38] removing unused import --- js/languages/en-US.json | 2 +- js/views/modals/orderDetail/ActionBar.js | 4 ---- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/js/languages/en-US.json b/js/languages/en-US.json index b8808ba25..87b3365b7 100644 --- a/js/languages/en-US.json +++ b/js/languages/en-US.json @@ -1024,7 +1024,7 @@ "noteFromLabel": "Note from %{store}:", "copyLink": "Copy", "digitalReadyForDlHeading": "Digital files are ready for download!", - "digitalReadyForDlText": "The files have been delivered. DOwnload when you're ready.", + "digitalReadyForDlText": "The files have been delivered. Download when you're ready.", "urlLabel": "File URL", "passwordLabel": "Password" }, diff --git a/js/views/modals/orderDetail/ActionBar.js b/js/views/modals/orderDetail/ActionBar.js index bcf5d9751..b2894864d 100644 --- a/js/views/modals/orderDetail/ActionBar.js +++ b/js/views/modals/orderDetail/ActionBar.js @@ -1,8 +1,4 @@ import _ from 'underscore'; -// import { -// openingDispute, -// events as orderEvents, -// } from '../../../utils/order'; import loadTemplate from '../../../utils/loadTemplate'; import BaseVw from '../../baseVw'; From 8ba03e6552082070fa44ad2eb29c21970867469a Mon Sep 17 00:00:00 2001 From: Rob Misiorowski Date: Thu, 22 Jun 2017 14:53:07 -0700 Subject: [PATCH 19/38] tweaked the purchase payment view to accept data in a more generic way --- .../modals/purchase/confirmWallet.html | 4 +- js/templates/modals/purchase/payment.html | 8 +- js/views/modals/purchase/ConfirmWallet.js | 14 ++- js/views/modals/purchase/Payment.js | 97 ++++++++++++++----- js/views/modals/purchase/Purchase.js | 20 ++-- 5 files changed, 99 insertions(+), 44 deletions(-) diff --git a/js/templates/modals/purchase/confirmWallet.html b/js/templates/modals/purchase/confirmWallet.html index 0503f37aa..3ed529f69 100644 --- a/js/templates/modals/purchase/confirmWallet.html +++ b/js/templates/modals/purchase/confirmWallet.html @@ -20,7 +20,7 @@

const msg = ob.displayCurrency === 'BTC' ? 'payBTC' : 'payFiat'; if (requiredTotal && fundGap <= 0) { %> - <%= ob.polyT(`purchase.pendingSection.confirmWallet.${msg}`, { amountBTC: ob.amountBTC, amountFiat }) %> + <%= ob.polyT(`purchase.pendingSection.confirmWallet.${msg}`, { amountBTC: ob.formatCurrency(ob.amount, 'BTC'), amountFiat }) %> <% } else { %> <%= ob.polyT('purchase.pendingSection.confirmWallet.insufficientFundsMsg1', { funds: fundsBTC }) %>

@@ -31,7 +31,7 @@

<% if (requiredTotal && fundGap > 0) { %>

- <%= ob.polyT('purchase.pendingSection.confirmWallet.insufficientFundsNote', { amount: ob.amountBTC, fee: feeBTC }) %> + <%= ob.polyT('purchase.pendingSection.confirmWallet.insufficientFundsNote', { amount: ob.formatCurrency(ob.amount, 'BTC'), fee: feeBTC }) %>

<% } %> diff --git a/js/templates/modals/purchase/payment.html b/js/templates/modals/purchase/payment.html index 5c7d98724..cae1378f0 100644 --- a/js/templates/modals/purchase/payment.html +++ b/js/templates/modals/purchase/payment.html @@ -1,14 +1,12 @@
- +
- - <%= ob.polyT('purchase.pendingSection.pay', { amountBTC: ob.amountBTC }) %> - + <%= ob.amountDueLine %>
<% } %> -<% if (ob.shouldShowPayForOrderSection) { %> -
-

<%= ob.polyT('orderDetail.summaryTab.payForOrder.heading') %>

-
-

Section under construction

-

<%= ob.paymentAddress %> needs to be funded with <%= ob.upToFixed(ob.balanceRemaining, 8) %> BTC.

-
-
-<% } %> +
diff --git a/js/templates/modals/purchase/payment.html b/js/templates/modals/purchase/payment.html index cae1378f0..522a95e07 100644 --- a/js/templates/modals/purchase/payment.html +++ b/js/templates/modals/purchase/payment.html @@ -1,8 +1,12 @@ -
-
+ +
+
-
+
diff --git a/js/views/modals/orderDetail/summaryTab/Payments.js b/js/views/modals/orderDetail/summaryTab/Payments.js index d202d3f99..69023b39e 100644 --- a/js/views/modals/orderDetail/summaryTab/Payments.js +++ b/js/views/modals/orderDetail/summaryTab/Payments.js @@ -177,9 +177,11 @@ export default class extends baseVw { this.payments = []; this.collection.models.forEach((payment, index) => { - const paidSoFar = this.collection.models + let paidSoFar = this.collection.models .slice(0, index + 1) .reduce((total, model) => total + model.get('value'), 0); + // round to 8 decimal places + paidSoFar = Math.round(paidSoFar * 100000000) / 100000000; const isMostRecentPayment = index === this.collection.length - 1; const paymentView = this.createPayment(payment, { initialState: { diff --git a/js/views/modals/orderDetail/summaryTab/Summary.js b/js/views/modals/orderDetail/summaryTab/Summary.js index e9e37982f..95b7941e2 100644 --- a/js/views/modals/orderDetail/summaryTab/Summary.js +++ b/js/views/modals/orderDetail/summaryTab/Summary.js @@ -20,6 +20,7 @@ import CompleteOrderForm from './CompleteOrderForm'; import OrderComplete from './OrderComplete'; import DisputeStarted from './DisputeStarted'; import DisputePayout from './DisputePayout'; +import PayForOrder from '../../../modals/purchase/Payment'; export default class extends BaseVw { constructor(options = {}) { @@ -117,8 +118,9 @@ export default class extends BaseVw { if (!this.isCase()) { this.listenTo(this.model.get('paymentAddressTransactions'), 'update', () => { - if (!this.shouldShowPayForOrderSection()) { - this.$('.js-payForOrderWrap').remove(); + if (this.payForOrder && !this.shouldShowPayForOrderSection()) { + this.payForOrder.remove(); + this.payForOrder = null; } if (this.payments) { @@ -428,7 +430,8 @@ export default class extends BaseVw { balanceRemaining = this.orderPriceBtc - totalPaid; } - return balanceRemaining; + // round to 8 decimal places + return Math.round(balanceRemaining * 100000000) / 100000000; } shouldShowPayForOrderSection() { @@ -684,6 +687,19 @@ export default class extends BaseVw { this.$subSections.prepend(this.disputePayout.render().el); } + renderPayForOrder() { + if (this.payForOrder) this.payForOrder.remove(); + + this.payForOrder = this.createChild(PayForOrder, { + balanceRemaining: this.getBalanceRemaining(), + paymentAddress: this.paymentAddress, + orderId: this.model.id, + isModerated: !!this.moderator, + }); + + this.getCachedEl('.js-payForOrderWrap').html(this.payForOrder.render().el); + } + /** * Will render sub-sections in order based on their timestamp. Exempt from * this are the Order Details, Payment Details and Accepted sections which @@ -761,21 +777,13 @@ export default class extends BaseVw { } render() { - const templateData = { - id: this.model.id, - shouldShowPayForOrderSection: this.shouldShowPayForOrderSection(), - isCase: this.isCase(), - paymentAddress: this.paymentAddress, - isTestnet: app.testnet, - ...this.model.toJSON(), - }; - - if (this.shouldShowPayForOrderSection()) { - templateData.balanceRemaining = this.getBalanceRemaining(); - } - loadTemplate('modals/orderDetail/summaryTab/summary.html', t => { - this.$el.html(t(templateData)); + this.$el.html(t({ + id: this.model.id, + isCase: this.isCase(), + isTestnet: app.testnet, + ...this.model.toJSON(), + })); this._$copiedToClipboard = null; if (this.stateProgressBar) this.stateProgressBar.remove(); @@ -791,6 +799,10 @@ export default class extends BaseVw { }); this.$('.js-orderDetailsWrap').html(this.orderDetails.render().el); + if (this.shouldShowPayForOrderSection()) { + this.renderPayForOrder(); + } + if (!this.isCase()) { if (this.payments) this.payments.remove(); this.payments = this.createChild(Payments, { diff --git a/js/views/modals/purchase/Payment.js b/js/views/modals/purchase/Payment.js index a7b183162..450d6f833 100644 --- a/js/views/modals/purchase/Payment.js +++ b/js/views/modals/purchase/Payment.js @@ -1,3 +1,8 @@ +/* + This view is also used by the Order Detail overlay. If you make any changes, please + ensure they are compatible with both the Purchase and Order Detail flows. +*/ + import $ from 'jquery'; import app from '../../../app'; import loadTemplate from '../../../utils/loadTemplate'; @@ -48,11 +53,11 @@ export default class extends BaseVw { const amount = integerToDecimal(payment.fundingTotal, true); if (amount >= this.balanceRemaining) { this.trigger('walletPaymentComplete', payment); + } else { + // Ensure the resulting balance has a maximum of 8 decimal places with not + // trailing zeros. + this.balanceRemaining = parseFloat((this.balanceRemaining - amount).toFixed(8)); } - - // Ensure the result balance has a maximum of 8 decimal places with not - // trailing zeros. - this.balanceRemaining = parseFloat((this.balanceRemaining - amount).toFixed(8)); } } }); diff --git a/styles/modules/modals/_orderDetail.scss b/styles/modules/modals/_orderDetail.scss index 02a8133fb..043e50c25 100644 --- a/styles/modules/modals/_orderDetail.scss +++ b/styles/modules/modals/_orderDetail.scss @@ -394,6 +394,12 @@ top: 45px; } } + + .payForOrderWrap { + &:empty { + display: none; + } + } } .fulfillOrderTab, .disputeOrderTab, .resolveDisputeTab { From f2ef4638ead2975b67ee2739e7ce7be2edea1579 Mon Sep 17 00:00:00 2001 From: Rob Misiorowski Date: Thu, 22 Jun 2017 16:28:28 -0700 Subject: [PATCH 21/38] fixing issue where the dispute icon remains in the middle of the progress bar on a refund --- js/views/modals/orderDetail/summaryTab/Summary.js | 1 + 1 file changed, 1 insertion(+) diff --git a/js/views/modals/orderDetail/summaryTab/Summary.js b/js/views/modals/orderDetail/summaryTab/Summary.js index 95b7941e2..17002de5d 100644 --- a/js/views/modals/orderDetail/summaryTab/Summary.js +++ b/js/views/modals/orderDetail/summaryTab/Summary.js @@ -389,6 +389,7 @@ export default class extends BaseVw { `orderDetail.summaryTab.orderDetails.progressBarStates.${orderState.toLowerCase()}`), ]; state.currentState = 2; + state.disputeState = 0; } else { switch (orderState) { case 'PENDING': From 28586f55920b346a4f351dbb1ffcd326ef7baa9b Mon Sep 17 00:00:00 2001 From: Rob Misiorowski Date: Fri, 23 Jun 2017 10:15:43 -0700 Subject: [PATCH 22/38] updating the state in the transactions tables based on state change in the order detail overlay --- js/views/transactions/Transactions.js | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/js/views/transactions/Transactions.js b/js/views/transactions/Transactions.js index 489b53c3d..518a72b8b 100644 --- a/js/views/transactions/Transactions.js +++ b/js/views/transactions/Transactions.js @@ -159,6 +159,24 @@ export default class extends baseVw { this.stopListening(orderDetail, 'close', onClose); }); + // On any changes to the order / case detail model state, we'll update the + // state in the correponding model in the respective collection driving + // the transaction table. + this.listenTo(model, 'change:state', (md, state) => { + let col = this.purchasesCol; + + if (type === 'sale') { + col = this.salesCol; + } else if (type === 'case') { + col = this.casesCol; + } + + const collectionMd = col.get(model.id); + if (collectionMd) { + collectionMd.set('state', state); + } + }); + return orderDetail; } From 0f816ab5ba0cf8c8eba2ed4375cc6b4e42e679cc Mon Sep 17 00:00:00 2001 From: Rob Misiorowski Date: Fri, 23 Jun 2017 12:18:56 -0700 Subject: [PATCH 23/38] remove the crowd-funding listing type --- js/languages/en-US.json | 1 - js/models/listing/Metadata.js | 1 - 2 files changed, 2 deletions(-) diff --git a/js/languages/en-US.json b/js/languages/en-US.json index a5fe50a55..be99a9c7a 100644 --- a/js/languages/en-US.json +++ b/js/languages/en-US.json @@ -600,7 +600,6 @@ "PHYSICAL_GOOD": "Physical Good", "DIGITAL_GOOD": "Digital Good", "SERVICE": "Service", - "CROWD_FUND": "Crowd fund" }, "conditionTypes": { "NEW": "New", diff --git a/js/models/listing/Metadata.js b/js/models/listing/Metadata.js index 619f98071..d9e269b40 100644 --- a/js/models/listing/Metadata.js +++ b/js/models/listing/Metadata.js @@ -17,7 +17,6 @@ export default class extends BaseModel { 'PHYSICAL_GOOD', 'DIGITAL_GOOD', 'SERVICE', - 'CROWD_FUND', ]; } From 9f56475e944625ed468bcbfe0e0d75694429e23a Mon Sep 17 00:00:00 2001 From: Rob Misiorowski Date: Mon, 26 Jun 2017 10:26:26 -0700 Subject: [PATCH 24/38] update based on changed function name --- js/languages/en-US.json | 2 +- js/models/order/Contract.js | 4 ++++ js/views/modals/listingDetail/Listing.js | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/js/languages/en-US.json b/js/languages/en-US.json index be99a9c7a..16ce9a06f 100644 --- a/js/languages/en-US.json +++ b/js/languages/en-US.json @@ -599,7 +599,7 @@ "formats": { "PHYSICAL_GOOD": "Physical Good", "DIGITAL_GOOD": "Digital Good", - "SERVICE": "Service", + "SERVICE": "Service" }, "conditionTypes": { "NEW": "New", diff --git a/js/models/order/Contract.js b/js/models/order/Contract.js index 1b11db17d..9339876aa 100644 --- a/js/models/order/Contract.js +++ b/js/models/order/Contract.js @@ -15,6 +15,10 @@ export default class extends BaseModel { .get('contractType'); } + get orderTotal() { + return 99; + } + parse(response) { return { ...response, diff --git a/js/views/modals/listingDetail/Listing.js b/js/views/modals/listingDetail/Listing.js index 336b3bf73..a81a8cd25 100644 --- a/js/views/modals/listingDetail/Listing.js +++ b/js/views/modals/listingDetail/Listing.js @@ -312,7 +312,7 @@ export default class extends BaseModal { this.totalPrice = _totalPrice; const adjPrice = convertAndFormatCurrency(this.totalPrice, this.model.get('metadata').get('pricingCurrency'), app.settings.get('localCurrency')); - this.getCachedElement('.js-price').text(adjPrice); + this.getCachedEl('.js-price').text(adjPrice); } } From 2ca22df1305c7eb976fba563a20fb6f25671555b Mon Sep 17 00:00:00 2001 From: Rob Misiorowski Date: Tue, 27 Jun 2017 08:56:45 -0700 Subject: [PATCH 25/38] using payment amount for total on order details --- .../modals/orderDetail/summaryTab/orderDetails.html | 10 +++++----- js/views/modals/orderDetail/summaryTab/Summary.js | 2 ++ 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/js/templates/modals/orderDetail/summaryTab/orderDetails.html b/js/templates/modals/orderDetail/summaryTab/orderDetails.html index 20e8cd5d2..353b1a8d2 100644 --- a/js/templates/modals/orderDetail/summaryTab/orderDetails.html +++ b/js/templates/modals/orderDetail/summaryTab/orderDetails.html @@ -22,14 +22,14 @@ // first item. var item = ob.order.items[0]; - var priceText = ob.convertAndFormatCurrency(ob.listing.item.price, - ob.listing.metadata.pricingCurrency, ob.userCurrency, { useBtcSymbol: false }); + var priceText = ob.formatCurrency(ob.order.payment.amount, 'BTC', + { useBtcSymbol: false }); if (ob.userCurrency !== 'BTC') { priceText = ob.polyT('fiatBtcPairing', { - fiatAmount: priceText, - btcAmount: ob.convertAndFormatCurrency(ob.listing.item.price, - ob.listing.metadata.pricingCurrency, 'BTC', { useBtcSymbol: false }), + fiatAmount: ob.convertAndFormatCurrency(ob.order.payment.amount, 'BTC', + ob.userCurrency), + btcAmount: priceText, }); } %> diff --git a/js/views/modals/orderDetail/summaryTab/Summary.js b/js/views/modals/orderDetail/summaryTab/Summary.js index 17002de5d..a1950c2aa 100644 --- a/js/views/modals/orderDetail/summaryTab/Summary.js +++ b/js/views/modals/orderDetail/summaryTab/Summary.js @@ -794,6 +794,8 @@ export default class extends BaseVw { this.$('.js-statusProgressBarContainer').html(this.stateProgressBar.render().el); if (this.orderDetails) this.orderDetails.remove(); + console.log('sizzle'); + window.sizzle = this.contract; this.orderDetails = this.createChild(OrderDetails, { model: this.contract, moderator: this.moderator, From 2c47a046b33b61054151f62efcebd069b2181b33 Mon Sep 17 00:00:00 2001 From: Rob Misiorowski Date: Tue, 27 Jun 2017 09:14:16 -0700 Subject: [PATCH 26/38] removed some debugging code --- js/views/modals/orderDetail/summaryTab/Summary.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/js/views/modals/orderDetail/summaryTab/Summary.js b/js/views/modals/orderDetail/summaryTab/Summary.js index a1950c2aa..17002de5d 100644 --- a/js/views/modals/orderDetail/summaryTab/Summary.js +++ b/js/views/modals/orderDetail/summaryTab/Summary.js @@ -794,8 +794,6 @@ export default class extends BaseVw { this.$('.js-statusProgressBarContainer').html(this.stateProgressBar.render().el); if (this.orderDetails) this.orderDetails.remove(); - console.log('sizzle'); - window.sizzle = this.contract; this.orderDetails = this.createChild(OrderDetails, { model: this.contract, moderator: this.moderator, From cb6c758a9abd450422d4bb8c585b0f4664c021b5 Mon Sep 17 00:00:00 2001 From: Rob Misiorowski Date: Tue, 27 Jun 2017 09:15:30 -0700 Subject: [PATCH 27/38] removed some debugging code --- js/models/order/Order.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/js/models/order/Order.js b/js/models/order/Order.js index b9d073fc4..9fc8b9200 100644 --- a/js/models/order/Order.js +++ b/js/models/order/Order.js @@ -54,7 +54,8 @@ export default class extends BaseModel { // response.contract.disputeResolution.payout.buyerOutput.amount = // integerToDecimal(response.contract.disputeResolution.payout.vendorOutput.amount, true); // response.contract.disputeResolution.payout.buyerOutput.amount = - // integerToDecimal(response.contract.disputeResolution.payout.moderatorOutput.amount, true); + // integerToDecimal(response.contract.disputeResolution.payout.moderatorOutput.amount, + // true); // } } From 3f3e9ab558e73d105ae7ded2e5cbf2f9b81d7252 Mon Sep 17 00:00:00 2001 From: Rob Misiorowski Date: Tue, 27 Jun 2017 15:32:03 -0700 Subject: [PATCH 28/38] handling dispute payout payment amounts --- js/models/order/Case.js | 23 +++++++++++------- js/models/order/Order.js | 24 ++++++++++++------- .../orderDetail/summaryTab/disputePayout.html | 7 ------ js/views/transactions/Transactions.js | 1 + 4 files changed, 31 insertions(+), 24 deletions(-) diff --git a/js/models/order/Case.js b/js/models/order/Case.js index cdf66c8e6..3ad99aa6c 100644 --- a/js/models/order/Case.js +++ b/js/models/order/Case.js @@ -41,14 +41,21 @@ export default class extends BaseModel { response.vendorContract.buyerOrder.payment.amount = integerToDecimal(response.vendorContract.buyerOrder.payment.amount, true); - // if (response.resolution) { - // response.resolution.payout.buyerOutput.amount = - // integerToDecimal(response.resolution.payout.buyerOutput.amount, true); - // response.resolution.buyerOutput.amount = - // integerToDecimal(response.resolution.payout.vendorOutput.amount, true); - // response.resolution.buyerOutput.amount = - // integerToDecimal(response.resolution.payout.moderatorOutput.amount, true); - // } + if (response.resolution) { + response.resolution.payout.buyerOutput = + response.resolution.payout.buyerOutput || {}; + response.resolution.payout.vendorOutput = + response.resolution.payout.vendorOutput || {}; + response.resolution.payout.moderatorOutput = + response.resolution.payout.moderatorOutput || {}; + + response.resolution.payout.buyerOutput.amount = + integerToDecimal(response.resolution.payout.buyerOutput.amount, true); + response.resolution.payout.vendorOutput.amount = + integerToDecimal(response.resolution.payout.vendorOutput.amount, true); + response.resolution.payout.moderatorOutput.amount = + integerToDecimal(response.resolution.payout.moderatorOutput.amount, true); + } } return response; diff --git a/js/models/order/Order.js b/js/models/order/Order.js index 9fc8b9200..b6bbb4433 100644 --- a/js/models/order/Order.js +++ b/js/models/order/Order.js @@ -48,15 +48,21 @@ export default class extends BaseModel { response.contract.buyerOrder.payment.amount = integerToDecimal(response.contract.buyerOrder.payment.amount, true); - // if (response.contract.disputeResolution) { - // response.contract.disputeResolution.payout.buyerOutput.amount = - // integerToDecimal(response.contract.disputeResolution.payout.buyerOutput.amount, true); - // response.contract.disputeResolution.payout.buyerOutput.amount = - // integerToDecimal(response.contract.disputeResolution.payout.vendorOutput.amount, true); - // response.contract.disputeResolution.payout.buyerOutput.amount = - // integerToDecimal(response.contract.disputeResolution.payout.moderatorOutput.amount, - // true); - // } + if (response.contract.disputeResolution) { + response.contract.disputeResolution.payout.buyerOutput = + response.contract.disputeResolution.payout.buyerOutput || {}; + response.contract.disputeResolution.payout.vendorOutput = + response.contract.disputeResolution.payout.vendorOutput || {}; + response.contract.disputeResolution.payout.moderatorOutput = + response.contract.disputeResolution.payout.moderatorOutput || {}; + + response.contract.disputeResolution.payout.buyerOutput.amount = + integerToDecimal(response.contract.disputeResolution.payout.buyerOutput.amount, true); + response.contract.disputeResolution.payout.vendorOutput.amount = + integerToDecimal(response.contract.disputeResolution.payout.vendorOutput.amount, true); + response.contract.disputeResolution.payout.moderatorOutput.amount = + integerToDecimal(response.contract.disputeResolution.payout.moderatorOutput.amount, true); + } } response.paymentAddressTransactions = response.paymentAddressTransactions || []; diff --git a/js/templates/modals/orderDetail/summaryTab/disputePayout.html b/js/templates/modals/orderDetail/summaryTab/disputePayout.html index db0a549dc..e3f2a52d6 100644 --- a/js/templates/modals/orderDetail/summaryTab/disputePayout.html +++ b/js/templates/modals/orderDetail/summaryTab/disputePayout.html @@ -1,11 +1,4 @@ <% - ob.payout.buyerOutput = ob.payout.buyerOutput || {}; - ob.payout.buyerOutput.amount = 0.1; - ob.payout.vendorOutput = ob.payout.vendorOutput || {}; - ob.payout.vendorOutput.amount = 0.05; - ob.payout.moderatorOutput = ob.payout.moderatorOutput || {}; - ob.payout.moderatorOutput.amount = 0.0001; - let priceLines = {}; let partyHeadings = {}; diff --git a/js/views/transactions/Transactions.js b/js/views/transactions/Transactions.js index 518a72b8b..29e33b1d6 100644 --- a/js/views/transactions/Transactions.js +++ b/js/views/transactions/Transactions.js @@ -425,6 +425,7 @@ export default class extends baseVw { }).done((data) => { if (this.socket) { this.listenTo(this.socket, 'message', (e) => { + if (!e.jsonData.peerId) return; if (e.jsonData.id === data.id) { this.profileDeferreds[e.jsonData.peerId].resolve(new Profile(e.jsonData.profile, { parse: true })); From e6a3b98cf3bc9aa6a6ccea1aa3410a1852be236f Mon Sep 17 00:00:00 2001 From: Rob Misiorowski Date: Tue, 27 Jun 2017 16:53:30 -0700 Subject: [PATCH 29/38] WIP of the dispute closed section --- js/languages/en-US.json | 8 +++ js/models/order/Case.js | 22 ++++++-- js/models/order/Order.js | 22 ++++++-- .../summaryTab/disputeAcceptance.html | 31 +++++++++++ .../summaryTab/DisputeAcceptance.js | 53 +++++++++++++++++++ .../modals/orderDetail/summaryTab/Summary.js | 53 +++++++++++++++---- 6 files changed, 173 insertions(+), 16 deletions(-) create mode 100644 js/templates/modals/orderDetail/summaryTab/disputeAcceptance.html create mode 100644 js/views/modals/orderDetail/summaryTab/DisputeAcceptance.js diff --git a/js/languages/en-US.json b/js/languages/en-US.json index 16ce9a06f..b380e8571 100644 --- a/js/languages/en-US.json +++ b/js/languages/en-US.json @@ -1115,6 +1115,14 @@ "btnConfirm": "Yes, Accept" } }, + "disputeAcceptance": { + "heading": "Dispute Closed", + "genericBuyerAcceptedPayout": "The buyer accepted the dispute payout", + "genericVendorAcceptedPayout": "The vendor accepted the dispute payout", + "userAcceptedPayout": "%{name} accepted the dispute payout", + "orderCompleteWhenYouReview": "The order will be complete when you leave a review.", + "orderCompleteWhenBuyerReviews": "The order will be complete when the buyer leaves a review." + }, "orderDetails": { "progressBarStates": { "paid": "Paid", diff --git a/js/models/order/Case.js b/js/models/order/Case.js index 3ad99aa6c..6c2468ad4 100644 --- a/js/models/order/Case.js +++ b/js/models/order/Case.js @@ -49,12 +49,28 @@ export default class extends BaseModel { response.resolution.payout.moderatorOutput = response.resolution.payout.moderatorOutput || {}; + // Temporary to account for server bug: + // https://github.com/OpenBazaar/openbazaar-go/issues/548 + // Sometimes the payment amounts are coming back as enormously inflated strings. + // For now, we'll just make them dummy values. + if (typeof response.resolution.payout.buyerOutput.amount === 'string') { + response.resolution.payout.buyerOutput.amount = 25000; + } + + if (typeof response.resolution.payout.vendorOutput.amount === 'string') { + response.resolution.payout.vendorOutput.amount = 12000; + } + + if (typeof response.resolution.payout.moderatorOutput.amount === 'string') { + response.resolution.payout.moderatorOutput.amount = 6000; + } + response.resolution.payout.buyerOutput.amount = - integerToDecimal(response.resolution.payout.buyerOutput.amount, true); + integerToDecimal(response.resolution.payout.buyerOutput.amount || 0, true); response.resolution.payout.vendorOutput.amount = - integerToDecimal(response.resolution.payout.vendorOutput.amount, true); + integerToDecimal(response.resolution.payout.vendorOutput.amount || 0, true); response.resolution.payout.moderatorOutput.amount = - integerToDecimal(response.resolution.payout.moderatorOutput.amount, true); + integerToDecimal(response.resolution.payout.moderatorOutput.amount || 0, true); } } diff --git a/js/models/order/Order.js b/js/models/order/Order.js index b6bbb4433..635e19c2b 100644 --- a/js/models/order/Order.js +++ b/js/models/order/Order.js @@ -56,12 +56,28 @@ export default class extends BaseModel { response.contract.disputeResolution.payout.moderatorOutput = response.contract.disputeResolution.payout.moderatorOutput || {}; + // https://github.com/OpenBazaar/openbazaar-go/issues/548 + if (typeof response.contract.disputeResolution.payout.buyerOutput.amount === 'string') { + response.contract.disputeResolution.payout.buyerOutput.amount = 25000; + } + + if (typeof response.contract.disputeResolution.payout.vendorOutput.amount === 'string') { + response.contract.disputeResolution.payout.vendorOutput.amount = 12000; + } + + if (typeof response.contract.disputeResolution.payout.moderatorOutput.amount === 'string') { + response.contract.disputeResolution.payout.moderatorOutput.amount = 6000; + } + response.contract.disputeResolution.payout.buyerOutput.amount = - integerToDecimal(response.contract.disputeResolution.payout.buyerOutput.amount, true); + integerToDecimal( + response.contract.disputeResolution.payout.buyerOutput.amount || 0, true); response.contract.disputeResolution.payout.vendorOutput.amount = - integerToDecimal(response.contract.disputeResolution.payout.vendorOutput.amount, true); + integerToDecimal( + response.contract.disputeResolution.payout.vendorOutput.amount || 0, true); response.contract.disputeResolution.payout.moderatorOutput.amount = - integerToDecimal(response.contract.disputeResolution.payout.moderatorOutput.amount, true); + integerToDecimal( + response.contract.disputeResolution.payout.moderatorOutput.amount || 0, true); } } diff --git a/js/templates/modals/orderDetail/summaryTab/disputeAcceptance.html b/js/templates/modals/orderDetail/summaryTab/disputeAcceptance.html new file mode 100644 index 000000000..337e27ca2 --- /dev/null +++ b/js/templates/modals/orderDetail/summaryTab/disputeAcceptance.html @@ -0,0 +1,31 @@ +

<%= ob.polyT('orderDetail.summaryTab.disputeAcceptance.heading') %>

+<% if (ob.timestamp) { %> +<%= ob.moment(ob.timestamp).format('lll') %> +<% } %> + +
+
+
+
+ <% + let introLine; + + if (ob.closerName) { + introLine = ob.polyT('orderDetail.summaryTab.disputeAcceptance.userAcceptedPayout', { + name: ob.closerName, + }); + } else { + introLine = ob.acceptedByBuyer ? + ob.polyT('orderDetail.summaryTab.disputeAcceptance.genericBuyerAcceptedPayout') : + ob.polyT('orderDetail.summaryTab.disputeAcceptance.genericVendorAcceptedPayout'); + } + + const subText = ob.buyerViewing ? + ob.polyT('orderDetail.summaryTab.disputeAcceptance.orderCompleteWhenYouReview') : + ob.polyT('orderDetail.summaryTab.disputeAcceptance.orderCompleteWhenBuyerReviews'); + %> +
<%= introLine %>
+
<%= subText %>
+
+
+
\ No newline at end of file diff --git a/js/views/modals/orderDetail/summaryTab/DisputeAcceptance.js b/js/views/modals/orderDetail/summaryTab/DisputeAcceptance.js new file mode 100644 index 000000000..36a0cc7fc --- /dev/null +++ b/js/views/modals/orderDetail/summaryTab/DisputeAcceptance.js @@ -0,0 +1,53 @@ +import _ from 'underscore'; +import moment from 'moment'; +import loadTemplate from '../../../../utils/loadTemplate'; +import BaseVw from '../../../baseVw'; + +export default class extends BaseVw { + constructor(options = {}) { + super(options); + + this._state = { + closerName: '', + closerAvatarHashes: {}, + buyerViewing: false, + ...options.initialState || {}, + }; + } + + className() { + return 'disputeAcceptanceEvent rowLg'; + } + + getState() { + return this._state; + } + + setState(state, replace = false, renderOnChange = true) { + let newState; + + if (replace) { + this._state = {}; + } else { + newState = _.extend({}, this._state, state); + } + + if (renderOnChange && !_.isEqual(this._state, newState)) { + this._state = newState; + this.render(); + } + + return this; + } + + render() { + loadTemplate('modals/orderDetail/summaryTab/disputeAcceptance.html', (t) => { + this.$el.html(t({ + ...this._state, + moment, + })); + }); + + return this; + } +} diff --git a/js/views/modals/orderDetail/summaryTab/Summary.js b/js/views/modals/orderDetail/summaryTab/Summary.js index 17002de5d..bd5fa8e61 100644 --- a/js/views/modals/orderDetail/summaryTab/Summary.js +++ b/js/views/modals/orderDetail/summaryTab/Summary.js @@ -20,6 +20,7 @@ import CompleteOrderForm from './CompleteOrderForm'; import OrderComplete from './OrderComplete'; import DisputeStarted from './DisputeStarted'; import DisputePayout from './DisputePayout'; +import DisputeAcceptance from './DisputeAcceptance'; import PayForOrder from '../../../modals/purchase/Payment'; export default class extends BaseVw { @@ -114,6 +115,10 @@ export default class extends BaseVw { this.completeOrderForm.remove(); this.completeOrderForm = null; } + + // if (this.buyer.id === app.profile.id) { + // this.renderCompleteOrderForm(); + // } }); if (!this.isCase()) { @@ -213,16 +218,6 @@ export default class extends BaseVw { // The timeout is needed in the handler so the updated // order state is available. setTimeout(() => this.renderDisputePayoutView()); - } else { - // NOTE!!!!!!! - // NOTE!!!!!!! - // NOTE!!!!!!! - // Temporarily putting this here. Once the server issue is - // fixed and a disputeClosed event comes, then this should - // go in the change handler of that event. - if (this.buyer.id === app.profile.id) { - this.renderCompleteOrderForm(); - } } }); @@ -701,6 +696,36 @@ export default class extends BaseVw { this.getCachedEl('.js-payForOrderWrap').html(this.payForOrder.render().el); } + renderDisputeAcceptanceView() { + const data = this.contract.get('disputeAcceptance'); + + if (!data) { + throw new Error('Unable to create the Dispute Acceptance view because the ' + + 'disputeAcceptance data object has not been set.'); + } + + const closer = data.closedBy === + this.buyer.id ? this.buyer : this.vendor; + + if (this.disputeAcceptance) this.disputeAcceptance.remove(); + this.disputeAcceptance = this.createChild(DisputeAcceptance, { + dataObject: data, + initialState: { + acceptedByBuyer: closer.id === this.buyer.id, + buyerViewing: app.profile.id === this.buyer.id, + }, + }); + + closer.getProfile() + .done(profile => + this.disputeAcceptance.setState({ + closerName: profile.get('name'), + closerAvatarHashes: profile.get('avatarHashes').toJSON(), + })); + + this.$subSections.prepend(this.disputeAcceptance.render().el); + } + /** * Will render sub-sections in order based on their timestamp. Exempt from * this are the Order Details, Payment Details and Accepted sections which @@ -758,6 +783,14 @@ export default class extends BaseVw { }); } + if (this.contract.get('disputeAcceptance')) { + sections.push({ + function: this.renderDisputeAcceptanceView, + timestamp: + (new Date(this.contract.get('disputeAcceptance').timestamp)), + }); + } + sections.sort((a, b) => (a.timestamp - b.timestamp)) .forEach(section => { if (typeof section.function === 'function') { From bd66b8615ccec8e06c0c9f1b8048d4669735b1d8 Mon Sep 17 00:00:00 2001 From: Rob Misiorowski Date: Tue, 27 Jun 2017 16:54:55 -0700 Subject: [PATCH 30/38] added in a comment --- js/models/order/Order.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/js/models/order/Order.js b/js/models/order/Order.js index 635e19c2b..9d3216bc0 100644 --- a/js/models/order/Order.js +++ b/js/models/order/Order.js @@ -56,7 +56,10 @@ export default class extends BaseModel { response.contract.disputeResolution.payout.moderatorOutput = response.contract.disputeResolution.payout.moderatorOutput || {}; + // Temporary to account for server bug: // https://github.com/OpenBazaar/openbazaar-go/issues/548 + // Sometimes the payment amounts are coming back as enormously inflated strings. + // For now, we'll just make them dummy values. if (typeof response.contract.disputeResolution.payout.buyerOutput.amount === 'string') { response.contract.disputeResolution.payout.buyerOutput.amount = 25000; } From 34dd176546a9ab5115e1a2a31cffcbe89342cf0e Mon Sep 17 00:00:00 2001 From: Rob Misiorowski Date: Wed, 28 Jun 2017 09:59:38 -0700 Subject: [PATCH 31/38] updated handling of profile fetcher to avoid odd race condition --- js/views/transactions/Transactions.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/js/views/transactions/Transactions.js b/js/views/transactions/Transactions.js index 29e33b1d6..1a029429b 100644 --- a/js/views/transactions/Transactions.js +++ b/js/views/transactions/Transactions.js @@ -422,11 +422,12 @@ export default class extends baseVw { data: JSON.stringify(profilesToFetch), dataType: 'json', contentType: 'application/json', - }).done((data) => { + }).done(() => { if (this.socket) { this.listenTo(this.socket, 'message', (e) => { - if (!e.jsonData.peerId) return; - if (e.jsonData.id === data.id) { + // if (!e.jsonData.peerId) return; + + if (this.profileDeferreds[e.jsonData.peerId]) { this.profileDeferreds[e.jsonData.peerId].resolve(new Profile(e.jsonData.profile, { parse: true })); } From 0481894b7181596e65305c996ca835941a1efcb3 Mon Sep 17 00:00:00 2001 From: Rob Misiorowski Date: Wed, 28 Jun 2017 10:21:33 -0700 Subject: [PATCH 32/38] showing the complete order form for the buyer when a dispute payout is accepted --- .../modals/orderDetail/summaryTab/Summary.js | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/js/views/modals/orderDetail/summaryTab/Summary.js b/js/views/modals/orderDetail/summaryTab/Summary.js index bd5fa8e61..d6a21e127 100644 --- a/js/views/modals/orderDetail/summaryTab/Summary.js +++ b/js/views/modals/orderDetail/summaryTab/Summary.js @@ -237,6 +237,9 @@ export default class extends BaseVw { this.listenTo(this.model, 'change:resolution', () => this.renderDisputePayoutView()); + + this.listenTo(this.model, 'change:disputeAcceptance', + () => this.renderDisputeAcceptanceView()); } const serverSocket = getSocket(); @@ -547,12 +550,6 @@ export default class extends BaseVw { } renderCompleteOrderForm() { - if (['FULFILLED', 'RESOLVED'].indexOf(this.model.get('state')) === -1 || - this.buyer.id !== app.profile.id) { - throw new Error('The complete order form should only be shown for the buyer and ' + - 'when the order is in a state of FULFILLED or REOLVED'); - } - const completingObject = completingOrder(this.model.id); const model = new OrderCompletion( completingObject ? completingObject.data : { orderId: this.model.id }); @@ -588,10 +585,7 @@ export default class extends BaseVw { this.$subSections.prepend(this.fulfilled.render().el); - // If the order is not complete and this is the buyer, we'll - // render a complete order form. - if (['FULFILLED', 'RESOLVED'].indexOf(this.model.get('state')) > -1 && - this.buyer.id === app.profile.id) { + if (this.model.get('state') === 'FULFILLED' && this.buyer.id === app.profile.id) { this.renderCompleteOrderForm(); } } @@ -724,6 +718,10 @@ export default class extends BaseVw { })); this.$subSections.prepend(this.disputeAcceptance.render().el); + + if (this.model.get('state') === 'RESOLVED' && this.buyer.id === app.profile.id) { + this.renderCompleteOrderForm(); + } } /** From f799e386ff9631f73dc05e931f75020a91e7477c Mon Sep 17 00:00:00 2001 From: Rob Misiorowski Date: Wed, 28 Jun 2017 14:15:50 -0700 Subject: [PATCH 33/38] finished up the dispute complete section --- js/views/modals/orderDetail/summaryTab/Summary.js | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/js/views/modals/orderDetail/summaryTab/Summary.js b/js/views/modals/orderDetail/summaryTab/Summary.js index d6a21e127..d9cf0cf88 100644 --- a/js/views/modals/orderDetail/summaryTab/Summary.js +++ b/js/views/modals/orderDetail/summaryTab/Summary.js @@ -111,14 +111,10 @@ export default class extends BaseVw { } if (this.completeOrderForm && - ['FULFILLED', 'RESOLVED'].indexOf(state) > -1) { + ['FULFILLED', 'RESOLVED'].indexOf(state) === -1) { this.completeOrderForm.remove(); this.completeOrderForm = null; } - - // if (this.buyer.id === app.profile.id) { - // this.renderCompleteOrderForm(); - // } }); if (!this.isCase()) { @@ -227,6 +223,9 @@ export default class extends BaseVw { this.model.fetch(); } }); + + this.listenTo(this.contract, 'change:disputeAcceptance', + () => this.renderDisputeAcceptanceView()); } else { this.listenTo(orderEvents, 'resolveDisputeComplete', e => { if (e.id === this.model.id) { @@ -237,9 +236,6 @@ export default class extends BaseVw { this.listenTo(this.model, 'change:resolution', () => this.renderDisputePayoutView()); - - this.listenTo(this.model, 'change:disputeAcceptance', - () => this.renderDisputeAcceptanceView()); } const serverSocket = getSocket(); From 069b2d034d4df10753554a0759eff72b4d96dd10 Mon Sep 17 00:00:00 2001 From: Rob Misiorowski Date: Fri, 30 Jun 2017 08:50:13 -0700 Subject: [PATCH 34/38] fixing bug with anonymous flag in the complete order form --- .../modals/orderDetail/summaryTab/completeOrderForm.html | 3 ++- js/views/modals/orderDetail/summaryTab/CompleteOrderForm.js | 5 ++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/js/templates/modals/orderDetail/summaryTab/completeOrderForm.html b/js/templates/modals/orderDetail/summaryTab/completeOrderForm.html index 4463e33de..29e365038 100644 --- a/js/templates/modals/orderDetail/summaryTab/completeOrderForm.html +++ b/js/templates/modals/orderDetail/summaryTab/completeOrderForm.html @@ -22,7 +22,8 @@

<%= ob.polyT('orderDetail.summaryTab.completeOrderForm.h name="anonymous" id="completeOrderAnon" class="centerLabel" - <% if (ob.anonymous) print('checked') %>> + data-var-type="boolean" + <% if (!ob.anonymous) print('checked') %>>

diff --git a/js/views/modals/orderDetail/summaryTab/CompleteOrderForm.js b/js/views/modals/orderDetail/summaryTab/CompleteOrderForm.js index 8124b6c9a..740d879ae 100644 --- a/js/views/modals/orderDetail/summaryTab/CompleteOrderForm.js +++ b/js/views/modals/orderDetail/summaryTab/CompleteOrderForm.js @@ -53,8 +53,11 @@ export default class extends BaseVw { } onClickCompleteOrder() { + const formData = this.getFormData(); + const data = { - ...this.getFormData(), + ...formData, + anonymous: !formData.anonymous, // If a rating is not set, the RatingStrip view will return 0. We'll // send undefined in that case since it gives us the error message we // prefer. From 7302852a76ac7f88eb83b207c68195505d5fffbb Mon Sep 17 00:00:00 2001 From: Rob Misiorowski Date: Fri, 30 Jun 2017 13:15:50 -0700 Subject: [PATCH 35/38] code review tweaks --- js/models/order/Contract.js | 4 -- js/views/modals/orderDetail/ConvoMessages.js | 34 +++-------------- js/views/modals/orderDetail/Discussion.js | 34 +++-------------- js/views/modals/orderDetail/DisputeOrder.js | 21 +--------- js/views/modals/orderDetail/OrderDetail.js | 16 ++++++++ js/views/modals/orderDetail/ResolveDispute.js | 35 ++++------------- .../orderDetail/summaryTab/OrderDetails.js | 21 +--------- .../modals/orderDetail/summaryTab/Payments.js | 21 +--------- .../modals/orderDetail/summaryTab/Summary.js | 38 ++----------------- 9 files changed, 44 insertions(+), 180 deletions(-) diff --git a/js/models/order/Contract.js b/js/models/order/Contract.js index 9339876aa..1b11db17d 100644 --- a/js/models/order/Contract.js +++ b/js/models/order/Contract.js @@ -15,10 +15,6 @@ export default class extends BaseModel { .get('contractType'); } - get orderTotal() { - return 99; - } - parse(response) { return { ...response, diff --git a/js/views/modals/orderDetail/ConvoMessages.js b/js/views/modals/orderDetail/ConvoMessages.js index 4ab1f8a58..a2ae34e4f 100644 --- a/js/views/modals/orderDetail/ConvoMessages.js +++ b/js/views/modals/orderDetail/ConvoMessages.js @@ -1,5 +1,6 @@ import $ from 'jquery'; import app from '../../../app'; +import { checkValidParticipantObject } from './OrderDetail.js'; import BaseVw from '../../baseVw'; import ConvoMessage from './ConvoMessage'; @@ -13,36 +14,11 @@ export default class extends BaseVw { throw new Error('Please provide the DOM element that handles scrolling for this view.'); } - const isValidParticipantObject = (participant) => { - let isValid = true; - if (!participant.id) isValid = false; - if (typeof participant.getProfile !== 'function') isValid = false; - return isValid; - }; + checkValidParticipantObject(options.buyer, 'buyer'); + checkValidParticipantObject(options.vendor, 'vendor'); - const getInvalidParticpantError = (type = '') => - (`The ${type} object is not valid. It should have an id ` + - 'as well as a getProfile function that returns a promise that ' + - 'resolves with a profile model.'); - - if (!options.buyer) { - throw new Error('Please provide a buyer object.'); - } - - if (!options.vendor) { - throw new Error('Please provide a vendor object.'); - } - - if (!isValidParticipantObject(options.buyer)) { - throw new Error(getInvalidParticpantError('buyer')); - } - - if (!isValidParticipantObject(options.vendor)) { - throw new Error(getInvalidParticpantError('vendor')); - } - - if (options.moderator && !isValidParticipantObject(options.moderator)) { - throw new Error(getInvalidParticpantError('moderator')); + if (options.moderator) { + checkValidParticipantObject(options.moderator, 'moderator'); } super(options); diff --git a/js/views/modals/orderDetail/Discussion.js b/js/views/modals/orderDetail/Discussion.js index f4fd5354b..2a4144e3a 100644 --- a/js/views/modals/orderDetail/Discussion.js +++ b/js/views/modals/orderDetail/Discussion.js @@ -7,6 +7,7 @@ import { getEmojiByName } from '../../../data/emojis'; import loadTemplate from '../../../utils/loadTemplate'; import ChatMessages from '../../../collections/ChatMessages'; import ChatMessage from '../../../models/chat/ChatMessage'; +import { checkValidParticipantObject } from './OrderDetail.js'; import baseVw from '../../baseVw'; import ConvoMessages from './ConvoMessages'; @@ -25,36 +26,11 @@ export default class extends baseVw { 'indicating whether Discussion is the active tab.'); } - const isValidParticipantObject = (participant) => { - let isValid = true; - if (!participant.id) isValid = false; - if (typeof participant.getProfile !== 'function') isValid = false; - return isValid; - }; - - const getInvalidParticpantError = (type = '') => - (`The ${type} object is not valid. It should have an id ` + - 'as well as a getProfile function that returns a promise that ' + - 'resolves with a profile model.'); - - if (!options.buyer) { - throw new Error('Please provide a buyer object.'); - } - - if (!options.vendor) { - throw new Error('Please provide a vendor object.'); - } - - if (!isValidParticipantObject(options.buyer)) { - throw new Error(getInvalidParticpantError('buyer')); - } - - if (!isValidParticipantObject(options.vendor)) { - throw new Error(getInvalidParticpantError('vendor')); - } + checkValidParticipantObject(options.buyer, 'buyer'); + checkValidParticipantObject(options.vendor, 'vendor'); - if (options.moderator && !isValidParticipantObject(options.moderator)) { - throw new Error(getInvalidParticpantError('moderator')); + if (options.moderator) { + checkValidParticipantObject(options.moderator, 'moderator'); } super(options); diff --git a/js/views/modals/orderDetail/DisputeOrder.js b/js/views/modals/orderDetail/DisputeOrder.js index 501290dac..4e8c3905c 100644 --- a/js/views/modals/orderDetail/DisputeOrder.js +++ b/js/views/modals/orderDetail/DisputeOrder.js @@ -4,6 +4,7 @@ import { events as orderEvents, } from '../../../utils/order'; import loadTemplate from '../../../utils/loadTemplate'; +import { checkValidParticipantObject } from './OrderDetail.js'; import BaseVw from '../../baseVw'; import ModFragment from './ModFragment'; @@ -15,25 +16,7 @@ export default class extends BaseVw { throw new Error('Please provide an DisputeOrder model.'); } - const isValidParticipantObject = (participant) => { - let isValid = true; - if (!participant.id) isValid = false; - if (typeof participant.getProfile !== 'function') isValid = false; - return isValid; - }; - - const getInvalidParticpantError = (type = '') => - (`The ${type} object is not valid. It should have an id ` + - 'as well as a getProfile function that returns a promise that ' + - 'resolves with a profile model.'); - - if (!options.moderator) { - throw new Error('Please provide a moderator object.'); - } - - if (!isValidParticipantObject(options.moderator)) { - throw new Error(getInvalidParticpantError('moderator')); - } + checkValidParticipantObject(options.moderator, 'moderator'); options.moderator.getProfile() .done((modProfile) => { diff --git a/js/views/modals/orderDetail/OrderDetail.js b/js/views/modals/orderDetail/OrderDetail.js index 612d6737c..9311d0eb5 100644 --- a/js/views/modals/orderDetail/OrderDetail.js +++ b/js/views/modals/orderDetail/OrderDetail.js @@ -516,3 +516,19 @@ export default class extends BaseModal { return this; } } + +export function checkValidParticipantObject(participant, type) { + if (typeof participant !== 'object') { + throw new Error(`Please provide a participant object for the ${type}.`); + } + + if (typeof type !== 'string') { + throw new Error('Please provide the participant type as a string.'); + } + + if (!participant.id || typeof participant.getProfile !== 'function') { + throw new Error(`The ${type} object is not valid. It should have an id ` + + 'as well as a getProfile function that returns a promise that ' + + 'resolves with a profile model.'); + } +} diff --git a/js/views/modals/orderDetail/ResolveDispute.js b/js/views/modals/orderDetail/ResolveDispute.js index cf1113e4e..b46837355 100644 --- a/js/views/modals/orderDetail/ResolveDispute.js +++ b/js/views/modals/orderDetail/ResolveDispute.js @@ -5,6 +5,7 @@ import { resolveDispute, events as orderEvents, } from '../../../utils/order'; +import { checkValidParticipantObject } from './OrderDetail.js'; import loadTemplate from '../../../utils/loadTemplate'; import BaseVw from '../../baseVw'; @@ -16,33 +17,8 @@ export default class extends BaseVw { throw new Error('Please provide an OrderFulfillment model.'); } - const isValidParticipantObject = (participant) => { - let isValid = true; - if (!participant.id) isValid = false; - if (typeof participant.getProfile !== 'function') isValid = false; - return isValid; - }; - - const getInvalidParticpantError = (type = '') => - (`The ${type} object is not valid. It should have an id ` + - 'as well as a getProfile function that returns a promise that ' + - 'resolves with a profile model.'); - - if (!options.vendor) { - throw new Error('Please provide a vendor object.'); - } - - if (!isValidParticipantObject(options.vendor)) { - throw new Error(getInvalidParticpantError('vendor')); - } - - if (!options.buyer) { - throw new Error('Please provide a buyer object.'); - } - - if (!isValidParticipantObject(options.buyer)) { - throw new Error(getInvalidParticpantError('buyer')); - } + checkValidParticipantObject(options.buyer, 'buyer'); + checkValidParticipantObject(options.vendor, 'vendor'); options.buyer.getProfile().done(profile => { this.buyerProfile = profile; @@ -145,6 +121,11 @@ export default class extends BaseVw { } } + remove() { + $(document).off(null, this.boundOnDocClick); + super.remove(); + } + render() { super.render(); loadTemplate('modals/orderDetail/resolveDispute.html', (t) => { diff --git a/js/views/modals/orderDetail/summaryTab/OrderDetails.js b/js/views/modals/orderDetail/summaryTab/OrderDetails.js index 0dd58245a..4483df4f5 100644 --- a/js/views/modals/orderDetail/summaryTab/OrderDetails.js +++ b/js/views/modals/orderDetail/summaryTab/OrderDetails.js @@ -7,6 +7,7 @@ import { clipboard } from 'electron'; import '../../../../utils/velocity'; import loadTemplate from '../../../../utils/loadTemplate'; import ModFragment from '../ModFragment'; +import { checkValidParticipantObject } from '../OrderDetail.js'; import BaseVw from '../../../baseVw'; export default class extends BaseVw { @@ -17,26 +18,8 @@ export default class extends BaseVw { throw new Error('Please provide a Contract model.'); } - const isValidParticipantObject = (participant) => { - let isValid = true; - if (!participant.id) isValid = false; - if (typeof participant.getProfile !== 'function') isValid = false; - return isValid; - }; - - const getInvalidParticpantError = (type = '') => - (`The ${type} object is not valid. It should have an id ` + - 'as well as a getProfile function that returns a promise that ' + - 'resolves with a profile model.'); - if (this.isModerated()) { - if (!options.moderator) { - throw new Error('Please provide a moderator object.'); - } - - if (!isValidParticipantObject(options.moderator)) { - throw new Error(getInvalidParticpantError('moderator')); - } + checkValidParticipantObject(options.moderator, 'moderator'); options.moderator.getProfile() .done((modProfile) => { diff --git a/js/views/modals/orderDetail/summaryTab/Payments.js b/js/views/modals/orderDetail/summaryTab/Payments.js index 69023b39e..cc4321727 100644 --- a/js/views/modals/orderDetail/summaryTab/Payments.js +++ b/js/views/modals/orderDetail/summaryTab/Payments.js @@ -8,6 +8,7 @@ import { cancelOrder, events as orderEvents, } from '../../../../utils/order'; +import { checkValidParticipantObject } from '../OrderDetail.js'; import baseVw from '../../../baseVw'; import Payment from './Payment'; @@ -39,25 +40,7 @@ export default class extends baseVw { 'confirmed by the current user.'); } - const isValidParticipantObject = (participant) => { - let isValid = true; - if (!participant.id) isValid = false; - if (typeof participant.getProfile !== 'function') isValid = false; - return isValid; - }; - - const getInvalidParticpantError = (type = '') => - (`The ${type} object is not valid. It should have an id ` + - 'as well as a getProfile function that returns a promise that ' + - 'resolves with a profile model.'); - - if (!opts.vendor) { - throw new Error('Please provide a vendor object.'); - } - - if (!isValidParticipantObject(options.vendor)) { - throw new Error(getInvalidParticpantError('vendor')); - } + checkValidParticipantObject(options.vendor, 'vendor'); super(opts); this.options = opts; diff --git a/js/views/modals/orderDetail/summaryTab/Summary.js b/js/views/modals/orderDetail/summaryTab/Summary.js index d9cf0cf88..f5c18a75c 100644 --- a/js/views/modals/orderDetail/summaryTab/Summary.js +++ b/js/views/modals/orderDetail/summaryTab/Summary.js @@ -9,6 +9,7 @@ import { } from '../../../../utils/order'; import Transactions from '../../../../collections/Transactions'; import OrderCompletion from '../../../../models/order/orderCompletion/OrderCompletion'; +import { checkValidParticipantObject } from '../OrderDetail.js'; import BaseVw from '../../../baseVw'; import StateProgressBar from './StateProgressBar'; import Payments from './Payments'; @@ -43,42 +44,11 @@ export default class extends BaseVw { this.contract = contract; - const isValidParticipantObject = (participant) => { - let isValid = true; - if (!participant.id) isValid = false; - if (typeof participant.getProfile !== 'function') isValid = false; - return isValid; - }; - - const getInvalidParticpantError = (type = '') => - (`The ${type} object is not valid. It should have an id ` + - 'as well as a getProfile function that returns a promise that ' + - 'resolves with a profile model.'); - - if (!options.vendor) { - throw new Error('Please provide a vendor object.'); - } - - if (!isValidParticipantObject(options.vendor)) { - throw new Error(getInvalidParticpantError('vendor')); - } - - if (!options.buyer) { - throw new Error('Please provide a buyer object.'); - } - - if (!isValidParticipantObject(options.buyer)) { - throw new Error(getInvalidParticpantError('buyer')); - } + checkValidParticipantObject(options.buyer, 'buyer'); + checkValidParticipantObject(options.vendor, 'vendor'); if (this.contract.get('buyerOrder').payment.moderator) { - if (!options.moderator) { - throw new Error('Please provide a moderator object.'); - } - - if (!isValidParticipantObject(options.moderator)) { - throw new Error(getInvalidParticpantError('moderator')); - } + checkValidParticipantObject(options.moderator, 'moderator'); } this.options = options || {}; From 864464ed858f123f4aa9f338ad82aa7d9b5ed3fe Mon Sep 17 00:00:00 2001 From: Rob Misiorowski Date: Fri, 30 Jun 2017 14:58:09 -0700 Subject: [PATCH 36/38] handling a locl pickup item in the fulfillment related views --- js/models/order/Contract.js | 11 +++++++++++ .../order/orderFulfillment/OrderFulfillment.js | 10 ++++++++-- .../modals/orderDetail/fulfillOrder.html | 2 +- .../orderDetail/summaryTab/fulfilled.html | 18 ++++++++++-------- js/utils/order.js | 4 ++-- js/views/modals/orderDetail/FulfillOrder.js | 12 +++++++++++- js/views/modals/orderDetail/OrderDetail.js | 10 +++++++--- .../modals/orderDetail/summaryTab/Fulfilled.js | 1 + 8 files changed, 51 insertions(+), 17 deletions(-) diff --git a/js/models/order/Contract.js b/js/models/order/Contract.js index 1b11db17d..081ef88ab 100644 --- a/js/models/order/Contract.js +++ b/js/models/order/Contract.js @@ -15,6 +15,17 @@ export default class extends BaseModel { .get('contractType'); } + get isLocalPickup() { + const buyerOrder = this.get('buyerOrder'); + + if (buyerOrder && buyerOrder.items && buyerOrder.items[0] && + buyerOrder.items[0].shippingOption) { + return buyerOrder.items[0].shippingOption.service === ''; + } + + return false; + } + parse(response) { return { ...response, diff --git a/js/models/order/orderFulfillment/OrderFulfillment.js b/js/models/order/orderFulfillment/OrderFulfillment.js index 0e2b8ddcb..5fe61cf5d 100644 --- a/js/models/order/orderFulfillment/OrderFulfillment.js +++ b/js/models/order/orderFulfillment/OrderFulfillment.js @@ -9,6 +9,11 @@ export default class extends BaseModel { throw new Error('Please provide the contract type.'); } + if (typeof options.isLocalPickup !== 'boolean') { + throw new Error('Please provide a boolean indicating whether the item is to ' + + 'be picked up locally.'); + } + // Since the contract type is not available on this when // the defaults are initially called, we need to set the // initial contract type dependant attributes here. We also @@ -16,12 +21,13 @@ export default class extends BaseModel { // be restored properly. if (options.contractType === 'DIGITAL_GOOD') { attrs.digitalDelivery = new DigitalDelivery(); - } else if (options.contractType === 'PHYSICAL_GOOD') { + } else if (options.contractType === 'PHYSICAL_GOOD' && !options.isLocalPickup) { attrs.physicalDelivery = new PhysicalDelivery(); } super(attrs, options); this.contractType = options.contractType; + this.isLocalPickup = options.isLocalPickup; } defaults() { @@ -29,7 +35,7 @@ export default class extends BaseModel { if (this.contractType === 'DIGITAL_GOOD') { defaults.digitalDelivery = new DigitalDelivery(); - } else if (this.contractType === 'PHYSICAL_GOOD') { + } else if (this.contractType === 'PHYSICAL_GOOD' && !this.isLocalPickup) { defaults.physicalDelivery = new PhysicalDelivery(); } diff --git a/js/templates/modals/orderDetail/fulfillOrder.html b/js/templates/modals/orderDetail/fulfillOrder.html index 76955cf4a..de35fbf1d 100644 --- a/js/templates/modals/orderDetail/fulfillOrder.html +++ b/js/templates/modals/orderDetail/fulfillOrder.html @@ -6,7 +6,7 @@

- <% if (ob.contractType === 'PHYSICAL_GOOD') { %> + <% if (ob.contractType === 'PHYSICAL_GOOD' && !ob.isLocalPickup) { %>
diff --git a/js/templates/modals/orderDetail/summaryTab/fulfilled.html b/js/templates/modals/orderDetail/summaryTab/fulfilled.html index d7de2b72e..32754248a 100644 --- a/js/templates/modals/orderDetail/summaryTab/fulfilled.html +++ b/js/templates/modals/orderDetail/summaryTab/fulfilled.html @@ -9,14 +9,16 @@

<%= ob.polyT('orderDetail.summaryTab.fulfilled.heading') <% const physicalDelivery = ob.physicalDelivery && ob.physicalDelivery[0] || {}; %>
-
<%= ob.polyT('orderDetail.summaryTab.fulfilled.shippedByLabel') %> <%= physicalDelivery.shipper %>
-
- <%= ob.polyT('orderDetail.summaryTab.fulfilled.trackingNumberLabel') %> <%= physicalDelivery.trackingNumber || ob.polyT('orderDetail.summaryTab.notApplicable') %> - <% if (physicalDelivery.trackingNumber) { %> - <%= ob.polyT('orderDetail.summaryTab.fulfilled.copyLink') %> - <%= ob.polyT('copiedToClipboard') %> - <% } %> -
+ <% if (!ob.isLocalPickup) { %> +
<%= ob.polyT('orderDetail.summaryTab.fulfilled.shippedByLabel') %> <%= physicalDelivery.shipper %>
+
+ <%= ob.polyT('orderDetail.summaryTab.fulfilled.trackingNumberLabel') %> <%= physicalDelivery.trackingNumber || ob.polyT('orderDetail.summaryTab.notApplicable') %> + <% if (physicalDelivery.trackingNumber) { %> + <%= ob.polyT('orderDetail.summaryTab.fulfilled.copyLink') %> + <%= ob.polyT('copiedToClipboard') %> + <% } %> +
+ <% } %>
<%= ob.polyT('orderDetail.summaryTab.fulfilled.noteFromLabel', { store: ob.storeName }) %>
<%= ob.note ? ob.parseEmojis(ob.note) : ob.polyT('orderDetail.summaryTab.notApplicable') %>
diff --git a/js/utils/order.js b/js/utils/order.js index 34b0876e9..3f7f7136c 100644 --- a/js/utils/order.js +++ b/js/utils/order.js @@ -153,7 +153,7 @@ export function fulfillingOrder(orderId) { return fulfillPosts[orderId] || false; } -export function fulfillOrder(contractType = 'PHYSICAL_GOOD', data = {}) { +export function fulfillOrder(contractType = 'PHYSICAL_GOOD', isLocalPickup = false, data = {}) { if (!data || !data.orderId) { throw new Error('An orderId must be provided with the data.'); } @@ -163,7 +163,7 @@ export function fulfillOrder(contractType = 'PHYSICAL_GOOD', data = {}) { let post = fulfillPosts[orderId]; if (!post) { - const model = new OrderFulfillment(data, { contractType }); + const model = new OrderFulfillment(data, { contractType, isLocalPickup }); post = model.save(); if (!post) { diff --git a/js/views/modals/orderDetail/FulfillOrder.js b/js/views/modals/orderDetail/FulfillOrder.js index c3b215299..971d6c142 100644 --- a/js/views/modals/orderDetail/FulfillOrder.js +++ b/js/views/modals/orderDetail/FulfillOrder.js @@ -18,7 +18,13 @@ export default class extends BaseVw { throw new Error('Please provide the contract type.'); } + if (typeof options.isLocalPickup !== 'boolean') { + throw new Error('Please provide a boolean indicating whether the item is to ' + + 'be picked up locally.'); + } + this.contractType = options.contractType; + this.isLocalPickup = options.isLocalPickup; this.listenTo(orderEvents, 'fulfillingOrder', this.onFulfillingOrder); this.listenTo(orderEvents, 'fulfillOrderComplete, fulfillOrderFail', this.onFulfillOrderAlways); @@ -55,7 +61,10 @@ export default class extends BaseVw { this.model.set({}, { validate: true }); if (!this.model.validationError) { - fulfillOrder(this.model.id, this.model.toJSON()); + fulfillOrder(this.contractType, this.isLocalPickup, { + ...this.model.toJSON(), + orderId: this.model.id, + }); } this.render(); @@ -91,6 +100,7 @@ export default class extends BaseVw { loadTemplate('modals/orderDetail/fulfillOrder.html', (t) => { this.$el.html(t({ contractType: this.contractType, + isLocalPickup: this.isLocalPickup, ...this.model.toJSON(), errors: this.model.validationError || {}, fulfillingOrder: fulfillingOrder(this.model.id), diff --git a/js/views/modals/orderDetail/OrderDetail.js b/js/views/modals/orderDetail/OrderDetail.js index 9311d0eb5..c0eff0f57 100644 --- a/js/views/modals/orderDetail/OrderDetail.js +++ b/js/views/modals/orderDetail/OrderDetail.js @@ -372,14 +372,18 @@ export default class extends BaseModal { // This should not be called on a Case. createFulfillOrderTabView() { - const contractType = this.model.get('contract').type; + const contract = this.model.get('contract'); const model = new OrderFulfillment({ orderId: this.model.id }, - { contractType }); + { + contractType: contract.type, + isLocalPickup: contract.isLocalPickup, + }); const view = this.createChild(FulfillOrder, { model, - contractType, + contractType: contract.type, + isLocalPickup: contract.isLocalPickup, }); this.listenTo(view, 'clickBackToSummary clickCancel', () => this.selectTab('summary')); diff --git a/js/views/modals/orderDetail/summaryTab/Fulfilled.js b/js/views/modals/orderDetail/summaryTab/Fulfilled.js index ceb954ead..4d5a6a1e6 100644 --- a/js/views/modals/orderDetail/summaryTab/Fulfilled.js +++ b/js/views/modals/orderDetail/summaryTab/Fulfilled.js @@ -15,6 +15,7 @@ export default class extends BaseVw { this._state = { contractType: 'PHYSICAL_GOOD', + isLocalPickup: true, showPassword: false, ...options.initialState || {}, }; From fd2b4a482fcb8cab301f0fb0474e8c9414ba999b Mon Sep 17 00:00:00 2001 From: Rob Misiorowski Date: Fri, 30 Jun 2017 15:05:11 -0700 Subject: [PATCH 37/38] changing sequence of where page state is updated upon purchase --- js/views/modals/purchase/Purchase.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/views/modals/purchase/Purchase.js b/js/views/modals/purchase/Purchase.js index 6b7bcc750..00bbd94d7 100644 --- a/js/views/modals/purchase/Purchase.js +++ b/js/views/modals/purchase/Purchase.js @@ -252,6 +252,7 @@ export default class extends BaseModal { contentType: 'application/json', }) .done((data) => { + this.updatePageState('pending'); this.actionBtn.render(); this.purchase.set(this.purchase.parse(data)); this.payment = this.createChild(Payment, { @@ -263,7 +264,6 @@ export default class extends BaseModal { this.listenTo(this.payment, 'walletPaymentComplete', (pmtCompleteData => this.completePurchase(pmtCompleteData))); this.$('.js-pending').append(this.payment.render().el); - this.updatePageState('pending'); }) .fail((jqXHR) => { if (jqXHR.statusText === 'abort') return; From 3ea43bf44fc1ec54d0af1bc07f4fee2008e6a539 Mon Sep 17 00:00:00 2001 From: Rob Misiorowski Date: Wed, 5 Jul 2017 09:41:14 -0700 Subject: [PATCH 38/38] fixing bug where shipping info was lost when fulfilling a physical order --- js/models/order/orderFulfillment/OrderFulfillment.js | 4 ++-- js/views/modals/orderDetail/FulfillOrder.js | 5 +---- js/views/modals/orderDetail/summaryTab/Fulfilled.js | 2 +- js/views/modals/orderDetail/summaryTab/Summary.js | 1 + 4 files changed, 5 insertions(+), 7 deletions(-) diff --git a/js/models/order/orderFulfillment/OrderFulfillment.js b/js/models/order/orderFulfillment/OrderFulfillment.js index 5fe61cf5d..5be8995da 100644 --- a/js/models/order/orderFulfillment/OrderFulfillment.js +++ b/js/models/order/orderFulfillment/OrderFulfillment.js @@ -20,9 +20,9 @@ export default class extends BaseModel { // set them in defaults, so if the model is reset, they'll // be restored properly. if (options.contractType === 'DIGITAL_GOOD') { - attrs.digitalDelivery = new DigitalDelivery(); + attrs.digitalDelivery = new DigitalDelivery(attrs.digitalDelivery || {}); } else if (options.contractType === 'PHYSICAL_GOOD' && !options.isLocalPickup) { - attrs.physicalDelivery = new PhysicalDelivery(); + attrs.physicalDelivery = new PhysicalDelivery(attrs.physicalDelivery || {}); } super(attrs, options); diff --git a/js/views/modals/orderDetail/FulfillOrder.js b/js/views/modals/orderDetail/FulfillOrder.js index 971d6c142..109b561e2 100644 --- a/js/views/modals/orderDetail/FulfillOrder.js +++ b/js/views/modals/orderDetail/FulfillOrder.js @@ -61,10 +61,7 @@ export default class extends BaseVw { this.model.set({}, { validate: true }); if (!this.model.validationError) { - fulfillOrder(this.contractType, this.isLocalPickup, { - ...this.model.toJSON(), - orderId: this.model.id, - }); + fulfillOrder(this.contractType, this.isLocalPickup, this.model.toJSON()); } this.render(); diff --git a/js/views/modals/orderDetail/summaryTab/Fulfilled.js b/js/views/modals/orderDetail/summaryTab/Fulfilled.js index 4d5a6a1e6..468c422bd 100644 --- a/js/views/modals/orderDetail/summaryTab/Fulfilled.js +++ b/js/views/modals/orderDetail/summaryTab/Fulfilled.js @@ -15,7 +15,7 @@ export default class extends BaseVw { this._state = { contractType: 'PHYSICAL_GOOD', - isLocalPickup: true, + isLocalPickup: false, showPassword: false, ...options.initialState || {}, }; diff --git a/js/views/modals/orderDetail/summaryTab/Summary.js b/js/views/modals/orderDetail/summaryTab/Summary.js index f5c18a75c..65205870d 100644 --- a/js/views/modals/orderDetail/summaryTab/Summary.js +++ b/js/views/modals/orderDetail/summaryTab/Summary.js @@ -542,6 +542,7 @@ export default class extends BaseVw { initialState: { contractType: this.contract.type, showPassword: this.moderator && this.moderator.id !== app.profile.id || true, + isLocalPickup: this.contract.isLocalPickup, }, });