JhHardwareWRS/utils/api.js
2026-07-25 05:47:20 +08:00

1958 lines
68 KiB
JavaScript

const requestClient = require('./request')
const util = require('./util')
const roleNames = {
worker: '冲压工人',
admin: '管理员',
manager: '经理',
}
const defaultRolePriority = ['worker', 'admin', 'manager']
const pickDefaultRole = people => {
const availableRoles = []
const personList = people || []
personList.forEach(person => {
if (person.role) {
availableRoles.push(person.role)
}
const roles = person.roles || []
roles.forEach(role => {
availableRoles.push(role)
})
})
const uniqueRoles = Array.from(new Set(availableRoles))
return defaultRolePriority.find(role => uniqueRoles.includes(role)) || uniqueRoles[0] || ''
}
const normalizeDeviceNo = value => {
return String(value || '').trim()
}
const moldDisplayName = (name, processName, stampingMethod = '') => {
const moldName = String(name || '').trim()
const process = String(processName || '').trim()
const stamping = String(stampingMethod || '').trim()
if (!moldName) {
return ''
}
if (stamping) {
return `${moldName} / ${process || '-'} / ${stamping}`
}
return process ? `${moldName} / ${process}` : moldName
}
const normalizeProcessValue = value => {
const text = String(value || '').trim()
if (text === '杂活') {
return text
}
const legacyMatch = text.match(/^0*(\d+)序$/)
if (legacyMatch) {
return String(Number(legacyMatch[1]))
}
const numberMatch = text.match(/^0*(\d+)$/)
if (numberMatch) {
return String(Number(numberMatch[1]))
}
return text
}
const processDisplayName = value => {
const process = normalizeProcessValue(value)
if (!process) {
return ''
}
return process === '杂活' ? process : `${process}`
}
const isCleaningStampingMethod = value => String(value || '').trim() === '清洗'
const isCleaningRow = row => (
isCleaningStampingMethod(row.stamping_method || row.stampingMethod)
)
const CONTINUOUS_DIE_STAMPING_METHOD = '连续模'
const isContinuousDieStampingMethod = value => String(value || '').trim() === CONTINUOUS_DIE_STAMPING_METHOD
const isContinuousDieRow = row => (
!!row && (
!!row.is_continuous_die
|| !!row.isContinuousDie
|| isContinuousDieStampingMethod(row.stamping_method || row.stampingMethod)
)
)
const MISC_WORK_PRODUCT_NAME = '处理杂活'
const MISC_WORK_PROCESS_NAME = '杂活'
const MISC_WORK_STAMPING_METHOD = '处理杂活'
const isMiscRow = row => (
!!row && (
row.is_misc
|| row.isMisc
|| String(row.stamping_method || row.stampingMethod || '').trim() === MISC_WORK_STAMPING_METHOD
|| (
String(row.product_name || row.productName || row.name || row.moldName || '').trim() === MISC_WORK_PRODUCT_NAME
&& String(row.process_name || row.process || row.processName || '').trim() === MISC_WORK_PROCESS_NAME
)
)
)
const isMultiPersonRow = row => (
!!row && (
!!row.is_multi_person
|| !!row.isMultiPerson
|| Number(row.operator_count || row.operatorCount || 1) > 1
)
)
const correctionValue = value => (
value === null || value === undefined || value === '' ? '-' : value
)
const correctionDisplayValue = value => {
const normalized = correctionValue(value)
if (typeof normalized === 'string' && normalized.includes('T')) {
return util.formatTime(normalized)
}
return normalized
}
const correctionPair = (corrections, key, label, currentValue) => {
if (!corrections || corrections[key] === undefined) {
return null
}
return {
key,
label,
oldValue: correctionValue(corrections[key]),
newValue: correctionValue(currentValue),
}
}
const round1 = value => Math.round(Number(value || 0) * 10) / 10
const round2 = value => Math.round(Number(value || 0) * 100) / 100
const calculateStandardWorkload = (allocatedMinutes, standardBeat) => {
const beat = Number(standardBeat || 0)
if (beat <= 0) {
return 0
}
return round1(Number(allocatedMinutes || 0) * 60 / beat)
}
const classifyBeat = (actualBeat, standardBeat) => {
const actual = Number(actualBeat || 0)
const standard = Number(standardBeat || 0)
if (standard <= 0 || actual <= 0) {
return { className: '', reason: '', isAbnormal: false }
}
if (actual > standard) {
return { className: 'metric-danger', reason: '慢', isAbnormal: true }
}
if (actual < standard * 0.7) {
return { className: 'metric-danger', reason: '快', isAbnormal: true }
}
if (actual < standard) {
return { className: 'metric-success', reason: '', isAbnormal: false }
}
return { className: '', reason: '', isAbnormal: false }
}
const classifyWorkload = (goodQty, standardWorkload) => {
const good = Number(goodQty || 0)
const standard = Number(standardWorkload || 0)
if (standard <= 0) {
return { className: '', reason: '', isAbnormal: false }
}
if (good < standard) {
return { className: 'metric-danger', reason: '小', isAbnormal: true }
}
if (good > standard * 1.3) {
return { className: 'metric-danger', reason: '大', isAbnormal: true }
}
if (good > standard) {
return { className: 'metric-success', reason: '', isAbnormal: false }
}
return { className: '', reason: '', isAbnormal: false }
}
const toCamelAttendancePoint = row => ({
name: row.name || '',
latitude: row.latitude === null || row.latitude === undefined ? '' : row.latitude,
longitude: row.longitude === null || row.longitude === undefined ? '' : row.longitude,
radiusMeters: row.radius_meters === null || row.radius_meters === undefined ? 500 : row.radius_meters,
dayStart: row.day_start || '08:00',
dayEnd: row.day_end || '17:20',
lunchStart: row.lunch_start || '11:40',
lunchEnd: row.lunch_end || '12:40',
dinnerStart: row.dinner_start || '17:20',
dinnerEnd: row.dinner_end || '18:00',
overtimeStart: row.overtime_start || '18:00',
overtimeEnd: row.overtime_end || '20:00',
nightStart: row.night_start || '20:00',
nightEnd: row.night_end || '06:00',
remark: row.remark || '',
isActive: row.is_active !== false,
createdAt: row.created_at || '',
updatedAt: row.updated_at || '',
updatedAtText: row.updated_at ? util.formatTime(row.updated_at) : '',
})
const toSnakeAttendancePoint = row => ({
name: row.name,
latitude: row.latitude === '' || row.latitude === undefined ? null : Number(row.latitude),
longitude: row.longitude === '' || row.longitude === undefined ? null : Number(row.longitude),
radius_meters: row.radiusMeters === '' || row.radiusMeters === undefined ? 500 : Number(row.radiusMeters),
day_start: row.dayStart || '08:00',
day_end: row.dayEnd || '17:20',
lunch_start: row.lunchStart || '11:40',
lunch_end: row.lunchEnd || '12:40',
dinner_start: row.dinnerStart || '17:20',
dinner_end: row.dinnerEnd || '18:00',
overtime_start: row.overtimeStart || '18:00',
overtime_end: row.overtimeEnd || '20:00',
night_start: row.nightStart || '20:00',
night_end: row.nightEnd || '06:00',
remark: row.remark || null,
is_active: row.isActive !== false,
original_name: row.originalName || null,
})
const toCamelPerson = row => {
const roles = row.roles && row.roles.length ? row.roles : (row.role ? [row.role] : [])
const roleNamesList = row.role_names && row.role_names.length
? row.role_names
: roles.map(role => roleNames[role] || role)
const temporaryExpiresAtText = row.temporary_expires_at ? util.formatTime(row.temporary_expires_at) : ''
const attendancePointNames = row.attendance_point_names || []
return {
phone: row.phone,
name: row.name,
role: row.role || roles[0] || '',
roleName: row.role_name || roleNamesList.join('、'),
roles,
roleNames: roleNamesList,
attendancePointNames,
attendancePoints: (row.attendance_points || []).map(toCamelAttendancePoint),
attendancePointsText: attendancePointNames.join('、'),
rolesDisplay: roles.map((role, index) => ({
role,
roleName: roleNamesList[index] || roleNames[role] || role,
})),
isTemporary: !!row.is_temporary,
temporaryExpiresAt: row.temporary_expires_at || '',
temporaryExpiresAtText,
temporaryExpired: !!row.temporary_expired,
temporaryStatusText: row.is_temporary
? (row.temporary_expired ? '临时工已过期' : `临时工有效至 ${temporaryExpiresAtText || '-'}`)
: '',
}
}
const toSnakePerson = row => ({
phone: row.phone,
name: row.name,
role: row.role,
attendance_point_names: row.attendancePointNames || [],
original_phone: row.originalPhone || null,
})
const toCamelProduct = row => ({
attendancePointName: row.attendance_point_name || '',
projectNo: row.project_no,
productName: row.product_name,
profileNo: row.profile_no || '',
materialCode: row.material_code || '',
materialName: row.material_name || '',
supplier: row.supplier || '',
productNetWeightKg: row.product_net_weight_kg === null || row.product_net_weight_kg === undefined ? '' : row.product_net_weight_kg,
productGrossWeightKg: row.product_gross_weight_kg === null || row.product_gross_weight_kg === undefined ? '' : row.product_gross_weight_kg,
allowedScrapRate: row.scrap_loss_rate === null || row.scrap_loss_rate === undefined ? '' : row.scrap_loss_rate,
wastePriceYuanPerKg: row.waste_price_yuan_per_kg === null || row.waste_price_yuan_per_kg === undefined ? '' : row.waste_price_yuan_per_kg,
deviceNo: row.device_no,
process: normalizeProcessValue(row.process_name || ''),
rawProcess: row.process_name || '',
processDisplay: processDisplayName(row.process_name || ''),
stampingMethod: row.stamping_method || '',
isCleaning: isCleaningRow(row),
isContinuousDie: isContinuousDieRow(row),
isMisc: isMiscRow(row),
isMultiPerson: isMultiPersonRow(row),
operatorCount: row.operator_count,
processUnitPriceYuan: row.process_unit_price_yuan || 0,
standardBeat: row.standard_beat,
standardWorkload: row.standard_workload,
displayName: row.display_name || `${row.project_no} / ${moldDisplayName(row.product_name, row.process_name, row.stamping_method)}`,
uniqueKey: `${row.attendance_point_name || ''}||${row.project_no || ''}||${row.product_name || ''}||${row.process_name || ''}`,
})
const toCamelNotice = row => ({
id: row.id,
title: row.title || '',
content: row.content || '',
attendancePointNames: row.attendance_point_names || [],
attendancePointsText: (row.attendance_point_names || []).join('、'),
sortOrder: row.sort_order || 0,
isActive: !!row.is_active,
createdBy: row.created_by || '',
creatorName: row.creator_name || '',
createdAt: row.created_at || '',
updatedAt: row.updated_at || '',
updatedAtText: row.updated_at ? util.formatTime(row.updated_at) : '',
})
const toCamelEquipment = row => ({
attendancePointName: row.attendance_point_name || '',
deviceNo: row.device_no || '',
deviceType: row.device_type || '冲压设备',
remark: row.remark || '',
createdAt: row.created_at || '',
updatedAt: row.updated_at || '',
updatedAtText: row.updated_at ? util.formatTime(row.updated_at) : '',
displayName: row.display_name || (row.remark ? `${row.device_no} / ${row.remark}` : row.device_no),
uniqueKey: `${row.attendance_point_name || ''}||${row.device_no || ''}`,
})
const toCamelMoldOption = row => ({
attendancePointName: row.attendance_point_name || '',
name: row.name || row.device_no || '',
moldName: row.name || row.device_no || '',
processName: row.process_name || '',
stampingMethod: row.stamping_method || '',
operatorCount: row.operator_count || 1,
isCleaning: !!row.is_cleaning || isCleaningRow(row),
isMisc: !!row.is_misc || isMiscRow(row),
isContinuousDie: !!row.is_continuous_die || isContinuousDieRow(row),
isMultiPerson: isMultiPersonRow(row),
displayName: row.display_name || moldDisplayName(row.name || row.device_no, row.process_name, row.stamping_method),
uniqueKey: `${row.attendance_point_name || ''}||${row.name || row.device_no || ''}||${row.process_name || ''}`,
})
const optionalNumber = value => (
value === '' || value === undefined || value === null ? null : Number(value)
)
const toSnakeProduct = row => ({
attendance_point_name: row.attendancePointName,
project_no: row.projectNo,
product_name: row.productName,
profile_no: row.profileNo || null,
material_code: row.materialCode || null,
material_name: row.materialName || null,
supplier: row.supplier || null,
product_net_weight_kg: optionalNumber(row.productNetWeightKg),
product_gross_weight_kg: optionalNumber(row.productGrossWeightKg),
scrap_loss_rate: optionalNumber(row.allowedScrapRate),
waste_price_yuan_per_kg: optionalNumber(row.wastePriceYuanPerKg),
device_no: '',
process_name: normalizeProcessValue(row.process || ''),
stamping_method: row.stampingMethod || null,
operator_count: Number(row.operatorCount || 1),
process_unit_price_yuan: Number(row.processUnitPriceYuan || 0),
standard_beat: Number(row.standardBeat || 0),
original_project_no: row.originalProjectNo || null,
original_attendance_point_name: row.originalAttendancePointName || null,
original_product_name: row.originalProductName || null,
original_device_no: row.originalDeviceNo !== undefined ? row.originalDeviceNo : '',
original_process_name: row.originalProcess !== undefined ? row.originalProcess : null,
})
const toSnakeNotice = row => ({
title: row.title,
content: row.content,
attendance_point_names: row.attendancePointNames || [],
sort_order: Number(row.sortOrder || 0),
is_active: !!row.isActive,
})
const toSnakeEquipment = row => ({
attendance_point_name: row.attendancePointName,
device_no: normalizeDeviceNo(row.deviceNo),
device_type: row.deviceType || '冲压设备',
remark: row.remark || null,
original_attendance_point_name: row.originalAttendancePointName || null,
original_device_no: row.originalDeviceNo || null,
})
const toCamelPage = (page, mapper) => ({
rows: (page.rows || []).map(mapper),
page: page.page,
pageSize: page.page_size,
total: page.total,
totalPages: page.total_pages,
})
const toCamelUsageStatsRow = row => ({
id: row.id === undefined || row.id === null ? '' : String(row.id),
category: row.category || '',
attendancePointName: row.attendance_point_name || '',
name: row.name || '',
objectType: row.object_type || '',
metricKind: row.metric_kind || '',
value: Number(row.value || 0),
reportCount: Number(row.report_count || 0),
productName: row.product_name || '',
processName: row.process_name || '',
stampingMethod: row.stamping_method || '',
tags: Array.isArray(row.tags) ? row.tags : [],
})
const toCamelUsageStatsDailyRow = row => ({
reportDate: row.report_date || '',
value: Number(row.value || 0),
reportCount: Number(row.report_count || 0),
})
const toCamelUsageStatsReportRow = row => ({
reportId: Number(row.report_id || 0),
reportDate: row.report_date || '',
employeePhone: row.employee_phone || '',
employeeName: row.employee_name || '',
displayName: row.display_name || '',
value: Number(row.value || 0),
reportCount: Number(row.report_count || 0),
})
const toCamelUsageStatsDetail = data => ({
...toCamelUsageStatsRow(data || {}),
dailyRows: ((data && data.daily_rows) || []).map(toCamelUsageStatsDailyRow),
reportRows: ((data && data.report_rows) || []).map(toCamelUsageStatsReportRow),
})
const usageStatsParams = filters => ({
category: filters.category,
id: filters.id,
metric_kind: filters.metricKind,
sort_by: filters.sortBy,
start_date: filters.startDate,
end_date: filters.endDate,
attendance_point_name: filters.attendancePointName,
keyword: filters.keyword,
name: filters.name,
product_name: filters.productName,
process_name: filters.processName,
stamping_method: filters.stampingMethod,
})
const formatDurationMinutes = minutes => {
const totalMinutes = Math.max(0, Math.round(Number(minutes || 0)))
if (totalMinutes < 60) {
return `${totalMinutes}分钟`
}
const days = Math.floor(totalMinutes / 1440)
const hours = Math.floor((totalMinutes % 1440) / 60)
const restMinutes = totalMinutes % 60
const parts = []
if (days) {
parts.push(`${days}`)
}
if (hours) {
parts.push(`${hours}小时`)
}
if (restMinutes) {
parts.push(`${restMinutes}分钟`)
}
return parts.join('')
}
const trimDurationNumber = value => {
const rounded = Math.round(Number(value || 0) * 100) / 100
return Number.isInteger(rounded) ? String(rounded) : String(rounded)
}
const formatDurationAllUnits = minutes => {
const safeMinutes = Math.max(0, Number(minutes || 0))
return `${trimDurationNumber(safeMinutes)}分钟(${trimDurationNumber(safeMinutes / 60)}小时 / ${trimDurationNumber(safeMinutes / 1440)}天)`
}
const toCamelMonitorLock = row => ({
id: row.id,
attendancePointName: row.attendance_point_name || '',
moldName: row.mold_name || '',
processName: row.process_name || '',
stampingMethod: row.stamping_method || '',
moldDisplayName: row.mold_display_name || moldDisplayName(row.mold_name, row.process_name, row.stamping_method),
occupiedPhone: row.occupied_phone || '',
occupiedName: row.occupied_name || '',
occupiedStartAt: row.occupied_start_at || '',
occupiedStartAtText: row.occupied_start_at ? util.formatTime(row.occupied_start_at) : '',
occupiedMinutes: row.occupied_minutes || 0,
occupiedDurationText: formatDurationMinutes(row.occupied_minutes),
})
const toCamelMoldLockFeedback = row => ({
id: row.id,
attendancePointName: row.attendance_point_name || '',
moldName: row.mold_name || '',
processName: row.process_name || '',
stampingMethod: row.stamping_method || '',
moldDisplayName: row.mold_display_name || moldDisplayName(row.mold_name, row.process_name, row.stamping_method),
lockId: row.lock_id,
reporterPhone: row.reporter_phone || '',
reporterName: row.reporter_name || '',
occupiedPhone: row.occupied_phone || '',
occupiedName: row.occupied_name || '',
feedbackAt: row.feedback_at || '',
feedbackAtText: row.feedback_at ? util.formatTime(row.feedback_at) : '',
readAt: row.read_at || '',
readAtText: row.read_at ? util.formatTime(row.read_at) : '',
handledAt: row.handled_at || '',
handledAtText: row.handled_at ? util.formatTime(row.handled_at) : '',
status: row.status || '',
statusName: row.status_name || '',
isHandled: row.status === 'handled',
canRelease: row.can_release !== false,
})
const toCamelSession = row => ({
id: row.id,
attendancePointName: row.attendance_point_name || '',
employeePhone: row.employee_phone,
startAt: row.start_at,
endAt: row.end_at || '',
status: row.status,
devices: row.devices || [],
})
const toCamelMetrics = row => ({
effectiveMinutes: row.effective_minutes,
shiftDistributionText: row.shift_distribution_text || '',
shiftDayMinutes: row.shift_day_minutes || 0,
shiftOvertimeMinutes: row.shift_overtime_minutes || 0,
shiftNightMinutes: row.shift_night_minutes || 0,
shiftOtherMinutes: row.shift_other_minutes || 0,
shiftMealBreakMinutes: row.shift_meal_break_minutes || 0,
totalGood: row.total_good_qty,
totalOutput: row.total_output_qty,
actualBeat: row.actual_beat,
standardBeat: row.standard_beat,
expectedWorkload: row.expected_workload,
paceRate: row.pace_rate,
workloadRate: row.workload_rate,
})
const formatWorkTime = minutes => {
const safeMinutes = Number(minutes || 0)
const seconds = Math.round(safeMinutes * 60)
if (seconds < 60) {
return `${seconds}`
}
return `${Math.round(safeMinutes * 100) / 100}分钟`
}
const toCamelAllocation = row => ({
allocationDate: row.allocation_date || '',
dayMinutes: Number(row.day_minutes || 0),
overtimeMinutes: Number(row.overtime_minutes || 0),
nightMinutes: Number(row.night_minutes || 0),
effectiveMinutes: Number(row.effective_minutes || 0),
goodQty: Number(row.good_qty || 0),
defectQty: Number(row.defect_qty || 0),
scrapQty: Number(row.scrap_qty || 0),
changeoverCount: Number(row.changeover_count || 0),
referenceWage: Number(row.reference_wage || 0),
})
const toCamelReportItem = row => {
const defectQty = Number(row.defect_qty || 0)
const scrapQty = Number(row.scrap_qty || 0)
const goodQty = Number(row.good_qty || 0)
const totalDefectQty = defectQty + scrapQty
const outputQty = goodQty + totalDefectQty
const allocatedMinutes = Number(row.allocated_minutes || 0)
const actualBeat = outputQty > 0 ? round2(allocatedMinutes * 60 / outputQty) : 0
const standardBeat = Number(row.standard_beat || 0)
const calculatedWorkload = calculateStandardWorkload(allocatedMinutes, standardBeat)
const standardWorkload = calculatedWorkload || Number(row.standard_workload || 0)
const paceRate = standardBeat > 0 ? round1((actualBeat - standardBeat) / standardBeat * 100) : 0
const workloadRate = standardWorkload > 0 ? round1((goodQty - standardWorkload) / standardWorkload * 100) : 0
const isCleaning = isCleaningRow(row)
const isContinuousDie = isContinuousDieRow(row)
const isMisc = isMiscRow(row)
const noOutputMetric = isCleaning || isMisc
const beatComparison = noOutputMetric ? classifyBeat(0, 0) : classifyBeat(actualBeat, standardBeat)
const workloadComparison = noOutputMetric ? classifyWorkload(0, 0) : classifyWorkload(goodQty, standardWorkload)
const corrections = row.corrections || {}
const correctionPairs = [
correctionPair(corrections, 'device_no', '设备号', row.device_no),
correctionPair(corrections, 'project_no', '项目号', row.project_no),
correctionPair(corrections, 'product_name', '产品名称', row.product_name),
correctionPair(corrections, 'process_name', '工序', row.process_name || ''),
correctionPair(corrections, 'stamping_method', '冲压方式', row.stamping_method || ''),
correctionPair(corrections, 'raw_material_batch_no', '材料库存批次号', row.raw_material_batch_no || ''),
correctionPair(corrections, 'process_unit_price_yuan', '工序单价', row.process_unit_price_yuan || 0),
correctionPair(corrections, 'changeover_count', '换料次数', row.changeover_count || 0),
correctionPair(corrections, 'good_qty', isCleaning ? '清洗数量' : '成品数量', goodQty),
correctionPair(corrections, 'defect_qty', '不良数量', Math.round(totalDefectQty * 100) / 100),
correctionPair(corrections, 'scrap_qty', '报废数量', scrapQty),
correctionPair(corrections, 'remark', '杂活事项', row.remark || ''),
].filter(Boolean)
return {
id: row.id,
attendancePointName: row.attendance_point_name || '',
startAt: row.started_at || '',
startAtText: row.started_at ? util.formatTime(row.started_at) : '',
deviceNo: row.device_no,
projectNo: row.project_no,
productName: row.product_name,
displayName: moldDisplayName(row.product_name, row.process_name, row.stamping_method),
materialCode: row.material_code || '',
materialName: row.material_name || '',
rawMaterialBatchNo: row.raw_material_batch_no || '',
process: row.process_name || '',
stampingMethod: row.stamping_method || '',
operatorCount: row.operator_count || 1,
processUnitPriceYuan: row.process_unit_price_yuan || 0,
isCleaning,
isContinuousDie,
changeoverCount: row.changeover_count === null || row.changeover_count === undefined ? 0 : Number(row.changeover_count || 0),
isMisc,
isMultiPerson: isMultiPersonRow(row),
isMultiPersonAssistant: !!row.is_multi_person_assistant,
standardBeat,
standardWorkload,
goodQty,
defectQty: Math.round(totalDefectQty * 100) / 100,
scrapQty,
allocatedMinutes,
workTimeText: isCleaning ? '不计工时' : formatWorkTime(allocatedMinutes),
actualBeat,
paceRate,
workloadRate,
paceText: noOutputMetric ? '不计节拍' : (paceRate > 0 ? `${Math.abs(paceRate)}%` : `${Math.abs(paceRate)}%`),
workloadText: noOutputMetric ? '不计标准工作量' : (workloadRate >= 0 ? `${Math.abs(workloadRate)}%` : `${Math.abs(workloadRate)}%`),
beatCompareClass: beatComparison.className,
beatReason: beatComparison.reason,
workloadCompareClass: workloadComparison.className,
workloadReason: workloadComparison.reason,
remark: row.remark || '',
corrections,
correctionPairs,
hasCorrections: correctionPairs.length > 0,
allocations: (row.allocations || []).map(toCamelAllocation),
}
}
const toCamelDeviceSegment = row => {
const corrections = row.corrections || {}
const correctionPairs = [
correctionPair(corrections, 'scanned_at', '换模具时间', row.scanned_at ? util.formatTime(row.scanned_at) : ''),
].filter(Boolean).map(item => ({
...item,
oldValue: item.oldValue === '-' ? '-' : util.formatTime(item.oldValue),
}))
return {
id: row.id,
attendancePointName: row.attendance_point_name || '',
deviceNo: row.device_no || '',
moldName: row.mold_name || row.device_no || '',
process: row.process_name || '',
stampingMethod: row.stamping_method || '',
displayName: row.display_name || moldDisplayName(row.mold_name || row.device_no, row.process_name, row.stamping_method),
scannedAt: row.scanned_at || '',
scannedAtText: row.scanned_at ? util.formatTime(row.scanned_at) : '',
releasedAt: row.released_at || '',
releasedAtText: row.released_at ? util.formatTime(row.released_at) : '',
sortOrder: Number(row.sort_order || 0),
corrections,
correctionPairs,
hasCorrections: correctionPairs.length > 0,
}
}
const toCamelReport = row => {
const paceRate = Number(row.metrics && row.metrics.pace_rate || 0)
const workloadRate = Number(row.metrics && row.metrics.workload_rate || 0)
const items = (row.items || []).map(toCamelReportItem)
const deviceSegments = (row.device_segments || []).map(toCamelDeviceSegment)
const isCleaning = items.length > 0 && items.every(item => item.isCleaning)
const hasMisc = items.some(item => item.isMisc)
const hasContinuousDie = items.some(item => item.isContinuousDie)
const hasMultiPerson = items.some(item => item.isMultiPerson)
const totalDefect = Math.round(items.reduce((sum, item) => sum + Number(item.defectQty || 0), 0) * 100) / 100
const corrections = row.corrections || {}
const reportCorrectionPairs = [
correctionPair(corrections, 'start_at', '上班时间', row.start_at ? util.formatTime(row.start_at) : ''),
correctionPair(corrections, 'end_at', '下班时间', row.end_at ? util.formatTime(row.end_at) : ''),
correctionPair(corrections, 'review_remark', '备注', row.review_remark || ''),
].filter(Boolean).map(item => ({
...item,
oldValue: (item.key === 'start_at' || item.key === 'end_at') && item.oldValue !== '-'
? util.formatTime(item.oldValue)
: item.oldValue,
})).concat(deviceSegments.flatMap(segment => (
(segment.correctionPairs || []).map(pair => ({
...pair,
label: `${segment.displayName} ${pair.label}`,
}))
)))
return {
id: row.id,
attendancePointName: row.attendance_point_name || '',
sessionId: row.session_id,
employeePhone: row.employee_phone,
employeeName: row.employee_name,
reportDate: row.report_date,
startAt: row.start_at,
endAt: row.end_at,
startAtText: util.formatTime(row.start_at),
endAtText: util.formatTime(row.end_at),
durationMinutes: row.duration_minutes,
breakMinutes: row.break_minutes,
status: row.status,
statusName: row.status_name,
reviewerPhone: row.reviewer_phone || '',
reviewerName: row.reviewer_name || '',
reviewedAt: row.reviewed_at || '',
reviewedAtText: row.reviewed_at ? util.formatTime(row.reviewed_at) : '',
rejectReason: row.reject_reason || '',
reviewRemark: row.review_remark || '',
submittedAt: row.submitted_at,
submittedAtText: util.formatTime(row.submitted_at),
isSystemAutoSubmitted: !!row.is_system_auto_submitted,
autoSubmitReason: row.auto_submit_reason || '超时-系统自动提交',
isMultiPersonAssistant: !!row.is_multi_person_assistant,
multiPersonSourceReportId: row.multi_person_source_report_id || null,
isVoided: !!row.is_voided,
voidedAt: row.voided_at || '',
voidedAtText: row.voided_at ? util.formatTime(row.voided_at) : '',
voidedBy: row.voided_by || '',
voidedByName: row.voided_by_name || '',
unvoidDeadlineAt: row.unvoid_deadline_at || '',
unvoidDeadlineAtText: row.unvoid_deadline_at ? util.formatTime(row.unvoid_deadline_at) : '',
canVoid: !!row.can_void,
canUnvoid: !!row.can_unvoid,
canEdit: !!row.can_edit,
isModified: !!row.is_modified,
allocationSummaryText: row.allocation_summary_text || '',
deviceSegments,
items,
isCleaning,
hasMisc,
hasContinuousDie,
hasMultiPerson,
metrics: {
...toCamelMetrics(row.metrics || {}),
totalDefect,
},
resultText: row.result_text,
paceText: paceRate > 0 ? `${Math.abs(paceRate)}%` : `${Math.abs(paceRate)}%`,
workloadText: workloadRate >= 0 ? `${Math.abs(workloadRate)}%` : `${Math.abs(workloadRate)}%`,
corrections,
correctionPairs: reportCorrectionPairs,
hasCorrections: reportCorrectionPairs.length > 0 || items.some(item => item.hasCorrections) || deviceSegments.some(item => item.hasCorrections),
}
}
const emptyDraftItem = startAt => ({
attendancePointName: '',
productIndex: -1,
deviceIndex: -1,
deviceNo: '',
startAt: startAt || '',
startAtText: startAt ? util.formatTime(startAt) : '',
projectNo: '',
productName: '',
materialCode: '',
materialName: '',
rawMaterialBatchNo: '',
rawMaterialBatchOptions: [],
rawMaterialBatchIndex: -1,
rawMaterialBatchError: false,
process: '',
stampingMethod: '',
operatorCount: 1,
processUnitPriceYuan: 0,
isContinuousDie: false,
isMultiPerson: false,
isMisc: false,
standardBeat: 0,
standardWorkload: 0,
changeoverCount: '',
changeoverCountError: false,
goodQty: '',
defectQty: '',
deviceError: false,
})
const batchIndexOf = (options = [], value = '') => {
if (!value) {
return -1
}
return (options || []).findIndex(option => String(option) === String(value))
}
const optionToDraftItem = (option, productIndex = 0, startAt = '', equipmentOptions = [], deviceNo = '', rawMaterialBatchOptions = [], rawMaterialBatchNo = '') => {
const selectedDeviceNo = deviceNo || ''
const deviceIndex = selectedDeviceNo
? Math.max(0, equipmentOptions.findIndex(item => item.deviceNo === selectedDeviceNo))
: -1
const batchOptions = rawMaterialBatchOptions || []
return {
productIndex,
attendancePointName: option ? option.attendancePointName : '',
deviceIndex,
deviceNo: selectedDeviceNo,
startAt: startAt || '',
startAtText: startAt ? util.formatTime(startAt) : '',
projectNo: option ? option.projectNo : '',
productName: option ? option.productName : '',
materialCode: option ? option.materialCode : '',
materialName: option ? option.materialName : '',
rawMaterialBatchNo: rawMaterialBatchNo || '',
rawMaterialBatchOptions: batchOptions,
rawMaterialBatchIndex: batchIndexOf(batchOptions, rawMaterialBatchNo),
rawMaterialBatchError: false,
process: option ? option.process : '',
stampingMethod: option ? option.stampingMethod : '',
operatorCount: option ? option.operatorCount : 1,
processUnitPriceYuan: option ? option.processUnitPriceYuan : 0,
isContinuousDie: option ? !!option.isContinuousDie : false,
isMultiPerson: option ? !!option.isMultiPerson : false,
isMisc: option ? !!option.isMisc : false,
standardBeat: option ? option.standardBeat : 0,
standardWorkload: option ? option.standardWorkload : 0,
changeoverCount: '',
changeoverCountError: false,
goodQty: '',
defectQty: '',
deviceError: false,
}
}
const apiDraftItemToCamel = (item, options, startAt = '', equipmentOptions = []) => {
const rawMaterialBatchOptions = item && item.raw_material_batch_options ? item.raw_material_batch_options : []
const rawMaterialBatchNo = item && item.raw_material_batch_no ? item.raw_material_batch_no : ''
if (options.length >= 1 && (!item || !item.product_name)) {
return optionToDraftItem(
options[0],
0,
startAt,
equipmentOptions,
item && item.device_no,
rawMaterialBatchOptions,
rawMaterialBatchNo,
)
}
const selectedDeviceNo = item.device_no || ''
const productIndex = Math.max(0, options.findIndex(option => (
option.projectNo === item.project_no
&& option.productName === item.product_name
&& (option.process || '') === (item.process_name || '')
)))
return {
productIndex,
attendancePointName: item.attendance_point_name || '',
deviceNo: selectedDeviceNo,
deviceIndex: selectedDeviceNo
? Math.max(0, equipmentOptions.findIndex(option => option.deviceNo === selectedDeviceNo))
: -1,
startAt: item.started_at || startAt || '',
startAtText: util.formatTime(item.started_at || startAt || ''),
projectNo: item.project_no || '',
productName: item.product_name || '',
materialCode: item.material_code || '',
materialName: item.material_name || '',
rawMaterialBatchNo,
rawMaterialBatchOptions,
rawMaterialBatchIndex: batchIndexOf(rawMaterialBatchOptions, rawMaterialBatchNo),
rawMaterialBatchError: false,
process: item.process_name || '',
stampingMethod: item.stamping_method || '',
operatorCount: item.operator_count || 1,
processUnitPriceYuan: item.process_unit_price_yuan || 0,
isContinuousDie: !!item.is_continuous_die || isContinuousDieRow(item),
isMultiPerson: isMultiPersonRow(item),
isMisc: !!item.is_misc || isMiscRow(item),
standardBeat: item.standard_beat || 0,
standardWorkload: item.standard_workload || 0,
changeoverCount: item.changeover_count === null || item.changeover_count === undefined ? '' : String(item.changeover_count || ''),
changeoverCountError: false,
goodQty: '',
defectQty: '',
deviceError: false,
}
}
const toCamelDraft = row => ({
sessionId: row.session_id,
attendancePointName: row.attendance_point_name || '',
employeePhone: row.employee_phone,
employeeName: row.employee_name,
startAt: row.start_at,
endAt: row.end_at,
startAtText: util.formatTime(row.start_at),
endAtText: util.formatTime(row.end_at),
durationMinutes: row.duration_minutes,
breakMinutes: 0,
deviceBlocks: (row.device_blocks || []).map(block => {
const options = (block.options || []).map(toCamelProduct)
const equipmentOptions = (block.equipment_options || []).map(toCamelEquipment)
const rawItems = block.items && block.items.length ? block.items : [{}]
return {
deviceNo: block.device_no,
attendancePointName: block.attendance_point_name || row.attendance_point_name || '',
moldName: block.mold_name || block.device_no,
process: block.process_name || '',
stampingMethod: block.stamping_method || '',
moldDisplayName: block.display_name || moldDisplayName(block.mold_name || block.device_no, block.process_name, block.stamping_method),
blockKey: `${block.device_no || ''}||${block.process_name || ''}`,
isMisc: !!block.is_misc || isMiscRow({ product_name: block.mold_name || block.device_no, process_name: block.process_name }),
isContinuousDie: !!block.is_continuous_die || isContinuousDieRow(block),
startAt: block.start_at,
endAt: block.end_at,
durationMinutes: block.duration_minutes || 0,
startAtText: util.formatTime(block.start_at),
endAtText: util.formatTime(block.end_at),
options,
canAddItem: new Set(options.map(option => `${option.projectNo}||${option.productName}||${option.process}`)).size > 1,
equipmentOptions,
items: rawItems.map(item => apiDraftItemToCamel(item, options, block.start_at, equipmentOptions)),
}
}),
})
const toSubmitPayload = draft => ({
session_id: Number(draft.sessionId),
device_blocks: (draft.deviceBlocks || []).map(block => ({
attendance_point_name: block.attendancePointName || draft.attendancePointName || '',
device_no: block.deviceNo,
process_name: block.process || '',
items: (block.items || []).map(item => ({
device_no: normalizeDeviceNo(item.deviceNo),
attendance_point_name: item.attendancePointName || block.attendancePointName || draft.attendancePointName || '',
project_no: item.projectNo,
product_name: item.productName,
material_code: item.materialCode || null,
material_name: item.materialName || null,
raw_material_batch_no: item.rawMaterialBatchNo || null,
process_name: item.process || null,
stamping_method: item.stampingMethod || null,
operator_count: Number(item.operatorCount || 1),
process_unit_price_yuan: Number(item.processUnitPriceYuan || 0),
changeover_count: item.isContinuousDie ? Number(item.changeoverCount || 0) : 0,
is_continuous_die: !!item.isContinuousDie,
is_misc: !!item.isMisc,
remark: item.remark || null,
standard_beat: Number(item.standardBeat || 0),
standard_workload: Number(item.standardWorkload || 0),
good_qty: Number(item.goodQty || 0),
defect_qty: Number(item.defectQty || 0),
scrap_qty: 0,
started_at: item.startAt || null,
})),
})),
})
const getCurrentUser = () => {
const auth = requestClient.getAuth()
return auth && auth.user ? auth.user : null
}
const refreshCurrentUser = async () => {
const auth = requestClient.getAuth()
if (!auth || !auth.accessToken) {
return null
}
const user = toCamelPerson(await requestClient.request({
url: '/api/auth/me',
}))
requestClient.setAuth({ ...auth, user })
return user
}
const saveAuth = data => {
const auth = {
accessToken: data.access_token,
user: toCamelPerson(data.user),
}
requestClient.setAuth(auth)
return auth.user
}
const normalizeLoginResult = data => {
if (data.needs_temporary_worker) {
return {
needsTemporaryWorker: true,
temporaryToken: data.temporary_token,
temporaryPhone: data.temporary_phone,
temporaryExpiresAt: data.temporary_expires_at || '',
}
}
if (data.needs_selection) {
return {
needsSelection: true,
selectionToken: data.selection_token,
matchedPeople: (data.matched_people || []).map(toCamelPerson),
}
}
return saveAuth(data)
}
const autoSelectDefaultRole = async result => {
if (!result || !result.needsSelection) {
return result
}
const role = pickDefaultRole(result.matchedPeople)
if (!role || !result.selectionToken) {
return result
}
return selectRole(result.selectionToken, role)
}
const loginWithPhoneCode = async (phoneCode, selectedRole = '') => {
const data = await requestClient.request({
url: '/api/auth/wechat-login',
method: 'POST',
data: {
phone_code: phoneCode,
selected_role: selectedRole || null,
},
})
return autoSelectDefaultRole(normalizeLoginResult(data))
}
const toCamelErpLoginPreview = data => ({
sessionId: data.session_id,
systemName: data.system_name || '嘉恒智能五金 ERP',
startedAt: data.started_at || '',
startedAtText: data.started_at ? util.formatTime(data.started_at) : '',
expiresAt: data.expires_at || '',
expiresAtText: data.expires_at ? util.formatTime(data.expires_at) : '',
requestIpHint: data.request_ip_hint || '',
userAgentHint: data.user_agent_hint || '',
})
const toCamelErpLoginAction = data => ({
status: data.status || '',
failureReason: data.failure_reason || '',
})
const getErpLoginPreview = async (ticket, sessionId) => toCamelErpLoginPreview(await requestClient.request({
url: `/api/erp-login/sessions/${encodeURIComponent(ticket)}`,
params: {
session_id: sessionId,
},
}))
const confirmErpLogin = async (ticket, sessionId, phoneCode) => toCamelErpLoginAction(await requestClient.request({
url: `/api/erp-login/sessions/${encodeURIComponent(ticket)}/confirm`,
method: 'POST',
params: {
session_id: sessionId,
},
data: {
phone_code: phoneCode,
},
}))
const cancelErpLogin = async (ticket, sessionId) => toCamelErpLoginAction(await requestClient.request({
url: `/api/erp-login/sessions/${encodeURIComponent(ticket)}/cancel`,
method: 'POST',
params: {
session_id: sessionId,
},
}))
const createTemporaryWorker = async (temporaryToken, temporaryName = '', attendancePoint = null) => {
const data = await requestClient.request({
url: '/api/auth/wechat-login',
method: 'POST',
data: {
create_temporary_worker: true,
temporary_token: temporaryToken,
temporary_name: temporaryName,
temporary_attendance_point_name: attendancePoint && attendancePoint.factory ? attendancePoint.factory.name : null,
temporary_latitude: attendancePoint && attendancePoint.current ? attendancePoint.current.latitude : null,
temporary_longitude: attendancePoint && attendancePoint.current ? attendancePoint.current.longitude : null,
},
})
return normalizeLoginResult(data)
}
const selectRole = async (selectionToken, role) => saveAuth(await requestClient.request({
url: '/api/auth/select-role',
method: 'POST',
data: {
selection_token: selectionToken,
role,
},
}))
const switchRole = async role => saveAuth(await requestClient.request({
url: '/api/auth/switch-role',
method: 'POST',
data: {
role,
},
}))
const logout = () => requestClient.clearAuth()
const listAttendancePoints = async (filters = {}) => {
const data = await requestClient.request({
url: '/api/attendance-points',
params: {
keyword: filters.keyword,
page: filters.page || 1,
page_size: filters.pageSize || 100,
},
})
return toCamelPage(data, toCamelAttendancePoint)
}
const listAccessibleAttendancePoints = async () => {
const data = await requestClient.request({
url: '/api/attendance-points/accessible',
})
return (data || []).map(toCamelAttendancePoint)
}
const listPublicAttendancePoints = async () => {
const data = await requestClient.request({
url: '/api/attendance-points/public',
})
return (data || []).map(toCamelAttendancePoint)
}
const saveAttendancePoint = async payload => toCamelAttendancePoint(await requestClient.request({
url: '/api/attendance-points',
method: 'POST',
data: toSnakeAttendancePoint(payload),
}))
const deleteAttendancePoint = name => requestClient.request({
url: `/api/attendance-points/${encodeURIComponent(name)}`,
method: 'DELETE',
})
const listPeople = async (filters = {}) => {
const data = await requestClient.request({
url: '/api/people',
params: {
keyword: filters.keyword,
page: filters.page,
page_size: filters.pageSize,
},
})
return toCamelPage(data, toCamelPerson)
}
const savePerson = async payload => toCamelPerson(await requestClient.request({
url: '/api/people',
method: 'POST',
data: toSnakePerson(payload),
}))
const deletePerson = (phone, role = '') => requestClient.request({
url: `/api/people/${encodeURIComponent(phone)}`,
method: 'DELETE',
params: {
role,
},
})
const importPeople = filePath => requestClient.upload({
url: '/api/people/import',
filePath,
})
const exportPeople = () => requestClient.download({
url: '/api/people/export',
})
const listProducts = async (filters = {}) => {
const data = await requestClient.request({
url: '/api/products',
params: {
keyword: filters.keyword,
attendance_point_name: filters.attendancePointName,
device_no: filters.deviceNo,
product_name: filters.productName,
page: filters.page,
page_size: filters.pageSize,
},
})
return toCamelPage(data, toCamelProduct)
}
const listMoldNames = async (filters = {}) => {
const data = await requestClient.request({
url: '/api/products/molds',
params: {
keyword: filters.keyword,
limit: filters.limit || 200,
},
})
return (data || []).map(toCamelMoldOption)
}
const saveProduct = async payload => toCamelProduct(await requestClient.request({
url: '/api/products',
method: 'POST',
data: toSnakeProduct(payload),
}))
const deleteProduct = product => requestClient.request({
url: '/api/products',
method: 'DELETE',
params: {
attendance_point_name: product.attendancePointName,
project_no: product.projectNo,
product_name: product.productName,
device_no: product.deviceNo,
process_name: product.rawProcess || product.process,
},
})
const importProducts = filePath => requestClient.upload({
url: '/api/products/import',
filePath,
})
const exportProducts = () => requestClient.download({
url: '/api/products/export',
params: { t: Date.now() },
})
const listEquipment = async (filters = {}) => {
const data = await requestClient.request({
url: '/api/equipment',
params: {
keyword: filters.keyword,
attendance_point_name: filters.attendancePointName,
device_type: filters.deviceType,
page: filters.page,
page_size: filters.pageSize,
},
})
return toCamelPage(data, toCamelEquipment)
}
const saveEquipment = async payload => toCamelEquipment(await requestClient.request({
url: '/api/equipment',
method: 'POST',
data: toSnakeEquipment(payload),
}))
const deleteEquipment = equipment => {
const deviceNo = typeof equipment === 'object' ? equipment.deviceNo : equipment
const attendancePointName = typeof equipment === 'object' ? equipment.attendancePointName : ''
return requestClient.request({
url: `/api/equipment/${encodeURIComponent(normalizeDeviceNo(deviceNo))}`,
method: 'DELETE',
params: {
attendance_point_name: attendancePointName,
},
})
}
const listNotices = async (filters = {}) => {
const data = await requestClient.request({
url: '/api/notices',
params: {
include_inactive: filters.includeInactive ? 1 : undefined,
page: filters.page,
page_size: filters.pageSize,
},
})
return toCamelPage(data, toCamelNotice)
}
const saveNotice = async payload => {
const noticeId = payload.id
const data = await requestClient.request({
url: noticeId ? `/api/notices/${noticeId}` : '/api/notices',
method: noticeId ? 'PUT' : 'POST',
data: toSnakeNotice(payload),
})
return toCamelNotice(data)
}
const deleteNotice = noticeId => requestClient.request({
url: `/api/notices/${noticeId}`,
method: 'DELETE',
})
const locationPayload = location => ({
latitude: location && location.latitude !== undefined ? location.latitude : undefined,
longitude: location && location.longitude !== undefined ? location.longitude : undefined,
})
const getClockState = async (moldName, processName = '', attendancePointName = '', location = null) => {
const row = await requestClient.request({
url: '/api/clock/state',
params: {
mold_name: String(moldName || '').trim(),
process_name: String(processName || '').trim(),
attendance_point_name: String(attendancePointName || '').trim(),
...locationPayload(location),
},
})
return {
action: row.action,
actionText: row.action_text,
attendancePointName: row.attendance_point_name || '',
deviceNo: row.device_no,
moldName: row.mold_name || row.device_no,
processName: row.process_name || '',
stampingMethod: row.stamping_method || '',
isCleaning: !!row.is_cleaning || isCleaningRow(row),
isMisc: !!row.is_misc || isMiscRow(row),
displayName: row.display_name || moldDisplayName(row.mold_name || row.device_no, row.process_name, row.stamping_method),
currentMoldName: row.current_mold_name || '',
currentProcessName: row.current_process_name || '',
session: row.session_id ? { id: row.session_id } : null,
startAtText: row.start_at ? util.formatTime(row.start_at) : '',
endAtText: row.end_at ? util.formatTime(row.end_at) : '',
devices: row.devices || [],
autoSubmitHours: row.auto_submit_hours || 15,
}
}
const startWork = async (moldName, processName = '', attendancePointName = '', location = null) => toCamelSession(await requestClient.request({
url: '/api/clock/start',
method: 'POST',
data: {
mold_name: String(moldName || '').trim(),
process_name: String(processName || '').trim(),
attendance_point_name: String(attendancePointName || '').trim(),
...locationPayload(location),
},
}))
const switchDevice = async (moldName, processName = '', attendancePointName = '', location = null) => toCamelSession(await requestClient.request({
url: '/api/clock/switch-device',
method: 'POST',
data: {
mold_name: String(moldName || '').trim(),
process_name: String(processName || '').trim(),
attendance_point_name: String(attendancePointName || '').trim(),
...locationPayload(location),
},
}))
const finishWork = async (moldName, processName = '', attendancePointName = '', location = null) => toCamelSession(await requestClient.request({
url: '/api/clock/finish',
method: 'POST',
data: {
mold_name: String(moldName || '').trim(),
process_name: String(processName || '').trim(),
attendance_point_name: String(attendancePointName || '').trim(),
...locationPayload(location),
},
}))
const continueWork = async sessionId => toCamelSession(await requestClient.request({
url: '/api/clock/continue-work',
method: 'POST',
data: { session_id: Number(sessionId) },
}))
const createMoldLockFeedback = async lockId => toCamelMoldLockFeedback(await requestClient.request({
url: '/api/monitor/feedbacks',
method: 'POST',
data: { lock_id: Number(lockId) },
}))
const listMonitorLocks = async () => (await requestClient.request({
url: '/api/monitor/locks',
})).map(toCamelMonitorLock)
const listMoldLockFeedbacks = async (filters = {}) => toCamelPage(await requestClient.request({
url: '/api/monitor/feedbacks',
params: {
box: filters.box || 'unread',
page: filters.page || 1,
page_size: filters.pageSize || 10,
},
}), toCamelMoldLockFeedback)
const readMoldLockFeedback = async feedbackId => toCamelMoldLockFeedback(await requestClient.request({
url: `/api/monitor/feedbacks/${feedbackId}/read`,
method: 'POST',
}))
const releaseMoldLock = async lockId => requestClient.request({
url: `/api/monitor/locks/${lockId}/release`,
method: 'POST',
})
const releaseMoldLockByFeedback = async feedbackId => toCamelMoldLockFeedback(await requestClient.request({
url: `/api/monitor/feedbacks/${feedbackId}/release`,
method: 'POST',
}))
const buildReportDraft = async sessionId => toCamelDraft(await requestClient.request({
url: '/api/reports/draft',
params: { session_id: sessionId },
}))
const submitReport = async draft => toCamelReport(await requestClient.request({
url: '/api/reports',
method: 'POST',
data: toSubmitPayload(draft),
}))
const submitCleaningReport = async (items, location = null) => toCamelReport(await requestClient.request({
url: '/api/reports/cleaning',
method: 'POST',
data: {
...locationPayload(location),
items: (items || []).map(item => ({
attendance_point_name: item.attendancePointName || '',
device_no: normalizeDeviceNo(item.deviceNo),
device_nos: item.deviceNos && item.deviceNos.length ? item.deviceNos.map(normalizeDeviceNo).filter(Boolean) : [],
product_name: item.moldName || item.productName || item.name || '',
process_name: item.processName || '',
stamping_method: item.stampingMethod || '',
quantity: Number(item.quantity || 0),
})),
},
}))
const getReport = async reportId => toCamelReport(await requestClient.request({
url: `/api/reports/${reportId}`,
}))
const listReports = async (filters = {}) => {
let url = '/api/reports/mine'
if (filters.status === 'pending' && !filters.employeePhone) {
url = '/api/reviews/pending'
}
if (filters.reviewerPhone && url !== '/api/reviews/pending') {
url = '/api/reviews/mine'
}
const params = {
start_date: filters.startDate,
end_date: filters.endDate,
page: filters.page,
page_size: filters.pageSize,
include_voided: filters.includeVoided ? 1 : undefined,
}
if (url === '/api/reports/mine' && filters.status) {
params.status = filters.status
}
if (url === '/api/reviews/mine' && filters.status) {
params.status = filters.status
}
const data = await requestClient.request({ url, params })
return toCamelPage(data, toCamelReport)
}
const approveReport = async (reportId, user, patch = {}) => toCamelReport(await requestClient.request({
url: `/api/reviews/${reportId}/approve`,
method: 'POST',
data: {
start_at: patch.startAt || null,
end_at: patch.endAt || null,
device_corrections: (patch.deviceCorrections || []).map(item => ({
id: item.id,
scanned_at: item.scannedAt || null,
})),
item_corrections: patch.itemCorrections || [],
review_remark: Object.prototype.hasOwnProperty.call(patch, 'reviewRemark') ? (patch.reviewRemark || null) : undefined,
remark: patch.remark || null,
},
}))
const voidReport = async reportId => toCamelReport(await requestClient.request({
url: `/api/reviews/${reportId}/void`,
method: 'POST',
}))
const unvoidReport = async reportId => toCamelReport(await requestClient.request({
url: `/api/reviews/${reportId}/unvoid`,
method: 'POST',
}))
const toCamelWorkSchedule = row => ({
id: row.id,
dayStart: row.day_start,
dayEnd: row.day_end,
lunchStart: row.lunch_start,
lunchEnd: row.lunch_end,
dinnerStart: row.dinner_start,
dinnerEnd: row.dinner_end,
overtimeStart: row.overtime_start,
overtimeEnd: row.overtime_end,
nightStart: row.night_start,
nightEnd: row.night_end,
attendanceLatitude: row.attendance_latitude === null || row.attendance_latitude === undefined ? '' : row.attendance_latitude,
attendanceLongitude: row.attendance_longitude === null || row.attendance_longitude === undefined ? '' : row.attendance_longitude,
attendanceRadiusMeters: row.attendance_radius_meters === null || row.attendance_radius_meters === undefined ? '' : row.attendance_radius_meters,
autoSubmitHours: row.auto_submit_hours === null || row.auto_submit_hours === undefined ? 15 : row.auto_submit_hours,
attendancePoints: (row.attendance_points || []).map(toCamelAttendancePoint),
updatedBy: row.updated_by || '',
updatedAt: row.updated_at || '',
updatedAtText: row.updated_at ? util.formatTime(row.updated_at) : '',
})
const toSnakeWorkSchedule = row => ({
day_start: row.dayStart,
day_end: row.dayEnd,
lunch_start: row.lunchStart,
lunch_end: row.lunchEnd,
dinner_start: row.dinnerStart,
dinner_end: row.dinnerEnd,
overtime_start: row.overtimeStart,
overtime_end: row.overtimeEnd,
night_start: row.nightStart,
night_end: row.nightEnd,
attendance_latitude: row.attendanceLatitude === '' || row.attendanceLatitude === undefined ? null : Number(row.attendanceLatitude),
attendance_longitude: row.attendanceLongitude === '' || row.attendanceLongitude === undefined ? null : Number(row.attendanceLongitude),
attendance_radius_meters: row.attendanceRadiusMeters === '' || row.attendanceRadiusMeters === undefined ? null : Number(row.attendanceRadiusMeters),
auto_submit_hours: row.autoSubmitHours === '' || row.autoSubmitHours === undefined ? 15 : Number(row.autoSubmitHours),
})
const getWorkSchedule = async () => toCamelWorkSchedule(await requestClient.request({
url: '/api/work-schedule',
}))
const saveWorkSchedule = async payload => toCamelWorkSchedule(await requestClient.request({
url: '/api/work-schedule',
method: 'PUT',
data: toSnakeWorkSchedule(payload),
}))
const saveWorkScheduleSettings = async payload => toCamelWorkSchedule(await requestClient.request({
url: '/api/work-schedule/settings',
method: 'PUT',
data: {
...toSnakeWorkSchedule(payload),
attendance_points: (payload.attendancePoints || []).map(toSnakeAttendancePoint),
},
}))
const listDashboard = async (filters = {}) => {
const data = await requestClient.request({
url: '/api/dashboard/reports',
params: {
start_date: filters.startDate,
end_date: filters.endDate,
page: filters.page,
page_size: filters.pageSize,
include_voided: filters.includeVoided ? 1 : undefined,
},
})
return toCamelPage(data, row => {
const isMisc = !!row.is_misc || isMiscRow(row)
const noOutputMetric = isMisc || isCleaningRow(row)
const beatComparison = noOutputMetric ? classifyBeat(0, 0) : classifyBeat(row.actual_beat, row.standard_beat)
const workloadComparison = noOutputMetric ? classifyWorkload(0, 0) : classifyWorkload(row.total_good_qty, row.expected_workload)
return {
id: row.id,
attendancePointName: row.attendance_point_name || '',
reportDate: row.report_date,
employeePhone: row.employee_phone,
employeeName: row.employee_name,
projectNo: row.project_no,
productName: row.product_name,
processName: row.process_name,
processDisplayName: moldDisplayName(row.product_name, row.process_name, row.stamping_method),
rawMaterialBatchNo: row.raw_material_batch_no || '',
stampingMethod: row.stamping_method || '',
isCleaning: isCleaningRow(row),
isContinuousDie: isContinuousDieRow(row),
changeoverCount: row.changeover_count || 0,
isMisc,
isSystemAutoSubmitted: !!row.is_system_auto_submitted,
isVoided: !!row.is_voided,
isMultiPerson: isMultiPersonRow(row),
isMultiPersonAssistant: !!row.is_multi_person_assistant,
workerType: row.worker_type || '正式工',
reportCount: row.report_count,
effectiveMinutes: row.effective_minutes,
shiftDistributionText: row.shift_distribution_text || '',
sourceReportDate: row.source_report_date || '',
sourceReportDates: row.source_report_dates || [],
sourceReportDatesText: (row.source_report_dates || []).join('、'),
shiftDayMinutes: row.shift_day_minutes || 0,
shiftOvertimeMinutes: row.shift_overtime_minutes || 0,
shiftNightMinutes: row.shift_night_minutes || 0,
shiftOtherMinutes: row.shift_other_minutes || 0,
totalGood: row.total_good_qty,
totalDefect: row.total_defect_qty,
totalOutput: row.total_output_qty,
actualBeat: row.actual_beat,
standardBeat: row.standard_beat,
processUnitPriceYuan: row.process_unit_price_yuan || 0,
referenceWage: round2(row.reference_wage || 0),
expectedWorkload: row.expected_workload,
paceRate: row.pace_rate,
workloadRate: row.workload_rate,
paceText: row.pace_text,
workloadText: row.workload_text,
beatCompareClass: beatComparison.className,
beatReason: beatComparison.reason,
workloadCompareClass: workloadComparison.className,
workloadReason: workloadComparison.reason,
reviewRemark: row.review_remark || '',
correctionPairs: (row.correction_pairs || []).map(pair => ({
key: pair.key,
label: pair.label,
oldValue: correctionDisplayValue(pair.old_value),
newValue: correctionDisplayValue(pair.new_value),
})),
}
})
}
const listUsageStats = async (filters = {}) => {
const data = await requestClient.request({
url: '/api/usage-stats/summary',
params: {
...usageStatsParams(filters),
page: filters.page,
page_size: filters.pageSize,
},
})
return toCamelPage(data, toCamelUsageStatsRow)
}
const getUsageStatsDetail = async (filters = {}) => toCamelUsageStatsDetail(await requestClient.request({
url: '/api/usage-stats/detail',
params: usageStatsParams(filters),
}))
const listReconciliationYears = async () => {
const data = await requestClient.request({
url: '/api/reconciliation/years',
})
return {
years: data.years || [],
currentYear: data.current_year,
}
}
const listReconciliationLedger = async year => {
const data = await requestClient.request({
url: '/api/reconciliation',
params: { year },
})
return {
year: data.year,
currentYear: data.current_year,
currentMonth: data.current_month,
rows: (data.rows || []).map(row => ({
attendancePointName: row.attendance_point_name || '',
productName: row.product_name,
uniqueKey: `${row.attendance_point_name || ''}||${row.product_name || ''}`,
months: (row.months || []).map(month => ({
month: month.month,
reportedGoodQty: month.reported_good_qty || 0,
reconciledGoodQty: month.reconciled_good_qty || 0,
returnQty: month.return_qty || 0,
})),
})),
}
}
const saveReconciliationEntry = async payload => {
const data = await requestClient.request({
url: '/api/reconciliation/entry',
method: 'PUT',
data: {
year: Number(payload.year),
month: Number(payload.month),
attendance_point_name: payload.attendancePointName || '',
product_name: payload.productName,
reconciled_good_qty: payload.reconciledGoodQty === undefined ? undefined : Number(payload.reconciledGoodQty || 0),
return_qty: payload.returnQty === undefined ? undefined : Number(payload.returnQty || 0),
},
})
return {
month: data.month,
reconciledGoodQty: data.reconciled_good_qty || 0,
returnQty: data.return_qty || 0,
}
}
const exportDashboard = filters => requestClient.download({
url: '/api/dashboard/reports/export',
params: {
start_date: filters && filters.startDate,
end_date: filters && filters.endDate,
include_voided: filters && filters.includeVoided ? 1 : undefined,
},
})
const exportUsageStats = (filters = {}) => requestClient.download({
url: '/api/usage-stats/export',
params: usageStatsParams(filters),
})
const generateMoldQr = async mold => {
const original = typeof mold === 'object' ? String(mold.moldName || mold.name || '').trim() : String(mold || '').trim()
const processName = typeof mold === 'object' ? String(mold.processName || '').trim() : ''
const attendancePointName = typeof mold === 'object' ? String(mold.attendancePointName || '').trim() : ''
const data = await requestClient.request({
url: '/api/devices/molds/qrcode',
method: 'POST',
data: {
attendance_point_name: attendancePointName,
product_name: original,
process_name: processName,
},
})
return {
deviceNo: data.device_no,
attendancePointName: data.attendance_point_name || '',
moldName: data.mold_name || data.device_no,
processName: data.process_name || '',
stampingMethod: data.stamping_method || '',
isCleaning: !!data.is_cleaning || isCleaningRow(data),
isMisc: !!data.is_misc || isMiscRow(data),
displayName: data.display_name || moldDisplayName(data.mold_name || data.device_no, data.process_name, data.stamping_method),
qrPath: `${data.page}?scene=${encodeURIComponent(data.scene)}`,
qrUrl: data.qr_url || '',
scene: data.scene,
}
}
const generateMoldQrBatch = async molds => {
const data = await requestClient.request({
url: '/api/devices/molds/qrcode/batch',
method: 'POST',
timeout: 120000,
data: {
items: (molds || []).map(mold => ({
attendance_point_name: String(mold.attendancePointName || '').trim(),
product_name: String(mold.moldName || mold.name || '').trim(),
process_name: String(mold.processName || '').trim(),
})),
},
})
return {
count: data.count || 0,
fileName: data.file_name || '',
zipUrl: data.zip_url || '',
}
}
const toCamelQrcodeBatchTask = row => ({
id: row.id,
fileName: row.file_name || '',
status: row.status || '',
statusName: row.status_name || '',
itemCount: Number(row.item_count || 0),
completedCount: Number(row.completed_count || 0),
failedCount: Number(row.failed_count || 0),
progressPercent: Number(row.progress_percent || 0),
zipUrl: row.zip_url || '',
errorMessage: row.error_message || '',
startedAt: row.started_at || '',
startedAtText: row.started_at ? util.formatTime(row.started_at) : '',
finishedAt: row.finished_at || '',
finishedAtText: row.finished_at ? util.formatTime(row.finished_at) : '',
createdAt: row.created_at || '',
createdAtText: row.created_at ? util.formatTime(row.created_at) : '',
updatedAt: row.updated_at || '',
isPending: row.status === 'pending',
isRunning: row.status === 'running',
isPaused: row.status === 'paused',
isCompleted: row.status === 'completed',
isFailed: row.status === 'failed',
})
const createMoldQrBatchTask = async molds => {
const data = await requestClient.request({
url: '/api/devices/molds/qrcode/batch/tasks',
method: 'POST',
timeout: 30000,
data: {
items: (molds || []).map(mold => ({
attendance_point_name: String(mold.attendancePointName || '').trim(),
product_name: String(mold.moldName || mold.name || '').trim(),
process_name: String(mold.processName || '').trim(),
})),
},
})
return toCamelQrcodeBatchTask(data)
}
const listMoldQrBatchTasks = async (filters = {}) => {
const page = await requestClient.request({
url: '/api/devices/molds/qrcode/batch/tasks',
params: {
page: filters.page || 1,
page_size: filters.pageSize || 5,
},
})
return toCamelPage(page, toCamelQrcodeBatchTask)
}
const stopMoldQrBatchTask = async taskId => toCamelQrcodeBatchTask(await requestClient.request({
url: `/api/devices/molds/qrcode/batch/tasks/${taskId}/stop`,
method: 'POST',
}))
const resumeMoldQrBatchTask = async taskId => toCamelQrcodeBatchTask(await requestClient.request({
url: `/api/devices/molds/qrcode/batch/tasks/${taskId}/resume`,
method: 'POST',
}))
const deleteMoldQrBatchTask = async taskId => requestClient.request({
url: `/api/devices/molds/qrcode/batch/tasks/${taskId}`,
method: 'DELETE',
})
const downloadFileByUrl = (url, filePath, timeout = 120000) => requestClient.downloadUrl({
url,
filePath,
timeout,
})
const generateDeviceQr = generateMoldQr
const resolveMoldScene = async scene => {
const data = await requestClient.request({
url: '/api/devices/resolve-scene',
params: { scene },
})
return {
attendancePointName: data.attendance_point_name || '',
moldName: data.mold_name || data.device_no || '',
processName: data.process_name || '',
stampingMethod: data.stamping_method || '',
isCleaning: !!data.is_cleaning || isCleaningRow(data),
isMisc: !!data.is_misc || isMiscRow(data),
displayName: data.display_name || moldDisplayName(data.mold_name || data.device_no, data.process_name, data.stamping_method),
}
}
const getHomeSummary = async user => {
if (!user) {
return {}
}
const summary = {
productCount: 0,
peopleCount: 0,
pendingCount: 0,
myToday: 0,
approvedToday: 0,
}
const today = util.today()
const tasks = [
listProducts({ page: 1, pageSize: 1 }).then(result => { summary.productCount = result.total }),
listReports({ startDate: today, endDate: today, page: 1, pageSize: 1 }).then(result => { summary.myToday = result.total }).catch(() => {}),
]
if (['admin', 'manager'].includes(user.role)) {
tasks.push(listPeople({ page: 1, pageSize: 1 }).then(result => { summary.peopleCount = result.total }).catch(() => {}))
}
if (user.role === 'admin') {
tasks.push(listReports({
status: 'pending',
page: 1,
pageSize: 1,
}).then(result => { summary.pendingCount = result.total }).catch(() => {}))
tasks.push(listReports({
reviewerPhone: user.phone,
status: 'approved',
startDate: today,
endDate: today,
page: 1,
pageSize: 1,
}).then(result => { summary.approvedToday = result.total }).catch(() => {}))
}
if (user.role === 'manager') {
tasks.push(listDashboard({ startDate: today, endDate: today, page: 1, pageSize: 1 }).then(result => { summary.approvedToday = result.total }).catch(() => {}))
}
await Promise.all(tasks)
return summary
}
module.exports = {
approveReport,
buildReportDraft,
calculateStandardWorkload,
cancelErpLogin,
classifyBeat,
classifyWorkload,
confirmErpLogin,
continueWork,
createTemporaryWorker,
createMoldLockFeedback,
createMoldQrBatchTask,
deleteMoldQrBatchTask,
deleteAttendancePoint,
deleteEquipment,
deletePerson,
deleteProduct,
deleteNotice,
downloadFileByUrl,
exportPeople,
exportProducts,
exportDashboard,
exportUsageStats,
formatDurationAllUnits,
formatDurationMinutes,
finishWork,
generateDeviceQr,
generateMoldQr,
generateMoldQrBatch,
getClockState,
getCurrentUser,
getErpLoginPreview,
getHomeSummary,
getReport,
getUsageStatsDetail,
getWorkSchedule,
importPeople,
importProducts,
listAccessibleAttendancePoints,
listAttendancePoints,
listDashboard,
listEquipment,
listMoldNames,
listMonitorLocks,
listMoldLockFeedbacks,
listMoldQrBatchTasks,
listNotices,
listPeople,
listProducts,
listPublicAttendancePoints,
listReconciliationLedger,
listReconciliationYears,
listReports,
listUsageStats,
loginWithPhoneCode,
logout,
isContinuousDieRow,
isContinuousDieStampingMethod,
moldDisplayName,
normalizeDeviceNo,
normalizeProcessValue,
processDisplayName,
refreshCurrentUser,
readMoldLockFeedback,
releaseMoldLock,
releaseMoldLockByFeedback,
resolveMoldScene,
roleNames,
saveAttendancePoint,
saveNotice,
saveEquipment,
savePerson,
saveProduct,
saveReconciliationEntry,
saveWorkSchedule,
saveWorkScheduleSettings,
selectRole,
startWork,
resumeMoldQrBatchTask,
stopMoldQrBatchTask,
submitCleaningReport,
submitReport,
switchRole,
switchDevice,
unvoidReport,
voidReport,
}