feat(assets): add dimension analysis and authorized user previews
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/push/woodpecker Pipeline was successful
Refactor asset drilldowns with combined dimension filters, parent summaries and progressive loading. Add the hidden /who read-only permission preview with scoped authorization, identity mapping and audit logging. Refine inventory counts and AI问数 presentation.
This commit is contained in:
@@ -1,5 +1,9 @@
|
||||
/** Current or most recent effective movement associated with a vehicle. */
|
||||
export interface AbnormalMoveDetail {
|
||||
/** Final PASS task completion time of the movement approval workflow. */
|
||||
approvalPassedAt?: string | null;
|
||||
/** Elapsed days at the analysis snapshot time; null when approval time is unknown. */
|
||||
elapsedDays?: number | null;
|
||||
id: string;
|
||||
type: string;
|
||||
work: string | null;
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
/** Aggregated management analysis; dimensions are shared by aggregates and final details. */
|
||||
export const ASSET_ANALYSIS_DIMENSIONS = [
|
||||
'type', 'model', 'region', 'city', 'assetStatus', 'assetDisposition', 'operationKind',
|
||||
'inventoryState', 'abnormalType', 'abnormalAge', 'department', 'manager', 'customer',
|
||||
] as const;
|
||||
|
||||
export type AssetAnalysisDimension = typeof ASSET_ANALYSIS_DIMENSIONS[number];
|
||||
export type AssetAnalysisMetric = 'assets' | 'operating' | 'inventory' | 'abnormal' | 'pending';
|
||||
export type AssetAnalysisFilters = Partial<Record<AssetAnalysisDimension, string>>;
|
||||
/** Values within a dimension are ORed; dimensions and the entry scope are ANDed. */
|
||||
export type AssetAnalysisFilterValues = Partial<Record<AssetAnalysisDimension, string[]>>;
|
||||
export type AbnormalAgeBucket = '0-3d' | '3-7d' | '7-15d' | '15-30d' | '30d+' | 'unknown';
|
||||
|
||||
export interface AssetAnalysisCell extends Record<AssetAnalysisDimension, string> {
|
||||
assetStatus: 'operating' | 'inventory' | 'other';
|
||||
assetDisposition: '在库' | '租赁' | '自营' | '其他运营' | '其他';
|
||||
inventoryState: 'abnormal' | 'pending' | 'available' | 'awaiting' | 'unclassified' | 'not-applicable';
|
||||
abnormalAge: AbnormalAgeBucket;
|
||||
quantity: number;
|
||||
}
|
||||
|
||||
export interface AssetAnalysisSummary {
|
||||
totalAssets: number;
|
||||
operating: { total: number; self: number; leased: number; public: number; hanging: number };
|
||||
inventory: { total: number; inStock: number; abnormal: number };
|
||||
pendingDelivery: number;
|
||||
otherAssets: number;
|
||||
}
|
||||
|
||||
export interface AssetAnalysisResponse {
|
||||
generatedAt: string;
|
||||
summary: AssetAnalysisSummary;
|
||||
cells: AssetAnalysisCell[];
|
||||
/** True only when a verified approval time source is available. */
|
||||
approvalTimeAvailable: boolean;
|
||||
}
|
||||
|
||||
export interface AssetAnalysisDetailRequest {
|
||||
metric: AssetAnalysisMetric;
|
||||
filters: AssetAnalysisFilters;
|
||||
filterValues?: AssetAnalysisFilterValues;
|
||||
/** Use the aggregate's reference time to keep duration bucket boundaries consistent. */
|
||||
generatedAt?: string;
|
||||
scope?: AssetAnalysisScope;
|
||||
inventoryKeys?: string[];
|
||||
}
|
||||
|
||||
/** Carries the existing dashboard entry's exact list scope into every drill level. */
|
||||
export interface AssetAnalysisScope {
|
||||
inventoryKeys?: string[];
|
||||
brand?: string;
|
||||
batch?: string;
|
||||
model?: string;
|
||||
location?: string;
|
||||
status?: string;
|
||||
category?: string;
|
||||
vehicleType?: string;
|
||||
manager?: string;
|
||||
customer?: string;
|
||||
department?: string;
|
||||
attendance?: string;
|
||||
isColdChain?: string;
|
||||
isTrailer?: string;
|
||||
source?: string;
|
||||
subject?: string | null;
|
||||
}
|
||||
|
||||
export interface AssetAnalysisRequest {
|
||||
scope?: AssetAnalysisScope;
|
||||
inventoryKeys?: string[];
|
||||
}
|
||||
@@ -34,3 +34,10 @@ export function canAccessEnergy(roles: readonly string[] | null | undefined): bo
|
||||
if (!roles || roles.length === 0) return false;
|
||||
return roles.some(r => ENERGY_ACCESS_ROLES.includes(r) || r === ENERGY_FULL_ACCESS);
|
||||
}
|
||||
|
||||
/** Shared by normal login and permission preview; full access wins over department. */
|
||||
export function resolvePermissionLevel(roles: readonly string[]): 'full' | 'department' | 'personal' {
|
||||
if (roles.some(role => FULL_ACCESS_ROLES.includes(role))) return 'full';
|
||||
if (roles.some(role => DEPT_ACCESS_ROLES.includes(role))) return 'department';
|
||||
return 'personal';
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user