quotation-approval.service.ts 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435
  1. import { Injectable, signal } from '@angular/core';
  2. import { Observable, of, BehaviorSubject } from 'rxjs';
  3. import { delay, map } from 'rxjs/operators';
  4. // 报价审核状态
  5. export type QuotationApprovalStatus = 'pending' | 'approved' | 'rejected' | 'revision_required';
  6. // 报价审核记录
  7. export interface QuotationApproval {
  8. id: string;
  9. quotationId: string;
  10. projectId: string;
  11. projectName: string;
  12. customerName: string;
  13. submittedBy: string; // 提交人
  14. submittedAt: Date;
  15. totalAmount: number;
  16. status: QuotationApprovalStatus;
  17. approvedBy?: string; // 审批人
  18. approvedAt?: Date;
  19. rejectionReason?: string;
  20. revisionNotes?: string;
  21. scriptUsed?: string; // 使用的话术模板ID
  22. communicationLog: CommunicationRecord[];
  23. }
  24. // 沟通记录
  25. export interface CommunicationRecord {
  26. id: string;
  27. timestamp: Date;
  28. type: 'script_used' | 'customer_response' | 'internal_note' | 'approval_comment';
  29. content: string;
  30. scriptId?: string; // 如果是使用话术,记录话术ID
  31. userId: string;
  32. userName: string;
  33. }
  34. // 话术使用统计
  35. export interface ScriptUsageStats {
  36. scriptId: string;
  37. scriptTitle: string;
  38. usageCount: number;
  39. successRate: number; // 成功率(客户接受报价的比例)
  40. avgResponseTime: number; // 平均响应时间(小时)
  41. lastUsed: string;
  42. }
  43. @Injectable({
  44. providedIn: 'root'
  45. })
  46. export class QuotationApprovalService {
  47. private approvals = signal<QuotationApproval[]>([]);
  48. private scriptUsageStats = signal<ScriptUsageStats[]>([]);
  49. // 实时通知流
  50. private approvalUpdates$ = new BehaviorSubject<QuotationApproval | null>(null);
  51. constructor() {
  52. this.initializeMockData();
  53. }
  54. // 初始化模拟数据
  55. private initializeMockData(): void {
  56. const mockApprovals: QuotationApproval[] = [
  57. {
  58. id: 'qa001',
  59. quotationId: 'q001',
  60. projectId: 'p001',
  61. projectName: '现代简约客厅设计',
  62. customerName: '张先生',
  63. submittedBy: '李设计师',
  64. submittedAt: new Date('2024-01-15T10:30:00'),
  65. totalAmount: 85000,
  66. status: 'pending',
  67. scriptUsed: 'script001',
  68. communicationLog: [
  69. {
  70. id: 'comm001',
  71. timestamp: new Date('2024-01-15T10:30:00'),
  72. type: 'script_used',
  73. content: '使用了"高端设计报价说明"话术模板',
  74. scriptId: 'script001',
  75. userId: 'designer001',
  76. userName: '李设计师'
  77. }
  78. ]
  79. },
  80. {
  81. id: 'qa002',
  82. quotationId: 'q002',
  83. projectId: 'p002',
  84. projectName: '北欧风卧室设计',
  85. customerName: '王女士',
  86. submittedBy: '陈设计师',
  87. submittedAt: new Date('2024-01-14T14:20:00'),
  88. totalAmount: 65000,
  89. status: 'approved',
  90. approvedBy: '财务主管',
  91. approvedAt: new Date('2024-01-14T16:45:00'),
  92. scriptUsed: 'script002',
  93. communicationLog: [
  94. {
  95. id: 'comm002',
  96. timestamp: new Date('2024-01-14T14:20:00'),
  97. type: 'script_used',
  98. content: '使用了"中档设计报价说明"话术模板',
  99. scriptId: 'script002',
  100. userId: 'designer002',
  101. userName: '陈设计师'
  102. },
  103. {
  104. id: 'comm003',
  105. timestamp: new Date('2024-01-14T16:45:00'),
  106. type: 'approval_comment',
  107. content: '报价合理,符合市场标准,批准通过',
  108. userId: 'finance001',
  109. userName: '财务主管'
  110. }
  111. ]
  112. }
  113. ];
  114. const mockStats: ScriptUsageStats[] = [
  115. {
  116. scriptId: 'script001',
  117. scriptTitle: '高端设计报价说明',
  118. usageCount: 15,
  119. successRate: 0.73,
  120. avgResponseTime: 2.5,
  121. lastUsed: '2024-01-15T10:30:00'
  122. },
  123. {
  124. scriptId: 'script002',
  125. scriptTitle: '中档设计报价说明',
  126. usageCount: 28,
  127. successRate: 0.82,
  128. avgResponseTime: 1.8,
  129. lastUsed: '2024-01-14T14:20:00'
  130. },
  131. {
  132. scriptId: 'script003',
  133. scriptTitle: '基础设计报价说明',
  134. usageCount: 35,
  135. successRate: 0.89,
  136. avgResponseTime: 1.2,
  137. lastUsed: '2024-01-13T09:15:00'
  138. }
  139. ];
  140. this.approvals.set(mockApprovals);
  141. this.scriptUsageStats.set(mockStats);
  142. }
  143. // 获取报价审核列表
  144. getQuotationApprovals(): Observable<QuotationApproval[]> {
  145. return of(this.approvals()).pipe(delay(300));
  146. }
  147. // 根据状态筛选报价审核记录
  148. getApprovalsByStatus(status: QuotationApprovalStatus): Observable<QuotationApproval[]> {
  149. return of(this.approvals().filter(approval => approval.status === status)).pipe(delay(300));
  150. }
  151. // 获取单个报价审核记录
  152. getApprovalById(id: string): Observable<QuotationApproval | undefined> {
  153. return of(this.approvals().find(approval => approval.id === id)).pipe(delay(200));
  154. }
  155. // 提交报价审核
  156. submitQuotationForApproval(quotationData: {
  157. quotationId: string;
  158. projectId: string;
  159. projectName: string;
  160. customerName: string;
  161. submittedBy: string;
  162. totalAmount: number;
  163. scriptUsed?: string;
  164. }): Observable<QuotationApproval> {
  165. const newApproval: QuotationApproval = {
  166. id: `qa${Date.now()}`,
  167. ...quotationData,
  168. submittedAt: new Date(),
  169. status: 'pending',
  170. communicationLog: quotationData.scriptUsed ? [{
  171. id: `comm${Date.now()}`,
  172. timestamp: new Date(),
  173. type: 'script_used',
  174. content: `使用了话术模板进行报价说明`,
  175. scriptId: quotationData.scriptUsed,
  176. userId: 'current_user',
  177. userName: quotationData.submittedBy
  178. }] : []
  179. };
  180. const updatedApprovals = [...this.approvals(), newApproval];
  181. this.approvals.set(updatedApprovals);
  182. // 通知更新
  183. this.approvalUpdates$.next(newApproval);
  184. return of(newApproval).pipe(delay(500));
  185. }
  186. // 审批报价
  187. approveQuotation(approvalId: string, approvedBy: string, comment?: string): Observable<QuotationApproval> {
  188. const approvals = this.approvals();
  189. const approvalIndex = approvals.findIndex(a => a.id === approvalId);
  190. if (approvalIndex === -1) {
  191. throw new Error('报价审核记录不存在');
  192. }
  193. const updatedApproval = {
  194. ...approvals[approvalIndex],
  195. status: 'approved' as QuotationApprovalStatus,
  196. approvedBy,
  197. approvedAt: new Date(),
  198. communicationLog: [
  199. ...approvals[approvalIndex].communicationLog,
  200. {
  201. id: `comm${Date.now()}`,
  202. timestamp: new Date(),
  203. type: 'approval_comment' as const,
  204. content: comment || '报价已批准',
  205. userId: 'current_user',
  206. userName: approvedBy
  207. }
  208. ]
  209. };
  210. const updatedApprovals = [...approvals];
  211. updatedApprovals[approvalIndex] = updatedApproval;
  212. this.approvals.set(updatedApprovals);
  213. // 通知更新
  214. this.approvalUpdates$.next(updatedApproval);
  215. return of(updatedApproval).pipe(delay(300));
  216. }
  217. // 拒绝报价
  218. rejectQuotation(approvalId: string, rejectedBy: string, reason: string): Observable<QuotationApproval> {
  219. const approvals = this.approvals();
  220. const approvalIndex = approvals.findIndex(a => a.id === approvalId);
  221. if (approvalIndex === -1) {
  222. throw new Error('报价审核记录不存在');
  223. }
  224. const updatedApproval = {
  225. ...approvals[approvalIndex],
  226. status: 'rejected' as QuotationApprovalStatus,
  227. approvedBy: rejectedBy,
  228. approvedAt: new Date(),
  229. rejectionReason: reason,
  230. communicationLog: [
  231. ...approvals[approvalIndex].communicationLog,
  232. {
  233. id: `comm${Date.now()}`,
  234. timestamp: new Date(),
  235. type: 'approval_comment' as const,
  236. content: `报价被拒绝:${reason}`,
  237. userId: 'current_user',
  238. userName: rejectedBy
  239. }
  240. ]
  241. };
  242. const updatedApprovals = [...approvals];
  243. updatedApprovals[approvalIndex] = updatedApproval;
  244. this.approvals.set(updatedApprovals);
  245. // 通知更新
  246. this.approvalUpdates$.next(updatedApproval);
  247. return of(updatedApproval).pipe(delay(300));
  248. }
  249. // 要求修订报价
  250. requestRevision(approvalId: string, requestedBy: string, notes: string): Observable<QuotationApproval> {
  251. const approvals = this.approvals();
  252. const approvalIndex = approvals.findIndex(a => a.id === approvalId);
  253. if (approvalIndex === -1) {
  254. throw new Error('报价审核记录不存在');
  255. }
  256. const updatedApproval = {
  257. ...approvals[approvalIndex],
  258. status: 'revision_required' as QuotationApprovalStatus,
  259. revisionNotes: notes,
  260. communicationLog: [
  261. ...approvals[approvalIndex].communicationLog,
  262. {
  263. id: `comm${Date.now()}`,
  264. timestamp: new Date(),
  265. type: 'approval_comment' as const,
  266. content: `要求修订:${notes}`,
  267. userId: 'current_user',
  268. userName: requestedBy
  269. }
  270. ]
  271. };
  272. const updatedApprovals = [...approvals];
  273. updatedApprovals[approvalIndex] = updatedApproval;
  274. this.approvals.set(updatedApprovals);
  275. // 通知更新
  276. this.approvalUpdates$.next(updatedApproval);
  277. return of(updatedApproval).pipe(delay(300));
  278. }
  279. // 记录话术使用
  280. recordScriptUsage(approvalId: string, scriptId: string, userId: string, userName: string): Observable<void> {
  281. const approvals = this.approvals();
  282. const approvalIndex = approvals.findIndex(a => a.id === approvalId);
  283. if (approvalIndex !== -1) {
  284. const communicationRecord: CommunicationRecord = {
  285. id: `comm${Date.now()}`,
  286. timestamp: new Date(),
  287. type: 'script_used',
  288. content: '使用了话术模板进行沟通',
  289. scriptId,
  290. userId,
  291. userName
  292. };
  293. const updatedApproval = {
  294. ...approvals[approvalIndex],
  295. communicationLog: [...approvals[approvalIndex].communicationLog, communicationRecord]
  296. };
  297. const updatedApprovals = [...approvals];
  298. updatedApprovals[approvalIndex] = updatedApproval;
  299. this.approvals.set(updatedApprovals);
  300. // 更新话术使用统计
  301. this.updateScriptUsageStats(scriptId);
  302. }
  303. return of(void 0).pipe(delay(200));
  304. }
  305. // 记录客户响应
  306. recordCustomerResponse(approvalId: string, response: string, userId: string, userName: string): Observable<void> {
  307. const approvals = this.approvals();
  308. const approvalIndex = approvals.findIndex(a => a.id === approvalId);
  309. if (approvalIndex !== -1) {
  310. const communicationRecord: CommunicationRecord = {
  311. id: `comm${Date.now()}`,
  312. timestamp: new Date(),
  313. type: 'customer_response',
  314. content: response,
  315. userId,
  316. userName
  317. };
  318. const updatedApproval = {
  319. ...approvals[approvalIndex],
  320. communicationLog: [...approvals[approvalIndex].communicationLog, communicationRecord]
  321. };
  322. const updatedApprovals = [...approvals];
  323. updatedApprovals[approvalIndex] = updatedApproval;
  324. this.approvals.set(updatedApprovals);
  325. }
  326. return of(void 0).pipe(delay(200));
  327. }
  328. // 获取话术使用统计
  329. getScriptUsageStats(): Observable<ScriptUsageStats[]> {
  330. return of(this.scriptUsageStats()).pipe(delay(300));
  331. }
  332. // 获取实时更新流
  333. getApprovalUpdates(): Observable<QuotationApproval | null> {
  334. return this.approvalUpdates$.asObservable();
  335. }
  336. // 更新话术使用统计
  337. private updateScriptUsageStats(scriptId: string): void {
  338. const stats = this.scriptUsageStats();
  339. const statIndex = stats.findIndex(s => s.scriptId === scriptId);
  340. if (statIndex !== -1) {
  341. const updatedStats = [...stats];
  342. updatedStats[statIndex] = {
  343. ...updatedStats[statIndex],
  344. usageCount: updatedStats[statIndex].usageCount + 1,
  345. lastUsed: new Date().toISOString()
  346. };
  347. this.scriptUsageStats.set(updatedStats);
  348. }
  349. }
  350. // 获取审核统计数据
  351. getApprovalStats(): Observable<{
  352. total: number;
  353. pending: number;
  354. approved: number;
  355. rejected: number;
  356. revisionRequired: number;
  357. avgApprovalTime: number; // 平均审批时间(小时)
  358. }> {
  359. const approvals = this.approvals();
  360. const total = approvals.length;
  361. const pending = approvals.filter(a => a.status === 'pending').length;
  362. const approved = approvals.filter(a => a.status === 'approved').length;
  363. const rejected = approvals.filter(a => a.status === 'rejected').length;
  364. const revisionRequired = approvals.filter(a => a.status === 'revision_required').length;
  365. // 计算平均审批时间
  366. const completedApprovals = approvals.filter(a => a.approvedAt);
  367. const avgApprovalTime = completedApprovals.length > 0
  368. ? completedApprovals.reduce((sum, approval) => {
  369. const timeDiff = approval.approvedAt!.getTime() - approval.submittedAt.getTime();
  370. return sum + (timeDiff / (1000 * 60 * 60)); // 转换为小时
  371. }, 0) / completedApprovals.length
  372. : 0;
  373. return of({
  374. total,
  375. pending,
  376. approved,
  377. rejected,
  378. revisionRequired,
  379. avgApprovalTime
  380. }).pipe(delay(200));
  381. }
  382. }