Skip to content

Instantly share code, notes, and snippets.

@lukych
Created January 19, 2022 13:13
Show Gist options
  • Save lukych/ee7de30224573d1146ee7dd86877b490 to your computer and use it in GitHub Desktop.
Save lukych/ee7de30224573d1146ee7dd86877b490 to your computer and use it in GitHub Desktop.
Компонент на Vie.js для управления лидами в админке одной из партнерок (список лидов)
<template>
<v-container fluid>
<v-alert
:value="showLeadBalanceEditSuccess"
type="success"
text
dismissible
transition="scale-transition"
>
Ставки успешно отредактированы
</v-alert>
<v-row>
<v-col>
<v-card class="d-flex">
<v-card-text style="font-size: 13px">Всего записей: {{ leadsListMeta.total }}</v-card-text>
<v-card-actions>
<v-btn
@click="cleanFilter"
color="orange"
class="white--text">
<v-icon title="Сбросить фильтры">mdi-filter-remove</v-icon>
</v-btn>
<v-btn
@click="chCurrent"
color="blue"
class="white--text">
<v-icon title="Обновить">mdi-refresh</v-icon>
</v-btn>
<v-btn
@click="showBalanceForm"
color="green"
class="white--text">
<v-icon title="Редактировать ставки">mdi-currency-usd</v-icon>
</v-btn>
</v-card-actions>
</v-card>
</v-col>
</v-row>
<v-row>
<v-col>
<v-card>
<v-data-table
v-model="selected"
:headers="headers"
:items="leadsList.data"
show-select
show-expand
:expanded.sync="expanded"
:options.sync="options"
must-sort
:footer-props="{
itemsPerPageOptions: [50, 100, 150],
itemsPerPageText: 'Количество лидов на странице'
}"
>
<template v-slot:body.prepend><!-- ФИЛЬТРЫ -->
<tr>
<td></td>
<td class="px-1"><!-- ФИЛЬТР ПО ID -->
<v-text-field
v-model="id"
@input="idInput"
outlined
dense
hide-details="auto"
class="table-font">
</v-text-field>
</td>
<td class="px-1"><!-- ФИЛЬТР ПО ДАТЕ СОЗД. -->
<v-menu
ref="menuDateCreated"
v-model="menuDateCreated"
:close-on-content-click="false"
:return-value.sync="dateRange"
transition="scale-transition"
offset-y
min-width="290px"
>
<template v-slot:activator="{ on, attrs }">
<v-text-field
v-model="dateRange"
readonly
v-bind="attrs"
v-on="on"
outlined
dense
hide-details="auto"
class="table-font"></v-text-field>
</template>
<div class="calendar d-flex ">
<v-list class="calendar-presets">
<v-list-item><button @click="chooseDatePreset" data-period="today" data-adv="false">Today</button></v-list-item>
<v-list-item><button @click="chooseDatePreset" data-period="yesterday" data-adv="false">Yesterday</button></v-list-item>
<v-list-item><button @click="chooseDatePreset" data-period="this-month" data-adv="false">This month</button></v-list-item>
<v-list-item><button @click="chooseDatePreset" data-period="this-year" data-adv="false">This year</button></v-list-item>
<v-list-item><button @click="chooseDatePreset" data-period="last-month" data-adv="false">Last month</button></v-list-item>
</v-list>
<v-date-picker
v-model="dateRange"
range
no-title
scrollable
@change="createdSelect"
>
<v-spacer></v-spacer>
<v-btn
text
color="primary"
@click="menuDateCreated = false"
>Cancel</v-btn>
<v-btn
text
color="primary"
@click="$refs.menuDateCreated.save(dateRange)"
>OK</v-btn>
</v-date-picker></div>
</v-menu>
</td>
<td class="px-1"><!-- ФИЛЬТР ПО ДАТЕ ОТПР. -->
<v-menu
ref="menuDateAdv"
v-model="menuDateAdv"
:close-on-content-click="false"
:return-value.sync="dateRangeAdvert"
transition="scale-transition"
offset-y
min-width="290px"
>
<template v-slot:activator="{ on, attrs }">
<v-text-field
v-model="dateRangeAdvert"
readonly
v-bind="attrs"
v-on="on"
outlined
dense
hide-details="auto"
class="table-font"></v-text-field>
</template>
<div class="calendar d-flex">
<v-list class="calendar-presets">
<v-list-item><button @click="chooseDatePreset" data-period="today" data-adv="true">Today</button></v-list-item>
<v-list-item><button @click="chooseDatePreset" data-period="yesterday" data-adv="true">Yesterday</button></v-list-item>
<v-list-item><button @click="chooseDatePreset" data-period="this-month" data-adv="true">This month</button></v-list-item>
<v-list-item><button @click="chooseDatePreset" data-period="this-year" data-adv="true">This year</button></v-list-item>
<v-list-item><button @click="chooseDatePreset" data-period="last-month" data-adv="true">Last month</button></v-list-item>
</v-list>
<v-date-picker
v-model="dateRangeAdvert"
range
no-title
scrollable
@change="createdAdvSelect"
>
<v-spacer></v-spacer>
<v-btn
text
color="primary"
@click="menuDateAdv = false"
>Cancel</v-btn>
<v-btn
text
color="primary"
@click="$refs.menuDateAdv.save(dateRangeAdvert)"
>OK</v-btn>
</v-date-picker>
</div>
</v-menu>
</td>
<td class="px-1"><!-- ФИЛЬТР ПО ВЕБМ -->
<v-select
v-model="webmasterId"
:items="webmastersFilterData"
placeholder="Вебмастер"
outlined
dense
hide-details="auto"
class="table-font">
</v-select>
</td>
<td class="px-1"><!-- ФИЛЬТР ПО РЕКЛ -->
<v-select
v-model="advertiserId"
:items="advertisersFilterData"
placeholder="Рекламодатель"
outlined
dense
hide-details="auto"
class="table-font">
</v-select>
</td>
<td class="px-1"><!-- ФИЛЬТР ПО ОФФЕРУ -->
<v-select
v-model="offerId"
:items="offersFilterData"
placeholder="Оффер"
outlined
dense
hide-details="auto"
class="table-font">
</v-select>
</td>
<td class="px-1"><!-- ФИЛЬТР ПО ГОРОДУ -->
<v-select
v-model="cityId"
:items="citiesFilterData"
placeholder="Город"
outlined
dense
hide-details="auto"
class="table-font">
</v-select>
</td>
<td class="px-1"><!-- ФИЛЬТР ПО ТЕЛЕФОНУ -->
<v-text-field
v-model="phone"
@input="phoneInput"
:value="leadsFilter.hasOwnProperty('phone') ? leadsFilter.phone : null"
outlined
dense
hide-details="auto"
class="table-font">
</v-text-field>
</td>
<td class="px-1"><!-- ФИЛЬТР ПО СТАТУСУ -->
<v-select
v-model="statusId"
:items="statusFilterData"
placeholder="Статус"
outlined
dense
hide-details="auto"
class="table-font">
</v-select>
</td>
</tr>
</template>
<template v-slot:item.id="{ item }">
<td class="table-data"><div class="table-sm-font">{{ item.id }}</div></td>
</template>
<template v-slot:item.created_at="{ item }">
<td class="table-data"><div class="table-sm-font">{{ item.created_at }}</div></td>
</template>
<template v-slot:item.created_at_advert="{ item }">
<td class="table-data"><div class="table-sm-font">{{ item.created_at_advert }}</div></td>
</template>
<template v-slot:item.webmaster="{ item }">
<td class="table-data"><div class="table-sm-font" v-if="item.webmaster !== null">{{ item.webmaster.name }}</div></td>
</template>
<template v-slot:item.advertiser="{ item }">
<td class="table-data"><div class="table-sm-font" v-if="item.advertiser !== null">{{ item.advertiser.name }}</div></td>
</template>
<template v-slot:item.offer="{ item }">
<td class="table-data"><div class="table-sm-font" v-if="item.offer !== null">{{ item.offer.name }}</div></td>
</template>
<template v-slot:item.city="{ item }">
<td class="table-data"><div class="table-sm-font" v-if="item.city !== null">{{ item.city.name }}</div></td>
</template>
<template v-slot:item.phone="{ item }">
<td class="table-data"><div class="table-sm-font">{{ item.phone }}</div></td>
</template>
<template v-slot:item.status="{ item }">
<td class="table-data">
<v-chip :color="getBadge(item.status)" x-small>{{ item.status }}</v-chip>
<div style="overflow-wrap: normal;font-size: 11px;" v-if="item.advertiser_status !== null">{{ item.advertiser_status.status }}</div>
</td>
</template>
<template v-slot:item.data-table-expand="{ item }">
<v-btn
@click="toggleDetails(item)"
color="gray"
x-small
class="px-1 py-4">
<v-icon title="Подробнее">mdi-file-document-outline</v-icon>
</v-btn>
</template>
<!-- <template v-slot:expanded-item="{ headers, item }">
<td :colspan="headers.length">
<v-row>
<v-col>
<h4>Имя: <b>{{item.name}}</b></h4>
<h4>Комментарии:</h4> <p>{{item.comments}}</p>
<h4 @click="showEditLeadData = true">Data:</h4> <p class="text-muted" @click="showEditLeadData = true">{{item.data}}</p>
<v-textarea
v-if="showEditLeadData"
:value="item.data"
auto-grow
dense
:hide-details="true"
rows="3"
append-icon="mdi-check"
@input="leadDataInput"
@click:append="editLeadData(item)"
></v-textarea>
</v-col>
<v-col>
<h4>Стоимость для рекла: <span v-if="item.balance !== null && Object.prototype.hasOwnProperty.call(item.balance, 'advertiser_sum')"><b>{{item.balance.advertiser_sum}}</b></span></h4>
<h4>Ставка для веба:</h4> <p v-if="item.balance !== null && Object.prototype.hasOwnProperty.call(item.balance, 'webmaster_sum')">{{item.balance.webmaster_sum}}</p>
<h4>Остаток для компании:</h4> <p class="text-muted" v-if="item.balance !== null && Object.prototype.hasOwnProperty.call(item.balance, 'company_sum')">{{item.balance.company_sum}}</p>
</v-col>
</v-row>
</td>
</template> -->
<template v-slot:expanded-item="{ item }">
<td :colspan="headers.length + 1" v-if="expanded.length">
<v-row>
<v-col>
<h4>Имя: <b v-if="expanded[expanded.indexOf(item)]">{{ expanded[expanded.indexOf(item)].name }}</b></h4>
<h4>Комментарии:</h4> <p v-if="expanded[expanded.indexOf(item)]">{{ expanded[expanded.indexOf(item)].comments }}</p>
<h4 @click="showEditLeadData = true">Data:</h4> <p class="text-muted" v-if="expanded[expanded.indexOf(item)]" @click="showEditLeadData = true">{{ expanded[expanded.indexOf(item)].data }}</p>
<v-textarea
v-if="showEditLeadData"
:value="item.data"
auto-grow
dense
:hide-details="true"
rows="3"
append-icon="mdi-check"
@input="leadDataInput"
@click:append="editLeadData(item)"
></v-textarea>
</v-col>
<v-col>
<h4>Стоимость для рекла: <span v-if="expanded[expanded.indexOf(item)] && expanded[expanded.indexOf(item)].balance !== null && Object.prototype.hasOwnProperty.call(item.balance, 'advertiser_sum')"><b>{{ expanded[expanded.indexOf(item)].balance.advertiser_sum }}</b></span></h4>
<h4>Ставка для веба:</h4> <p v-if="expanded[expanded.indexOf(item)] && expanded[expanded.indexOf(item)].balance !== null && Object.prototype.hasOwnProperty.call(item.balance, 'webmaster_sum')">{{ expanded[expanded.indexOf(item)].balance.webmaster_sum }}</p>
<h4>Остаток для компании:</h4> <p class="text-muted" v-if="expanded[expanded.indexOf(item)] && expanded[expanded.indexOf(item)].balance !== null && Object.prototype.hasOwnProperty.call(item.balance, 'company_sum')">{{ expanded[expanded.indexOf(item)].balance.company_sum }}</p>
</v-col>
</v-row>
</td>
</template>
<template v-slot:item.buttons="{ item }">
<td class="table-data">
<v-list class="d-flex btns-list">
<v-list-item
class="btns-list-item">
<v-btn
color="orange"
@click="showBalanceFormLead(item)"
x-small
class="px-1 py-4">
<v-icon title="Поменять ставки">mdi-currency-usd</v-icon>
</v-btn>
</v-list-item>
<v-list-item
class="btns-list-item">
<v-btn
color="green"
@click="setQueue(item.id)"
x-small
class="px-1 py-4 white--text">
<v-icon title="В очередь">mdi-refresh</v-icon>
</v-btn>
</v-list-item>
<v-list-item
class="btns-list-item">
<v-btn
id="lead-delete-activator"
color="red"
@click="leadToDelete = item.id; leadDeleteModal = true"
x-small
class="px-1 py-4 white--text">
<v-icon title="Удалить">mdi-trash-can-outline</v-icon>
</v-btn>
</v-list-item>
</v-list>
</td>
</template>
</v-data-table>
<v-pagination
v-show="leadsListMeta.last_page > 1"
v-model="current_page"
:length="leadsListMeta.last_page"
:total-visible="7">
</v-pagination>
<v-dialog
v-model="leadDeleteModal"
small
color="red"
width="500"
>
<v-card>
<v-card-title>Действительно удалить лид?</v-card-title>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn @click="leadDeleteModal = false">Отмена</v-btn>
<v-btn @click="setDelete" color="red">Удалить </v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<BalanceEditModal></BalanceEditModal>
</v-card>
</v-col>
</v-row>
</v-container>
</template>
<script>
import {mapGetters} from 'vuex'
import BalanceEditModal from "@/components/BalanceEditModal";
import Axios from 'axios'
export default {
name: "Leads",
components: { BalanceEditModal },
data() {
return {
id: null,
menuDateCreated: false,
menuDateAdv: false,
dateRange: [],
dateRangeAdvert: [],
webmasterId: null,
advertiserId: null,
offerId: null,
cityId: null,
phone: null,
statusId: null,
webmastersFilterData: [],
advertisersFilterData: [],
offersFilterData: [],
statusFilterData: [],
citiesFilterData: [],
last_page: 1,
current_page: 1,
sorter: {},
filter: {},
expanded: [],
leadDeleteModal: false,
leadToDelete: null,
url: process.env.VUE_APP_URL,
options: {},
selected: [],
selectedId: [],
showEditLeadData: false,
leadDataText: '',
}
},
computed: {
headers() {
return [
{text: 'ID', value: 'id', width: 35},
{text: 'Дата созд.', value: 'created_at', width: 150},
{text: 'Дата отпр.', value: 'created_at_advert', width: 150},
{text: 'Вебм', value: 'webmaster'},
{text: 'Рекл', value: 'advertiser'},
{text: 'Оффер', value: 'offer'},
{text: 'Город', value: 'city'},
{text: 'Телефон', value: 'phone'},
{text: 'Статус', value: 'status'},
{text: '', value: 'data-table-expand'},
{text: 'Управление', value: 'buttons', sortable: false, width: 192},
]
},
...mapGetters(['leadsList', 'leadsListMeta', 'leadsListLinks', 'leadsSort', 'leadsPage', 'leadsFilter', "showLeadBalanceEditSuccess"]),
},
methods: {
idInput(val) {
this.filter = Object.assign(this.filter, {id: val})
this.chFilter()
},
createdSelect(val) {
let start_date = new Date(val[0]).getTime() / 1000 | 0
let end_date = new Date(val[1]).getTime() / 1000 | 0
this.filter = Object.assign(this.filter, {created_at_from: start_date, created_at_to: end_date})
this.chFilter()
},
createdAdvSelect(val) {
let start_date = new Date(val[0]).getTime() / 1000 | 0
let end_date = new Date(val[1]).getTime() / 1000 | 0
this.filter = Object.assign(this.filter, {created_at_adv_from: start_date, created_at_adv_to: end_date})
this.chFilter()
},
phoneInput (val) {
if (val.length >= 3) {
this.filter = Object.assign(this.filter, { phone: val} )
this.chFilter()
}
if (!val) {
this.filter = Object.assign(this.filter, { phone: null} )
this.chFilter()
}
},
chFilter() {
this.$store.commit('SET_LEADS_FILTER', this.filter)
this.chCurrent()
},
chCurrent() {
window.scrollTo(0, document.body.scrollTop)
this.$store.dispatch('getLeads')
},
chPage(val) {
this.$store.commit('SET_LEADS_PAGE', val)
this.chCurrent()
},
chSort(val) {
this.$store.commit('SET_LEADS_SORT', val)
this.chCurrent()
},
getBadge(status) {
switch (status) {
case 'accepted':
return 'green'
case 'sent':
return 'blue'
case 'waiting':
return 'orange'
case 'queue':
return 'red'
case 'canceled' :
return 'gray'
default:
'primary'
}
},
/*toggleDetails(item) {
let index = this.expanded.indexOf(item)
if (index < 0) {
this.$set(this.expanded, this.expanded.length, item)
} else {
this.expanded.splice(index, 1)
}
},*/
toggleDetails(item) {
let index = this.expanded.indexOf(item)
if (index < 0) {
this.$set(this.expanded, this.expanded.length, item)
} else {
this.expanded.splice(index, 1)
}
},
showBalanceForm() {
this.$store.commit('SET_SHOW_LEAD_BALANCE_EDIT_MODAL', true)
},
showBalanceFormLead(item) {
this.$store.dispatch('getLeadOne', item.id)
this.$store.commit('SET_SHOW_LEAD_BALANCE_EDIT_MODAL', true)
},
setQueue(id) {
this.$store.dispatch('queueLead', id)
},
setDelete() {
this.$store.dispatch('deleteLead', this.leadToDelete)
this.leadDeleteModal = false
this.leadToDelete = null
},
async cleanFilter() {
this.id = null
this.dateRange = []
this.dateRangeAdvert = []
this.webmasterId = null
this.advertiserId = null
this.offerId = null
this.cityId = null
this.statusId = null
let headers = { 'Access-Control-Allow-Origin': '*' }
await Axios.get(this.url + '/api/leads?page=1', { headers: headers })
.then(data => { this.$store.commit('SET_LEADS', data) })
this.filter = {}
await this.chFilter()
await this.setSearchFormsData()
},
async setSearchFormsData() {
this.dateRange[0] = Object.prototype.hasOwnProperty.call(this.filter, 'created_at_from') ? this.filter.created_at_from * 1000 : null
this.dateRange[1] = Object.prototype.hasOwnProperty.call(this.filter, 'created_at_to') ? this.filter.created_at_to * 1000 : null
this.dateRangeAdvert[0] = Object.prototype.hasOwnProperty.call(this.filter, 'created_at_adv_from') ? this.filter.created_at_adv_from * 1000 : null
this.dateRangeAdvert[1] = Object.prototype.hasOwnProperty.call(this.filter, 'created_at_adv_to') ? this.filter.created_at_adv_to * 1000 : null
let headers = { 'Access-Control-Allow-Origin': '*' }
await Axios.get(this.url + '/api/service/cities-list', { headers: headers })
.then(data => {
let empty = { text: '', value: '' }
for (let elem of data.data) {
elem.text = elem.label
delete elem.label
}
data.data.unshift(empty)
this.citiesFilterData = data.data
})
.catch(e => {
console.log(e)
})
await Axios.get(this.url + '/api/service/lead-status-list', { headers: headers })
.then(data => {
let empty = { text: '', value: '' }
for (let elem of data.data) {
elem.text = elem.label
delete elem.label
}
data.data.unshift(empty)
this.statusFilterData = data.data
})
.catch(e => {
console.log(e)
})
await Axios.get(this.url + '/api/service/advertisers-list', { headers: headers })
.then(data => {
let empty = { text: '', value: '' }
for (let elem of data.data) {
elem.text = elem.label
delete elem.label
}
data.data.unshift(empty)
this.advertisersFilterData = data.data
})
.catch(e => {
console.log(e)
})
await Axios.get(this.url + '/api/service/offers-list', { headers: headers })
.then(data => {
let empty = { text: '', value: '' }
for (let elem of data.data) {
elem.text = elem.label
delete elem.label
}
data.data.unshift(empty)
this.offersFilterData = data.data //Object.assign(empty, data.data)
})
.catch(e => {
console.log(e)
})
await Axios.get(this.url + '/api/service/webmasters-list', { headers: headers })
.then(data => {
let empty = { text: '', value: '' }
for (let elem of data.data) {
elem.text = elem.label
delete elem.label
}
data.data.unshift(empty)
this.webmastersFilterData = data.data
})
.catch(e => {
console.log(e)
})
},
chooseDatePreset(e) {
var moment = require('moment')
moment().format()
let startDay, endDay
if (e.target.dataset.period == 'today') {
startDay = endDay = moment().format().substr(0, 10)
}
if (e.target.dataset.period == 'yesterday') {
startDay = endDay = moment().subtract(1, 'days').format().substr(0, 10)
}
if (e.target.dataset.period == 'this-month') {
startDay = moment([moment().year(), moment().month(), 1]).format().substr(0, 10)
endDay = moment([moment().year(), moment().month(), moment().daysInMonth()]).format().substr(0, 10)
}
if (e.target.dataset.period == 'this-year') {
startDay = moment([moment().year(), 0, 1]).format().substr(0, 10)
endDay = moment([moment().year(), 11, 31]).format().substr(0, 10)
}
if (e.target.dataset.period == 'last-month') {
let monthAgo = moment().subtract(1, 'months')
let prevDay = moment([moment().year(), moment().month(), 1]).subtract(1, 'days')
startDay = moment([monthAgo.get('year'), monthAgo.get('month'), 1]).format().substr(0, 10)
endDay = moment([monthAgo.get('year'), monthAgo.get('month'), prevDay.get('date')]).format().substr(0, 10)
}
if (e.target.dataset.adv == 'false') {
this.$set(this.dateRange, 0, startDay)
this.$set(this.dateRange, 1, endDay)
this.createdSelect(this.dateRange)
}
if (e.target.dataset.adv == 'true') {
this.$set(this.dateRangeAdvert, 0, startDay)
this.$set(this.dateRangeAdvert, 1, endDay)
this.createdAdvSelect(this.dateRangeAdvert)
}
},
leadDataInput(val) {
this.leadDataText = val
},
editLeadData(item) {
this.$store.dispatch('getLeadOne', item.id)
let data = {
id: item.id,
data: this.leadDataText
}
this.$store.dispatch('editLeadData', data)
this.showEditLeadData = false
}
},
watch: {
webmasterId: function(val) {
this.$set(this.filter, 'webmaster_id', val)
this.chFilter()
},
advertiserId: function(val) {
this.$set(this.filter, 'advertiser_id', val)
this.chFilter()
},
offerId: function(val) {
this.$set(this.filter, 'offer_id', val)
this.chFilter()
},
cityId: function(val) {
this.$set(this.filter, 'city_id', val)
this.chFilter()
},
statusId: function(val) {
this.$set(this.filter, 'status_id', val)
this.chFilter()
},
current_page: function(val) {
this.$emit('update:page', val)
this.chPage(val)
},
options: async function(val) {
//this.$emit('update:options', val)
await this.$store.commit('SET_LEADS_PER_PAGE', val.itemsPerPage)
let sorter = {
column: null,
asc: true
}
if (val.sortBy.length != 0 && val.sortDesc.length != 0) {
sorter.column = val.sortBy[0]
sorter.asc = !val.sortDesc[0]
}
await this.chSort(sorter)
},
selected: function(val) {
let selectedLeads = []
for (let elem of val) {
selectedLeads.push(elem.id)
}
this.$store.commit('SET_SELECTED_LEADS', selectedLeads)
},
},
mounted() {
this.last_page = this.leadsListMeta.last_page
if (Object.prototype.hasOwnProperty.call(this.leadsListMeta, 'current_page')) {
this.current_page = this.leadsListMeta.current_page
}
if (this.leadsSort) {
this.sorter = this.leadsSort
}
if (this.leadsFilter) {
this.filter = this.leadsFilter
}
this.setSearchFormsData()
}
}
</script>
<style>
.form-group {
margin-bottom: unset;
}
.table-sm-font{
font-size: 10.5px;
}
.text-start {
box-sizing: border-box;
padding: 10px !important;
}
.text-start:last-child {
padding-left: 0 !important;
}
.btns-list {
background: none !important;
}
.v-list-item {
min-height: auto;
}
.btns-list-item {
padding-left: 0 !important;
padding-right: 10px !important;
}
.calendar-presets button {
outline: none;
font-size: 14px;
}
.calendar-presets button:hover {
border-bottom: 1px solid #0d47a1;
}
.table-font {
font-size: 10.5px;
}
.sortable {
min-width: 94px;
}
</style>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment