数据异步问题处理v1.0

feature/fj
MustangDeng 3 years ago
parent 6e668c9d89
commit 1341e4cdac

@ -73,86 +73,156 @@ export default {
"parentId": 2 "parentId": 2
} }
], ],
'GET /user/data': [ 'GET /user/data': {
{ "data": [
"id": 1, {
"parentId": null, "fleaderjob": "销售",
"ftype": 0, "fname": "维森总部",
"fname": "维森集团", "ftype": "0",
"fleadername": "杨文元", "fleadername": "杨文元",
"fleaderimg": "./img/avator.png", "fonjob": "7902",
"fleaderjob": "董事长", "fleaderimg": "",
"fplan": 1000, "id": "15",
"fonjob": 987 "fplan": "10",
}, "expand": true
{ },
"id": 2, {
"parentId": 1, "fleaderjob": "部长",
"ftype": 1, "fname": "人力资源部",
"fname": "南京分公司", "ftype": "2",
"fleadername": "杨文元", "fleadername": "杨文元",
"fleaderimg": "./img/avator.png", "fonjob": "198",
"fleaderjob": "总经理", "fleaderimg": "",
"fplan": 300, "id": "5",
"fonjob": 287 "fnumber": "5",
}, "parentId": "2",
{ "fplan": "200"
"id": 3, },
"parentId": 1, {
"ftype": 1, "fleaderjob": "部长",
"fname": "南京分公司", "fname": "培训组",
"ftype": "2",
"fleadername": "杨文元",
"fonjob": "198",
"fleaderimg": "",
"id": "6",
"fnumber": "6",
"parentId": "2",
"fplan": "200"
},
{
"fleaderjob": "部长",
"fname": "服务管理组",
"ftype": "2",
"fleadername": "杨文元",
"fonjob": "198",
"fleaderimg": "",
"id": "7",
"fnumber": "7",
"parentId": "2",
"fplan": "200"
},
{
"fleaderjob": "部长",
"fname": "人事服务组",
"ftype": "2",
"fleadername": "杨文元",
"fonjob": "198",
"fleaderimg": "",
"id": "8",
"fnumber": "8",
"parentId": "2",
"fplan": "200"
},
{
"fleaderjob": "部长",
"fname": "员工关系组",
"ftype": "2",
"fleadername": "杨文元",
"fonjob": "198",
"fleaderimg": "",
"id": "9",
"fnumber": "9",
"parentId": "2",
"fplan": "200"
},
{
"fleaderjob": "部长",
"fname": "信息与数据组",
"ftype": "2",
"fleadername": "杨文元",
"fonjob": "198",
"fleaderimg": "",
"id": "10",
"fnumber": "10",
"parentId": "2",
"fplan": "200"
},
{
"fleaderjob": "部长",
"fname": "招聘组",
"ftype": "2",
"fleadername": "杨文元",
"fonjob": "198",
"fleaderimg": "",
"id": "11",
"fnumber": "11",
"parentId": "3",
"fplan": "200",
"expand": "1"
},
{
"fleaderjob": "部长",
"fname": "薪酬核算组",
"ftype": "2",
"fleadername": "杨文元", "fleadername": "杨文元",
"fleaderimg": "./img/avator.png", "fonjob": "198",
"fleaderimg": "",
"id": "12",
"fnumber": "12",
"parentId": "4",
"fplan": "200"
},
{
"fleaderjob": "总经理", "fleaderjob": "总经理",
"fplan": 300, "fname": "共享服务中心",
"fonjob": 287 "ftype": "1",
},
{
"id": 4,
"parentId": 1,
"ftype": 1,
"fname": "南京分公司",
"fleadername": "杨文元", "fleadername": "杨文元",
"fleaderimg": "./img/avator.png", "fonjob": "198",
"fleaderimg": "",
"id": "2",
"fnumber": "2",
"parentId": "15",
"fplan": "200",
"expand": "1"
},
{
"fleaderjob": "总经理", "fleaderjob": "总经理",
"fplan": 300, "fname": "事业部A",
"fonjob": 287 "ftype": "1",
},
{
"id": 5,
"parentId": 2,
"ftype": 2,
"fname": "销售部",
"fleadername": "杨文元", "fleadername": "杨文元",
"fleaderimg": "./img/avator.png", "fonjob": "198",
"fleaderjob": "部长", "fleaderimg": "",
"fplan": 200, "id": "3",
"fonjob": 200 "fnumber": "3",
}, "parentId": "15",
{ "fplan": "200",
"id": 6, "expand": "1"
"parentId": 5, },
"ftype": 3, {
"fname": "销售", "fleaderjob": "总经理",
"fleadername": null, "fname": "苏州分公司",
"fleaderimg": null, "ftype": "1",
"fleaderjob": null,
"fplan": 200,
"fonjob": 200
},
{
"id": 7,
"parentId": 6,
"ftype": 4,
"fname": "杨文元",
"fleadername": "杨文元", "fleadername": "杨文元",
"department": "销售部", "fonjob": "198",
"fleaderimg": "./img/avator.png", "fleaderimg": "",
"fleaderjob": "销售", "id": "4",
"mobile": "13989058743", "fnumber": "4",
"address": "秦淮区新街口12-201", "parentId": "15",
"fplan": 200, "fplan": "200",
"fonjob": 200 "expand": "1"
} }
] ],
"api_status": true
}
} }

