|
@@ -0,0 +1,823 @@
|
|
|
+import { Component, OnInit, signal, computed, Inject } from '@angular/core';
|
|
|
+import { CommonModule } from '@angular/common';
|
|
|
+import { FormsModule } from '@angular/forms';
|
|
|
+import { MatButtonModule } from '@angular/material/button';
|
|
|
+import { MatCardModule } from '@angular/material/card';
|
|
|
+import { MatIconModule } from '@angular/material/icon';
|
|
|
+import { MatDialogModule, MatDialog, MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
|
|
|
+import { MatTabsModule } from '@angular/material/tabs';
|
|
|
+import { MatTooltipModule } from '@angular/material/tooltip';
|
|
|
+import { MatTableModule } from '@angular/material/table';
|
|
|
+import { Asset, AssetType, AssetStatus, AssetAssignment, Employee } from '../../../models/hr.model';
|
|
|
+
|
|
|
+// 资产分配对话框组件
|
|
|
+@Component({
|
|
|
+ selector: 'app-asset-assignment-dialog',
|
|
|
+ standalone: true,
|
|
|
+ imports: [
|
|
|
+ CommonModule,
|
|
|
+ FormsModule,
|
|
|
+ MatButtonModule,
|
|
|
+ MatIconModule
|
|
|
+ ],
|
|
|
+ template: `
|
|
|
+ <div class="dialog-header">
|
|
|
+ <h2>{{ isEdit ? '修改资产分配' : '分配资产' }}</h2>
|
|
|
+ <button class="close-btn" (click)="dialogRef.close()">
|
|
|
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
|
+ <line x1="18" y1="6" x2="6" y2="18"></line>
|
|
|
+ <line x1="6" y1="6" x2="18" y2="18"></line>
|
|
|
+ </svg>
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ <div class="dialog-content">
|
|
|
+ <div class="info-item">
|
|
|
+ <label>资产名称:</label>
|
|
|
+ <span>{{ asset.name }}</span>
|
|
|
+ </div>
|
|
|
+ <div class="form-group">
|
|
|
+ <label>选择员工:</label>
|
|
|
+ <input
|
|
|
+ type="text"
|
|
|
+ [(ngModel)]="selectedEmployeeName"
|
|
|
+ (input)="filterEmployees($event.target.value)"
|
|
|
+ placeholder="搜索员工姓名或工号..."
|
|
|
+ class="employee-search"
|
|
|
+ [disabled]="isReturning"
|
|
|
+ >
|
|
|
+ <div class="employee-dropdown" *ngIf="showEmployeeDropdown && !isReturning">
|
|
|
+ <div *ngFor="let employee of filteredEmployees"
|
|
|
+ class="employee-item"
|
|
|
+ (click)="selectEmployee(employee)">
|
|
|
+ <span class="employee-name">{{ employee.name }}</span>
|
|
|
+ <span class="employee-id">{{ employee.employeeId }}</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="form-group" *ngIf="!isReturning">
|
|
|
+ <label>分配开始日期:</label>
|
|
|
+ <input type="date" [(ngModel)]="startDate" class="date-input">
|
|
|
+ </div>
|
|
|
+ <div class="form-group" *ngIf="!isReturning">
|
|
|
+ <label>预计归还日期:</label>
|
|
|
+ <input type="date" [(ngModel)]="endDate" class="date-input">
|
|
|
+ </div>
|
|
|
+ <div class="form-group" *ngIf="isReturning">
|
|
|
+ <label>实际归还日期:</label>
|
|
|
+ <input type="date" [(ngModel)]="returnDate" class="date-input">
|
|
|
+ </div>
|
|
|
+ <div class="form-group">
|
|
|
+ <label>备注:</label>
|
|
|
+ <textarea
|
|
|
+ [(ngModel)]="notes"
|
|
|
+ placeholder="请输入备注信息..."
|
|
|
+ rows="3"
|
|
|
+ class="notes-input"
|
|
|
+ ></textarea>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="dialog-actions">
|
|
|
+ <button mat-button (click)="dialogRef.close()">取消</button>
|
|
|
+ <button mat-raised-button color="primary" (click)="submit()">
|
|
|
+ {{ isReturning ? '确认归还' : '确认分配' }}
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ `,
|
|
|
+ styles: [`
|
|
|
+ .dialog-header {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+ margin-bottom: 24px;
|
|
|
+ padding-bottom: 16px;
|
|
|
+ border-bottom: 1px solid #e5e7eb;
|
|
|
+ }
|
|
|
+ .close-btn {
|
|
|
+ background: none;
|
|
|
+ border: none;
|
|
|
+ cursor: pointer;
|
|
|
+ color: #6b7280;
|
|
|
+ padding: 4px;
|
|
|
+ }
|
|
|
+ .dialog-content {
|
|
|
+ max-width: 500px;
|
|
|
+ }
|
|
|
+ .info-item {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ margin-bottom: 16px;
|
|
|
+ padding: 8px 0;
|
|
|
+ border-bottom: 1px solid #f3f4f6;
|
|
|
+ }
|
|
|
+ .form-group {
|
|
|
+ margin-bottom: 20px;
|
|
|
+ position: relative;
|
|
|
+ }
|
|
|
+ label {
|
|
|
+ display: block;
|
|
|
+ margin-bottom: 4px;
|
|
|
+ font-weight: 500;
|
|
|
+ color: #374151;
|
|
|
+ }
|
|
|
+ .employee-search,
|
|
|
+ .date-input {
|
|
|
+ width: 100%;
|
|
|
+ padding: 8px 12px;
|
|
|
+ border: 1px solid #e5e7eb;
|
|
|
+ border-radius: 6px;
|
|
|
+ font-size: 14px;
|
|
|
+ }
|
|
|
+ .employee-dropdown {
|
|
|
+ position: absolute;
|
|
|
+ top: 100%;
|
|
|
+ left: 0;
|
|
|
+ right: 0;
|
|
|
+ background-color: white;
|
|
|
+ border: 1px solid #e5e7eb;
|
|
|
+ border-radius: 6px;
|
|
|
+ max-height: 200px;
|
|
|
+ overflow-y: auto;
|
|
|
+ box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
|
|
+ z-index: 1000;
|
|
|
+ }
|
|
|
+ .employee-item {
|
|
|
+ padding: 10px 12px;
|
|
|
+ cursor: pointer;
|
|
|
+ transition: background-color 0.2s;
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+ }
|
|
|
+ .employee-item:hover {
|
|
|
+ background-color: #f3f4f6;
|
|
|
+ }
|
|
|
+ .employee-name {
|
|
|
+ font-weight: 500;
|
|
|
+ color: #1f2937;
|
|
|
+ }
|
|
|
+ .employee-id {
|
|
|
+ font-size: 12px;
|
|
|
+ color: #6b7280;
|
|
|
+ }
|
|
|
+ .notes-input {
|
|
|
+ width: 100%;
|
|
|
+ padding: 8px 12px;
|
|
|
+ border: 1px solid #e5e7eb;
|
|
|
+ border-radius: 6px;
|
|
|
+ font-size: 14px;
|
|
|
+ resize: vertical;
|
|
|
+ }
|
|
|
+ .dialog-actions {
|
|
|
+ display: flex;
|
|
|
+ justify-content: flex-end;
|
|
|
+ gap: 12px;
|
|
|
+ margin-top: 24px;
|
|
|
+ }
|
|
|
+ `]
|
|
|
+}) class AssetAssignmentDialog {
|
|
|
+ asset: Asset;
|
|
|
+ employees: Employee[] = [];
|
|
|
+ filteredEmployees: Employee[] = [];
|
|
|
+ showEmployeeDropdown = false;
|
|
|
+ selectedEmployeeId = '';
|
|
|
+ selectedEmployeeName = '';
|
|
|
+ startDate = new Date().toISOString().split('T')[0];
|
|
|
+ endDate = '';
|
|
|
+ returnDate = new Date().toISOString().split('T')[0];
|
|
|
+ notes = '';
|
|
|
+ isEdit = false;
|
|
|
+ isReturning = false;
|
|
|
+
|
|
|
+ constructor(
|
|
|
+ public dialogRef: MatDialogRef<AssetAssignmentDialog>,
|
|
|
+ @Inject(MAT_DIALOG_DATA) public data: any
|
|
|
+ ) {
|
|
|
+ this.asset = data.asset;
|
|
|
+ this.employees = data.employees;
|
|
|
+ this.filteredEmployees = [...this.employees];
|
|
|
+ this.isEdit = data.isEdit || false;
|
|
|
+ this.isReturning = data.isReturning || false;
|
|
|
+
|
|
|
+ if (this.isEdit && data.assignment) {
|
|
|
+ const assignment = data.assignment;
|
|
|
+ const employee = this.employees.find(e => e.id === assignment.employeeId);
|
|
|
+ if (employee) {
|
|
|
+ this.selectedEmployeeId = employee.id;
|
|
|
+ this.selectedEmployeeName = employee.name;
|
|
|
+ }
|
|
|
+ if (assignment.startDate) {
|
|
|
+ this.startDate = new Date(assignment.startDate).toISOString().split('T')[0];
|
|
|
+ }
|
|
|
+ if (assignment.endDate) {
|
|
|
+ this.endDate = new Date(assignment.endDate).toISOString().split('T')[0];
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ filterEmployees(query: string) {
|
|
|
+ if (!query.trim()) {
|
|
|
+ this.filteredEmployees = [...this.employees];
|
|
|
+ } else {
|
|
|
+ const term = query.toLowerCase();
|
|
|
+ this.filteredEmployees = this.employees.filter(emp =>
|
|
|
+ emp.name.toLowerCase().includes(term) ||
|
|
|
+ emp.employeeId.toLowerCase().includes(term)
|
|
|
+ );
|
|
|
+ }
|
|
|
+ this.showEmployeeDropdown = true;
|
|
|
+ }
|
|
|
+
|
|
|
+ selectEmployee(employee: Employee) {
|
|
|
+ this.selectedEmployeeId = employee.id;
|
|
|
+ this.selectedEmployeeName = employee.name;
|
|
|
+ this.showEmployeeDropdown = false;
|
|
|
+ }
|
|
|
+
|
|
|
+ submit() {
|
|
|
+ if (!this.selectedEmployeeId && !this.isReturning) {
|
|
|
+ alert('请选择员工');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ this.dialogRef.close({
|
|
|
+ employeeId: this.selectedEmployeeId,
|
|
|
+ startDate: this.startDate,
|
|
|
+ endDate: this.isReturning ? this.returnDate : this.endDate,
|
|
|
+ notes: this.notes
|
|
|
+ });
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 报修对话框组件
|
|
|
+@Component({
|
|
|
+ selector: 'app-asset-repair-dialog',
|
|
|
+ standalone: true,
|
|
|
+ imports: [
|
|
|
+ CommonModule,
|
|
|
+ FormsModule,
|
|
|
+ MatButtonModule,
|
|
|
+ MatIconModule
|
|
|
+ ],
|
|
|
+ template: `
|
|
|
+ <div class="dialog-header">
|
|
|
+ <h2>资产报修</h2>
|
|
|
+ <button class="close-btn" (click)="dialogRef.close()">
|
|
|
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
|
+ <line x1="18" y1="6" x2="6" y2="18"></line>
|
|
|
+ <line x1="6" y1="6" x2="18" y2="18"></line>
|
|
|
+ </svg>
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ <div class="dialog-content">
|
|
|
+ <div class="info-item">
|
|
|
+ <label>资产名称:</label>
|
|
|
+ <span>{{ asset.name }}</span>
|
|
|
+ </div>
|
|
|
+ <div class="info-item">
|
|
|
+ <label>资产类型:</label>
|
|
|
+ <span>{{ asset.type }}</span>
|
|
|
+ </div>
|
|
|
+ <div class="info-item">
|
|
|
+ <label>序列号:</label>
|
|
|
+ <span>{{ asset.serialNumber || '无' }}</span>
|
|
|
+ </div>
|
|
|
+ <div class="form-group">
|
|
|
+ <label>故障描述 *:</label>
|
|
|
+ <textarea
|
|
|
+ [(ngModel)]="problemDescription"
|
|
|
+ placeholder="请详细描述故障情况..."
|
|
|
+ rows="4"
|
|
|
+ class="description-input"
|
|
|
+ required
|
|
|
+ ></textarea>
|
|
|
+ </div>
|
|
|
+ <div class="form-group">
|
|
|
+ <label>期望修复时间:</label>
|
|
|
+ <input type="date" [(ngModel)]="expectedFixDate" class="date-input">
|
|
|
+ </div>
|
|
|
+ <div class="form-group">
|
|
|
+ <label>联系人:</label>
|
|
|
+ <input type="text" [(ngModel)]="contactPerson" class="contact-input" placeholder="请输入联系人姓名">
|
|
|
+ </div>
|
|
|
+ <div class="form-group">
|
|
|
+ <label>联系电话:</label>
|
|
|
+ <input type="text" [(ngModel)]="contactPhone" class="phone-input" placeholder="请输入联系电话">
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="dialog-actions">
|
|
|
+ <button mat-button (click)="dialogRef.close()">取消</button>
|
|
|
+ <button mat-raised-button color="primary" (click)="submit()" [disabled]="!problemDescription.trim()">
|
|
|
+ 提交报修申请
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ `,
|
|
|
+ styles: [`
|
|
|
+ .dialog-header {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+ margin-bottom: 24px;
|
|
|
+ padding-bottom: 16px;
|
|
|
+ border-bottom: 1px solid #e5e7eb;
|
|
|
+ }
|
|
|
+ .close-btn {
|
|
|
+ background: none;
|
|
|
+ border: none;
|
|
|
+ cursor: pointer;
|
|
|
+ color: #6b7280;
|
|
|
+ padding: 4px;
|
|
|
+ }
|
|
|
+ .dialog-content {
|
|
|
+ max-width: 500px;
|
|
|
+ }
|
|
|
+ .info-item {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ margin-bottom: 16px;
|
|
|
+ padding: 8px 0;
|
|
|
+ border-bottom: 1px solid #f3f4f6;
|
|
|
+ }
|
|
|
+ .form-group {
|
|
|
+ margin-bottom: 20px;
|
|
|
+ }
|
|
|
+ label {
|
|
|
+ display: block;
|
|
|
+ margin-bottom: 4px;
|
|
|
+ font-weight: 500;
|
|
|
+ color: #374151;
|
|
|
+ }
|
|
|
+ .description-input,
|
|
|
+ .date-input,
|
|
|
+ .contact-input,
|
|
|
+ .phone-input {
|
|
|
+ width: 100%;
|
|
|
+ padding: 8px 12px;
|
|
|
+ border: 1px solid #e5e7eb;
|
|
|
+ border-radius: 6px;
|
|
|
+ font-size: 14px;
|
|
|
+ }
|
|
|
+ .description-input {
|
|
|
+ resize: vertical;
|
|
|
+ }
|
|
|
+ .dialog-actions {
|
|
|
+ display: flex;
|
|
|
+ justify-content: flex-end;
|
|
|
+ gap: 12px;
|
|
|
+ margin-top: 24px;
|
|
|
+ }
|
|
|
+ `]
|
|
|
+}) class AssetRepairDialog {
|
|
|
+ asset: Asset;
|
|
|
+ problemDescription = '';
|
|
|
+ expectedFixDate = new Date().toISOString().split('T')[0];
|
|
|
+ contactPerson = '';
|
|
|
+ contactPhone = '';
|
|
|
+
|
|
|
+ constructor(
|
|
|
+ public dialogRef: MatDialogRef<AssetRepairDialog>,
|
|
|
+ @Inject(MAT_DIALOG_DATA) public data: any
|
|
|
+ ) {
|
|
|
+ this.asset = data.asset;
|
|
|
+ }
|
|
|
+
|
|
|
+ submit() {
|
|
|
+ if (!this.problemDescription.trim()) {
|
|
|
+ alert('请填写故障描述');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ this.dialogRef.close({
|
|
|
+ problemDescription: this.problemDescription,
|
|
|
+ expectedFixDate: this.expectedFixDate,
|
|
|
+ contactPerson: this.contactPerson,
|
|
|
+ contactPhone: this.contactPhone
|
|
|
+ });
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 生成模拟资产数据
|
|
|
+const generateMockAssets = (): Asset[] => {
|
|
|
+ const assets: Asset[] = [];
|
|
|
+ const types: AssetType[] = ['电脑', '外设', '软件账号', '域名', '其他'];
|
|
|
+ const statuses: AssetStatus[] = ['空闲', '占用', '故障', '报修中'];
|
|
|
+ const departments = ['设计部', '客户服务部', '财务部', '人力资源部'];
|
|
|
+ const computerModels = ['MacBook Pro', 'Dell XPS', 'HP EliteBook', 'Lenovo ThinkPad', 'Surface Laptop'];
|
|
|
+ const peripheralTypes = ['显示器', '键盘', '鼠标', '打印机', '扫描仪', '投影仪'];
|
|
|
+ const softwareTypes = ['Adobe Creative Cloud', 'AutoCAD', 'Office 365', '渲染农场账号', '素材库账号'];
|
|
|
+ const otherAssets = ['办公桌', '办公椅', '服务器', '网络设备', '空调'];
|
|
|
+
|
|
|
+ for (let i = 1; i <= 30; i++) {
|
|
|
+ const type = types[Math.floor(Math.random() * types.length)];
|
|
|
+ const status = statuses[Math.floor(Math.random() * statuses.length)];
|
|
|
+ const purchaseDate = new Date();
|
|
|
+ purchaseDate.setMonth(purchaseDate.getMonth() - Math.floor(Math.random() * 36));
|
|
|
+
|
|
|
+ let name = '';
|
|
|
+ let serialNumber = `SN${Math.floor(Math.random() * 1000000)}`;
|
|
|
+
|
|
|
+ switch (type) {
|
|
|
+ case '电脑':
|
|
|
+ name = `${computerModels[Math.floor(Math.random() * computerModels.length)]} ${2020 + Math.floor(Math.random() * 4)}`;
|
|
|
+ break;
|
|
|
+ case '外设':
|
|
|
+ name = peripheralTypes[Math.floor(Math.random() * peripheralTypes.length)];
|
|
|
+ break;
|
|
|
+ case '软件账号':
|
|
|
+ name = softwareTypes[Math.floor(Math.random() * softwareTypes.length)];
|
|
|
+ serialNumber = '无';
|
|
|
+ break;
|
|
|
+ case '域名':
|
|
|
+ name = `example-${i}.com`;
|
|
|
+ serialNumber = '无';
|
|
|
+ break;
|
|
|
+ case '其他':
|
|
|
+ name = otherAssets[Math.floor(Math.random() * otherAssets.length)];
|
|
|
+ break;
|
|
|
+ }
|
|
|
+
|
|
|
+ const value = Math.floor(Math.random() * 10000) + 500;
|
|
|
+ const assignedTo = status === '占用' ? `emp-${Math.floor(Math.random() * 10) + 1}` : undefined;
|
|
|
+ const assignedToName = assignedTo ? `员工${Math.floor(Math.random() * 20) + 1}` : undefined;
|
|
|
+
|
|
|
+ // 随机设置保修截止日期
|
|
|
+ const warrantyExpiry = new Date(purchaseDate);
|
|
|
+ warrantyExpiry.setFullYear(warrantyExpiry.getFullYear() + (Math.random() > 0.5 ? 1 : 2));
|
|
|
+
|
|
|
+ assets.push({
|
|
|
+ id: `asset-${i}`,
|
|
|
+ name,
|
|
|
+ type,
|
|
|
+ status,
|
|
|
+ purchaseDate,
|
|
|
+ value,
|
|
|
+ assignedTo,
|
|
|
+ assignedToName,
|
|
|
+ department: departments[Math.floor(Math.random() * departments.length)],
|
|
|
+ description: `这是一台${name},用于日常办公和项目开发。`,
|
|
|
+ serialNumber,
|
|
|
+ warrantyExpiry
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ return assets;
|
|
|
+};
|
|
|
+
|
|
|
+// 生成模拟员工数据
|
|
|
+const generateMockEmployees = (): Employee[] => {
|
|
|
+ const employees: Employee[] = [];
|
|
|
+ const departments = ['设计部', '客户服务部', '财务部', '人力资源部'];
|
|
|
+ const positions = ['设计师', '客服专员', '财务专员', '人事专员', '经理', '助理'];
|
|
|
+ const names = ['张三', '李四', '王五', '赵六', '钱七', '孙八', '周九', '吴十'];
|
|
|
+
|
|
|
+ for (let i = 1; i <= 20; i++) {
|
|
|
+ employees.push({
|
|
|
+ id: `emp-${i}`,
|
|
|
+ name: names[i % names.length] + i,
|
|
|
+ department: departments[Math.floor(Math.random() * departments.length)],
|
|
|
+ position: positions[Math.floor(Math.random() * positions.length)],
|
|
|
+ employeeId: `EMP2023${String(i).padStart(3, '0')}`,
|
|
|
+ phone: `138${Math.floor(Math.random() * 100000000)}`,
|
|
|
+ email: `employee${i}@example.com`,
|
|
|
+ gender: i % 2 === 0 ? '女' : '男',
|
|
|
+ birthDate: new Date(1980 + Math.floor(Math.random() * 20), Math.floor(Math.random() * 12), Math.floor(Math.random() * 28) + 1),
|
|
|
+ hireDate: new Date(2020 + Math.floor(Math.random() * 3), Math.floor(Math.random() * 12), Math.floor(Math.random() * 28) + 1),
|
|
|
+ status: '在职'
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ return employees;
|
|
|
+};
|
|
|
+
|
|
|
+// 生成模拟资产分配记录
|
|
|
+const generateMockAssignments = (): AssetAssignment[] => {
|
|
|
+ const assignments: AssetAssignment[] = [];
|
|
|
+
|
|
|
+ for (let i = 1; i <= 15; i++) {
|
|
|
+ const startDate = new Date();
|
|
|
+ startDate.setMonth(startDate.getMonth() - Math.floor(Math.random() * 6));
|
|
|
+
|
|
|
+ const endDate = Math.random() > 0.5 ? new Date(startDate) : undefined;
|
|
|
+ if (endDate) {
|
|
|
+ endDate.setMonth(endDate.getMonth() + Math.floor(Math.random() * 3) + 1);
|
|
|
+ }
|
|
|
+
|
|
|
+ assignments.push({
|
|
|
+ id: `assignment-${i}`,
|
|
|
+ assetId: `asset-${Math.floor(Math.random() * 20) + 1}`,
|
|
|
+ employeeId: `emp-${Math.floor(Math.random() * 15) + 1}`,
|
|
|
+ startDate,
|
|
|
+ endDate,
|
|
|
+ status: endDate ? '已归还' : '进行中'
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ return assignments;
|
|
|
+};
|
|
|
+
|
|
|
+// 主组件
|
|
|
+@Component({
|
|
|
+ selector: 'app-assets-stats',
|
|
|
+ standalone: true,
|
|
|
+ imports: [
|
|
|
+ CommonModule,
|
|
|
+ FormsModule,
|
|
|
+ MatButtonModule,
|
|
|
+ MatCardModule,
|
|
|
+ MatIconModule,
|
|
|
+ MatDialogModule,
|
|
|
+ MatTabsModule,
|
|
|
+ MatTooltipModule,
|
|
|
+ MatTableModule
|
|
|
+ ],
|
|
|
+ templateUrl: './assets-stats.html',
|
|
|
+ styleUrl: './assets-stats.scss'
|
|
|
+}) export class AssetsStats implements OnInit {
|
|
|
+ // 暴露Math对象给模板使用
|
|
|
+ readonly Math = Math;
|
|
|
+
|
|
|
+ // 数据
|
|
|
+ assets = signal<Asset[]>([]);
|
|
|
+ employees = signal<Employee[]>([]);
|
|
|
+ assignments = signal<AssetAssignment[]>([]);
|
|
|
+ selectedView = signal<'grid' | 'list'>('grid');
|
|
|
+ searchTerm = signal('');
|
|
|
+ typeFilter = signal<AssetType | ''>('');
|
|
|
+ statusFilter = signal<AssetStatus | ''>('');
|
|
|
+ departmentFilter = signal('');
|
|
|
+
|
|
|
+ // 计算属性
|
|
|
+ filteredAssets = computed(() => {
|
|
|
+ let filtered = this.assets();
|
|
|
+
|
|
|
+ // 按搜索词筛选
|
|
|
+ if (this.searchTerm()) {
|
|
|
+ const term = this.searchTerm().toLowerCase();
|
|
|
+ filtered = filtered.filter(asset =>
|
|
|
+ asset.name.toLowerCase().includes(term) ||
|
|
|
+ (asset.serialNumber && asset.serialNumber.toLowerCase().includes(term)) ||
|
|
|
+ (asset.department && asset.department.toLowerCase().includes(term)) ||
|
|
|
+ (asset.assignedToName && asset.assignedToName.toLowerCase().includes(term))
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ // 按类型筛选
|
|
|
+ if (this.typeFilter()) {
|
|
|
+ filtered = filtered.filter(asset => asset.type === this.typeFilter());
|
|
|
+ }
|
|
|
+
|
|
|
+ // 按状态筛选
|
|
|
+ if (this.statusFilter()) {
|
|
|
+ filtered = filtered.filter(asset => asset.status === this.statusFilter());
|
|
|
+ }
|
|
|
+
|
|
|
+ // 按部门筛选
|
|
|
+ if (this.departmentFilter()) {
|
|
|
+ filtered = filtered.filter(asset => asset.department === this.departmentFilter());
|
|
|
+ }
|
|
|
+
|
|
|
+ return filtered;
|
|
|
+ });
|
|
|
+
|
|
|
+ // 资产统计
|
|
|
+ assetStats = computed(() => {
|
|
|
+ const total = this.assets().length;
|
|
|
+ const occupied = this.assets().filter(a => a.status === '占用').length;
|
|
|
+ const idle = this.assets().filter(a => a.status === '空闲').length;
|
|
|
+ const faulty = this.assets().filter(a => a.status === '故障').length;
|
|
|
+ const repairing = this.assets().filter(a => a.status === '报修中').length;
|
|
|
+
|
|
|
+ const totalValue = this.assets().reduce((sum, asset) => sum + asset.value, 0);
|
|
|
+
|
|
|
+ // 按类型统计
|
|
|
+ const typeStats = new Map<AssetType, { count: number, value: number }>();
|
|
|
+ this.assets().forEach(asset => {
|
|
|
+ const current = typeStats.get(asset.type) || { count: 0, value: 0 };
|
|
|
+ current.count++;
|
|
|
+ current.value += asset.value;
|
|
|
+ typeStats.set(asset.type, current);
|
|
|
+ });
|
|
|
+
|
|
|
+ // 转换为数组格式以便在模板中使用
|
|
|
+ const typeStatsArray = Array.from(typeStats.entries()).map(([key, value]) => ({
|
|
|
+ type: key,
|
|
|
+ ...value
|
|
|
+ }));
|
|
|
+
|
|
|
+ // 按部门统计
|
|
|
+ const departmentStats = new Map<string, { count: number, value: number }>();
|
|
|
+ this.assets().forEach(asset => {
|
|
|
+ const department = asset.department || '未知部门'; // 提供默认值
|
|
|
+ const current = departmentStats.get(department) || { count: 0, value: 0 };
|
|
|
+ current.count++;
|
|
|
+ current.value += asset.value;
|
|
|
+ departmentStats.set(department, current);
|
|
|
+ });
|
|
|
+
|
|
|
+ // 转换为数组格式以便在模板中使用
|
|
|
+ const departmentStatsArray = Array.from(departmentStats.entries()).map(([key, value]) => ({
|
|
|
+ department: key,
|
|
|
+ ...value
|
|
|
+ }));
|
|
|
+
|
|
|
+ // 使用时长统计(模拟数据)
|
|
|
+ const usageStats = [
|
|
|
+ { type: '电脑', avgHours: Math.floor(Math.random() * 40) + 100 },
|
|
|
+ { type: '外设', avgHours: Math.floor(Math.random() * 30) + 80 },
|
|
|
+ { type: '软件账号', avgHours: Math.floor(Math.random() * 50) + 120 },
|
|
|
+ { type: '域名', avgHours: Math.floor(Math.random() * 20) + 60 },
|
|
|
+ { type: '其他', avgHours: Math.floor(Math.random() * 20) + 40 }
|
|
|
+ ];
|
|
|
+
|
|
|
+ return {
|
|
|
+ total,
|
|
|
+ occupied,
|
|
|
+ idle,
|
|
|
+ faulty,
|
|
|
+ repairing,
|
|
|
+ totalValue,
|
|
|
+ typeStats,
|
|
|
+ typeStatsArray,
|
|
|
+ departmentStats,
|
|
|
+ departmentStatsArray,
|
|
|
+ usageStats
|
|
|
+ };
|
|
|
+ });
|
|
|
+
|
|
|
+ // 获取资产类型列表
|
|
|
+ assetTypes = computed(() => {
|
|
|
+ return Array.from(new Set(this.assets().map(asset => asset.type)));
|
|
|
+ });
|
|
|
+
|
|
|
+ // 获取部门列表
|
|
|
+ departments = computed(() => {
|
|
|
+ return Array.from(new Set(this.assets().map(asset => asset.department)));
|
|
|
+ });
|
|
|
+
|
|
|
+ constructor(private dialog: MatDialog) {}
|
|
|
+
|
|
|
+ ngOnInit() {
|
|
|
+ // 加载模拟数据
|
|
|
+ this.assets.set(generateMockAssets());
|
|
|
+ this.employees.set(generateMockEmployees());
|
|
|
+ this.assignments.set(generateMockAssignments());
|
|
|
+ }
|
|
|
+
|
|
|
+ // 切换视图(网格/列表)
|
|
|
+ switchView(view: 'grid' | 'list') {
|
|
|
+ this.selectedView.set(view);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 格式化日期
|
|
|
+ formatDate(date: Date): string {
|
|
|
+ if (!date) return '';
|
|
|
+ const d = new Date(date);
|
|
|
+ return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 格式化金额
|
|
|
+ formatCurrency(amount: number): string {
|
|
|
+ return new Intl.NumberFormat('zh-CN', { style: 'currency', currency: 'CNY' }).format(amount);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 获取状态样式类
|
|
|
+ getStatusClass(status: string): string {
|
|
|
+ switch (status) {
|
|
|
+ case '空闲':
|
|
|
+ return 'status-idle';
|
|
|
+ case '占用':
|
|
|
+ return 'status-occupied';
|
|
|
+ case '故障':
|
|
|
+ return 'status-faulty';
|
|
|
+ case '报修中':
|
|
|
+ return 'status-repairing';
|
|
|
+ default:
|
|
|
+ return '';
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 获取类型图标
|
|
|
+ getTypeIcon(type: AssetType): string {
|
|
|
+ switch (type) {
|
|
|
+ case '电脑':
|
|
|
+ return 'laptop';
|
|
|
+ case '外设':
|
|
|
+ return 'devices';
|
|
|
+ case '软件账号':
|
|
|
+ return 'cloud';
|
|
|
+ case '域名':
|
|
|
+ return 'link';
|
|
|
+ case '其他':
|
|
|
+ return 'category';
|
|
|
+ default:
|
|
|
+ return 'help';
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 打开资产分配对话框
|
|
|
+ openAssignmentDialog(asset: Asset, isReturning: boolean = false) {
|
|
|
+ let assignment: AssetAssignment | undefined;
|
|
|
+ if (isReturning) {
|
|
|
+ assignment = this.assignments().find(a => a.assetId === asset.id && a.status === '进行中');
|
|
|
+ }
|
|
|
+
|
|
|
+ const dialogRef = this.dialog.open(AssetAssignmentDialog, {
|
|
|
+ width: '500px',
|
|
|
+ maxWidth: '90vw',
|
|
|
+ disableClose: true,
|
|
|
+ data: {
|
|
|
+ asset,
|
|
|
+ employees: this.employees(),
|
|
|
+ isEdit: !!assignment,
|
|
|
+ isReturning,
|
|
|
+ assignment
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ dialogRef.afterClosed().subscribe(result => {
|
|
|
+ if (result) {
|
|
|
+ if (isReturning && assignment) {
|
|
|
+ // 更新归还信息
|
|
|
+ this.assignments.update(assignments =>
|
|
|
+ assignments.map(a =>
|
|
|
+ a.id === assignment!.id ? { ...a, endDate: result.endDate, status: '已归还' } : a
|
|
|
+ )
|
|
|
+ );
|
|
|
+
|
|
|
+ // 更新资产状态为空闲
|
|
|
+ this.assets.update(assets =>
|
|
|
+ assets.map(a =>
|
|
|
+ a.id === asset.id ? { ...a, status: '空闲', assignedTo: undefined, assignedToName: undefined } : a
|
|
|
+ )
|
|
|
+ );
|
|
|
+ } else {
|
|
|
+ // 创建新分配记录
|
|
|
+ const newAssignment: AssetAssignment = {
|
|
|
+ id: `assignment-${Date.now()}`,
|
|
|
+ assetId: asset.id,
|
|
|
+ employeeId: result.employeeId,
|
|
|
+ startDate: new Date(result.startDate),
|
|
|
+ endDate: result.endDate ? new Date(result.endDate) : undefined,
|
|
|
+ status: '进行中'
|
|
|
+ };
|
|
|
+
|
|
|
+ this.assignments.update(assignments => [newAssignment, ...assignments]);
|
|
|
+
|
|
|
+ // 更新资产状态为占用
|
|
|
+ const employee = this.employees().find(e => e.id === result.employeeId);
|
|
|
+ this.assets.update(assets =>
|
|
|
+ assets.map(a =>
|
|
|
+ a.id === asset.id ? { ...a, status: '占用', assignedTo: result.employeeId, assignedToName: employee?.name } : a
|
|
|
+ )
|
|
|
+ );
|
|
|
+ }
|
|
|
+ alert(isReturning ? '资产归还成功' : '资产分配成功');
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ // 打开报修对话框
|
|
|
+ openRepairDialog(asset: Asset) {
|
|
|
+ const dialogRef = this.dialog.open(AssetRepairDialog, {
|
|
|
+ width: '500px',
|
|
|
+ maxWidth: '90vw',
|
|
|
+ disableClose: true,
|
|
|
+ data: { asset }
|
|
|
+ });
|
|
|
+
|
|
|
+ dialogRef.afterClosed().subscribe(result => {
|
|
|
+ if (result) {
|
|
|
+ // 更新资产状态为报修中
|
|
|
+ this.assets.update(assets =>
|
|
|
+ assets.map(a =>
|
|
|
+ a.id === asset.id ? { ...a, status: '报修中' } : a
|
|
|
+ )
|
|
|
+ );
|
|
|
+ alert('报修申请已提交');
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ // 导出资产台账
|
|
|
+ exportAssetLedger() {
|
|
|
+ alert('资产台账导出功能待实现');
|
|
|
+ }
|
|
|
+
|
|
|
+ // 重置筛选条件
|
|
|
+ resetFilters() {
|
|
|
+ this.searchTerm.set('');
|
|
|
+ this.typeFilter.set('');
|
|
|
+ this.statusFilter.set('');
|
|
|
+ this.departmentFilter.set('');
|
|
|
+ }
|
|
|
+
|
|
|
+ // 获取资产使用情况
|
|
|
+ getAssetUsage(assetId: string): AssetAssignment | undefined {
|
|
|
+ return this.assignments().find(a => a.assetId === assetId && a.status === '进行中');
|
|
|
+ }
|
|
|
+
|
|
|
+ // 获取员工姓名
|
|
|
+ getEmployeeName(employeeId: string): string {
|
|
|
+ const employee = this.employees().find(e => e.id === employeeId);
|
|
|
+ return employee ? employee.name : '未知员工';
|
|
|
+ }
|
|
|
+}
|