contract_form.html 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269
  1. {% extends "base.html" %}
  2. {% block title %}{% if renew %}续签{% elif contract %}编辑{% else %}新建{% endif %}合同{% endblock %}
  3. {% block content %}
  4. <div class="d-flex justify-content-between align-items-center mb-4">
  5. <h2 class="fw-bold">{% if renew %}续签{% elif contract %}编辑{% else %}新建{% endif %}合同</h2>
  6. <a href="{{ url_for('contract_list') }}" class="btn btn-outline-secondary">返回列表</a>
  7. </div>
  8. <div class="card shadow-sm">
  9. <div class="card-body">
  10. <form method="POST" enctype="multipart/form-data">
  11. <div class="row g-3">
  12. <!-- 合同类型 -->
  13. <div class="col-md-6">
  14. <label class="form-label fw-semibold">合同类型</label>
  15. <select class="form-select" name="type_id" id="type-select" required>
  16. <option value="">选择合同类型</option>
  17. {% for type in contract_types %}
  18. <option value="{{ type.id }}"
  19. {% if contract and contract.type_id == type.id %}selected
  20. {% elif renew and original_contract is defined and original_contract.type_id == type.id %}selected{% endif %}>
  21. {{ type.name }}
  22. </option>
  23. {% endfor %}
  24. </select>
  25. </div>
  26. <!-- 合同编号 -->
  27. <div class="col-md-6">
  28. <label class="form-label fw-semibold">合同编号</label>
  29. {% if renew %}
  30. <input type="text" class="form-control" name="contract_number" id="contract-number" value="自动生成" readonly>
  31. <div class="form-text">续签时编号将在保存后自动生成</div>
  32. {% else %}
  33. <input type="text" class="form-control" name="contract_number" id="contract-number"
  34. value="{{ contract.contract_number if contract else default_number }}" required>
  35. <div class="form-text">系统已自动生成编号,您也可以自行修改</div>
  36. {% endif %}
  37. </div>
  38. <!-- 合同名称 -->
  39. <div class="col-md-6">
  40. <label class="form-label fw-semibold">合同名称</label>
  41. <input type="text" class="form-control" name="name"
  42. value="{{ contract.name if contract else '' }}" required>
  43. </div>
  44. <!-- 我方主体 -->
  45. <div class="col-md-6">
  46. <label class="form-label fw-semibold">我方主体</label>
  47. <select class="form-select" name="company_entity_id" required>
  48. <option value="">选择我方主体</option>
  49. {% for entity in company_entities %}
  50. <option value="{{ entity.id }}"
  51. {% if contract and contract.company_entity_id == entity.id %}selected
  52. {% elif renew and original_contract is defined and original_contract.company_entity_id == entity.id %}selected{% endif %}>
  53. {{ entity.name }}
  54. </option>
  55. {% endfor %}
  56. </select>
  57. </div>
  58. <!-- 开始日期 -->
  59. <div class="col-md-6">
  60. <label class="form-label fw-semibold">开始日期</label>
  61. <input type="date" class="form-control" id="start-date" name="start_date"
  62. value="{{ contract.start_date.strftime('%Y-%m-%d') if contract else '' }}" required>
  63. </div>
  64. <!-- 结束日期 -->
  65. <div class="col-md-6">
  66. <label class="form-label fw-semibold">结束日期</label>
  67. <input type="date" class="form-control" id="end-date" name="end_date"
  68. value="{{ contract.end_date.strftime('%Y-%m-%d') if contract else '' }}" required>
  69. </div>
  70. <!-- 签约方式 -->
  71. <div class="col-md-6">
  72. <label class="form-label fw-semibold">签约方式</label>
  73. {% set signing_method_value = contract.signing_method if contract else (original_contract.signing_method if renew and original_contract is defined else '纸质签') %}
  74. <select class="form-select" name="signing_method" required>
  75. <option value="纸质签" {% if signing_method_value == '纸质签' %}selected{% endif %}>纸质签</option>
  76. <option value="电子签" {% if signing_method_value == '电子签' %}selected{% endif %}>电子签</option>
  77. <option value="电子签+纸质签" {% if signing_method_value == '电子签+纸质签' %}selected{% endif %}>电子签+纸质签</option>
  78. </select>
  79. </div>
  80. <!-- 合同收回时间 -->
  81. <div class="col-md-6">
  82. <label class="form-label fw-semibold">合同收回时间</label>
  83. <input type="date" class="form-control" name="collected_date"
  84. value="{{ contract.collected_date.strftime('%Y-%m-%d') if contract and contract.collected_date else (original_contract.collected_date.strftime('%Y-%m-%d') if renew and original_contract and original_contract.collected_date else '') }}">
  85. </div>
  86. <!-- 存储盒名称 -->
  87. <div class="col-md-6">
  88. <label class="form-label fw-semibold">存储盒名称</label>
  89. <input type="text" class="form-control" name="storage_box"
  90. value="{{ contract.storage_box if contract else (original_contract.storage_box if renew and original_contract is defined else '') }}">
  91. </div>
  92. <!-- 对方主体 -->
  93. <div class="col-12">
  94. <label class="form-label fw-semibold">对方主体(可多个)</label>
  95. <div id="counterparties-container">
  96. {% set counterparties = contract.counterparties if contract else (original_contract.counterparties if renew and original_contract is defined else []) %}
  97. {% if counterparties %}
  98. {% for cp in counterparties %}
  99. <div class="input-group mb-2">
  100. <input type="text" class="form-control" name="counterparty_name" value="{{ cp.name }}">
  101. <button type="button" class="btn btn-outline-danger remove-counterparty">删除</button>
  102. </div>
  103. {% endfor %}
  104. {% else %}
  105. <div class="input-group mb-2">
  106. <input type="text" class="form-control" name="counterparty_name">
  107. <button type="button" class="btn btn-outline-danger remove-counterparty">删除</button>
  108. </div>
  109. {% endif %}
  110. </div>
  111. <button type="button" id="add-counterparty" class="btn btn-sm btn-outline-secondary mt-1">添加对方主体</button>
  112. </div>
  113. <!-- 提前提醒天数 -->
  114. <div class="col-md-6">
  115. <label class="form-label fw-semibold">到期前提醒(天)</label>
  116. <input type="number" class="form-control" id="remind-before" name="remind_before"
  117. value="{{ contract.remind_before if contract else 30 }}" min="1" max="365">
  118. </div>
  119. <!-- 备注 -->
  120. <div class="col-12">
  121. <label class="form-label fw-semibold">备注</label>
  122. <textarea class="form-control" name="notes" rows="3">{{ contract.notes if contract else '' }}</textarea>
  123. </div>
  124. <!-- 附件上传(已修复) -->
  125. <div class="col-12">
  126. <label class="form-label fw-semibold">附件</label>
  127. <input type="file" class="form-control" name="new_attachments" multiple
  128. accept=".pdf,.jpg,.jpeg,.png,.gif,.bmp,.tiff,.webp,.doc,.docx,.xls,.xlsx,.ppt,.pptx">
  129. <div class="form-text">仅支持 PDF、图片、Office 文件,单个不超过 5MB</div>
  130. {% if attachments %}
  131. <div class="mt-3">
  132. <label class="form-label fw-semibold">已上传附件</label>
  133. <ul class="list-group">
  134. {% for att in attachments %}
  135. <!-- 修改附件链接 -->
  136. <li class="list-group-item d-flex justify-content-between align-items-center">
  137. <div>
  138. <i class="bi bi-paperclip me-2"></i>
  139. <a href="#" onclick="previewAttachment('{{ url_for('download_file', attachment_id=att.id) }}', '{{ att.filename }}')">
  140. {{ att.filename }}
  141. </a>
  142. </div>
  143. <div>
  144. <input type="checkbox" name="delete_attachment" value="{{ att.id }}" class="form-check-input me-1">
  145. <label>删除</label>
  146. </div>
  147. </li>
  148. {% endfor %}
  149. </ul>
  150. </div>
  151. {% endif %}
  152. </div>
  153. </div>
  154. <!-- 提交按钮 -->
  155. <div class="d-grid gap-2 mt-4">
  156. <button type="submit" class="btn btn-primary btn-lg">{{ '续签合同' if renew else '保存合同' }}</button>
  157. </div>
  158. </form>
  159. </div>
  160. </div>
  161. <script>
  162. document.addEventListener('DOMContentLoaded', function() {
  163. const typeSelect = document.getElementById('type-select');
  164. const contractInput = document.getElementById('contract-number');
  165. const startDateInput = document.getElementById('start-date');
  166. const endDateInput = document.getElementById('end-date');
  167. const fileInput = document.querySelector('input[name="new_attachments"]');
  168. const addCounterpartyBtn = document.getElementById('add-counterparty');
  169. const counterpartiesContainer = document.getElementById('counterparties-container');
  170. const form = document.querySelector('form');
  171. // 文件校验(支持 PDF、图片、Word、Excel、PowerPoint)
  172. function validateFileType(input) {
  173. const validTypes = [
  174. 'application/pdf',
  175. 'image/jpeg',
  176. 'image/png',
  177. 'image/gif',
  178. 'image/bmp',
  179. 'image/tiff',
  180. 'image/webp',
  181. 'application/msword',
  182. 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
  183. 'application/vnd.ms-excel',
  184. 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
  185. 'application/vnd.openxmlformats-officedocument.presentationml.presentation'
  186. ];
  187. const files = input.files;
  188. for (let i = 0; i < files.length; i++) {
  189. if (!validTypes.includes(files[i].type)) {
  190. alert(`文件 "${files[i].name}" 不是有效的 PDF、图片、Word、Excel 或 PowerPoint 文件`);
  191. input.value = '';
  192. return false;
  193. }
  194. if (files[i].size > 5 * 1024 * 1024) { // 最大 5MB
  195. alert(`文件 "${files[i].name}" 超过 5MB`);
  196. input.value = '';
  197. return false;
  198. }
  199. }
  200. return true;
  201. }
  202. form.addEventListener('submit', function(e) {
  203. if (fileInput.files.length > 0 && !validateFileType(fileInput)) e.preventDefault();
  204. });
  205. fileInput.addEventListener('change', () => validateFileType(fileInput));
  206. // 合同编号自动生成
  207. async function updateContractNumber(typeId) {
  208. if (!typeId || contractInput.readOnly) return;
  209. try {
  210. const res = await fetch(`/contract/generate_number/${typeId}`);
  211. const data = await res.json();
  212. if (!contractInput.dataset.userEdited) contractInput.value = data.number;
  213. } catch (err) {
  214. console.error('获取合同编号失败', err);
  215. }
  216. }
  217. typeSelect.addEventListener('change', function() { updateContractNumber(this.value); });
  218. contractInput.addEventListener('input', function(){ this.dataset.userEdited='true'; });
  219. // 自动结束日期 = 开始日期 +1年
  220. startDateInput.addEventListener('change', () => {
  221. if (!endDateInput.value) {
  222. const startDate = new Date(startDateInput.value);
  223. if (!isNaN(startDate)) {
  224. startDate.setFullYear(startDate.getFullYear() + 1);
  225. endDateInput.value = startDate.toISOString().split('T')[0];
  226. }
  227. }
  228. });
  229. // 对方主体增删
  230. function addCounterpartyField(value='') {
  231. const div = document.createElement('div');
  232. div.className = 'input-group mb-2';
  233. div.innerHTML = `<input type="text" class="form-control" name="counterparty_name" value="${value}"><button type="button" class="btn btn-outline-danger remove-counterparty">删除</button>`;
  234. counterpartiesContainer.appendChild(div);
  235. }
  236. addCounterpartyBtn.addEventListener('click', () => addCounterpartyField());
  237. counterpartiesContainer.addEventListener('click', function(e) {
  238. if (e.target.classList.contains('remove-counterparty')) e.target.parentElement.remove();
  239. });
  240. });
  241. </script>
  242. {% endblock %}