南大三十年教职工疗休养

This commit is contained in:
2026-06-25 14:24:29 +08:00
parent 3d0bcabc65
commit 521fc4446b
26 changed files with 2219 additions and 99 deletions
@@ -109,6 +109,24 @@ public class ThirtyTeachTourBranchUserAssignmentController {
return Result.success(tourUserAssignmentService.branchQuotaInfo(settingId));
}
/**
* 查询当前配置下已启用的出行时间段,供人员分配弹窗按钮组使用。
*/
@At
@SaCheckPermission("thirtyTeachTour.branchUserAssignment.assign")
public Result travelPeriodOptions(String settingId) {
return Result.success(tourUserAssignmentService.listBranchEnabledTravelPeriods(settingId));
}
/**
* 查询当前分工会在指定出行时间段内的正式名额,正式人员分配按该口径控制。
*/
@At
@SaCheckPermission("thirtyTeachTour.branchUserAssignment.assign")
public Result periodQuotaInfo(String settingId, String periodId) {
return Result.success(tourUserAssignmentService.branchPeriodQuotaInfo(settingId, periodId));
}
/**
* 保存分工会人员分配,保存时数据来源固定为 BRANCH_UNION。
*/
@@ -116,7 +134,22 @@ public class ThirtyTeachTourBranchUserAssignmentController {
@Aop(TransAop.READ_COMMITTED)
@SaCheckPermission("thirtyTeachTour.branchUserAssignment.assign")
@SLog(type = "tour", tag = "疗休养分工会人员分配", msg = "保存分工会人员分配")
public Result doAssign(String settingId, String matterId, String personType, @Param("userIds") String userIds) {
public Result doAssign(String settingId, String matterId, String periodId, String personType,
@Param("userIds") String userIds, String photoFiles,
@Param("assignItems") String assignItems) {
if (StrUtil.isNotBlank(assignItems)) {
List<NutMap> parsedItems;
try {
parsedItems = parseAssignItems(assignItems);
} catch (Exception e) {
return Result.error("人员分配明细参数错误");
}
try {
return Result.success(tourUserAssignmentService.assignBranchUsers(settingId, matterId, periodId, personType, parsedItems));
} catch (IllegalArgumentException e) {
return Result.error(e.getMessage());
}
}
List<String> parsedUserIds;
try {
parsedUserIds = parseUserIds(userIds);
@@ -124,7 +157,8 @@ public class ThirtyTeachTourBranchUserAssignmentController {
return Result.error("人员参数错误");
}
try {
return Result.success(tourUserAssignmentService.assignBranchUsers(settingId, matterId, personType, parsedUserIds));
return Result.success(tourUserAssignmentService.assignBranchUsers(settingId, matterId, periodId, personType,
buildAssignItems(parsedUserIds, photoFiles)));
} catch (IllegalArgumentException e) {
return Result.error(e.getMessage());
}
@@ -152,9 +186,25 @@ public class ThirtyTeachTourBranchUserAssignmentController {
@Aop(TransAop.READ_COMMITTED)
@SaCheckPermission("thirtyTeachTour.branchUserAssignment.selectMatter")
@SLog(type = "tour", tag = "疗休养分工会人员分配", msg = "选择分工会分配事项")
public Result selectMatter(String id, String matterId) {
public Result selectMatter(String id, String matterId, String photoFiles) {
try {
return Result.success(tourUserAssignmentService.selectCurrentBranchAssignmentMatter(id, matterId));
return Result.success(tourUserAssignmentService.selectCurrentBranchAssignmentMatter(id, matterId, photoFiles));
} catch (IllegalArgumentException e) {
return Result.error(e.getMessage());
}
}
/**
* 更新当前分工会人员分配记录的图片材料,已生成台账时同步回写台账图片。
*/
@At
@Aop(TransAop.READ_COMMITTED)
@SaCheckPermission("thirtyTeachTour.branchUserAssignment.assign")
@SLog(type = "tour", tag = "疗休养分工会人员分配", msg = "上传分工会人员分配图片")
public Result updatePhotoFiles(String id, String photoFiles) {
try {
tourUserAssignmentService.updateCurrentBranchAssignmentPhotoFiles(id, photoFiles);
return Result.success();
} catch (IllegalArgumentException e) {
return Result.error(e.getMessage());
}
@@ -215,6 +265,29 @@ public class ThirtyTeachTourBranchUserAssignmentController {
return Lang.isEmpty(list) ? Collections.emptyList() : list;
}
/**
* 解析前端批量人员分配明细,支持每个人携带独立图片材料。
*/
private List<NutMap> parseAssignItems(String assignItems) {
if (StrUtil.isBlank(assignItems)) {
return Collections.emptyList();
}
List<NutMap> list = Json.fromJsonAsList(NutMap.class, assignItems);
return Lang.isEmpty(list) ? Collections.emptyList() : list;
}
/**
* 兼容旧的 userIds 入参,把公共图片材料转换成按人员明细的统一 service 入参。
*/
private List<NutMap> buildAssignItems(List<String> userIds, String photoFiles) {
if (Lang.isEmpty(userIds)) {
return Collections.emptyList();
}
List<NutMap> list = new ArrayList<>();
userIds.forEach(userId -> list.add(NutMap.NEW().addv("userId", userId).addv("photoFiles", photoFiles)));
return list;
}
/**
* 定义人员分配导出字段,保持与页面列表核心字段一致并补充人员基础信息。
*/
@@ -512,7 +512,8 @@ public class ThirtyTeachTourMySignupController {
tourLedgerService.updateIgnoreNull(ledger);
// 报名台账更新后,仅回填已存在的人员分配记录事项信息,不新增记录、不改变分配来源。
tourUserAssignmentService.backfillExistingAssignmentAfterSignup(matter.getSettingId(), matter.getId(), SecurityUtil.getUserId(), ledger.getBoardingPlace());
tourUserAssignmentService.backfillExistingAssignmentAfterSignup(matter.getSettingId(), matter.getId(), SecurityUtil.getUserId(),
ledger.getBoardingPlace(), ledger.getPhotoFiles());
tourLedgerFamilyService.clear(Cnd.where(ThirtyTeachTourLedgerFamily::getLedgerId, "=", ledger.getId()));
tourLedgerDirectRelativeService.clear(Cnd.where(ThirtyTeachTourLedgerDirectRelative::getLedgerId, "=", ledger.getId()));
if (Lang.isNotEmpty(familyList)) {
@@ -100,8 +100,10 @@ public class ThirtyTeachTourSchoolUserAssignmentController {
*/
@At
@SaCheckPermission("thirtyTeachTour.schoolUserAssignment")
public Result candidatePageData(PageForm pageForm, String settingId, String unionId, String keyword, @Param("userIds") String userIds) {
return Result.success(tourUserAssignmentService.schoolCandidatePage(pageForm, settingId, unionId, keyword, parseUserIds(userIds)));
public Result candidatePageData(PageForm pageForm, String settingId, String unionId, String keyword, @Param("userIds") String userIds,
Boolean summerJoined, Boolean lastThirtyJoined) {
return Result.success(tourUserAssignmentService.schoolCandidatePage(pageForm, settingId, unionId, keyword,
parseUserIds(userIds), summerJoined, lastThirtyJoined));
}
/**
@@ -138,7 +140,7 @@ public class ThirtyTeachTourSchoolUserAssignmentController {
@Aop(TransAop.READ_COMMITTED)
@SaCheckPermission("thirtyTeachTour.schoolUserAssignment")
@SLog(type = "tour", tag = "疗休养校工会人员分配", msg = "保存校工会人员分配")
public Result doAssign(String settingId, String matterId, String personType, @Param("userIds") String userIds) {
public Result doAssign(String settingId, String matterId, String personType, @Param("userIds") String userIds, String photoFiles) {
List<String> parsedUserIds;
try {
parsedUserIds = parseUserIds(userIds);
@@ -146,7 +148,7 @@ public class ThirtyTeachTourSchoolUserAssignmentController {
return Result.error("人员参数错误");
}
try {
return Result.success(tourUserAssignmentService.assignSchoolUsers(settingId, matterId, personType, parsedUserIds));
return Result.success(tourUserAssignmentService.assignSchoolUsers(settingId, matterId, personType, parsedUserIds, photoFiles));
} catch (IllegalArgumentException e) {
return Result.error(e.getMessage());
}
@@ -180,9 +182,25 @@ public class ThirtyTeachTourSchoolUserAssignmentController {
@Aop(TransAop.READ_COMMITTED)
@SaCheckPermission("thirtyTeachTour.schoolUserAssignment")
@SLog(type = "tour", tag = "疗休养校工会人员分配", msg = "选择校工会分配事项")
public Result selectMatter(String id, String matterId) {
public Result selectMatter(String id, String matterId, String photoFiles) {
try {
return Result.success(tourUserAssignmentService.selectSchoolAssignmentMatter(id, matterId));
return Result.success(tourUserAssignmentService.selectSchoolAssignmentMatter(id, matterId, photoFiles));
} catch (IllegalArgumentException e) {
return Result.error(e.getMessage());
}
}
/**
* 更新校工会人员分配记录的图片材料,已生成台账时同步回写台账图片。
*/
@At
@Aop(TransAop.READ_COMMITTED)
@SaCheckPermission("thirtyTeachTour.schoolUserAssignment")
@SLog(type = "tour", tag = "疗休养校工会人员分配", msg = "上传校工会人员分配图片")
public Result updatePhotoFiles(String id, String photoFiles) {
try {
tourUserAssignmentService.updateSchoolAssignmentPhotoFiles(id, photoFiles);
return Result.success();
} catch (IllegalArgumentException e) {
return Result.error(e.getMessage());
}
@@ -8,6 +8,7 @@ import com.budwk.app.base.param.PageForm;
import com.budwk.app.base.result.Result;
import com.budwk.app.zhgh.dayofficework.thirtyTeachTour.models.ThirtyTeachTourSetting;
import com.budwk.app.zhgh.dayofficework.thirtyTeachTour.models.ThirtyTeachTourSettingLot;
import com.budwk.app.zhgh.dayofficework.thirtyTeachTour.models.ThirtyTeachTourSettingTravelPeriod;
import com.budwk.app.zhgh.dayofficework.thirtyTeachTour.service.ThirtyTeachTourSettingService;
import com.budwk.app.zhgh.dayofficework.thirtyTeachTour.service.ThirtyTeachTourUserAssignmentService;
import org.nutz.aop.interceptor.ioc.TransAop;
@@ -22,8 +23,11 @@ import org.nutz.mvc.annotation.At;
import org.nutz.mvc.annotation.Ok;
import org.nutz.mvc.annotation.Param;
import java.time.LocalDate;
import java.time.format.DateTimeParseException;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
@IocBean
@@ -77,7 +81,9 @@ public class ThirtyTeachTourSettingController {
Cnd lotCnd = Cnd.NEW();
lotCnd.desc(ThirtyTeachTourSettingLot::getLotValue);
tourSettingService.dao().fetchLinks(tourSetting, "lots", lotCnd);
tourSetting.setUnionQuotas(tourSettingService.listUnionQuotaRows(id));
tourSetting.setUnionQuotas(tourSettingService.listUnionQuotaRows(id, tourSetting.getYear(), tourSetting.getActivityGroupId()));
tourSetting.setTravelPeriods(tourSettingService.listTravelPeriods(id));
tourSetting.setPeriodQuotas(tourSettingService.listPeriodQuotas(id));
return Result.success(tourSetting);
}
@@ -98,15 +104,17 @@ public class ThirtyTeachTourSettingController {
Cnd lotCnd = Cnd.NEW();
lotCnd.desc(ThirtyTeachTourSettingLot::getLotValue);
tourSettingService.dao().fetchLinks(tourSetting, "lots", lotCnd);
tourSetting.setUnionQuotas(tourSettingService.listUnionQuotaRows(tourSetting.getId()));
tourSetting.setUnionQuotas(tourSettingService.listUnionQuotaRows(tourSetting.getId(), tourSetting.getYear(), tourSetting.getActivityGroupId()));
tourSetting.setTravelPeriods(tourSettingService.listTravelPeriods(tourSetting.getId()));
tourSetting.setPeriodQuotas(tourSettingService.listPeriodQuotas(tourSetting.getId()));
return Result.success(tourSetting);
}
@At
@SaCheckPermission("thirtyTeachTour.setting")
public Result unionQuotaRows(String settingId) {
// 新增配置时 settingId 为空,service 会返回所有分工会的空名额行;编辑时合并已保存名额。
return Result.success(tourSettingService.listUnionQuotaRows(settingId));
public Result unionQuotaRows(String settingId, Integer year, String activityGroupId) {
// 新增配置时 settingId 为空,前端传入年度和可参加人员范围;编辑时 service 会兜底读取配置并合并已保存名额。
return Result.success(tourSettingService.listUnionQuotaRows(settingId, year, activityGroupId));
}
@At
@@ -123,6 +131,8 @@ public class ThirtyTeachTourSettingController {
public Result doSubmit(ThirtyTeachTourSetting tourSetting,
@Param(value = "lots") String lots,
@Param(value = "unionQuotas") String unionQuotas,
@Param(value = "travelPeriods") String travelPeriods,
@Param(value = "periodQuotas") String periodQuotas,
@Param(value = "lotDeleteList") String[] lotDeleteList) {
Result checkResult = check(tourSetting);
if (checkResult != null) {
@@ -145,6 +155,10 @@ public class ThirtyTeachTourSettingController {
if (StrUtil.isNotBlank(quotaLimitMessage)) {
return Result.error(quotaLimitMessage);
}
String periodQuotaLimitMessage = tourSettingService.checkPeriodQuotaLimit(periodQuotas, unionQuotas);
if (StrUtil.isNotBlank(periodQuotaLimitMessage)) {
return Result.error(periodQuotaLimitMessage);
}
if (tourSetting.getEnabled() == null) {
tourSetting.setEnabled(true);
@@ -186,6 +200,10 @@ public class ThirtyTeachTourSettingController {
if (lotCheckResult != null) {
return lotCheckResult;
}
Result periodCheckResult = checkTravelPeriods(travelPeriods);
if (periodCheckResult != null) {
return periodCheckResult;
}
if (StrUtil.isBlank(tourSetting.getId())) {
if (Lang.isEmpty(tourSetting.getLots())) {
tourSettingService.insert(tourSetting);
@@ -193,6 +211,8 @@ public class ThirtyTeachTourSettingController {
tourSettingService.insertWith(tourSetting, "lots");
}
tourSettingService.saveUnionQuotas(tourSetting.getId(), unionQuotas);
Set<String> validPeriodIds = tourSettingService.saveTravelPeriods(tourSetting.getId(), travelPeriods);
tourSettingService.savePeriodQuotas(tourSetting.getId(), periodQuotas, validPeriodIds);
} else {
// 编辑时先处理页面删除的标段,再保存配置和当前标段行。
if (Lang.isNotEmpty(lotDeleteList)) {
@@ -201,6 +221,8 @@ public class ThirtyTeachTourSettingController {
tourSettingService.updateIgnoreNull(tourSetting);
saveLots(tourSetting);
tourSettingService.saveUnionQuotas(tourSetting.getId(), unionQuotas);
Set<String> validPeriodIds = tourSettingService.saveTravelPeriods(tourSetting.getId(), travelPeriods);
tourSettingService.savePeriodQuotas(tourSetting.getId(), periodQuotas, validPeriodIds);
}
return Result.success();
}
@@ -215,6 +237,7 @@ public class ThirtyTeachTourSettingController {
}
tourSettingService.dao().clear(ThirtyTeachTourSettingLot.class, Cnd.where(ThirtyTeachTourSettingLot::getSettingId, "=", id));
tourSettingService.clearUnionQuotas(id);
tourSettingService.clearTravelPeriodsAndQuotas(id);
tourUserAssignmentService.clearBySettingId(id);
tourSettingService.delete(id);
return Result.success();
@@ -339,4 +362,42 @@ public class ThirtyTeachTourSettingController {
}
return null;
}
private Result checkTravelPeriods(String travelPeriods) {
if (StrUtil.isBlank(travelPeriods)) {
return null;
}
List<ThirtyTeachTourSettingTravelPeriod> periodList;
try {
periodList = Json.fromJsonAsList(ThirtyTeachTourSettingTravelPeriod.class, travelPeriods);
} catch (Exception e) {
return Result.error("时间段数据格式不正确");
}
if (Lang.isEmpty(periodList)) {
return null;
}
for (int i = 0; i < periodList.size(); i++) {
ThirtyTeachTourSettingTravelPeriod period = periodList.get(i);
if (period == null) {
continue;
}
String rowNo = "" + (i + 1) + "";
if (StrUtil.isBlank(period.getPeriodName())) {
return Result.error(rowNo + "时间段名称不能为空");
}
if (StrUtil.isBlank(period.getStartDate()) || StrUtil.isBlank(period.getEndDate())) {
return Result.error(rowNo + "开始日期和结束日期不能为空");
}
try {
LocalDate startDate = LocalDate.parse(period.getStartDate());
LocalDate endDate = LocalDate.parse(period.getEndDate());
if (startDate.isAfter(endDate)) {
return Result.error(rowNo + "开始日期不能大于结束日期");
}
} catch (DateTimeParseException e) {
return Result.error(rowNo + "日期格式不正确");
}
}
return null;
}
}
@@ -919,7 +919,8 @@ public class ThirtyTeachTourSignupController {
tourLedgerService.insert(ledger);
}
// 报名台账落库后仅回填已存在的人员分配记录事项信息不新增记录不改变分配来源
tourUserAssignmentService.backfillExistingAssignmentAfterSignup(matter.getSettingId(), matter.getId(), SecurityUtil.getUserId(), ledger.getBoardingPlace());
tourUserAssignmentService.backfillExistingAssignmentAfterSignup(matter.getSettingId(), matter.getId(), SecurityUtil.getUserId(),
ledger.getBoardingPlace(), ledger.getPhotoFiles());
if (Lang.isNotEmpty(familyList)) {
familyList.forEach(item -> {
item.setLedgerId(ledger.getId());
@@ -126,6 +126,11 @@ public class ThirtyTeachTourLedger extends BaseModel implements Serializable {
@ColDefine(type = ColType.VARCHAR, width = 100)
private String boardingPlace;
@Column
@Comment("报名图片材料")
@ColDefine(type = ColType.TEXT)
private String photoFiles;
@Column
@Comment("是否携带家属")
@ColDefine(type = ColType.BOOLEAN)
@@ -155,11 +155,22 @@ public class ThirtyTeachTourSetting extends BaseModel implements Serializable {
@Many(field = "settingId")
private List<ThirtyTeachTourSettingLot> lots;
/**
* 时间段管理仅用于配置弹窗回显与提交不作为 thirty_teach_tour_setting 表字段保存
*/
@Many(field = "settingId")
private List<ThirtyTeachTourSettingTravelPeriod> travelPeriods;
/**
* 分工会名额分配仅用于配置弹窗回显与提交不作为 thirty_teach_tour_setting 表字段保存
*/
private List<ThirtyTeachTourSettingUnionQuota> unionQuotas;
/**
* 时间段名额分配仅用于配置弹窗回显与提交不作为 thirty_teach_tour_setting 表字段保存
*/
private List<ThirtyTeachTourSettingPeriodQuota> periodQuotas;
@Column
@Comment("服务须知")
@ColDefine(type = ColType.TEXT)
@@ -0,0 +1,66 @@
package com.budwk.app.zhgh.dayofficework.thirtyTeachTour.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;
/**
* 30年教龄疗休养配置时间段名额
* 该表把分工会正式名额继续拆分到各时间段保存时会校验同一分工会合计不超过正式名额
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Table("thirty_teach_tour_setting_period_quota")
@TableMeta("{'mysql-charset':'utf8mb4'}")
@Comment("30年教龄疗休养配置时间段名额")
public class ThirtyTeachTourSettingPeriodQuota 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("分工会ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String unionId;
@Column
@Comment("分工会名称")
@ColDefine(type = ColType.VARCHAR, width = 100)
private String unionName;
@Column
@Comment("时间段ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String periodId;
@Column
@Comment("时间段名称")
@ColDefine(type = ColType.VARCHAR, width = 50)
private String periodName;
@Column
@Comment("正式人员名额")
@ColDefine(type = ColType.INT)
@Default("0")
private Integer formalQuota;
}
@@ -0,0 +1,59 @@
package com.budwk.app.zhgh.dayofficework.thirtyTeachTour.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 java.io.Serializable;
/**
* 30年教龄疗休养配置时间段
* 时间段只负责配置端细分名额的维度报名/线路占用校验由后续业务接入时再按该配置扩展
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Table("thirty_teach_tour_setting_travel_period")
@TableMeta("{'mysql-charset':'utf8mb4'}")
@Comment("30年教龄疗休养配置时间段")
public class ThirtyTeachTourSettingTravelPeriod extends BaseModel implements Serializable {
@Column
@Name
@Comment("ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
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 periodName;
@Column
@Comment("开始日期")
@ColDefine(type = ColType.VARCHAR, width = 10)
private String startDate;
@Column
@Comment("结束日期")
@ColDefine(type = ColType.VARCHAR, width = 10)
private String endDate;
@Column
@Comment("是否启用")
@ColDefine(type = ColType.BOOLEAN)
@Default("1")
private Boolean enabled;
}
@@ -77,6 +77,21 @@ public class ThirtyTeachTourUserAssignment extends BaseModel implements Serializ
@ColDefine(type = ColType.VARCHAR, width = 100)
private String boardingPlace;
@Column
@Comment("报名图片材料")
@ColDefine(type = ColType.TEXT)
private String photoFiles;
@Column
@Comment("出行时间段ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String periodId;
@Column
@Comment("出行时间段名称")
@ColDefine(type = ColType.VARCHAR, width = 50)
private String periodName;
@Column
@Comment("人员ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
@@ -2,19 +2,40 @@ package com.budwk.app.zhgh.dayofficework.thirtyTeachTour.service;
import com.budwk.app.base.service.BaseService;
import com.budwk.app.zhgh.dayofficework.thirtyTeachTour.models.ThirtyTeachTourSetting;
import com.budwk.app.zhgh.dayofficework.thirtyTeachTour.models.ThirtyTeachTourSettingPeriodQuota;
import com.budwk.app.zhgh.dayofficework.thirtyTeachTour.models.ThirtyTeachTourSettingTravelPeriod;
import com.budwk.app.zhgh.dayofficework.thirtyTeachTour.models.ThirtyTeachTourSettingUnionQuota;
import java.util.List;
import java.util.Set;
public interface ThirtyTeachTourSettingService extends BaseService<ThirtyTeachTourSetting> {
/**
* 查询所有分工会在指定疗休养配置下的名额实时补充当前工会会员数供页面分配时参考
* 查询所有分工会在指定疗休养配置下的名额实时补充可参加人数供页面分配时参考
*
* @param settingId 疗休养配置ID新增时可为空
* @param settingId 疗休养配置ID新增时可为空
* @param year 配置年度新增未落库时用于计算历史参加情况
* @param activityGroupId 可参加人员范围分组ID新增未落库时用于计算可参加人员
* @return 按分工会编码排序后的名额列表
*/
List<ThirtyTeachTourSettingUnionQuota> listUnionQuotaRows(String settingId);
List<ThirtyTeachTourSettingUnionQuota> listUnionQuotaRows(String settingId, Integer year, String activityGroupId);
/**
* 查询配置下维护的时间段按开始日期和创建时间排序
*
* @param settingId 疗休养配置ID
* @return 时间段列表
*/
List<ThirtyTeachTourSettingTravelPeriod> listTravelPeriods(String settingId);
/**
* 查询配置下已保存的时间段名额
*
* @param settingId 疗休养配置ID
* @return 时间段名额列表
*/
List<ThirtyTeachTourSettingPeriodQuota> listPeriodQuotas(String settingId);
/**
* 保存指定疗休养配置下的分工会名额前端传入的是 JSON 数组字符串
@@ -24,6 +45,25 @@ public interface ThirtyTeachTourSettingService extends BaseService<ThirtyTeachTo
*/
void saveUnionQuotas(String settingId, String unionQuotas);
/**
* 保存指定配置下的时间段前端传入的是 JSON 数组字符串
* 时间段ID由前端生成并由服务端原样保留确保同一次提交里的时间段名额可以稳定关联
*
* @param settingId 疗休养配置ID
* @param travelPeriods 时间段 JSON
* @return 本次实际保存且已启用的时间段ID集合
*/
Set<String> saveTravelPeriods(String settingId, String travelPeriods);
/**
* 保存指定配置下的时间段名额保存前会清理无效时间段和零名额行
*
* @param settingId 疗休养配置ID
* @param periodQuotas 时间段名额 JSON
* @param validPeriodIds 当前配置下本次实际保存且已启用的时间段ID集合
*/
void savePeriodQuotas(String settingId, String periodQuotas, Set<String> validPeriodIds);
/**
* 校验分工会正式人员名额合计是否超过分工会总名额
*
@@ -33,10 +73,26 @@ public interface ThirtyTeachTourSettingService extends BaseService<ThirtyTeachTo
*/
String checkBranchFormalQuotaLimit(String unionQuotas, int branchTotalQuota);
/**
* 校验时间段名额合计是否超过对应分工会正式名额
*
* @param periodQuotas 时间段名额 JSON
* @param unionQuotas 分工会名额 JSON
* @return 通过返回空字符串不通过返回业务提示
*/
String checkPeriodQuotaLimit(String periodQuotas, String unionQuotas);
/**
* 清理指定疗休养配置下的分工会名额
*
* @param settingId 疗休养配置ID
*/
void clearUnionQuotas(String settingId);
/**
* 清理指定配置下的时间段和时间段名额
*
* @param settingId 疗休养配置ID
*/
void clearTravelPeriodsAndQuotas(String settingId);
}
@@ -73,9 +73,12 @@ public interface ThirtyTeachTourUserAssignmentService extends BaseService<Thirty
* @param unionId 所属分工会ID
* @param keyword 姓名或工号关键字
* @param userIds 指定候选人员ID列表人员选择器多选查询时使用
* @param summerJoinedFilter 今年是否已参加暑期疗休养过滤true false null 不过滤
* @param lastThirtyJoinedFilter 去年是否参加30年教龄疗休养过滤true false null 不过滤
* @return 可分配候选人分页数据
*/
Pagination<NutMap> schoolCandidatePage(PageForm pageForm, String settingId, String unionId, String keyword, List<String> userIds);
Pagination<NutMap> schoolCandidatePage(PageForm pageForm, String settingId, String unionId, String keyword, List<String> userIds,
Boolean summerJoinedFilter, Boolean lastThirtyJoinedFilter);
/**
* 分页查询当前登录人所在分工会的人员分配记录列表只读取人员分配表不读取报名台账
@@ -157,11 +160,11 @@ public interface ThirtyTeachTourUserAssignmentService extends BaseService<Thirty
* @param userIds 待分配人员ID列表
* @return 保存结果包含实际分配数量跳过数量和代报名台账写入数量
*/
NutMap assignSchoolUsers(String settingId, String matterId, String personType, List<String> userIds);
NutMap assignSchoolUsers(String settingId, String matterId, String personType, List<String> userIds, String photoFiles);
/**
* 保存校工会人员分配支持每个候选人员单独选择分配线路
* assignItems 中每项包含 userIdmatterIdmatterId 为空时只保存人员分配不写台账
* assignItems 中每项包含 userIdmatterIdphotoFilesmatterId 为空时只保存人员分配不写台账
*
* @param settingId 疗休养配置ID
* @param assignItems 人员和分配线路明细
@@ -177,7 +180,7 @@ public interface ThirtyTeachTourUserAssignmentService extends BaseService<Thirty
* @param matterId 疗休养事项ID
* @return 处理结果包含台账写入数量
*/
NutMap selectSchoolAssignmentMatter(String id, String matterId);
NutMap selectSchoolAssignmentMatter(String id, String matterId, String photoFiles);
/**
* 给当前分工会已分配的正式人员补选分配路线并按代报名写入疗休养台账
@@ -187,19 +190,38 @@ public interface ThirtyTeachTourUserAssignmentService extends BaseService<Thirty
* @param matterId 疗休养事项ID
* @return 处理结果包含台账写入数量
*/
NutMap selectCurrentBranchAssignmentMatter(String id, String matterId);
NutMap selectCurrentBranchAssignmentMatter(String id, String matterId, String photoFiles);
/**
* 查询指定配置下已启用的出行时间段供分工会人员分配弹窗按时间段选择正式名额
*
* @param settingId 疗休养配置ID
* @return 已启用出行时间段列表
*/
List<NutMap> listBranchEnabledTravelPeriods(String settingId);
/**
* 查询当前登录人所在分工会在指定时间段下的正式名额使用情况
*
* @param settingId 疗休养配置ID
* @param periodId 出行时间段ID
* @return 时间段正式名额已分配人数和剩余人数
*/
NutMap branchPeriodQuotaInfo(String settingId, String periodId);
/**
* 保存分工会人员分配保存前会重新按配置可参加人员范围和当前登录人所在分工会过滤并校验正式/替补名额
* 正式人员必须选择出行时间段并按当前分工会 + 出行时间段校验正式名额替补人员沿用分工会替补名额
* 疗休养事项为可选正式人员选择事项时视为代报名并同步写入疗休养台账替补人员不能分配事项
*
* @param settingId 疗休养配置ID
* @param matterId 疗休养事项ID可为空
* @param periodId 出行时间段ID正式人员必填
* @param personType 人员类型FORMAL 正式人员BACKUP 替补人员
* @param userIds 待分配人员ID列表
* @param assignItems 待分配人员明细每项包含 userIdphotoFiles
* @return 保存结果包含实际分配数量跳过数量代报名台账写入数量和剩余名额
*/
NutMap assignBranchUsers(String settingId, String matterId, String personType, List<String> userIds);
NutMap assignBranchUsers(String settingId, String matterId, String periodId, String personType, List<NutMap> assignItems);
/**
* 切换当前分工会人员分配记录的正式/替补状态
@@ -251,7 +273,7 @@ public interface ThirtyTeachTourUserAssignmentService extends BaseService<Thirty
* @param boardingPlace 用户报名时最终选择的乘车地点
* @return 更新记录数
*/
int backfillExistingAssignmentAfterSignup(String settingId, String matterId, String userId, String boardingPlace);
int backfillExistingAssignmentAfterSignup(String settingId, String matterId, String userId, String boardingPlace, String photoFiles);
/**
* 用户取消报名删除台账后清空该用户在同一疗休养配置下已存在人员分配记录的事项快照
@@ -297,6 +319,22 @@ public interface ThirtyTeachTourUserAssignmentService extends BaseService<Thirty
*/
NutMap branchDeleteInfo(String id);
/**
* 更新校工会人员分配记录的图片材料并同步该人员已存在的报名台账图片
*
* @param id 人员分配记录ID
* @param photoFiles 图片材料多个文件以逗号分隔
*/
void updateSchoolAssignmentPhotoFiles(String id, String photoFiles);
/**
* 更新当前分工会人员分配记录的图片材料并同步该人员已存在的报名台账图片
*
* @param id 人员分配记录ID
* @param photoFiles 图片材料多个文件以逗号分隔
*/
void updateCurrentBranchAssignmentPhotoFiles(String id, String photoFiles);
/**
* 删除指定来源的人员分配记录避免校工会页面误删分工会分配的数据
*
@@ -4,6 +4,8 @@ import cn.hutool.core.util.StrUtil;
import com.budwk.app.base.service.impl.BaseServiceImpl;
import com.budwk.app.sys.models.Sys_union;
import com.budwk.app.zhgh.dayofficework.thirtyTeachTour.models.ThirtyTeachTourSetting;
import com.budwk.app.zhgh.dayofficework.thirtyTeachTour.models.ThirtyTeachTourSettingPeriodQuota;
import com.budwk.app.zhgh.dayofficework.thirtyTeachTour.models.ThirtyTeachTourSettingTravelPeriod;
import com.budwk.app.zhgh.dayofficework.thirtyTeachTour.models.ThirtyTeachTourSettingUnionQuota;
import com.budwk.app.zhgh.dayofficework.thirtyTeachTour.service.ThirtyTeachTourSettingService;
import org.nutz.dao.Cnd;
@@ -15,8 +17,10 @@ import org.nutz.json.Json;
import org.nutz.lang.Lang;
import org.nutz.lang.util.NutMap;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
@IocBean(args = {"refer:dao"})
@@ -27,12 +31,18 @@ public class ThirtyTeachTourSettingServiceImpl extends BaseServiceImpl<ThirtyTea
}
@Override
public List<ThirtyTeachTourSettingUnionQuota> listUnionQuotaRows(String settingId) {
// 一次性查出所有分工会已保存名额和实时会员数避免页面打开时按分工会循环统计造成 N+1 查询
public List<ThirtyTeachTourSettingUnionQuota> listUnionQuotaRows(String settingId, Integer year, String activityGroupId) {
ThirtyTeachTourSetting setting = resolveQuotaSetting(settingId);
Integer effectiveYear = year == null && setting != null ? setting.getYear() : year;
String effectiveActivityGroupId = StrUtil.isBlank(activityGroupId) && setting != null ? setting.getActivityGroupId() : activityGroupId;
String quotaSettingId = settingId == null ? "" : settingId;
int previousYear = effectiveYear == null ? 0 : effectiveYear - 1;
// 一次性查出所有分工会已保存名额和可参加人数避免页面打开时按分工会循环统计造成 N+1 查询
// 可参加人数口径当前活动人员范围内人员扣除去年已参加30年教龄疗休养和本年度已参加暑期疗休养的人员
Sql sql = Sqls.create("""
SELECT
quota.id AS id,
@settingId AS settingId,
@quotaSettingId AS settingId,
un.id AS unionId,
un.name AS unionName,
COALESCE(quota.formalQuota, 0) AS formalQuota,
@@ -41,16 +51,37 @@ public class ThirtyTeachTourSettingServiceImpl extends BaseServiceImpl<ThirtyTea
FROM sys_union un
LEFT JOIN thirty_teach_tour_setting_union_quota quota
ON quota.unionId = un.id
AND quota.settingId = @settingId
AND quota.settingId = @quotaSettingId
LEFT JOIN (
SELECT unionId, COUNT(1) AS memberCount
FROM vw_user
WHERE member = 1
GROUP BY unionId
SELECT u.unionid AS unionId, COUNT(DISTINCT u.id) AS memberCount
FROM activity_user_scope aus
INNER JOIN vw_user u ON u.id = aus.userId
WHERE aus.groupId = @activityGroupId
AND @year > 0
AND NOT EXISTS (
SELECT 1
FROM thirty_teach_tour_ledger previous_ledger
WHERE previous_ledger.delFlag = 0
AND previous_ledger.joined = 1
AND previous_ledger.`year` = @previousYear
AND previous_ledger.jobNo = u.loginname
)
AND NOT EXISTS (
SELECT 1
FROM tour_ledger summer_ledger
WHERE summer_ledger.delFlag = 0
AND summer_ledger.joined = 1
AND summer_ledger.`year` = @year
AND summer_ledger.jobNo = u.loginname
)
GROUP BY u.unionid
) user_count ON user_count.unionId = un.id
ORDER BY un.unionCode ASC
""");
sql.setParam("settingId", settingId == null ? "" : settingId);
sql.setParam("quotaSettingId", quotaSettingId);
sql.setParam("activityGroupId", StrUtil.blankToDefault(effectiveActivityGroupId, ""));
sql.setParam("year", effectiveYear == null ? 0 : effectiveYear);
sql.setParam("previousYear", previousYear);
List<NutMap> rows = listMap(sql);
return rows.stream().map(row -> {
ThirtyTeachTourSettingUnionQuota quota = new ThirtyTeachTourSettingUnionQuota();
@@ -65,6 +96,37 @@ public class ThirtyTeachTourSettingServiceImpl extends BaseServiceImpl<ThirtyTea
}).collect(Collectors.toList());
}
private ThirtyTeachTourSetting resolveQuotaSetting(String settingId) {
// 编辑配置时允许前端只传 settingId服务层兜底读取年度和可参加人员范围保证接口兼容已有调用
if (StrUtil.isBlank(settingId)) {
return null;
}
return fetch(settingId);
}
@Override
public List<ThirtyTeachTourSettingTravelPeriod> listTravelPeriods(String settingId) {
if (StrUtil.isBlank(settingId)) {
return List.of();
}
Cnd cnd = Cnd.NEW();
cnd.and("settingId", "=", settingId)
.and("delFlag", "=", false)
.asc("startDate")
.asc("createdAt");
return dao().query(ThirtyTeachTourSettingTravelPeriod.class, cnd);
}
@Override
public List<ThirtyTeachTourSettingPeriodQuota> listPeriodQuotas(String settingId) {
if (StrUtil.isBlank(settingId)) {
return List.of();
}
Cnd cnd = Cnd.where("settingId", "=", settingId)
.and("delFlag", "=", false);
return dao().query(ThirtyTeachTourSettingPeriodQuota.class, cnd);
}
@Override
public void saveUnionQuotas(String settingId, String unionQuotas) {
if (StrUtil.isBlank(settingId)) {
@@ -90,6 +152,76 @@ public class ThirtyTeachTourSettingServiceImpl extends BaseServiceImpl<ThirtyTea
}
}
@Override
public Set<String> saveTravelPeriods(String settingId, String travelPeriods) {
if (StrUtil.isBlank(settingId)) {
return Collections.emptySet();
}
dao().clear(ThirtyTeachTourSettingTravelPeriod.class,
Cnd.where("settingId", "=", settingId));
if (StrUtil.isBlank(travelPeriods)) {
return Collections.emptySet();
}
List<ThirtyTeachTourSettingTravelPeriod> periodList = Json.fromJsonAsList(ThirtyTeachTourSettingTravelPeriod.class, travelPeriods);
if (Lang.isEmpty(periodList)) {
return Collections.emptySet();
}
List<ThirtyTeachTourSettingTravelPeriod> saveList = periodList.stream()
.filter(item -> item != null
&& StrUtil.isNotBlank(item.getId())
&& StrUtil.isNotBlank(item.getPeriodName())
&& StrUtil.isNotBlank(item.getStartDate())
&& StrUtil.isNotBlank(item.getEndDate()))
.peek(item -> {
// 前端会为新增时间段生成临时ID服务端保留该ID以便时间段名额能稳定关联
item.setSettingId(settingId);
item.setEnabled(Boolean.TRUE.equals(item.getEnabled()));
})
.collect(Collectors.toList());
if (Lang.isNotEmpty(saveList)) {
dao().insert(saveList);
}
return saveList.stream()
.filter(item -> Boolean.TRUE.equals(item.getEnabled()))
.map(ThirtyTeachTourSettingTravelPeriod::getId)
.collect(Collectors.toSet());
}
@Override
public void savePeriodQuotas(String settingId, String periodQuotas, Set<String> validPeriodIds) {
if (StrUtil.isBlank(settingId)) {
return;
}
dao().clear(ThirtyTeachTourSettingPeriodQuota.class,
Cnd.where("settingId", "=", settingId));
if (StrUtil.isBlank(periodQuotas)) {
return;
}
List<ThirtyTeachTourSettingPeriodQuota> quotaList = Json.fromJsonAsList(ThirtyTeachTourSettingPeriodQuota.class, periodQuotas);
if (Lang.isEmpty(quotaList)) {
return;
}
Set<String> periodIds = Lang.isEmpty(validPeriodIds)
? listTravelPeriods(settingId).stream()
.filter(item -> Boolean.TRUE.equals(item.getEnabled()))
.map(ThirtyTeachTourSettingTravelPeriod::getId)
.collect(Collectors.toSet())
: validPeriodIds;
Map<String, Sys_union> unionMap = dao().query(Sys_union.class, Cnd.NEW()).stream()
.collect(Collectors.toMap(Sys_union::getId, item -> item, (a, b) -> a));
List<ThirtyTeachTourSettingPeriodQuota> saveList = quotaList.stream()
.filter(item -> item != null
&& StrUtil.isNotBlank(item.getUnionId())
&& StrUtil.isNotBlank(item.getPeriodId())
&& periodIds.contains(item.getPeriodId())
&& defaultInt(item.getFormalQuota()) > 0)
.map(item -> normalizePeriodQuota(settingId, item, unionMap))
.collect(Collectors.toList());
if (Lang.isNotEmpty(saveList)) {
dao().insert(saveList);
}
}
@Override
public String checkBranchFormalQuotaLimit(String unionQuotas, int branchTotalQuota) {
if (StrUtil.isBlank(unionQuotas)) {
@@ -109,6 +241,37 @@ public class ThirtyTeachTourSettingServiceImpl extends BaseServiceImpl<ThirtyTea
return "";
}
@Override
public String checkPeriodQuotaLimit(String periodQuotas, String unionQuotas) {
if (StrUtil.isBlank(periodQuotas)) {
return "";
}
List<ThirtyTeachTourSettingPeriodQuota> periodQuotaList = Json.fromJsonAsList(ThirtyTeachTourSettingPeriodQuota.class, periodQuotas);
if (Lang.isEmpty(periodQuotaList)) {
return "";
}
List<ThirtyTeachTourSettingUnionQuota> unionQuotaList = StrUtil.isBlank(unionQuotas)
? List.of()
: Json.fromJsonAsList(ThirtyTeachTourSettingUnionQuota.class, unionQuotas);
Map<String, ThirtyTeachTourSettingUnionQuota> unionQuotaMap = unionQuotaList.stream()
.filter(item -> item != null && StrUtil.isNotBlank(item.getUnionId()))
.collect(Collectors.toMap(ThirtyTeachTourSettingUnionQuota::getUnionId, item -> item, (a, b) -> a));
Map<String, Integer> periodQuotaTotalMap = periodQuotaList.stream()
.filter(item -> item != null && StrUtil.isNotBlank(item.getUnionId()))
.collect(Collectors.groupingBy(ThirtyTeachTourSettingPeriodQuota::getUnionId,
Collectors.summingInt(item -> defaultInt(item.getFormalQuota()))));
for (Map.Entry<String, Integer> entry : periodQuotaTotalMap.entrySet()) {
ThirtyTeachTourSettingUnionQuota unionQuota = unionQuotaMap.get(entry.getKey());
int formalQuota = unionQuota == null ? 0 : defaultInt(unionQuota.getFormalQuota());
if (entry.getValue() > formalQuota) {
String unionName = unionQuota == null ? "" : unionQuota.getUnionName();
return "分工会【" + StrUtil.blankToDefault(unionName, entry.getKey()) + "】时间段名额合计 "
+ entry.getValue() + ",不能大于当前分工会正式名额 " + formalQuota;
}
}
return "";
}
@Override
public void clearUnionQuotas(String settingId) {
if (StrUtil.isNotBlank(settingId)) {
@@ -116,6 +279,15 @@ public class ThirtyTeachTourSettingServiceImpl extends BaseServiceImpl<ThirtyTea
}
}
@Override
public void clearTravelPeriodsAndQuotas(String settingId) {
if (StrUtil.isBlank(settingId)) {
return;
}
dao().clear(ThirtyTeachTourSettingPeriodQuota.class, Cnd.where("settingId", "=", settingId));
dao().clear(ThirtyTeachTourSettingTravelPeriod.class, Cnd.where("settingId", "=", settingId));
}
private ThirtyTeachTourSettingUnionQuota normalizeUnionQuota(String settingId, ThirtyTeachTourSettingUnionQuota item, Map<String, Sys_union> unionMap) {
Sys_union union = unionMap.get(item.getUnionId());
ThirtyTeachTourSettingUnionQuota quota = new ThirtyTeachTourSettingUnionQuota();
@@ -127,6 +299,18 @@ public class ThirtyTeachTourSettingServiceImpl extends BaseServiceImpl<ThirtyTea
return quota;
}
private ThirtyTeachTourSettingPeriodQuota normalizePeriodQuota(String settingId, ThirtyTeachTourSettingPeriodQuota item, Map<String, Sys_union> unionMap) {
Sys_union union = unionMap.get(item.getUnionId());
ThirtyTeachTourSettingPeriodQuota quota = new ThirtyTeachTourSettingPeriodQuota();
quota.setSettingId(settingId);
quota.setUnionId(item.getUnionId());
quota.setUnionName(union == null ? item.getUnionName() : union.getName());
quota.setPeriodId(item.getPeriodId());
quota.setPeriodName(item.getPeriodName());
quota.setFormalQuota(defaultInt(item.getFormalQuota()));
return quota;
}
private Integer defaultInt(Integer value) {
return value == null || value < 0 ? 0 : value;
}
@@ -122,13 +122,14 @@ public class ThirtyTeachTourUserAssignmentServiceImpl extends BaseServiceImpl<Th
}
@Override
public Pagination<NutMap> schoolCandidatePage(PageForm pageForm, String settingId, String unionId, String keyword, List<String> userIds) {
public Pagination<NutMap> schoolCandidatePage(PageForm pageForm, String settingId, String unionId, String keyword, List<String> userIds,
Boolean summerJoinedFilter, Boolean lastThirtyJoinedFilter) {
ThirtyTeachTourSetting setting = fetchSettingForAssignment(settingId);
if (setting == null || StrUtil.isBlank(setting.getActivityGroupId())) {
return emptyPagination(pageForm);
}
Sql sql = buildCandidateSql(settingId);
Cnd cnd = buildCandidateCnd(settingId, setting.getActivityGroupId(), unionId, keyword);
Sql sql = buildCandidateSql(settingId, setting.getYear());
Cnd cnd = buildCandidateCnd(setting.getActivityGroupId(), unionId, keyword, summerJoinedFilter, lastThirtyJoinedFilter);
List<String> normalizedUserIds = normalizeUserIds(userIds);
// 人员选择器多选查询时候选范围仍受活动组分工会已分配排除规则约束再按指定人员ID精确过滤
if (!Lang.isEmpty(normalizedUserIds)) {
@@ -171,7 +172,7 @@ public class ThirtyTeachTourUserAssignmentServiceImpl extends BaseServiceImpl<Th
if (StrUtil.isBlank(unionId)) {
return emptyPagination(pageForm);
}
return schoolCandidatePage(pageForm, settingId, unionId, keyword, userIds);
return schoolCandidatePage(pageForm, settingId, unionId, keyword, userIds, false, false);
}
/**
@@ -342,7 +343,7 @@ public class ThirtyTeachTourUserAssignmentServiceImpl extends BaseServiceImpl<Th
}
@Override
public NutMap assignSchoolUsers(String settingId, String matterId, String personType, List<String> userIds) {
public NutMap assignSchoolUsers(String settingId, String matterId, String personType, List<String> userIds, String photoFiles) {
List<String> distinctUserIds = normalizeUserIds(userIds);
if (StrUtil.isBlank(settingId) || Lang.isEmpty(distinctUserIds)) {
throw new IllegalArgumentException("请选择疗休养配置和分配人员");
@@ -359,11 +360,12 @@ public class ThirtyTeachTourUserAssignmentServiceImpl extends BaseServiceImpl<Th
}
// 保存前重新按配置人员范围 + 未分配查询防止前端绕过候选列表提交范围外人员
Sql userSql = buildCandidateSql(settingId);
Cnd userCnd = buildCandidateCnd(settingId, setting.getActivityGroupId(), null, null);
Sql userSql = buildCandidateSql(settingId, setting.getYear());
Cnd userCnd = buildCandidateCnd(setting.getActivityGroupId(), null, null, false, false);
userCnd.and("u.id", "in", distinctUserIds);
userSql.setCondition(userCnd);
List<NutMap> candidateUsers = listMap(userSql);
fillCandidatePhotoFiles(candidateUsers, normalizePhotoFiles(photoFiles));
List<ThirtyTeachTourUserAssignment> assignments = candidateUsers.stream()
.map(user -> buildSchoolAssignment(settingId, matterInfo, normalizedPersonType, user))
.collect(Collectors.toList());
@@ -392,6 +394,7 @@ public class ThirtyTeachTourUserAssignmentServiceImpl extends BaseServiceImpl<Th
}
Map<String, String> matterByUserId = new HashMap<>();
Map<String, String> photoFilesByUserId = new HashMap<>();
List<String> userIds = assignItems.stream()
.map(item -> item == null ? "" : item.getString("userId", ""))
.filter(StrUtil::isNotBlank)
@@ -406,6 +409,7 @@ public class ThirtyTeachTourUserAssignmentServiceImpl extends BaseServiceImpl<Th
return;
}
matterByUserId.put(userId, item.getString("matterId", ""));
photoFilesByUserId.put(userId, normalizePhotoFiles(item.getString("photoFiles")));
});
if (Lang.isEmpty(userIds)) {
throw new IllegalArgumentException("请选择分配人员");
@@ -424,11 +428,12 @@ public class ThirtyTeachTourUserAssignmentServiceImpl extends BaseServiceImpl<Th
});
// 保存前重新按配置人员范围 + 未分配查询防止前端绕过候选列表提交范围外人员
Sql userSql = buildCandidateSql(settingId);
Cnd userCnd = buildCandidateCnd(settingId, setting.getActivityGroupId(), null, null);
Sql userSql = buildCandidateSql(settingId, setting.getYear());
Cnd userCnd = buildCandidateCnd(setting.getActivityGroupId(), null, null, false, false);
userCnd.and("u.id", "in", userIds);
userSql.setCondition(userCnd);
List<NutMap> candidateUsers = listMap(userSql);
candidateUsers.forEach(user -> user.put("photoFiles", photoFilesByUserId.get(user.getString("userId", ""))));
List<ThirtyTeachTourUserAssignment> assignments = candidateUsers.stream()
.map(user -> {
@@ -456,7 +461,7 @@ public class ThirtyTeachTourUserAssignmentServiceImpl extends BaseServiceImpl<Th
}
@Override
public NutMap selectSchoolAssignmentMatter(String id, String matterId) {
public NutMap selectSchoolAssignmentMatter(String id, String matterId, String photoFiles) {
if (StrUtil.isBlank(id) || StrUtil.isBlank(matterId)) {
throw new IllegalArgumentException("请选择人员分配记录和分配线路");
}
@@ -477,13 +482,14 @@ public class ThirtyTeachTourUserAssignmentServiceImpl extends BaseServiceImpl<Th
if (matterInfo == null) {
throw new IllegalArgumentException("分配线路不存在或未配置线路");
}
updateAssignmentMatterSnapshot(id, matterInfo, ThirtyTeachTourUserAssignment.ASSIGN_SOURCE_SCHOOL_UNION);
updateAssignmentMatterSnapshot(id, matterInfo, ThirtyTeachTourUserAssignment.ASSIGN_SOURCE_SCHOOL_UNION, photoFiles);
assignment.setPhotoFiles(normalizePhotoFiles(photoFiles));
int ledgerCount = insertProxySignupLedgers(matterInfo, Collections.singletonList(fetchAssignmentUserSnapshot(assignment)));
return NutMap.NEW().addv("ledgerCount", ledgerCount);
}
@Override
public NutMap selectCurrentBranchAssignmentMatter(String id, String matterId) {
public NutMap selectCurrentBranchAssignmentMatter(String id, String matterId, String photoFiles) {
if (StrUtil.isBlank(id) || StrUtil.isBlank(matterId)) {
throw new IllegalArgumentException("请选择人员分配记录和分配路线");
}
@@ -505,23 +511,43 @@ public class ThirtyTeachTourUserAssignmentServiceImpl extends BaseServiceImpl<Th
if (!isMatterSignupOpen(matterInfo)) {
throw new IllegalArgumentException("当前线路不在报名时间内,不能选择");
}
assignment.setPhotoFiles(normalizePhotoFiles(photoFiles));
List<NutMap> users = Collections.singletonList(fetchAssignmentUserSnapshot(assignment));
checkProxySignupMaxGroupPeople(matterInfo, users);
updateAssignmentMatterSnapshot(id, matterInfo, ThirtyTeachTourUserAssignment.ASSIGN_SOURCE_BRANCH_UNION);
updateAssignmentMatterSnapshot(id, matterInfo, ThirtyTeachTourUserAssignment.ASSIGN_SOURCE_BRANCH_UNION, photoFiles);
int ledgerCount = insertProxySignupLedgers(matterInfo, users);
return NutMap.NEW().addv("ledgerCount", ledgerCount);
}
@Override
public NutMap assignBranchUsers(String settingId, String matterId, String personType, List<String> userIds) {
public NutMap assignBranchUsers(String settingId, String matterId, String periodId, String personType, List<NutMap> assignItems) {
String unionId = SecurityUtil.getUnionId();
if (StrUtil.isBlank(unionId)) {
throw new IllegalArgumentException("当前登录人未绑定分工会");
}
List<String> distinctUserIds = normalizeUserIds(userIds);
if (StrUtil.isBlank(settingId) || Lang.isEmpty(distinctUserIds)) {
if (StrUtil.isBlank(settingId) || Lang.isEmpty(assignItems)) {
throw new IllegalArgumentException("请选择疗休养配置和分配人员");
}
Map<String, String> photoFilesByUserId = new HashMap<>();
List<String> distinctUserIds = assignItems.stream()
.map(item -> item == null ? "" : item.getString("userId", ""))
.filter(StrUtil::isNotBlank)
.distinct()
.collect(Collectors.toList());
assignItems.forEach(item -> {
if (item == null) {
return;
}
String userId = item.getString("userId", "");
if (StrUtil.isBlank(userId)) {
return;
}
// 图片材料来自人员分配弹窗的行级暂存按人员写入后续分配表和台账
photoFilesByUserId.put(userId, normalizePhotoFiles(item.getString("photoFiles")));
});
if (Lang.isEmpty(distinctUserIds)) {
throw new IllegalArgumentException("请选择分配人员");
}
String normalizedPersonType = normalizePersonType(personType);
NutMap matterInfo = prepareMatterInfoForAssignment(settingId, matterId, normalizedPersonType);
// 分工会代报名占用事项最大成团人数在同一事务中锁定事项行避免多个分工会同时校验通过后并发写入导致超额
@@ -539,26 +565,47 @@ public class ThirtyTeachTourUserAssignmentServiceImpl extends BaseServiceImpl<Th
throw new IllegalArgumentException("疗休养配置未设置可参加人员范围");
}
NutMap quotaInfo = branchQuotaInfo(settingId);
NutMap periodInfo = NutMap.NEW();
ThirtyTeachTourSettingTravelPeriod selectedPeriod = null;
int remainingQuota = defaultInt(quotaInfo.getInt(remainingQuotaKey(normalizedPersonType)));
if (ThirtyTeachTourUserAssignment.PERSON_TYPE_FORMAL.equals(normalizedPersonType)) {
if (StrUtil.isBlank(periodId)) {
throw new IllegalArgumentException("请选择出行时间");
}
selectedPeriod = fetchEnabledTravelPeriod(settingId, periodId);
if (selectedPeriod == null) {
throw new IllegalArgumentException("出行时间不存在或已禁用");
}
lockBranchPeriodQuota(settingId, periodId, unionId);
// 正式人员按当前分工会 + 已启用时间段读取细分名额避免继续占用配置页签中的分工会总正式名额
periodInfo = branchPeriodQuotaInfo(settingId, periodId);
remainingQuota = defaultInt(periodInfo.getInt("formalRemaining"));
}
if (remainingQuota <= 0) {
throw new IllegalArgumentException("当前分工会该人员类型暂无剩余名额");
}
// 保存前重新按配置人员范围 + 当前分工会 + 未分配查询防止前端提交范围外或其它分工会人员
Sql userSql = buildCandidateSql(settingId);
Cnd userCnd = buildCandidateCnd(settingId, setting.getActivityGroupId(), unionId, null);
Sql userSql = buildCandidateSql(settingId, setting.getYear());
Cnd userCnd = buildCandidateCnd(setting.getActivityGroupId(), unionId, null, false, false);
userCnd.and("u.id", "in", distinctUserIds);
userSql.setCondition(userCnd);
List<NutMap> candidateUsers = listMap(userSql);
candidateUsers.forEach(user -> user.put("photoFiles", photoFilesByUserId.get(user.getString("userId", ""))));
if (candidateUsers.size() > remainingQuota) {
throw new IllegalArgumentException("选择人数超过当前分工会剩余名额");
}
// 分工会代报名会占用线路成团人数人员分配和台账写入前先做后端兜底校验
checkProxySignupMaxGroupPeople(matterInfo, candidateUsers);
final NutMap assignmentMatterInfo = matterInfo;
final ThirtyTeachTourSettingTravelPeriod assignmentPeriod = selectedPeriod;
List<ThirtyTeachTourUserAssignment> assignments = candidateUsers.stream()
.map(user -> buildAssignment(settingId, assignmentMatterInfo, normalizedPersonType, user,
ThirtyTeachTourUserAssignment.ASSIGN_SOURCE_BRANCH_UNION))
.map(user -> {
ThirtyTeachTourUserAssignment assignment = buildAssignment(settingId, assignmentMatterInfo, normalizedPersonType, user,
ThirtyTeachTourUserAssignment.ASSIGN_SOURCE_BRANCH_UNION);
fillTravelPeriodSnapshot(assignment, assignmentPeriod);
return assignment;
})
.collect(Collectors.toList());
if (Lang.isNotEmpty(assignments)) {
dao().insert(assignments);
@@ -566,11 +613,14 @@ public class ThirtyTeachTourUserAssignmentServiceImpl extends BaseServiceImpl<Th
// 事项为空时只写人员分配表正式人员选择事项时按代报名写入台账
int ledgerCount = insertProxySignupLedgers(matterInfo, candidateUsers);
NutMap newQuotaInfo = branchQuotaInfo(settingId);
NutMap newPeriodQuotaInfo = ThirtyTeachTourUserAssignment.PERSON_TYPE_FORMAL.equals(normalizedPersonType)
? branchPeriodQuotaInfo(settingId, periodId) : NutMap.NEW();
return NutMap.NEW()
.addv("assignCount", assignments.size())
.addv("skipCount", distinctUserIds.size() - assignments.size())
.addv("ledgerCount", ledgerCount)
.addv("formalRemaining", defaultInt(newQuotaInfo.getInt("formalRemaining")))
.addv("formalRemaining", ThirtyTeachTourUserAssignment.PERSON_TYPE_FORMAL.equals(normalizedPersonType)
? defaultInt(newPeriodQuotaInfo.getInt("formalRemaining")) : defaultInt(newQuotaInfo.getInt("formalRemaining")))
.addv("backupRemaining", defaultInt(newQuotaInfo.getInt("backupRemaining")));
}
@@ -625,6 +675,79 @@ public class ThirtyTeachTourUserAssignmentServiceImpl extends BaseServiceImpl<Th
.addv("backupRemaining", Math.max(backupQuota - backupUsed, 0));
}
@Override
public List<NutMap> listBranchEnabledTravelPeriods(String settingId) {
if (StrUtil.isBlank(settingId)) {
return Collections.emptyList();
}
List<ThirtyTeachTourSettingTravelPeriod> periods = dao().query(ThirtyTeachTourSettingTravelPeriod.class,
Cnd.where("settingId", "=", settingId)
.and("enabled", "=", true)
.and("delFlag", "=", false)
.asc("startDate")
.asc("createdAt"));
return periods.stream()
.map(period -> NutMap.NEW()
.addv("id", period.getId())
.addv("periodName", period.getPeriodName())
.addv("startDate", period.getStartDate())
.addv("endDate", period.getEndDate())
.addv("label", travelPeriodButtonLabel(period)))
.collect(Collectors.toList());
}
@Override
public NutMap branchPeriodQuotaInfo(String settingId, String periodId) {
String unionId = SecurityUtil.getUnionId();
if (StrUtil.isBlank(settingId) || StrUtil.isBlank(periodId) || StrUtil.isBlank(unionId)) {
return emptyPeriodQuotaInfo(periodId);
}
Sql sql = Sqls.create("""
SELECT
COALESCE(q.formalQuota, 0) AS formalQuota,
COALESCE(used_quota.formalUsed, 0) AS formalUsed
FROM thirty_teach_tour_setting_period_quota q
LEFT JOIN (
SELECT
settingId,
unionId,
periodId,
COUNT(1) AS formalUsed
FROM thirty_teach_tour_user_assignment
WHERE delFlag = 0
AND assignSource = @assignSource
AND personType = @formalType
AND settingId = @settingId
AND unionId = @unionId
AND periodId = @periodId
AND (cancelled = 0 OR cancelled IS NULL)
GROUP BY settingId, unionId, periodId
) used_quota ON used_quota.settingId = q.settingId
AND used_quota.unionId = q.unionId
AND used_quota.periodId = q.periodId
WHERE q.settingId = @settingId
AND q.unionId = @unionId
AND q.periodId = @periodId
AND q.delFlag = 0
LIMIT 1
""");
sql.setParam("assignSource", ThirtyTeachTourUserAssignment.ASSIGN_SOURCE_BRANCH_UNION);
sql.setParam("formalType", ThirtyTeachTourUserAssignment.PERSON_TYPE_FORMAL);
sql.setParam("settingId", settingId);
sql.setParam("unionId", unionId);
sql.setParam("periodId", periodId);
List<NutMap> rows = listMap(sql);
NutMap quotaInfo = Lang.isEmpty(rows) ? emptyPeriodQuotaInfo(periodId) : rows.get(0);
int formalQuota = defaultInt(quotaInfo.getInt("formalQuota"));
int formalUsed = defaultInt(quotaInfo.getInt("formalUsed"));
// NutMap.addv 对已存在的同名 key 会形成数组这里直接构造最终返回值避免前端显示 [0, 12]
return NutMap.NEW()
.addv("periodId", periodId)
.addv("formalQuota", formalQuota)
.addv("formalUsed", formalUsed)
.addv("formalRemaining", Math.max(formalQuota - formalUsed, 0));
}
@Override
public NutMap switchCurrentBranchPersonType(String id, String personType) {
String unionId = SecurityUtil.getUnionId();
@@ -651,6 +774,14 @@ public class ThirtyTeachTourUserAssignmentServiceImpl extends BaseServiceImpl<Th
}
NutMap quotaInfo = branchQuotaInfo(assignment.getSettingId());
int remainingQuota = defaultInt(quotaInfo.getInt(remainingQuotaKey(targetPersonType)));
if (ThirtyTeachTourUserAssignment.PERSON_TYPE_FORMAL.equals(targetPersonType)) {
if (StrUtil.isBlank(assignment.getPeriodId())) {
throw new IllegalArgumentException("当前人员未记录出行时间,不能直接转为正式人员");
}
// 转为正式人员会重新占用时间段名额按人员原有时间段快照做后端兜底校验
NutMap periodQuotaInfo = branchPeriodQuotaInfo(assignment.getSettingId(), assignment.getPeriodId());
remainingQuota = defaultInt(periodQuotaInfo.getInt("formalRemaining"));
}
if (remainingQuota <= 0) {
throw new IllegalArgumentException("当前分工会目标人员类型暂无剩余名额");
}
@@ -743,6 +874,7 @@ public class ThirtyTeachTourUserAssignmentServiceImpl extends BaseServiceImpl<Th
unionId,
unionName,
boardingPlace,
photoFiles,
personType,
assignSource,
IFNULL(cancelled, 0) AS cancelled
@@ -762,7 +894,7 @@ public class ThirtyTeachTourUserAssignmentServiceImpl extends BaseServiceImpl<Th
}
@Override
public int backfillExistingAssignmentAfterSignup(String settingId, String matterId, String userId, String boardingPlace) {
public int backfillExistingAssignmentAfterSignup(String settingId, String matterId, String userId, String boardingPlace, String photoFiles) {
if (StrUtil.isBlank(settingId) || StrUtil.isBlank(matterId) || StrUtil.isBlank(userId)) {
return 0;
}
@@ -777,7 +909,8 @@ public class ThirtyTeachTourUserAssignmentServiceImpl extends BaseServiceImpl<Th
.add("lineName", matterInfo.getString("lineName"))
.add("travelAgencyId", matterInfo.getString("travelAgencyId"))
.add("travelAgencyName", matterInfo.getString("travelAgencyName"))
.add("boardingPlace", StrUtil.blankToDefault(boardingPlace, matterInfo.getString("defaultBoardingPlace"))),
.add("boardingPlace", StrUtil.blankToDefault(boardingPlace, matterInfo.getString("defaultBoardingPlace")))
.add("photoFiles", normalizePhotoFiles(photoFiles)),
Cnd.where(ThirtyTeachTourUserAssignment::getSettingId, "=", settingId)
.and(ThirtyTeachTourUserAssignment::getUserId, "=", userId)
.and(ThirtyTeachTourUserAssignment::getDelFlag, "=", false));
@@ -834,6 +967,24 @@ public class ThirtyTeachTourUserAssignmentServiceImpl extends BaseServiceImpl<Th
return assignmentDeleteInfo(fetchCurrentBranchAssignment(id));
}
@Override
public void updateSchoolAssignmentPhotoFiles(String id, String photoFiles) {
ThirtyTeachTourUserAssignment assignment = fetchAssignmentBySource(id, ThirtyTeachTourUserAssignment.ASSIGN_SOURCE_SCHOOL_UNION);
if (assignment == null) {
throw new IllegalArgumentException("人员分配记录不存在或无权限操作");
}
updateAssignmentPhotoFiles(assignment, photoFiles);
}
@Override
public void updateCurrentBranchAssignmentPhotoFiles(String id, String photoFiles) {
ThirtyTeachTourUserAssignment assignment = fetchCurrentBranchAssignment(id);
if (assignment == null) {
throw new IllegalArgumentException("人员分配记录不存在或无权限操作");
}
updateAssignmentPhotoFiles(assignment, photoFiles);
}
@Override
public void deleteBySource(String id, String assignSource) {
deleteBySource(id, assignSource, false);
@@ -997,7 +1148,11 @@ public class ThirtyTeachTourUserAssignmentServiceImpl extends BaseServiceImpl<Th
return listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql);
}
private Sql buildCandidateSql(String settingId) {
/**
* 构建人员分配候选人查询额外带出今年暑期疗休养和去年30年教龄疗休养参加标记
*/
private Sql buildCandidateSql(String settingId, Integer year) {
int currentYear = year == null ? LocalDate.now().getYear() : year;
return Sqls.create("""
SELECT
u.id AS userId,
@@ -1011,23 +1166,49 @@ public class ThirtyTeachTourUserAssignmentServiceImpl extends BaseServiceImpl<Th
u.unitid AS unitId,
u.unitname AS unitName,
u.unionid AS unionId,
u.unionname AS unionName
u.unionname AS unionName,
CASE WHEN IFNULL(summer_joined.joinedCount, 0) > 0 THEN 1 ELSE 0 END AS summerJoined,
CASE WHEN IFNULL(last_thirty_joined.joinedCount, 0) > 0 THEN 1 ELSE 0 END AS lastThirtyJoined
FROM activity_user_scope aus
LEFT JOIN vw_user u ON u.id = aus.userId
LEFT JOIN thirty_teach_tour_user_assignment a
ON a.settingId = @settingId
AND a.userId = aus.userId
AND a.delFlag = 0
LEFT JOIN (
SELECT jobNo, COUNT(1) AS joinedCount
FROM tour_ledger
WHERE delFlag = 0
AND joined = 1
AND `year` = @currentYear
GROUP BY jobNo
) summer_joined ON summer_joined.jobNo = u.loginname
LEFT JOIN (
SELECT jobNo, COUNT(1) AS joinedCount
FROM thirty_teach_tour_ledger
WHERE delFlag = 0
AND joined = 1
AND `year` = @lastThirtyYear
GROUP BY jobNo
) last_thirty_joined ON last_thirty_joined.jobNo = u.loginname
$condition
""").setParam("settingId", settingId);
""").setParam("settingId", settingId)
.setParam("currentYear", currentYear)
.setParam("lastThirtyYear", currentYear - 1);
}
private Cnd buildCandidateCnd(String settingId, String activityGroupId, String unionId, String keyword) {
/**
* 组装候选人过滤条件参游记录过滤为null时表示不限制该条件
*/
private Cnd buildCandidateCnd(String activityGroupId, String unionId, String keyword,
Boolean summerJoinedFilter, Boolean lastThirtyJoinedFilter) {
Cnd cnd = Cnd.NEW();
cnd.and("aus.groupId", "=", activityGroupId);
cnd.and("u.id", "is not", null);
cnd.and("a.id", "is", null);
cnd.andEX("u.unionid", "=", unionId);
appendJoinedFilter(cnd, "summer_joined.joinedCount", summerJoinedFilter);
appendJoinedFilter(cnd, "last_thirty_joined.joinedCount", lastThirtyJoinedFilter);
if (StrUtil.isNotBlank(keyword)) {
SqlExpressionGroup group = new SqlExpressionGroup();
group.orLike("u.username", keyword);
@@ -1037,6 +1218,16 @@ public class ThirtyTeachTourUserAssignmentServiceImpl extends BaseServiceImpl<Th
return cnd;
}
/**
* 按是否参加过对应疗休养活动追加过滤true查已参加false查未参加null不追加条件
*/
private void appendJoinedFilter(Cnd cnd, String countColumn, Boolean joinedFilter) {
if (joinedFilter == null) {
return;
}
cnd.and("IFNULL(" + countColumn + ", 0)", joinedFilter ? ">" : "=", 0);
}
private NutMap assignmentDeleteInfo(ThirtyTeachTourUserAssignment assignment) {
int ledgerCount = countAssignmentLedgers(assignment);
return NutMap.NEW()
@@ -1089,6 +1280,28 @@ public class ThirtyTeachTourUserAssignmentServiceImpl extends BaseServiceImpl<Th
.and(ThirtyTeachTourLedger::getDelFlag, "=", false));
}
/**
* 更新人员分配图片材料并同步同一事项和工号下已生成的报名台账
* 未选择线路或尚未生成台账时只更新人员分配表保证主列表上传不会强制产生台账数据
*/
private void updateAssignmentPhotoFiles(ThirtyTeachTourUserAssignment assignment, String photoFiles) {
if (assignment == null || StrUtil.isBlank(assignment.getId())) {
throw new IllegalArgumentException("人员分配记录不存在或无权限操作");
}
String normalizedPhotoFiles = normalizePhotoFiles(photoFiles);
update(Chain.make("photoFiles", normalizedPhotoFiles),
Cnd.where(ThirtyTeachTourUserAssignment::getId, "=", assignment.getId())
.and(ThirtyTeachTourUserAssignment::getDelFlag, "=", false));
if (StrUtil.isBlank(assignment.getMatterId()) || StrUtil.isBlank(assignment.getLoginName())) {
return;
}
dao().update(ThirtyTeachTourLedger.class,
Chain.make("photoFiles", normalizedPhotoFiles),
Cnd.where(ThirtyTeachTourLedger::getMatterId, "=", assignment.getMatterId())
.and(ThirtyTeachTourLedger::getJobNo, "=", assignment.getLoginName())
.and(ThirtyTeachTourLedger::getDelFlag, "=", false));
}
private void deleteAssignmentLedgers(ThirtyTeachTourUserAssignment assignment) {
List<ThirtyTeachTourLedger> ledgers = dao().query(ThirtyTeachTourLedger.class, Cnd.where(ThirtyTeachTourLedger::getMatterId, "=", assignment.getMatterId())
.and(ThirtyTeachTourLedger::getJobNo, "=", assignment.getLoginName())
@@ -1145,6 +1358,11 @@ public class ThirtyTeachTourUserAssignmentServiceImpl extends BaseServiceImpl<Th
String personType = normalizePersonType(assignment.getPersonType());
NutMap quotaInfo = branchQuotaInfo(assignment.getSettingId());
int remainingQuota = defaultInt(quotaInfo.getInt(remainingQuotaKey(personType)));
if (ThirtyTeachTourUserAssignment.PERSON_TYPE_FORMAL.equals(personType) && StrUtil.isNotBlank(assignment.getPeriodId())) {
// 恢复已退出正式人员时同步按该人员原时间段重新占用细分名额
NutMap periodQuotaInfo = branchPeriodQuotaInfo(assignment.getSettingId(), assignment.getPeriodId());
remainingQuota = defaultInt(periodQuotaInfo.getInt("formalRemaining"));
}
// 恢复已退出人员会重新占用当前人员类型名额因此恢复前必须按正式/替补剩余名额校验
if (remainingQuota <= 0) {
throw new IllegalArgumentException("当前分工会目标人员类型暂无剩余名额");
@@ -1332,6 +1550,8 @@ public class ThirtyTeachTourUserAssignmentServiceImpl extends BaseServiceImpl<Th
ThirtyTeachTourUserAssignment assignment = new ThirtyTeachTourUserAssignment();
assignment.setSettingId(settingId);
fillMatterSnapshot(assignment, matterInfo);
// 图片材料来自人员分配/报名提交快照不能放在事项快照方法中读取
assignment.setPhotoFiles(normalizePhotoFiles(user.getString("photoFiles")));
assignment.setUserId(user.getString("userId"));
assignment.setLoginName(user.getString("loginName"));
assignment.setUserName(user.getString("userName"));
@@ -1365,7 +1585,18 @@ public class ThirtyTeachTourUserAssignmentServiceImpl extends BaseServiceImpl<Th
assignment.setBoardingPlace(matterInfo.getString("defaultBoardingPlace"));
}
private void updateAssignmentMatterSnapshot(String id, NutMap matterInfo, String assignSource) {
/**
* 保存分工会正式人员时保留时间段快照后续名额使用统计直接按人员分配表 periodId 汇总
*/
private void fillTravelPeriodSnapshot(ThirtyTeachTourUserAssignment assignment, ThirtyTeachTourSettingTravelPeriod period) {
if (period == null) {
return;
}
assignment.setPeriodId(period.getId());
assignment.setPeriodName(period.getPeriodName());
}
private void updateAssignmentMatterSnapshot(String id, NutMap matterInfo, String assignSource, String photoFiles) {
// 补选事项时同时回写人员分配表事项快照后续列表筛选无需再跨表追溯事项信息
update(Chain.make("matterId", matterInfo.getString("matterId"))
.add("matterName", matterInfo.getString("matterName"))
@@ -1373,7 +1604,8 @@ public class ThirtyTeachTourUserAssignmentServiceImpl extends BaseServiceImpl<Th
.add("lineName", matterInfo.getString("lineName"))
.add("travelAgencyId", matterInfo.getString("travelAgencyId"))
.add("travelAgencyName", matterInfo.getString("travelAgencyName"))
.add("boardingPlace", matterInfo.getString("defaultBoardingPlace")),
.add("boardingPlace", matterInfo.getString("defaultBoardingPlace"))
.add("photoFiles", normalizePhotoFiles(photoFiles)),
Cnd.where(ThirtyTeachTourUserAssignment::getId, "=", id)
.and(ThirtyTeachTourUserAssignment::getAssignSource, "=", assignSource));
}
@@ -1484,7 +1716,9 @@ public class ThirtyTeachTourUserAssignmentServiceImpl extends BaseServiceImpl<Th
sql.setCondition(cnd);
List<NutMap> users = listMap(sql);
if (Lang.isNotEmpty(users)) {
return users.get(0);
NutMap user = users.get(0);
user.put("photoFiles", assignment.getPhotoFiles());
return user;
}
// 视图中找不到人员时使用分配表快照兜底保证补选事项不会因非关键字段缺失中断
return NutMap.NEW()
@@ -1496,7 +1730,8 @@ public class ThirtyTeachTourUserAssignmentServiceImpl extends BaseServiceImpl<Th
.addv("unitId", assignment.getUnitId())
.addv("unitName", assignment.getUnitName())
.addv("unionId", assignment.getUnionId())
.addv("unionName", assignment.getUnionName());
.addv("unionName", assignment.getUnionName())
.addv("photoFiles", assignment.getPhotoFiles());
}
private ThirtyTeachTourLedger buildProxySignupLedger(NutMap matterInfo, NutMap user) {
@@ -1518,6 +1753,7 @@ public class ThirtyTeachTourUserAssignmentServiceImpl extends BaseServiceImpl<Th
ledger.setTravelAgencyId(matterInfo.getString("travelAgencyId"));
ledger.setTravelAgencyName(matterInfo.getString("travelAgencyName"));
ledger.setBoardingPlace(matterInfo.getString("defaultBoardingPlace"));
ledger.setPhotoFiles(normalizePhotoFiles(user.getString("photoFiles")));
ledger.setHasFamily(false);
ledger.setJoined(false);
ledger.setReimbursed(false);
@@ -1575,6 +1811,33 @@ public class ThirtyTeachTourUserAssignmentServiceImpl extends BaseServiceImpl<Th
.collect(Collectors.toList());
}
/**
* 清洗多图上传返回值统一保存为逗号分隔路径避免空白项和重复路径写入快照表
*/
private String normalizePhotoFiles(String photoFiles) {
if (StrUtil.isBlank(photoFiles)) {
return "";
}
List<String> photos = StrUtil.splitTrim(photoFiles, ",");
if (Lang.isEmpty(photos)) {
return "";
}
return photos.stream()
.filter(StrUtil::isNotBlank)
.distinct()
.collect(Collectors.joining(","));
}
/**
* 批量人员共用同一组上传图片时将图片路径写入候选人快照后续构建分配表和台账时直接读取
*/
private void fillCandidatePhotoFiles(List<NutMap> candidateUsers, String photoFiles) {
if (Lang.isEmpty(candidateUsers)) {
return;
}
candidateUsers.forEach(user -> user.put("photoFiles", photoFiles));
}
private Pagination<NutMap> emptyPagination(PageForm pageForm) {
int pageNumber = pageForm == null || pageForm.getPageNumber() == null ? 1 : pageForm.getPageNumber();
int pageSize = pageForm == null || pageForm.getPageSize() == null ? 10 : pageForm.getPageSize();
@@ -1591,6 +1854,60 @@ public class ThirtyTeachTourUserAssignmentServiceImpl extends BaseServiceImpl<Th
.addv("backupRemaining", 0);
}
private NutMap emptyPeriodQuotaInfo(String periodId) {
return NutMap.NEW()
.addv("periodId", periodId)
.addv("formalQuota", 0)
.addv("formalUsed", 0)
.addv("formalRemaining", 0);
}
/**
* 正式分配只允许选择当前配置下已启用的时间段避免前端传入历史或其它配置的时间段ID
*/
private ThirtyTeachTourSettingTravelPeriod fetchEnabledTravelPeriod(String settingId, String periodId) {
if (StrUtil.isBlank(settingId) || StrUtil.isBlank(periodId)) {
return null;
}
return dao().fetch(ThirtyTeachTourSettingTravelPeriod.class,
Cnd.where("id", "=", periodId)
.and("settingId", "=", settingId)
.and("enabled", "=", true)
.and("delFlag", "=", false));
}
/**
* 保存正式人员前锁定当前分工会当前时间段的名额行使名额校验和人员插入在同一事务内串行执行
*/
private void lockBranchPeriodQuota(String settingId, String periodId, String unionId) {
Sql sql = Sqls.create("""
SELECT id
FROM thirty_teach_tour_setting_period_quota
WHERE settingId = @settingId
AND periodId = @periodId
AND unionId = @unionId
AND delFlag = 0
FOR UPDATE
""");
sql.setParam("settingId", settingId);
sql.setParam("periodId", periodId);
sql.setParam("unionId", unionId);
List<NutMap> rows = listMap(sql);
if (Lang.isEmpty(rows)) {
throw new IllegalArgumentException("当前分工会未配置该出行时间名额");
}
}
private String travelPeriodButtonLabel(ThirtyTeachTourSettingTravelPeriod period) {
if (period == null) {
return "";
}
if (StrUtil.isNotBlank(period.getStartDate()) && StrUtil.isNotBlank(period.getEndDate())) {
return period.getStartDate().replace("-", "") + "-" + period.getEndDate().replace("-", "");
}
return StrUtil.blankToDefault(period.getPeriodName(), "");
}
private String remainingQuotaKey(String personType) {
return ThirtyTeachTourUserAssignment.PERSON_TYPE_BACKUP.equals(personType) ? "backupRemaining" : "formalRemaining";
}
@@ -0,0 +1,7 @@
-- 30年教龄疗休养报名图片材料字段。
-- PC端选择线路、移动端提交报名上传的多张图片统一保存为逗号分隔路径。
ALTER TABLE `thirty_teach_tour_user_assignment`
ADD COLUMN `photoFiles` text DEFAULT NULL COMMENT '报名图片材料' AFTER `boardingPlace`;
ALTER TABLE `thirty_teach_tour_ledger`
ADD COLUMN `photoFiles` text DEFAULT NULL COMMENT '报名图片材料' AFTER `boardingPlace`;
@@ -22,6 +22,7 @@ CREATE TABLE IF NOT EXISTS `thirty_teach_tour_ledger` (
`travelAgencyId` varchar(32) DEFAULT NULL COMMENT '旅行社ID',
`travelAgencyName` varchar(100) DEFAULT NULL COMMENT '旅行社',
`boardingPlace` varchar(100) DEFAULT NULL COMMENT '乘车地点',
`photoFiles` text DEFAULT NULL COMMENT '报名图片材料',
`hasFamily` tinyint(1) DEFAULT 0 COMMENT '是否携带家属',
`intendedRoommate` varchar(100) DEFAULT NULL COMMENT '意向拼床人',
`bedType` varchar(50) DEFAULT NULL COMMENT '床型',
@@ -74,6 +74,42 @@ CREATE TABLE IF NOT EXISTS `thirty_teach_tour_setting_union_quota` (
KEY `idx_thirty_teach_tour_setting_union_quota_union` (`unionId`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='疗休养配置分工会名额分配';
CREATE TABLE IF NOT EXISTS `thirty_teach_tour_setting_travel_period` (
`id` varchar(32) NOT NULL COMMENT 'ID',
`settingId` varchar(32) DEFAULT NULL COMMENT '所属疗休养配置ID',
`periodName` varchar(50) DEFAULT NULL COMMENT '时间段名称',
`startDate` varchar(10) DEFAULT NULL COMMENT '开始日期',
`endDate` varchar(10) DEFAULT NULL COMMENT '结束日期',
`enabled` tinyint(1) DEFAULT 1 COMMENT '是否启用',
`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`),
KEY `idx_thirty_teach_tour_setting_period_setting` (`settingId`),
KEY `idx_thirty_teach_tour_setting_period_date` (`startDate`, `endDate`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='30年教龄疗休养配置时间段';
CREATE TABLE IF NOT EXISTS `thirty_teach_tour_setting_period_quota` (
`id` varchar(32) NOT NULL COMMENT 'ID',
`settingId` varchar(32) DEFAULT NULL COMMENT '所属疗休养配置ID',
`unionId` varchar(32) DEFAULT NULL COMMENT '分工会ID',
`unionName` varchar(100) DEFAULT NULL COMMENT '分工会名称',
`periodId` varchar(32) DEFAULT NULL COMMENT '时间段ID',
`periodName` varchar(50) DEFAULT NULL COMMENT '时间段名称',
`formalQuota` int DEFAULT 0 COMMENT '正式人员名额',
`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`),
KEY `idx_thirty_teach_tour_period_quota_setting` (`settingId`),
KEY `idx_thirty_teach_tour_period_quota_union` (`unionId`),
KEY `idx_thirty_teach_tour_period_quota_period` (`periodId`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='30年教龄疗休养配置时间段名额';
-- 疗休养人员分配表。
CREATE TABLE IF NOT EXISTS `thirty_teach_tour_user_assignment` (
`id` varchar(32) NOT NULL COMMENT 'ID',
@@ -85,6 +121,8 @@ CREATE TABLE IF NOT EXISTS `thirty_teach_tour_user_assignment` (
`lineId` varchar(32) DEFAULT NULL COMMENT '线路ID',
`lineName` varchar(100) DEFAULT NULL COMMENT '线路名称',
`boardingPlace` varchar(100) DEFAULT NULL COMMENT '乘车地点',
`periodId` varchar(32) DEFAULT NULL COMMENT '出行时间段ID',
`periodName` varchar(50) DEFAULT NULL COMMENT '出行时间段名称',
`userId` varchar(32) DEFAULT NULL COMMENT '人员ID',
`loginName` varchar(50) DEFAULT NULL COMMENT '工号',
`userName` varchar(100) DEFAULT NULL COMMENT '姓名',
@@ -112,6 +150,7 @@ CREATE TABLE IF NOT EXISTS `thirty_teach_tour_user_assignment` (
KEY `idx_thirty_teach_tour_user_assignment_setting` (`settingId`),
KEY `idx_thirty_teach_tour_user_assignment_user` (`userId`),
KEY `idx_thirty_teach_tour_user_assignment_union` (`unionId`),
KEY `idx_thirty_teach_tour_user_assignment_period` (`periodId`),
KEY `idx_thirty_teach_tour_user_assignment_source` (`assignSource`),
KEY `idx_thirty_teach_tour_user_assignment_person_type` (`personType`),
KEY `idx_thirty_teach_tour_user_assignment_cancelled` (`cancelled`)
@@ -0,0 +1,35 @@
CREATE TABLE IF NOT EXISTS `thirty_teach_tour_setting_travel_period` (
`id` varchar(32) NOT NULL COMMENT 'ID',
`settingId` varchar(32) DEFAULT NULL COMMENT '所属疗休养配置ID',
`periodName` varchar(50) DEFAULT NULL COMMENT '时间段名称',
`startDate` varchar(10) DEFAULT NULL COMMENT '开始日期',
`endDate` varchar(10) DEFAULT NULL COMMENT '结束日期',
`enabled` tinyint(1) DEFAULT 1 COMMENT '是否启用',
`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`),
KEY `idx_thirty_teach_tour_setting_period_setting` (`settingId`),
KEY `idx_thirty_teach_tour_setting_period_date` (`startDate`, `endDate`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='30年教龄疗休养配置时间段';
CREATE TABLE IF NOT EXISTS `thirty_teach_tour_setting_period_quota` (
`id` varchar(32) NOT NULL COMMENT 'ID',
`settingId` varchar(32) DEFAULT NULL COMMENT '所属疗休养配置ID',
`unionId` varchar(32) DEFAULT NULL COMMENT '分工会ID',
`unionName` varchar(100) DEFAULT NULL COMMENT '分工会名称',
`periodId` varchar(32) DEFAULT NULL COMMENT '时间段ID',
`periodName` varchar(50) DEFAULT NULL COMMENT '时间段名称',
`formalQuota` int DEFAULT 0 COMMENT '正式人员名额',
`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`),
KEY `idx_thirty_teach_tour_period_quota_setting` (`settingId`),
KEY `idx_thirty_teach_tour_period_quota_union` (`unionId`),
KEY `idx_thirty_teach_tour_period_quota_period` (`periodId`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='30年教龄疗休养配置时间段名额';
@@ -0,0 +1,5 @@
-- 分工会正式人员分配按配置时间段占用名额,人员分配表保留所选时间段快照。
ALTER TABLE `thirty_teach_tour_user_assignment`
ADD COLUMN `periodId` varchar(32) DEFAULT NULL COMMENT '出行时间段ID' AFTER `boardingPlace`,
ADD COLUMN `periodName` varchar(50) DEFAULT NULL COMMENT '出行时间段名称' AFTER `periodId`,
ADD KEY `idx_thirty_teach_tour_user_assignment_period` (`periodId`);
@@ -9,6 +9,9 @@ CREATE TABLE IF NOT EXISTS `thirty_teach_tour_user_assignment` (
`lineId` varchar(32) DEFAULT NULL COMMENT '线路ID',
`lineName` varchar(100) DEFAULT NULL COMMENT '线路名称',
`boardingPlace` varchar(100) DEFAULT NULL COMMENT '乘车地点',
`photoFiles` text DEFAULT NULL COMMENT '报名图片材料',
`periodId` varchar(32) DEFAULT NULL COMMENT '出行时间段ID',
`periodName` varchar(50) DEFAULT NULL COMMENT '出行时间段名称',
`userId` varchar(32) DEFAULT NULL COMMENT '人员ID',
`loginName` varchar(50) DEFAULT NULL COMMENT '工号',
`userName` varchar(100) DEFAULT NULL COMMENT '姓名',
@@ -36,6 +39,7 @@ CREATE TABLE IF NOT EXISTS `thirty_teach_tour_user_assignment` (
KEY `idx_thirty_teach_tour_user_assignment_setting` (`settingId`),
KEY `idx_thirty_teach_tour_user_assignment_user` (`userId`),
KEY `idx_thirty_teach_tour_user_assignment_union` (`unionId`),
KEY `idx_thirty_teach_tour_user_assignment_period` (`periodId`),
KEY `idx_thirty_teach_tour_user_assignment_source` (`assignSource`),
KEY `idx_thirty_teach_tour_user_assignment_person_type` (`personType`),
KEY `idx_thirty_teach_tour_user_assignment_cancelled` (`cancelled`)
@@ -69,7 +69,13 @@ layout("/layouts/platform.html"){
<el-table-column label="所在单位" prop="unitName" min-width="180" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="线路" prop="lineName" min-width="200" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="出行时间" prop="travelPeriod" min-width="180" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="出行时间" prop="travelPeriod" min-width="180" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="乘车地点" prop="boardingPlace" min-width="140" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="已上传图片数量" width="140" align="center" header-align="center">
<template slot-scope="{row}">
{{ photoFileCount(row) }}
</template>
</el-table-column>
<el-table-column label="人员类型" prop="personType" width="110" sortable="custom" align="center" header-align="center">
<template slot-scope="{row}">
<el-tag size="mini" :type="row.personType === 'BACKUP' ? 'warning' : 'success'">{{ personTypeText(row.personType) }}</el-tag>
@@ -81,9 +87,10 @@ layout("/layouts/platform.html"){
</template>
</el-table-column>
<el-table-column label="分配时间" prop="assignedAt" width="170" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="操作" width="380" align="center" header-align="center" fixed="right">
<el-table-column label="操作" width="470" align="center" header-align="center" fixed="right">
<template slot-scope="{row}">
<el-button v-if="!row.matterId && row.personType !== 'BACKUP' && $auth.hasPermission('thirtyTeachTour.branchUserAssignment.selectMatter')" size="mini" type="primary" @click="openSelectMatter(row)">选择路线</el-button>
<el-button v-if="$auth.hasPermission('thirtyTeachTour.branchUserAssignment.assign')" size="mini" type="primary" @click="openListPhotoUpload(row)">{{ row.photoFiles ? "已上传" : "上传图片" }}</el-button>
<el-button v-if="$auth.hasPermission('thirtyTeachTour.branchUserAssignment.switchPersonType')" size="mini" type="primary" :loading="personTypeSwitching === row.id" @click="switchPersonType(row, row.personType === 'BACKUP' ? 'FORMAL' : 'BACKUP')">{{ row.personType === 'BACKUP' ? '转为正式' : '转为替补' }}</el-button>
<el-button v-if="isCancelled(row) && $auth.hasPermission('thirtyTeachTour.branchUserAssignment.cancelRestore')" size="mini" type="warning" @click="restoreCancel(row)">取消退出</el-button>
<el-button v-if="$auth.hasPermission('thirtyTeachTour.branchUserAssignment.delete')" size="mini" type="danger" @click="doDelete(row)">删除</el-button>
@@ -129,7 +136,7 @@ layout("/layouts/platform.html"){
</el-select>
</el-form-item>
</el-col>
<el-col>
<el-col :span="6">
<el-form-item label="人员类型" prop="personType">
<el-radio-group v-model="assignForm.personType" @change="assignPersonTypeChange">
<el-radio-button label="FORMAL">正式人员</el-radio-button>
@@ -137,7 +144,22 @@ layout("/layouts/platform.html"){
</el-radio-group>
</el-form-item>
</el-col>
</el-row>
<el-col :span="18" v-if="assignForm.personType === 'FORMAL'">
<el-form-item label="出行时间">
<div class="period-button-list">
<el-button
v-for="item in assignPeriodOptions"
:key="item.id"
size="mini"
:type="assignForm.periodId === item.id ? 'primary' : 'default'"
@click="toggleAssignPeriod(item)">
{{ item.label || item.periodName }}
</el-button>
<span v-if="assignPeriodOptions.length === 0" class="period-empty">暂无启用时间段</span>
</div>
</el-form-item>
</el-col>
</el-row>
</el-form>
<div class="quota-bar">
@@ -190,6 +212,13 @@ layout("/layouts/platform.html"){
<el-table-column label="手机号" prop="mobile" width="130" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="所属分工会" prop="unionName" min-width="160" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="所在单位" prop="unitName" min-width="220" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="材料" width="110" align="center" header-align="center">
<template slot-scope="{row}">
<el-button type="text" size="mini" :disabled="!isCandidateSelected(row)" @click="openPhotoUpload(row)">
{{ row.photoFiles ? "已上传" : "上传" }}
</el-button>
</template>
</el-table-column>
</el-table>
<el-row class="el-pagination-container candidate-pagination">
<el-pagination
@@ -209,6 +238,64 @@ layout("/layouts/platform.html"){
</span>
</el-dialog>
<el-dialog
title="上传图片材料"
:visible.sync="photoUploadDialogVisible"
:close-on-click-modal="false"
width="520px"
@closed="resetPhotoUploadDialog">
<el-form :model="photoUploadForm" label-width="100px">
<el-form-item label="人员">
<el-input v-model="photoUploadForm.userName" disabled></el-input>
</el-form-item>
<el-form-item label="图片材料">
<file-upload
style="--upload-width: 96px;--upload-height:96px"
:upload_number="9"
:upload_size="20971520"
:value.sync="photoUploadForm.photoFiles"
accept=".jpg,.jpeg,.png"
complete_result
upload_mode="image"
upload_result_category="interval">
</file-upload>
</el-form-item>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="photoUploadDialogVisible = false">取消</el-button>
<el-button type="primary" @click="confirmPhotoUpload">确定</el-button>
</span>
</el-dialog>
<el-dialog
title="上传图片材料"
:visible.sync="listPhotoDialogVisible"
:close-on-click-modal="false"
width="520px"
@closed="resetListPhotoUploadDialog">
<el-form :model="listPhotoForm" label-width="100px">
<el-form-item label="人员">
<el-input v-model="listPhotoForm.userName" disabled></el-input>
</el-form-item>
<el-form-item label="图片材料">
<file-upload
style="--upload-width: 96px;--upload-height:96px"
:upload_number="9"
:upload_size="20971520"
:value.sync="listPhotoForm.photoFiles"
accept=".jpg,.jpeg,.png"
complete_result
upload_mode="image"
upload_result_category="interval">
</file-upload>
</el-form-item>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="listPhotoDialogVisible = false">取消</el-button>
<el-button type="primary" :loading="listPhotoSubmitting" @click="saveListPhotoFiles">保存</el-button>
</span>
</el-dialog>
<el-dialog
title="选择分配路线"
:visible.sync="selectMatterDialogVisible"
@@ -224,6 +311,18 @@ layout("/layouts/platform.html"){
<el-option v-for="item in selectMatterOptions" :key="item.matterId" :label="matterOptionLabel(item)" :value="item.matterId"></el-option>
</el-select>
</el-form-item>
<el-form-item label="图片材料">
<file-upload
style="--upload-width: 96px;--upload-height:96px"
:upload_number="9"
:upload_size="20971520"
:value.sync="selectMatterForm.photoFiles"
accept=".jpg,.jpeg,.png"
complete_result
upload_mode="image"
upload_result_category="interval">
</file-upload>
</el-form-item>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="selectMatterDialogVisible = false">取消</el-button>
@@ -272,6 +371,24 @@ layout("/layouts/platform.html"){
color: #606266;
white-space: nowrap;
}
.period-button-list {
display: flex;
flex-wrap: wrap;
gap: 8px;
min-height: 32px;
align-items: center;
}
.period-button-list .el-button + .el-button {
margin-left: 0;
}
.period-empty {
color: #909399;
font-size: 13px;
}
.candidate-pagination {
margin-top: 12px;
margin-bottom: 0;
@@ -311,6 +428,7 @@ layout("/layouts/platform.html"){
pageMatterOptions: [],
assignSettingOptions: [],
assignMatterOptions: [],
assignPeriodOptions: [],
quotaInfo: {
formalQuota: 0,
backupQuota: 0,
@@ -327,9 +445,24 @@ layout("/layouts/platform.html"){
selectedCandidates: [],
selectedCandidateIds: [],
assignSubmitting: false,
photoUploadDialogVisible: false,
photoUploadRow: null,
photoUploadForm: {
userId: "",
userName: "",
photoFiles: ""
},
selectMatterDialogVisible: false,
selectMatterSubmitting: false,
selectMatterOptions: [],
listPhotoDialogVisible: false,
listPhotoSubmitting: false,
listPhotoRow: null,
listPhotoForm: {
id: "",
userName: "",
photoFiles: ""
},
personTypeSwitching: "",
pageForm: {
pageNumber: 1,
@@ -347,6 +480,7 @@ layout("/layouts/platform.html"){
year: currentYear,
settingId: "",
matterId: "",
periodId: "",
personType: "FORMAL"
},
candidateForm: {
@@ -359,7 +493,8 @@ layout("/layouts/platform.html"){
id: "",
settingId: "",
matterId: "",
userName: ""
userName: "",
photoFiles: ""
},
assignRules: {
year: [{required: true, message: "请选择年度", trigger: ["change", "blur"]}],
@@ -377,6 +512,7 @@ layout("/layouts/platform.html"){
year: currentYear || moment().format("YYYY"),
settingId: "",
matterId: "",
periodId: "",
personType: "FORMAL"
}
},
@@ -403,7 +539,8 @@ layout("/layouts/platform.html"){
id: "",
settingId: "",
matterId: "",
userName: ""
userName: "",
photoFiles: ""
}
},
resetSearch() {
@@ -466,6 +603,7 @@ layout("/layouts/platform.html"){
const year = this.pageForm.year || moment().format("YYYY")
this.assignForm = this.defaultAssignForm(year)
this.candidateForm = this.defaultCandidateForm()
this.assignPeriodOptions = []
this.quotaInfo = this.defaultQuotaInfo()
this.candidateData = []
this.selectedCandidates = []
@@ -478,6 +616,7 @@ layout("/layouts/platform.html"){
this.assignForm = this.defaultAssignForm(moment().format("YYYY"))
this.candidateForm = this.defaultCandidateForm()
this.assignMatterOptions = []
this.assignPeriodOptions = []
this.quotaInfo = this.defaultQuotaInfo()
this.candidateData = []
this.selectedCandidates = []
@@ -488,7 +627,9 @@ layout("/layouts/platform.html"){
assignYearChange() {
this.assignForm.settingId = ""
this.assignForm.matterId = ""
this.assignForm.periodId = ""
this.assignMatterOptions = []
this.assignPeriodOptions = []
this.quotaInfo = this.defaultQuotaInfo()
this.selectedCandidateIds = []
this.candidateUserOptions = []
@@ -498,19 +639,22 @@ layout("/layouts/platform.html"){
},
assignSettingChange() {
this.assignForm.matterId = ""
this.assignForm.periodId = ""
this.assignMatterOptions = []
this.assignPeriodOptions = []
this.selectedCandidateIds = []
this.candidateUserOptions = []
this.candidateForm.keyword = ""
this.clearCandidateSelection()
this.loadAssignMatterOptions()
this.loadQuotaInfo()
this.loadAssignTravelPeriods(true)
this.loadCandidatePageData()
},
assignPersonTypeChange() {
if (this.assignForm.personType === "BACKUP") {
this.assignForm.matterId = ""
}
this.loadQuotaInfo()
},
loadAssignSettingOptions() {
this.$axios.post(loc() + "/settingOptions", {year: this.assignForm.year}).then((res) => {
@@ -520,14 +664,15 @@ layout("/layouts/platform.html"){
if (!this.assignForm.settingId && this.assignSettingOptions.length > 0) {
this.assignForm.settingId = this.assignSettingOptions[0].id
this.loadAssignMatterOptions()
this.loadQuotaInfo()
this.loadAssignTravelPeriods(true)
this.loadCandidatePageData()
} else if (this.assignForm.settingId) {
this.loadAssignMatterOptions()
this.loadQuotaInfo()
this.loadAssignTravelPeriods(true)
this.loadCandidatePageData()
} else {
this.assignMatterOptions = []
this.assignPeriodOptions = []
this.quotaInfo = this.defaultQuotaInfo()
this.candidateData = []
this.candidateForm.totalCount = 0
@@ -535,6 +680,29 @@ layout("/layouts/platform.html"){
}
})
},
loadAssignTravelPeriods(defaultFirst) {
if (!this.assignForm.settingId) {
this.assignPeriodOptions = []
this.assignForm.periodId = ""
this.loadQuotaInfo()
return
}
this.$axios.post(loc() + "/travelPeriodOptions", {settingId: this.assignForm.settingId}).then((res) => {
if (res.code === 0) {
this.assignPeriodOptions = res.data || []
const hasCurrent = this.assignPeriodOptions.some(item => item.id === this.assignForm.periodId)
if ((defaultFirst || !hasCurrent) && this.assignPeriodOptions.length > 0) {
this.assignForm.periodId = this.assignPeriodOptions[0].id
} else if (!hasCurrent) {
this.assignForm.periodId = ""
}
} else {
this.assignPeriodOptions = []
this.assignForm.periodId = ""
}
this.loadQuotaInfo()
})
},
loadAssignMatterOptions() {
if (!this.assignForm.settingId) {
this.assignMatterOptions = []
@@ -553,10 +721,41 @@ layout("/layouts/platform.html"){
}
this.$axios.post(loc() + "/quotaInfo", {settingId: this.assignForm.settingId}).then((res) => {
if (res.code === 0) {
this.quotaInfo = Object.assign(this.defaultQuotaInfo(), res.data || {})
const baseQuota = Object.assign(this.defaultQuotaInfo(), res.data || {})
if (this.assignForm.personType === "FORMAL") {
baseQuota.formalQuota = 0
baseQuota.formalUsed = 0
baseQuota.formalRemaining = 0
}
this.quotaInfo = baseQuota
if (this.assignForm.personType === "FORMAL" && this.assignForm.periodId) {
this.loadPeriodQuotaInfo()
}
}
})
},
loadPeriodQuotaInfo() {
this.$axios.post(loc() + "/periodQuotaInfo", {
settingId: this.assignForm.settingId,
periodId: this.assignForm.periodId
}).then((res) => {
if (res.code === 0) {
const periodQuota = res.data || {}
this.quotaInfo = Object.assign({}, this.quotaInfo, {
formalQuota: periodQuota.formalQuota || 0,
formalUsed: periodQuota.formalUsed || 0,
formalRemaining: periodQuota.formalRemaining || 0
})
}
})
},
toggleAssignPeriod(item) {
this.assignForm.periodId = this.assignForm.periodId === item.id ? "" : item.id
this.candidateForm.pageNumber = 1
this.clearCandidateSelection()
this.loadQuotaInfo()
this.loadCandidatePageData()
},
loadCandidatePageData() {
if (!this.assignForm.settingId) {
this.candidateData = []
@@ -573,7 +772,9 @@ layout("/layouts/platform.html"){
}).then((res) => {
if (res.code === 0) {
const data = res.data || {}
this.candidateData = data.list || []
this.candidateData = (data.list || []).map(item => Object.assign({}, item, {
photoFiles: ""
}))
this.candidateForm.totalCount = data.totalCount || 0
this.mergeCandidateOptions(this.candidateData)
} else {
@@ -654,6 +855,80 @@ layout("/layouts/platform.html"){
})
return Object.keys(map).map(key => map[key])
},
isCandidateSelected(row) {
return !!row && this.selectedCandidates.some(item => item.userId === row.userId)
},
openPhotoUpload(row) {
if (!this.isCandidateSelected(row)) {
this.$message.warning("请先勾选人员")
return
}
this.photoUploadRow = row
// 行级图片只是暂存到候选人员对象中,再次打开时从当前行回显。
this.photoUploadForm = {
userId: row.userId || "",
userName: row.userName || "",
photoFiles: row.photoFiles || ""
}
this.photoUploadDialogVisible = true
},
confirmPhotoUpload() {
if (this.photoUploadRow) {
this.$set(this.photoUploadRow, "photoFiles", this.photoUploadForm.photoFiles || "")
}
this.photoUploadDialogVisible = false
},
resetPhotoUploadDialog() {
this.photoUploadRow = null
this.photoUploadForm = {
userId: "",
userName: "",
photoFiles: ""
}
},
openListPhotoUpload(row) {
this.listPhotoRow = row
// 主列表上传读取当前行 photoFiles;人员分配弹窗已保存过图片时可直接回显。
this.listPhotoForm = {
id: row.id || "",
userName: row.userName || "",
photoFiles: row.photoFiles || ""
}
this.listPhotoDialogVisible = true
},
resetListPhotoUploadDialog() {
this.listPhotoSubmitting = false
this.listPhotoRow = null
this.listPhotoForm = {
id: "",
userName: "",
photoFiles: ""
}
},
saveListPhotoFiles() {
if (!this.listPhotoForm.id) {
this.$message.warning("人员分配记录不存在")
return
}
this.listPhotoSubmitting = true
this.$axios.post(loc() + "/updatePhotoFiles", {
id: this.listPhotoForm.id,
photoFiles: this.listPhotoForm.photoFiles || ""
}).then((res) => {
if (res.code === 0) {
if (this.listPhotoRow) {
this.$set(this.listPhotoRow, "photoFiles", this.listPhotoForm.photoFiles || "")
}
this.$message.success("图片材料保存成功")
this.listPhotoDialogVisible = false
this.doSearch()
} else {
this.$message.warning(res.msg || "图片材料保存失败")
}
}).finally(() => {
this.listPhotoSubmitting = false
})
},
clearCandidateSelection() {
this.selectedCandidates = []
if (this.$refs.candidateTable) {
@@ -667,18 +942,28 @@ layout("/layouts/platform.html"){
this.$message.warning("请选择需要分配的人员")
return
}
if (this.assignForm.personType === "FORMAL" && !this.assignForm.periodId) {
this.$message.warning("请选择出行时间")
return
}
this.$confirm("确定保存当前人员分配吗?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}).then(() => {
const userIds = this.selectedCandidates.map(item => item.userId)
const assignItems = this.selectedCandidates.map(item => {
return {
userId: item.userId,
photoFiles: item.photoFiles || ""
}
})
this.assignSubmitting = true
this.$axios.post(loc() + "/doAssign", {
settingId: this.assignForm.settingId,
matterId: this.assignForm.personType === "BACKUP" ? "" : this.assignForm.matterId,
periodId: this.assignForm.personType === "BACKUP" ? "" : this.assignForm.periodId,
personType: this.assignForm.personType,
userIds: JSON.stringify(userIds)
assignItems: JSON.stringify(assignItems)
}).then((res) => {
if (res.code === 0) {
const data = res.data || {}
@@ -715,7 +1000,8 @@ layout("/layouts/platform.html"){
id: row.id,
settingId: row.settingId,
matterId: "",
userName: row.userName || ""
userName: row.userName || "",
photoFiles: ""
}
this.selectMatterOptions = []
this.selectMatterDialogVisible = true
@@ -743,7 +1029,8 @@ layout("/layouts/platform.html"){
this.selectMatterSubmitting = true
this.$axios.post(loc() + "/selectMatter", {
id: this.selectMatterForm.id,
matterId: this.selectMatterForm.matterId
matterId: this.selectMatterForm.matterId,
photoFiles: this.selectMatterForm.photoFiles
}).then((res) => {
if (res.code === 0) {
const data = res.data || {}
@@ -792,6 +1079,12 @@ layout("/layouts/platform.html"){
isCancelled(row) {
return row && (row.cancelled === true || row.cancelled === 1)
},
photoFileCount(row) {
if (!row || !row.photoFiles) {
return 0
}
return String(row.photoFiles).split(",").map(item => item.trim()).filter(item => item).length
},
restoreCancel(row) {
this.$confirm("确定将【" + row.userName + "】恢复为未退出状态吗?", "提示", {
confirmButtonText: "确定",
@@ -88,6 +88,11 @@ layout("/layouts/platform.html"){
<el-table-column label="线路" prop="lineName" min-width="200" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="出行时间" prop="travelPeriod" min-width="180" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="乘车地点" prop="boardingPlace" min-width="140" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="已上传图片数量" width="140" align="center" header-align="center">
<template slot-scope="{row}">
{{ photoFileCount(row) }}
</template>
</el-table-column>
<el-table-column label="分配类别" prop="assignSource" width="120" sortable="custom" align="center" header-align="center">
<template slot-scope="{row}">
<el-tag size="mini" :type="row.assignSource === 'BRANCH_UNION' ? 'warning' : 'primary'">{{ assignSourceText(row.assignSource) }}</el-tag>
@@ -104,9 +109,10 @@ layout("/layouts/platform.html"){
</template>
</el-table-column>
<el-table-column label="分配时间" prop="assignedAt" width="170" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="操作" width="320" align="center" header-align="center" fixed="right">
<el-table-column label="操作" width="410" align="center" header-align="center" fixed="right">
<template slot-scope="{row}">
<el-button v-if="isSchoolUnionAssignment(row) && !row.matterId && row.personType !== 'BACKUP'" size="mini" type="primary" @click="openSelectMatter(row)">选择线路</el-button>
<el-button v-if="isSchoolUnionAssignment(row)" size="mini" type="primary" @click="openListPhotoUpload(row)">{{ row.photoFiles ? "已上传" : "上传图片" }}</el-button>
<el-button v-if="isSchoolUnionAssignment(row) && isCancelled(row) && $auth.hasPermission('thirtyTeachTour.schoolUserAssignment.cancelRestore')" size="mini" type="warning" @click="restoreCancel(row)">取消退出</el-button>
<el-button v-if="isSchoolUnionAssignment(row)" size="mini" type="danger" @click="doDelete(row)">删除</el-button>
</template>
@@ -189,6 +195,14 @@ layout("/layouts/platform.html"){
<el-select v-model="candidateForm.unionId" clearable filterable placeholder="所属分工会" style="width: 220px" @change="candidateSearch">
<el-option v-for="item in unionOptions" :key="item.id" :label="item.name" :value="item.id"></el-option>
</el-select>
<el-select v-model="candidateForm.summerJoined" clearable placeholder="今年是否已参加暑期疗休养" style="width: 230px" @change="candidateSearch">
<el-option label="否" value="false"></el-option>
<el-option label="是" value="true"></el-option>
</el-select>
<el-select v-model="candidateForm.lastThirtyJoined" clearable placeholder="去年是否参加30年教龄疗休养" style="width: 250px" @change="candidateSearch">
<el-option label="否" value="false"></el-option>
<el-option label="是" value="true"></el-option>
</el-select>
<el-select
v-model="selectedCandidateIds"
multiple
@@ -238,6 +252,13 @@ layout("/layouts/platform.html"){
</el-select>
</template>
</el-table-column>
<el-table-column label="材料" width="110" align="center" header-align="center">
<template slot-scope="{row}">
<el-button type="text" size="mini" :disabled="!isCandidateSelected(row)" @click="openPhotoUpload(row)">
{{ row.photoFiles ? "已上传" : "上传" }}
</el-button>
</template>
</el-table-column>
</el-table>
<el-row class="el-pagination-container candidate-pagination">
<el-pagination
@@ -257,6 +278,64 @@ layout("/layouts/platform.html"){
</span>
</el-dialog>
<el-dialog
title="上传图片材料"
:visible.sync="photoUploadDialogVisible"
:close-on-click-modal="false"
width="520px"
@closed="resetPhotoUploadDialog">
<el-form :model="photoUploadForm" label-width="100px">
<el-form-item label="人员">
<el-input v-model="photoUploadForm.userName" disabled></el-input>
</el-form-item>
<el-form-item label="图片材料">
<file-upload
style="--upload-width: 96px;--upload-height:96px"
:upload_number="9"
:upload_size="20971520"
:value.sync="photoUploadForm.photoFiles"
accept=".jpg,.jpeg,.png"
complete_result
upload_mode="image"
upload_result_category="interval">
</file-upload>
</el-form-item>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="photoUploadDialogVisible = false">取消</el-button>
<el-button type="primary" @click="confirmPhotoUpload">确定</el-button>
</span>
</el-dialog>
<el-dialog
title="上传图片材料"
:visible.sync="listPhotoDialogVisible"
:close-on-click-modal="false"
width="520px"
@closed="resetListPhotoUploadDialog">
<el-form :model="listPhotoForm" label-width="100px">
<el-form-item label="人员">
<el-input v-model="listPhotoForm.userName" disabled></el-input>
</el-form-item>
<el-form-item label="图片材料">
<file-upload
style="--upload-width: 96px;--upload-height:96px"
:upload_number="9"
:upload_size="20971520"
:value.sync="listPhotoForm.photoFiles"
accept=".jpg,.jpeg,.png"
complete_result
upload_mode="image"
upload_result_category="interval">
</file-upload>
</el-form-item>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="listPhotoDialogVisible = false">取消</el-button>
<el-button type="primary" :loading="listPhotoSubmitting" @click="saveListPhotoFiles">保存</el-button>
</span>
</el-dialog>
<el-dialog
title="选择分配线路"
:visible.sync="selectMatterDialogVisible"
@@ -272,6 +351,18 @@ layout("/layouts/platform.html"){
<el-option v-for="item in selectMatterOptions" :key="item.matterId" :label="matterOptionLabel(item)" :value="item.matterId"></el-option>
</el-select>
</el-form-item>
<el-form-item label="图片材料">
<file-upload
style="--upload-width: 96px;--upload-height:96px"
:upload_number="9"
:upload_size="20971520"
:value.sync="selectMatterForm.photoFiles"
accept=".jpg,.jpeg,.png"
complete_result
upload_mode="image"
upload_result_category="interval">
</file-upload>
</el-form-item>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="selectMatterDialogVisible = false">取消</el-button>
@@ -350,9 +441,24 @@ layout("/layouts/platform.html"){
selectedCandidates: [],
selectedCandidateIds: [],
assignSubmitting: false,
photoUploadDialogVisible: false,
photoUploadRow: null,
photoUploadForm: {
userId: "",
userName: "",
photoFiles: ""
},
selectMatterDialogVisible: false,
selectMatterSubmitting: false,
selectMatterOptions: [],
listPhotoDialogVisible: false,
listPhotoSubmitting: false,
listPhotoRow: null,
listPhotoForm: {
id: "",
userName: "",
photoFiles: ""
},
pageForm: {
pageNumber: 1,
pageSize: 10,
@@ -378,13 +484,16 @@ layout("/layouts/platform.html"){
pageSize: 10,
totalCount: 0,
unionId: "",
summerJoined: "false",
lastThirtyJoined: "false",
keyword: ""
},
selectMatterForm: {
id: "",
settingId: "",
matterId: "",
userName: ""
userName: "",
photoFiles: ""
},
remindForm: {
content: ""
@@ -415,6 +524,8 @@ layout("/layouts/platform.html"){
pageSize: 10,
totalCount: 0,
unionId: "",
summerJoined: "false",
lastThirtyJoined: "false",
keyword: ""
}
},
@@ -423,7 +534,8 @@ layout("/layouts/platform.html"){
id: "",
settingId: "",
matterId: "",
userName: ""
userName: "",
photoFiles: ""
}
},
defaultRemindForm() {
@@ -649,13 +761,16 @@ layout("/layouts/platform.html"){
pageSize: this.candidateForm.pageSize,
settingId: this.assignForm.settingId,
unionId: this.candidateForm.unionId,
summerJoined: this.candidateForm.summerJoined,
lastThirtyJoined: this.candidateForm.lastThirtyJoined,
keyword: (this.selectedCandidateIds || []).length > 0 ? "" : this.candidateForm.keyword,
userIds: JSON.stringify(this.selectedCandidateIds || [])
}).then((res) => {
if (res.code === 0) {
const data = res.data || {}
this.candidateData = (data.list || []).map(item => Object.assign({}, item, {
assignmentMatterId: ""
assignmentMatterId: "",
photoFiles: ""
}))
this.candidateForm.totalCount = data.totalCount || 0
this.mergeCandidateOptions(this.candidateData)
@@ -678,6 +793,8 @@ layout("/layouts/platform.html"){
},
resetCandidateSearch() {
this.candidateForm.unionId = ""
this.candidateForm.summerJoined = "false"
this.candidateForm.lastThirtyJoined = "false"
this.candidateForm.keyword = ""
this.candidateForm.pageNumber = 1
this.selectedCandidateIds = []
@@ -723,6 +840,8 @@ layout("/layouts/platform.html"){
pageSize: 20,
settingId: this.assignForm.settingId,
unionId: this.candidateForm.unionId,
summerJoined: this.candidateForm.summerJoined,
lastThirtyJoined: this.candidateForm.lastThirtyJoined,
keyword: keyword || ""
}).then((res) => {
if (res.code === 0) {
@@ -758,6 +877,77 @@ layout("/layouts/platform.html"){
isCandidateSelected(row) {
return !!row && this.selectedCandidates.some(item => item.userId === row.userId)
},
openPhotoUpload(row) {
if (!this.isCandidateSelected(row)) {
this.$message.warning("请先勾选人员")
return
}
this.photoUploadRow = row
// 行级图片只是暂存到候选人员对象中,再次打开时从当前行回显。
this.photoUploadForm = {
userId: row.userId || "",
userName: row.userName || "",
photoFiles: row.photoFiles || ""
}
this.photoUploadDialogVisible = true
},
confirmPhotoUpload() {
if (this.photoUploadRow) {
this.$set(this.photoUploadRow, "photoFiles", this.photoUploadForm.photoFiles || "")
}
this.photoUploadDialogVisible = false
},
resetPhotoUploadDialog() {
this.photoUploadRow = null
this.photoUploadForm = {
userId: "",
userName: "",
photoFiles: ""
}
},
openListPhotoUpload(row) {
this.listPhotoRow = row
// 主列表上传读取当前行 photoFiles;人员分配弹窗已保存过图片时可直接回显。
this.listPhotoForm = {
id: row.id || "",
userName: row.userName || "",
photoFiles: row.photoFiles || ""
}
this.listPhotoDialogVisible = true
},
resetListPhotoUploadDialog() {
this.listPhotoSubmitting = false
this.listPhotoRow = null
this.listPhotoForm = {
id: "",
userName: "",
photoFiles: ""
}
},
saveListPhotoFiles() {
if (!this.listPhotoForm.id) {
this.$message.warning("人员分配记录不存在")
return
}
this.listPhotoSubmitting = true
this.$axios.post(loc() + "/updatePhotoFiles", {
id: this.listPhotoForm.id,
photoFiles: this.listPhotoForm.photoFiles || ""
}).then((res) => {
if (res.code === 0) {
if (this.listPhotoRow) {
this.$set(this.listPhotoRow, "photoFiles", this.listPhotoForm.photoFiles || "")
}
this.$message.success("图片材料保存成功")
this.listPhotoDialogVisible = false
this.doSearch()
} else {
this.$message.warning(res.msg || "图片材料保存失败")
}
}).finally(() => {
this.listPhotoSubmitting = false
})
},
clearCandidateSelection() {
this.selectedCandidates = []
if (this.$refs.candidateTable) {
@@ -779,7 +969,8 @@ layout("/layouts/platform.html"){
const assignItems = this.selectedCandidates.map(item => {
return {
userId: item.userId,
matterId: item.assignmentMatterId || this.assignForm.matterId || ""
matterId: item.assignmentMatterId || this.assignForm.matterId || "",
photoFiles: item.photoFiles || ""
}
})
this.assignSubmitting = true
@@ -806,7 +997,8 @@ layout("/layouts/platform.html"){
id: row.id,
settingId: row.settingId,
matterId: "",
userName: row.userName || ""
userName: row.userName || "",
photoFiles: ""
}
this.selectMatterOptions = []
this.selectMatterDialogVisible = true
@@ -834,7 +1026,8 @@ layout("/layouts/platform.html"){
this.selectMatterSubmitting = true
this.$axios.post(loc() + "/selectMatter", {
id: this.selectMatterForm.id,
matterId: this.selectMatterForm.matterId
matterId: this.selectMatterForm.matterId,
photoFiles: this.selectMatterForm.photoFiles
}).then((res) => {
if (res.code === 0) {
const data = res.data || {}
@@ -895,6 +1088,12 @@ layout("/layouts/platform.html"){
isCancelled(row) {
return row && (row.cancelled === true || row.cancelled === 1)
},
photoFileCount(row) {
if (!row || !row.photoFiles) {
return 0
}
return String(row.photoFiles).split(",").map(item => item.trim()).filter(item => item).length
},
restoreCancel(row) {
this.$confirm("确定将【" + row.userName + "】恢复为未退出状态吗?", "提示", {
confirmButtonText: "确定",
@@ -347,7 +347,7 @@ layout("/layouts/platform.html"){
<strong>{{ branchTotalQuota }}</strong>
</div>
<div class="tour-quota-stat">
<span>分工会总会员</span>
<span>分工会可参加总人</span>
<strong>{{ branchTotalMemberCount }}</strong>
</div>
<el-input-number
@@ -371,7 +371,7 @@ layout("/layouts/platform.html"){
style="width: 100%">
<el-table-column label="序号" type="index" width="70" align="center" header-align="center"></el-table-column>
<el-table-column label="分工会" prop="unionName" min-width="220" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="当前会员数" prop="memberCount" width="120" align="center" header-align="center"></el-table-column>
<el-table-column label="可参加人数" prop="memberCount" width="120" align="center" header-align="center"></el-table-column>
<el-table-column label="正式人员数量" width="170" align="center" header-align="center">
<template slot-scope="{row}">
<el-input-number v-model="row.formalQuota" :controls="false" :min="0" :precision="0" style="width: 100%"></el-input-number>
@@ -385,6 +385,120 @@ layout("/layouts/platform.html"){
</el-table>
</div>
</el-tab-pane>
<el-tab-pane label="时间段管理" name="travelPeriod">
<div class="tour-tab-fill">
<el-alert
title="时间段用于划分报名的时间范围,保存后将应用于时间段名额分配。"
type="info"
show-icon
:closable="false"
class="tour-tab-alert">
</el-alert>
<div class="tour-tab-toolbar tour-tab-toolbar-left">
<el-button type="primary" size="medium" icon="el-icon-plus" @click="openPeriodDialog()">新增时间段</el-button>
</div>
<el-table
:data="formData.travelPeriods"
border
class="vi-table tour-tab-table"
empty-text="暂无时间段"
height="100%"
style="width: 100%">
<el-table-column label="序号" type="index" width="70" align="center" header-align="center"></el-table-column>
<el-table-column label="时间段名称" prop="periodName" min-width="180" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="开始日期" prop="startDate" width="150" align="center" header-align="center"></el-table-column>
<el-table-column label="结束日期" prop="endDate" width="150" align="center" header-align="center"></el-table-column>
<el-table-column label="状态" prop="enabled" width="100" align="center" header-align="center">
<template slot-scope="{row}">
<el-tag size="mini" type="success" v-if="row.enabled">启用</el-tag>
<el-tag size="mini" type="info" v-else>禁用</el-tag>
</template>
</el-table-column>
<el-table-column label="创建时间" prop="createdAt" width="170" align="center" header-align="center">
<template slot-scope="{row}">
{{ formatPeriodCreatedAt(row.createdAt) }}
</template>
</el-table-column>
<el-table-column label="操作" width="130" align="center" header-align="center">
<template slot-scope="scope">
<el-button type="text" size="mini" @click="openPeriodDialog(scope.row, scope.$index)">编辑</el-button>
<el-button type="text" size="mini" class="text-danger" @click="deleteTravelPeriod(scope.$index, scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
</div>
</el-tab-pane>
<el-tab-pane label="时间段名额分配" name="periodQuota">
<div class="tour-tab-fill">
<el-alert
title="按时间段分配各分工会的可用名额,所有时间段名额之和不能超过分工会的正式名额。"
type="info"
show-icon
:closable="false"
class="tour-tab-alert">
</el-alert>
<div class="tour-period-quota-toolbar">
<el-select v-model="periodQuotaUnionId" clearable filterable placeholder="全部分工会" size="medium" class="tour-period-quota-union">
<el-option
v-for="item in formData.unionQuotas"
:key="item.unionId"
:label="item.unionName"
:value="item.unionId">
</el-option>
</el-select>
<el-date-picker
v-model="periodQuotaDateRange"
type="daterange"
value-format="yyyy-MM-dd"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
size="medium">
</el-date-picker>
<el-button type="primary" size="medium" @click="checkPeriodQuotaMessage">批量检查</el-button>
</div>
<el-table
:data="periodQuotaMatrixRows"
border
class="vi-table tour-tab-table tour-period-quota-table"
empty-text="暂无分工会名额"
height="100%"
show-summary
:summary-method="periodQuotaSummary"
style="width: 100%">
<el-table-column label="序号" type="index" width="70" align="center" header-align="center"></el-table-column>
<el-table-column label="分工会" prop="unionName" min-width="200" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="总名额" prop="formalQuota" width="110" align="center" header-align="center"></el-table-column>
<el-table-column label="时间段名额分配(可用名额)" align="center" header-align="center">
<el-table-column
v-for="period in periodQuotaDisplayPeriods"
:key="period.id"
:label="period.startDate + ' ~ ' + period.endDate"
min-width="230"
align="center"
header-align="center">
<template slot-scope="{row}">
<div class="tour-period-quota-cell">
<el-input
:value="periodQuotaValue(row.unionId, period.id)"
maxlength="10"
@input="setPeriodQuotaValue(row, period, $event)">
</el-input>
<span>可用:{{ periodQuotaRemaining(row.unionId) }}</span>
</div>
</template>
</el-table-column>
</el-table-column>
<el-table-column label="已分配合计" width="150" align="center" header-align="center">
<template slot-scope="{row}">
<span :class="periodQuotaRowTotal(row.unionId) > toNonNegativeInteger(row.formalQuota) ? 'text-danger' : 'text-success'">
{{ periodQuotaRowTotal(row.unionId) }} / {{ toNonNegativeInteger(row.formalQuota) }}
</span>
</template>
</el-table-column>
</el-table>
</div>
</el-tab-pane>
<el-tab-pane label="服务须知" name="notice">
<el-form-item prop="serviceNotice" label-width="0">
<text-editor v-model="formData.serviceNotice"></text-editor>
@@ -397,6 +511,46 @@ layout("/layouts/platform.html"){
<el-button type="primary" :loading="submitLoading" @click="doSubmit">确认</el-button>
</span>
</el-dialog>
<el-dialog
:title="periodDialogTitle"
:visible.sync="periodDialogVisible"
:close-on-click-modal="false"
width="520px"
append-to-body>
<el-form :model="periodForm" :rules="periodRules" label-width="110px" ref="periodForm">
<el-form-item label="时间段名称" prop="periodName">
<el-input v-model="periodForm.periodName" maxlength="20" show-word-limit placeholder="请输入时间段名称"></el-input>
</el-form-item>
<el-form-item label="开始日期" prop="startDate">
<el-date-picker
v-model="periodForm.startDate"
type="date"
value-format="yyyy-MM-dd"
placeholder="请选择开始日期"
style="width: 100%">
</el-date-picker>
</el-form-item>
<el-form-item label="结束日期" prop="endDate">
<el-date-picker
v-model="periodForm.endDate"
type="date"
value-format="yyyy-MM-dd"
placeholder="请选择结束日期"
style="width: 100%">
</el-date-picker>
</el-form-item>
<el-form-item label="状态" prop="enabled">
<el-radio-group v-model="periodForm.enabled">
<el-radio :label="true">启用</el-radio>
<el-radio :label="false">禁用</el-radio>
</el-radio-group>
</el-form-item>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="periodDialogVisible = false">取消</el-button>
<el-button type="primary" @click="savePeriodDialog">确定</el-button>
</span>
</el-dialog>
<drawer-user-scope
ref="drawerUserScope"
:group_id.sync="formData.activityGroupId"
@@ -456,6 +610,18 @@ layout("/layouts/platform.html"){
quotaAllocate: {
ratio: null
},
periodQuotaUnionId: "",
periodQuotaDateRange: [],
periodDialogVisible: false,
periodDialogTitle: "新增时间段",
periodEditIndex: -1,
periodForm: {
id: "",
periodName: "",
startDate: "",
endDate: "",
enabled: true
},
inheritLoading: false,
formRules: {
year: [{required: true, message: "必填", trigger: ["blur", "change"]}],
@@ -466,6 +632,11 @@ layout("/layouts/platform.html"){
outProvinceFixedPeople: [{validator: checkOutProvinceFixedPeople, trigger: ["blur", "change"]}],
cycleStartYear: [{validator: checkCycleYear, trigger: ["blur", "change"]}],
cycleEndYear: [{validator: checkCycleYear, trigger: ["blur", "change"]}]
},
periodRules: {
periodName: [{required: true, message: "必填", trigger: ["blur", "change"]}],
startDate: [{required: true, message: "必填", trigger: ["blur", "change"]}],
endDate: [{required: true, message: "必填", trigger: ["blur", "change"]}]
}
}
},
@@ -483,11 +654,48 @@ layout("/layouts/platform.html"){
branchTotalQuota() {
const quota = this.travelPeopleQuota - this.toNonNegativeInteger(this.quotaOverview.schoolFormalAssignedCount)
return quota > 0 ? quota : 0
},
enabledTravelPeriods() {
const rows = this.formData.travelPeriods || []
return rows.filter((row) => {
return row && row.id && row.enabled
}).sort((a, b) => {
return String(a.startDate || "").localeCompare(String(b.startDate || ""))
})
},
periodQuotaDisplayPeriods() {
const range = this.periodQuotaDateRange || []
return this.enabledTravelPeriods.filter((period) => {
if (!range.length || !range[0] || !range[1]) {
return true
}
return String(period.endDate || "") >= range[0] && String(period.startDate || "") <= range[1]
})
},
periodQuotaMatrixRows() {
const rows = this.formData.unionQuotas || []
return rows.filter((row) => {
return !this.periodQuotaUnionId || row.unionId === this.periodQuotaUnionId
}).map((row) => {
return Object.assign({}, row, {
formalQuota: this.toNonNegativeInteger(row.formalQuota)
})
})
}
},
watch: {
"formData.travelPeopleQuota": function() {
this.refreshQuotaRatio()
},
"formData.year": function() {
if (this.dialogVisible) {
this.reloadUnionQuotaRows()
}
},
"formData.activityGroupId": function() {
if (this.dialogVisible) {
this.reloadUnionQuotaRows()
}
}
},
methods: {
@@ -502,15 +710,22 @@ layout("/layouts/platform.html"){
})
},
loadUnionQuotaRows(settingId) {
this.$axios.post(loc() + "/unionQuotaRows", {settingId: settingId || ""}).then((res) => {
this.$axios.post(loc() + "/unionQuotaRows", {
settingId: settingId || "",
year: this.formData.year || "",
activityGroupId: this.formData.activityGroupId || ""
}).then((res) => {
if (res.code === 0) {
this.$set(this.formData, "unionQuotas", this.normalizeUnionQuotaRows(res.data || []))
this.$set(this.formData, "unionQuotas", this.mergeUnionQuotaRows(res.data || []))
this.refreshQuotaRatio()
} else {
this.$message.warning(res.msg || "查询分工会名额失败")
}
})
},
reloadUnionQuotaRows() {
this.loadUnionQuotaRows(this.formData.id || "")
},
loadUnionQuotaOverview(settingId) {
this.$axios.post(loc() + "/unionQuotaOverview", {settingId: settingId || ""}).then((res) => {
if (res.code === 0) {
@@ -531,6 +746,50 @@ layout("/layouts/platform.html"){
})
})
},
normalizeTravelPeriods(rows) {
return (rows || []).map((item) => {
return Object.assign({}, item, {
id: item.id || this.newClientId(),
periodName: item.periodName || "",
startDate: item.startDate || "",
endDate: item.endDate || "",
enabled: item.enabled === undefined || item.enabled === null ? true : !!item.enabled
})
})
},
normalizePeriodQuotas(rows) {
return (rows || []).map((item) => {
return Object.assign({}, item, {
formalQuota: this.toNonNegativeInteger(item.formalQuota)
})
}).filter((item) => {
return item.unionId && item.periodId
})
},
mergeUnionQuotaRows(rows) {
const currentRows = this.formData.unionQuotas || []
const currentQuotaMap = {}
currentRows.forEach((row) => {
if (row && row.unionId) {
currentQuotaMap[row.unionId] = row
}
})
// 刷新可参加人数时只更新统计口径,保留页面上已继承或已录入的正式/替补名额。
return this.normalizeUnionQuotaRows(rows).map((row) => {
const current = currentQuotaMap[row.unionId]
if (!current) {
return row
}
return Object.assign({}, row, {
formalQuota: this.toNonNegativeInteger(current.formalQuota),
backupQuota: this.toNonNegativeInteger(current.backupQuota)
})
})
},
newClientId() {
const source = String(Date.now()) + String(Math.floor(Math.random() * 100000000000000000))
return (source + "00000000000000000000000000000000").substring(0, 32)
},
toNonNegativeInteger(value) {
const numberValue = parseInt(value, 10)
if (isNaN(numberValue) || numberValue < 0) {
@@ -538,6 +797,148 @@ layout("/layouts/platform.html"){
}
return numberValue
},
formatPeriodCreatedAt(value) {
if (!value) {
return ""
}
if (String(value).length === 13) {
return moment(Number(value)).format("YYYY-MM-DD HH:mm:ss")
}
if (String(value).length === 10) {
return moment(Number(value) * 1000).format("YYYY-MM-DD HH:mm:ss")
}
return value
},
openPeriodDialog(row, index) {
this.periodDialogTitle = row ? "编辑时间段" : "新增时间段"
this.periodEditIndex = row ? index : -1
this.periodForm = Object.assign({
id: this.newClientId(),
periodName: "",
startDate: "",
endDate: "",
enabled: true
}, row || {})
this.periodDialogVisible = true
this.$nextTick(() => this.$refs.periodForm && this.$refs.periodForm.clearValidate())
},
savePeriodDialog() {
this.$refs.periodForm.validate((valid) => {
if (!valid) return
if (this.periodForm.startDate > this.periodForm.endDate) {
this.$message.warning("开始日期不能大于结束日期")
return
}
if (!this.formData.travelPeriods) {
this.$set(this.formData, "travelPeriods", [])
}
const row = Object.assign({}, this.periodForm, {
id: this.periodForm.id || this.newClientId(),
enabled: !!this.periodForm.enabled
})
if (this.periodEditIndex >= 0) {
this.$set(this.formData.travelPeriods, this.periodEditIndex, row)
} else {
this.formData.travelPeriods.push(row)
}
this.periodDialogVisible = false
})
},
deleteTravelPeriod(index, row) {
this.$confirm("确定删除该时间段吗?删除后对应的时间段名额也会移除。", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}).then(() => {
this.formData.travelPeriods.splice(index, 1)
this.formData.periodQuotas = (this.formData.periodQuotas || []).filter((item) => {
return item.periodId !== row.id
})
})
},
periodQuotaKey(unionId, periodId) {
return String(unionId || "") + "_" + String(periodId || "")
},
findPeriodQuota(unionId, periodId) {
const key = this.periodQuotaKey(unionId, periodId)
const rows = this.formData.periodQuotas || []
return rows.find((item) => {
return this.periodQuotaKey(item.unionId, item.periodId) === key
})
},
periodQuotaValue(unionId, periodId) {
const row = this.findPeriodQuota(unionId, periodId)
return row ? this.toNonNegativeInteger(row.formalQuota) : 0
},
periodEnabledMap() {
const result = {}
;(this.formData.travelPeriods || []).forEach((period) => {
if (period && period.id && period.enabled) {
result[period.id] = true
}
})
return result
},
setPeriodQuotaValue(row, period, value) {
const nextValue = this.toNonNegativeInteger(String(value || "").replace(/[^\d]/g, ""))
if (!this.formData.periodQuotas) {
this.$set(this.formData, "periodQuotas", [])
}
const existing = this.findPeriodQuota(row.unionId, period.id)
if (existing) {
this.$set(existing, "formalQuota", nextValue)
this.$set(existing, "periodName", period.periodName || "")
this.$set(existing, "unionName", row.unionName || "")
} else {
this.formData.periodQuotas.push({
unionId: row.unionId,
unionName: row.unionName,
periodId: period.id,
periodName: period.periodName,
formalQuota: nextValue
})
}
},
periodQuotaRowTotal(unionId) {
const rows = this.formData.periodQuotas || []
const periodMap = this.periodEnabledMap()
return rows.filter((item) => {
return item.unionId === unionId && periodMap[item.periodId]
}).reduce((sum, item) => {
return sum + this.toNonNegativeInteger(item.formalQuota)
}, 0)
},
periodQuotaRemaining(unionId) {
const unionRow = (this.formData.unionQuotas || []).find((item) => item.unionId === unionId)
const formalQuota = unionRow ? this.toNonNegativeInteger(unionRow.formalQuota) : 0
const remaining = formalQuota - this.periodQuotaRowTotal(unionId)
return remaining > 0 ? remaining : 0
},
buildPeriodQuotasForSubmit() {
const periodMap = {}
;(this.formData.travelPeriods || []).forEach((period) => {
if (period && period.id && period.enabled) {
periodMap[period.id] = period
}
})
const unionMap = {}
;(this.formData.unionQuotas || []).forEach((row) => {
if (row && row.unionId) {
unionMap[row.unionId] = row
}
})
return (this.formData.periodQuotas || []).filter((item) => {
return item && item.unionId && item.periodId && periodMap[item.periodId] && this.toNonNegativeInteger(item.formalQuota) > 0
}).map((item) => {
const period = periodMap[item.periodId] || {}
const unionRow = unionMap[item.unionId] || {}
return Object.assign({}, item, {
unionName: unionRow.unionName || item.unionName || "",
periodName: period.periodName || item.periodName || "",
formalQuota: this.toNonNegativeInteger(item.formalQuota)
})
})
},
toNumber(value) {
const numberValue = parseFloat(value)
return isNaN(numberValue) ? null : numberValue
@@ -572,6 +973,33 @@ layout("/layouts/platform.html"){
return ""
})
},
periodQuotaSummary(param) {
const columns = param.columns || []
const rows = this.periodQuotaMatrixRows || []
return columns.map((column, index) => {
if (index === 0) {
return "合计"
}
if (column.property === "formalQuota") {
return rows.reduce((sum, row) => sum + this.toNonNegativeInteger(row.formalQuota), 0)
}
if (index >= 3 && index < 3 + this.periodQuotaDisplayPeriods.length) {
const period = this.periodQuotaDisplayPeriods[index - 3]
return rows.reduce((sum, row) => sum + this.periodQuotaValue(row.unionId, period.id), 0)
}
if (column.label === "已分配合计") {
const total = rows.reduce((sum, row) => sum + this.periodQuotaRowTotal(row.unionId), 0)
const quota = rows.reduce((sum, row) => sum + this.toNonNegativeInteger(row.formalQuota), 0)
return total + " / " + quota
}
return ""
})
},
checkPeriodQuotaMessage() {
if (this.validatePeriodQuotas()) {
this.$message.success("时间段名额校验通过")
}
},
allocateQuotaByRatio() {
const rows = this.formData.unionQuotas || []
const ratio = this.toNumber(this.quotaAllocate.ratio)
@@ -587,10 +1015,11 @@ layout("/layouts/platform.html"){
this.$message.warning("比例不能为负数")
return
}
// 按比例分配:比例 * 分工会会员数,小数直接舍去,不补余数。
// 按比例分配:比例 * 分工会可参加人数,小数直接舍去;单个分工会结果不能超过本分工会可参加人数。
const nextFormalQuotas = rows.map(row => {
const memberCount = this.toNonNegativeInteger(row.memberCount)
return Math.floor(ratio * memberCount)
const formalQuota = Math.floor(ratio * memberCount)
return Math.min(formalQuota, memberCount)
})
const formalTotal = nextFormalQuotas.reduce((sum, value) => sum + value, 0)
if (formalTotal > this.branchTotalQuota) {
@@ -607,11 +1036,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 = ""
@@ -645,6 +1074,8 @@ layout("/layouts/platform.html"){
homeSignupEntryImage: "",
lots: [],
unionQuotas: [],
travelPeriods: [],
periodQuotas: [],
serviceNotice: ""
}
},
@@ -655,6 +1086,8 @@ layout("/layouts/platform.html"){
// 新建时给出默认年度和开关值,减少校工会管理员录入成本。
this.formData = this.defaultFormData()
this.boardingPlaceRows = []
this.periodQuotaUnionId = ""
this.periodQuotaDateRange = []
this.quotaOverview.schoolFormalAssignedCount = 0
this.dialogVisible = true
this.loadUnionQuotaRows("")
@@ -689,12 +1122,36 @@ layout("/layouts/platform.html"){
settingId: ""
})
})
const periodIdMap = {}
const inheritedTravelPeriods = this.normalizeTravelPeriods(previous.travelPeriods || []).map((item) => {
const newId = this.newClientId()
periodIdMap[item.id] = newId
return Object.assign({}, item, {
id: newId,
settingId: "",
createdAt: "",
createdBy: "",
updatedAt: "",
updatedBy: ""
})
})
const inheritedPeriodQuotas = this.normalizePeriodQuotas(previous.periodQuotas || []).filter((item) => {
return !!periodIdMap[item.periodId]
}).map((item) => {
return Object.assign({}, item, {
id: "",
settingId: "",
periodId: periodIdMap[item.periodId]
})
})
this.formData = Object.assign(this.defaultFormData(), previous, {
year: String(currentYear),
cycleStartYear: previous.cycleStartYear ? String(previous.cycleStartYear) : "",
cycleEndYear: previous.cycleEndYear ? String(previous.cycleEndYear) : "",
lots: inheritedLots,
unionQuotas: inheritedUnionQuotas
unionQuotas: inheritedUnionQuotas,
travelPeriods: inheritedTravelPeriods,
periodQuotas: inheritedPeriodQuotas
})
this.boardingPlaceRows = this.parseBoardingPlaceRows(this.formData.boardingPlace)
delete this.formData.id
@@ -719,6 +1176,8 @@ layout("/layouts/platform.html"){
this.$set(this.formData, "homeSignupEntryEnabled", false)
this.$set(this.formData, "homeSignupEntryImage", "")
this.lotDeleteList = []
this.periodQuotaUnionId = ""
this.periodQuotaDateRange = []
this.$message.success("已延用上一年配置信息")
this.$nextTick(() => this.$refs.form && this.$refs.form.clearValidate())
}).catch(() => {
@@ -749,6 +1208,8 @@ layout("/layouts/platform.html"){
homeSignupEntryImage: "",
lots: [],
unionQuotas: [],
travelPeriods: [],
periodQuotas: [],
serviceNotice: ""
}, res.data || {})
this.boardingPlaceRows = this.parseBoardingPlaceRows(this.formData.boardingPlace)
@@ -779,6 +1240,10 @@ layout("/layouts/platform.html"){
allowOverReimbursement: !!item.allowOverReimbursement
}))
this.formData.unionQuotas = this.normalizeUnionQuotaRows(this.formData.unionQuotas || [])
this.formData.travelPeriods = this.normalizeTravelPeriods(this.formData.travelPeriods || [])
this.formData.periodQuotas = this.normalizePeriodQuotas(this.formData.periodQuotas || [])
this.periodQuotaUnionId = ""
this.periodQuotaDateRange = []
this.loadUnionQuotaOverview(row.id)
this.dialogVisible = true
this.$nextTick(() => this.$refs.form && this.$refs.form.clearValidate())
@@ -792,6 +1257,8 @@ layout("/layouts/platform.html"){
if (!valid) return
if (!this.validateLots()) return
if (!this.validateUnionQuotas()) return
if (!this.validateTravelPeriods()) return
if (!this.validatePeriodQuotas()) return
this.submitLoading = true
// Nutz 对子表集合按字符串化 JSON 绑定更稳定,和体检项目维护的提交方式保持一致。
const submitData = JSON.parse(JSON.stringify(this.formData))
@@ -808,6 +1275,8 @@ layout("/layouts/platform.html"){
}
submitData.lots = JSON.stringify(this.formData.lots || [])
submitData.unionQuotas = JSON.stringify(this.formData.unionQuotas || [])
submitData.travelPeriods = JSON.stringify(this.formData.travelPeriods || [])
submitData.periodQuotas = JSON.stringify(this.buildPeriodQuotasForSubmit())
submitData.lotDeleteList = JSON.stringify(this.lotDeleteList)
this.$axios.post(loc() + "/doSubmit", submitData).then((res) => {
this.submitLoading = false
@@ -955,6 +1424,39 @@ layout("/layouts/platform.html"){
}
return true
},
validateTravelPeriods() {
const rows = this.formData.travelPeriods || []
for (let i = 0; i < rows.length; i++) {
const row = rows[i]
const rowNo = "第" + (i + 1) + "行"
if (!row.periodName) {
this.$message.warning(rowNo + "时间段名称不能为空")
return false
}
if (!row.startDate || !row.endDate) {
this.$message.warning(rowNo + "开始日期和结束日期不能为空")
return false
}
if (row.startDate > row.endDate) {
this.$message.warning(rowNo + "开始日期不能大于结束日期")
return false
}
}
return true
},
validatePeriodQuotas() {
const rows = this.formData.unionQuotas || []
for (let i = 0; i < rows.length; i++) {
const row = rows[i]
const formalQuota = this.toNonNegativeInteger(row.formalQuota)
const usedQuota = this.periodQuotaRowTotal(row.unionId)
if (usedQuota > formalQuota) {
this.$message.warning("分工会【" + row.unionName + "】时间段名额合计 " + usedQuota + ",不能大于正式名额 " + formalQuota)
return false
}
}
return true
},
deleteLot(index, row) {
this.$confirm("确定删除该标段吗?", "提示", {
confirmButtonText: "确定",
@@ -993,6 +1495,17 @@ layout("/layouts/platform.html"){
this.quotaOverview = {
schoolFormalAssignedCount: 0
}
this.periodQuotaUnionId = ""
this.periodQuotaDateRange = []
this.periodDialogVisible = false
this.periodEditIndex = -1
this.periodForm = {
id: "",
periodName: "",
startDate: "",
endDate: "",
enabled: true
}
this.inheritLoading = false
this.activeTab = "basic"
}
@@ -1094,11 +1607,56 @@ layout("/layouts/platform.html"){
text-align: right;
}
.tour-tab-toolbar-left {
text-align: left;
}
.tour-tab-alert {
flex-shrink: 0;
margin-bottom: 10px;
}
.tour-tab-table {
flex: 1;
min-height: 0;
}
.tour-period-quota-toolbar {
align-items: center;
display: flex;
flex-shrink: 0;
gap: 10px;
margin-bottom: 10px;
}
.tour-period-quota-union {
width: 180px;
}
.tour-period-quota-cell {
align-items: center;
display: flex;
gap: 8px;
justify-content: center;
}
.tour-period-quota-cell .el-input {
width: 120px;
}
.tour-period-quota-cell span {
color: #606266;
white-space: nowrap;
}
.text-danger {
color: #f56c6c;
}
.text-success {
color: #67c23a;
}
.tour-setting-dialog {
height: 85vh;
max-height: 85vh;
@@ -183,6 +183,20 @@ layout("/layouts/platform.html"){
<el-input v-model="signupForm.travelAgencyName" :disabled="isDirectFamilyLine(signupForm)" readonly></el-input>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="图片材料">
<file-upload
style="--upload-width: 96px;--upload-height:96px"
:upload_number="9"
:upload_size="20971520"
:value.sync="signupForm.photoFiles"
accept=".jpg,.jpeg,.png"
complete_result
upload_mode="image"
upload_result_category="interval">
</file-upload>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="8">
@@ -867,6 +881,7 @@ layout("/layouts/platform.html"){
travelAgencyId: matter.travelAgencyId || "",
travelAgencyName: matter.travelAgencyName || "",
boardingPlace: matter.defaultBoardingPlace || "",
photoFiles: "",
travelPeriod: matter.travelPeriod || "",
travelStartTime: matter.travelStartTime || "",
travelEndTime: matter.travelEndTime || "",
@@ -886,6 +901,7 @@ layout("/layouts/platform.html"){
travelAgencyId: matter.travelAgencyId || ledger.travelAgencyId || "",
travelAgencyName: matter.travelAgencyName || ledger.travelAgencyName || "",
boardingPlace: ledger.boardingPlace || matter.defaultBoardingPlace || "",
photoFiles: ledger.photoFiles || "",
travelPeriod: matter.travelPeriod || "",
travelStartTime: matter.travelStartTime || "",
travelEndTime: matter.travelEndTime || "",
@@ -129,7 +129,7 @@ layout("/layouts/platform.html"){
</el-select>
</el-form-item>
</el-col>
<el-col>
<el-col :span="6">
<el-form-item label="人员类型" prop="personType">
<el-radio-group v-model="assignForm.personType" @change="assignPersonTypeChange">
<el-radio-button label="FORMAL">正式人员</el-radio-button>
@@ -115,6 +115,17 @@ layout("/layouts/platform_h5.html"){
font-size: 13px;
text-align: center;
}
.tour-upload-field {
padding: 12px 16px 14px;
}
.tour-upload-label {
margin-bottom: 8px;
color: #323233;
font-size: 14px;
line-height: 20px;
}
</style>
<div id="app">
@@ -162,6 +173,18 @@ layout("/layouts/platform_h5.html"){
</van-field>
<van-field v-if="fillBedInfo" label="床位信息" v-model="signupForm.bedInfo" maxlength="100" placeholder="请输入床位数量"></van-field>
<van-field v-if="fillBedInfo" label="意向拼床人" v-model="signupForm.intendedRoommate" maxlength="100" placeholder="请输入意向拼床人"></van-field>
<div class="tour-upload-field">
<div class="tour-upload-label">图片材料</div>
<h5-file-upload
:upload_number="9"
:upload_size="20971520"
:value.sync="signupForm.photoFiles"
accept=".jpg,.jpeg,.png"
complete_result
upload_mode="image"
upload_result_category="array">
</h5-file-upload>
</div>
<van-field v-if="canApplyOverReimbursement(signupForm)" class="tour-over-cost" label="报销超出费用">
<template #input>
<van-radio-group v-model="signupForm.overCostReimbursed" direction="horizontal">
@@ -411,6 +434,7 @@ layout("/layouts/platform_h5.html"){
travelAgencyId: matter.travelAgencyId || "",
travelAgencyName: matter.travelAgencyName || "",
boardingPlace: "",
photoFiles: "",
travelPeriod: matter.travelPeriod || "",
travelStartTime: matter.travelStartTime || "",
travelEndTime: matter.travelEndTime || "",
@@ -430,6 +454,7 @@ layout("/layouts/platform_h5.html"){
travelAgencyId: matter.travelAgencyId || ledger.travelAgencyId || "",
travelAgencyName: matter.travelAgencyName || ledger.travelAgencyName || "",
boardingPlace: ledger.boardingPlace || assignment.boardingPlace || "",
photoFiles: this.toUploadFileList(ledger.photoFiles || assignment.photoFiles || ""),
travelPeriod: matter.travelPeriod || "",
travelStartTime: matter.travelStartTime || "",
travelEndTime: matter.travelEndTime || "",
@@ -466,6 +491,38 @@ layout("/layouts/platform_h5.html"){
return String(value).split(",").map((item) => item.trim()).filter((item) => item)
}
},
toUploadFileList(value) {
if (!value) return []
return String(value)
.split(",")
.map((url, index) => {
url = url.trim()
return {
url: url,
name: "photo_" + index + ".jpg",
status: null,
isImage: true
}
})
},
fileNameFromUrl(url) {
if (!url) return "图片材料"
const pureUrl = String(url).split("?")[0]
const name = pureUrl.substring(pureUrl.lastIndexOf("/") + 1)
return name || "图片材料"
},
serializePhotoFiles(value) {
if (!value) return ""
if (Array.isArray(value)) {
return value.map((item) => {
if (typeof item === "string") return item.trim()
return item && item.url ? String(item.url).trim() : ""
}).filter((item) => item).join(",")
}
return String(value).split(",").map((item) => item.trim()).filter((item) => item).join(",")
},
emptyFamily() {
return {
familyName: "",
@@ -667,6 +724,7 @@ layout("/layouts/platform_h5.html"){
if (!this.validateDirectRelative()) return
const families = this.allowFamily && this.signupForm.hasFamily ? this.familyData : []
const form = Object.assign({}, this.signupForm, {
photoFiles: this.serializePhotoFiles(this.signupForm.photoFiles),
families: JSON.stringify(families),
directRelative: this.isDirectFamilyLine(this.signupForm) ? JSON.stringify(this.directRelativeForm) : ""
})