增加坐席报表

This commit is contained in:
Shaun Chyxion 2020-10-21 23:19:13 +08:00
parent 5549c82587
commit 99d94d177b
19 changed files with 410 additions and 96 deletions

View File

@ -18,13 +18,18 @@ import com.pudonghot.yo.basic.model.PrivacyLevel;
import com.pudonghot.yo.mapper.CallRecordingMapper;
import com.pudonghot.yo.model.domain.CallRecording;
import com.pudonghot.yo.model.domain.CallDetailRecord;
import com.pudonghot.yo.model.dbobject.CallDetailReport;
import org.springframework.beans.factory.annotation.Value;
import com.pudonghot.yo.cms.form.FormListCallDetailRecord;
import com.wacai.tigon.web.controller.BaseQueryController;
import com.pudonghot.yo.cms.service.CallDetailRecordService;
import com.pudonghot.yo.cellphone.privacy.NumberPrivacyUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.RequestMapping;
import static com.pudonghot.yo.model.domain.CallDetailRecord.*;
import com.pudonghot.yo.cms.form.FormCallDetailRecordAccountReport;
import com.pudonghot.yo.model.request.ReqCallDetailRecordAccountReport;
/**
* @author Donghuang
@ -79,6 +84,11 @@ public class CallDetailRecordController
}
}
@RequestMapping("/account-report")
public List<CallDetailReport> accountReport(@Validated final FormCallDetailRecordAccountReport form) {
return ((CallDetailRecordService) queryService).accountReport(form.copy(new ReqCallDetailRecordAccountReport()));
}
/**
* {@inheritDoc}
*/

View File

@ -0,0 +1,26 @@
package com.pudonghot.yo.cms.form;
import lombok.Getter;
import lombok.Setter;
import java.util.Date;
import lombok.ToString;
import javax.validation.constraints.NotNull;
import com.wacai.tigon.format.annotation.Trim;
import com.wacai.tigon.format.annotation.EmptyToNull;
/**
* @author Donghuang
* @date Oct 21, 2020 21:14:01
*/
@Getter
@Setter
@ToString(callSuper = true)
public class FormCallDetailRecordAccountReport extends BasicForm {
@NotNull
private Date dateFrom;
@NotNull
private Date dateTo;
@Trim
@EmptyToNull
private String account;
}

View File

@ -4,6 +4,7 @@ import lombok.Getter;
import lombok.Setter;
import java.util.Date;
import com.wacai.tigon.form.FormList;
import javax.validation.constraints.NotNull;
/**
* @author Donghuang
@ -12,10 +13,13 @@ import com.wacai.tigon.form.FormList;
@Getter
@Setter
public class FormListCallDetailRecord extends FormList {
@NotNull
private Date startDate;
@NotNull
private Date endDate;
private String connId;
private String account;
private String callerNumber;
private String calledNumber;
private Boolean related;
}

View File

@ -1,12 +1,22 @@
package com.pudonghot.yo.cms.service;
import com.pudonghot.yo.model.domain.CallDetailRecord;
import com.pudonghot.yo.model.dbobject.CallDetailReport;
import com.pudonghot.yo.model.request.ReqCallDetailRecordAccountReport;
import com.wacai.tigon.service.BaseQueryService;
import com.pudonghot.yo.model.domain.CallDetailRecord;
import java.util.List;
/**
* @author bingpo
* @date 2020/3/30 下午3:23
* @author Donghuang
* @date Oct 21, 2020 21:10:19
*/
public interface CallDetailRecordService extends BaseQueryService<Integer, CallDetailRecord> {
/**
* account report
* @param form
* @return
*/
List<CallDetailReport> accountReport(ReqCallDetailRecordAccountReport form);
}

View File

