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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522
  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. {{ designer.name }}
  173. @if (designer.isTeamLeader) {
  174. <span class="leader-badge">组长</span>
  175. }
  176. </div>
  177. <div class="designer-status">
  178. <span class="status-text" [style.color]="getDesignerStatusColor(designer.status)">
  179. {{ getDesignerStatusText(designer.status) }}
  180. </span>
  181. <span class="project-count" [style.color]="getDesignerStatusColor(designer.status)">
  182. {{ designer.currentProjects }}个项目
  183. </span>
  184. </div>
  185. <div class="designer-metrics">
  186. <div class="metric-item" [class]="getIdleDaysClass(designer.idleDays)">
  187. <span class="metric-label">{{ getRecentOrdersText(designer) }}</span>
  188. </div>
  189. @if (designer.availableDates.length > 0) {
  190. <div class="metric-item">
  191. <span class="metric-label">{{ getAvailableDatesText(designer) }}</span>
  192. </div>
  193. }
  194. @if (designer.reviewDates.length > 0) {
  195. <div class="metric-item review-dates">
  196. <span class="metric-label">
  197. 对图日期:{{ designer.reviewDates.slice(0, 2).join(', ') }}
  198. @if (designer.reviewDates.length > 2) {
  199. <span class="more-dates">等{{ designer.reviewDates.length }}个</span>
  200. }
  201. </span>
  202. </div>
  203. }
  204. </div>
  205. <div class="designer-skills">
  206. @for (skill of designer.skills.slice(0, 2); track skill) {
  207. <span class="skill-tag">{{ skill }}</span>
  208. }
  209. @if (designer.skills.length > 2) {
  210. <span class="skill-more">+{{ designer.skills.length - 2 }}</span>
  211. }
  212. </div>
  213. </div>
  214. <div class="designer-actions">
  215. <button
  216. class="calendar-btn"
  217. (click)="$event.stopPropagation(); showDesignerEmployeeDetail(designer)"
  218. title="查看设计师详情"
  219. >
  220. 👤 详情
  221. </button>
  222. @if (enableSpaceAssignment && spaceScenes.length > 0) {
  223. <button
  224. class="space-assign-btn"
  225. (click)="$event.stopPropagation(); openSpaceAssignment(designer)"
  226. title="分配空间"
  227. >
  228. 🏠
  229. </button>
  230. }
  231. </div>
  232. @if (enableSpaceAssignment && isDesignerSelected(designer)) {
  233. <div class="designer-spaces-info">
  234. <span class="spaces-label">负责空间:</span>
  235. <span class="spaces-value">{{ getDesignerSpacesText(designer.id) }}</span>
  236. </div>
  237. }
  238. </div>
  239. }
  240. </div>
  241. </div>
  242. <!-- 跨组合作选项 -->
  243. <div class="cross-team-section">
  244. <div class="cross-team-header">
  245. <label class="checkbox-label">
  246. <input
  247. type="checkbox"
  248. [(ngModel)]="allowCrossTeamSelection"
  249. >
  250. <span class="checkmark"></span>
  251. 允许跨组合作
  252. </label>
  253. <span class="cross-team-hint">可从其他项目组选择成员参与协作</span>
  254. </div>
  255. @if (allowCrossTeamSelection) {
  256. <div class="cross-team-designers">
  257. <h4>其他项目组成员</h4>
  258. <div class="designer-grid">
  259. @for (designer of getOtherTeamDesigners(); track designer.id) {
  260. <div
  261. class="designer-card cross-team"
  262. [class.selected]="isCrossTeamCollaborator(designer)"
  263. [class.status-idle]="designer.status === 'idle'"
  264. [class.status-reviewing]="designer.status === 'reviewing'"
  265. [class.status-stagnant]="designer.status === 'stagnant'"
  266. (click)="toggleCrossTeamCollaborator(designer)"
  267. >
  268. <div class="designer-avatar">
  269. @if (designer.avatar) {
  270. <img [src]="designer.avatar" [alt]="designer.name">
  271. } @else {
  272. <div class="avatar-placeholder">{{ designer.name.charAt(0) }}</div>
  273. }
  274. <div class="status-dot" [style.background-color]="getDesignerStatusColor(designer.status)"></div>
  275. </div>
  276. <div class="designer-info">
  277. <div class="designer-name">
  278. {{ designer.name }}
  279. <span class="team-tag">{{ designer.teamName }}</span>
  280. </div>
  281. <div class="designer-status">
  282. <span class="status-text" [style.color]="getDesignerStatusColor(designer.status)">
  283. {{ getDesignerStatusText(designer.status) }}
  284. </span>
  285. <span class="workload" [class]="getWorkloadClass(designer.workload)">
  286. {{ designer.workload }}%
  287. </span>
  288. </div>
  289. <div class="designer-metrics">
  290. <div class="metric-item" [class]="getIdleDaysClass(designer.idleDays)">
  291. <span class="metric-label">{{ getRecentOrdersText(designer) }}</span>
  292. </div>
  293. </div>
  294. </div>
  295. <div class="designer-actions">
  296. <button
  297. class="calendar-btn"
  298. (click)="$event.stopPropagation(); showDesignerEmployeeDetail(designer)"
  299. title="查看设计师详情"
  300. >
  301. 👤 详情
  302. </button>
  303. </div>
  304. </div>
  305. }
  306. </div>
  307. </div>
  308. }
  309. </div>
  310. </div>
  311. }
  312. </div>
  313. <div class="modal-footer">
  314. <div class="selection-summary">
  315. @if (selectedDesigners.length > 0) {
  316. <div class="summary-item">
  317. <span class="summary-label">主要团队:</span>
  318. <span class="summary-value">{{ getSelectedDesignersNames() }}</span>
  319. </div>
  320. }
  321. @if (internalCrossTeamCollaborators.length > 0) {
  322. <div class="summary-item">
  323. <span class="summary-label">跨组合作:</span>
  324. <span class="summary-value">{{ getCrossTeamCollaboratorsNames() }}</span>
  325. </div>
  326. }
  327. </div>
  328. <div class="modal-actions">
  329. <button class="btn-secondary" (click)="closeModal()">取消</button>
  330. <button
  331. class="btn-primary"
  332. [disabled]="!canConfirmAssignment()"
  333. (click)="confirmAssignment()"
  334. >
  335. 确认分配
  336. </button>
  337. </div>
  338. </div>
  339. </div>
  340. </div>
  341. <!-- 设计师日历弹窗 -->
  342. @if (showDesignerCalendar) {
  343. <div class="designer-calendar-modal">
  344. <div class="modal-mask" (click)="closeDesignerCalendar()"></div>
  345. <div class="modal-content">
  346. <h3>设计师工作日历</h3>
  347. <button class="close-btn" (click)="closeDesignerCalendar()">关闭</button>
  348. <!-- 统一使用控制流指令:@if 与 @for -->
  349. @if (selectedCalendarDesigners.length > 0) {
  350. <app-designer-calendar
  351. [designers]="selectedCalendarDesigners">
  352. </app-designer-calendar>
  353. } @else {
  354. <div class="empty">暂无数据</div>
  355. }
  356. </div>
  357. </div>
  358. }
  359. <!-- 设计师详细日历弹窗 -->
  360. @if (showDesignerCalendar && selectedDesignerForCalendar) {
  361. <div class="calendar-modal-overlay" (click)="closeDesignerCalendar()">
  362. <div class="calendar-modal-container" (click)="$event.stopPropagation()">
  363. <div class="calendar-modal-header">
  364. <h3>{{ selectedDesignerForCalendar.name }} - 详细日历</h3>
  365. <button class="close-btn" (click)="closeDesignerCalendar()">
  366. <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
  367. <line x1="18" y1="6" x2="6" y2="18"></line>
  368. <line x1="6" y1="6" x2="18" y2="18"></line>
  369. </svg>
  370. </button>
  371. </div>
  372. <div class="calendar-modal-body">
  373. <app-designer-calendar
  374. [designers]="selectedCalendarDesigners"
  375. [showSingleDesigner]="true"
  376. [timeRange]="calendarViewMode"
  377. ></app-designer-calendar>
  378. </div>
  379. </div>
  380. </div>
  381. }
  382. <!-- 空间分配弹窗 -->
  383. @if (selectedDesignerForSpaceAssignment) {
  384. <div class="space-assignment-overlay" (click)="closeSpaceAssignment()">
  385. <div class="space-assignment-container" (click)="$event.stopPropagation()">
  386. <div class="space-assignment-header">
  387. <div class="designer-preview">
  388. <div class="designer-avatar">
  389. @if (selectedDesignerForSpaceAssignment.avatar) {
  390. <img [src]="selectedDesignerForSpaceAssignment.avatar"
  391. [alt]="selectedDesignerForSpaceAssignment.name">
  392. } @else {
  393. <div class="avatar-placeholder">
  394. {{ selectedDesignerForSpaceAssignment.name.charAt(0) }}
  395. </div>
  396. }
  397. </div>
  398. <div class="designer-name">{{ selectedDesignerForSpaceAssignment.name }}</div>
  399. </div>
  400. <button class="close-btn" (click)="closeSpaceAssignment()">
  401. <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
  402. <line x1="18" y1="6" x2="6" y2="18"></line>
  403. <line x1="6" y1="6" x2="18" y2="18"></line>
  404. </svg>
  405. </button>
  406. </div>
  407. <div class="modal-content">
  408. <div class="space-selection-section">
  409. <h4 class="form-label">
  410. 指派空间场景
  411. <span class="required">*</span>
  412. </h4>
  413. <p class="form-help">请选择该设计师负责的空间(从Product表加载)</p>
  414. <!-- 加载状态 -->
  415. @if (loadingSpaces) {
  416. <div class="space-loading">
  417. <div class="spinner"></div>
  418. <span>正在加载空间数据...</span>
  419. </div>
  420. }
  421. <!-- 加载错误 -->
  422. @if (spaceLoadError && !loadingSpaces) {
  423. <div class="space-error">
  424. <svg class="icon-warning" viewBox="0 0 24 24">
  425. <path fill="currentColor" d="M12 2L1 21h22L12 2zm0 3.5L19.5 19h-15L12 5.5zM11 10v4h2v-4h-2zm0 6v2h2v-2h-2z"/>
  426. </svg>
  427. <span>{{ spaceLoadError }}</span>
  428. </div>
  429. }
  430. <!-- 空间列表 -->
  431. @if (!loadingSpaces && !spaceLoadError) {
  432. <div class="space-checkbox-list">
  433. @if (spaceScenes.length === 0) {
  434. <div class="space-empty">
  435. <svg class="icon-empty" viewBox="0 0 24 24">
  436. <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"/>
  437. </svg>
  438. <p>该项目暂无空间数据</p>
  439. <small>请先在项目中创建空间产品(Product)</small>
  440. </div>
  441. } @else {
  442. @for (space of spaceScenes; track space.id) {
  443. <label class="space-checkbox-item">
  444. <input
  445. type="checkbox"
  446. [checked]="isSpaceSelected(selectedDesignerForSpaceAssignment.id, space.id)"
  447. (change)="toggleSpaceSelection(selectedDesignerForSpaceAssignment.id, space.id)"
  448. >
  449. <span class="checkbox-custom"></span>
  450. <div class="space-info">
  451. <span class="space-name">{{ space.name }}</span>
  452. @if (space.area) {
  453. <span class="space-area">{{ space.area }}㎡</span>
  454. }
  455. @if (space.description) {
  456. <span class="space-desc">{{ space.description }}</span>
  457. }
  458. </div>
  459. </label>
  460. }
  461. }
  462. </div>
  463. }
  464. </div>
  465. </div>
  466. <div class="space-assignment-footer">
  467. <button class="btn-secondary" (click)="closeSpaceAssignment()">取消</button>
  468. <button class="btn-primary" (click)="closeSpaceAssignment()">确认</button>
  469. </div>
  470. </div>
  471. </div>
  472. }
  473. <!-- 员工详情面板(复用组长端) -->
  474. @if (showEmployeeDetailPanel && employeeDetailData) {
  475. <app-employee-detail-panel
  476. [visible]="true"
  477. [employeeDetail]="employeeDetailData"
  478. (close)="closeEmployeeDetailPanel()"
  479. (calendarMonthChange)="changeEmployeeCalendarMonth($event)"
  480. (calendarDayClick)="onCalendarDayClick($event)"
  481. (projectClick)="onEmployeeDetailProjectClick($event)"
  482. (refreshSurvey)="refreshEmployeeSurvey()">
  483. </app-employee-detail-panel>
  484. }