|
|
@@ -4,7 +4,8 @@ import type {
|
|
|
User, Store, Community, Group, GroupMember, GroupMessage,
|
|
|
Document, RiskEvent, RiskKeyword, WorkOrder, ContentMaterial,
|
|
|
WeeklyPlan, PlanItem, IntentLead, KOC, AcquisitionChannel,
|
|
|
- ShowroomTarget, SalesData, LiveData, AppNotification, ComplianceResult
|
|
|
+ ShowroomTarget, SalesData, LiveData, AppNotification, ComplianceResult,
|
|
|
+ LifecyclePhase, HealthGrade,
|
|
|
} from '../models';
|
|
|
|
|
|
@Injectable({ providedIn: 'root' })
|
|
|
@@ -94,11 +95,9 @@ export class MockDataService {
|
|
|
private generateUsers(): void {
|
|
|
const users: User[] = [
|
|
|
{ id: 'u1', name: '张总监', email: 'admin@lami.com', phone: '13800000001', role: 'director', storeId: 's1', storeName: '上海总部', department: '总部', position: '总监', createdAt: new Date('2025-01-01') },
|
|
|
- { id: 'u2', name: '周督导', email: 'dudao@lami.com', phone: '13800000002', role: 'regional_supervisor', storeId: 's1', storeName: '上海总部', department: '督导部', position: '区域督导', createdAt: new Date('2025-01-10') },
|
|
|
+ { id: 'u2', name: '周督导', email: 'dudao@lami.com', phone: '13800000002', role: 'regional_supervisor', storeId: 's1', storeName: '上海总部', regionCode: '华东', department: '督导部', position: '区域督导', createdAt: new Date('2025-01-10') },
|
|
|
{ id: 'u3', name: '李店长', email: 'lidian@lami.com', phone: '13800000003', role: 'store_manager', storeId: 's1', storeName: '上海总部', department: '直营店', position: '店长', createdAt: new Date('2025-01-15') },
|
|
|
- { id: 'u4', name: '王单群', email: 'wangying@lami.com', phone: '13800000004', role: 'single_group', storeId: 's1', storeName: '上海总部', department: '运营部', position: '单群', createdAt: new Date('2025-02-01') },
|
|
|
{ id: 'u6', name: '刘店长', email: 'liudian2@lami.com', phone: '13800000006', role: 'store_manager', storeId: 's2', storeName: '北京旗舰店', department: '直营店', position: '店长', createdAt: new Date('2025-03-15') },
|
|
|
- { id: 'u7', name: '陈单群', email: 'chenying@lami.com', phone: '13800000007', role: 'single_group', storeId: 's2', storeName: '北京旗舰店', department: '运营部', position: '单群', createdAt: new Date('2025-04-01') },
|
|
|
];
|
|
|
(this as any).users_cache = users;
|
|
|
}
|
|
|
@@ -112,21 +111,45 @@ export class MockDataService {
|
|
|
(this as any).stores_cache = stores;
|
|
|
}
|
|
|
|
|
|
+ private mockLifecyclePhase(deliveryDate: string): LifecyclePhase {
|
|
|
+ const delivery = new Date(`${deliveryDate.length === 7 ? deliveryDate + '-01' : deliveryDate}T00:00:00`);
|
|
|
+ const today = new Date();
|
|
|
+ today.setHours(0, 0, 0, 0);
|
|
|
+ const daysUntil = Math.round((delivery.getTime() - today.getTime()) / 86400000);
|
|
|
+ if (daysUntil > 90) return 'pre_handover';
|
|
|
+ if (daysUntil >= 0) return 'near_handover';
|
|
|
+ const daysSince = Math.abs(daysUntil);
|
|
|
+ if (daysSince <= 180) return 'near_handover';
|
|
|
+ if (daysSince <= 730) return 'post_handover';
|
|
|
+ return 'legacy';
|
|
|
+ }
|
|
|
+
|
|
|
+ private mockHealthGrade(score: number): HealthGrade {
|
|
|
+ if (score >= 85) return 'S';
|
|
|
+ if (score >= 70) return 'A';
|
|
|
+ if (score >= 55) return 'B';
|
|
|
+ return 'C';
|
|
|
+ }
|
|
|
+
|
|
|
private generateCommunities(): void {
|
|
|
- const communities: Community[] = [
|
|
|
- { id: 'c1', name: '翠湖天地', address: '上海市黄浦区济南路260号', totalHouseholds: 1200, avgPrice: 168000, deliveryDate: '2024-06', storeId: 's1', storeName: '上海总部', coverageRate: 0.32, groupCount: 5, reachRate: 0.28 },
|
|
|
- { id: 'c2', name: '汤臣一品', address: '上海市浦东新区花园石桥路28号', totalHouseholds: 800, avgPrice: 250000, deliveryDate: '2023-12', storeId: 's1', storeName: '上海总部', coverageRate: 0.45, groupCount: 4, reachRate: 0.41 },
|
|
|
+ const raw = [
|
|
|
+ { id: 'c1', name: '翠湖天地', address: '上海市黄浦区济南路260号', totalHouseholds: 1200, avgPrice: 168000, deliveryDate: '2027-06', storeId: 's1', storeName: '上海总部', coverageRate: 0.32, groupCount: 5, reachRate: 0.28 },
|
|
|
+ { id: 'c2', name: '汤臣一品', address: '上海市浦东新区花园石桥路28号', totalHouseholds: 800, avgPrice: 250000, deliveryDate: '2025-12', storeId: 's1', storeName: '上海总部', coverageRate: 0.45, groupCount: 4, reachRate: 0.41 },
|
|
|
{ id: 'c3', name: '华侨城苏河湾', address: '上海市静安区北苏州路912号', totalHouseholds: 600, avgPrice: 135000, deliveryDate: '2024-03', storeId: 's1', storeName: '上海总部', coverageRate: 0.38, groupCount: 3, reachRate: 0.35 },
|
|
|
- { id: 'c4', name: '万达瑞华公馆', address: '上海市徐汇区龙腾大道288号', totalHouseholds: 500, avgPrice: 185000, deliveryDate: '2024-09', storeId: 's1', storeName: '上海总部', coverageRate: 0.22, groupCount: 3, reachRate: 0.18 },
|
|
|
+ { id: 'c4', name: '万达瑞华公馆', address: '上海市徐汇区龙腾大道288号', totalHouseholds: 500, avgPrice: 185000, deliveryDate: '2026-08', storeId: 's1', storeName: '上海总部', coverageRate: 0.22, groupCount: 3, reachRate: 0.18 },
|
|
|
{ id: 'c5', name: '宝格丽公寓', address: '上海市静安区河南北路108号', totalHouseholds: 400, avgPrice: 220000, deliveryDate: '2024-01', storeId: 's1', storeName: '上海总部', coverageRate: 0.52, groupCount: 3, reachRate: 0.48 },
|
|
|
- { id: 'c6', name: '北京壹号院', address: '北京市朝阳区农展馆南路8号', totalHouseholds: 900, avgPrice: 180000, deliveryDate: '2024-06', storeId: 's2', storeName: '北京旗舰店', coverageRate: 0.35, groupCount: 4, reachRate: 0.31 },
|
|
|
+ { id: 'c6', name: '北京壹号院', address: '北京市朝阳区农展馆南路8号', totalHouseholds: 900, avgPrice: 180000, deliveryDate: '2026-09', storeId: 's2', storeName: '北京旗舰店', coverageRate: 0.35, groupCount: 4, reachRate: 0.31 },
|
|
|
{ id: 'c7', name: '中粮瑞府', address: '北京市海淀区西三环中路', totalHouseholds: 700, avgPrice: 120000, deliveryDate: '2024-04', storeId: 's2', storeName: '北京旗舰店', coverageRate: 0.28, groupCount: 3, reachRate: 0.25 },
|
|
|
{ id: 'c8', name: '融创壹号院子', address: '北京市通州区新华北路', totalHouseholds: 550, avgPrice: 95000, deliveryDate: '2025-01', storeId: 's2', storeName: '北京旗舰店', coverageRate: 0.15, groupCount: 3, reachRate: 0.12 },
|
|
|
- { id: 'c9', name: '万柳书院', address: '北京市海淀区万柳中路', totalHouseholds: 650, avgPrice: 155000, deliveryDate: '2024-08', storeId: 's2', storeName: '北京旗舰店', coverageRate: 0.3, groupCount: 4, reachRate: 0.26 },
|
|
|
- { id: 'c10', name: '深圳湾一号', address: '深圳市南山区科苑南路', totalHouseholds: 1000, avgPrice: 200000, deliveryDate: '2024-05', storeId: 's3', storeName: '深圳体验店', coverageRate: 0.4, groupCount: 4, reachRate: 0.36 },
|
|
|
+ { id: 'c9', name: '万柳书院', address: '北京市海淀区万柳中路', totalHouseholds: 650, avgPrice: 155000, deliveryDate: '2022-01', storeId: 's2', storeName: '北京旗舰店', coverageRate: 0.3, groupCount: 4, reachRate: 0.26 },
|
|
|
+ { id: 'c10', name: '深圳湾一号', address: '深圳市南山区科苑南路', totalHouseholds: 1000, avgPrice: 200000, deliveryDate: '2025-06', storeId: 's3', storeName: '深圳体验店', coverageRate: 0.4, groupCount: 4, reachRate: 0.36 },
|
|
|
{ id: 'c11', name: '华润城润府', address: '深圳市南山区深南大道', totalHouseholds: 750, avgPrice: 135000, deliveryDate: '2024-11', storeId: 's3', storeName: '深圳体验店', coverageRate: 0.25, groupCount: 3, reachRate: 0.22 },
|
|
|
- { id: 'c12', name: '保利天珺', address: '深圳市福田区彩田路', totalHouseholds: 600, avgPrice: 160000, deliveryDate: '2025-03', storeId: 's3', storeName: '深圳体验店', coverageRate: 0.1, groupCount: 3, reachRate: 0.08 },
|
|
|
+ { id: 'c12', name: '保利天珺', address: '深圳市福田区彩田路', totalHouseholds: 600, avgPrice: 160000, deliveryDate: '2020-03', storeId: 's3', storeName: '深圳体验店', coverageRate: 0.1, groupCount: 3, reachRate: 0.08 },
|
|
|
];
|
|
|
+ const communities: Community[] = raw.map((c) => ({
|
|
|
+ ...c,
|
|
|
+ lifecyclePhase: this.mockLifecyclePhase(c.deliveryDate),
|
|
|
+ }));
|
|
|
(this as any).communities_cache = communities;
|
|
|
}
|
|
|
|
|
|
@@ -147,20 +170,25 @@ export class MockDataService {
|
|
|
c10: '深圳体验店', c11: '深圳体验店', c12: '深圳体验店'
|
|
|
};
|
|
|
const owners = [
|
|
|
- { id: 'u4', name: '王单群', role: 'single_group' },
|
|
|
- { id: 'u7', name: '陈单群', role: 'single_group' },
|
|
|
{ id: 'u3', name: '李店长', role: 'store_manager' },
|
|
|
+ { id: 'u6', name: '刘店长', role: 'store_manager' },
|
|
|
];
|
|
|
|
|
|
const groups: Group[] = [];
|
|
|
let gid = 1;
|
|
|
|
|
|
+ const communityLifecycle: Record<string, LifecyclePhase> = {};
|
|
|
+ for (const c of (this as any).communities_cache as Community[]) {
|
|
|
+ communityLifecycle[c.id] = c.lifecyclePhase;
|
|
|
+ }
|
|
|
+
|
|
|
for (const cid of Object.keys(communityMap)) {
|
|
|
const groupCount = 3 + Math.floor(Math.random() * 4);
|
|
|
for (let i = 1; i <= groupCount; i++) {
|
|
|
const owner = owners[Math.floor(Math.random() * owners.length)];
|
|
|
const msgCount = Math.floor(Math.random() * 80) + 5;
|
|
|
const healthScore = Math.floor(Math.random() * 40) + 55;
|
|
|
+ const healthGrade = this.mockHealthGrade(healthScore);
|
|
|
const statuses: Array<Group['activityLevel']> = ['high', 'medium', 'low', 'inactive'];
|
|
|
groups.push({
|
|
|
id: `g${gid}`,
|
|
|
@@ -176,6 +204,10 @@ export class MockDataService {
|
|
|
createdAt: new Date(2024, Math.floor(Math.random() * 12), Math.floor(Math.random() * 28) + 1),
|
|
|
healthScore,
|
|
|
healthStatus: healthScore >= 80 ? 'healthy' : healthScore >= 60 ? 'warning' : 'critical',
|
|
|
+ healthGrade,
|
|
|
+ opsActivityScore: Math.min(100, Math.floor(healthScore * 0.55 + Math.random() * 20)),
|
|
|
+ customerActivityScore: Math.min(100, Math.floor(healthScore * 0.45 + Math.random() * 25)),
|
|
|
+ lifecyclePhase: communityLifecycle[cid] || 'pre_handover',
|
|
|
hasDocument: Math.random() > 0.2,
|
|
|
documentPinned: Math.random() > 0.3,
|
|
|
documentInNotice: Math.random() > 0.4,
|
|
|
@@ -539,8 +571,12 @@ export class MockDataService {
|
|
|
return this.getUsers().find(u => u.id === id);
|
|
|
}
|
|
|
|
|
|
+ getUserByPhone(phone: string): User | undefined {
|
|
|
+ return this.getUsers().find((u) => u.phone === phone.trim());
|
|
|
+ }
|
|
|
+
|
|
|
getUserByEmail(email: string): User | undefined {
|
|
|
- return this.getUsers().find(u => u.email === email);
|
|
|
+ return this.getUsers().find((u) => u.email === email);
|
|
|
}
|
|
|
|
|
|
// ==================== Auth & persistence helpers ====================
|
|
|
@@ -563,11 +599,11 @@ export class MockDataService {
|
|
|
|
|
|
registerUser(data: {
|
|
|
name: string;
|
|
|
- email: string;
|
|
|
phone: string;
|
|
|
password: string;
|
|
|
- role: User['role'];
|
|
|
+ email: string;
|
|
|
}): boolean {
|
|
|
+ if (this.getUserByPhone(data.phone)) return false;
|
|
|
if (this.getUserByEmail(data.email)) return false;
|
|
|
|
|
|
const users = [...this.getUsers()];
|
|
|
@@ -577,7 +613,7 @@ export class MockDataService {
|
|
|
name: data.name.trim(),
|
|
|
email: data.email.trim(),
|
|
|
phone: data.phone.trim(),
|
|
|
- role: data.role,
|
|
|
+ role: 'store_manager',
|
|
|
storeId: 's1',
|
|
|
storeName: '上海总部',
|
|
|
department: '运营部',
|
|
|
@@ -595,6 +631,15 @@ export class MockDataService {
|
|
|
return true;
|
|
|
}
|
|
|
|
|
|
+ resetPasswordByEmail(email: string, newPassword: string): boolean {
|
|
|
+ const user = this.getUserByEmail(email.trim());
|
|
|
+ if (!user) return false;
|
|
|
+ const passwords = this.storage.get<Record<string, string>>(this.passwordsKey) ?? {};
|
|
|
+ passwords[user.id] = newPassword;
|
|
|
+ this.storage.set(this.passwordsKey, passwords);
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
updateUser(user: User): void {
|
|
|
const users = this.getUsers().map((u) => (u.id === user.id ? { ...user } : u));
|
|
|
(this as any).users_cache = users;
|