@ -1,15 +1,18 @@
package com.pudonghot.yo.cms.service.impl;
import com.pudonghot.yo.cms.service.CallDetailRecordService;
import com.pudonghot.yo.mapper.CallDetailRecordMapper;
import com.pudonghot.yo.model.domain.CallDetailRecord;
import com.wacai.tigon.service.support.BaseQueryServiceSupport;
import java.util.List;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import com.pudonghot.yo.model.domain.CallDetailRecord;
import com.pudonghot.yo.mapper.CallDetailRecordMapper;
import com.pudonghot.yo.model.dbobject.CallDetailReport;
import com.pudonghot.yo.cms.service.CallDetailRecordService;
import com.wacai.tigon.service.support.BaseQueryServiceSupport;
import com.pudonghot.yo.model.request.ReqCallDetailRecordAccountReport;
/**
* @author bingpo
* @date 2020/3/30 下午3:26
* @author Donghuang
* @date Oct 21, 2020 21:10:57
*/
@Slf4j
@Service
@ -17,4 +20,11 @@ public class CallDetailRecordServiceImpl
extends BaseQueryServiceSupport<Integer, CallDetailRecord, CallDetailRecordMapper>
implements CallDetailRecordService{
/**
* {@inheritDoc}
*/
@Override
public List<CallDetailReport> accountReport(final ReqCallDetailRecordAccountReport form) {
return mapper.accountReport(form);
}
}

View File

