Update form report summary, gắn api

This commit is contained in:
nguyentrungthat 2026-05-14 10:23:22 +07:00
parent 9cd3defc1d
commit 884c113a03
3 changed files with 95 additions and 33 deletions

View File

@ -11,6 +11,7 @@ import {
detectConfigRamByModel, detectConfigRamByModel,
detectScenarioByModel, detectScenarioByModel,
escapeHtml, escapeHtml,
getIncomingInfoBySN,
isRamSufficient, isRamSufficient,
isValidJson, isValidJson,
LogStreamBuffer, LogStreamBuffer,
@ -1933,6 +1934,14 @@ Ports Missing/Down: ${missing.length}\n\n`
const totalPoE = testedPoE.length + missingPoE.length const totalPoE = testedPoE.length + missingPoE.length
const totalSFP = testedSFP.length + missingSFP.length const totalSFP = testedSFP.length + missingSFP.length
const dataIncomingBySN = await getIncomingInfoBySN(config?.inventory?.sn)
const serialInfo = dataIncomingBySN?.serialNumbersInfo?.find(
(s: any) => s.serialNumberA === config?.inventory?.sn
)
const listImages = dataIncomingBySN?.packagePo?.listFiles?.filter(
(s: any) => s.kind === 'other'
)
const showVersion = config?.data?.find( const showVersion = config?.data?.find(
(d) => d.command?.trim()?.includes('show ver') || d.command?.trim()?.includes('sh ver') (d) => d.command?.trim()?.includes('show ver') || d.command?.trim()?.includes('sh ver')
) )
@ -2077,13 +2086,20 @@ Ports Missing/Down: ${missing.length}\n\n`
// Physical Check checklist // Physical Check checklist
const checklistItems: Array<[string, string]> = [ const checklistItems: Array<[string, string]> = [
['ok', 'Packaging intact — no damage to box or foam'], [
['ok', 'No physical damage — chassis, fans, PSU'], serialInfo?.optionVisualInspection?.statusChassis ? 'ok' : 'warn',
['ok', `S/N matches label — ${productSN} verified`], serialInfo?.optionVisualInspection?.statusChassis
['ok', 'All 48 GigE + 4 SFP+ ports clean'], ? 'Overall hardware status is normal'
['ok', 'Accessories — power cable, rack ears, console cable'], : 'Hardware issue detected on chassis/system',
['warn', 'Minor scratch on top chassis (2cm) — cosmetic only'], ],
[
serialInfo?.optionVisualInspection?.statusPortsPOE ? 'ok' : 'warn',
serialInfo?.optionVisualInspection?.statusPortsPOE
? 'All ports and PoE functions are operating normally'
: 'Port or PoE issue detected',
],
] ]
const checklistRowsHtml = checklistItems const checklistRowsHtml = checklistItems
.map(([k, t]) => .map(([k, t]) =>
k === 'ok' k === 'ok'
@ -2096,6 +2112,30 @@ Ports Missing/Down: ${missing.length}\n\n`
const photoCellHtml = (label: string) => 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>` `<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>`
// Photo cell with actual image
const imageCellHtml = (url: string, label: string) =>
`<a href="${url}" target="_blank" style="display:block;text-decoration:none;"><table cellpadding="0" cellspacing="0" border="0" width="100%" style="border:1px solid #e5e7eb;border-radius:6px;background:#f9fafb;border-collapse:separate;overflow:hidden;height:120px;cursor:pointer;"><tr><td align="center" style="padding:0;background-size:cover;background-position:center;background-image:url('${url}');position:relative;"></td></tr></table></a>`
// Prepare image grid: get first 4 images from listImages if available
const imageList = listImages && Array.isArray(listImages) ? listImages.slice(0, 4) : []
const imageLabels = ['Front', 'Rear', 'S/N Label', 'Package']
const getPhotoCell = (idx: number) => {
const image = imageList[idx]
const label = imageLabels[idx]
return image && image.url
? imageCellHtml(process.env.ERP_URL + image.url, label)
: photoCellHtml(label)
}
const photoGridRowsHtml = `
<tr>
<td width="50%" style="padding:0 3px 6px 0;">${getPhotoCell(0)}</td>
<td width="50%" style="padding:0 0 6px 3px;">${getPhotoCell(1)}</td>
</tr>
<tr>
<td width="50%" style="padding:0 3px 0 0;">${getPhotoCell(2)}</td>
<td width="50%" style="padding:0 0 0 3px;">${getPhotoCell(3)}</td>
</tr>`
// Helper function to highlight SNs from listInventory in outputTestLog // Helper function to highlight SNs from listInventory in outputTestLog
const highlightSnInConsoleOutput = (text: string, listInventory: any[] | undefined) => { const highlightSnInConsoleOutput = (text: string, listInventory: any[] | undefined) => {
if (!text || !listInventory || listInventory.length === 0) { if (!text || !listInventory || listInventory.length === 0) {
@ -2184,8 +2224,8 @@ Ports Missing/Down: ${missing.length}\n\n`
<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;">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;">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;">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;">Cond.</td><td style="padding:3px 0;font-weight:500;vertical-align:top;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;">Cond.</td><td style="padding:3px 0;font-weight:500;vertical-align:top;font-size:12px;">${serialInfo?.condition || '-'}</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;font-weight:500;vertical-align:top;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;">Supplier</td><td style="padding:3px 0;font-weight:500;vertical-align:top;font-size:12px;">${serialInfo?.supplier?.name || '-'}</td></tr>
</table> </table>
</td></tr> </td></tr>
</table> </table>
@ -2240,9 +2280,10 @@ Ports Missing/Down: ${missing.length}\n\n`
<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 &amp; Inspection Notes</div> <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 &amp; 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="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;">&#9888; Warning from Warehouse</div> <div style="font-weight:700;margin-bottom:4px;font-size:11px;">&#9888; Warning from Warehouse</div>
<p style="display:none; 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> <p style="margin:0;">${dataIncomingBySN?.packagePo?.notes || ''}</p>
<p style="padding:6px 0;border-bottom:1px dashed #f0f1f3;font-weight:600;color:#5f6978; font-size:11px; font-style:italic;">Not Available</p> <p style="margin:0;">${serialInfo?.notes || ''}</p>
</div> ${!dataIncomingBySN?.packagePo?.notes && !serialInfo?.notes ? '<p style="margin:0;">No notes available.</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="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> <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;"> <table cellpadding="0" cellspacing="0" border="0" style="margin-top:6px;">
@ -2287,10 +2328,10 @@ Ports Missing/Down: ${missing.length}\n\n`
Received Received
</div> </div>
<div style="font-size:11px;font-weight:600;color:#1a1d23;font-style:italic;"> <div style="font-size:11px;font-weight:600;color:#1a1d23;font-style:italic;">
Not Available ${dataIncomingBySN?.packagePo?.receivedBy?.fullName || 'Unknown'}
</div> </div>
<div style="margin-top:4px;font-size:10px;color:#9ca3af;"> <div style="margin-top:4px;font-size:10px;color:#9ca3af;">
${momentTZ().tz(timeZone).format('DD MMM')} ${dataIncomingBySN?.packagePo?.receivedDate ? momentTZ(dataIncomingBySN?.packagePo?.receivedDate).tz(timeZone).format('DD MMM, HH:mm') : ''}
</div> </div>
</td> </td>
<!-- Step 2 --> <!-- Step 2 -->
@ -2303,10 +2344,10 @@ Ports Missing/Down: ${missing.length}\n\n`
Visual Check Visual Check
</div> </div>
<div style="font-size:11px;font-weight:600;color:#1a1d23;font-style:italic;"> <div style="font-size:11px;font-weight:600;color:#1a1d23;font-style:italic;">
Not Available ${dataIncomingBySN?.packagePo?.receivedBy?.fullName || 'Unknown'}
</div> </div>
<div style="margin-top:4px;font-size:10px;color:#9ca3af;"> <div style="margin-top:4px;font-size:10px;color:#9ca3af;">
${momentTZ().tz(timeZone).format('DD MMM')} ${dataIncomingBySN?.packagePo?.receivedDate ? momentTZ(dataIncomingBySN?.packagePo?.receivedDate).tz(timeZone).format('DD MMM, HH:mm') : ''}
</div> </div>
</td> </td>
<!-- Step 3 --> <!-- Step 3 -->
@ -2318,7 +2359,7 @@ Ports Missing/Down: ${missing.length}\n\n`
Software Test Software Test
</div> </div>
<div style="font-size:11px;font-weight:600;color:#1a1d23;"> <div style="font-size:11px;font-weight:600;color:#1a1d23;">
${this?.userTest?.dpelp?.name || ''} ${this?.userTest?.dpelp?.name || 'Unknown'}
</div> </div>
<div style="margin-top:4px;font-size:10px;color:#9ca3af;"> <div style="margin-top:4px;font-size:10px;color:#9ca3af;">
${momentTZ(this?.userTest?.dpelp?.time).tz(timeZone).format('DD MMM, HH:mm')} ${momentTZ(this?.userTest?.dpelp?.time).tz(timeZone).format('DD MMM, HH:mm')}
@ -2351,29 +2392,19 @@ Ports Missing/Down: ${missing.length}\n\n`
<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> <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;">Visual Check</span> <span style="vertical-align:middle;margin-left:8px;">Visual Check</span>
</td> </td>
<td align="right" style="display:none;padding:7px 12px;color:#166534;font-size:11px;font-weight:500;opacity:.65;">${this?.userTest?.physical?.name || ''} · ${momentTZ(this?.userTest?.physical?.time).tz(timeZone).format('DD MMM, HH:mm')}</td> <td align="right" style="padding:7px 12px;color:#166534;font-size:11px;font-weight:500;opacity:.65;">${dataIncomingBySN?.packagePo?.receivedBy?.fullName || 'Unknown'} · ${dataIncomingBySN?.packagePo?.receivedDate ? momentTZ(dataIncomingBySN?.packagePo?.receivedDate).tz(timeZone).format('DD MMM, HH:mm') : ''}</td>
<td align="right" style="padding:7px 12px;color:#166534;font-size:11px;font-weight:500;opacity:.65; font-style:italic;">Not Available</td>
</tr> </tr>
</table> </table>
<table cellpadding="0" cellspacing="0" border="0" width="100%"> <table cellpadding="0" cellspacing="0" border="0" width="100%">
<tr> <tr>
<td width="200" valign="top" style="padding-right:14px;"> <td width="200" valign="top" style="padding-right:14px;">
<table cellpadding="0" cellspacing="0" border="0" width="100%"> <table cellpadding="0" cellspacing="0" border="0" width="100%">
<tr> ${photoGridRowsHtml}
<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> </table>
</td> </td>
<td style="display: none;" valign="top"> <td valign="top">
${checklistRowsHtml} ${checklistRowsHtml}
</td> </td>
<td style="padding:6px 0;border-bottom:1px dashed #f0f1f3;font-weight:600;color:#5f6978; font-size:11px; font-style:italic;">Not Available</td>
</tr>
</table> </table>
</td></tr> </td></tr>
</table> </table>

View File

@ -1437,3 +1437,35 @@ export function canInputCommand(buffer: string): boolean {
return false return false
} }
export async function getIncomingInfoBySN(sn: string) {
try {
if (!sn) return
const remoteUrl = process.env.ERP_URL || 'https://stage.nswteam.net'
const header = {
Authorization: 'Bearer ' + process.env.ERP_TOKEN,
}
const responseDataSN = await axios.post(
remoteUrl + '/api/transferGetData',
{
urlAPI: '/api/package-po/get-incoming-by-sn',
filter: {
where: {
serialNumber: sn,
},
},
},
{
headers: header,
}
)
if (!responseDataSN?.data?.data) {
return
}
return responseDataSN?.data?.data
} catch (error) {
console.log('getIncomingInfoBySN', error)
}
}

View File

@ -1447,8 +1447,8 @@ export class WebSocketIo {
*/ */
generateZulipMessage(results: any[]) { generateZulipMessage(results: any[]) {
let msg = `` let msg = ``
msg += `| Line | PID | SN | MAC | IOS | License | Summary | Issues |\n` msg += `| Line | PID | SN | MAC | IOS | License | Issues |\n`
msg += `| ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- |\n` msg += `| ---- | ---- | ---- | ---- | ---- | ---- | ---- |\n`
for (const item of results) { for (const item of results) {
if (!item) continue if (!item) continue
@ -1464,7 +1464,7 @@ export class WebSocketIo {
// Format issues // Format issues
const issuesMd = item.issues?.length const issuesMd = item.issues?.length
? item.issues.map((i: string) => `${i}`).join(' --') ? item.issues.map((i: string) => `${i.replace('|', '')}`).join(' --')
: '- No issues detected.' : '- No issues detected.'
msg += msg +=
@ -1474,7 +1474,6 @@ export class WebSocketIo {
` | ${item.mac || ''}` + ` | ${item.mac || ''}` +
` | ${item.ios || ''}` + ` | ${item.ios || ''}` +
` | ${licenseMd}` + ` | ${licenseMd}` +
` | ${item.summary || ''}` +
` | ${issuesMd}` + ` | ${issuesMd}` +
` |\n` ` |\n`
} }