@ -17,17 +17,20 @@ export const OrgChartComponent = (props, ref) => {
useLayoutEffect(() => { useLayoutEffect(() => {
if (props.data && d3Container.current) { if (props.data && d3Container.current) {
if (!chart) { if (!chart) {
chart = new OrgChart(); chart = new OrgChart({expandLevel: 3});
window.chart = chart
} }
props.setChart(chart) props.setChart(chart)
try { try {
let buttonClick = chart.onButtonClick
chart chart
.container(d3Container.current) .container(d3Container.current)
.data(props.data) .data(props.data)
.nodeWidth(props.nodeWidth) .nodeWidth(props.nodeWidth)
.nodeHeight(props.nodeHeight) .nodeHeight(props.nodeHeight)
.layout("left") .layout("left")
.linkUpdate(function(d, i, arr) { .linkUpdate(function(d, i, arr) {
d3.select(this) d3.select(this)
.attr("stroke", "#66BAF5") .attr("stroke", "#66BAF5")
@ -38,11 +41,17 @@ export const OrgChartComponent = (props, ref) => {
console.log(d, 'Id of clicked node '); console.log(d, 'Id of clicked node ');
props.onNodeClick(d); props.onNodeClick(d);
}) })
.buttonContent(props.buttonContent) .buttonContent(props.buttonContent)
.nodeContent(props.nodeContent) .nodeContent(props.nodeContent)
.render(); .render()
chart.setCentered(chart.getChartState().root.id).expandExpandNodes().render();
chart.onButtonClick = (event, d) => {
buttonClick.bind(chart)(event, d)
props.onButtonClick && props.onButtonClick(event, d);
}
chart.expandAll()
} catch(err) { } catch(err) {
console.log(err); console.log(err);
} }

@ -65,12 +65,12 @@ export class TopBar extends React.Component {
<div className={style.topbarWrapper}> <div className={style.topbarWrapper}>
<Row> <Row>
<Col span={4}> <Col span={5}>
数据日期<DatePicker placeholder="请选择日期" style={{ width: 120 }} defaultValue={moment(new Date())} value={moment(this.state.requestData.date)} onChange={(value) => this.handleFormChange({date: value && value != "" ? value.format("YYYY-MM-DD") : ""})} /> 数据日期<DatePicker placeholder="请选择日期" style={{ width: 120 }} defaultValue={moment(new Date())} value={this.state.requestData.date && this.state.requestData.data != "" ? moment(this.state.requestData.date) : ""} onChange={(value) => this.handleFormChange({date: value && value != "" ? value.format("YYYY-MM-DD") : ""})} />
</Col> </Col>
<Col span={4}> <Col span={5}>
维度<Select defaultValue="0" style={{ width: 120 }} value={this.state.requestData.fclass} onChange={(value) => this.handleFormChange({fclass: value})}> 维度<Select defaultValue="0" style={{ width: 120 }} value={this.state.requestData.fclass} onChange={(value) => this.handleFormChange({fclass: value})}>
{ {
this.state.fclasslist.map(item => (<Option value={item.id}>{item.companyname}</Option>)) this.state.fclasslist.map(item => (<Option value={item.id}>{item.companyname}</Option>))
@ -78,7 +78,7 @@ export class TopBar extends React.Component {
</Select> </Select>
</Col> </Col>
<Col span={4}> <Col span={5}>
根节点<Select 根节点<Select
showSearch showSearch
filterOption={(input, option) => (option?.children ).includes(input)} filterOption={(input, option) => (option?.children ).includes(input)}
@ -89,7 +89,7 @@ export class TopBar extends React.Component {
</Select> </Select>
</Col> </Col>
<Col span={4}> <Col span={5}>
显示层级<Select defaultValue="3" style={{ width: 120 }} value={this.state.requestData.level} onChange={(value) => this.handleFormChange({level: value})}> 显示层级<Select defaultValue="3" style={{ width: 120 }} value={this.state.requestData.level} onChange={(value) => this.handleFormChange({level: value})}>
<Option value="1">一级</Option> <Option value="1">一级</Option>
<Option value="2">二级</Option> <Option value="2">二级</Option>
@ -102,9 +102,9 @@ export class TopBar extends React.Component {
</Select> </Select>
</Col> </Col>
<Col span={4}> {/* <Col span={4}>
<Checkbox style={{ marginTop: "5px" }} onChange={(e) => this.handleFormChange({fisvitual: e.target.checked ? "1": "0"})}>显示虚拟组织</Checkbox> <Checkbox style={{ marginTop: "5px" }} onChange={(e) => this.handleFormChange({fisvitual: e.target.checked ? "1": "0"})}>显示虚拟组织</Checkbox>
</Col> </Col> */}
<Col span={4}> <Col span={4}>
<Button type="primary" style={{ marginRight: "10px" }} onClick={() => {this.props.onSearch(this.state.requestData)}}>查询</Button> <Button type="primary" style={{ marginRight: "10px" }} onClick={() => {this.props.onSearch(this.state.requestData)}}>查询</Button>

@ -14,12 +14,51 @@ export default function companyPage() {
const [sliderProgress, setSliderProgress] = useState(50); const [sliderProgress, setSliderProgress] = useState(50);
let addNodeChildFunc = null; let addNodeChildFunc = null;
let orgChart = null; let orgChart = null;
let topBarSearchRequest = null;
// //
function onNodeClick(nodeId) { function onNodeClick(nodeId) {
// alert('clicked ' + nodeId); // alert('clicked ' + nodeId);
} }
//
function onButtonClick(event, d) {
if(d.children) {
let idsList = []
d.children.forEach(item => {
if(item.data.hasChildren && !item._children) {
idsList.push(item.data.id);
}
})
if(idsList.length == 0) {
return
}
let idsStr = idsList.join(",")
console.log("idsStr", idsStr);
let api = "";
if(topBarSearchRequest) {
let request = {...topBarSearchRequest, ids: idsStr}
api = "/api/bs/hrmorganization/orgchart/asyncCompanyData" + qs.stringify(request, {addQueryPrefix: true})
} else {
api = "/api/bs/hrmorganization/orgchart/asyncCompanyData?fclass=0&root=0&date=" + moment(new Date()).format("YYYY-MM-DD") + "&ids="+idsStr
}
fetch(api).then(res => res.json()).then(data => {
if(data.data) {
data.data.forEach(item => {
window.chart.addNode(item)
})
}
})
}
}
// //
function getDepartmentImage() { function getDepartmentImage() {
let index = Math.floor(Math.random() * 8) + 1 let index = Math.floor(Math.random() * 8) + 1
@ -78,8 +117,15 @@ export default function companyPage() {
const nodeContentRender = (d, i, arr, state) => { const nodeContentRender = (d, i, arr, state) => {
//
let companyUrl = "/spa/organization/static/index.html#/main/organization/group"
//
let subcompanyUrl = "/spa/organization/static/index.html#/main/organization/companyExtend/"
//
let departmentUrl = "/spa/organization/static/index.html#/main/organization/departmentExtend/"
if(d.data.ftype == 0) { if(d.data.ftype == 0) {
return `<div> return `<div onclick="window.open('${companyUrl}', '_blank')">
<div style="display: inline-block; vertical-align: top;"> <div style="display: inline-block; vertical-align: top;">
<img src="./img/company.png" /> <img src="./img/company.png" />
</div> </div>
@ -104,7 +150,7 @@ export default function companyPage() {
</div> </div>
</div>` </div>`
} else if(d.data.ftype == 1) { } else if(d.data.ftype == 1) {
return `<div> return `<div onclick="window.open('${subcompanyUrl + d.data.fnumber}', '_blank')">
<div style="width: 85px; height: 85px; border: 1px solid #66BAF5; border-radius: 50%;text-align: center; line-height: 85px; margin: 0 auto;"> <div style="width: 85px; height: 85px; border: 1px solid #66BAF5; border-radius: 50%;text-align: center; line-height: 85px; margin: 0 auto;">
<img src="${getSubcompanyImage()}" /> <img src="${getSubcompanyImage()}" />
</div> </div>
@ -121,7 +167,7 @@ export default function companyPage() {
</div>` </div>`
} else if(d.data.ftype == 2) { } else if(d.data.ftype == 2) {
return ` return `
<div style="width: 100%; height: 100%; background: url('./img/company_job_label.png');background-size: 100% 100%;"> <div style="width: 100%; height: 100%; background: url('./img/company_job_label.png');background-size: 100% 100%;" onclick="window.open('${departmentUrl + d.data.fnumber}')">
<div style="padding-left: 8px; padding-top: 23px;"> <div style="padding-left: 8px; padding-top: 23px;">
<img src="${getDepartmentImage()}"/> <img src="${getDepartmentImage()}"/>
<span style=" <span style="
@ -212,7 +258,8 @@ export default function companyPage() {
} }
const handleSearch = (requestData) => { const handleSearch = (requestData) => {
let api = "/api/bs/hrmorganization/orgchartt/companyData" + qs.stringify(requestData, {addQueryPrefix: true}) topBarSearchRequest = requestData
let api = "/api/bs/hrmorganization/orgchart/companyData" + qs.stringify(requestData, {addQueryPrefix: true})
fetch(api).then(res => res.json()).then(data => { fetch(api).then(res => res.json()).then(data => {
setData(data.data); setData(data.data);
}) })
@ -226,7 +273,7 @@ export default function companyPage() {
<TopBar <TopBar
onExport={(type) => {handleExport(type)}} onExport={(type) => {handleExport(type)}}
onSearch={(requestData) => {handleSearch(requestData)}} onSearch={(requestData) => {handleSearch(requestData)}}
url="/bs/hrmorganization/orgchart/jcl/orgchart/getCondition?type=company" url="/api/bs/hrmorganization/orgchart/getCondition?type=company"
/> />
<ToolBar <ToolBar
onTopLayoutClick={(progressBtn) => handleTopLayoutClick(progressBtn)} onTopLayoutClick={(progressBtn) => handleTopLayoutClick(progressBtn)}

@ -14,12 +14,51 @@ export default function userPage() {
let addNodeChildFunc = null; let addNodeChildFunc = null;
let orgChart = null; let orgChart = null;
let progressBtnRef = null; let progressBtnRef = null;
let topBarSearchRequest = null;
// //
function onNodeClick(nodeId) { function onNodeClick(nodeId) {
// alert('clicked ' + nodeId); // alert('clicked ' + nodeId);
} }
function onButtonClick(event, d) {
if(d.children) {
let idsList = []
d.children.forEach(item => {
if(item.data.hasChildren && !item._children) {
idsList.push(item.data.id);
}
})
if(idsList.length == 0) {
return
}
let idsStr = idsList.join(",")
console.log("idsStr", idsStr);
let api = "";
if(topBarSearchRequest) {
let request = {...topBarSearchRequest, ids: idsStr}
api = "/api/bs/hrmorganization/orgchart/asyncUserData" + qs.stringify(request, {addQueryPrefix: true})
} else {
api = "/api/bs/hrmorganization/orgchart/asyncUserData?fclass=0&root=0&date=" + moment(new Date()).format("YYYY-MM-DD") + "&ids="+idsStr
}
fetch(api).then(res => res.json()).then(data => {
if(data.data) {
data.data.forEach(item => {
window.chart.addNode(item)
})
}
})
}
}
// //
function getDepartmentImage() { function getDepartmentImage() {
let index = Math.floor(Math.random() * 8) + 1 let index = Math.floor(Math.random() * 8) + 1
@ -64,12 +103,12 @@ export default function userPage() {
// tool bar start // tool bar start
const handleTopLayoutClick = (progressBtn) => { const handleTopLayoutClick = (progressBtn) => {
progressBtn.current.style.top= 50 + "px"; progressBtn.current.style.top= 50 + "px";
orgChart && orgChart.layout('top').render().fit(); orgChart && orgChart.layout('top').render()
} }
const handleLeftLayoutClick = (progressBtn) => { const handleLeftLayoutClick = (progressBtn) => {
progressBtn.current.style.top= 50 + "px"; progressBtn.current.style.top= 50 + "px";
orgChart && orgChart.layout('left').render().fit(); orgChart && orgChart.layout('left').render()
} }
const handleZoomIn = (progressBtn) => { const handleZoomIn = (progressBtn) => {
@ -130,7 +169,8 @@ export default function userPage() {
} }
const handleSearch = (requestData) => { const handleSearch = (requestData) => {
let api = "/api/bs/hrmorganization/orgchart/jcl/orgchart/userData" + qs.stringify(requestData, {addQueryPrefix: true}) topBarSearchRequest = requestData
let api = "/api/bs/hrmorganization/orgchart/userData" + qs.stringify(requestData, {addQueryPrefix: true})
fetch(api).then(res => res.json()).then(data => { fetch(api).then(res => res.json()).then(data => {
setData(data.data) setData(data.data)
}) })
@ -139,6 +179,21 @@ export default function userPage() {
// top bar end // top bar end
const nodeContentRender = (d, i, arr, state) => { const nodeContentRender = (d, i, arr, state) => {
//
let companyUrl = "/spa/organization/static/index.html#/main/organization/group"
//
let subcompanyUrl = "/spa/organization/static/index.html#/main/organization/companyExtend/"
//
let departmentUrl = "/spa/organization/static/index.html#/main/organization/departmentExtend/"
//
let jobtitleUrl = "/spa/organization/static/index.html#/main/organization/jobExtend/";
//
let userUrl = "/spa/hrm/index_mobx.html#/main/hrm/card/cardInfo/";
//
let addressBookUrl = "/spa/hrm/index_mobx.html#/main/hrm/addressBook";
if(d.data.ftype == 0 || d.data.ftype == 1 || d.data.ftype == 2) { if(d.data.ftype == 0 || d.data.ftype == 1 || d.data.ftype == 2) {
return `<div> return `<div>
<div style="position: relative;"> <div style="position: relative;">
@ -154,13 +209,15 @@ export default function userPage() {
font-family: Microsoft YaHei-Bold, Microsoft YaHei; font-family: Microsoft YaHei-Bold, Microsoft YaHei;
font-weight: bold; font-weight: bold;
color: #000000; color: #000000;
">${d.data.fname}</span> " onclick="window.open('${d.data.ftype == 0 ? companyUrl :
d.data.ftype == 1 ? subcompanyUrl + d.data.fnumber :
d.data.ftype == 2 ? departmentUrl + d.data.fnumber : ""}', '_blank')">${d.data.fname}</span>
<span style="margin-left: 70px;"> <span style="margin-left: 70px;">
<img src="./img/user-card/line1.png" /> <img src="./img/user-card/line1.png" />
<img src="./img/user-card/line2.png" /> <img src="./img/user-card/line2.png" />
</span> </span>
<div style="background: url('./img/user-card/user-card.png'); height: 152px;background-size: 100% 100%;box-sizing: border-box;padding-top: 30px;"> <div style="background: url('./img/user-card/user-card.png'); height: 152px;background-size: 100% 100%;box-sizing: border-box;padding-top: 30px;" onclick="window.open('${userUrl + d.data.fleader}', '_blank')">
<div style="display: inline-block; background: url('./img/user-card/avatar-outer.png'); background-size: 100% 100%; width: 90px; height: 90px; text-align:center; vertical-align: top; margin-left: 11px;box-sizing: border;"> <div onclick="window.open('${userUrl + d.data.fleader}', '_blank')" style="display: inline-block; background: url('./img/user-card/avatar-outer.png'); background-size: 100% 100%; width: 90px; height: 90px; text-align:center; vertical-align: top; margin-left: 11px;box-sizing: border;">
<img src="${d.data.fleaderimg ? d.data.fleaderimg : "./img/default_avator.png"}" style="width: 58px; height: 58px; border-radius: 50%; margin-top: 16px;"/> <img src="${d.data.fleaderimg ? d.data.fleaderimg : "./img/default_avator.png"}" style="width: 58px; height: 58px; border-radius: 50%; margin-top: 16px;"/>
</div> </div>
<div style="display: inline-block; margin-left: 6px;"> <div style="display: inline-block; margin-left: 6px;">
@ -178,9 +235,9 @@ export default function userPage() {
color: #333333; color: #333333;
margin-bottom: 19px; margin-bottom: 19px;
">${d.data.fname} / ${d.data.fleaderjob}</div> ">${d.data.fname} / ${d.data.fleaderjob}</div>
<div style="display: flex;"> <div style="display: flex;" onclick="window.open('${addressBookUrl}', '_blank')">
<div style="height: 28px;border: 1px solid #00C2FF; border-radius: 10px; line-height: 28px; padding: 0px 5px; min-width: 69px;">编制: ${d.data.fplan}</div> <div style="height: 28px;border: 1px solid #00C2FF; border-radius: 10px; line-height: 24px; padding: 0px 5px; min-width: 69px;">编制: ${d.data.fplan}</div>
<div style="height: 28px;border: 1px solid #00C2FF; border-radius: 10px; line-height: 28px; padding: 0px 5px; min-width: 69px; margin-left: 10px;">在岗: ${d.data.fonjob}</div> <div style="height: 28px;border: 1px solid #00C2FF; border-radius: 10px; line-height: 24px; padding: 0px 5px; min-width: 69px; margin-left: 10px;">在岗: ${d.data.fonjob}</div>
</div> </div>
</div> </div>
</div> </div>
@ -201,7 +258,9 @@ export default function userPage() {
font-family: Microsoft YaHei-Bold, Microsoft YaHei; font-family: Microsoft YaHei-Bold, Microsoft YaHei;
font-weight: bold; font-weight: bold;
color: #000000; color: #000000;
">${d.data.fname}</span> " onclick="window.open('${
jobtitleUrl + d.data.fnumber
}', '_blank')">${d.data.fname}</span>
<span style="margin-left: 70px;"> <span style="margin-left: 70px;">
<img src="./img/user-card/line1.png" /> <img src="./img/user-card/line1.png" />
<img src="./img/user-card/line2.png" /> <img src="./img/user-card/line2.png" />
@ -215,13 +274,15 @@ export default function userPage() {
font-weight: bold; font-weight: bold;
color: #333333; color: #333333;
margin-bottom: 23px; margin-bottom: 23px;
">${d.data.fname}</div> " onclick="window.open('${
jobtitleUrl + d.data.fnumber
}', '_blank')">${d.data.fname}</div>
<div style=" <div style="
font-size: 13px; font-size: 13px;
font-family: Microsoft YaHei-Regular, Microsoft YaHei; font-family: Microsoft YaHei-Regular, Microsoft YaHei;
font-weight: 400; font-weight: 400;
color: #333333; color: #333333;
"> " onclick="window.open('${addressBookUrl}', '_blank')">
<span>编制${d.data.fplan}</span> <span>编制${d.data.fplan}</span>
<span style="margin-left: 10px;">在岗${d.data.fonjob}</span> <span style="margin-left: 10px;">在岗${d.data.fonjob}</span>
</div> </div>
@ -231,7 +292,7 @@ export default function userPage() {
</div>` </div>`
} else if(d.data.ftype == 4) { } else if(d.data.ftype == 4) {
return `<div> return `<div>
<div style="position: relative;"> <div style="position: relative;" onclick="window.open('${userUrl + d.data.fnumber}', '_blank')">
<img src="./img/user-card/card-label-start.png" /> <img src="./img/user-card/card-label-start.png" />
<span > <span >
<img src="./img/user-card/line1.png" /> <img src="./img/user-card/line1.png" />
@ -291,6 +352,7 @@ export default function userPage() {
setChart={(chart) => orgChart = chart} setChart={(chart) => orgChart = chart}
setClick={click => (addNodeChildFunc = click)} setClick={click => (addNodeChildFunc = click)}
onNodeClick={onNodeClick} onNodeClick={onNodeClick}
onButtonClick={onButtonClick}
data={data} data={data}
buttonContent={ buttonContent={
buttonContentRender buttonContentRender

Loading…
Cancel
Save