@ -1,48 +0,0 @@
server.port=8180
server.error.include-stacktrace=always
spring.application.name=yo-cms
spring.jackson.time-zone=GMT+8
spring.jackson.serialization.write-dates-as-timestamps=true
spring.jackson.serialization.fail-on-empty-beans=false
spring.servlet.multipart.max-file-size=256MB
spring.servlet.multipart.max-request-size=256MB
site.context-path=
# Datasource
yo.datasource.url=jdbc:mysql://localhost/yoqw?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useLegacyDatetimeCode=false&serverTimezone=Asia/Shanghai
yo.datasource.username=yoqw
yo.datasource.password=yoqw_query!
# Datasource
yo.fs.datasource.url=jdbc:mysql://localhost/fs_dev?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useLegacyDatetimeCode=false&serverTimezone=Asia/Shanghai
yo.fs.datasource.username=freeswitch
yo.fs.datasource.password=RR!h5IpirsnJ
# Redis
spring.redis.host=localhost
spring.redis.port=6379
spring.redis.password=123456
yo.cms.recording-server.base-path=http://172.16.52.80/fs/rec/
# CAS
tigon.shiro.cas.server.addr=https://cas.pudong-hot.com
tigon.shiro.cas.client.addr=http://localhost:4200
tigon.shiro.filter-chain=${site.context-path}/auth/login=anon \
/=anon \
/state-machine/**=anon \
/nlp-component/**=anon \
/index.html=anon \
/assets/**=anon \
/**=user
yo.cms.sound.store-dir=/Users/chyxion/cms-sound
yo.cms.sound.base-path=http://localhost:8180/sound/file/
# Dubbo
## Dubbo Registry
dubbo.registry.address=zookeeper://localhost:2182
dubbo.registry.file=${user.home}/dubbo-cache/${spring.application.name}/dubbo.cache
dubbo.consumer.check=false

View File

@ -0,0 +1,66 @@
server:
port: 8180
error:
include-stacktrace: always
spring:
application:
name: yo-cms
jackson:
time-zone: GMT+8
serialization:
write-dates-as-timestamps: true
fail-on-empty-beans: false
servlet:
multipart:
max-file-size: 256MB
max-request-size: 256MB
redis:
host: localhost
port: 6379
password: 123456
site:
context-path:
# Datasource
yo:
datasource:
url: jdbc:mysql://localhost/yoqw?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useLegacyDatetimeCode=false&serverTimezone=Asia/Shanghai
username: yoqw
password: yoqw_query!
fs:
datasource:
url: jdbc:mysql://localhost/fs_dev?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useLegacyDatetimeCode=false&serverTimezone=Asia/Shanghai
username: freeswitch
password: RR!h5IpirsnJ
cms:
recording-server:
base-path: http://172.16.52.80/fs/rec/
sound:
store-dir: /Users/chyxion/cms-sound
base-path: http://localhost:8180/sound/file/
# CAS
tigon:
shiro:
cas:
server:
addr: https://cas.pudong-hot.com
client:
addr: http://localhost:4200
filter-chain: >
${site.context-path}/auth/login=anon
/=anon
/index.html=anon
/assets/**=anon
/**=user
## Dubbo Registry
dubbo:
registry:
address: zookeeper://localhost:2181
file: ${user.home}/dubbo-cache/${spring.application.name}/dubbo.cache
consumer:
check: false

View File

@ -1,12 +1,11 @@
package com.pudonghot.yo.mapper;
import java.util.List;
import com.pudonghot.yo.model.request.ReqCallDetailRecordAccountReport;
import com.wacai.tigon.mybatis.BaseMapper;
import org.apache.ibatis.annotations.Param;
import com.pudonghot.yo.model.domain.CallDetailRecord;
import com.pudonghot.yo.model.dbobject.CallDetailReport;
import com.pudonghot.yo.model.request.ReqCallDetailRecordAccountReport;
/**
* @author Donghuang <br>

View File

@ -104,6 +104,9 @@
max(start_stamp) max_start_stamp
from br_call_detail_record
where start_stamp between #{arg.dateFrom} and #{arg.dateTo}
<if test="arg.account != null">
and account = #{arg.account}
</if>
and tenant_id = #{arg.tenantId}
group by account, dial_type, call_type
</select>

View File

@ -19,4 +19,5 @@ public class ReqCallDetailRecordAccountReport implements Serializable {
private Integer tenantId;
private Date dateFrom;
private Date dateTo;
private String account;
}

View File

@ -0,0 +1,14 @@
import { helper } from '@ember/component/helper';
export default helper(function secToTime([sec]) {
let seconds = parseInt(sec % 60),
minutes = parseInt((sec / 60) % 60),
hours = parseInt((sec / (60 * 60)) % 24);
hours = (hours < 10) ? "0" + hours : hours;
minutes = (minutes < 10) ? "0" + minutes : minutes;
seconds = (seconds < 10) ? "0" + seconds : seconds;
return hours + ":" + minutes + ":" + seconds;
});

View File

@ -104,6 +104,12 @@ export default (function() {
icon: 'fa fa-calendar-check-o',
text: '话务质检'
},
{
perm: 'PERM_VIEW_CALL_DETAIL_RECORD_ACCOUNT_REPORT',
route: 'call-detail-record.account-report',
icon: 'fa fa-database',
text: '账户报表'
},
{
perm: 'PERM_VIEW_PHONE_WHITELIST_LIST',
route: 'phone-whitelist.list',

View File

@ -106,6 +106,7 @@ Router.map(function() {
this.route('call-detail-record', function() {
this.route('list');
this.route('quality-inspection');
this.route('account-report');
});
this.route('agent-status', function() {

View File

@ -0,0 +1,44 @@
import BaseListRoute from '../base-list';
export default BaseListRoute.extend({
perm: 'PERM_VIEW_CALL_DETAIL_RECORD_ACCOUNT_REPORT',
breadcrumbs: [{text: '账户报表'}],
queryParams: {
dateFrom: {
refreshModel: false
},
dateTo: {
refreshModel: false
}
},
model(params) {
const me = this;
if (!params.dateFrom) {
params.dateFrom = me.dateFrom().getTime();
}
if (!params.dateTo) {
params.dateTo = new Date().getTime();
}
return me.get('service').ajaxGet('account-report', params);
},
setupController(controller) {
const me = this;
me._super(...arguments);
if (!controller.get('dateFrom')) {
controller.set('dateFrom', me.dateFrom().getTime());
}
if (!controller.get('dateTo')) {
controller.set('dateTo', new Date().getTime());
}
},
actions: {
search() {
const me = this;
me.refresh();
}
},
dateFrom() {
return new Date(new Date().toDateString());
}
});

View File

@ -0,0 +1,132 @@
<div class="widget-box transparent" style="padding-top: 2px; border: 1px solid #ddd;">
<div class="space"></div>
<form class="form-horizontal" role="form">
<div class="row">
{{form-input-datetimepicker
class='col-sm-5 col-md-5'
label-class='col-sm-3 col-md-3'
input-class='col-sm-9 col-md-9'
error-msg=false
model=this
name='dateFrom'
label='起始时间'}}
{{form-input-datetimepicker
class='col-sm-5 col-md-5'
label-class='col-sm-3 col-md-3'
input-class='col-sm-9 col-md-9'
error-msg=false
model=this
name='dateTo'
label='截止时间'}}
</div>
<div class="row">
<div class="col-sm-5 col-md-5">
<div class="col-sm-3 col-md-3">
</div>
<button type="button" class="btn btn-info btn-sm col-sm-4 col-md-4"
{{action (route-action 'search')}}>
<i class="ace-icon fa fa-search bigger-110"></i>查询
</button>
</div>
</div>
</form>
<div class="space-18"></div>
<div class="widget-body">
<!-- #section:custom/scrollbar -->
<div class="widget-main no-padding">
<table class="table table-striped table-bordered table-hover dataTable" style="border-top-width: 1px;">
<thead class="thin-border-bottom">
<tr>
<th>
坐席
</th>
<th>
呼叫类型
</th>
<th>
拨打类型
</th>
<th>
开始时间
</th>
<th>
结束时间
</th>
<th>
总量
</th>
<th>
接通
</th>
<th>
通话时长
</th>
<th>
最大通话时长
</th>
<th>
平均通话时长
</th>
</tr>
</thead>
<tbody>
{{#each model as |it|}}
<tr>
<td>
{{it.account}}
</td>
<td>
<span class="label label-sm {{if (eq 'INTERNAL' it.callType) 'label-info' (if (eq 'OUTBOUND' it.callType) 'label-success' 'label-info2')}}">
{{#if (eq 'INTERNAL' it.callType)}}
{{else if (eq 'OUTBOUND' it.callType)}}
{{else}}
{{/if}}
</span>
</td>
<td>
<span class="label label-sm {{if (eq 'MANUAL' it.dialType) 'label-info' (if (eq 'CAMPAIGN' it.dialType) 'label-success' 'label-info2')}}">
{{#if (eq 'MANUAL' it.dialType)}}
{{else if (eq 'CAMPAIGN' it.dialType)}}
{{else}}
{{/if}}
</span>
</td>
<td>
{{date-cell value=it.minStartStamp}}
</td>
<td>
{{date-cell value=it.maxStartStamp}}
</td>
<td>
{{it.total}}
</td>
<td>
{{it.answered}}
</td>
<td>
{{it.duration}}
</td>
<td>
{{it.maxDuration}}
</td>
<td>
{{it.avgDuration}}
</td>
</tr>
{{/each}}
</tbody>
</table>
</div>
</div>
</div>
{{outlet}}

View File

@ -77,13 +77,13 @@
<th>
坐席
</th>
<th>
<th style="min-width: 88px;">
{{th-filter name='callType'
text='呼叫类型'
options=model.callTypeList
}}
</th>
<th>
<th style="min-width: 88px;">
{{th-filter name='dialType'
text='拨打类型'
options=model.dialTypeList
@ -99,13 +99,7 @@
开始时间
</th>
<th>
接通时间
</th>
<th>
结束时间
</th>
<th>
通话时长(s)
通话时长
</th>
<th>
挂断原因
@ -125,10 +119,26 @@
{{it.account}}
</td>
<td>
{{it.callType}}
<span class="label label-sm {{if (eq 'INTERNAL' it.callType) 'label-info' (if (eq 'OUTBOUND' it.callType) 'label-success' 'label-info2')}}">
{{#if (eq 'INTERNAL' it.callType)}}
{{else if (eq 'OUTBOUND' it.callType)}}
{{else}}
{{/if}}
</span>
</td>
<td>
{{it.dialType}}
<span class="label label-sm {{if (eq 'MANUAL' it.dialType) 'label-info' (if (eq 'CAMPAIGN' it.dialType) 'label-success' 'label-info2')}}">
{{#if (eq 'MANUAL' it.dialType)}}
{{else if (eq 'CAMPAIGN' it.dialType)}}
{{else}}
{{/if}}
</span>
</td>
<td>
{{it.calledNumber}}
@ -137,16 +147,10 @@
{{it.callingPartyNumber}}
</td>
<td>
{{date-cell value=it.startStamp format='YYYY-MM-DD HH:mm:ss'}}
{{date-cell value=it.startStamp}}
</td>
<td>
{{date-cell value=it.answerStamp format='YYYY-MM-DD HH:mm:ss'}}
</td>
<td>
{{date-cell value=it.endStamp format='YYYY-MM-DD HH:mm:ss'}}
</td>
<td>
{{it.billsec}}
{{sec-to-time it.billsec}}
</td>
<td>
{{it.hangupCause}}

View File

@ -78,13 +78,13 @@
<th>
坐席
</th>
<th>
<th style="min-width: 88px;">
{{th-filter name='callType'
text='呼叫类型'
options=model.callTypeList
}}
</th>
<th>
<th style="min-width: 88px;">
{{th-filter name='dialType'
text='拨打类型'
options=model.dialTypeList
@ -99,12 +99,6 @@
<th>
接通时间
</th>
<th>
结束时间
</th>
<th>
通话时长(s)
</th>
<th>
挂断原因
</th>
@ -123,25 +117,35 @@
{{it.account}}
</td>
<td>
{{it.callType}}
<span class="label label-sm {{if (eq 'INTERNAL' it.callType) 'label-info' (if (eq 'OUTBOUND' it.callType) 'label-success' 'label-info2')}}">
{{#if (eq 'INTERNAL' it.callType)}}
{{else if (eq 'OUTBOUND' it.callType)}}
{{else}}
{{/if}}
</span>
</td>
<td>
{{it.dialType}}
<span class="label label-sm {{if (eq 'MANUAL' it.dialType) 'label-info' (if (eq 'CAMPAIGN' it.dialType) 'label-success' 'label-info2')}}">
{{#if (eq 'MANUAL' it.dialType)}}
{{else if (eq 'CAMPAIGN' it.dialType)}}
{{else}}
{{/if}}
</span>
</td>
<td>
{{it.calledNumber}}
</td>
<td>
{{date-cell value=it.startStamp format='YYYY-MM-DD HH:mm:ss'}}
{{date-cell value=it.startStamp}}
</td>
<td>
{{date-cell value=it.answerStamp format='YYYY-MM-DD HH:mm:ss'}}
</td>
<td>
{{date-cell value=it.endStamp format='YYYY-MM-DD HH:mm:ss'}}
</td>
<td>
{{it.billsec}}
{{sec-to-time it.billsec}}
</td>
<td>
{{it.hangupCause}}

View File

@ -0,0 +1,17 @@
import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import { render } from '@ember/test-helpers';
import hbs from 'htmlbars-inline-precompile';
module('Integration | Helper | sec-to-time', function(hooks) {
setupRenderingTest(hooks);
// Replace this with your real tests.
test('it renders', async function(assert) {
this.set('inputValue', '1234');
await render(hbs`{{sec-to-time inputValue}}`);
assert.equal(this.element.textContent.trim(), '1234');
});
});

View File

@ -0,0 +1,11 @@
import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';
module('Unit | Route | call-detail-record/account-report', function(hooks) {
setupTest(hooks);
test('it exists', function(assert) {
let route = this.owner.lookup('route:call-detail-record/account-report');
assert.ok(route);
});
});