1940 lines
67 KiB
JavaScript
1940 lines
67 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 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,
|
|
}
|
|
}
|
|
|
|
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,
|
|
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 || '',
|
|
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,
|
|
}
|