Merge pull request 'Update form summary, login' (#21) from new-form-summary into main
Reviewed-on: #21
This commit is contained in:
commit
19d5aec7ab
|
|
@ -6,7 +6,7 @@ export default class AuthController {
|
||||||
// Đăng ký
|
// Đăng ký
|
||||||
async register({ request, response }: HttpContext) {
|
async register({ request, response }: HttpContext) {
|
||||||
try {
|
try {
|
||||||
const data = request.only(['email', 'password', 'user_name'])
|
const data = request.only(['email', 'password', 'user_name', 'first_name', 'last_name'])
|
||||||
|
|
||||||
const user = await User.query().where('user_name', data.user_name).first()
|
const user = await User.query().where('user_name', data.user_name).first()
|
||||||
|
|
||||||
|
|
@ -23,7 +23,12 @@ export default class AuthController {
|
||||||
|
|
||||||
// Đăng nhập
|
// Đăng nhập
|
||||||
async login({ request, auth, response }: HttpContext) {
|
async login({ request, auth, response }: HttpContext) {
|
||||||
const { user_name: userName, password } = request.only(['user_name', 'password'])
|
const { user_name: userName, password } = request.only([
|
||||||
|
'user_name',
|
||||||
|
'password',
|
||||||
|
'first_name',
|
||||||
|
'last_name',
|
||||||
|
])
|
||||||
const user = await User.query().where('user_name', userName).first()
|
const user = await User.query().where('user_name', userName).first()
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
|
|
@ -47,10 +52,18 @@ export default class AuthController {
|
||||||
email: remoteUser.userEmail,
|
email: remoteUser.userEmail,
|
||||||
userName: userName,
|
userName: userName,
|
||||||
password: password,
|
password: password,
|
||||||
|
firstName: remoteUser?.firstName || null,
|
||||||
|
lastName: remoteUser?.lastName || null,
|
||||||
})
|
})
|
||||||
return response.json({
|
return response.json({
|
||||||
message: 'Login successful',
|
message: 'Login successful',
|
||||||
user: { id: newUser.id, email: newUser.email, userName: newUser.userName },
|
user: {
|
||||||
|
id: newUser.id,
|
||||||
|
email: newUser.email,
|
||||||
|
userName: newUser.userName,
|
||||||
|
firstName: newUser.firstName,
|
||||||
|
lastName: newUser.lastName,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -62,7 +75,13 @@ export default class AuthController {
|
||||||
|
|
||||||
return response.json({
|
return response.json({
|
||||||
message: 'Login successful',
|
message: 'Login successful',
|
||||||
user: { id: user.id, email: user.email, userName: user.userName },
|
user: {
|
||||||
|
id: user.id,
|
||||||
|
email: user.email,
|
||||||
|
userName: user.userName,
|
||||||
|
firstName: user.firstName,
|
||||||
|
lastName: user.lastName,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
} catch {
|
} catch {
|
||||||
return response.status(401).json({ message: 'Invalid credentials' })
|
return response.status(401).json({ message: 'Invalid credentials' })
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,12 @@ export default class User extends BaseModel {
|
||||||
@column()
|
@column()
|
||||||
declare password: string
|
declare password: string
|
||||||
|
|
||||||
|
@column()
|
||||||
|
declare firstName: string | null
|
||||||
|
|
||||||
|
@column()
|
||||||
|
declare lastName: string | null
|
||||||
|
|
||||||
@column.dateTime({ autoCreate: true })
|
@column.dateTime({ autoCreate: true })
|
||||||
declare createdAt: DateTime
|
declare createdAt: DateTime
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -145,6 +145,11 @@ export default class LineConnection {
|
||||||
private debounceSendSummaryReport: NodeJS.Timeout | null = null
|
private debounceSendSummaryReport: NodeJS.Timeout | null = null
|
||||||
private isPingToServer: boolean
|
private isPingToServer: boolean
|
||||||
private outputPingToServer: string
|
private outputPingToServer: string
|
||||||
|
private outputTestLog: string
|
||||||
|
private userTest: {
|
||||||
|
dpelp: { name: string; time: number }
|
||||||
|
physical: { name: string; time: number }
|
||||||
|
}
|
||||||
|
|
||||||
constructor(config: LineConfig, socketIO: any, handleClearLine: () => void) {
|
constructor(config: LineConfig, socketIO: any, handleClearLine: () => void) {
|
||||||
this.config = config
|
this.config = config
|
||||||
|
|
@ -180,6 +185,8 @@ export default class LineConnection {
|
||||||
this.outputTestingPortPoE = ''
|
this.outputTestingPortPoE = ''
|
||||||
this.isPingToServer = false
|
this.isPingToServer = false
|
||||||
this.outputPingToServer = ''
|
this.outputPingToServer = ''
|
||||||
|
this.outputTestLog = ''
|
||||||
|
this.userTest = { dpelp: { name: '', time: 0 }, physical: { name: '', time: 0 } }
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* Connect to line with socket
|
* Connect to line with socket
|
||||||
|
|
@ -225,6 +232,7 @@ export default class LineConnection {
|
||||||
this.waitingScenario = true
|
this.waitingScenario = true
|
||||||
this.outputBuffer += message
|
this.outputBuffer += message
|
||||||
this.outputScenario += message
|
this.outputScenario += message
|
||||||
|
this.outputTestLog += cleanData(data.toString())
|
||||||
if (!this.config.inventory)
|
if (!this.config.inventory)
|
||||||
this.outputInventory = this.outputInventory.slice(-3000) + message
|
this.outputInventory = this.outputInventory.slice(-3000) + message
|
||||||
}
|
}
|
||||||
|
|
@ -439,6 +447,10 @@ export default class LineConnection {
|
||||||
})
|
})
|
||||||
if (script?.send_result || script?.sendResult) {
|
if (script?.send_result || script?.sendResult) {
|
||||||
this.dataDPELP = ''
|
this.dataDPELP = ''
|
||||||
|
this.userTest = {
|
||||||
|
...this.userTest,
|
||||||
|
dpelp: { name: userName || '', time: Date.now() },
|
||||||
|
}
|
||||||
// this.config.inventory = ''
|
// this.config.inventory = ''
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -542,10 +554,11 @@ export default class LineConnection {
|
||||||
if (
|
if (
|
||||||
['show inventory', 'sh inventory', 'show inv', 'sh inv'].includes(item.command)
|
['show inventory', 'sh inventory', 'show inv', 'sh inv'].includes(item.command)
|
||||||
) {
|
) {
|
||||||
const dataInventory = JSON.parse(item.textfsm)[0]
|
const listInventory = JSON.parse(item.textfsm)
|
||||||
|
const dataInventory = listInventory[0]
|
||||||
this.config.inventory = this.config.inventory
|
this.config.inventory = this.config.inventory
|
||||||
? { ...this.config.inventory, ...dataInventory }
|
? { ...this.config.inventory, ...dataInventory, listInventory }
|
||||||
: dataInventory
|
: { ...dataInventory, listInventory }
|
||||||
pid = dataInventory?.pid || ''
|
pid = dataInventory?.pid || ''
|
||||||
this.addHistory(this.config.stationId, this.config.id, {
|
this.addHistory(this.config.stationId, this.config.id, {
|
||||||
id: this.config.id,
|
id: this.config.id,
|
||||||
|
|
@ -804,9 +817,10 @@ export default class LineConnection {
|
||||||
data.forEach((item) => {
|
data.forEach((item) => {
|
||||||
if (item?.textfsm && isValidJson(item?.textfsm)) {
|
if (item?.textfsm && isValidJson(item?.textfsm)) {
|
||||||
if (['show inventory', 'sh inventory', 'show inv', 'sh inv'].includes(item.command)) {
|
if (['show inventory', 'sh inventory', 'show inv', 'sh inv'].includes(item.command)) {
|
||||||
const dataInventory = JSON.parse(item.textfsm)[0]
|
const listInventory = JSON.parse(item.textfsm)
|
||||||
|
const dataInventory = listInventory[0]
|
||||||
this.config.inventory = this.config.inventory
|
this.config.inventory = this.config.inventory
|
||||||
? { ...this.config.inventory, ...dataInventory }
|
? { ...this.config.inventory, ...dataInventory, listInventory }
|
||||||
: dataInventory
|
: dataInventory
|
||||||
}
|
}
|
||||||
item.textfsm = JSON.parse(item.textfsm)
|
item.textfsm = JSON.parse(item.textfsm)
|
||||||
|
|
@ -1161,7 +1175,7 @@ Ports Missing/Down: ${missing.length}\n\n`
|
||||||
/**
|
/**
|
||||||
* Starting physical test (PoE ports testing)
|
* Starting physical test (PoE ports testing)
|
||||||
*/
|
*/
|
||||||
async runPhysicalTest() {
|
async runPhysicalTest(userName?: string) {
|
||||||
if (this.config.runningPhysical) {
|
if (this.config.runningPhysical) {
|
||||||
console.log('Running physical test')
|
console.log('Running physical test')
|
||||||
return
|
return
|
||||||
|
|
@ -1173,6 +1187,7 @@ Ports Missing/Down: ${missing.length}\n\n`
|
||||||
this.config.reasonSkipPhysical = ''
|
this.config.reasonSkipPhysical = ''
|
||||||
this.testingPortPoE = true
|
this.testingPortPoE = true
|
||||||
this.outputTestingPortPoE = ''
|
this.outputTestingPortPoE = ''
|
||||||
|
this.userTest = { ...this.userTest, physical: { name: userName || '', time: Date.now() } }
|
||||||
const listPorts = await this.getPorts()
|
const listPorts = await this.getPorts()
|
||||||
this.socketIO.emit('running_scenario', {
|
this.socketIO.emit('running_scenario', {
|
||||||
stationId: this.config.stationId,
|
stationId: this.config.stationId,
|
||||||
|
|
@ -1895,6 +1910,522 @@ Ports Missing/Down: ${missing.length}\n\n`
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send summary report using the new "Equipment Receiving & Testing Report" template.
|
||||||
|
* Email-safe HTML: table-based layout, inline styles, no external CSS or web fonts.
|
||||||
|
*/
|
||||||
|
sendReportSummaryV2 = async (snapshot?: {
|
||||||
|
snapConfig: LineConfig
|
||||||
|
snapPhysical: PhysicalPortTest
|
||||||
|
reason: string
|
||||||
|
outputTestLog: string
|
||||||
|
userTest: {
|
||||||
|
dpelp: { name: string; time: number }
|
||||||
|
physical: { name: string; time: number }
|
||||||
|
}
|
||||||
|
}) => {
|
||||||
|
if (this.debounceSendSummaryReport) clearTimeout(this.debounceSendSummaryReport)
|
||||||
|
const timeZone = process.env.TIME_ZONE || 'Australia/Sydney'
|
||||||
|
const physicalTest = snapshot?.snapPhysical ? snapshot?.snapPhysical : this.physicalTest
|
||||||
|
const config = snapshot?.snapConfig ? snapshot?.snapConfig : this.config
|
||||||
|
const portPhysical = Array.from(physicalTest.ports.values())
|
||||||
|
const missing = portPhysical.filter((p) => !p.tested)
|
||||||
|
const missingPoE = missing.filter((p) => !p.name.includes('SFP'))
|
||||||
|
const missingSFP = missing.filter((p) => p.name.includes('SFP'))
|
||||||
|
const tested = portPhysical.filter((p) => p.tested)
|
||||||
|
const testedPoE = tested.filter((p) => !p.name.includes('SFP'))
|
||||||
|
const testedSFP = tested.filter((p) => p.name.includes('SFP'))
|
||||||
|
const totalPoE = testedPoE.length + missingPoE.length
|
||||||
|
const totalSFP = testedSFP.length + missingSFP.length
|
||||||
|
|
||||||
|
const showVersion = config?.data?.find(
|
||||||
|
(d) => d.command?.trim()?.includes('show ver') || d.command?.trim()?.includes('sh ver')
|
||||||
|
)
|
||||||
|
const dataShowVersion =
|
||||||
|
showVersion?.textfsm && (showVersion?.textfsm as any)?.[0]
|
||||||
|
? (showVersion?.textfsm as any)?.[0]
|
||||||
|
: config?.inventory
|
||||||
|
|
||||||
|
const showLicense = config?.data?.find(
|
||||||
|
(d) => d.command?.trim()?.includes('show lic') || d.command?.trim()?.includes('sh lic')
|
||||||
|
)
|
||||||
|
const dataShowLic =
|
||||||
|
showLicense?.textfsm && Array.isArray(showLicense?.textfsm)
|
||||||
|
? (showLicense?.textfsm as any[])
|
||||||
|
: null
|
||||||
|
|
||||||
|
const issues: string[] = config?.latestScenario?.detectAI?.issue || []
|
||||||
|
const skipReason = this.config.reasonSkipPhysical || snapshot?.reason || ''
|
||||||
|
const isSkipped = typeof skipReason === 'string' && skipReason.trim().length > 0
|
||||||
|
|
||||||
|
const verdictPass = missing.length === 0 && !isSkipped
|
||||||
|
const verdictLabel = verdictPass ? 'PASSED' : 'NEEDS REVIEW'
|
||||||
|
const verdictMsg = verdictPass
|
||||||
|
? 'All tests passed — Ready for deployment'
|
||||||
|
: 'Issues detected — review required before deployment'
|
||||||
|
const verdictBg = verdictPass ? '#ecfdf5' : '#fef2f2'
|
||||||
|
const verdictBd = verdictPass ? '#a7f3d0' : '#fecaca'
|
||||||
|
const verdictTx = verdictPass ? '#065f46' : '#991b1b'
|
||||||
|
|
||||||
|
const reportId = `RPT-${momentTZ().tz(timeZone).format('YYYY-MMDD')}`
|
||||||
|
const reportDate = momentTZ().tz(timeZone).format('DD MMM YYYY')
|
||||||
|
|
||||||
|
const memText = dataShowVersion?.MEMORY
|
||||||
|
? convertFromKilobytesString(dataShowVersion.MEMORY)
|
||||||
|
: '—'
|
||||||
|
const flashText = dataShowVersion?.USB_FLASH
|
||||||
|
? convertFromKilobytesString(dataShowVersion.USB_FLASH)
|
||||||
|
: '—'
|
||||||
|
|
||||||
|
// ---- Template-fallback values (use file's hardcoded content when no real data) ----
|
||||||
|
const productName = escapeHtml(String(config?.inventory?.name || ''))
|
||||||
|
const productPN = escapeHtml(String(config?.inventory?.pid || ''))
|
||||||
|
const productSN = escapeHtml(String(config?.inventory?.sn || ''))
|
||||||
|
const productVid = escapeHtml(String(config?.inventory?.vid || ''))
|
||||||
|
const iosVersion = escapeHtml(String(dataShowVersion?.VERSION || ''))
|
||||||
|
const macAddress = escapeHtml(String(dataShowVersion?.MAC_ADDRESS || ''))
|
||||||
|
const memDisplay = escapeHtml(memText !== '—' ? memText : '-')
|
||||||
|
const flashDisplay = escapeHtml(flashText !== '—' ? flashText : '-')
|
||||||
|
const configRam = await detectConfigRamByModel(config?.inventory?.pid)
|
||||||
|
|
||||||
|
// AI issue rows (one per real AI issue, fall back to file's hardcoded row when none)
|
||||||
|
const aiIssueRowsHtml =
|
||||||
|
issues.length > 0
|
||||||
|
? issues
|
||||||
|
.slice(0, 1)
|
||||||
|
.map(
|
||||||
|
(issue) =>
|
||||||
|
`<table cellpadding="0" cellspacing="0" border="0" width="100%" style="background:#f5f3ff;border:1px solid #c4b5fd;border-radius:6px;margin-bottom:5px;border-collapse:separate;"><tr><td style="padding:7px 12px;font-size:12px;color:#5f6978;font-weight:500;"><span style="display:inline-block;background:#7c3aed;color:#fff;font-size:9px;font-weight:700;letter-spacing:.5px;padding:2px 6px;border-radius:4px;vertical-align:middle;">★ AI</span><span style="margin-left:8px;vertical-align:middle;">${escapeHtml(issue)}</span></td><td align="right" style="padding:7px 12px;width:90px;"><span style="display:inline-block;padding:1px 7px;border-radius:50px;font-size:11px;font-weight:600;background:#eff6ff;color:#1d4ed8;border:1px solid #bfdbfe;">Investigate</span></td></tr></table>`
|
||||||
|
)
|
||||||
|
.join('')
|
||||||
|
: `<table cellpadding="0" cellspacing="0" border="0" width="100%" style="background:#f5f3ff;border:1px solid #c4b5fd;border-radius:6px;margin-bottom:5px;border-collapse:separate;"><tr><td style="padding:7px 12px;font-size:12px;color:#5f6978;font-weight:500;"><span style="display:inline-block;background:#7c3aed;color:#fff;font-size:9px;font-weight:700;letter-spacing:.5px;padding:2px 6px;border-radius:4px;vertical-align:middle;">★ AI</span><span style="margin-left:8px;vertical-align:middle;">Potential intermittent power instability. PSU #1 POST logs show 3 retries before handshake.</span></td><td align="right" style="padding:7px 12px;width:90px;"><span style="display:inline-block;padding:1px 7px;border-radius:50px;font-size:11px;font-weight:600;background:#eff6ff;color:#1d4ed8;border:1px solid #bfdbfe;">Investigate</span></td></tr></table>`
|
||||||
|
|
||||||
|
// License boxes (real licenses if available, else file's hardcoded boxes)
|
||||||
|
const licenseBoxesHtml =
|
||||||
|
dataShowLic && dataShowLic.length > 0
|
||||||
|
? dataShowLic
|
||||||
|
.filter((l) => l.LICENSE_TYPE && l.FEATURE)
|
||||||
|
.map(
|
||||||
|
(l: any) =>
|
||||||
|
`<div style="background:#f9fafb;border:1px solid #f0f1f3;border-radius:6px;padding:8px 12px;margin-bottom:6px;"><div style="font-weight:700;color:#3b82f6;font-size:13px;">${escapeHtml(String(l.FEATURE || ''))}</div><div style="font-size:10px;color:#9ca3af;">${escapeHtml(String(l.LICENSE_TYPE || ''))}${l.STATUS ? ' · ' + escapeHtml(String(l.STATUS)) : ''}</div></div>`
|
||||||
|
)
|
||||||
|
.join('')
|
||||||
|
: ``
|
||||||
|
|
||||||
|
// Port stat values (real numbers if any port data, else file's defaults)
|
||||||
|
const hasPortData = portPhysical.length > 0
|
||||||
|
const poeText = hasPortData ? `${testedPoE.length}/${totalPoE}` : '0/0'
|
||||||
|
const sfpText = hasPortData ? `${testedSFP.length}/${totalSFP}` : '0/0'
|
||||||
|
const poeColor =
|
||||||
|
!hasPortData || (totalPoE > 0 && testedPoE.length === totalPoE) ? '#10b981' : '#f59e0b'
|
||||||
|
const sfpColor =
|
||||||
|
!hasPortData || (totalSFP > 0 && testedSFP.length === totalSFP) ? '#10b981' : '#f59e0b'
|
||||||
|
|
||||||
|
// Missing-port detail blocks (only when there is something to show)
|
||||||
|
const missingParts: string[] = []
|
||||||
|
if (missingPoE.length) {
|
||||||
|
missingParts.push(
|
||||||
|
`<div style="margin-top:8px;padding:8px 12px;background:#fef2f2;border-left:3px solid #ef4444;border-radius:0 6px 6px 0;font-size:10px;color:#991b1b;"><b>Missing PoE (${missingPoE.length}):</b><br/><span style="font-family:Consolas,monospace;color:#5f6978;">${missingPoE.map((p) => escapeHtml(physicalTest.normalizePortName(p.name))).join(', ')}</span></div>`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (missingSFP.length) {
|
||||||
|
missingParts.push(
|
||||||
|
`<div style="margin-top:6px;padding:8px 12px;background:#fef2f2;border-left:3px solid #ef4444;border-radius:0 6px 6px 0;font-size:10px;color:#991b1b;"><b>Missing SFP (${missingSFP.length}):</b><br/><span style="font-family:Consolas,monospace;color:#5f6978;">${missingSFP.map((p) => escapeHtml(physicalTest.normalizePortName(p.name))).join(', ')}</span></div>`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (isSkipped) {
|
||||||
|
missingParts.push(
|
||||||
|
`<div style="margin-top:6px;padding:8px 12px;background:#fffbeb;border-left:3px solid #f59e0b;border-radius:0 6px 6px 0;font-size:10px;color:#92400e;"><b>User Skipped Physical Test:</b><br/>${escapeHtml(skipReason)}</div>`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
const missingDetailsHtml = missingParts.join('')
|
||||||
|
|
||||||
|
// Verdict checkmark / cross path
|
||||||
|
const verdictPathSvg = verdictPass
|
||||||
|
? '<path d="M6.5 10l2.5 2.5 4.5-4.5" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/>'
|
||||||
|
: '<path d="M7 7l6 6M13 7l-6 6" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/>'
|
||||||
|
|
||||||
|
// Physical Check checklist
|
||||||
|
const checklistItems: Array<[string, string]> = [
|
||||||
|
['ok', 'Packaging intact — no damage to box or foam'],
|
||||||
|
['ok', 'No physical damage — chassis, fans, PSU'],
|
||||||
|
['ok', `S/N matches label — ${productSN} verified`],
|
||||||
|
['ok', 'All 48 GigE + 4 SFP+ ports clean'],
|
||||||
|
['ok', 'Accessories — power cable, rack ears, console cable'],
|
||||||
|
['warn', 'Minor scratch on top chassis (2cm) — cosmetic only'],
|
||||||
|
]
|
||||||
|
const checklistRowsHtml = checklistItems
|
||||||
|
.map(([k, t]) =>
|
||||||
|
k === 'ok'
|
||||||
|
? `<table cellpadding="0" cellspacing="0" border="0" width="100%" style="background:#ecfdf5;border:1px solid #a7f3d0;border-radius:6px;margin-bottom:4px;border-collapse:separate;"><tr><td style="padding:6px 10px;font-size:13px;font-weight:600;color:#065f46;"><span style="display:inline-block;width:18px;height:18px;background:#a7f3d0;color:#065f46;border-radius:50%;text-align:center;line-height:18px;font-size:11px;font-weight:800;vertical-align:middle;">✓</span><span style="margin-left:8px;vertical-align:middle;">${t}</span></td></tr></table>`
|
||||||
|
: `<table cellpadding="0" cellspacing="0" border="0" width="100%" style="background:#fffbeb;border:1px solid #fde68a;border-radius:6px;margin-bottom:4px;border-collapse:separate;"><tr><td style="padding:6px 10px;font-size:13px;font-weight:600;color:#92400e;"><span style="display:inline-block;width:18px;height:18px;background:#fde68a;color:#92400e;border-radius:50%;text-align:center;line-height:18px;font-size:11px;font-weight:800;vertical-align:middle;">!</span><span style="margin-left:8px;vertical-align:middle;">${t}</span></td></tr></table>`
|
||||||
|
)
|
||||||
|
.join('')
|
||||||
|
|
||||||
|
// Physical Check photo placeholder cell (4 of these in the photo grid)
|
||||||
|
const photoCellHtml = (label: string) =>
|
||||||
|
`<table cellpadding="0" cellspacing="0" border="0" width="100%" style="border:1px dashed #e5e7eb;border-radius:6px;background:#f9fafb;border-collapse:separate;"><tr><td align="center" style="padding:18px 0;color:#9ca3af;"><svg viewBox="0 0 40 40" width="22" height="22" fill="none" style="display:inline-block;color:#9ca3af;"><rect x="4" y="8" width="32" height="24" rx="3" stroke="currentColor" stroke-width="1.5"/><circle cx="14" cy="18" r="3" stroke="currentColor" stroke-width="1.5"/><path d="M4 28l8-6 6 4 8-8 10 8" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg><div style="font-size:9px;font-weight:600;margin-top:3px;">${label}</div></td></tr></table>`
|
||||||
|
|
||||||
|
// ---- Body: full template mirroring index.html, table-based + inline styles ----
|
||||||
|
const body = `<!DOCTYPE html>
|
||||||
|
<html lang="vi">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<title>Equipment Report — Mail Summary</title>
|
||||||
|
</head>
|
||||||
|
<body style="margin:0;padding:24px 16px 48px;background:#f3f4f6;color:#1a1d23;font-family:Arial,Helvetica,sans-serif;font-size:13px;line-height:1.5;">
|
||||||
|
|
||||||
|
<!-- HEADER + VERDICT -->
|
||||||
|
<table align="center" cellpadding="0" cellspacing="0" border="0" width="100%" style="max-width:880px;margin:0 auto 12px;">
|
||||||
|
<tr><td>
|
||||||
|
<table cellpadding="0" cellspacing="0" border="0" width="100%" style="background:#fff;border:1px solid #e5e7eb;border-bottom:none;border-radius:10px 10px 0 0;border-collapse:separate;">
|
||||||
|
<tr><td style="padding:14px 20px;">
|
||||||
|
<table cellpadding="0" cellspacing="0" border="0" width="100%">
|
||||||
|
<tr>
|
||||||
|
<td style="vertical-align:middle;">
|
||||||
|
<table cellpadding="0" cellspacing="0" border="0">
|
||||||
|
<tr>
|
||||||
|
<td style="background:#1e293b;border-radius:8px;width:34px;height:34px;text-align:center;vertical-align:middle;color:#cbd5e1;padding:6px;">
|
||||||
|
<svg viewBox="0 0 32 32" width="22" height="22" fill="none" style="display:block;margin:auto;color:#cbd5e1;"><rect x="2" y="6" width="28" height="20" rx="3" stroke="currentColor" stroke-width="2"/><path d="M8 16h16M8 12h10M8 20h6" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/><circle cx="24" cy="20" r="2" fill="currentColor"/></svg>
|
||||||
|
</td>
|
||||||
|
<td style="padding-left:10px;vertical-align:middle;">
|
||||||
|
<strong style="font-size:15px;letter-spacing:1.2px;display:block;">PROLOGY IT</strong>
|
||||||
|
<span style="font-size:11px;color:#5f6978;font-weight:500;">Equipment Receiving & Testing Report</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
<td align="right" style="vertical-align:middle;">
|
||||||
|
<span style="font-size:12px;font-weight:700;color:#3b82f6;font-family:'Courier New',monospace;display:block;">#${escapeHtml(reportId)}</span>
|
||||||
|
<span style="font-size:11px;color:#9ca3af;">${escapeHtml(reportDate)}</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td></tr>
|
||||||
|
</table>
|
||||||
|
<table cellpadding="0" cellspacing="0" border="0" width="100%" style="background:${verdictBg};border:1px solid ${verdictBd};border-radius:0 0 10px 10px;border-collapse:separate;">
|
||||||
|
<tr><td style="padding:9px 20px;color:${verdictTx};font-size:12px;font-weight:600;">
|
||||||
|
<svg viewBox="0 0 20 20" width="18" height="18" fill="none" style="vertical-align:middle;color:${verdictTx};"><circle cx="10" cy="10" r="8" stroke="currentColor" stroke-width="1.8"/>${verdictPathSvg}</svg>
|
||||||
|
<b style="letter-spacing:.8px;vertical-align:middle;margin-left:8px;">${verdictLabel}</b>
|
||||||
|
<span style="opacity:.7;font-weight:500;vertical-align:middle;margin-left:8px;">${escapeHtml(verdictMsg)}</span>
|
||||||
|
</td></tr>
|
||||||
|
</table>
|
||||||
|
</td></tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<!-- MAIN -->
|
||||||
|
<table align="center" cellpadding="0" cellspacing="0" border="0" width="100%" style="max-width:880px;margin:0 auto;">
|
||||||
|
|
||||||
|
<!-- ZONE 1: AT-A-GLANCE — Product Info + Tech Specs -->
|
||||||
|
<tr><td style="padding-bottom:10px;">
|
||||||
|
<table cellpadding="0" cellspacing="0" border="0" width="100%">
|
||||||
|
<tr>
|
||||||
|
<td width="50%" valign="top" style="padding-right:5px;">
|
||||||
|
<table cellpadding="0" cellspacing="0" border="0" height="265px" width="100%" style="background:#fff;border:1px solid #e5e7eb;border-radius:10px;border-collapse:separate; font-size:14px;">
|
||||||
|
<tr><td style="padding:16px 20px;">
|
||||||
|
<div style="font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.8px;color:#9ca3af;padding-bottom:7px;border-bottom:1px solid #f0f1f3;margin-bottom:8px;">Product Info</div>
|
||||||
|
<table cellpadding="0" cellspacing="0" border="0" width="100%">
|
||||||
|
<tr><td style="font-size:10px;font-weight:600;color:#9ca3af;text-transform:uppercase;letter-spacing:.4px;width:68px;padding:3px 8px 3px 0;white-space:nowrap;vertical-align:top;">Name</td><td style="padding:3px 0;font-weight:500;vertical-align:top;font-size:14px;"><strong>${productName}</strong></td></tr>
|
||||||
|
<tr><td style="font-size:10px;font-weight:600;color:#9ca3af;text-transform:uppercase;letter-spacing:.4px;padding:3px 8px 3px 0;white-space:nowrap;vertical-align:top;">P/N</td><td style="padding:3px 0;font-weight:500;vertical-align:top;font-size:12px;"><strong>${productPN}</strong></td></tr>
|
||||||
|
<tr><td style="font-size:10px;font-weight:600;color:#9ca3af;text-transform:uppercase;letter-spacing:.4px;padding:3px 8px 3px 0;white-space:nowrap;vertical-align:top;">S/N</td><td style="padding:3px 0;font-weight:500;vertical-align:top;font-size:12px;"><strong>${productSN}</strong></td></tr>
|
||||||
|
<tr><td style="font-size:10px;font-weight:600;color:#9ca3af;text-transform:uppercase;letter-spacing:.4px;padding:3px 8px 3px 0;white-space:nowrap;vertical-align:top;">MAC</td><td style="padding:3px 0;font-weight:500;vertical-align:top;font-size:12px;">${macAddress}</td></tr>
|
||||||
|
<tr><td style="font-size:10px;font-weight:600;color:#9ca3af;text-transform:uppercase;letter-spacing:.4px;padding:3px 8px 3px 0;white-space:nowrap;vertical-align:top;">Type</td><td style="padding:3px 0;vertical-align:top;font-weight:500;font-size:12px;">-<span style="display:none;padding:1px 7px;border-radius:50px;font-size:11px;font-weight:600;background:#eff6ff;color:#1d4ed8;border:1px solid #bfdbfe;">-</span></td></tr>
|
||||||
|
<tr><td style="font-size:10px;font-weight:600;color:#9ca3af;text-transform:uppercase;letter-spacing:.4px;padding:3px 8px 3px 0;white-space:nowrap;vertical-align:top;">Cond.</td><td style="padding:3px 0;vertical-align:top;font-weight:500;font-size:12px;">-<span style="display:none;padding:1px 7px;border-radius:50px;font-size:11px;font-weight:600;background:#ecfdf5;color:#065f46;border:1px solid #a7f3d0;">-</span></td></tr>
|
||||||
|
<tr><td style="font-size:10px;font-weight:600;color:#9ca3af;text-transform:uppercase;letter-spacing:.4px;padding:3px 8px 3px 0;white-space:nowrap;vertical-align:top;">Supplier</td><td style="padding:3px 0;vertical-align:top;font-weight:500;font-size:12px;">-</td></tr>
|
||||||
|
<tr><td style="font-size:10px;font-weight:600;color:#9ca3af;text-transform:uppercase;letter-spacing:.4px;padding:3px 8px 3px 0;white-space:nowrap;vertical-align:top;">Warranty</td><td style="padding:3px 0;vertical-align:top;font-weight:500;font-size:12px;">-</td></tr>
|
||||||
|
</table>
|
||||||
|
</td></tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
<td width="50%" valign="top" style="padding-left:5px;">
|
||||||
|
<table cellpadding="0" cellspacing="0" border="0" height="265px" width="100%" style="background:#fff;border:1px solid #e5e7eb;border-radius:10px;border-collapse:separate;">
|
||||||
|
<tr><td style="padding:16px 20px;">
|
||||||
|
<div style="font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.8px;color:#9ca3af;padding-bottom:7px;border-bottom:1px solid #f0f1f3;margin-bottom:8px;">Technical Specs</div>
|
||||||
|
<table cellpadding="0" cellspacing="0" border="0" width="100%" style="font-size:11px;">
|
||||||
|
<tr>
|
||||||
|
<th align="left" style="font-size:9px;text-transform:uppercase;color:#9ca3af;padding:0 0 6px 0;border-bottom:1px solid #f0f1f3;">Specification</th>
|
||||||
|
<th align="left" style="font-size:9px;text-transform:uppercase;color:#9ca3af;padding:0 0 6px 0;border-bottom:1px solid #f0f1f3;">Actual</th>
|
||||||
|
<th align="left" style="font-size:9px;text-transform:uppercase;color:#9ca3af;padding:0 0 6px 0;border-bottom:1px solid #f0f1f3;">Default</th>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="padding:6px 0;border-bottom:1px dashed #f0f1f3;font-weight:600;color:#5f6978;">IOS-XE Version</td>
|
||||||
|
<td style="padding:6px 0;border-bottom:1px dashed #f0f1f3;font-weight:700;font-family:Consolas,monospace;">${iosVersion}</td>
|
||||||
|
<td style="padding:6px 0;border-bottom:1px dashed #f0f1f3;font-weight:500;color:#9ca3af;font-family:Consolas,monospace;">-</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="padding:6px 0;border-bottom:1px dashed #f0f1f3;font-weight:600;color:#5f6978;">System RAM</td>
|
||||||
|
<td style="padding:6px 0;border-bottom:1px dashed #f0f1f3;font-weight:700;font-family:Consolas,monospace;">${memDisplay}</td>
|
||||||
|
<td style="padding:6px 0;border-bottom:1px dashed #f0f1f3;font-weight:500;color:#9ca3af;font-family:Consolas,monospace;">${configRam?.ram || '-'}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="padding:6px 0;border-bottom:1px dashed #f0f1f3;font-weight:600;color:#5f6978;">Flash Storage</td>
|
||||||
|
<td style="padding:6px 0;border-bottom:1px dashed #f0f1f3;font-weight:700;font-family:Consolas,monospace;">${flashDisplay}</td>
|
||||||
|
<td style="padding:6px 0;border-bottom:1px dashed #f0f1f3;font-weight:500;color:#9ca3af;font-family:Consolas,monospace;">${configRam?.flash || '-'}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="padding:6px 0;border-bottom:1px dashed #f0f1f3;font-weight:600;color:#5f6978;">Uplink Module</td>
|
||||||
|
<td style="padding:6px 0;border-bottom:1px dashed #f0f1f3;font-weight:700;font-family:Consolas,monospace;">-</td>
|
||||||
|
<td style="padding:6px 0;border-bottom:1px dashed #f0f1f3;font-style:italic;color:#cbd5e1;font-family:Consolas,monospace;">-</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="padding:6px 0;border-bottom:1px dashed #f0f1f3;font-weight:600;color:#5f6978;">PSU Model</td>
|
||||||
|
<td style="padding:6px 0;border-bottom:1px dashed #f0f1f3;font-weight:700;font-family:Consolas,monospace;">-</td>
|
||||||
|
<td style="padding:6px 0;border-bottom:1px dashed #f0f1f3;font-weight:500;color:#9ca3af;font-family:Consolas,monospace;">-</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="padding:6px 0;font-weight:600;color:#5f6978;">PoE Budget</td>
|
||||||
|
<td style="padding:6px 0;font-weight:700;font-family:Consolas,monospace;">-</td>
|
||||||
|
<td style="padding:6px 0;font-weight:500;color:#9ca3af;font-family:Consolas,monospace;">-</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td></tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td></tr>
|
||||||
|
|
||||||
|
<!-- Issues Found -->
|
||||||
|
<tr><td style="padding-bottom:10px;">
|
||||||
|
<table cellpadding="0" cellspacing="0" border="0" width="100%" style="background:#fff;border:1px solid #e5e7eb;border-radius:10px;border-collapse:separate;">
|
||||||
|
<tr><td style="padding:16px 20px;">
|
||||||
|
<div style="font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.8px;color:#9ca3af;padding-bottom:7px;border-bottom:1px solid #f0f1f3;margin-bottom:10px;">Issues Found</div>
|
||||||
|
${aiIssueRowsHtml}
|
||||||
|
<table cellpadding="0" cellspacing="0" border="0" width="100%" style="background:#fffbeb;border:1px solid #fde68a;border-radius:6px;margin-bottom:5px;border-collapse:separate;">
|
||||||
|
<tr>
|
||||||
|
<td style="padding:7px 12px;font-size:12px;color:#5f6978;font-weight:500;"><span style="display:inline-block;font-size:9px;font-weight:700;letter-spacing:.5px;text-transform:uppercase;padding:2px 6px;border-radius:4px;background:#fef3c7;color:#92400e;vertical-align:middle;">COSMETIC</span><span style="margin-left:8px;vertical-align:middle;">Minor scratch on top chassis (2cm) — non-functional</span></td>
|
||||||
|
<td align="right" style="padding:7px 12px;width:90px;"><span style="display:inline-block;padding:1px 7px;border-radius:50px;font-size:11px;font-weight:600;background:#ecfdf5;color:#065f46;border:1px solid #a7f3d0;">Accepted</span></td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
<table cellpadding="0" cellspacing="0" border="0" width="100%" style="background:#fffbeb;border:1px solid #fde68a;border-radius:6px;margin-bottom:6px;border-collapse:separate;">
|
||||||
|
<tr>
|
||||||
|
<td style="padding:7px 12px;font-size:12px;color:#5f6978;font-weight:500;"><span style="display:inline-block;font-size:9px;font-weight:700;letter-spacing:.5px;text-transform:uppercase;padding:2px 6px;border-radius:4px;background:#ffedd5;color:#9a3412;vertical-align:middle;">MINOR</span><span style="margin-left:8px;vertical-align:middle;">Fan #2 at 48dB under stress (spec 45dB) — within rack tolerance</span></td>
|
||||||
|
<td align="right" style="padding:7px 12px;width:90px;"><span style="display:inline-block;padding:1px 7px;border-radius:50px;font-size:11px;font-weight:600;background:#eff6ff;color:#1d4ed8;border:1px solid #bfdbfe;">Monitor</span></td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
<div style="text-align:center;font-size:11px;color:#9ca3af;padding-top:6px;border-top:1px solid #f0f1f3;"><b>0</b> Critical · <b>0</b> Major · <b>1</b> Minor · <b>1</b> Cosmetic</div>
|
||||||
|
</td></tr>
|
||||||
|
</table>
|
||||||
|
</td></tr>
|
||||||
|
|
||||||
|
<!-- Receiving & Inspection Notes -->
|
||||||
|
<tr><td style="padding-bottom:10px;">
|
||||||
|
<table cellpadding="0" cellspacing="0" border="0" width="100%" style="background:#fff;border:1px solid #e5e7eb;border-radius:10px;border-collapse:separate;">
|
||||||
|
<tr><td style="padding:16px 20px;">
|
||||||
|
<div style="font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.8px;color:#9ca3af;padding-bottom:7px;border-bottom:1px solid #f0f1f3;margin-bottom:10px;">Receiving & Inspection Notes</div>
|
||||||
|
<div style="padding:10px 14px;background:#fffbeb;border-left:3px solid #f59e0b;border-radius:0 6px 6px 0;font-size:12px;color:#92400e;margin-bottom:8px;">
|
||||||
|
<div style="font-weight:700;margin-bottom:4px;font-size:11px;">⚠ Warning from Warehouse</div>
|
||||||
|
<p style="margin:0;">Box arrived with slight indentation on the left corner. Internal foam was still intact. Serial number on box was partially obscured by shipping label but verified upon unboxing.</p>
|
||||||
|
</div>
|
||||||
|
<div style="padding:10px 14px;background:#f9fafb;border-left:3px solid #e5e7eb;border-radius:0 6px 6px 0;font-size:12px;color:#5f6978;">
|
||||||
|
<div style="font-weight:700;margin-bottom:4px;font-size:11px;">Accessory Checklist</div>
|
||||||
|
<table cellpadding="0" cellspacing="0" border="0" style="margin-top:6px;">
|
||||||
|
<tr>
|
||||||
|
<td style="padding:0 4px 0 0;"><span style="display:inline-block;padding:3px 10px;background:#f0fdf4;border:1px solid #bbf7d0;border-radius:4px;font-size:11px;font-weight:600;color:#166534;"><span style="display:inline-block;width:6px;height:6px;border-radius:50%;background:#10b981;margin-right:5px;vertical-align:middle;"></span>Rackmount</span></td>
|
||||||
|
<td style="padding:0 4px;"><span style="display:inline-block;padding:3px 10px;background:#f0fdf4;border:1px solid #bbf7d0;border-radius:4px;font-size:11px;font-weight:600;color:#166534;"><span style="display:inline-block;width:6px;height:6px;border-radius:50%;background:#10b981;margin-right:5px;vertical-align:middle;"></span>PSU (Internal)</span></td>
|
||||||
|
<td style="padding:0 4px;"><span style="display:inline-block;padding:3px 10px;background:#f0fdf4;border:1px solid #bbf7d0;border-radius:4px;font-size:11px;font-weight:600;color:#166534;"><span style="display:inline-block;width:6px;height:6px;border-radius:50%;background:#10b981;margin-right:5px;vertical-align:middle;"></span>Console Cable</span></td>
|
||||||
|
<td style="padding:0 4px;"><span style="display:inline-block;padding:3px 10px;background:#fef2f2;border:1px solid #fecaca;border-radius:4px;font-size:11px;font-weight:600;color:#991b1b;"><span style="display:inline-block;width:6px;height:6px;border-radius:50%;background:#ef4444;margin-right:5px;vertical-align:middle;"></span>Documents</span></td>
|
||||||
|
<td style="padding:0 0 0 4px;"><span style="display:inline-block;padding:3px 10px;background:#fef2f2;border:1px solid #fecaca;border-radius:4px;font-size:11px;font-weight:600;color:#991b1b;"><span style="display:inline-block;width:6px;height:6px;border-radius:50%;background:#ef4444;margin-right:5px;vertical-align:middle;"></span>Original Box</span></td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</td></tr>
|
||||||
|
</table>
|
||||||
|
</td></tr>
|
||||||
|
|
||||||
|
<!-- Inspection Log Workflow -->
|
||||||
|
<tr><td style="padding-bottom:10px;">
|
||||||
|
<table cellpadding="0" cellspacing="0" border="0" width="100%" style="background:#fff;border:1px solid #e5e7eb;border-radius:10px;border-collapse:separate;">
|
||||||
|
<tr><td style="padding:16px 20px;">
|
||||||
|
<table cellpadding="0" cellspacing="0" border="0" width="100%">
|
||||||
|
<!-- line -->
|
||||||
|
<tbody style="position:relative; z-index:0;">
|
||||||
|
<tr>
|
||||||
|
<td colspan="3" style="padding:0 55px;">
|
||||||
|
<div style="height:2px;background:#e2e8f0;font-size:0;line-height:0; position:absolute; z-index:-1; width: 90%; top: 10px;">
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<!-- steps -->
|
||||||
|
<tr>
|
||||||
|
<!-- Step 1 -->
|
||||||
|
<td width="33%" align="center" valign="top" style="padding:0 4px 4px 4px;">
|
||||||
|
<div style="display:inline-block;width:26px;height:26px;background:#fff;border:2px solid #10b981;border-radius:50%;color:#10b981;font-size:14px;font-weight:800;line-height:22px;text-align:center;margin-top:-14px;margin-bottom:8px;">
|
||||||
|
✓
|
||||||
|
</div>
|
||||||
|
<div style="font-size:10px;font-weight:700;text-transform:uppercase;color:#5f6978;margin-bottom:2px;">
|
||||||
|
Received
|
||||||
|
</div>
|
||||||
|
<div style="font-size:11px;font-weight:600;color:#1a1d23;">
|
||||||
|
Unknown
|
||||||
|
</div>
|
||||||
|
<div style="margin-top:4px;font-size:10px;color:#9ca3af;">
|
||||||
|
${momentTZ().tz(timeZone).format('DD MMM')}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<!-- Step 2 -->
|
||||||
|
<td width="34%" align="center" valign="top" style="padding:0 4px 4px 4px;">
|
||||||
|
<div style="display:inline-block;width:26px;height:26px;background:#fff;border:2px solid #10b981;border-radius:50%;color:#10b981;font-size:14px;font-weight:800;line-height:22px;text-align:center;margin-top:-14px;margin-bottom:8px;">
|
||||||
|
✓
|
||||||
|
</div>
|
||||||
|
<div style="font-size:10px;font-weight:700;text-transform:uppercase;color:#5f6978;margin-bottom:2px;">
|
||||||
|
Software Test
|
||||||
|
</div>
|
||||||
|
<div style="font-size:11px;font-weight:600;color:#1a1d23;">
|
||||||
|
${snapshot?.userTest?.dpelp?.name || ''}
|
||||||
|
</div>
|
||||||
|
<div style="margin-top:4px;font-size:10px;color:#9ca3af;">
|
||||||
|
${momentTZ(snapshot?.userTest?.dpelp?.time).tz(timeZone).format('DD MMM, HH:mm')}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<!-- Step 3 -->
|
||||||
|
<td width="33%" align="center" valign="top" style="padding:0 4px 4px 4px;">
|
||||||
|
<div style="display:inline-block;width:26px;height:26px;background:#fff;border:2px solid #10b981;border-radius:50%;color:#10b981;font-size:14px;font-weight:800;line-height:22px;text-align:center;margin-top:-14px;margin-bottom:8px;">
|
||||||
|
✓
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="font-size:10px;font-weight:700;text-transform:uppercase;color:#5f6978;margin-bottom:2px;">
|
||||||
|
Physical Check
|
||||||
|
</div>
|
||||||
|
<div style="font-size:11px;font-weight:600;color:#1a1d23;">
|
||||||
|
${snapshot?.userTest?.physical?.name || ''}
|
||||||
|
</div>
|
||||||
|
<div style="margin-top:4px;font-size:10px;color:#9ca3af;">
|
||||||
|
${momentTZ(snapshot?.userTest?.physical?.time).tz(timeZone).format('DD MMM, HH:mm')}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr></tbody>
|
||||||
|
</table>
|
||||||
|
</td></tr>
|
||||||
|
</table>
|
||||||
|
</td></tr>
|
||||||
|
|
||||||
|
<!-- Divider -->
|
||||||
|
<tr><td style="padding:6px 0;">
|
||||||
|
<table cellpadding="0" cellspacing="0" border="0" width="100%">
|
||||||
|
<tr>
|
||||||
|
<td style="border-top:1px solid #e5e7eb;line-height:1px;font-size:1px;"> </td>
|
||||||
|
<td width="60" align="center" style="font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:1.5px;color:#9ca3af;padding:0 12px;">Detail</td>
|
||||||
|
<td style="border-top:1px solid #e5e7eb;line-height:1px;font-size:1px;"> </td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td></tr>
|
||||||
|
|
||||||
|
<!-- Physical Check section -->
|
||||||
|
<tr><td style="padding-bottom:10px;">
|
||||||
|
<table cellpadding="0" cellspacing="0" border="0" width="100%" style="background:#fff;border:1px solid #e5e7eb;border-radius:10px;border-collapse:separate;">
|
||||||
|
<tr><td style="padding:16px 20px;">
|
||||||
|
<table cellpadding="0" cellspacing="0" border="0" width="100%" style="background:#f0fdf4;border:1px solid #bbf7d0;border-radius:6px;margin-bottom:12px;border-collapse:separate;">
|
||||||
|
<tr>
|
||||||
|
<td style="padding:7px 12px;color:#166534;font-size:13px;font-weight:700;">
|
||||||
|
<svg viewBox="0 0 20 20" width="17" height="17" fill="none" style="vertical-align:middle;color:#166534;"><rect x="2" y="2" width="16" height="16" rx="3" stroke="currentColor" stroke-width="1.5"/><path d="M7 10h6M10 7v6" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>
|
||||||
|
<span style="vertical-align:middle;margin-left:8px;">Physical Check</span>
|
||||||
|
</td>
|
||||||
|
<td align="right" style="padding:7px 12px;color:#166534;font-size:11px;font-weight:500;opacity:.65;">${snapshot?.userTest?.physical?.name || ''} · ${momentTZ(snapshot?.userTest?.physical?.time).tz(timeZone).format('DD MMM, HH:mm')}</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
<table cellpadding="0" cellspacing="0" border="0" width="100%">
|
||||||
|
<tr>
|
||||||
|
<td width="200" valign="top" style="padding-right:14px;">
|
||||||
|
<table cellpadding="0" cellspacing="0" border="0" width="100%">
|
||||||
|
<tr>
|
||||||
|
<td width="50%" style="padding:0 3px 6px 0;">${photoCellHtml('Front')}</td>
|
||||||
|
<td width="50%" style="padding:0 0 6px 3px;">${photoCellHtml('Rear')}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td width="50%" style="padding:0 3px 0 0;">${photoCellHtml('S/N Label')}</td>
|
||||||
|
<td width="50%" style="padding:0 0 0 3px;">${photoCellHtml('Package')}</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
<td valign="top">
|
||||||
|
${checklistRowsHtml}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td></tr>
|
||||||
|
</table>
|
||||||
|
</td></tr>
|
||||||
|
|
||||||
|
<!-- Software Check section -->
|
||||||
|
<tr><td style="padding-bottom:10px;">
|
||||||
|
<table cellpadding="0" cellspacing="0" border="0" width="100%" style="background:#fff;border:1px solid #e5e7eb;border-radius:10px;border-collapse:separate;">
|
||||||
|
<tr><td style="padding:16px 20px;">
|
||||||
|
<table cellpadding="0" cellspacing="0" border="0" width="100%" style="background:#eff6ff;border:1px solid #93c5fd;border-radius:6px;margin-bottom:12px;border-collapse:separate;">
|
||||||
|
<tr>
|
||||||
|
<td style="padding:7px 12px;color:#1e40af;font-size:13px;font-weight:700;">
|
||||||
|
<svg viewBox="0 0 20 20" width="17" height="17" fill="none" style="vertical-align:middle;color:#1e40af;"><rect x="2" y="3" width="16" height="11" rx="2" stroke="currentColor" stroke-width="1.5"/><path d="M7 17h6M10 14v3" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>
|
||||||
|
<span style="vertical-align:middle;margin-left:8px;">Software Check</span>
|
||||||
|
</td>
|
||||||
|
<td align="right" style="padding:7px 12px;color:#1e40af;font-size:11px;font-weight:500;opacity:.65;">${snapshot?.userTest?.dpelp?.name || ''} · ${momentTZ(snapshot?.userTest?.dpelp?.time).tz(timeZone).format('DD MMM, HH:mm')}</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
<table cellpadding="0" cellspacing="0" border="0" width="100%">
|
||||||
|
<tr>
|
||||||
|
<td width="33%" valign="top" style="padding-right:8px;">
|
||||||
|
<div style="font-size:10px;font-weight:700;text-transform:uppercase;color:#9ca3af;letter-spacing:.5px;margin-bottom:8px;">Hardware Inventory</div>
|
||||||
|
<table cellpadding="0" cellspacing="0" border="0" width="100%" style="font-size:11px;">
|
||||||
|
${
|
||||||
|
this.config?.inventory?.listInventory
|
||||||
|
?.map(
|
||||||
|
(item: any) => `
|
||||||
|
<tr><td style="padding:2px 0;border-bottom:1px solid #f0f1f3;font-weight:600;color:#5f6978;">${item.pid}</td><td style="padding:2px 0;border-bottom:1px solid #f0f1f3;font-family:Consolas,monospace;color:#9ca3af;text-align:right;">${item.sn}</td></tr>`
|
||||||
|
)
|
||||||
|
.join('') || ''
|
||||||
|
}
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
<td width="33%" valign="top" style="padding:0 4px;">
|
||||||
|
<div style="font-size:10px;font-weight:700;text-transform:uppercase;color:#9ca3af;letter-spacing:.5px;margin-bottom:8px;">System & License</div>
|
||||||
|
${licenseBoxesHtml}
|
||||||
|
</td>
|
||||||
|
<td width="34%" valign="top" style="padding-left:8px;">
|
||||||
|
<div style="font-size:10px;font-weight:700;text-transform:uppercase;color:#9ca3af;letter-spacing:.5px;margin-bottom:8px;">Port Test Summary</div>
|
||||||
|
<table cellpadding="0" cellspacing="0" border="0" width="100%">
|
||||||
|
<tr>
|
||||||
|
<td width="50%" style="padding:0 3px 6px 0;"><table cellpadding="0" cellspacing="0" border="0" width="100%" style="background:#f8fafc;border:1px solid #e2e8f0;border-radius:6px;border-collapse:separate;"><tr><td align="center" style="padding:6px;"><div style="font-weight:800;font-size:14px;color:${poeColor};">${escapeHtml(poeText)}</div><div style="font-size:9px;font-weight:600;color:#9ca3af;text-transform:uppercase;">${hasPortData ? 'PoE UP' : 'GigE UP'}</div></td></tr></table></td>
|
||||||
|
<td width="50%" style="padding:0 0 6px 3px;"><table cellpadding="0" cellspacing="0" border="0" width="100%" style="background:#f8fafc;border:1px solid #e2e8f0;border-radius:6px;border-collapse:separate;"><tr><td align="center" style="padding:6px;"><div style="font-weight:800;font-size:14px;color:${sfpColor};">${escapeHtml(sfpText)}</div><div style="font-size:9px;font-weight:600;color:#9ca3af;text-transform:uppercase;">SFP+ UP</div></td></tr></table></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td width="50%" style="padding:0 3px 0 0;"><table cellpadding="0" cellspacing="0" border="0" width="100%" style="background:#f8fafc;border:1px solid #e2e8f0;border-radius:6px;border-collapse:separate;"><tr><td align="center" style="padding:6px;"><div style="font-weight:800;font-size:14px;color: ${missingSFP.length > 0 || missingPoE.length > 0 ? '#f59e0b' : '#10b981'};">${missingSFP.length > 0 || missingPoE.length > 0 ? 'WARN' : 'PASS'}</div><div style="font-size:9px;font-weight:600;color:#9ca3af;text-transform:uppercase;">PoE+ Test</div></td></tr></table></td>
|
||||||
|
<td width="50%" style="padding:0 0 0 3px;"><table cellpadding="0" cellspacing="0" border="0" width="100%" style="background:#f8fafc;border:1px solid #e2e8f0;border-radius:6px;border-collapse:separate;"><tr><td align="center" style="padding:6px;"><div style="font-weight:800;font-size:14px;color: ${missingSFP.length > 0 || missingPoE.length > 0 ? '#f59e0b' : '#10b981'};">${totalPoE + totalSFP === 0 ? 100 : Math.round(((totalPoE + totalSFP - (missingPoE.length + missingSFP.length)) / (totalPoE + totalSFP)) * 100)}%</div><div style="font-size:9px;font-weight:600;color:#9ca3af;text-transform:uppercase;">Throughput</div></td></tr></table></td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
${missingDetailsHtml}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<!-- CONSOLE RAW OUTPUT -->
|
||||||
|
<table cellpadding="0" cellspacing="0" border="0" width="100%" style="margin-top:16px;background:#1e293b;border-radius:6px;border:1px solid #334155;border-collapse:separate;">
|
||||||
|
<tr><td style="padding:6px 12px;background:#334155;color:#94a3b8;font-size:10px;font-weight:700;letter-spacing:.5px;border-radius:6px 6px 0 0;">CONSOLE RAW OUTPUT (Boot Log snippet)</td></tr>
|
||||||
|
<tr><td><pre style="overflow-y: auto; max-height: 300px; padding:12px;color:#cbd5e1;font-family:Consolas,'Courier New',monospace;font-size:11px;line-height:1.6;white-space:pre-wrap;word-break:break-all;">${snapshot?.outputTestLog || 'No test log available'}</pre></td></tr>
|
||||||
|
</table>
|
||||||
|
</td></tr>
|
||||||
|
</table>
|
||||||
|
</td></tr>
|
||||||
|
|
||||||
|
<!-- Footer -->
|
||||||
|
<tr><td align="center" style="padding-top:16px;font-size:10px;color:#9ca3af;">Prology IT — Equipment QA System · Confidential — Internal Use Only</td></tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>`
|
||||||
|
|
||||||
|
this.updateNote(config?.inventory?.sn, this.dataDPELP as DataDPELP)
|
||||||
|
await sendMessageToMail(
|
||||||
|
`[ATC] - [${config.stationName} - Line: ${config.lineNumber}] - [${this.config.inventory?.pid}] - [${this.config.inventory?.sn}] - Summary of Testing Results`,
|
||||||
|
body
|
||||||
|
)
|
||||||
|
this.socketIO.emit('summary_tested', {
|
||||||
|
stationId: this.config.stationId,
|
||||||
|
lineId: this.config.id,
|
||||||
|
body: body,
|
||||||
|
title: `[${config.stationName} - Line: ${config.lineNumber}] - Summary of Testing Results`,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reset config information of line
|
* Reset config information of line
|
||||||
*/
|
*/
|
||||||
|
|
@ -1933,6 +2464,8 @@ Ports Missing/Down: ${missing.length}\n\n`
|
||||||
snapConfig: this.config,
|
snapConfig: this.config,
|
||||||
snapPhysical: this.physicalTest,
|
snapPhysical: this.physicalTest,
|
||||||
reason: '',
|
reason: '',
|
||||||
|
outputTestLog: this.outputTestLog,
|
||||||
|
userTest: this.userTest,
|
||||||
}
|
}
|
||||||
this.debounceSendSummaryReport = setTimeout(() => {
|
this.debounceSendSummaryReport = setTimeout(() => {
|
||||||
if (!this.config.listFeatureTested?.includes('PHYSICAL')) {
|
if (!this.config.listFeatureTested?.includes('PHYSICAL')) {
|
||||||
|
|
@ -1942,7 +2475,9 @@ Ports Missing/Down: ${missing.length}\n\n`
|
||||||
}
|
}
|
||||||
this.config.listFeatureTested = ['DPELP', 'PHYSICAL', 'SUMMARY']
|
this.config.listFeatureTested = ['DPELP', 'PHYSICAL', 'SUMMARY']
|
||||||
this.sendFeatureTested()
|
this.sendFeatureTested()
|
||||||
this.sendReportSummary(snapshot)
|
this.sendReportSummaryV2(snapshot)
|
||||||
|
this.outputTestLog = ''
|
||||||
|
this.userTest = { dpelp: { name: '', time: 0 }, physical: { name: '', time: 0 } }
|
||||||
}, timeout)
|
}, timeout)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,11 @@ import XRegExp from 'xregexp'
|
||||||
// Parser function
|
// Parser function
|
||||||
const parseLog = (data: string) => {
|
const parseLog = (data: string) => {
|
||||||
const patterns = [
|
const patterns = [
|
||||||
XRegExp('^.*Software.*\\((?<SOFTWARE_IMAGE>\\S+)\\),\\s+Version\\s+(?<VERSION>[\\w\\.-]+)'),
|
|
||||||
XRegExp(
|
XRegExp(
|
||||||
'^\\*?\\s*\\d+\\s+\\d+\\s+[\\w-]+\\s+(?<VERSION>\\d[\\w\\.-]+)\\s+(?<SOFTWARE_IMAGE>[\\w-]+)\\s+(?:BUNDLE|INSTALL)'
|
'^.*Software.*\\((?<SOFTWARE_IMAGE>\\S+)\\),\\s+Version\\s+(?<VERSION>[\\w\\.\\(\\)\\-]+)'
|
||||||
|
),
|
||||||
|
XRegExp(
|
||||||
|
'^\\*?\\s*\\d+\\s+\\d+\\s+[\\w-]+\\s+(?<VERSION>\\d[\\w\\.\\(\\)\\-]+)\\s+(?<SOFTWARE_IMAGE>[\\w-]+)\\s+(?:BUNDLE|INSTALL)'
|
||||||
),
|
),
|
||||||
XRegExp('System\\s+image\\s+file\\s+is\\s+"(?:[^:]*:)?(?<SOFTWARE_IMAGE>[^"]+)"'),
|
XRegExp('System\\s+image\\s+file\\s+is\\s+"(?:[^:]*:)?(?<SOFTWARE_IMAGE>[^"]+)"'),
|
||||||
XRegExp('Active-image:\\s+(?<SOFTWARE_IMAGE>\\S+)'),
|
XRegExp('Active-image:\\s+(?<SOFTWARE_IMAGE>\\S+)'),
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
import { BaseSchema } from '@adonisjs/lucid/schema'
|
||||||
|
|
||||||
|
export default class extends BaseSchema {
|
||||||
|
protected tableName = 'users'
|
||||||
|
|
||||||
|
async up() {
|
||||||
|
this.schema.alterTable(this.tableName, (table) => {
|
||||||
|
table.string('first_name').nullable().after('user_name')
|
||||||
|
table.string('last_name').nullable().after('first_name')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async down() {
|
||||||
|
this.schema.alterTable(this.tableName, (table) => {
|
||||||
|
table.dropColumn('first_name')
|
||||||
|
table.dropColumn('last_name')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -203,6 +203,7 @@ export class WebSocketIo {
|
||||||
socket.on('run_scenario', async (data) => {
|
socket.on('run_scenario', async (data) => {
|
||||||
const lineId = data.id
|
const lineId = data.id
|
||||||
const scenario = data.scenario
|
const scenario = data.scenario
|
||||||
|
const name = data.userName || userName
|
||||||
// Check station is active
|
// Check station is active
|
||||||
const activeStation = await checkStationActive(data.stationId)
|
const activeStation = await checkStationActive(data.stationId)
|
||||||
if (!activeStation) return
|
if (!activeStation) return
|
||||||
|
|
@ -211,7 +212,7 @@ export class WebSocketIo {
|
||||||
io,
|
io,
|
||||||
data.stationId,
|
data.stationId,
|
||||||
[lineId],
|
[lineId],
|
||||||
async (line) => line.runScript(scenario, userName),
|
async (line) => line.runScript(scenario, name),
|
||||||
{
|
{
|
||||||
scenario,
|
scenario,
|
||||||
}
|
}
|
||||||
|
|
@ -728,7 +729,7 @@ export class WebSocketIo {
|
||||||
})
|
})
|
||||||
|
|
||||||
socket.on('run_physical_test', async (data) => {
|
socket.on('run_physical_test', async (data) => {
|
||||||
const { stationId, lineId } = data
|
const { stationId, lineId, userName: name } = data
|
||||||
// Check station is active
|
// Check station is active
|
||||||
const activeStation = await checkStationActive(stationId)
|
const activeStation = await checkStationActive(stationId)
|
||||||
if (!activeStation) return
|
if (!activeStation) return
|
||||||
|
|
@ -737,7 +738,7 @@ export class WebSocketIo {
|
||||||
io,
|
io,
|
||||||
stationId,
|
stationId,
|
||||||
[lineId],
|
[lineId],
|
||||||
async (lineCon) => lineCon.runPhysicalTest(),
|
async (lineCon) => lineCon.runPhysicalTest(name || userName),
|
||||||
{}
|
{}
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
@ -907,7 +908,7 @@ export class WebSocketIo {
|
||||||
stationId,
|
stationId,
|
||||||
[lineId],
|
[lineId],
|
||||||
async (lineCon) => {
|
async (lineCon) => {
|
||||||
lineCon.sendReportSummary()
|
lineCon.sendReportSummaryV2()
|
||||||
},
|
},
|
||||||
{}
|
{}
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -223,8 +223,8 @@ const BottomToolBar = ({
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setSelectedLines(
|
setSelectedLines(
|
||||||
selectedLines.filter(
|
selectedLines.filter(
|
||||||
(line) => line.id !== el.id
|
(line) => line.id !== el.id,
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
socket?.emit("close_cli", {
|
socket?.emit("close_cli", {
|
||||||
lineId: el?.id,
|
lineId: el?.id,
|
||||||
|
|
@ -288,7 +288,7 @@ const BottomToolBar = ({
|
||||||
const lines = station.lines.filter(
|
const lines = station.lines.filter(
|
||||||
(line) =>
|
(line) =>
|
||||||
!line?.userOpenCLI ||
|
!line?.userOpenCLI ||
|
||||||
line?.userOpenCLI === user?.userName
|
line?.userOpenCLI === user?.userName,
|
||||||
);
|
);
|
||||||
if (selectedLines.length !== lines.length) {
|
if (selectedLines.length !== lines.length) {
|
||||||
setSelectedLines(lines);
|
setSelectedLines(lines);
|
||||||
|
|
@ -365,7 +365,7 @@ const BottomToolBar = ({
|
||||||
selectedLines={selectedLines}
|
selectedLines={selectedLines}
|
||||||
isDisable={isDisable || selectedLines.length === 0}
|
isDisable={isDisable || selectedLines.length === 0}
|
||||||
dataDPELP={scenarios?.find(
|
dataDPELP={scenarios?.find(
|
||||||
(el) => el.title.toUpperCase() === "DPELP"
|
(el) => el.title.toUpperCase() === "DPELP",
|
||||||
)}
|
)}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (selectedLines.length > 0) {
|
if (selectedLines.length > 0) {
|
||||||
|
|
@ -380,6 +380,11 @@ const BottomToolBar = ({
|
||||||
setIsDisable(false);
|
setIsDisable(false);
|
||||||
}, 5000);
|
}, 5000);
|
||||||
}}
|
}}
|
||||||
|
userName={
|
||||||
|
user?.firstName
|
||||||
|
? `${user.firstName} ${user.lastName || ""}`
|
||||||
|
: user?.userName || "Unknown User"
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
fw={400}
|
fw={400}
|
||||||
|
|
@ -400,7 +405,7 @@ const BottomToolBar = ({
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (
|
if (
|
||||||
selectedLines?.filter(
|
selectedLines?.filter(
|
||||||
(line) => line?.runningPhysical
|
(line) => line?.runningPhysical,
|
||||||
)?.length > 0
|
)?.length > 0
|
||||||
) {
|
) {
|
||||||
selectedLines
|
selectedLines
|
||||||
|
|
@ -430,7 +435,7 @@ const BottomToolBar = ({
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{selectedLines?.filter(
|
{selectedLines?.filter(
|
||||||
(line) => line?.runningPhysical
|
(line) => line?.runningPhysical,
|
||||||
)?.length > 0
|
)?.length > 0
|
||||||
? "Done/End"
|
? "Done/End"
|
||||||
: "Physical"}
|
: "Physical"}
|
||||||
|
|
@ -545,8 +550,8 @@ const BottomToolBar = ({
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setSelectedLines(
|
setSelectedLines(
|
||||||
selectedLines.filter(
|
selectedLines.filter(
|
||||||
(line) => line.id !== el.id
|
(line) => line.id !== el.id,
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
socket?.emit("close_cli", {
|
socket?.emit("close_cli", {
|
||||||
lineId: el?.id,
|
lineId: el?.id,
|
||||||
|
|
@ -645,7 +650,7 @@ const BottomToolBar = ({
|
||||||
const lines = station.lines.filter(
|
const lines = station.lines.filter(
|
||||||
(line) =>
|
(line) =>
|
||||||
!line?.userOpenCLI ||
|
!line?.userOpenCLI ||
|
||||||
line?.userOpenCLI === user?.userName
|
line?.userOpenCLI === user?.userName,
|
||||||
);
|
);
|
||||||
if (selectedLines.length !== lines.length) {
|
if (selectedLines.length !== lines.length) {
|
||||||
setSelectedLines(lines);
|
setSelectedLines(lines);
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ export const ButtonDPELP = ({
|
||||||
dataDPELP,
|
dataDPELP,
|
||||||
text = "DPELP",
|
text = "DPELP",
|
||||||
color = "#00a164",
|
color = "#00a164",
|
||||||
|
userName = "Unknown User",
|
||||||
}: {
|
}: {
|
||||||
socket: Socket | null;
|
socket: Socket | null;
|
||||||
isDisable: boolean;
|
isDisable: boolean;
|
||||||
|
|
@ -27,6 +28,7 @@ export const ButtonDPELP = ({
|
||||||
dataDPELP?: IScenario;
|
dataDPELP?: IScenario;
|
||||||
text?: string;
|
text?: string;
|
||||||
color?: string;
|
color?: string;
|
||||||
|
userName?: string;
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
|
|
@ -161,7 +163,8 @@ export const ButtonDPELP = ({
|
||||||
timeout: 360000,
|
timeout: 360000,
|
||||||
body: JSON.stringify(body),
|
body: JSON.stringify(body),
|
||||||
},
|
},
|
||||||
})
|
userName: userName,
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
|
|
@ -179,6 +182,7 @@ export const ButtonScenario = ({
|
||||||
scenario,
|
scenario,
|
||||||
fontSize = "12px",
|
fontSize = "12px",
|
||||||
station,
|
station,
|
||||||
|
userName,
|
||||||
}: {
|
}: {
|
||||||
socket: Socket | null;
|
socket: Socket | null;
|
||||||
isDisable: boolean;
|
isDisable: boolean;
|
||||||
|
|
@ -187,6 +191,7 @@ export const ButtonScenario = ({
|
||||||
scenario: IScenario;
|
scenario: IScenario;
|
||||||
fontSize?: string;
|
fontSize?: string;
|
||||||
station: TStation | undefined;
|
station: TStation | undefined;
|
||||||
|
userName?: string;
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
|
|
@ -206,7 +211,7 @@ export const ButtonScenario = ({
|
||||||
(el) =>
|
(el) =>
|
||||||
el.outlet &&
|
el.outlet &&
|
||||||
Number(el.outlet) > 0 &&
|
Number(el.outlet) > 0 &&
|
||||||
(el.apcName === "apc_1" || el.apc_name === "apc_1")
|
(el.apcName === "apc_1" || el.apc_name === "apc_1"),
|
||||||
)
|
)
|
||||||
?.map((el) => el.outlet);
|
?.map((el) => el.outlet);
|
||||||
const lineApc2 = selectedLines
|
const lineApc2 = selectedLines
|
||||||
|
|
@ -214,7 +219,7 @@ export const ButtonScenario = ({
|
||||||
(el) =>
|
(el) =>
|
||||||
el.outlet &&
|
el.outlet &&
|
||||||
Number(el.outlet) > 0 &&
|
Number(el.outlet) > 0 &&
|
||||||
(el.apcName === "apc_2" || el.apc_name === "apc_2")
|
(el.apcName === "apc_2" || el.apc_name === "apc_2"),
|
||||||
)
|
)
|
||||||
?.map((el) => el.outlet);
|
?.map((el) => el.outlet);
|
||||||
if (lineApc1.length > 0)
|
if (lineApc1.length > 0)
|
||||||
|
|
@ -250,7 +255,8 @@ export const ButtonScenario = ({
|
||||||
? scenario?.isReboot
|
? scenario?.isReboot
|
||||||
: scenario?.is_reboot,
|
: scenario?.is_reboot,
|
||||||
},
|
},
|
||||||
})
|
userName: userName,
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
|
|
@ -278,7 +284,7 @@ export const ButtonCopy = ({
|
||||||
?.map((el) => {
|
?.map((el) => {
|
||||||
// Get data platform
|
// Get data platform
|
||||||
const dataPlatform = el.data?.find(
|
const dataPlatform = el.data?.find(
|
||||||
(comm: TextFSM) => comm.command?.trim() === "show platform"
|
(comm: TextFSM) => comm.command?.trim() === "show platform",
|
||||||
);
|
);
|
||||||
const DPELP =
|
const DPELP =
|
||||||
dataPlatform && !dataPlatform?.output?.includes("Incomplete")
|
dataPlatform && !dataPlatform?.output?.includes("Incomplete")
|
||||||
|
|
@ -289,7 +295,7 @@ export const ButtonCopy = ({
|
||||||
const dataLicense = el.data?.find(
|
const dataLicense = el.data?.find(
|
||||||
(comm: TextFSM) =>
|
(comm: TextFSM) =>
|
||||||
comm.command?.trim() === "show license" ||
|
comm.command?.trim() === "show license" ||
|
||||||
comm.command?.trim() === "sh license"
|
comm.command?.trim() === "sh license",
|
||||||
);
|
);
|
||||||
const listLicense =
|
const listLicense =
|
||||||
dataLicense?.textfsm && Array.isArray(dataLicense?.textfsm)
|
dataLicense?.textfsm && Array.isArray(dataLicense?.textfsm)
|
||||||
|
|
@ -341,7 +347,7 @@ export const ButtonSelect = ({
|
||||||
>
|
>
|
||||||
{selectedLines.length !==
|
{selectedLines.length !==
|
||||||
station.lines.filter(
|
station.lines.filter(
|
||||||
(line) => !line?.userOpenCLI || line?.userOpenCLI === userName
|
(line) => !line?.userOpenCLI || line?.userOpenCLI === userName,
|
||||||
).length
|
).length
|
||||||
? "Select All"
|
? "Select All"
|
||||||
: "Deselect"}
|
: "Deselect"}
|
||||||
|
|
|
||||||
|
|
@ -596,6 +596,11 @@ const CardLine = ({
|
||||||
setIsDisabled(false);
|
setIsDisabled(false);
|
||||||
}, 5000);
|
}, 5000);
|
||||||
}}
|
}}
|
||||||
|
userName={
|
||||||
|
user?.firstName
|
||||||
|
? `${user.firstName} ${user.lastName || ""}`
|
||||||
|
: user?.userName || "Unknown User"
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<Menu
|
<Menu
|
||||||
closeOnItemClick={false}
|
closeOnItemClick={false}
|
||||||
|
|
@ -639,6 +644,11 @@ const CardLine = ({
|
||||||
}}
|
}}
|
||||||
scenario={el}
|
scenario={el}
|
||||||
fontSize="11px"
|
fontSize="11px"
|
||||||
|
userName={
|
||||||
|
user?.firstName
|
||||||
|
? `${user.firstName} ${user.lastName || ""}`
|
||||||
|
: user?.userName || "Unknown User"
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import {
|
import {
|
||||||
Modal,
|
Modal,
|
||||||
Box,
|
Box,
|
||||||
|
|
@ -33,6 +33,12 @@ export default function ModalConfirmRunPhysical({
|
||||||
socket,
|
socket,
|
||||||
station,
|
station,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
|
const user = useMemo(() => {
|
||||||
|
return localStorage.getItem("user") &&
|
||||||
|
typeof localStorage.getItem("user") === "string"
|
||||||
|
? JSON.parse(localStorage.getItem("user") || "")
|
||||||
|
: null;
|
||||||
|
}, []);
|
||||||
const [dataLines, setDataLines] = useState<PropsLines[]>([]);
|
const [dataLines, setDataLines] = useState<PropsLines[]>([]);
|
||||||
const [isDisabled, setIsDisabled] = useState<boolean>(false);
|
const [isDisabled, setIsDisabled] = useState<boolean>(false);
|
||||||
|
|
||||||
|
|
@ -48,7 +54,7 @@ export default function ModalConfirmRunPhysical({
|
||||||
sn: line?.inventory?.sn,
|
sn: line?.inventory?.sn,
|
||||||
vid: line?.inventory?.vid,
|
vid: line?.inventory?.vid,
|
||||||
checked: true,
|
checked: true,
|
||||||
}))
|
})),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}, [listLines]);
|
}, [listLines]);
|
||||||
|
|
@ -100,8 +106,8 @@ export default function ModalConfirmRunPhysical({
|
||||||
...el,
|
...el,
|
||||||
checked: e.target.checked,
|
checked: e.target.checked,
|
||||||
}
|
}
|
||||||
: el
|
: el,
|
||||||
)
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
@ -133,13 +139,16 @@ export default function ModalConfirmRunPhysical({
|
||||||
socket?.emit("run_physical_test", {
|
socket?.emit("run_physical_test", {
|
||||||
lineId: line?.id,
|
lineId: line?.id,
|
||||||
stationId: Number(station?.id),
|
stationId: Number(station?.id),
|
||||||
|
userName: user?.firstName
|
||||||
|
? `${user.firstName} ${user.lastName || ""}`
|
||||||
|
: user?.userName || "Unknown User",
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
setDataLines(listNotChecked);
|
setDataLines(listNotChecked);
|
||||||
setListLines(
|
setListLines(
|
||||||
listLines.filter((el) =>
|
listLines.filter((el) =>
|
||||||
listNotChecked.find((li) => li.id === el.id)
|
listNotChecked.find((li) => li.id === el.id),
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
setIsDisabled(true);
|
setIsDisabled(true);
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
|
|
||||||
|
|
@ -664,6 +664,9 @@ const ModalTerminal = ({
|
||||||
timeout: 360000,
|
timeout: 360000,
|
||||||
body: JSON.stringify(bodyDPELP),
|
body: JSON.stringify(bodyDPELP),
|
||||||
},
|
},
|
||||||
|
userName: user?.firstName
|
||||||
|
? `${user.firstName} ${user.lastName || ""}`
|
||||||
|
: user?.userName || "Unknown User",
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
socket?.emit("run_all_dpelp", {
|
socket?.emit("run_all_dpelp", {
|
||||||
|
|
@ -687,17 +690,17 @@ const ModalTerminal = ({
|
||||||
setIsDisable(false);
|
setIsDisable(false);
|
||||||
}, 15000);
|
}, 15000);
|
||||||
}
|
}
|
||||||
if (activeStep >= 2 && !isDisable) {
|
// if (activeStep >= 2 && !isDisable) {
|
||||||
setActiveStep(3);
|
// setActiveStep(3);
|
||||||
setIsDisable(true);
|
// setIsDisable(true);
|
||||||
setTimeout(() => {
|
// setTimeout(() => {
|
||||||
setIsDisable(false);
|
// setIsDisable(false);
|
||||||
}, 5000);
|
// }, 5000);
|
||||||
socket?.emit("send_summary_report", {
|
// socket?.emit("send_summary_report", {
|
||||||
lineId: line?.id,
|
// lineId: line?.id,
|
||||||
stationId: line?.station_id || line?.stationId,
|
// stationId: line?.station_id || line?.stationId,
|
||||||
});
|
// });
|
||||||
}
|
// }
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Stepper.Step
|
<Stepper.Step
|
||||||
|
|
@ -737,11 +740,7 @@ const ModalTerminal = ({
|
||||||
<Stepper.Step
|
<Stepper.Step
|
||||||
disabled={isDisable}
|
disabled={isDisable}
|
||||||
label="Completed"
|
label="Completed"
|
||||||
description={
|
description={"Complete all to send report"}
|
||||||
activeStep < 2
|
|
||||||
? "Complete all to send report"
|
|
||||||
: "Click to send report"
|
|
||||||
}
|
|
||||||
></Stepper.Step>
|
></Stepper.Step>
|
||||||
</Stepper>
|
</Stepper>
|
||||||
<Flex></Flex>
|
<Flex></Flex>
|
||||||
|
|
@ -1492,6 +1491,11 @@ const ModalTerminal = ({
|
||||||
dataDPELP={scenarios?.find(
|
dataDPELP={scenarios?.find(
|
||||||
(el) => el.title.toUpperCase() === "DPELP",
|
(el) => el.title.toUpperCase() === "DPELP",
|
||||||
)}
|
)}
|
||||||
|
userName={
|
||||||
|
user?.firstName
|
||||||
|
? `${user.firstName} ${user.lastName || ""}`
|
||||||
|
: user?.userName || "Unknown User"
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
fw={400}
|
fw={400}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue