designer-team-assignment-modal.component.html 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555
  1. <div class="modal-overlay" [class.visible]="visible" (click)="closeModal()">
  2. <div class="modal-container" (click)="$event.stopPropagation()">
  3. <div class="modal-header">
  4. <h2>设计师组分配</h2>
  5. <button class="close-btn" (click)="closeModal()">
  6. <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
  7. <line x1="18" y1="6" x2="6" y2="18"></line>
  8. <line x1="6" y1="6" x2="18" y2="18"></line>
  9. </svg>
  10. </button>
  11. </div>
  12. <div class="modal-body">
  13. <!-- 项目组选择 -->
  14. <div class="team-selection-section">
  15. <h3>选择项目组</h3>
  16. <div class="team-grid">
  17. @for (team of projectTeams; track team.id) {
  18. <div
  19. class="team-card"
  20. [class.selected]="internalSelectedTeamId === team.id"
  21. (click)="selectTeam(team.id)"
  22. >
  23. <div class="team-header">
  24. <h4>{{ team.name }}</h4>
  25. <span class="team-leader">组长:{{ team.leaderName }}</span>
  26. </div>
  27. <div class="team-description">{{ team.description }}</div>
  28. <div class="team-stats">
  29. <div class="stat-item">
  30. <span class="stat-label">成员</span>
  31. <span class="stat-value">{{ team.members.length }}人</span>
  32. </div>
  33. <div class="stat-item">
  34. <span class="stat-label">空闲</span>
  35. <span class="stat-value idle">{{ getIdleDesignersCount(team) }}人</span>
  36. </div>
  37. </div>
  38. </div>
  39. }
  40. </div>
  41. </div>
  42. <!-- 设计师列表 -->
  43. @if (internalSelectedTeamId) {
  44. <div class="designer-selection-section">
  45. <div class="section-header">
  46. <h3>{{ getSelectedTeam()?.name }} - 设计师列表</h3>
  47. <div class="selection-summary">
  48. 已选择:{{ internalSelectedDesigners.length + internalCrossTeamCollaborators.length }}人
  49. </div>
  50. </div>
  51. <!-- 推荐设计师 -->
  52. @if (getRecommendedDesigners().length > 0) {
  53. <div class="recommended-section">
  54. <h4>
  55. <span class="recommend-icon">⭐</span>
  56. 推荐分配(长期闲置优先)
  57. </h4>
  58. <div class="designer-grid">
  59. @for (designer of getRecommendedDesigners(); track designer.id) {
  60. <div
  61. class="designer-card recommended"
  62. [class.selected]="isDesignerSelected(designer)"
  63. [class.status-idle]="designer.status === 'idle'"
  64. [class.status-reviewing]="designer.status === 'reviewing'"
  65. [class.status-stagnant]="designer.status === 'stagnant'"
  66. (click)="toggleDesignerSelection(designer)"
  67. >
  68. <div class="designer-avatar">
  69. @if (designer.avatar) {
  70. <img [src]="designer.avatar" [alt]="designer.name">
  71. } @else {
  72. <div class="avatar-placeholder">{{ designer.name.charAt(0) }}</div>
  73. }
  74. <div class="status-dot" [style.background-color]="getDesignerStatusColor(designer.status)"></div>
  75. </div>
  76. <div class="designer-info">
  77. <div class="designer-name">
  78. {{ designer.name }}
  79. @if (designer.isTeamLeader) {
  80. <span class="leader-badge">组长</span>
  81. }
  82. </div>
  83. <div class="designer-status">
  84. <span class="status-text" [style.color]="getDesignerStatusColor(designer.status)">
  85. {{ getDesignerStatusText(designer.status) }}
  86. </span>
  87. <span class="project-count" [style.color]="getDesignerStatusColor(designer.status)">
  88. {{ designer.currentProjects }}个项目
  89. </span>
  90. </div>
  91. <div class="designer-metrics">
  92. <div class="metric-item" [class]="getIdleDaysClass(designer.idleDays)">
  93. <span class="metric-label">{{ getRecentOrdersText(designer) }}</span>
  94. </div>
  95. @if (designer.availableDates.length > 0) {
  96. <div class="metric-item">
  97. <span class="metric-label">{{ getAvailableDatesText(designer) }}</span>
  98. </div>
  99. }
  100. @if (designer.reviewDates.length > 0) {
  101. <div class="metric-item review-dates">
  102. <span class="metric-label">
  103. 对图日期:{{ designer.reviewDates.slice(0, 2).join(', ') }}
  104. @if (designer.reviewDates.length > 2) {
  105. <span class="more-dates">等{{ designer.reviewDates.length }}个</span>
  106. }
  107. </span>
  108. </div>
  109. }
  110. </div>
  111. <div class="designer-skills">
  112. @for (skill of designer.skills.slice(0, 2); track skill) {
  113. <span class="skill-tag">{{ skill }}</span>
  114. }
  115. @if (designer.skills.length > 2) {
  116. <span class="skill-more">+{{ designer.skills.length - 2 }}</span>
  117. }
  118. </div>
  119. </div>
  120. <div class="designer-actions">
  121. <button
  122. class="calendar-btn"
  123. (click)="$event.stopPropagation(); showDesignerEmployeeDetail(designer)"
  124. title="查看设计师详情"
  125. >
  126. 👤 详情
  127. </button>
  128. @if (enableSpaceAssignment && spaceScenes.length > 0) {
  129. <button
  130. class="space-assign-btn"
  131. (click)="$event.stopPropagation(); openSpaceAssignment(designer)"
  132. title="分配空间"
  133. >
  134. 🏠
  135. </button>
  136. }
  137. </div>
  138. @if (enableSpaceAssignment && isDesignerSelected(designer)) {
  139. <div class="designer-spaces-info">
  140. <span class="spaces-label">负责空间:</span>
  141. <span class="spaces-value">{{ getDesignerSpacesText(designer.id) }}</span>
  142. </div>
  143. }
  144. </div>
  145. }
  146. </div>
  147. </div>
  148. }
  149. <!-- 所有团队成员 -->
  150. <div class="all-members-section">
  151. <h4>所有团队成员</h4>
  152. <div class="designer-grid">
  153. @for (designer of getSelectedTeam()?.members; track designer.id) {
  154. <div
  155. class="designer-card"
  156. [class.selected]="isDesignerSelected(designer)"
  157. [class.status-idle]="designer.status === 'idle'"
  158. [class.status-reviewing]="designer.status === 'reviewing'"
  159. [class.status-stagnant]="designer.status === 'stagnant'"
  160. (click)="toggleDesignerSelection(designer)"
  161. >
  162. <div class="designer-avatar">
  163. @if (designer.avatar) {
  164. <img [src]="designer.avatar" [alt]="designer.name">
  165. } @else {
  166. <div class="avatar-placeholder">{{ designer.name.charAt(0) }}</div>
  167. }
  168. <div class="status-dot" [style.background-color]="getDesignerStatusColor(designer.status)"></div>
  169. </div>
  170. <div class="designer-info">
  171. <div class="designer-name">
  172. @if (isProjectLeader(designer)) {
  173. <span class="project-leader-star">⭐</span>
  174. }
  175. {{ designer.name }}
  176. @if (designer.isTeamLeader) {
  177. <span class="leader-badge">组长</span>
  178. }
  179. @if (isProjectLeader(designer)) {
  180. <span class="project-leader-badge">负责人</span>
  181. }
  182. </div>
  183. <div class="designer-status">
  184. <span class="status-text" [style.color]="getDesignerStatusColor(designer.status)">
  185. {{ getDesignerStatusText(designer.status) }}
  186. </span>
  187. <span class="project-count" [style.color]="getDesignerStatusColor(designer.status)">
  188. {{ designer.currentProjects }}个项目
  189. </span>
  190. </div>
  191. <div class="designer-metrics">
  192. <div class="metric-item" [class]="getIdleDaysClass(designer.idleDays)">
  193. <span class="metric-label">{{ getRecentOrdersText(designer) }}</span>
  194. </div>
  195. @if (designer.availableDates.length > 0) {
  196. <div class="metric-item">
  197. <span class="metric-label">{{ getAvailableDatesText(designer) }}</span>
  198. </div>
  199. }
  200. @if (designer.reviewDates.length > 0) {
  201. <div class="metric-item review-dates">
  202. <span class="metric-label">
  203. 对图日期:{{ designer.reviewDates.slice(0, 2).join(', ') }}
  204. @if (designer.reviewDates.length > 2) {
  205. <span class="more-dates">等{{ designer.reviewDates.length }}个</span>
  206. }
  207. </span>
  208. </div>
  209. }
  210. </div>
  211. <div class="designer-skills">
  212. @for (skill of designer.skills.slice(0, 2); track skill) {
  213. <span class="skill-tag">{{ skill }}</span>
  214. }
  215. @if (designer.skills.length > 2) {
  216. <span class="skill-more">+{{ designer.skills.length - 2 }}</span>
  217. }
  218. </div>
  219. </div>
  220. <div class="designer-actions">
  221. <button
  222. class="calendar-btn"
  223. (click)="$event.stopPropagation(); showDesignerEmployeeDetail(designer)"
  224. title="查看设计师详情"
  225. >
  226. 👤 详情
  227. </button>
  228. @if (enableSpaceAssignment && spaceScenes.length > 0) {
  229. <button
  230. class="space-assign-btn"
  231. (click)="$event.stopPropagation(); openSpaceAssignment(designer)"
  232. title="分配空间"
  233. >
  234. 🏠
  235. </button>
  236. }
  237. </div>
  238. @if (enableSpaceAssignment && isDesignerSelected(designer)) {
  239. <div class="designer-spaces-info">
  240. <span class="spaces-label">负责空间:</span>
  241. <span class="spaces-value">{{ getDesignerSpacesText(designer.id) }}</span>
  242. </div>
  243. }
  244. </div>
  245. }
  246. </div>
  247. </div>
  248. <!-- 跨组合作选项 -->
  249. <div class="cross-team-section">
  250. <div class="cross-team-header">
  251. <label class="checkbox-label">
  252. <input
  253. type="checkbox"
  254. [(ngModel)]="allowCrossTeamSelection"
  255. >
  256. <span class="checkmark"></span>
  257. 允许跨组合作
  258. </label>
  259. <span class="cross-team-hint">可从其他项目组选择成员参与协作</span>
  260. </div>
  261. @if (allowCrossTeamSelection) {
  262. <div class="cross-team-designers">
  263. <h4>其他项目组成员</h4>
  264. <div class="designer-grid">
  265. @for (designer of getOtherTeamDesigners(); track designer.id) {
  266. <div
  267. class="designer-card cross-team"
  268. [class.selected]="isCrossTeamCollaborator(designer)"
  269. [class.status-idle]="designer.status === 'idle'"
  270. [class.status-reviewing]="designer.status === 'reviewing'"
  271. [class.status-stagnant]="designer.status === 'stagnant'"
  272. (click)="toggleCrossTeamCollaborator(designer)"
  273. >
  274. <div class="designer-avatar">
  275. @if (designer.avatar) {
  276. <img [src]="designer.avatar" [alt]="designer.name">
  277. } @else {
  278. <div class="avatar-placeholder">{{ designer.name.charAt(0) }}</div>
  279. }
  280. <div class="status-dot" [style.background-color]="getDesignerStatusColor(designer.status)"></div>
  281. </div>
  282. <div class="designer-info">
  283. <div class="designer-name">
  284. {{ designer.name }}
  285. <span class="team-tag">{{ designer.teamName }}</span>
  286. </div>
  287. <div class="designer-status">
  288. <span class="status-text" [style.color]="getDesignerStatusColor(designer.status)">
  289. {{ getDesignerStatusText(designer.status) }}
  290. </span>
  291. <span class="workload" [class]="getWorkloadClass(designer.workload)">
  292. {{ designer.workload }}%
  293. </span>
  294. </div>
  295. <div class="designer-metrics">
  296. <div class="metric-item" [class]="getIdleDaysClass(designer.idleDays)">
  297. <span class="metric-label">{{ getRecentOrdersText(designer) }}</span>
  298. </div>
  299. </div>
  300. </div>
  301. <div class="designer-actions">
  302. <button
  303. class="calendar-btn"
  304. (click)="$event.stopPropagation(); showDesignerEmployeeDetail(designer)"
  305. title="查看设计师详情"
  306. >
  307. 👤 详情
  308. </button>
  309. @if (enableSpaceAssignment && spaceScenes.length > 0) {
  310. <button
  311. class="space-assign-btn"
  312. (click)="$event.stopPropagation(); openSpaceAssignment(designer)"
  313. title="分配空间"
  314. >
  315. 🏠
  316. </button>
  317. }
  318. </div>
  319. @if (enableSpaceAssignment && isCrossTeamCollaborator(designer)) {
  320. <div class="designer-spaces-info">
  321. <span class="spaces-label">负责空间:</span>
  322. <span class="spaces-value">{{ getDesignerSpacesText(designer.id) }}</span>
  323. </div>
  324. }
  325. </div>
  326. }
  327. </div>
  328. </div>
  329. }
  330. </div>
  331. </div>
  332. <!-- 分配摘要和确认按钮区域 - 移到内容区域内 -->
  333. <div class="assignment-summary-section">
  334. <div class="summary-header">
  335. <h4>分配摘要</h4>
  336. </div>
  337. <div class="selection-summary">
  338. @if (getProjectLeader()) {
  339. <div class="summary-item">
  340. <span class="summary-label">项目负责人:</span>
  341. <span class="summary-value project-leader">⭐ {{ getProjectLeader()?.name }}</span>
  342. </div>
  343. }
  344. @if (getSelectedDesignersNames()) {
  345. <div class="summary-item">
  346. <span class="summary-label">主要团队:</span>
  347. <span class="summary-value">{{ getSelectedDesignersNames() }}</span>
  348. </div>
  349. }
  350. @if (internalCrossTeamCollaborators.length > 0) {
  351. <div class="summary-item">
  352. <span class="summary-label">跨组合作:</span>
  353. <span class="summary-value">{{ getCrossTeamCollaboratorsNames() }}</span>
  354. </div>
  355. }
  356. </div>
  357. <div class="modal-actions">
  358. <button class="btn-secondary" (click)="closeModal()">取消</button>
  359. <button
  360. class="btn-primary"
  361. [disabled]="!canConfirmAssignment()"
  362. (click)="confirmAssignment()"
  363. >
  364. 确认分配
  365. </button>
  366. </div>
  367. </div>
  368. }
  369. </div>
  370. </div>
  371. </div>
  372. <!-- 设计师日历弹窗 -->
  373. @if (showDesignerCalendar) {
  374. <div class="designer-calendar-modal">
  375. <div class="modal-mask" (click)="closeDesignerCalendar()"></div>
  376. <div class="modal-content">
  377. <h3>设计师工作日历</h3>
  378. <button class="close-btn" (click)="closeDesignerCalendar()">关闭</button>
  379. <!-- 统一使用控制流指令:@if 与 @for -->
  380. @if (selectedCalendarDesigners.length > 0) {
  381. <app-designer-calendar
  382. [designers]="selectedCalendarDesigners">
  383. </app-designer-calendar>
  384. } @else {
  385. <div class="empty">暂无数据</div>
  386. }
  387. </div>
  388. </div>
  389. }
  390. <!-- 设计师详细日历弹窗 -->
  391. @if (showDesignerCalendar && selectedDesignerForCalendar) {
  392. <div class="calendar-modal-overlay" (click)="closeDesignerCalendar()">
  393. <div class="calendar-modal-container" (click)="$event.stopPropagation()">
  394. <div class="calendar-modal-header">
  395. <h3>{{ selectedDesignerForCalendar.name }} - 详细日历</h3>
  396. <button class="close-btn" (click)="closeDesignerCalendar()">
  397. <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
  398. <line x1="18" y1="6" x2="6" y2="18"></line>
  399. <line x1="6" y1="6" x2="18" y2="18"></line>
  400. </svg>
  401. </button>
  402. </div>
  403. <div class="calendar-modal-body">
  404. <app-designer-calendar
  405. [designers]="selectedCalendarDesigners"
  406. [showSingleDesigner]="true"
  407. [timeRange]="calendarViewMode"
  408. ></app-designer-calendar>
  409. </div>
  410. </div>
  411. </div>
  412. }
  413. <!-- 空间分配弹窗 -->
  414. @if (selectedDesignerForSpaceAssignment) {
  415. <div class="space-assignment-overlay" (click)="closeSpaceAssignment()">
  416. <div class="space-assignment-container" (click)="$event.stopPropagation()">
  417. <div class="space-assignment-header">
  418. <div class="designer-preview">
  419. <div class="designer-avatar">
  420. @if (selectedDesignerForSpaceAssignment.avatar) {
  421. <img [src]="selectedDesignerForSpaceAssignment.avatar"
  422. [alt]="selectedDesignerForSpaceAssignment.name">
  423. } @else {
  424. <div class="avatar-placeholder">
  425. {{ selectedDesignerForSpaceAssignment.name.charAt(0) }}
  426. </div>
  427. }
  428. </div>
  429. <div class="designer-name">{{ selectedDesignerForSpaceAssignment.name }}</div>
  430. </div>
  431. <button class="close-btn" (click)="closeSpaceAssignment()">
  432. <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
  433. <line x1="18" y1="6" x2="6" y2="18"></line>
  434. <line x1="6" y1="6" x2="18" y2="18"></line>
  435. </svg>
  436. </button>
  437. </div>
  438. <div class="modal-content">
  439. <div class="space-selection-section">
  440. <h4 class="form-label">
  441. 指派空间场景
  442. <span class="required">*</span>
  443. </h4>
  444. <p class="form-help">请选择该设计师负责的空间(从Product表加载)</p>
  445. <!-- 加载状态 -->
  446. @if (loadingSpaces) {
  447. <div class="space-loading">
  448. <div class="spinner"></div>
  449. <span>正在加载空间数据...</span>
  450. </div>
  451. }
  452. <!-- 加载错误 -->
  453. @if (spaceLoadError && !loadingSpaces) {
  454. <div class="space-error">
  455. <svg class="icon-warning" viewBox="0 0 24 24">
  456. <path fill="currentColor" d="M12 2L1 21h22L12 2zm0 3.5L19.5 19h-15L12 5.5zM11 10v4h2v-4h-2zm0 6v2h2v-2h-2z"/>
  457. </svg>
  458. <span>{{ spaceLoadError }}</span>
  459. </div>
  460. }
  461. <!-- 空间列表 -->
  462. @if (!loadingSpaces && !spaceLoadError) {
  463. <div class="space-checkbox-list">
  464. @if (spaceScenes.length === 0) {
  465. <div class="space-empty">
  466. <svg class="icon-empty" viewBox="0 0 24 24">
  467. <path fill="currentColor" d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm-1-13h2v6h-2zm0 8h2v2h-2z"/>
  468. </svg>
  469. <p>该项目暂无空间数据</p>
  470. <small>请先在项目中创建空间产品(Product)</small>
  471. </div>
  472. } @else {
  473. @for (space of spaceScenes; track space.id) {
  474. <label class="space-checkbox-item">
  475. <input
  476. type="checkbox"
  477. [checked]="isSpaceSelected(selectedDesignerForSpaceAssignment.id, space.id)"
  478. (change)="toggleSpaceSelection(selectedDesignerForSpaceAssignment.id, space.id)"
  479. >
  480. <span class="checkbox-custom"></span>
  481. <div class="space-info">
  482. <span class="space-name">{{ space.name }}</span>
  483. @if (space.area) {
  484. <span class="space-area">{{ space.area }}㎡</span>
  485. }
  486. @if (space.description) {
  487. <span class="space-desc">{{ space.description }}</span>
  488. }
  489. </div>
  490. </label>
  491. }
  492. }
  493. </div>
  494. }
  495. </div>
  496. </div>
  497. <div class="space-assignment-footer">
  498. <button class="btn-secondary" (click)="closeSpaceAssignment()">取消</button>
  499. <button class="btn-primary" (click)="closeSpaceAssignment()">确认</button>
  500. </div>
  501. </div>
  502. </div>
  503. }
  504. <!-- 员工详情面板(复用组长端) -->
  505. @if (showEmployeeDetailPanel && employeeDetailData) {
  506. <app-employee-detail-panel
  507. [visible]="true"
  508. [employeeDetail]="employeeDetailData"
  509. (close)="closeEmployeeDetailPanel()"
  510. (calendarMonthChange)="changeEmployeeCalendarMonth($event)"
  511. (calendarDayClick)="onCalendarDayClick($event)"
  512. (projectClick)="onEmployeeDetailProjectClick($event)"
  513. (refreshSurvey)="refreshEmployeeSurvey()">
  514. </app-employee-detail-panel>
  515. }