疗休养人员名单

This commit is contained in:
2026-07-21 15:42:34 +08:00
parent 878b0de75f
commit b05543d624
26 changed files with 1476 additions and 35 deletions
@@ -309,9 +309,9 @@ public class SysUserServiceImpl extends BaseServiceImpl<Sys_user> implements Sys
String decodePwd = Base64Decoder.decodeStr(passowrd);
String hashedPassword = PwdUtil.getPassword(decodePwd, user.getSalt());
if (!Strings.sNull(hashedPassword).equalsIgnoreCase(user.getPassword())) {
// if (Globals.sso) {
if (Globals.sso) {
throw new BaseException("用户名或者密码不正确");
// }
}
}
user = this.fetchLinks(user, "unit");
if (Lang.isNotEmpty(user.getUnit()) && StrUtil.isNotBlank(user.getUnit().getUnionId())) {
@@ -26,6 +26,7 @@ import com.budwk.app.zhgh.dayofficework.tour.models.TourTravelAgency;
import com.budwk.app.zhgh.dayofficework.tour.service.TourLedgerDirectRelativeService;
import com.budwk.app.zhgh.dayofficework.tour.service.TourLedgerFamilyService;
import com.budwk.app.zhgh.dayofficework.tour.service.TourLedgerService;
import com.budwk.app.zhgh.dayofficework.tour.service.TourParticipantRosterService;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.hssf.usermodel.HSSFWorkbook;
import org.apache.poi.ss.usermodel.BorderStyle;
@@ -90,6 +91,9 @@ public class TourLedgerController {
@Inject
private TourLedgerDirectRelativeService tourLedgerDirectRelativeService;
@Inject
private TourParticipantRosterService tourParticipantRosterService;
@At("")
@Ok("beetl:/platform/zhgh/dayofficework/Tour/ledger/index.html")
@SaCheckPermission("tour.ledger")
@@ -216,6 +220,8 @@ public class TourLedgerController {
if (ledger == null) {
return Result.error("台账记录不存在");
}
// Reset the participant roster before deleting the ledger record from the admin ledger page.
tourParticipantRosterService.markNotJoinedByLedger(ledger);
tourLedgerFamilyService.clear(Cnd.where(TourLedgerFamily::getLedgerId, "=", id));
tourLedgerService.delete(id);
return Result.success();
@@ -31,6 +31,7 @@ import com.budwk.app.zhgh.dayofficework.tour.service.TourLedgerDirectRelativeSer
import com.budwk.app.zhgh.dayofficework.tour.service.TourLedgerFamilyService;
import com.budwk.app.zhgh.dayofficework.tour.service.TourLedgerService;
import com.budwk.app.zhgh.dayofficework.tour.service.TourMatterService;
import com.budwk.app.zhgh.dayofficework.tour.service.TourParticipantRosterService;
import org.nutz.aop.interceptor.ioc.TransAop;
import org.nutz.dao.Cnd;
import org.nutz.dao.Sqls;
@@ -83,6 +84,9 @@ public class TourMySignupController {
@Inject
private TourMatterService tourMatterService;
@Inject
private TourParticipantRosterService tourParticipantRosterService;
@Inject
private SysDictService sysDictService;
@@ -507,6 +511,8 @@ public class TourMySignupController {
return workflowResult;
}
}
// Signup modification keeps the staff member joined and refreshes the linked ledger id.
tourParticipantRosterService.markJoinedByLedger(ledger);
return Result.success().addMsg(approvalRequired ? "修改已提交,等待审核" : "修改成功");
}
@@ -529,6 +535,8 @@ public class TourMySignupController {
if (signupEndResult != null) {
return signupEndResult;
}
// Reset the roster before deleting the ledger record, because the ledger still carries setting linkage here.
tourParticipantRosterService.markNotJoinedByLedger(ledger);
tourLedgerFamilyService.clear(Cnd.where(TourLedgerFamily::getLedgerId, "=", ledger.getId()));
tourLedgerDirectRelativeService.clear(Cnd.where(TourLedgerDirectRelative::getLedgerId, "=", ledger.getId()));
tourLedgerService.clear(Cnd.where(TourLedger::getId, "=", ledger.getId()));
@@ -548,6 +556,8 @@ public class TourMySignupController {
if (signupEndResult != null) {
return signupEndResult;
}
// Reset the roster before deleting the ledger record, because the ledger still carries setting linkage here.
tourParticipantRosterService.markNotJoinedByLedger(ledger);
tourLedgerFamilyService.clear(Cnd.where(TourLedgerFamily::getLedgerId, "=", ledger.getId()));
tourLedgerDirectRelativeService.clear(Cnd.where(TourLedgerDirectRelative::getLedgerId, "=", ledger.getId()));
tourLedgerService.clear(Cnd.where(TourLedger::getId, "=", ledger.getId()));
@@ -0,0 +1,366 @@
package com.budwk.app.zhgh.dayofficework.tour.controller;
import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.hutool.core.util.StrUtil;
import com.budwk.app.base.page.Pagination;
import com.budwk.app.base.param.PageForm;
import com.budwk.app.base.result.Result;
import com.budwk.app.zhgh.dayofficework.tour.service.TourParticipantRosterService;
import org.nutz.dao.Cnd;
import org.nutz.dao.Sqls;
import org.nutz.dao.sql.Sql;
import org.nutz.dao.util.cri.SqlExpressionGroup;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.json.Json;
import org.nutz.lang.util.NutMap;
import org.nutz.mvc.annotation.At;
import org.nutz.mvc.annotation.Ok;
import org.nutz.mvc.annotation.Param;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
/**
* 疗休养人员名单统计查询控制器。
* 名单表保存可参与人员基础信息,报名线路、成团等信息通过台账补充读取。
*/
@IocBean
@Ok("json:full")
@At("/platform/tour/participantRoster")
public class TourParticipantRosterController {
@Inject
private TourParticipantRosterService tourParticipantRosterService;
/**
* 打开 PC 端疗休养人员名单查询页面。
*/
@At("")
@Ok("beetl:/platform/zhgh/dayofficework/Tour/participantRoster/index.html")
@SaCheckPermission("tour.participantRoster")
public void index() {
}
/**
* 按年度范围和可选报名维度查询人员名单。
*/
@At
@SaCheckPermission("tour.participantRoster")
public Result pageData(PageForm pageForm, Integer startYear, Integer endYear, String settingId, String matterId,
String keyword, String unitName, String unionId, Boolean joined, String lineType,
String groupCode, String undertakeUnionId) {
Result yearResult = checkYearRange(startYear, endYear);
if (yearResult != null) {
return yearResult;
}
Cnd cnd = buildQueryCnd(startYear, endYear, settingId, matterId, keyword, unitName, unionId, joined,
lineType, groupCode, undertakeUnionId);
Sql countSql = Sqls.create("""
SELECT COUNT(1)
FROM tour_participant_roster r
INNER JOIN tour_setting s ON s.id = r.settingId AND s.delFlag = 0
LEFT JOIN tour_ledger t ON t.id = r.ledgerId AND t.delFlag = 0
LEFT JOIN tour_matter m ON m.id = t.matterId AND m.delFlag = 0
LEFT JOIN tour_line l ON l.id = t.lineId AND l.delFlag = 0
$condition
""");
countSql.setCondition(cnd);
countSql.setCallback(Sqls.callback.integer());
tourParticipantRosterService.dao().execute(countSql);
Sql listSql = Sqls.create("""
SELECT
r.id,
s.`year`,
s.configName,
r.jobNo,
r.userName,
r.unitId,
r.unitName,
r.unionId,
r.unionName,
r.joined,
r.ledgerId,
t.lineType,
COALESCE(NULLIF(l.lineName, ''), t.lineName) AS lineName,
m.matterName,
CASE
WHEN IFNULL(m.travelStartTime, '') <> '' AND IFNULL(m.travelEndTime, '') <> ''
THEN CONCAT(m.travelStartTime, ' 至 ', m.travelEndTime)
ELSE ''
END AS travelPeriod,
t.groupCode,
t.groupName,
t.undertakeUnionId,
t.undertakeUnionName,
t.signupTime
FROM tour_participant_roster r
INNER JOIN tour_setting s ON s.id = r.settingId AND s.delFlag = 0
LEFT JOIN tour_ledger t ON t.id = r.ledgerId AND t.delFlag = 0
LEFT JOIN tour_matter m ON m.id = t.matterId AND m.delFlag = 0
LEFT JOIN tour_line l ON l.id = t.lineId AND l.delFlag = 0
$condition
ORDER BY $orderColumn $orderBy, s.`year` DESC, r.unionName ASC, r.unitName ASC, r.jobNo ASC
""");
listSql.setCondition(cnd);
listSql.setVar("orderColumn", getOrderColumn(pageForm.getPageOrderName()));
listSql.setVar("orderBy", "descending".equals(pageForm.getPageOrderBy()) ? "desc" : "asc");
listSql.setPager(tourParticipantRosterService.dao().createPager(pageForm.getPageNumber(), pageForm.getPageSize()));
listSql.setCallback(Sqls.callback.maps());
tourParticipantRosterService.dao().execute(listSql);
Pagination<NutMap> pagination = new Pagination<>(
pageForm.getPageNumber(),
pageForm.getPageSize(),
countSql.getInt(),
listSql.getList(NutMap.class)
);
return Result.success(pagination);
}
/**
* 根据年度范围加载疗休养配置下拉选项。
*/
@At
@SaCheckPermission("tour.participantRoster")
public Result settingOptions(Integer startYear, Integer endYear) {
Result yearResult = checkYearRange(startYear, endYear);
if (yearResult != null) {
return yearResult;
}
Sql sql = Sqls.create("""
SELECT id, configName, `year`
FROM tour_setting
WHERE delFlag = 0
AND `year` >= @startYear
AND `year` <= @endYear
ORDER BY `year` DESC, sortNo ASC, createdAt DESC
""");
sql.setParam("startYear", startYear);
sql.setParam("endYear", endYear);
sql.setCallback(Sqls.callback.maps());
tourParticipantRosterService.dao().execute(sql);
return Result.success(sql.getList(NutMap.class));
}
/**
* 加载线路选择器使用的疗休养事项选项。
* 未选择配置时加载年度范围内全部事项,选择配置后只加载当前配置下事项。
*/
@At
@SaCheckPermission("tour.participantRoster")
public Result lineOptions(Integer startYear, Integer endYear, String settingId) {
Result yearResult = checkYearRange(startYear, endYear);
if (yearResult != null) {
return yearResult;
}
Cnd cnd = Cnd.NEW();
cnd.and("s.delFlag", "=", false);
cnd.and("m.delFlag", "=", false);
cnd.and("m.lineId", "IS NOT", null);
cnd.and("m.lineId", "<>", "");
cnd.and("s.`year`", ">=", startYear);
cnd.and("s.`year`", "<=", endYear);
cnd.andEX("s.id", "=", settingId);
Sql sql = Sqls.create("""
SELECT
m.id AS matterId,
m.matterName,
s.id AS settingId,
s.configName,
s.`year`,
l.id AS lineId,
l.lineName,
l.lineType,
CONCAT(s.configName, '【', s.`year`, '】 / ', m.matterName) AS label
FROM tour_matter m
INNER JOIN tour_setting s ON s.id = m.settingId
LEFT JOIN tour_line l ON l.id = m.lineId AND l.delFlag = 0
$condition
ORDER BY s.`year` DESC, s.sortNo ASC, m.travelStartTime ASC, m.createdAt DESC
""");
sql.setCondition(cnd);
sql.setCallback(Sqls.callback.maps());
tourParticipantRosterService.dao().execute(sql);
return Result.success(sql.getList(NutMap.class));
}
/**
* 从年度范围内的人员名单加载工会下拉选项。
*/
@At
@SaCheckPermission("tour.participantRoster")
public Result unionOptions(Integer startYear, Integer endYear) {
Result yearResult = checkYearRange(startYear, endYear);
if (yearResult != null) {
return yearResult;
}
Sql sql = Sqls.create("""
SELECT DISTINCT r.unionId AS id, r.unionName AS name
FROM tour_participant_roster r
INNER JOIN tour_setting s ON s.id = r.settingId AND s.delFlag = 0
WHERE r.delFlag = 0
AND IFNULL(r.unionId, '') <> ''
AND s.`year` >= @startYear
AND s.`year` <= @endYear
ORDER BY r.unionName ASC
""");
sql.setParam("startYear", startYear);
sql.setParam("endYear", endYear);
sql.setCallback(Sqls.callback.maps());
tourParticipantRosterService.dao().execute(sql);
return Result.success(sql.getList(NutMap.class));
}
/**
* 根据参与人员范围和现有报名台账重新生成所选配置的人员名单。
*/
@At
@SaCheckPermission("tour.participantRoster")
public Result rebuildRoster(Integer startYear, Integer endYear, @Param("settingIds") String settingIds) {
Result yearResult = checkYearRange(startYear, endYear);
if (yearResult != null) {
return yearResult;
}
List<String> settingIdList = parseSettingIds(settingIds);
if (settingIdList.isEmpty()) {
return Result.error("请选择需要重新生成的疗休养配置");
}
Result settingResult = checkSettingsInYearRange(startYear, endYear, settingIdList);
if (settingResult != null) {
return settingResult;
}
tourParticipantRosterService.rebuildSettingRosters(settingIdList);
return Result.success().addMsg("人员名单重新生成成功");
}
/**
* 校验年度范围是否完整且顺序合法。
*/
private Result checkYearRange(Integer startYear, Integer endYear) {
if (startYear == null || endYear == null) {
return Result.error("年度范围不能为空");
}
if (startYear > endYear) {
return Result.error("开始年度不能大于结束年度");
}
int nextYear = LocalDate.now().getYear() + 1;
if (startYear < 1900 || endYear > nextYear) {
return Result.error("年度范围不合法");
}
return null;
}
/**
* 解析前端多选配置 ID,并过滤空值和重复值。
*/
private List<String> parseSettingIds(String settingIds) {
if (StrUtil.isBlank(settingIds)) {
return new ArrayList<>();
}
List<String> rawList = Json.fromJsonAsList(String.class, settingIds);
Set<String> idSet = new LinkedHashSet<>();
for (String id : rawList) {
if (StrUtil.isNotBlank(id)) {
idSet.add(id.trim());
}
}
return new ArrayList<>(idSet);
}
/**
* 校验所选疗休养配置是否仍属于页面提交的年度范围。
*/
private Result checkSettingsInYearRange(Integer startYear, Integer endYear, List<String> settingIds) {
Sql sql = Sqls.create("""
SELECT COUNT(1)
FROM tour_setting
WHERE delFlag = 0
AND id IN (@settingIds)
AND `year` >= @startYear
AND `year` <= @endYear
""");
sql.setParam("settingIds", settingIds.toArray(new String[0]));
sql.setParam("startYear", startYear);
sql.setParam("endYear", endYear);
sql.setCallback(Sqls.callback.integer());
tourParticipantRosterService.dao().execute(sql);
if (sql.getInt() != settingIds.size()) {
return Result.error("所选疗休养配置与年度范围不匹配,请刷新后重试");
}
return null;
}
/**
* 构造总数查询和列表查询共用的 SQL 条件。
*/
private Cnd buildQueryCnd(Integer startYear, Integer endYear, String settingId, String matterId, String keyword,
String unitName, String unionId, Boolean joined, String lineType,
String groupCode, String undertakeUnionId) {
Cnd cnd = Cnd.NEW();
cnd.and("r.delFlag", "=", false);
cnd.and("s.`year`", ">=", startYear);
cnd.and("s.`year`", "<=", endYear);
cnd.andEX("r.settingId", "=", settingId);
cnd.andEX("t.matterId", "=", matterId);
cnd.andEX("r.unionId", "=", unionId);
cnd.andEX("r.unitName", "like", StrUtil.isBlank(unitName) ? null : "%" + unitName.trim() + "%");
cnd.andEX("t.lineType", "=", lineType);
cnd.andEX("t.undertakeUnionId", "=", undertakeUnionId);
appendKeywordFilter(cnd, keyword);
return cnd;
}
/**
* 增加工号和姓名模糊查询条件。
*/
private void appendKeywordFilter(Cnd cnd, String keyword) {
if (StrUtil.isBlank(keyword)) {
return;
}
SqlExpressionGroup group = new SqlExpressionGroup();
group.orLike("r.jobNo", keyword.trim());
group.orLike("r.userName", keyword.trim());
cnd.and(group);
}
/**
* 将前端排序字段映射为安全的 SQL 排序列。
*/
private String getOrderColumn(String orderName) {
if ("year".equals(orderName)) {
return "s.`year`";
}
if ("jobNo".equals(orderName)) {
return "r.jobNo";
}
if ("userName".equals(orderName)) {
return "r.userName";
}
if ("unitName".equals(orderName)) {
return "r.unitName";
}
if ("unionName".equals(orderName)) {
return "r.unionName";
}
if ("joined".equals(orderName)) {
return "r.joined";
}
if ("lineType".equals(orderName)) {
return "t.lineType";
}
if ("lineName".equals(orderName)) {
return "COALESCE(NULLIF(l.lineName, ''), t.lineName)";
}
if ("signupTime".equals(orderName)) {
return "t.signupTime";
}
return "s.`year`";
}
}
@@ -8,6 +8,7 @@ import com.budwk.app.base.param.PageForm;
import com.budwk.app.base.result.Result;
import com.budwk.app.zhgh.dayofficework.tour.models.TourSetting;
import com.budwk.app.zhgh.dayofficework.tour.models.TourSettingLot;
import com.budwk.app.zhgh.dayofficework.tour.service.TourParticipantRosterService;
import com.budwk.app.zhgh.dayofficework.tour.service.TourSettingService;
import org.nutz.aop.interceptor.ioc.TransAop;
import org.nutz.dao.Cnd;
@@ -32,6 +33,9 @@ public class TourSettingController {
@Inject
private TourSettingService tourSettingService;
@Inject
private TourParticipantRosterService tourParticipantRosterService;
@At("")
@Ok("beetl:/platform/zhgh/dayofficework/Tour/setting/index.html")
@SaCheckPermission("tour.setting")
@@ -156,6 +160,8 @@ public class TourSettingController {
tourSettingService.updateWithNullableSignupTime(tourSetting);
saveLots(tourSetting);
}
// Initialize the participant roster only once; existing roster data is preserved on later setting edits.
tourParticipantRosterService.syncSettingRosterIfEmpty(tourSetting.getId());
return Result.success();
}
@@ -168,6 +174,8 @@ public class TourSettingController {
return Result.error("参数错误");
}
tourSettingService.dao().clear(TourSettingLot.class, Cnd.where(TourSettingLot::getSettingId, "=", id));
// Remove roster rows for the deleted setting so later statistics ignore obsolete data.
tourParticipantRosterService.clearSettingRoster(id);
tourSettingService.delete(id);
return Result.success();
}
@@ -35,6 +35,7 @@ import com.budwk.app.zhgh.dayofficework.tour.service.TourLedgerDirectRelativeSer
import com.budwk.app.zhgh.dayofficework.tour.service.TourLedgerFamilyService;
import com.budwk.app.zhgh.dayofficework.tour.service.TourLedgerService;
import com.budwk.app.zhgh.dayofficework.tour.service.TourMatterService;
import com.budwk.app.zhgh.dayofficework.tour.service.TourParticipantRosterService;
import org.nutz.aop.interceptor.ioc.TransAop;
import org.nutz.dao.Cnd;
import org.nutz.dao.Sqls;
@@ -79,6 +80,9 @@ public class TourSignupController {
@Inject
private TourLedgerDirectRelativeService tourLedgerDirectRelativeService;
@Inject
private TourParticipantRosterService tourParticipantRosterService;
@Inject
private SysDictService sysDictService;
@@ -798,6 +802,8 @@ public class TourSignupController {
return workflowResult;
}
}
// Signup and modification both mean the staff member is counted as joined for this setting.
tourParticipantRosterService.markJoinedByLedger(ledger);
return Result.success().addMsg(approvalRequired ? (update ? "修改已提交,等待审核" : "申请已提交,等待审核") : (update ? "修改成功" : "报名成功"));
}
@@ -828,6 +834,8 @@ public class TourSignupController {
if (signupTimeResult != null) {
return signupTimeResult;
}
// Reset the roster before deleting the ledger record, because the ledger still carries setting linkage here.
tourParticipantRosterService.markNotJoinedByLedger(ledger);
tourLedgerFamilyService.clear(Cnd.where(TourLedgerFamily::getLedgerId, "=", ledger.getId()));
tourLedgerDirectRelativeService.clear(Cnd.where(TourLedgerDirectRelative::getLedgerId, "=", ledger.getId()));
tourLedgerService.clear(Cnd.where(TourLedger::getId, "=", ledger.getId()));
@@ -0,0 +1,81 @@
package com.budwk.app.zhgh.dayofficework.tour.models;
import com.budwk.app.base.model.BaseModel;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.nutz.dao.entity.annotation.ColDefine;
import org.nutz.dao.entity.annotation.ColType;
import org.nutz.dao.entity.annotation.Column;
import org.nutz.dao.entity.annotation.Comment;
import org.nutz.dao.entity.annotation.Default;
import org.nutz.dao.entity.annotation.Name;
import org.nutz.dao.entity.annotation.Table;
import org.nutz.dao.entity.annotation.TableMeta;
import org.nutz.dao.interceptor.annotation.PrevInsert;
import java.io.Serializable;
/**
* Tour participant roster stores every eligible staff member for a tour setting.
* It is used for joined/not-joined statistics and links to tour_ledger only when the user has a signup record.
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Table("tour_participant_roster")
@TableMeta("{'mysql-charset':'utf8mb4'}")
@Comment("疗休养人员名单")
public class TourParticipantRoster extends BaseModel implements Serializable {
@Column
@Name
@Comment("ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
@PrevInsert(uu32 = true)
private String id;
@Column
@Comment("疗休养配置ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String settingId;
@Column
@Comment("工号")
@ColDefine(type = ColType.VARCHAR, width = 50)
private String jobNo;
@Column
@Comment("姓名")
@ColDefine(type = ColType.VARCHAR, width = 100)
private String userName;
@Column
@Comment("所在单位ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String unitId;
@Column
@Comment("所在单位")
@ColDefine(type = ColType.VARCHAR, width = 100)
private String unitName;
@Column
@Comment("所在工会ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String unionId;
@Column
@Comment("所在工会")
@ColDefine(type = ColType.VARCHAR, width = 100)
private String unionName;
@Column
@Comment("是否参加")
@ColDefine(type = ColType.BOOLEAN)
@Default("0")
private Boolean joined;
@Column
@Comment("疗休养台账ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String ledgerId;
}
@@ -0,0 +1,55 @@
package com.budwk.app.zhgh.dayofficework.tour.service;
import com.budwk.app.base.service.BaseService;
import com.budwk.app.zhgh.dayofficework.tour.models.TourLedger;
import com.budwk.app.zhgh.dayofficework.tour.models.TourParticipantRoster;
import java.util.List;
/**
* Service for maintaining the tour participant roster used by yearly joined/not-joined statistics.
*/
public interface TourParticipantRosterService extends BaseService<TourParticipantRoster> {
/**
* Synchronizes eligible staff of one tour setting into the roster table.
*
* @param settingId tour setting id whose activity group defines the eligible staff scope.
*/
void syncSettingRoster(String settingId);
/**
* Synchronizes the setting roster only when no roster rows exist for that setting.
*
* @param settingId tour setting id whose roster should be lazily initialized.
*/
void syncSettingRosterIfEmpty(String settingId);
/**
* Rebuilds selected setting rosters from eligible staff scopes and existing tour ledgers.
*
* @param settingIds selected tour setting ids.
*/
void rebuildSettingRosters(List<String> settingIds);
/**
* Clears roster rows when a tour setting is deleted.
*
* @param settingId deleted tour setting id.
*/
void clearSettingRoster(String settingId);
/**
* Marks the roster row as joined and links it with the newest tour ledger.
*
* @param ledger signup ledger created or modified successfully.
*/
void markJoinedByLedger(TourLedger ledger);
/**
* Marks the roster row as not joined and clears the linked ledger id.
*
* @param ledger signup ledger that is being cancelled or deleted.
*/
void markNotJoinedByLedger(TourLedger ledger);
}
@@ -0,0 +1,288 @@
package com.budwk.app.zhgh.dayofficework.tour.service.impl;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import com.budwk.app.base.service.impl.BaseServiceImpl;
import com.budwk.app.sys.views.View_user;
import com.budwk.app.zhgh.activity.basic.service.ActivityBasicScopeService;
import com.budwk.app.zhgh.dayofficework.tour.models.TourLedger;
import com.budwk.app.zhgh.dayofficework.tour.models.TourMatter;
import com.budwk.app.zhgh.dayofficework.tour.models.TourParticipantRoster;
import com.budwk.app.zhgh.dayofficework.tour.models.TourSetting;
import com.budwk.app.zhgh.dayofficework.tour.service.TourParticipantRosterService;
import org.nutz.dao.Chain;
import org.nutz.dao.Cnd;
import org.nutz.dao.Dao;
import org.nutz.dao.Sqls;
import org.nutz.dao.sql.Sql;
import org.nutz.aop.interceptor.ioc.TransAop;
import org.nutz.ioc.aop.Aop;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* Default implementation that keeps tour participant roster data in sync with settings and signup ledgers.
*/
@IocBean(args = {"refer:dao"})
public class TourParticipantRosterServiceImpl extends BaseServiceImpl<TourParticipantRoster> implements TourParticipantRosterService {
private static final int USER_QUERY_BATCH_SIZE = 500;
@Inject
private ActivityBasicScopeService activityBasicScopeService;
public TourParticipantRosterServiceImpl(Dao dao) {
super(dao);
}
/**
* Reads eligible users from the setting activity group and upserts roster rows without resetting joined status.
*/
@Override
public void syncSettingRoster(String settingId) {
if (StrUtil.isBlank(settingId)) {
return;
}
TourSetting setting = dao().fetch(TourSetting.class, settingId);
if (setting == null || StrUtil.isBlank(setting.getActivityGroupId())) {
return;
}
Integer groupId = parseGroupId(setting.getActivityGroupId());
if (groupId == null) {
return;
}
List<String> userIds = activityBasicScopeService.listGroupUserIds(groupId);
if (CollUtil.isEmpty(userIds)) {
return;
}
for (View_user user : queryUsers(userIds)) {
if (user == null || StrUtil.isBlank(user.getLoginname())) {
continue;
}
upsertRosterUser(settingId, user);
}
}
/**
* Keeps normal setting save lightweight: once a setting has roster rows, editing the setting will not reload users.
*/
@Override
public void syncSettingRosterIfEmpty(String settingId) {
if (StrUtil.isBlank(settingId)) {
return;
}
int rosterCount = count(Cnd.where("settingId", "=", settingId));
if (rosterCount > 0) {
return;
}
syncSettingRoster(settingId);
}
/**
* Rebuilds rosters in one transaction: clear old rows, reload eligible staff, then mark users with active ledgers as joined.
*/
@Override
@Aop(TransAop.READ_COMMITTED)
public void rebuildSettingRosters(List<String> settingIds) {
if (CollUtil.isEmpty(settingIds)) {
return;
}
for (String settingId : settingIds) {
if (StrUtil.isBlank(settingId)) {
continue;
}
clear(Cnd.where("settingId", "=", settingId));
syncSettingRoster(settingId);
replayLedgerJoinedState(settingId);
}
}
/**
* Removes roster rows that belong to a deleted setting so statistics do not keep obsolete setting data.
*/
@Override
public void clearSettingRoster(String settingId) {
if (StrUtil.isBlank(settingId)) {
return;
}
clear(Cnd.where("settingId", "=", settingId));
}
/**
* Signup success and signup modification both keep the roster row joined and point it at the effective ledger.
*/
@Override
public void markJoinedByLedger(TourLedger ledger) {
String settingId = resolveSettingId(ledger);
if (StrUtil.isBlank(settingId) || ledger == null || StrUtil.isBlank(ledger.getJobNo())) {
return;
}
TourParticipantRoster roster = fetchRoster(settingId, ledger.getJobNo());
if (roster == null) {
roster = new TourParticipantRoster();
roster.setSettingId(settingId);
roster.setJobNo(ledger.getJobNo());
roster.setJoined(true);
fillRosterFromLedger(roster, ledger);
insert(roster);
return;
}
Chain chain = Chain.make("joined", true)
.add("ledgerId", StrUtil.blankToDefault(ledger.getId(), ""))
.add("userName", StrUtil.blankToDefault(ledger.getUserName(), roster.getUserName()))
.add("unitId", StrUtil.blankToDefault(ledger.getUnitId(), roster.getUnitId()))
.add("unitName", StrUtil.blankToDefault(ledger.getUnitName(), roster.getUnitName()))
.add("unionId", StrUtil.blankToDefault(ledger.getUnionId(), roster.getUnionId()))
.add("unionName", StrUtil.blankToDefault(ledger.getUnionName(), roster.getUnionName()));
dao().update(TourParticipantRoster.class, chain, Cnd.where("id", "=", roster.getId()));
}
/**
* Cancellation only resets the current user's roster row; historical ledger data remains in tour_ledger deletion history.
*/
@Override
public void markNotJoinedByLedger(TourLedger ledger) {
String settingId = resolveSettingId(ledger);
if (StrUtil.isBlank(settingId) || ledger == null || StrUtil.isBlank(ledger.getJobNo())) {
return;
}
TourParticipantRoster roster = fetchRoster(settingId, ledger.getJobNo());
if (roster == null) {
roster = new TourParticipantRoster();
roster.setSettingId(settingId);
roster.setJobNo(ledger.getJobNo());
roster.setJoined(false);
fillRosterFromLedger(roster, ledger);
roster.setLedgerId("");
insert(roster);
return;
}
dao().update(TourParticipantRoster.class,
Chain.make("joined", false).add("ledgerId", ""),
Cnd.where("id", "=", roster.getId()));
}
/**
* Converts the setting activity group id stored as text into an integer accepted by the activity scope service.
*/
private Integer parseGroupId(String activityGroupId) {
try {
return Integer.parseInt(activityGroupId);
} catch (NumberFormatException e) {
return null;
}
}
/**
* Queries the user view in batches so large participant scopes do not generate an oversized SQL IN clause.
*/
private List<View_user> queryUsers(List<String> userIds) {
List<View_user> users = new ArrayList<>();
for (int i = 0; i < userIds.size(); i += USER_QUERY_BATCH_SIZE) {
int end = Math.min(i + USER_QUERY_BATCH_SIZE, userIds.size());
List<String> batch = userIds.subList(i, end);
users.addAll(dao().query(View_user.class, Cnd.where("id", "in", batch)));
}
return users;
}
/**
* Inserts new eligible users and refreshes basic organization data for existing rows.
*/
private void upsertRosterUser(String settingId, View_user user) {
TourParticipantRoster roster = fetchRoster(settingId, user.getLoginname());
if (roster == null) {
roster = new TourParticipantRoster();
roster.setSettingId(settingId);
roster.setJobNo(user.getLoginname());
roster.setJoined(false);
fillRosterFromUser(roster, user);
insert(roster);
return;
}
dao().update(TourParticipantRoster.class,
Chain.make("userName", StrUtil.blankToDefault(user.getUsername(), ""))
.add("unitId", StrUtil.blankToDefault(user.getUnitId(), ""))
.add("unitName", StrUtil.blankToDefault(user.getUnitName(), ""))
.add("unionId", StrUtil.blankToDefault(user.getUnionId(), ""))
.add("unionName", StrUtil.blankToDefault(user.getUnionName(), "")),
Cnd.where("id", "=", roster.getId()));
}
/**
* Replays active tour ledgers under one setting so rebuilt rosters keep the current signup state.
*/
private void replayLedgerJoinedState(String settingId) {
Sql sql = Sqls.create("""
SELECT t.*
FROM tour_ledger t
INNER JOIN tour_matter m ON m.id = t.matterId AND m.delFlag = 0
WHERE t.delFlag = 0
AND m.settingId = @settingId
AND IFNULL(t.jobNo, '') <> ''
ORDER BY t.signupTime DESC, t.createdAt DESC
""");
sql.setParam("settingId", settingId);
sql.setEntity(dao().getEntity(TourLedger.class));
sql.setCallback(Sqls.callback.entities());
dao().execute(sql);
Set<String> handledJobNos = new HashSet<>();
for (TourLedger ledger : sql.getList(TourLedger.class)) {
if (ledger == null || StrUtil.isBlank(ledger.getJobNo()) || handledJobNos.contains(ledger.getJobNo())) {
continue;
}
handledJobNos.add(ledger.getJobNo());
markJoinedByLedger(ledger);
}
}
/**
* Finds an active roster row by setting and job number, which is the logical unique key.
*/
private TourParticipantRoster fetchRoster(String settingId, String jobNo) {
return fetch(Cnd.where("settingId", "=", settingId)
.and("jobNo", "=", jobNo)
.and("delFlag", "=", false));
}
/**
* Resolves setting id through the ledger matter because tour_ledger intentionally does not store settingId.
*/
private String resolveSettingId(TourLedger ledger) {
if (ledger == null || StrUtil.isBlank(ledger.getMatterId())) {
return "";
}
TourMatter matter = dao().fetch(TourMatter.class, ledger.getMatterId());
return matter == null ? "" : matter.getSettingId();
}
/**
* Copies staff base data from the user view into the roster row.
*/
private void fillRosterFromUser(TourParticipantRoster roster, View_user user) {
roster.setUserName(StrUtil.blankToDefault(user.getUsername(), ""));
roster.setUnitId(StrUtil.blankToDefault(user.getUnitId(), ""));
roster.setUnitName(StrUtil.blankToDefault(user.getUnitName(), ""));
roster.setUnionId(StrUtil.blankToDefault(user.getUnionId(), ""));
roster.setUnionName(StrUtil.blankToDefault(user.getUnionName(), ""));
}
/**
* Copies staff and organization data from the ledger when roster sync has not created the row yet.
*/
private void fillRosterFromLedger(TourParticipantRoster roster, TourLedger ledger) {
roster.setUserName(StrUtil.blankToDefault(ledger.getUserName(), ""));
roster.setUnitId(StrUtil.blankToDefault(ledger.getUnitId(), ""));
roster.setUnitName(StrUtil.blankToDefault(ledger.getUnitName(), ""));
roster.setUnionId(StrUtil.blankToDefault(ledger.getUnionId(), ""));
roster.setUnionName(StrUtil.blankToDefault(ledger.getUnionName(), ""));
roster.setLedgerId(StrUtil.blankToDefault(ledger.getId(), ""));
}
}
@@ -0,0 +1,26 @@
CREATE TABLE IF NOT EXISTS `tour_participant_roster`
(
`id` varchar(32) NOT NULL COMMENT 'ID',
`settingId` varchar(32) NOT NULL COMMENT '疗休养配置ID',
`jobNo` varchar(50) NOT NULL COMMENT '工号',
`userName` varchar(100) DEFAULT NULL COMMENT '姓名',
`unitId` varchar(32) DEFAULT NULL COMMENT '所在单位ID',
`unitName` varchar(100) DEFAULT NULL COMMENT '所在单位',
`unionId` varchar(32) DEFAULT NULL COMMENT '所在工会ID',
`unionName` varchar(100) DEFAULT NULL COMMENT '所在工会',
`joined` tinyint(1) DEFAULT 0 COMMENT '是否参加:0否,1是',
`ledgerId` varchar(32) DEFAULT NULL COMMENT '疗休养台账ID',
`createdBy` varchar(32) DEFAULT NULL COMMENT '创建人',
`createdAt` bigint DEFAULT NULL COMMENT '创建时间',
`updatedBy` varchar(32) DEFAULT NULL COMMENT '修改人',
`updatedAt` bigint DEFAULT NULL COMMENT '修改时间',
`delFlag` tinyint(1) DEFAULT 0 COMMENT '删除标记',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_tour_participant_roster_setting_job` (`settingId`, `jobNo`),
KEY `idx_tour_participant_roster_setting` (`settingId`),
KEY `idx_tour_participant_roster_job` (`jobNo`),
KEY `idx_tour_participant_roster_joined` (`joined`),
KEY `idx_tour_participant_roster_ledger` (`ledgerId`),
KEY `idx_tour_participant_roster_union` (`unionId`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4 COMMENT ='疗休养人员名单';
@@ -0,0 +1,15 @@
-- 疗休养人员名单查询菜单。
INSERT INTO sys_menu (id, parentId, path, name, aliasName, type, href, target, icon, showit, disabled, permission, note, location, hasChildren, createdBy, createdAt, updatedBy, updatedAt, delFlag, platform, moduleId, picIcon, initialPinyinName, isRecommendApp, isRecommendService)
SELECT 'b7c63f0c73a44a9a8c4f3d3bb1a10014', p.id, CONCAT(p.path, '0013'), '疗休养人员名单', 'Tour Participant Roster', 'menu', '/platform/tour/participantRoster', 'data-pjax', '', 1, 0, 'tour.participantRoster', NULL, 13, 0, '', UNIX_TIMESTAMP(NOW()) * 1000, '', UNIX_TIMESTAMP(NOW()) * 1000, 0, 'PC', NULL, NULL, 'l', 0, 0
FROM sys_menu p
WHERE p.permission = 'tour'
AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM sys_menu WHERE permission = 'tour.participantRoster') t);
-- 默认给系统管理员授权,其它角色可以在角色管理中按需分配。
INSERT INTO sys_role_menu (roleId, menuId)
SELECT r.id, m.id
FROM sys_role r
JOIN sys_menu m ON m.permission = 'tour.participantRoster'
LEFT JOIN sys_role_menu rm ON rm.roleId = r.id AND rm.menuId = m.id
WHERE r.code = 'SYSADMIN'
AND rm.roleId IS NULL;
@@ -0,0 +1,67 @@
const tourTableHeightMixins = {
data() {
return {
tourTableHeight: Math.max(120, window.innerHeight - 378),
tourTableResizeTimer: null,
tourTableHeightReady: false
}
},
methods: {
updateTourTableHeight() {
this.$nextTick(() => {
const table = this.$refs.tourMainTable || this.$refs.tableRef || this.$refs.table
const tableEl = table && table.$el ? table.$el : null
if (!tableEl) {
return
}
// 根据当前屏幕、表格位置、分页区域动态计算主列表高度,避免整页被数据行撑出滚动条。
const mainContent = document.querySelector(".sub-app-container-main-content")
const viewportBottom = mainContent ? mainContent.getBoundingClientRect().bottom : window.innerHeight
const tableTop = tableEl.getBoundingClientRect().top
const pagination = this.$el.querySelector(".el-pagination-container")
const paginationHeight = pagination ? pagination.getBoundingClientRect().height : 46
const reserveHeight = Number(this.tourTableReserveHeight || 42)
const minHeight = Number(this.tourTableMinHeight || 120)
const height = Math.floor(viewportBottom - tableTop - paginationHeight - reserveHeight)
this.tourTableHeight = height > minHeight ? height : minHeight
this.tourTableHeightReady = true
// Element 表格高度变化后需要重新布局,否则固定列和表头可能错位。
this.$nextTick(() => {
if (table && table.doLayout) {
table.doLayout()
}
})
})
},
handleTourTableResize() {
if (this.tourTableResizeTimer) {
clearTimeout(this.tourTableResizeTimer)
}
this.tourTableResizeTimer = setTimeout(() => {
this.updateTourTableHeight()
this.tourTableResizeTimer = null
}, 120)
}
},
mounted() {
this.updateTourTableHeight()
window.addEventListener("resize", this.handleTourTableResize)
},
beforeDestroy() {
if (this.tourTableResizeTimer) {
clearTimeout(this.tourTableResizeTimer)
this.tourTableResizeTimer = null
}
window.removeEventListener("resize", this.handleTourTableResize)
},
watch: {
tableData() {
this.updateTourTableHeight()
},
"pageForm.pageSize"() {
this.updateTourTableHeight()
}
}
}
@@ -87,6 +87,7 @@
<script src="${base!}/assets/platform/js/util/coordinateUtil.js"></script>
<script src="${base!}/assets/platform/js/main.js"></script>
<script src="${base!}/assets/platform/js/mixin/initTableMixins.js"></script>
<script src="${base!}/assets/platform/js/mixin/tourTableHeightMixins.js?v=2026072103"></script>
<script src="${base!}/assets/platform/js/mixin/styleMixin.js"></script>
<script src="${base!}/assets/platform/js/mixin/flowMixins.js"></script>
<script src="${base!}/assets/platform/js/tool/businessTool.js"></script>
@@ -1,4 +1,4 @@
<!--#
<!--#
layout("/layouts/platform.html"){
#-->
@@ -51,7 +51,9 @@ layout("/layouts/platform.html"){
border
:default-sort="{prop: 'lineName', order: 'ascending'}"
:row-class-name="tableRowClassName"
@sort-change="pageOrder">
@sort-change="pageOrder"
ref="tourMainTable"
:height="tourTableHeight">
<el-table-column label="序号" type="index" :index="indexMethod" width="80" align="center" header-align="center"></el-table-column>
<el-table-column label="线路名称" prop="lineName" min-width="220" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="出行时段" prop="travelPeriod" width="260" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column>
@@ -194,7 +196,7 @@ layout("/layouts/platform.html"){
<script nonce="${cspNonce!}">
new Vue({
el: "#app",
mixins: [initTableMixins],
mixins: [initTableMixins, tourTableHeightMixins],
data() {
const currentYear = moment().format("YYYY")
return {
@@ -99,7 +99,8 @@ layout("/layouts/platform.html"){
row-key="id"
:row-class-name="tableRowClassName"
@selection-change="handleSelectionChange"
@sort-change="pageOrder">
@sort-change="pageOrder"
:height="tourTableHeight">
<el-table-column type="selection" width="55" align="center" header-align="center"></el-table-column>
<el-table-column label="序号" type="index" :index="indexMethod" width="80" align="center" header-align="center"></el-table-column>
<el-table-column label="工号" prop="jobNo" width="130" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column>
@@ -348,7 +349,7 @@ layout("/layouts/platform.html"){
<script nonce="${cspNonce!}">
new Vue({
el: "#app",
mixins: [initTableMixins],
mixins: [initTableMixins, tourTableHeightMixins],
dicts: ["PROCESS_TASK_SUBMIT_TYPE"],
data() {
const currentYear = moment().format("YYYY")
@@ -84,7 +84,9 @@ layout("/layouts/platform.html"){
border
:default-sort="{prop: 'lineCode', order: 'ascending'}"
:row-class-name="tableRowClassName"
@sort-change="pageOrder">
@sort-change="pageOrder"
ref="tourMainTable"
:height="tourTableHeight">
<el-table-column label="序号" type="index" :index="indexMethod" width="80" align="center" header-align="center"></el-table-column>
<el-table-column label="年度" prop="year" width="110" sortable="custom" align="center" header-align="center"></el-table-column>
<el-table-column label="事项名称" prop="matterName" min-width="240" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column>
@@ -279,7 +281,7 @@ layout("/layouts/platform.html"){
new Vue({
el: "#app",
store,
mixins: [initTableMixins],
mixins: [initTableMixins, tourTableHeightMixins],
data() {
const currentYear = moment().format("YYYY")
const validateUnion = (rule, value, callback) => {
@@ -43,7 +43,9 @@ layout("/layouts/platform.html"){
:data="tableData"
:size="tableSize"
border
@sort-change="pageOrder">
@sort-change="pageOrder"
ref="tourMainTable"
:height="tourTableHeight">
<el-table-column label="序号" type="index" :index="indexMethod" width="80" align="center" header-align="center"></el-table-column>
<el-table-column label="工号" prop="jobNo" width="130" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="姓名" prop="userName" width="120" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column>
@@ -730,7 +732,7 @@ layout("/layouts/platform.html"){
<script nonce="${cspNonce!}">
new Vue({
el: "#app",
mixins: [initTableMixins],
mixins: [initTableMixins, tourTableHeightMixins],
dicts: ["PROCESS_TASK_SUBMIT_TYPE"],
data() {
return {
@@ -0,0 +1,488 @@
<!--#
layout("/layouts/platform.html"){
#-->
<div id="app" v-cloak>
<guava ref="guava">
<el-card shadow="never">
<search :is-search-button="false">
<search-item label="年度">
<div class="tour-year-range">
<el-date-picker
v-model="pageForm.startYear"
type="year"
value-format="yyyy"
placeholder="开始年度"
class="tour-year-picker">
</el-date-picker>
<span class="tour-year-separator">-</span>
<el-date-picker
v-model="pageForm.endYear"
type="year"
value-format="yyyy"
placeholder="结束年度"
class="tour-year-picker">
</el-date-picker>
</div>
</search-item>
<search-item label="疗休养配置">
<el-select
v-model="pageForm.settingId"
clearable
filterable
placeholder="请选择疗休养配置"
style="width: 100%"
@change="settingChange">
<el-option
v-for="item in settingOptions"
:key="item.id"
:label="settingOptionLabel(item)"
:value="item.id">
</el-option>
</el-select>
</search-item>
<search-item label="线路">
<el-select
v-model="pageForm.matterId"
clearable
filterable
placeholder="请选择线路"
style="width: 100%"
@visible-change="lineVisibleChange">
<el-option
v-for="item in lineOptions"
:key="item.matterId"
:label="item.label"
:value="item.matterId">
<span>{{ item.label }}</span>
<span class="tour-line-option">{{ item.lineName }}</span>
</el-option>
</el-select>
</search-item>
<search-item label="姓名/工号">
<el-input
v-model="pageForm.keyword"
clearable
placeholder="请输入姓名或工号"
style="width: 100%"
@keyup.enter.native="doSearch">
</el-input>
</search-item>
<search-item label="所在工会">
<el-select v-model="pageForm.unionId" clearable filterable placeholder="请选择所在工会" style="width: 100%">
<el-option v-for="item in unionOptions" :key="item.id" :label="item.name" :value="item.id"></el-option>
</el-select>
</search-item>
<search-item label="线路类型">
<dict-select
v-model="pageForm.lineType"
code="lineType"
option_value="name"
placeholder="请选择线路类型">
</dict-select>
</search-item>
<search-item label="领办工会">
<el-select v-model="pageForm.undertakeUnionId" clearable filterable placeholder="请选择领办工会" style="width: 100%">
<el-option v-for="item in unionOptions" :key="item.id" :label="item.name" :value="item.id"></el-option>
</el-select>
</search-item>
<div class="search-query">
<el-button size="medium" type="primary" icon="el-icon-search" @click="doSearch">搜索</el-button>
<el-button size="medium" @click="resetSearch">重置</el-button>
</div>
</search>
</el-card>
<el-card class="mt10" shadow="never">
<div class="tour-roster-toolbar">
<table-tool :app="this" label="疗休养人员名单"></table-tool>
<el-button size="medium" type="primary" icon="el-icon-refresh" @click="openRebuildDialog">重新生成名单</el-button>
</div>
<el-table
ref="tableRef"
v-loading="tableLoading"
:data="tableData"
:size="tableSize"
border
row-key="id"
@sort-change="pageOrder"
:height="tourTableHeight">
<el-table-column label="序号" type="index" :index="indexMethod" width="80" align="center" header-align="center"></el-table-column>
<el-table-column label="工号" prop="jobNo" width="130" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="姓名" prop="userName" width="120" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="所在单位" prop="unitName" min-width="180" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="所在工会" prop="unionName" min-width="160" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="是否参加" prop="joined" width="110" sortable="custom" align="center" header-align="center">
<template slot-scope="{row}">
<el-tag size="mini" :type="toBoolean(row.joined) ? 'success' : 'info'">{{ toBoolean(row.joined) ? '已参加' : '未参加' }}</el-tag>
</template>
</el-table-column>
<el-table-column label="线路类型" prop="lineType" width="130" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="线路" prop="lineName" min-width="220" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="疗休养事项" prop="matterName" min-width="180" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="出行时段" prop="travelPeriod" min-width="220" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="成团信息" prop="groupName" width="160" align="center" header-align="center" show-overflow-tooltip>
<template slot-scope="{row}">
<span>{{ formatGroupInfo(row) }}</span>
</template>
</el-table-column>
<el-table-column label="领办工会" prop="undertakeUnionName" min-width="160" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="报名时间" prop="signupTime" width="170" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column>
</el-table>
<!--#include("/layouts/pagination.html"){}#-->
</el-card>
</guava>
<el-dialog
title="重新生成名单"
:visible.sync="rebuildDialogVisible"
:close-on-click-modal="false"
width="620px">
<el-form :model="rebuildForm" label-width="110px" size="medium">
<el-form-item label="开始年度" required>
<el-date-picker
v-model="rebuildForm.startYear"
type="year"
value-format="yyyy"
placeholder="请选择开始年度"
style="width: 100%"
@change="rebuildYearChange">
</el-date-picker>
</el-form-item>
<el-form-item label="结束年度" required>
<el-date-picker
v-model="rebuildForm.endYear"
type="year"
value-format="yyyy"
placeholder="请选择结束年度"
style="width: 100%"
@change="rebuildYearChange">
</el-date-picker>
</el-form-item>
<el-form-item label="疗休养配置" required>
<el-select
v-model="rebuildForm.settingIds"
multiple
clearable
filterable
placeholder="请选择疗休养配置"
style="width: 100%">
<el-option
v-for="item in rebuildSettingOptions"
:key="item.id"
:label="settingOptionLabel(item)"
:value="item.id">
</el-option>
</el-select>
</el-form-item>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="rebuildDialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitRebuildRoster">确认</el-button>
</span>
</el-dialog>
</div>
<style>
.tour-roster-toolbar {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 10px;
}
.tour-year-range {
display: flex;
align-items: center;
width: 100%;
}
.tour-year-picker {
width: 112px;
}
.tour-year-separator {
padding: 0 8px;
color: #606266;
}
.tour-line-option {
float: right;
color: #8492a6;
font-size: 12px;
max-width: 45%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
</style>
<script nonce="${cspNonce!}">
new Vue({
el: "#app",
mixins: [initTableMixins, tourTableHeightMixins],
data() {
const currentYear = moment().format("YYYY")
return {
settingOptions: [],
rebuildSettingOptions: [],
lineOptions: [],
unionOptions: [],
optionTimer: null,
rebuildDialogVisible: false,
rebuildForm: {
startYear: currentYear,
endYear: currentYear,
settingIds: []
},
pageForm: {
pageNumber: 1,
pageSize: 10,
totalCount: 0,
pageOrderName: "year",
pageOrderBy: "descending",
startYear: currentYear,
endYear: currentYear,
settingId: "",
matterId: "",
keyword: "",
unitName: "",
unionId: "",
joined: "",
lineType: "",
groupCode: "",
undertakeUnionId: ""
}
}
},
methods: {
toBoolean(value) {
return value === true || value === 1 || value === "1" || value === "true" || value === "TRUE"
},
settingOptionLabel(item) {
if (!item) {
return ""
}
return (item.configName || "") + "【" + (item.year || "") + "】"
},
formatGroupInfo(row) {
if (!row || !row.groupCode) {
return ""
}
return row.groupName ? row.groupCode + " " + row.groupName : row.groupCode
},
checkYearRange() {
if (!this.pageForm.startYear || !this.pageForm.endYear) {
this.$message.warning("年度范围不能为空")
return false
}
if (Number(this.pageForm.startYear) > Number(this.pageForm.endYear)) {
this.$message.warning("开始年度不能大于结束年度")
return false
}
return true
},
optionParams() {
return {
startYear: this.pageForm.startYear,
endYear: this.pageForm.endYear,
settingId: this.pageForm.settingId
}
},
loadSettingOptions() {
if (!this.checkYearRange()) {
return
}
this.$axios.post(loc() + "/settingOptions", {
startYear: this.pageForm.startYear,
endYear: this.pageForm.endYear
}).then((res) => {
if (res.code === 0) {
this.settingOptions = res.data || []
if (this.pageForm.settingId && !this.settingOptions.some(item => item.id === this.pageForm.settingId)) {
this.pageForm.settingId = ""
}
} else {
this.$message.warning(res.msg || "疗休养配置加载失败")
}
})
},
loadLineOptions() {
if (!this.checkYearRange()) {
return
}
this.$axios.post(loc() + "/lineOptions", this.optionParams()).then((res) => {
if (res.code === 0) {
this.lineOptions = res.data || []
if (this.pageForm.matterId && !this.lineOptions.some(item => item.matterId === this.pageForm.matterId)) {
this.pageForm.matterId = ""
}
} else {
this.$message.warning(res.msg || "线路加载失败")
}
})
},
loadUnionOptions() {
if (!this.checkYearRange()) {
return
}
this.$axios.post(loc() + "/unionOptions", {
startYear: this.pageForm.startYear,
endYear: this.pageForm.endYear
}).then((res) => {
if (res.code === 0) {
this.unionOptions = res.data || []
if (this.pageForm.unionId && !this.unionOptions.some(item => item.id === this.pageForm.unionId)) {
this.pageForm.unionId = ""
}
if (this.pageForm.undertakeUnionId && !this.unionOptions.some(item => item.id === this.pageForm.undertakeUnionId)) {
this.pageForm.undertakeUnionId = ""
}
}
})
},
loadOptions() {
this.loadSettingOptions()
this.loadLineOptions()
this.loadUnionOptions()
},
loadRebuildSettingOptions() {
if (!this.checkRebuildYearRange()) {
this.rebuildSettingOptions = []
return
}
this.$axios.post(loc() + "/settingOptions", {
startYear: this.rebuildForm.startYear,
endYear: this.rebuildForm.endYear
}).then((res) => {
if (res.code === 0) {
this.rebuildSettingOptions = res.data || []
} else {
this.rebuildSettingOptions = []
this.$message.warning(res.msg || "疗休养配置加载失败")
}
})
},
checkRebuildYearRange() {
if (!this.rebuildForm.startYear || !this.rebuildForm.endYear) {
this.$message.warning("年度范围不能为空")
return false
}
if (Number(this.rebuildForm.startYear) > Number(this.rebuildForm.endYear)) {
this.$message.warning("开始年度不能大于结束年度")
return false
}
return true
},
openRebuildDialog() {
const currentYear = moment().format("YYYY")
this.rebuildForm.startYear = currentYear
this.rebuildForm.endYear = currentYear
this.rebuildForm.settingIds = []
this.rebuildDialogVisible = true
this.loadRebuildSettingOptions()
},
rebuildYearChange() {
this.rebuildForm.settingIds = []
this.loadRebuildSettingOptions()
},
submitRebuildRoster() {
if (!this.checkRebuildYearRange()) {
return
}
if (!this.rebuildForm.settingIds.length) {
this.$message.warning("请选择需要重新生成的疗休养配置")
return
}
this.$confirm("重新生成后将删除所选配置下已有人员名单,并根据可参与人员范围和现有疗休养台账重新生成,是否继续?", "提示", {
confirmButtonText: "确认生成",
cancelButtonText: "取消",
type: "warning"
}).then(() => {
this.$axios.post(loc() + "/rebuildRoster", {
startYear: this.rebuildForm.startYear,
endYear: this.rebuildForm.endYear,
settingIds: JSON.stringify(this.rebuildForm.settingIds)
}).then((res) => {
if (res.code === 0) {
this.$message.success(res.msg || "人员名单重新生成成功")
this.rebuildDialogVisible = false
this.pageData()
this.loadOptions()
} else {
this.$message.warning(res.msg || "人员名单重新生成失败")
}
})
})
},
scheduleLoadOptions() {
if (this.optionTimer) {
clearTimeout(this.optionTimer)
}
this.optionTimer = setTimeout(() => {
this.loadOptions()
this.optionTimer = null
}, 80)
},
lineVisibleChange(visible) {
if (!visible) {
return
}
if (!this.checkYearRange()) {
return
}
if (!this.lineOptions.length) {
this.$message.warning(this.pageForm.settingId ? "当前配置下暂无可选线路" : "当前年度范围下暂无可选线路")
}
},
settingChange() {
this.pageForm.matterId = ""
this.loadLineOptions()
},
doSearch() {
if (!this.checkYearRange()) {
return
}
this.tableKey = new Date().getTime()
this.pageForm.pageNumber = 1
this.pageData()
this.scheduleLoadOptions()
},
resetSearch() {
const currentYear = moment().format("YYYY")
this.pageForm.startYear = currentYear
this.pageForm.endYear = currentYear
this.pageForm.settingId = ""
this.pageForm.matterId = ""
this.pageForm.keyword = ""
this.pageForm.unitName = ""
this.pageForm.unionId = ""
this.pageForm.joined = ""
this.pageForm.lineType = ""
this.pageForm.groupCode = ""
this.pageForm.undertakeUnionId = ""
this.doSearch()
}
},
mounted() {
this.loadOptions()
this.pageData()
},
watch: {
"pageForm.startYear"() {
this.pageForm.settingId = ""
this.pageForm.matterId = ""
this.scheduleLoadOptions()
},
"pageForm.endYear"() {
this.pageForm.settingId = ""
this.pageForm.matterId = ""
this.scheduleLoadOptions()
}
}
})
</script>
<!--#
}
#-->
@@ -57,7 +57,9 @@ layout("/layouts/platform.html"){
border
:default-sort="{prop: 'lineCode', order: 'ascending'}"
:row-class-name="tableRowClassName"
@sort-change="pageOrder">
@sort-change="pageOrder"
ref="tourMainTable"
:height="tourTableHeight">
<el-table-column label="序号" type="index" :index="indexMethod" width="80" align="center" header-align="center"></el-table-column>
<el-table-column label="创建年度" prop="year" width="120" sortable="custom" align="center" header-align="center"></el-table-column>
<el-table-column label="线路名称" prop="lineName" min-width="220" align="center" header-align="center" show-overflow-tooltip></el-table-column>
@@ -366,7 +368,7 @@ layout("/layouts/platform.html"){
new Vue({
el: "#app",
store,
mixins: [initTableMixins],
mixins: [initTableMixins, tourTableHeightMixins],
data() {
const currentYear = moment().format("YYYY")
const validateMatterUnion = (rule, value, callback) => {
@@ -72,7 +72,9 @@ layout("/layouts/platform.html"){
:data="tableData"
:size="tableSize"
border
@sort-change="pageOrder">
@sort-change="pageOrder"
ref="tourMainTable"
:height="tourTableHeight">
<el-table-column label="序号" type="index" :index="indexMethod" width="80" align="center" header-align="center"></el-table-column>
<el-table-column label="工号" prop="jobNo" width="130" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="姓名" prop="userName" width="120" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column>
@@ -413,7 +415,7 @@ layout("/layouts/platform.html"){
new Vue({
el: "#app",
mixins: [initTableMixins],
mixins: [initTableMixins, tourTableHeightMixins],
data() {
const currentYear = moment().format("YYYY")
return {
@@ -48,7 +48,7 @@ layout("/layouts/platform.html"){
row-key="id"
v-loading="tableLoading"
@sort-change="pageOrder"
>
:height="tourTableHeight">
<el-table-column label="序号" type="index" :index="indexMethod" width="70" align="center" header-align="center"></el-table-column>
<el-table-column label="年度" prop="year" width="100" align="center" header-align="center"></el-table-column>
<el-table-column label="疗休养配置名称" prop="configName" min-width="260" align="center" header-align="center" show-overflow-tooltip></el-table-column>
@@ -311,7 +311,7 @@ layout("/layouts/platform.html"){
<script nonce="${cspNonce!}">
new Vue({
el: "#app",
mixins: [initTableMixins],
mixins: [initTableMixins, tourTableHeightMixins],
data() {
const checkSignupTime = (rule, value, callback) => {
if (this.formData.signupStartTime && this.formData.signupEndTime
@@ -369,11 +369,12 @@ layout("/layouts/platform.html"){
"drawer-user-scope": httpVueLoader("/components/module/activity/DrawerUserScope.vue")
},
methods: {
async getActivityGroup() {
const {data} = await this.$axios.post("/platform/activity/basic/scope/getActivityUserScopeGroup")
this.activityGroupList = (data || []).map((item) => {
return Object.assign({}, item, {
groupId: item.groupId === null || item.groupId === undefined ? "" : String(item.groupId)
getActivityGroup() {
return this.$axios.post("/platform/activity/basic/scope/getActivityUserScopeGroup").then((res) => {
this.activityGroupList = ((res && res.data) || []).map((item) => {
return Object.assign({}, item, {
groupId: item.groupId === null || item.groupId === undefined ? "" : String(item.groupId)
})
})
})
},
@@ -383,11 +384,11 @@ layout("/layouts/platform.html"){
this.$refs.drawerUserScope.userScopeDialog = true
}
},
async handleActivityGroupChange() {
handleActivityGroupChange() {
if (this.formData.activityGroupId !== null && this.formData.activityGroupId !== undefined) {
this.formData.activityGroupId = String(this.formData.activityGroupId)
}
await this.getActivityGroup()
this.getActivityGroup()
},
resetSearch() {
this.pageForm.year = ""
@@ -54,7 +54,9 @@ layout("/layouts/platform.html"){
border
:default-sort="{prop: 'lineCode', order: 'ascending'}"
:row-class-name="tableRowClassName"
@sort-change="pageOrder">
@sort-change="pageOrder"
ref="tourMainTable"
:height="tourTableHeight">
<el-table-column label="序号" type="index" :index="indexMethod" width="80" align="center" header-align="center"></el-table-column>
<el-table-column label="线路名称" prop="lineName" min-width="220" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="出行时段" prop="travelPeriod" width="240" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column>
@@ -631,7 +633,7 @@ layout("/layouts/platform.html"){
<script nonce="${cspNonce!}">
new Vue({
el: "#app",
mixins: [initTableMixins],
mixins: [initTableMixins, tourTableHeightMixins],
data() {
const currentYear = moment().format("YYYY")
return {
@@ -63,7 +63,9 @@ layout("/layouts/platform.html"){
:size="tableSize"
border
:default-sort="{prop: 'agencyCode', order: 'ascending'}"
@sort-change="pageOrder">
@sort-change="pageOrder"
ref="tourMainTable"
:height="tourTableHeight">
<el-table-column label="序号" type="index" :index="indexMethod" width="80" align="center" header-align="center"></el-table-column>
<el-table-column label="年度" prop="year" width="110" sortable="custom" align="center" header-align="center"></el-table-column>
<el-table-column label="旅行社名称" prop="agencyName" min-width="220" align="center" header-align="center" show-overflow-tooltip></el-table-column>
@@ -179,7 +181,7 @@ layout("/layouts/platform.html"){
<script nonce="${cspNonce!}">
new Vue({
el: "#app",
mixins: [initTableMixins],
mixins: [initTableMixins, tourTableHeightMixins],
data() {
const currentYear = moment().format("YYYY")
const validateMobile = (rule, value, callback) => {
@@ -1,4 +1,4 @@
<!--#
<!--#
layout("/layouts/platform.html"){
#-->
@@ -56,7 +56,9 @@ layout("/layouts/platform.html"){
border
:default-sort="{prop: 'lineName', order: 'ascending'}"
:row-class-name="tableRowClassName"
@sort-change="pageOrder">
@sort-change="pageOrder"
ref="tourMainTable"
:height="tourTableHeight">
<el-table-column label="序号" type="index" :index="indexMethod" width="80" align="center" header-align="center"></el-table-column>
<el-table-column label="线路名称" prop="lineName" min-width="220" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="出行时段" prop="travelPeriod" width="260" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column>
@@ -171,7 +173,7 @@ layout("/layouts/platform.html"){
<script nonce="${cspNonce!}">
new Vue({
el: "#app",
mixins: [initTableMixins],
mixins: [initTableMixins, tourTableHeightMixins],
data() {
const currentYear = moment().format("YYYY")
return {
@@ -72,7 +72,9 @@ layout("/layouts/platform.html"){
:data="tableData"
:size="tableSize"
border
@sort-change="pageOrder">
@sort-change="pageOrder"
ref="tourMainTable"
:height="tourTableHeight">
<el-table-column label="序号" type="index" :index="indexMethod" width="80" align="center" header-align="center"></el-table-column>
<el-table-column label="工号" prop="jobNo" width="130" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="姓名" prop="userName" width="120" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column>
@@ -413,7 +415,7 @@ layout("/layouts/platform.html"){
new Vue({
el: "#app",
mixins: [initTableMixins],
mixins: [initTableMixins, tourTableHeightMixins],
data() {
const currentYear = moment().format("YYYY")
return {
@@ -102,7 +102,9 @@ layout("/layouts/platform.html"){
:data="tableData"
:size="tableSize"
border
@sort-change="pageOrder">
@sort-change="pageOrder"
ref="tourMainTable"
:height="tourTableHeight">
<el-table-column label="序号" type="index" :index="indexMethod" width="80" align="center" header-align="center"></el-table-column>
<el-table-column label="工号" prop="jobNo" width="130" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="姓名" prop="userName" width="120" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column>
@@ -310,7 +312,7 @@ layout("/layouts/platform.html"){
<script nonce="${cspNonce!}">
new Vue({
el: "#app",
mixins: [initTableMixins],
mixins: [initTableMixins, tourTableHeightMixins],
dicts: ["PROCESS_TASK_SUBMIT_TYPE"],
data() {
const currentYear = moment().format("YYYY")