WorkingGroup.vue 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516
  1. <template>
  2. <view class="workGroup-list">
  3. <view class="working-group" v-for="({ data, info }, index) of historyList" :key="index">
  4. <view>
  5. <view class="title" v-if="!info.isCheckIn">{{ '开始上通道/负责区域' }}</view>
  6. <view v-if="!info.isCheckIn" class="content">
  7. <view class="empty" style="margin: 20px auto 0;"></view>
  8. <view class="empty-text">当前未上通道/区域</view>
  9. </view>
  10. <view v-else class="content">
  11. <view class="content-cell" v-if="notkezhang">
  12. <text class="content-cell-label">通道</text>
  13. <text class="content-cell-value">{{ info.channelName || info.channelCode || '--' }}</text>
  14. </view>
  15. <view class="content-cell" v-if="notkezhang">
  16. <text class="content-cell-label">当班人员</text>
  17. <text></text>
  18. </view>
  19. <view class="content-workings" v-if="notkezhang">
  20. <view class="personnel-list" v-if="data.length">
  21. <view class="personnel-item" v-for="item of data" :key="item.id">
  22. <view class="personnel-img">
  23. <UserAvatar :userName="item.userName" :avatarLink="item.avatar" />
  24. </view>
  25. <view class="personnel-name">{{ item.userName }}</view>
  26. </view>
  27. </view>
  28. <view class="empty" v-else></view>
  29. </view>
  30. <view class="content-cell" style="height: auto;display: flex;flex-direction: column;align-items: flex-start;"
  31. v-if="!notkezhang">
  32. <text class="content-cell-label">负责区域</text>
  33. <!-- <text class="content-cell-value">{{ info.regionalName || info.regionalCode || '--' }}</text> -->
  34. <view class="selected-areas"
  35. v-if="getHandleAreaData(data) && getHandleAreaData(data).length && !notkezhang">
  36. <view class="area-item" v-for="area in getHandleAreaData(data)" :key="area.code">
  37. <view class="area-label">{{ area.label }}</view>
  38. <text-tag :showClose="false" :tags="area.children" :labelColumn="'regionalName'" />
  39. </view>
  40. </view>
  41. </view>
  42. <view class="content-cell">
  43. <text class="content-cell-label">上通道时间</text>
  44. <text class="content-cell-value">{{ info.checkInTimeFormat }}</text>
  45. </view>
  46. <view class="content-cell" v-if="info.isCheckIn">
  47. <text class="content-cell-label">下通道时间</text>
  48. <text class="content-cell-value">{{ info.checkOutTime || '-:-' }}</text>
  49. </view>
  50. </view>
  51. </view>
  52. <!-- {{ selectedMember.length === 0 }}{{ !userInfo.roles.includes('banzuzhang') }}{{ !attendanceInfo.checkInTime }} -->
  53. <!-- :disabled="!attendanceInfo.checkInTime || checkOutStatus" -->
  54. <AddAttendancePersonnelModal v-if="index === 0" :disabled="checkOutStatus" :notkezhang="notkezhang"
  55. :userInfo="userInfo" @updateRecord="invokerGetPostRecordList" :selectedMember="selectedMember"
  56. :attendanceInfo="attendanceInfo">
  57. <!-- :class="{ disabled: !attendanceInfo.checkInTime }" 没有选人不可以上通道 -->
  58. <view v-if="authority" class="custom-btn" @click="openModal"
  59. :class="{ disabled: selectedMember.length === 0 && !userInfo.roles.includes('banzuzhang') || (!attendanceInfo.checkInTime || (attendanceInfo.checkInTime && attendanceInfo.checkOutTime)) }">
  60. {{ checkOutStatus ? '下通道' : '上通道' }}
  61. </view>
  62. </AddAttendancePersonnelModal>
  63. </view>
  64. <u-popup :show="checkOutWork" mode="center" :round="8">
  65. <view class="modal-content">
  66. <view class="title">
  67. <text>员工下通道</text>
  68. <u-icon name="close" color="#666666" size="20" @click="close" />
  69. </view>
  70. <view class="title-cell">
  71. <view class="title-text">{{ '选择下通道时间' }}</view>
  72. <uni-datetime-picker type="datetime" v-model="currentTime" />
  73. </view>
  74. <view class="title-cell">
  75. <view class="custom-btn-normal" @click="invokerUpdatePostRecord" :class="{ disabled: !currentTime }">确认下通道
  76. </view>
  77. </view>
  78. </view>
  79. </u-popup>
  80. </view>
  81. </template>
  82. <script>
  83. import { listgroupbyTimeanduserid, updatePostRecord } from "@/api/attendance/attendance"
  84. import { formatTime, formatName } from '@/utils/formatUtils'
  85. import AddAttendancePersonnelModal from './AddAttendancePersonnelModal';
  86. import UserAvatar from './UserAvatar'
  87. export default {
  88. components: { AddAttendancePersonnelModal, UserAvatar },
  89. props: {
  90. userInfo: {
  91. type: Object,
  92. default: () => ({})
  93. },
  94. attendanceInfo: { // 考勤信息
  95. type: Object,
  96. default: () => ({})
  97. },
  98. selectedMember: {
  99. type: Array,
  100. default: () => []
  101. }
  102. },
  103. data() {
  104. return {
  105. curUserWorkingGroupData: {
  106. passage: '',
  107. checkInTime: '',
  108. checkOutTime: '',
  109. isCheckIn: false,
  110. },
  111. checkOutStatus: false,
  112. checkOutWork: false,
  113. currentTime: undefined,
  114. workingGroupId: '',
  115. historyList: [], //班组工作记录 todo 现在只能做科长
  116. }
  117. },
  118. computed: {
  119. notkezhang() {
  120. return !this.userInfo.roles || !this.userInfo.roles.includes('banzuzhang')
  121. },
  122. authority() { // 上通道权限
  123. return this.userInfo.roles && (
  124. this.userInfo.roles.includes('banzuzhang') ||
  125. this.userInfo.roles.includes('xiaozuzhang')
  126. )
  127. }
  128. },
  129. methods: {
  130. openModal() {
  131. if (!this.checkOutStatus) {
  132. return;
  133. }
  134. this.currentTime = formatTime(new Date())
  135. this.checkOutWork = true
  136. },
  137. close() {
  138. this.currentTime = undefined
  139. this.checkOutWork = false
  140. },
  141. isCheckOutJobs(curUserWorkingInfo = this.curUserWorkingGroupData) {
  142. return Boolean(curUserWorkingInfo.checkInTime) && (!curUserWorkingInfo.checkOutTime || curUserWorkingInfo.checkOutTime === '2000-01-01 00:00:00')
  143. },
  144. getHandleAreaData(data) {
  145. let louObj = {};
  146. let areaArr = [];
  147. data.forEach(element => {
  148. let name = `${element.terminlName}-${element.terminlCode}`
  149. if (!louObj[name]) {
  150. louObj[name] = [];
  151. }
  152. louObj[name].push(element);
  153. });
  154. Object.keys(louObj).forEach(key => {
  155. areaArr.push({
  156. label: key.split('-')[0],
  157. code: key.split('-')[1],
  158. children: louObj[key]
  159. });
  160. });
  161. return areaArr;
  162. },
  163. invokerGetPostRecordList(callback) {
  164. return listgroupbyTimeanduserid().then(res => {
  165. if (res.code === 200) {
  166. const resData = (res.data || [])
  167. this.historyList = resData.reduce((cur, item, index) => {
  168. let info = {}
  169. const workDataList = item.records.map((attr) => {
  170. const result = {
  171. ...attr,
  172. isCheckIn: Boolean(attr.checkInTime), // 是否上过通道
  173. checkInTimeFormat: formatTime(attr.checkInTime, 'hh:mm:ss'),
  174. checkOutTime: attr.checkOutTime === '2000-01-01 00:00:00' ? '' : formatTime(attr.checkOutTime, 'hh:mm:ss'),
  175. userName: formatName(attr.userName),
  176. }
  177. // if (attr.userId === this.userInfo.userId) {
  178. // info = result
  179. // }
  180. info = result
  181. if (index === 0) { // 取最新纪录
  182. this.curUserWorkingGroupData = result
  183. this.workingGroupId = item.shiftCode
  184. }
  185. return result
  186. })
  187. cur.push({
  188. info: info,
  189. data: workDataList,
  190. workingGroupId: item.shiftCode
  191. })
  192. return cur
  193. }, [])
  194. this.checkOutStatus = this.isCheckOutJobs(this.curUserWorkingGroupData)
  195. if (!this.checkOutStatus) {
  196. this.historyList = [{
  197. info: {},
  198. data: [],
  199. workingGroupId: undefined
  200. }, ...this.historyList]
  201. }
  202. }
  203. }).catch(() => {
  204. return Promise.resolve()
  205. }).finally(() => {
  206. callback && callback()
  207. })
  208. },
  209. createRecordData(curUserRecordInfo) {
  210. // 🔥 重要:计算工作时长
  211. const checkTime = new Date(this.currentTime);
  212. const workDurationMinutes = Math.floor((Date.now() - checkTime.getTime()) / 1000 / 60);
  213. // 🔥 重要:构建更新数据
  214. let res = [];
  215. //如果是科长走多选逻辑,如果是班组长走单选逻辑
  216. if (!this.notkezhang) {
  217. console.log(this.historyList)
  218. this.historyList.forEach(item => {
  219. item.data.forEach(element => {
  220. res.push(
  221. {
  222. // 基本字段
  223. ...curUserRecordInfo,
  224. isCheckIn: undefined,
  225. revision: (curUserRecordInfo.revision || 1) + 1,
  226. // 时间相关
  227. checkInTime: typeof curUserRecordInfo.checkInTime === 'string' ? curUserRecordInfo.checkInTime : formatTime(new Date(curUserRecordInfo.checkInTime)),
  228. attendanceDate: typeof curUserRecordInfo.attendanceDate === 'string' ? curUserRecordInfo.attendanceDate : formatTime(new Date(curUserRecordInfo.attendanceDate), 'YYYY-MM-DD'),
  229. workDuration: workDurationMinutes, // 重新计算工作时长
  230. overDuration: curUserRecordInfo.overDuration || 0,
  231. // 组织架构信息
  232. attendanceTeamId: curUserRecordInfo.attendanceTeamId || null,
  233. attendanceTeamName: curUserRecordInfo.attendanceTeamName || '',
  234. attendanceDepartmentId: curUserRecordInfo.attendanceDepartmentId || null,
  235. attendanceDepartmentName: curUserRecordInfo.attendanceDepartmentName || '',
  236. attendanceStationId: curUserRecordInfo.attendanceStationId || null,
  237. attendanceStationName: curUserRecordInfo.attendanceStationName || '',
  238. remark: curUserRecordInfo.remark || '手动添加上通道记录',
  239. // 审计字段
  240. createBy: curUserRecordInfo.createBy || (this.userInfo.userId + ''),
  241. createTime: typeof curUserRecordInfo.createTime === 'string' ? curUserRecordInfo.createTime : formatTime(new Date(curUserRecordInfo.createTime)),
  242. updateBy: (this.userInfo.userId + ''),
  243. updateTime: formatTime(new Date()),
  244. ...element,
  245. checkOutTime: formatTime(checkTime), // 设置实际下岗时间
  246. }
  247. )
  248. })
  249. })
  250. } else {
  251. res = [{
  252. // 基本字段
  253. ...curUserRecordInfo,
  254. userName: (curUserRecordInfo.userName || '').replace(/\s/g, ''),
  255. isCheckIn: undefined,
  256. revision: (curUserRecordInfo.revision || 1) + 1,
  257. // 时间相关
  258. checkInTime: typeof curUserRecordInfo.checkInTime === 'string' ? curUserRecordInfo.checkInTime : formatTime(new Date(curUserRecordInfo.checkInTime)),
  259. checkOutTime: formatTime(checkTime), // 设置实际下岗时间
  260. attendanceDate: typeof curUserRecordInfo.attendanceDate === 'string' ? curUserRecordInfo.attendanceDate : formatTime(new Date(curUserRecordInfo.attendanceDate), 'YYYY-MM-DD'),
  261. workDuration: workDurationMinutes, // 重新计算工作时长
  262. overDuration: curUserRecordInfo.overDuration || 0,
  263. // 组织架构信息
  264. attendanceTeamId: curUserRecordInfo.attendanceTeamId || null,
  265. attendanceTeamName: curUserRecordInfo.attendanceTeamName || '',
  266. attendanceDepartmentId: curUserRecordInfo.attendanceDepartmentId || null,
  267. attendanceDepartmentName: curUserRecordInfo.attendanceDepartmentName || '',
  268. attendanceStationId: curUserRecordInfo.attendanceStationId || null,
  269. attendanceStationName: curUserRecordInfo.attendanceStationName || '',
  270. remark: curUserRecordInfo.remark || '手动添加上通道记录',
  271. // 审计字段
  272. createBy: curUserRecordInfo.createBy || (this.userInfo.userId + ''),
  273. createTime: typeof curUserRecordInfo.createTime === 'string' ? curUserRecordInfo.createTime : formatTime(new Date(curUserRecordInfo.createTime)),
  274. updateBy: (this.userInfo.userId + ''),
  275. updateTime: formatTime(new Date())
  276. }]
  277. }
  278. console.log(res, "res")
  279. return res;
  280. },
  281. invokerUpdatePostRecord() {
  282. if (this.checkOutStatus) {
  283. // 科长下区域
  284. if (!this.notkezhang) {
  285. uni.showLoading({ title: '正在下通道...' });
  286. updatePostRecord(this.createRecordData(this.curUserWorkingGroupData)).then(res => {
  287. if (res && res.code === 200) {
  288. uni.showToast({
  289. title: '下通道完成!',
  290. icon: 'success',
  291. duration: 2000
  292. });
  293. } else {
  294. uni.showToast({
  295. title: `下通道失败!${res.msg || '未知错误'}`,
  296. icon: 'none',
  297. duration: 2000
  298. });
  299. }
  300. this.close()
  301. return this.invokerGetPostRecordList()
  302. }).catch(error => {
  303. uni.showToast({
  304. title: '下通道失败:' + (error.msg || '请重试'),
  305. icon: 'error',
  306. duration: 2000
  307. });
  308. }).finally(() => {
  309. uni.hideLoading();
  310. })
  311. } else {// 当班人员下通道
  312. uni.showLoading({ title: '正在批量下通道...' });
  313. // 获取当前最新的工作组数据
  314. const recordList = this.historyList.find(item => item.workingGroupId === this.workingGroupId) || { data: [] }
  315. const promises = recordList.data.map(item => {
  316. return updatePostRecord(this.createRecordData(item)).then(res => {
  317. if (res && res.code === 200) {
  318. return `${item.userName}: 下通道完成!`
  319. } else {
  320. return `${item.userName}下通道失败: ${res.msg || '未知错误'}`
  321. }
  322. }).catch((err) => {
  323. return Promise.resolve(`${item.userName}下通道失败: 网络错误!`)
  324. })
  325. })
  326. Promise.all(promises).then((res) => {
  327. uni.showModal({
  328. title: '批量下通道结果',
  329. content: `${res.join('\n')}`,
  330. showCancel: false,
  331. confirmText: '确定'
  332. });
  333. this.close()
  334. return this.invokerGetPostRecordList()
  335. }).finally(() => {
  336. uni.hideLoading();
  337. })
  338. }
  339. }
  340. }
  341. },
  342. }
  343. </script>
  344. <style lang="scss" scoped>
  345. .workGroup-list {
  346. display: flex;
  347. flex-direction: column;
  348. row-gap: 15px;
  349. }
  350. .working-group {
  351. width: 100%;
  352. background: #FFFFFF;
  353. box-shadow: 0px 4px 10px 0px rgba(0, 0, 0, 0.08);
  354. border-radius: 16px 16px 16px 16px;
  355. border: 1px solid #F0F8FF;
  356. padding: 15px;
  357. box-sizing: border-box;
  358. .empty {
  359. margin: 0 auto;
  360. width: 160px;
  361. height: 155px;
  362. background: url("../../../static/images/Empty.png") no-repeat;
  363. background-size: cover;
  364. }
  365. .empty-text {
  366. font-weight: 400;
  367. font-size: 14px;
  368. color: #3D3D3D;
  369. line-height: 16px;
  370. text-align: center;
  371. font-style: normal;
  372. text-transform: none;
  373. margin-bottom: 20px;
  374. }
  375. .title {
  376. font-weight: 400;
  377. font-size: 18px;
  378. color: #333333;
  379. line-height: 24px;
  380. text-align: left;
  381. font-style: normal;
  382. text-transform: none;
  383. padding: 5px 0;
  384. }
  385. .content-cell {
  386. display: flex;
  387. align-items: center;
  388. justify-content: space-between;
  389. font-weight: 400;
  390. font-size: 14px;
  391. font-style: normal;
  392. text-transform: none;
  393. width: 100%;
  394. height: 36px;
  395. .content-cell-label {
  396. color: #222222;
  397. }
  398. .content-cell-value {
  399. color: #999999;
  400. padding-right: 5px;
  401. }
  402. .selected-areas {
  403. .area-item {
  404. .area-label {
  405. height: 72rpx;
  406. line-height: 72rpx;
  407. }
  408. }
  409. }
  410. }
  411. .content-workings {
  412. display: flex;
  413. padding: 10px 0;
  414. .personnel-list {
  415. display: flex;
  416. flex-wrap: wrap;
  417. row-gap: 10px;
  418. column-gap: 10px;
  419. .personnel-item {
  420. width: fit-content;
  421. height: 34px;
  422. background: #F0F0F0;
  423. border-radius: 6px;
  424. display: flex;
  425. align-items: center;
  426. column-gap: 8px;
  427. padding: 0 10px;
  428. .personnel-img {
  429. width: 24px;
  430. height: 24px;
  431. border-radius: 6px;
  432. overflow: hidden;
  433. }
  434. .personnel-name {
  435. width: fit-content;
  436. font-weight: 400;
  437. font-size: 13px;
  438. color: #3D3D3D;
  439. text-align: left;
  440. font-style: normal;
  441. text-transform: none;
  442. }
  443. }
  444. }
  445. }
  446. }
  447. .modal-content {
  448. width: 90vw;
  449. max-height: 75vh;
  450. padding: 10px 0;
  451. .title {
  452. height: 24px;
  453. font-weight: 400;
  454. font-size: 18px;
  455. color: #333333;
  456. line-height: 21px;
  457. display: flex;
  458. justify-content: space-between;
  459. align-items: center;
  460. padding: 15px;
  461. box-sizing: border-box;
  462. }
  463. .title-cell {
  464. padding: 10px 15px;
  465. box-sizing: border-box;
  466. .title-text {
  467. height: 16px;
  468. font-weight: 400;
  469. font-size: 14px;
  470. color: #333333;
  471. line-height: 16px;
  472. text-align: left;
  473. margin-bottom: 14px;
  474. text-indent: 5px;
  475. }
  476. }
  477. }
  478. </style>