fix: align asset customer data with OneOS and refine statistics display
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/push/woodpecker Pipeline was successful
This commit is contained in:
@@ -34,7 +34,7 @@ import {
|
|||||||
deriveInventoryView,
|
deriveInventoryView,
|
||||||
deriveModalVehicleView,
|
deriveModalVehicleView,
|
||||||
formatLocalDateTime,
|
formatLocalDateTime,
|
||||||
getWeeklyFlowRange,
|
getRecentFlowRange,
|
||||||
selectFlowDetails,
|
selectFlowDetails,
|
||||||
type VehicleModalSelection,
|
type VehicleModalSelection,
|
||||||
type FlowSelection,
|
type FlowSelection,
|
||||||
@@ -84,10 +84,10 @@ export default function AssetsModule() {
|
|||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [lastUpdate, setLastUpdate] = useState<string>(() => formatLocalDateTime(new Date()));
|
const [lastUpdate, setLastUpdate] = useState<string>(() => formatLocalDateTime(new Date()));
|
||||||
const [modalLoading, setModalLoading] = useState(false);
|
const [modalLoading, setModalLoading] = useState(false);
|
||||||
const [flowRange, setFlowRange] = useState(() => getWeeklyFlowRange());
|
const [flowRange, setFlowRange] = useState(() => getRecentFlowRange());
|
||||||
const [flowStats, setFlowStats] = useState<FlowStatsResponse | null>(null);
|
const [flowStats, setFlowStats] = useState<FlowStatsResponse | null>(null);
|
||||||
const [flowLoading, setFlowLoading] = useState(false);
|
const [flowLoading, setFlowLoading] = useState(false);
|
||||||
const [flowDailyExpanded, setFlowDailyExpanded] = useState(false);
|
const [flowDailyExpanded, setFlowDailyExpanded] = useState(true);
|
||||||
const [selectedFlow, setSelectedFlow] = useState<FlowSelection | null>(null);
|
const [selectedFlow, setSelectedFlow] = useState<FlowSelection | null>(null);
|
||||||
|
|
||||||
// Dept/Region/Customer data
|
// Dept/Region/Customer data
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useEffect, useRef, useState } from 'react';
|
import React from 'react';
|
||||||
import { ChevronDown, Filter, Loader2, Search } from 'lucide-react';
|
import { ChevronDown, Filter, Loader2, Search } from 'lucide-react';
|
||||||
import { motion } from 'motion/react';
|
import { motion } from 'motion/react';
|
||||||
import type { SubjectOption } from '../api';
|
import type { SubjectOption } from '../api';
|
||||||
@@ -30,42 +30,6 @@ const TABS = [
|
|||||||
{ id: 'customer', label: '按客户' },
|
{ id: 'customer', label: '按客户' },
|
||||||
];
|
];
|
||||||
|
|
||||||
function MarqueeBanner() {
|
|
||||||
const trackRef = useRef<HTMLDivElement>(null);
|
|
||||||
const innerRef = useRef<HTMLDivElement>(null);
|
|
||||||
const [overflow, setOverflow] = useState(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const check = () => {
|
|
||||||
if (!trackRef.current || !innerRef.current) return;
|
|
||||||
setOverflow(innerRef.current.scrollWidth > trackRef.current.clientWidth);
|
|
||||||
};
|
|
||||||
check();
|
|
||||||
const ro = new ResizeObserver(check);
|
|
||||||
ro.observe(trackRef.current!);
|
|
||||||
return () => ro.disconnect();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const text = '车辆资产已于 2026 年 6 月 18 日完成“运营状态”与“业务关联”校验';
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="relative -mx-6 mb-4 bg-green-50 border-y border-green-200">
|
|
||||||
<div ref={trackRef} className="overflow-hidden">
|
|
||||||
<div className={`flex w-max py-2 ${overflow ? 'animate-marquee' : 'w-full justify-center'}`}>
|
|
||||||
<span ref={innerRef} className="inline-block whitespace-nowrap px-6 text-xs text-green-700 font-medium">
|
|
||||||
{text}
|
|
||||||
</span>
|
|
||||||
{overflow && (
|
|
||||||
<span className="inline-block whitespace-nowrap px-6 text-xs text-green-700 font-medium">
|
|
||||||
{text}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function AssetsHeader({
|
export function AssetsHeader({
|
||||||
activeTab,
|
activeTab,
|
||||||
setActiveTab,
|
setActiveTab,
|
||||||
@@ -246,16 +210,16 @@ export function AssetsHeader({
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
{/* Status row */}
|
{/* Status row */}
|
||||||
<div className="flex items-center justify-center gap-4 py-1.5 text-[10px] text-gray-400">
|
<div className="flex flex-wrap items-center justify-center gap-x-1 gap-y-0.5 px-3 py-1.5 text-[10px] text-gray-400">
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<span className="w-1 h-1 rounded-full bg-blue-400 inline-block" />
|
<span className="w-1 h-1 rounded-full bg-blue-400 inline-block" />
|
||||||
最后更新: {lastUpdate}
|
数据更新时间:{lastUpdate.slice(0, 16)}
|
||||||
</div>
|
</div>
|
||||||
|
<span aria-hidden="true">|</span>
|
||||||
|
<span className="whitespace-nowrap">运维台账每日统计口径:当日17:00</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* OneOS 迁移提示滚动条 */}
|
|
||||||
<MarqueeBanner />
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useRef, useState } from 'react';
|
import { useRef, useState } from 'react';
|
||||||
import { ArrowRight, CalendarDays, Check, ChevronDown, ChevronLeft, ChevronRight, X } from 'lucide-react';
|
import { ArrowRight, CalendarDays, Check, ChevronDown, ChevronLeft, ChevronRight, X } from 'lucide-react';
|
||||||
import { getWeeklyFlowRange, type DateRange } from '../../model';
|
import { getRecentFlowRange, getWeeklyFlowRange, type DateRange } from '../../model';
|
||||||
|
|
||||||
const weekdays = ['一', '二', '三', '四', '五', '六', '日'];
|
const weekdays = ['一', '二', '三', '四', '五', '六', '日'];
|
||||||
const ymd = (date: Date) => `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
|
const ymd = (date: Date) => `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
|
||||||
@@ -25,7 +25,7 @@ export function FlowDateRangePicker({ value, onChange }: { value: DateRange; onC
|
|||||||
{ label: '今天', range: { start: today, end: today } },
|
{ label: '今天', range: { start: today, end: today } },
|
||||||
{ label: '昨天', range: { start: ymd(shiftDay(new Date(), -1)), end: ymd(shiftDay(new Date(), -1)) } },
|
{ label: '昨天', range: { start: ymd(shiftDay(new Date(), -1)), end: ymd(shiftDay(new Date(), -1)) } },
|
||||||
{ label: '业务周', range: getWeeklyFlowRange() },
|
{ label: '业务周', range: getWeeklyFlowRange() },
|
||||||
{ label: '近 7 天', range: { start: ymd(shiftDay(new Date(), -6)), end: today } },
|
{ label: '近 7 天', range: getRecentFlowRange() },
|
||||||
{ label: '近 30 天', range: { start: ymd(shiftDay(new Date(), -29)), end: today } },
|
{ label: '近 30 天', range: { start: ymd(shiftDay(new Date(), -29)), end: today } },
|
||||||
];
|
];
|
||||||
const open = () => {
|
const open = () => {
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ export function FlowStatisticsCard({
|
|||||||
{flowLoading && (
|
{flowLoading && (
|
||||||
<div className="rounded-xl bg-slate-50 px-3 py-4 text-center text-[11px] font-bold text-slate-400">正在加载交还车统计...</div>
|
<div className="rounded-xl bg-slate-50 px-3 py-4 text-center text-[11px] font-bold text-slate-400">正在加载交还车统计...</div>
|
||||||
)}
|
)}
|
||||||
{!flowLoading && flowStats?.daily.map((day) => (
|
{!flowLoading && flowStats?.daily.slice().sort((a, b) => b.date.localeCompare(a.date)).map((day) => (
|
||||||
<div key={day.date} className="rounded-2xl border border-slate-100 bg-white px-3 py-3 shadow-sm">
|
<div key={day.date} className="rounded-2xl border border-slate-100 bg-white px-3 py-3 shadow-sm">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="text-[11px] font-black text-slate-400">{day.date}</div>
|
<div className="text-[11px] font-black text-slate-400">{day.date}</div>
|
||||||
|
|||||||
@@ -126,6 +126,13 @@ function addDays(date: Date, days: number): Date {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 资产流转周报按周六至周五统计;周末查看时仍停留在刚结束的周五。
|
// 资产流转周报按周六至周五统计;周末查看时仍停留在刚结束的周五。
|
||||||
|
export function getRecentFlowRange(referenceDate = new Date()): DateRange {
|
||||||
|
return {
|
||||||
|
start: formatLocalDate(addDays(referenceDate, -6)),
|
||||||
|
end: formatLocalDate(referenceDate),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function getWeeklyFlowRange(referenceDate = new Date()): DateRange {
|
export function getWeeklyFlowRange(referenceDate = new Date()): DateRange {
|
||||||
const day = referenceDate.getDay();
|
const day = referenceDate.getDay();
|
||||||
const end = day === 6
|
const end = day === 6
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
import pool from '../../db.js';
|
import pool from '../../db.js';
|
||||||
|
import { LATEST_VEHICLE_LEASE_JOIN_SQL } from '../../vehicle-lease-sql.js';
|
||||||
import type { VehicleInfoRow } from './types.js';
|
import type { VehicleInfoRow } from './types.js';
|
||||||
|
|
||||||
/** 车辆关联信息 SQL(客户名、部门、经理、租赁状态、主体、项目、品牌) */
|
/** 车辆关联信息 SQL(客户名、部门、经理、租赁状态、主体、项目、品牌) */
|
||||||
export const VEHICLE_INFO_SQL = `SELECT
|
export const VEHICLE_INFO_SQL = `SELECT
|
||||||
vi.plate_number AS plate,
|
vi.plate_number AS plate,
|
||||||
vi.vin AS vin,
|
vi.vin AS vin,
|
||||||
COALESCE(c.customer_name, vor.customer_name, ci.customer_name) AS customer,
|
vor.customer_name AS customer,
|
||||||
COALESCE(c.business_department_name, vor.business_dept) AS department,
|
vor.business_dept AS department,
|
||||||
COALESCE(c.business_manager_name, vor.business_manager) AS manager,
|
vor.business_manager AS manager,
|
||||||
CAST(COALESCE(c.business_manager_id, vi.business_id) AS CHAR) AS manager_id,
|
CAST(COALESCE(c.business_manager_id, vi.business_id) AS CHAR) AS manager_id,
|
||||||
CASE vs.operation_status
|
CASE vs.operation_status
|
||||||
WHEN '1' THEN '租赁'
|
WHEN '1' THEN '租赁'
|
||||||
@@ -18,7 +19,7 @@ export const VEHICLE_INFO_SQL = `SELECT
|
|||||||
ELSE vs.operation_status
|
ELSE vs.operation_status
|
||||||
END AS rent_status,
|
END AS rent_status,
|
||||||
NULLIF(vi.registered_ownership, '') AS entity,
|
NULLIF(vi.registered_ownership, '') AS entity,
|
||||||
COALESCE(c.project_name, vor.project_name) AS project,
|
vor.project_name AS project,
|
||||||
COALESCE(vi_operation_city.NAME, NULLIF(vi.operation_city, '')) AS inventory_location,
|
COALESCE(vi_operation_city.NAME, NULLIF(vi.operation_city, '')) AS inventory_location,
|
||||||
CASE vm.brand
|
CASE vm.brand
|
||||||
WHEN 'hyundai' THEN CASE WHEN vm.model LIKE '%帕力安%' OR vm.model LIKE '%冷链%' OR vm.model LIKE '%双飞翼%' THEN '帕力安牌' ELSE '现代' END
|
WHEN 'hyundai' THEN CASE WHEN vm.model LIKE '%帕力安%' OR vm.model LIKE '%冷链%' OR vm.model LIKE '%双飞翼%' THEN '帕力安牌' ELSE '现代' END
|
||||||
@@ -50,21 +51,7 @@ LEFT JOIN vehicle_model vm
|
|||||||
LEFT JOIN common_district vi_operation_city
|
LEFT JOIN common_district vi_operation_city
|
||||||
ON vi_operation_city.CODE = vi.operation_city COLLATE utf8mb4_unicode_ci
|
ON vi_operation_city.CODE = vi.operation_city COLLATE utf8mb4_unicode_ci
|
||||||
AND vi_operation_city.STATUS = 'VALID'
|
AND vi_operation_city.STATUS = 'VALID'
|
||||||
LEFT JOIN vehicle_lease_order_record vor
|
${LATEST_VEHICLE_LEASE_JOIN_SQL}
|
||||||
ON vor.vehicle_id = vi.id
|
|
||||||
AND vor.del_flag = '0'
|
|
||||||
AND vor.id = (
|
|
||||||
SELECT MAX(vor2.id)
|
|
||||||
FROM vehicle_lease_order_record vor2
|
|
||||||
WHERE vor2.vehicle_id = vi.id
|
|
||||||
AND vor2.del_flag = '0'
|
|
||||||
)
|
|
||||||
LEFT JOIN vehicle_lease_contract_info c
|
|
||||||
ON c.order_id = vor.contract_id
|
|
||||||
AND c.del_flag = '0'
|
|
||||||
LEFT JOIN customer_info ci
|
|
||||||
ON ci.id = vi.customer_id
|
|
||||||
AND ci.del_flag = '0'
|
|
||||||
WHERE vi.del_flag = '0'
|
WHERE vi.del_flag = '0'
|
||||||
AND COALESCE(vs.operation_status, '') <> '5'`;
|
AND COALESCE(vs.operation_status, '') <> '5'`;
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import pool from '../../db.js';
|
import pool from '../../db.js';
|
||||||
|
import { LATEST_VEHICLE_LEASE_JOIN_SQL } from '../../vehicle-lease-sql.js';
|
||||||
import type { Vehicle, VehicleRow } from '../../types.js';
|
import type { Vehicle, VehicleRow } from '../../types.js';
|
||||||
import { transformRow } from './model.js';
|
import { transformRow } from './model.js';
|
||||||
import type { WeeklyTruckIds } from './types.js';
|
import type { WeeklyTruckIds } from './types.js';
|
||||||
@@ -80,13 +81,13 @@ export const MAIN_SQL = `SELECT
|
|||||||
ELSE vm.brand
|
ELSE vm.brand
|
||||||
END AS 车辆品牌Label,
|
END AS 车辆品牌Label,
|
||||||
c.id AS 合同ID,
|
c.id AS 合同ID,
|
||||||
COALESCE(c.contract_code, vor.contract_code, vi.contract_code) AS 合同编码,
|
vor.contract_code AS 合同编码,
|
||||||
COALESCE(c.customer_name, vor.customer_name, ci.customer_name) AS 客户名称,
|
vor.customer_name AS 客户名称,
|
||||||
c.signing_company AS 合同归属公司,
|
c.signing_company AS 合同归属公司,
|
||||||
COALESCE(c.business_department_name, vor.business_dept) AS 合同归属部门,
|
vor.business_dept AS 合同归属部门,
|
||||||
NULLIF(vi.registered_ownership, '') AS 主体,
|
NULLIF(vi.registered_ownership, '') AS 主体,
|
||||||
COALESCE(c.project_name, vor.project_name) AS 项目名称,
|
vor.project_name AS 项目名称,
|
||||||
COALESCE(c.business_manager_name, vor.business_manager) AS 客户经理,
|
vor.business_manager AS 客户经理,
|
||||||
CAST(COALESCE(c.business_manager_id, vi.business_id) AS CHAR) AS 经理ID
|
CAST(COALESCE(c.business_manager_id, vi.business_id) AS CHAR) AS 经理ID
|
||||||
FROM vehicle_info vi
|
FROM vehicle_info vi
|
||||||
LEFT JOIN vehicle_status vs
|
LEFT JOIN vehicle_status vs
|
||||||
@@ -113,21 +114,7 @@ LEFT JOIN common_district vi_city
|
|||||||
LEFT JOIN common_district vi_operation_city
|
LEFT JOIN common_district vi_operation_city
|
||||||
ON vi_operation_city.CODE = vi.operation_city COLLATE utf8mb4_unicode_ci
|
ON vi_operation_city.CODE = vi.operation_city COLLATE utf8mb4_unicode_ci
|
||||||
AND vi_operation_city.STATUS = 'VALID'
|
AND vi_operation_city.STATUS = 'VALID'
|
||||||
LEFT JOIN vehicle_lease_order_record vor
|
${LATEST_VEHICLE_LEASE_JOIN_SQL}
|
||||||
ON vor.vehicle_id = vi.id
|
|
||||||
AND vor.del_flag = '0'
|
|
||||||
AND vor.id = (
|
|
||||||
SELECT MAX(vor2.id)
|
|
||||||
FROM vehicle_lease_order_record vor2
|
|
||||||
WHERE vor2.vehicle_id = vi.id
|
|
||||||
AND vor2.del_flag = '0'
|
|
||||||
)
|
|
||||||
LEFT JOIN vehicle_lease_contract_info c
|
|
||||||
ON c.order_id = vor.contract_id
|
|
||||||
AND c.del_flag = '0'
|
|
||||||
LEFT JOIN customer_info ci
|
|
||||||
ON ci.id = vi.customer_id
|
|
||||||
AND ci.del_flag = '0'
|
|
||||||
WHERE vi.del_flag = '0'
|
WHERE vi.del_flag = '0'
|
||||||
AND COALESCE(vs.operation_status, '') <> '5'`;
|
AND COALESCE(vs.operation_status, '') <> '5'`;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
/**
|
||||||
|
* 与 OneOS VehicleInfoMapper.LatestVehicleLeaseJoin 保持一致。
|
||||||
|
* vi 为车辆表;vor 为当前业务记录;c 为合同。
|
||||||
|
* 还车会清空 vor 的客户与合同信息,调用方不得用历史档案补回空值。
|
||||||
|
*/
|
||||||
|
export const LATEST_VEHICLE_LEASE_JOIN_SQL = `LEFT JOIN (
|
||||||
|
SELECT ranked_record.*
|
||||||
|
FROM (
|
||||||
|
SELECT source_record.*,
|
||||||
|
ROW_NUMBER() OVER (
|
||||||
|
PARTITION BY source_record.vehicle_id
|
||||||
|
ORDER BY COALESCE(
|
||||||
|
source_record.last_delivery_time,
|
||||||
|
source_record.last_return_time,
|
||||||
|
source_record.update_time
|
||||||
|
) DESC, source_record.id DESC
|
||||||
|
) AS lease_rn
|
||||||
|
FROM vehicle_lease_order_record source_record
|
||||||
|
WHERE source_record.del_flag = '0'
|
||||||
|
) ranked_record
|
||||||
|
WHERE ranked_record.lease_rn = 1
|
||||||
|
) vor ON vor.vehicle_id = vi.id
|
||||||
|
LEFT JOIN vehicle_lease_contract_info c
|
||||||
|
ON c.id = vor.contract_id
|
||||||
|
AND c.del_flag = '0'`;
|
||||||
Reference in New Issue
Block a user