现在是时候来编写合同的详情页了,目前由于还没有汇总数据,现在只能够来展示合同最基础的信息,其他的执行情况,就要等到后边有了journal
类才可以。
合同详情页面
合同详情页面类似于展示项目的详情页面,所以我就基本照抄那个页面。
控制器
由于URL
是pms/contract/detail/{id}
,这里我们就来获取合同id
,然后把对应的预算和项目都一并传入页面,控制器如下:
@GetMapping("/detail/{id}")
public String contractDetailPage(@PathVariable("id") int cid, Model model) {
Contract contract = contractService.getContractById(cid);
Set<Budget> budgets = contract.getBudgets();
Project project = ((Budget) budgets.toArray()[0]).getProject();
model.addAttribute("contract", contract);
model.addAttribute("budgets", budgets);
model.addAttribute("project", project);
return "pms/contract/contractDetail";
}
页面比较简单:
<main class="col-lg-8 mx-auto container">
<nav style="--bs-breadcrumb-divider: '>';" aria-label="breadcrumb" class="mt-3">
<ol class="breadcrumb">
<li class="breadcrumb-item" aria-current="page"><a href="/pms/project/list" th:href="@{/pms/project/list}"
class="link-secondary">项目管理</a></li>
<li class="breadcrumb-item" aria-current="page"><a href="/pms/project/list"
th:href="${project.absoluteUrl()}"
class="link-secondary">[[${project.projectName}]]</a>
</li>
<li class="breadcrumb-item" aria-current="page"><a href="/pms/project/list"
th:href="${contract.absoluteUrl()}"
class="link-secondary">[[${contract.name}]]</a>
</li>
</ol>
</nav>
<h1 th:text="${contract.name}">合同名称</h1>
<hr>
<div class="row g-5">
<div class="col-md-9">
<h2>合同基础信息</h2>
<table class="table table-sm table-striped" th:object="${contract}">
<thead>
<tr>
<th>属性</th>
<th>内容</th>
</tr>
</thead>
<tbody>
<tr>
<td>合同编号</td>
<td th:text="*{number}"></td>
</tr>
<tr>
<td>合同总价</td>
<td th:text="${#numbers.formatDecimal(contract.totalPrice,0,'COMMA',2,'POINT')}"></td>
</tr>
<tr>
<td>合同条款</td>
<td th:text="*{terms}"></td>
</tr>
<tr>
<td>主要合同对手</td>
<td th:text="*{supplier}"></td>
</tr>
<tr>
<td>合同所属预算条目</td>
<td >
<span class="pe-1" th:each="budget:${budgets}" th:text="${budget.getBudgetName()}"></span>
</td>
</tr>
<tr>
<td>合同印花税类型</td>
<td>[[${contract.stamp.getType()}]] - [[${contract.stamp.getRate()}]]</td>
</tr>
<tr>
<td>不含税价</td>
<td th:text="${#numbers.formatDecimal(contract.getPriceWithoutVAT(),0,'COMMA',2,'POINT')}"></td>
</tr>
<tr>
<td>增值税额</td>
<td th:text="${#numbers.formatDecimal(contract.getVAT(),0,'COMMA',2,'POINT')}"></td>
</tr>
<tr>
<td>结算价格</td>
<td th:text="${#numbers.formatDecimal(contract.getFinalPrice(),0,'COMMA',2,'POINT')}"></td>
</tr>
<tr>
<td>其他合同当事方1</td>
<td th:text="*{getSecondarySupplier1()}"></td>
</tr>
<tr>
<td>其他合同当事方2</td>
<td th:text="*{getSecondarySupplier2()}"></td>
</tr>
<tr>
<td>其他合同当事方3</td>
<td th:text="*{getSecondarySupplier3()}"></td>
</tr>
<tr>
<td>签订时间</td>
<td th:text="*{signDate}"></td>
</tr>
<tr>
<td>终止时间</td>
<td th:text="*{endDate}"></td>
</tr>
<tr>
<td>合同标的</td>
<td th:text="*{target}"></td>
</tr>
<tr>
<td>联系人</td>
<td th:text="*{contact}"></td>
</tr>
<tr>
<td>联系电话</td>
<td th:text="*{phone}"></td>
</tr>
</tbody>
</table>
</div>
<div class="col-md-3">
<h2>功能</h2>
<ul class="list-group list-group-flush">
<li class="list-group-item"><a href="/pms/project/detail" th:href="${project.editUrl()}"><i
class="fas fa-pencil-alt"></i> 编辑合同信息</a></li>
<li class="list-group-item"><a href="/pms/contract/project/add" th:href="${project.addContractUrl()}"><i class="fas fa-file-import"></i> 新增合同</a></li>
<li class="list-group-item"><a href="/pms/budget/list" th:href="${project.budgetUrl()}" class="link-success"><i class="fas fa-chart-line"></i> 项目预算详情</a>
</li>
<li class="list-group-item"><a th:href="${project.contractListUrl()}" class="link-success"><i class="fas fa-list"></i> 项目合同列表</a></li>
<li class="list-group-item"><a href="" class="link-danger" data-bs-toggle="modal"
data-bs-target="#deleteModal"><i class="fas fa-trash-alt"></i> 删除项目</a>
</li>
</ul>
</div>
</div>
</main>
编辑合同
这个几乎就是新增合同的翻版,写起来也很快,只要记得写完之后将合同的编辑链接添加到详情页上,这里要给Project
再添加一个生成URL
的方法:
public String editContractByProject() {
return "/pms/contract/" + id + "/edit";
}
控制器与页面
这个几乎就是复制新添加合同的控制器,编写如下:
@GetMapping("/{pid}/edit/{id}")
public String editContractPage(@PathVariable("pid") int pid, @PathVariable("id") int cid, Model model) {
Project project = projectService.findById(pid);
List<Budget> budgets = budgetService.getBudgetByProjectId(pid);
Contract contract = contractService.getContractById(cid);
model.addAttribute("project", project);
model.addAttribute("budgets", budgets);
model.addAttribute("contract", contract);
model.addAttribute("stamps", stampService.getAllStamps());
return "pms/contract/contractEdit";
}
在编写页面的时候,发现之前犯了个错误。即:
<div class="col-sm-6">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="isOpen" name="isOpen"
th:checked="*{isOpen}">
<label class="form-check-label" for="isOpen">单价合同或开口合同</label>
</div>
</div>
这其中,不勾选的时候,这个数据不会发送到服务器,所以必须要单独来获取isOpen
的属性,接受POST
请求的控制器如下:
// 接受编辑修改
@PostMapping("/{pid}/edit/{id}")
public String editContract(@ModelAttribute("contract") Contract contract, BindingResult rs,
@PathVariable("pid") int pid, @PathVariable("id") int cid, Model model,
RedirectAttributes attributes, HttpServletRequest request) {
Project project = projectService.findById(pid);
List<Budget> budgets = budgetService.getBudgetByProjectId(pid);
model.addAttribute("project", project);
model.addAttribute("budgets", budgets);
model.addAttribute("stamps", stampService.getAllStamps());
if (rs.hasErrors()) {
return "pms/contract/contractEdit";
} else {
// 不再判断合同编号是否重复
// 没有对应的预算条目
if (contract.getBudgets() == null || contract.getBudgets().size() == 0) {
model.addAttribute("numberError", "未选择预算条目");
return "pms/contract/contractEdit";
}
// 如果有一个不为空,另外一个为空,将其设置为0
if ((contract.getPriceWithoutVAT() == null) && (contract.getVAT() != null)) {
contract.setPriceWithoutVAT(new BigDecimal("0.00"));
}
if ((contract.getPriceWithoutVAT() != null) && (contract.getVAT() == null)) {
contract.setVAT(new BigDecimal("0.00"));
}
// 判断都不为空的情况下,是否相等
if ((contract.getPriceWithoutVAT() != null) && (contract.getVAT() != null)) {
if (!contract.getTotalPrice().equals(contract.getPriceWithoutVAT().add(contract.getVAT()))) {
model.addAttribute("numberError", "不含税价+增值税 不等于 合同总价");
return "pms/contract/contractEdit";
}
}
contract.setId(cid);
contract.setOpen(request.getParameter("isOpen") != null);
contractService.saveContract(contract);
attributes.addFlashAttribute("updated", "成功修改合同 " + contract.getName());
return "redirect:" + contract.absoluteUrl();
}
}
在接受新增合同的控制器那里,也应该传入HttpServletRequest request
然后添加一行:
contract.setOpen(request.getParameter("isOpen") != null);
最后是页面,基本上和新增合同的页面差不多:
<main class="container">
<nav style="--bs-breadcrumb-divider: '>';" aria-label="breadcrumb" class="mt-3">
<ol class="breadcrumb">
<li class="breadcrumb-item" aria-current="page"><a href="/pms/project/list" th:href="@{/pms/project/list}"
class="link-secondary">项目管理</a></li>
<li class="breadcrumb-item" aria-current="page"><a href="/pms/project/list"
th:href="${project.absoluteUrl()}"
class="link-secondary">[[${project.projectName}]]</a>
</li>
<li class="breadcrumb-item" aria-current="page"><a href="/pms/project/list"
th:href="${project.editContractByProject()+'/'+contract.getId()}"
class="link-secondary">编辑合同</a></li>
</ol>
</nav>
<div class="py-1 text-center">
<h2>编辑合同</h2>
</div>
<div class="row py-1">
<div class="col-sm-4"></div>
<div th:if="${numberError}" class="text-center alert alert-danger col-12 col-sm-4" role="alert">
[[${numberError}]]
</div>
<div class="col-sm-4"></div>
</div>
<div class="row g-5">
<div class="col-2"></div>
<div class="col-md-8 col-lg-8">
<h4 class="mb-3">必填内容</h4>
<form class="needs-validation" action="/pms/contract/edit" th:action="${project.editContractByProject()+'/'+contract.getId()}"
method="post"
th:object="${contract}">
<div class="row g-3 mb-3">
<div class="col-sm-12">
<label for="contractName" class="form-label">合同名称</label>
<input type="text" class="form-control" id="contractName" th:field="*{name}"
th:classappend="${#fields.hasErrors('name')}? 'is-invalid'" required>
<div id="addressError" th:if="${#fields.hasErrors('name')}"
th:errors="*{name}"
class="invalid-feedback"></div>
</div>
<div class="col-sm-6">
<label for="contractNumber" class="form-label">合同编号</label>
<input type="text" class="form-control" id="contractNumber"
th:field="*{number}"
th:classappend="${#fields.hasErrors('number')}? 'is-invalid'" required>
<div id="numberError" th:if="${#fields.hasErrors('number')}"
th:errors="*{number}"
class="invalid-feedback"></div>
</div>
<div class="col-sm-6">
<label for="projectManager" class="form-label">主要合同方</label>
<input type="text" class="form-control" id="projectManager" th:field="*{supplier}"
th:classappend="${#fields.hasErrors('supplier')}? 'is-invalid'" required>
<div id="managerError" th:if="${#fields.hasErrors('supplier')}"
th:errors="*{supplier}"
class="invalid-feedback"></div>
</div>
<div class="col-12">
<label for="projectDescription" class="form-label">合同主要条款</label>
<textarea class="form-control" id="projectDescription" th:field="*{terms}" rows="3"
th:classappend="${#fields.hasErrors('terms')}? 'is-invalid'" required>
</textarea>
<div id="descriptionError" th:if="${#fields.hasErrors('terms')}"
th:errors="*{terms}"
class="invalid-feedback"></div>
</div>
<div class="col-sm-6">
<p class="mb-0">合同所属预算条目(可按Ctrl键多选)</p>
<select class="form-select mt-2" multiple aria-label="budgets" name="budgets" th:field="*{budgets}">
<option th:each="budget, control:${budgets}" th:value="${budget.getId()}" >
[[${budget.budgetName}]]
</option>
</select>
</div>
<div class="col-sm-6">
<p class="mb-0">合同印花税类型</p>
<select class="form-select mt-2" aria-label="stamp" name="stamp" th:field="*{stamp}">
<option th:each="stamp, control:${stamps}" th:value="${stamp.getId()}">[[${stamp.type}]] -
[[${stamp.rate}]]
</option>
</select>
</div>
<div class="col-sm-6">
<label for="totalPrice" class="form-label">合同总价</label>
<input type="number" step="0.01" class="form-control" id="totalPrice" th:field="*{totalPrice}"
th:classappend="${#fields.hasErrors('totalPrice')}? 'is-invalid'" required>
<div id="totalPriceError" th:if="${#fields.hasErrors('totalPrice')}"
th:errors="*{totalPrice}"
class="invalid-feedback"></div>
</div>
<div class="col-sm-6">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="isOpen" name="isOpen" th:checked="*{isOpen}"
>
<label class="form-check-label" for="isOpen">单价合同或开口合同</label>
</div>
</div>
<hr>
<h4 class="mb-3">选填内容</h4>
<div class="col-sm-6">
<label for="priceWithoutVAT" class="form-label">不含税价</label>
<input type="number" step="0.01" class="form-control" id="priceWithoutVAT"
th:field="*{priceWithoutVAT}"
th:classappend="${#fields.hasErrors('priceWithoutVAT')}? 'is-invalid'"
placeholder="无法区分不含税价请留空">
<div id="priceWithoutVATError" th:if="${#fields.hasErrors('priceWithoutVAT')}"
th:errors="*{priceWithoutVAT}"
class="invalid-feedback"></div>
</div>
<div class="col-sm-6">
<label for="VAT" class="form-label">增值税额</label>
<input type="number" step="0.01" class="form-control" id="VAT" th:field="*{VAT}"
th:classappend="${#fields.hasErrors('VAT')}? 'is-invalid'" placeholder="无法区分税额请留空">
<div id="VATError" th:if="${#fields.hasErrors('VAT')}"
th:errors="*{VAT}"
class="invalid-feedback"></div>
</div>
<div class="col-sm-6">
<label for="finalPrice" class="form-label">结算价格</label>
<input type="number" step="0.01" class="form-control" id="finalPrice" th:field="*{finalPrice}"
th:classappend="${#fields.hasErrors('finalPrice')}? 'is-invalid'" placeholder="未结算请留空">
<div id="finalPriceError" th:if="${#fields.hasErrors('finalPrice')}"
th:errors="*{finalPrice}"
class="invalid-feedback"></div>
</div>
<p class="text-secondary">合同无法区分不含税价和增值税则留空。需要结算的合同在尚未结算时也请留空。(不需要填写0)</p>
<div class="col-sm-6">
<label for="signDate" class="form-label">签订时间</label>
<input type="date" class="form-control" id="signDate" th:field="*{signDate}"
th:classappend="${#fields.hasErrors('signDate')}? 'is-invalid'">
<div id="startTimeError" th:if="${#fields.hasErrors('signDate')}"
th:errors="*{signDate}"
class="invalid-feedback"></div>
</div>
<div class="col-sm-6">
<label for="endDate" class="form-label">终止时间</label>
<input type="date" class="form-control" id="endDate" th:field="*{endDate}"
th:classappend="${#fields.hasErrors('endDate')}? 'is-invalid'">
<div id="endTimeError" th:if="${#fields.hasErrors('endDate')}"
th:errors="*{endDate}"
class="invalid-feedback"></div>
</div>
<div class="col-sm-4">
<label for="secondarySupplier1" class="form-label">其他合同方1</label>
<input class="form-control" id="secondarySupplier1" th:field="*{secondarySupplier1}"
th:classappend="${#fields.hasErrors('secondarySupplier1')}? 'is-invalid'">
<div id="supplier1Error" th:if="${#fields.hasErrors('secondarySupplier1')}"
th:errors="*{secondarySupplier1}"
class="invalid-feedback"></div>
</div>
<div class="col-sm-4">
<label for="secondarySupplier2" class="form-label">其他合同方2</label>
<input class="form-control" id="secondarySupplier2" th:field="*{secondarySupplier2}"
th:classappend="${#fields.hasErrors('secondarySupplier2')}? 'is-invalid'">
<div id="supplier2Error" th:if="${#fields.hasErrors('secondarySupplier2')}"
th:errors="*{secondarySupplier2}"
class="invalid-feedback"></div>
</div>
<div class="col-sm-4">
<label for="secondarySupplier3" class="form-label">其他合同方3</label>
<input class="form-control" id="secondarySupplier3" th:field="*{secondarySupplier3}"
th:classappend="${#fields.hasErrors('secondarySupplier3')}? 'is-invalid'">
<div id="supplier3Error" th:if="${#fields.hasErrors('secondarySupplier3')}"
th:errors="*{secondarySupplier3}"
class="invalid-feedback"></div>
</div>
<div class="col-sm-6">
<label for="target" class="form-label">合同标的</label>
<input class="form-control" id="target" th:field="*{target}"
th:classappend="${#fields.hasErrors('target')}? 'is-invalid'">
<div id="targetError" th:if="${#fields.hasErrors('target')}"
th:errors="*{target}"
class="invalid-feedback"></div>
</div>
<div class="col-sm-6">
<label for="contact" class="form-label">联系人</label>
<input class="form-control" id="contact" th:field="*{contact}"
th:classappend="${#fields.hasErrors('contact')}? 'is-invalid'">
<div id="contactError" th:if="${#fields.hasErrors('contact')}"
th:errors="*{contact}"
class="invalid-feedback"></div>
</div>
<div class="col-sm-6">
<label for="phone" class="form-label">联系电话</label>
<input class="form-control" id="phone" th:field="*{phone}"
th:classappend="${#fields.hasErrors('phone')}? 'is-invalid'">
<div id="phoneError" th:if="${#fields.hasErrors('phone')}"
th:errors="*{phone}"
class="invalid-feedback"></div>
</div>
</div>
<div class="text-center mb-3">
<button class="btn btn-primary btn-lg" type="submit"><i class="fas fa-check"></i> 提交</button>
<a class="btn btn-secondary btn-lg" th:href="${project.absoluteUrl()}" href="/pms/project/list"><i
class="fas fa-undo-alt"></i> 返回</a>
</div>
</form>
</div>
<div class="col-2"></div>
</div>
</main>
删除合同
这都是套路了,只要配置好控制器以及权限就行了
@PostMapping("/delete")
public String deleteContract(@RequestParam("deleteId") int id,
@RequestParam("name") String name,
@RequestParam("projectId") int pid,
RedirectAttributes attributes) {
contractService.deleteContractById(id);
attributes.addFlashAttribute("deleted", "已删除合同:" + name);
String url = "/pms/contract/project/" + pid + "/list";
return "redirect:" + url;
}
然后是页面,其实就是修改那个模态框,为了方便,加了一个项目id
的隐藏输入框:
<div class="modal fade" id="deleteModal" tabindex="-1" aria-labelledby="deleteLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="deleteLabel">删除合同</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
将删除[[${contract.name}]]
</div>
<form class="modal-footer" method="post" action="/pms/project/delete" th:action="@{/pms/contract/delete}">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal"><i class="fas fa-undo"></i>
返回
</button>
<input type="hidden" name="deleteId" th:value="${contract.id}">
<input type="hidden" name="projectId" th:value="${project.id}">
<input type="hidden" name="name" th:value="${contract.name}">
<button type="submit" class="btn btn-danger"><i class="fas fa-info-circle"></i>
确定删除
</button>
</form>
</div>
</div>
</div>
这里我进行测试的时候,提示了SQL
错误,这是因为我在Contract
类中把多对多映射设置成cascade = CascadeType.ALL
,结果导致在删除一个合同的时候,也会去尝试删除对应的预算,这显然不合理,所以将其修改成CascadeType.PERSIST
即可,之后还有一个问题,就是删除一个预算,就需要删除其对应的所有合同,所以还需要在Bugdet
类中修改映射的级联为级联删除(或者设置为ALL
也可以)即可:
//关联到合同的多对多映射
@ManyToMany(mappedBy = "budgets",cascade = CascadeType.ALL)
private List<Contract> contracts;
这样做有一个问题,就是删除某一个预算的时候,会把其对应的所有合同都删除,即使合同已经关联到了其他的预算项目。不过考虑到预算项目一般是比较稳固不会变动的,所以就暂且这么做吧。否则应该保留对应合同。
最后在Spring Security
中把对应的路径保护上:
.antMatchers("/pms/contract/delete").hasAnyAuthority("ADMIN", "SUPERUSER")
总结
这里千万不要忘记,CHECKBOX
类型的输入框,在设置为不勾选的时候,是不会包含在表单中的,如果勾选,默认是传入一个on
到后端。这个其实我在编写修改用户的时候已经单独获取了布尔值的输入框,可惜今天写的时候忘记了,还好回去看了一下,发现问题并解决掉了。