refactor(scheduling): shared types, structured reason, cross-region candidates
- Extract shared types to src/shared/scheduling/types.ts (client/server both re-export)
- Convert SchedulingSuggestion.reason from string to structured { lines, conclusion }
- Remove hard region filter; algorithm keeps cross-region candidates with isSameRegion flag
- SuggestionDetail renders same-region vs cross-region sections with a divider
- Close detail modal when selected suggestion no longer exists in data
- Unify estimatedGain definition (strict canQualifyAfterSwap) between algorithm and API layers
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
83
src/shared/scheduling/types.ts
Normal file
83
src/shared/scheduling/types.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
// Shared scheduling types — used by both client (modules/scheduling) and server
|
||||
// (server/routes/scheduling). Keep server-only types (EnrichedVehicle etc.) in
|
||||
// server/routes/scheduling/types.ts.
|
||||
|
||||
export interface SchedulingVehicleInfo {
|
||||
plateNumber: string;
|
||||
targetId: number;
|
||||
targetName: string;
|
||||
vehicleType: string;
|
||||
totalMileage: number;
|
||||
currentYearMileage: number;
|
||||
completionRate: number;
|
||||
yearTarget: number;
|
||||
region: string;
|
||||
province: string;
|
||||
customer: string | null;
|
||||
department: string | null;
|
||||
manager: string | null;
|
||||
customerAvgDaily: number;
|
||||
predictedYearEnd: number;
|
||||
daysLeft: number;
|
||||
}
|
||||
|
||||
export interface CandidateVehicle {
|
||||
plateNumber: string;
|
||||
targetId: number | null;
|
||||
targetName: string | null;
|
||||
vehicleType: string;
|
||||
totalMileage: number;
|
||||
completionRate: number;
|
||||
yearTarget: number | null;
|
||||
daysLeft: number;
|
||||
region: string;
|
||||
province: string;
|
||||
mileageGap: number;
|
||||
predictedAfterSwap: number;
|
||||
canQualifyAfterSwap: boolean;
|
||||
isSameRegion: boolean;
|
||||
}
|
||||
|
||||
export interface ReasonLine {
|
||||
label: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface ReasonBlock {
|
||||
lines: ReasonLine[];
|
||||
conclusion: string;
|
||||
}
|
||||
|
||||
export interface SchedulingSuggestion {
|
||||
id: string;
|
||||
priority: 'high' | 'medium';
|
||||
type: 'replace_qualified' | 'rescue_hopeless';
|
||||
currentVehicle: SchedulingVehicleInfo;
|
||||
candidates: CandidateVehicle[];
|
||||
reason: ReasonBlock;
|
||||
}
|
||||
|
||||
export interface SchedulingSummary {
|
||||
qualifiedCount: number;
|
||||
hopelessCount: number;
|
||||
suggestionCount: number;
|
||||
estimatedGain: number;
|
||||
}
|
||||
|
||||
export interface SchedulingTargetOption {
|
||||
id: number;
|
||||
name: string;
|
||||
vehicleCount: number;
|
||||
}
|
||||
|
||||
export interface SchedulingResponse {
|
||||
summary: SchedulingSummary;
|
||||
suggestions: SchedulingSuggestion[];
|
||||
targets: SchedulingTargetOption[];
|
||||
}
|
||||
|
||||
export interface NotifyRequest {
|
||||
suggestionId: string;
|
||||
currentPlate: string;
|
||||
candidatePlate: string;
|
||||
}
|
||||
Reference in New Issue
Block a user