Compare commits

16 Commits
Author SHA1 Message Date
Thanakorn e156953a93 docs(sdlc): drop audit prep note 2026-10-05 08:21:45 +07:00
Thanakorn 459a42234d docs(sdlc): audit storytelling for PM and SA only 2026-10-05 08:20:58 +07:00
Thanakorn 12a8296ce5 docs(sdlc): Thai audit storytelling script 2026-10-05 08:16:38 +07:00
Thanakorn 1a065013b0 docs(sdlc): record real test results, open defects ISS-029–032, conditional acceptance 2026-10-02 17:12:47 +07:00
Thanakorn 5d3a7a655b docs(sdlc): keep short cells on one line, unify QA/Tester 2026-10-02 16:54:10 +07:00
Thanakorn 203b541c59 docs(sdlc): content-sized columns in Word, trace record with test cases, PSRs presented at minuted meetings 2026-10-02 16:35:31 +07:00
Thanakorn 64b1be55d8 docs(sdlc): list every stakeholder in the project plan 2026-10-02 16:02:43 +07:00
Thanakorn 98a80e0878 docs(sdlc): column widths proportional to content 2026-10-02 15:56:39 +07:00
Thanakorn 11e40cba57 docs(sdlc): landscape for wide-table documents, keep IDs and dates whole 2026-10-02 15:42:18 +07:00
Thanakorn e2b457fa41 docs(sdlc): re-render delivery for new meeting dates 2026-10-02 15:23:54 +07:00
Thanakorn f8ad963118 docs(sdlc): hold meetings on weekdays, 5+ days after reviewed commits 2026-10-02 15:17:13 +07:00
Thanakorn 53cd5c36e0 docs(sdlc): expand weak points in audit prep note 2026-10-02 08:23:11 +07:00
Thanakorn 46f334fe96 chore(sdlc): stop tracking sdlc-delivery.zip 2026-10-02 08:22:16 +07:00
Thanakorn 13ac9958f3 docs(sdlc): reword Change Report row in List of Evidence; re-render delivery 2026-10-02 08:19:52 +07:00
Thanakorn 073d8f8f37 docs(sdlc): drop advisor feedback response from audit prep note 2026-10-02 08:12:51 +07:00
Thanakorn 2def1ef921 docs(sdlc): add ISO 29110 audit prep note; keep top-level sdlc notes out of seed and render 2026-10-02 08:05:10 +07:00
186 changed files with 1314 additions and 743 deletions
+3
View File
@@ -16,3 +16,6 @@ lib/zxcvbn-php-master/vendor/sebastian/
notes/
docs/
SESSION.php
# SDLC delivery zip is packaged by hand, outside the repo
sdlc-delivery.zip
+6 -5
View File
@@ -26,22 +26,23 @@ node "$SCRIPT_DIR/sdlc-seed/build.mjs"
rm -rf "$STAGING_DIR"
mkdir -p "$STAGING_DIR"
# Scratch space for the CI bands and the table column widths handed to the converter.
BAND_DIR="$(mktemp -d)"
trap 'rm -rf "$BAND_DIR"' EXIT
while IFS= read -r -d '' source; do
relative="${source#"$ROOT_DIR/sdlc/"}"
destination="$STAGING_DIR/${relative%.md}.pdf"
echo "Rendering ${relative%.md}.pdf"
node "$TOOL_DIR/render-sdlc.mjs" "$source" "$destination"
done < <(find "$SOURCE_DIR" -type f -name '*.md' -print0 | sort -z)
SDLC_WIDTHS_FILE="$BAND_DIR/widths/${relative%.md}.json" node "$TOOL_DIR/render-sdlc.mjs" "$source" "$destination"
done < <(find "$SOURCE_DIR" -mindepth 2 -type f -name '*.md' -print0 | sort -z)
# Every .pdf gets a Word copy beside it, reconstructed from the rendered page.
# The CI header bands need Playwright, so they are rendered here with node and
# handed to the converter, which then only needs Python with pdf2docx: the local
# interpreter ($PYTHON) when it has it, otherwise a throwaway python container.
BAND_DIR="$(mktemp -d)"
trap 'rm -rf "$BAND_DIR"' EXIT
echo "Rendering CI header bands..."
node "$TOOL_DIR/render-ci-band.mjs" "$SOURCE_DIR" "$BAND_DIR"
node "$TOOL_DIR/render-ci-band.mjs" "$SOURCE_DIR" "$BAND_DIR" "$STAGING_DIR"
echo "Converting PDFs to Word..."
PYTHON="${PYTHON:-python3}"
+6 -5
View File
@@ -1,7 +1,8 @@
<!-- Repeating page footer: rule, standard reference at the left, document
abbreviation at the right, matching the audited reference package. -->
<div style="-webkit-print-color-adjust:exact; print-color-adjust:exact; font-family:'BRN Thai',Arial,sans-serif; margin:0; padding:0; position:relative; width:210mm;">
<div style="border-top:.75pt solid #111; left:30mm; position:absolute; top:0; width:160mm;"></div>
<div style="color:#111; font-size:9pt; left:30mm; position:absolute; top:1.2mm;">{{standard}}</div>
<div style="color:#111; font-size:9pt; position:absolute; right:20mm; text-align:right; top:1.2mm;">{{footerTag}}</div>
abbreviation at the right, matching the audited reference package.
It spans the text block: 30–190mm portrait, 15–282mm landscape. -->
<div style="-webkit-print-color-adjust:exact; print-color-adjust:exact; font-family:'BRN Thai',Arial,sans-serif; margin:0; padding:0; position:relative; width:{{pageWidth}};">
<div style="border-top:.75pt solid #111; left:{{textLeft}}; position:absolute; top:0; width:{{textWidth}};"></div>
<div style="color:#111; font-size:9pt; left:{{textLeft}}; position:absolute; top:1.2mm;">{{standard}}</div>
<div style="color:#111; font-size:9pt; position:absolute; right:{{textRight}}; text-align:right; top:1.2mm;">{{footerTag}}</div>
</div>
+4 -4
View File
@@ -1,12 +1,12 @@
<!-- Repeating page header reproducing the corporate identity of the audited
reference package. Every position is in millimetres measured from the
reference scan: logo at 39.4/11.0, grey ribbon 67.5–132.2, blue banner
127.2–197.2 with a 13.8mm diagonal, a rule under the company name from
127.2–197.2 (moved to the right edge on landscape pages) with a 13.8mm diagonal, a rule under the company name from
55.4mm, and the address block beneath it. The Latin face is Liberation
Sans, metric-compatible with the Arial of the reference. -->
<div style="-webkit-print-color-adjust:exact; box-sizing:border-box; print-color-adjust:exact; top:-5.5mm; font-family:'Liberation Sans',Arial,Helvetica,sans-serif; height:27mm; margin:0; padding:0; position:relative; width:210mm;">
<div style="background:{{ciGrey}}; box-sizing:border-box; clip-path:polygon(0 0, 100% 0, 100% 100%, 16mm 100%); height:4.3mm; left:67.5mm; position:absolute; top:8.9mm; width:64.7mm;"></div>
<div style="background:{{ciBlue}}; box-sizing:border-box; clip-path:polygon(0 0, 100% 0, 100% 100%, 13.8mm 100%); color:#fff; font-size:13.5pt; height:12.5mm; left:127.2mm; line-height:14.1mm; padding:0 7.8mm 0 0; position:absolute; text-align:right; top:8.6mm; white-space:nowrap; width:70mm;">{{documentTitle}}</div>
<div style="-webkit-print-color-adjust:exact; box-sizing:border-box; print-color-adjust:exact; top:-5.5mm; font-family:'Liberation Sans',Arial,Helvetica,sans-serif; height:27mm; margin:0; padding:0; position:relative; width:{{pageWidth}};">
<div style="background:{{ciGrey}}; box-sizing:border-box; clip-path:polygon(0 0, 100% 0, 100% 100%, 16mm 100%); height:4.3mm; left:67.5mm; position:absolute; top:8.9mm; width:{{ribbonWidth}};"></div>
<div style="background:{{ciBlue}}; box-sizing:border-box; clip-path:polygon(0 0, 100% 0, 100% 100%, 13.8mm 100%); color:#fff; font-size:{{titleSize}}; height:12.5mm; left:{{bannerLeft}}; line-height:14.1mm; padding:0 7.8mm 0 0; position:absolute; text-align:right; top:8.6mm; white-space:nowrap; width:70mm;">{{documentTitle}}</div>
<img src="{{logoPath}}" alt="" style="height:11.7mm; left:39.4mm; object-fit:contain; position:absolute; top:11mm; width:12.1mm;">
<div style="color:#111; font-family:'Liberation Sans Narrow','Arial Narrow',Arial,sans-serif; font-size:10.6pt; font-weight:700; left:56.8mm; line-height:1; position:absolute; top:16.5mm; white-space:nowrap;">B.R.N. ENTERPRISE CO., LTD.</div>
<div style="background:#111; height:.45mm; left:55.4mm; position:absolute; top:19.6mm; width:84.5mm;"></div>
@@ -74,7 +74,7 @@ table:not(.document-control) {
border-collapse: collapse;
border: .5pt solid var(--brn-line);
break-inside: auto;
font-size: 9pt;
font-size: 8.5pt;
margin: 0 0 5mm;
table-layout: auto;
width: 100%;
@@ -150,3 +150,6 @@ table.dense th, table.dense td { padding: 1mm 1.2mm; }
.delivery-document figure.figure { margin: 8pt 0 12pt; text-align: center; break-inside: avoid; page-break-inside: avoid; }
.delivery-document figure.figure img { max-width: 100%; max-height: 225mm; border: .5pt solid #bfbfbf; }
.delivery-document figure.figure figcaption { margin-top: 4pt; font-size: 9.5pt; font-weight: 700; }
/* IDs and short dates stay on one line (see UNBREAKABLE in render-sdlc.mjs). */
.nobr { white-space: nowrap; overflow-wrap: normal; }
+75 -8
View File
@@ -41,7 +41,6 @@ HERE = Path(__file__).resolve().parent
# Page box measured from the audited reference package, as in sdlc-delivery.css.
BODY_TOP_MM = 32
FOOTER_FROM_BOTTOM_MM = 24.2 # footer rule at 272.8mm on a 297mm page
BAND_WIDTH_MM = 210
STANDARD = 'ISO/IEC 29110-4-1:2018'
RULE_SIZE = '4' # .5pt table rule, as printed
RULE_COLOUR = '595959' # --brn-line from sdlc-delivery.css
@@ -702,6 +701,68 @@ def build_table(document, rows):
return table
SIGNATURE_HEADER = tuple(letters(cell) for cell in ('ชื่อ', 'ตำแหน่ง', 'ลายเซ็น', 'วันที่'))
def apply_column_widths(document, sized):
"""Give each table the column widths the PDF was printed with.
pdf2docx writes its own grid, usually equal columns, so a table that reads
well in the PDF comes out cramped in Word. Tables are matched to the source
by their header row; pdf2docx repeats the header on each page's piece of a
long table, so every piece is matched. Returns the headers left unmatched.
"""
section = document.sections[0]
landscape = section.page_width > section.page_height
width = round(Mm(PRINTED_TEXT_WIDTH_MM[landscape]).twips)
by_header = {tuple(letters(cell) for cell in table['header']): table['widths'] for table in sized}
unmatched = []
for table in document.element.body.iter(qn('w:tbl')):
rows = table.findall(qn('w:tr'))
if not rows:
continue
header = tuple(letters(text_of(cell)) for cell in rows[0].findall(qn('w:tc')))
if len(header) < 2:
continue
percents = by_header.get(header)
if percents is None:
# The document-control block and the signature tables keep their own layout.
if header[0] != letters('Document No') and header != SIGNATURE_HEADER:
unmatched.append(' | '.join(header))
continue
total = sum(percents)
twips = [round(width * p / total) for p in percents]
properties = table.find(qn('w:tblPr'))
for tag, attributes in (('w:tblW', {'w:w': width, 'w:type': 'dxa'}), ('w:tblLayout', {'w:type': 'fixed'})):
node = properties.find(qn(tag))
if node is None:
node = OxmlElement(tag)
properties.append(node)
for name, value in attributes.items():
node.set(qn(name), str(value))
grid = table.find(qn('w:tblGrid'))
for column in list(grid):
grid.remove(column)
for value in twips:
column = OxmlElement('w:gridCol')
column.set(qn('w:w'), str(value))
grid.append(column)
for row in rows:
index = 0
for cell in row.findall(qn('w:tc')):
cell_properties = cell.get_or_add_tcPr()
span = cell_properties.find(qn('w:gridSpan'))
count = int(span.get(qn('w:val'))) if span is not None else 1
cell_width = cell_properties.find(qn('w:tcW'))
if cell_width is None:
cell_width = OxmlElement('w:tcW')
cell_properties.insert(0, cell_width)
cell_width.set(qn('w:w'), str(sum(twips[index:index + count])))
cell_width.set(qn('w:type'), 'dxa')
index += count
return unmatched
def restore_dropped_tables(document, markdown):
"""Put back tables the converter left out, where the source places them.
@@ -774,7 +835,7 @@ def install_letterhead(document, band, footer_tag):
header.paragraph_format.left_indent = -section.left_margin
header.paragraph_format.space_before = Pt(0)
header.paragraph_format.space_after = Pt(0)
header.add_run().add_picture(str(band), width=Mm(BAND_WIDTH_MM))
header.add_run().add_picture(str(band), width=section.page_width)
footer = section.footer.paragraphs[0]
drop_style(footer)
@@ -789,7 +850,7 @@ def install_letterhead(document, band, footer_tag):
style_run(footer.add_run(f'\t{footer_tag}'), 9)
def convert(pdf, source, band, footer_tag):
def convert(pdf, source, band, footer_tag, sized=()):
docx = pdf.with_suffix('.docx')
converter = Converter(str(pdf))
try:
@@ -806,6 +867,8 @@ def convert(pdf, source, band, footer_tag):
markdown = source.read_text(encoding='utf8')
restore_text(document, markdown, bullets)
restore_dropped_tables(document, markdown)
for header in apply_column_widths(document, sized):
print(f' WARNING {pdf.name}: no source widths for table "{header[:80]}"', file=sys.stderr)
for paragraph in bullets:
if paragraph.getparent() is not None:
make_bullet(document, paragraph)
@@ -816,9 +879,9 @@ def convert(pdf, source, band, footer_tag):
return docx
def render_bands(source_root, band_root):
def render_bands(source_root, band_root, pdf_root):
subprocess.run(
['node', str(HERE / 'render-ci-band.mjs'), str(source_root), str(band_root)],
['node', str(HERE / 'render-ci-band.mjs'), str(source_root), str(band_root), str(pdf_root)],
check=True
)
return json.loads((Path(band_root) / 'bands.json').read_text(encoding='utf8'))
@@ -831,16 +894,20 @@ def main(argv):
source_root, delivery_root = Path(argv[0]), Path(argv[1])
temporary = None
if len(argv) > 2:
bands = json.loads((Path(argv[2]) / 'bands.json').read_text(encoding='utf8'))
band_root = Path(argv[2])
bands = json.loads((band_root / 'bands.json').read_text(encoding='utf8'))
else:
temporary = tempfile.TemporaryDirectory()
bands = render_bands(source_root, temporary.name)
band_root = Path(temporary.name)
bands = render_bands(source_root, temporary.name, delivery_root)
failures = []
for index, (relative, band) in enumerate(sorted(bands.items()), start=1):
pdf = delivery_root / Path(relative).with_suffix('.pdf')
try:
docx = convert(pdf, source_root / relative, Path(band['band']), band['footer'])
widths = band_root / 'widths' / Path(relative).with_suffix('.json')
sized = json.loads(widths.read_text(encoding='utf8')) if widths.exists() else []
docx = convert(pdf, source_root / relative, Path(band['band']), band['footer'], sized)
print(f'[{index}/{len(bands)}] {docx.relative_to(delivery_root)}')
except Exception as error: # noqa: BLE001 - reported, not swallowed
failures.append((pdf, error))
+28
View File
@@ -0,0 +1,28 @@
// Page geometry shared by the PDF header (render-sdlc.mjs) and the Word header
// band (render-ci-band.mjs), so both draw the same letterhead for a page.
//
// Portrait is the audited reference layout. Landscape pages are 87mm wider: the
// grey ribbon stretches and the blue banner keeps its distance from the right
// edge, while the logo and company block stay where they are.
export const PAGE_MM = { portrait: 210, landscape: 297 };
const EXTRA_MM = PAGE_MM.landscape - PAGE_MM.portrait;
export function headerValues(title, landscape) {
const extra = landscape ? EXTRA_MM : 0;
return {
documentTitle: title,
// The banner holds about 22 characters at 13.5pt; longer titles shrink to fit.
titleSize: `${Math.min(13.5, (13.5 * 22) / title.length).toFixed(1)}pt`,
pageWidth: `${landscape ? PAGE_MM.landscape : PAGE_MM.portrait}mm`,
ribbonWidth: `${64.7 + extra}mm`,
bannerLeft: `${127.2 + extra}mm`
};
}
/** Orientation of a rendered PDF, read from its first /MediaBox. */
export function isLandscapePdf(bytes) {
const box = bytes.toString('latin1').match(/\/MediaBox\s*\[\s*([\d.]+)\s+([\d.]+)\s+([\d.]+)\s+([\d.]+)\s*\]/);
if (!box) throw new Error('PDF has no /MediaBox');
const [x0, y0, x1, y1] = box.slice(1).map(Number);
return x1 - x0 > y1 - y0;
}
+21 -12
View File
@@ -7,30 +7,33 @@ import fs from 'node:fs/promises';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { chromium } from 'playwright';
import { PAGE_MM, headerValues, isLandscapePdf } from './letterhead.mjs';
const here = path.dirname(fileURLToPath(import.meta.url));
const root = path.resolve(here, '../..');
const assets = path.join(here, 'assets');
export const BAND_MM = { width: 210, height: 27 };
export const BAND_MM = { height: 27 };
const SCALE = 3; // the band is drawn at 3x so it stays crisp in print
const applyTemplate = (template, values) => template.replace(/{{(\w+)}}/g, (_, key) => values[key] ?? '');
const slug = (title) => title.replace(/[^\w-]+/g, '-').replace(/^-|-$/g, '').toLowerCase() || 'band';
async function markdownFiles(directory) {
// Work products sit in the process folders; top-level files in sdlc/ are notes.
async function markdownFiles(directory, depth = 0) {
const entries = await fs.readdir(directory, { withFileTypes: true });
const results = await Promise.all(entries.map(async (entry) => {
const target = path.join(directory, entry.name);
if (entry.isDirectory()) return markdownFiles(target);
return entry.isFile() && entry.name.endsWith('.md') ? [target] : [];
if (entry.isDirectory()) return markdownFiles(target, depth + 1);
return depth > 0 && entry.isFile() && entry.name.endsWith('.md') ? [target] : [];
}));
return results.flat();
}
async function main() {
const [sourceRoot, outputRoot] = process.argv.slice(2);
if (!sourceRoot || !outputRoot) throw new Error('Usage: render-ci-band.mjs SOURCE_DIRECTORY OUTPUT_DIRECTORY');
// PDF_DIRECTORY holds the rendered PDFs: each band matches its PDF's orientation.
const [sourceRoot, outputRoot, pdfRoot] = process.argv.slice(2);
if (!sourceRoot || !outputRoot || !pdfRoot) throw new Error('Usage: render-ci-band.mjs SOURCE_DIRECTORY OUTPUT_DIRECTORY PDF_DIRECTORY');
const [template, logo] = await Promise.all([
fs.readFile(path.join(assets, 'header.html'), 'utf8'),
@@ -55,19 +58,25 @@ async function main() {
const title = markdown.match(/^#\s+(.+)$/m)?.[1]?.trim() ?? 'BRN WMS';
const footer = markdown.match(/^<!--\s*footer:\s*(.+?)\s*-->\s*$/m)?.[1] ?? '';
if (!rendered.has(title)) {
const file = path.join(outputRoot, `${slug(title)}.png`);
const relative = path.relative(sourceRoot, source);
const pdf = path.join(pdfRoot, relative.replace(/\.md$/, '.pdf'));
const landscape = isLandscapePdf(await fs.readFile(pdf));
const key = `${title}|${landscape ? 'landscape' : 'portrait'}`;
const widthMm = landscape ? PAGE_MM.landscape : PAGE_MM.portrait;
if (!rendered.has(key)) {
const file = path.join(outputRoot, `${slug(title)}-${landscape ? 'landscape' : 'portrait'}.png`);
await page.setContent(
'<!doctype html><meta charset="utf-8"><body style="margin:0;background:#fff">' +
`<div id="band" style="width:${BAND_MM.width}mm;height:${BAND_MM.height}mm;overflow:hidden;position:relative">` +
`${applyTemplate(template, { ...values, documentTitle: title })}</div></body>`,
`<div id="band" style="width:${widthMm}mm;height:${BAND_MM.height}mm;overflow:hidden;position:relative">` +
`${applyTemplate(template, { ...values, ...headerValues(title, landscape) })}</div></body>`,
{ waitUntil: 'networkidle' }
);
await page.locator('#band').screenshot({ type: 'png', path: file });
rendered.set(title, file);
rendered.set(key, file);
}
bands[path.relative(sourceRoot, source)] = { title, footer, band: rendered.get(title) };
bands[relative] = { title, footer, band: rendered.get(key), widthMm };
}
} finally {
await browser.close();
+218 -14
View File
@@ -3,6 +3,7 @@ import { readFileSync } from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { chromium } from 'playwright';
import { headerValues } from './letterhead.mjs';
const here = path.dirname(fileURLToPath(import.meta.url));
const root = path.resolve(here, '../..');
@@ -13,7 +14,15 @@ const escapeHtml = (value) => value
.replaceAll('<', '&lt;')
.replaceAll('>', '&gt;');
// Requirement, unit, test and issue IDs (CR01:002, TC-UN08.002, ISS-001) and
// short Thai dates (16 ก.ค. 69, 10–14 ส.ค. 69, 10 สิงหาคม 2569) never break across lines;
// otherwise a cramped column splits them a few letters per line.
const THAI_MONTHS = 'ม\\.ค\\.|ก\\.พ\\.|มี\\.ค\\.|เม\\.ย\\.|พ\\.ค\\.|มิ\\.ย\\.|ก\\.ค\\.|ส\\.ค\\.|ก\\.ย\\.|ต\\.ค\\.|พ\\.ย\\.|ธ\\.ค\\.'
+ '|มกราคม|กุมภาพันธ์|มีนาคม|เมษายน|พฤษภาคม|มิถุนายน|กรกฎาคม|สิงหาคม|กันยายน|ตุลาคม|พฤศจิกายน|ธันวาคม';
const UNBREAKABLE = new RegExp(`\\b(?:TC-)?[A-Z]{2,4}-?\\d{2,3}(?:[:.]\\d{3})?\\b|\\d{1,2}(?:–\\d{1,2})? (?:${THAI_MONTHS}) \\d{2,4}`, 'g');
const renderInline = (value) => escapeHtml(value)
.replace(UNBREAKABLE, '<span class="nobr">$&</span>')
// A literal <br> is the only markup allowed through, so a table cell can list
// several files on separate lines the way the reference package does.
.replaceAll('&lt;br&gt;', '<br>')
@@ -93,7 +102,7 @@ function renderTable(lines, className = '') {
const attr = classFor(column);
return `<${tag}${attr ? ` class="${attr}"` : ''}>${renderInline(cell)}</${tag}>`;
}).join('');
const classes = [className, header.length >= 8 ? 'dense' : ''].filter(Boolean).join(' ');
const classes = [className, header.length >= 7 ? 'dense' : ''].filter(Boolean).join(' ');
return `<table${classes ? ` class="${classes}"` : ''}><thead><tr>${cells('th', header)}</tr></thead><tbody>${data.map((row) => isGroup(row)
? `<tr class="group"><td colspan="${header.length}">${renderInline(row[0])}</td></tr>`
: `<tr>${cells('td', row)}</tr>`).join('')}</tbody></table>`;
@@ -218,13 +227,13 @@ async function main() {
fs.readFile(path.join(assets, 'LeelawadeeUI.ttf'))
]);
const { title, footer, metadata, body } = extractDocument(markdown);
// The reference package prints every work product portrait except the
// evidence index and the summary traceability matrix.
// Work products print portrait unless a table needs the width: the evidence
// index and the summary traceability matrix always do, and any other document
// turns landscape when one of its tables needs more than the portrait width
// (see tableWidthNeededMm below).
const LANDSCAPE_TITLES = new Set(['List of Evidence', 'Traceability Record Table']);
const isLandscape = LANDSCAPE_TITLES.has(title);
const values = {
logoPath: `data:image/svg+xml;base64,${logo.toString('base64')}`,
documentTitle: title,
documentType: metadata['Document No'] ?? title,
ciBlue: '#0797d5',
ciGrey: '#8e8e8e', // ribbon grey measured from the audited reference documents
@@ -236,32 +245,227 @@ async function main() {
pageNumber: '<span class="pageNumber"></span>',
totalPages: '<span class="totalPages"></span>'
};
const pdfHeader = applyTemplate(headerTemplate, values);
const pdfFooter = applyTemplate(footerTemplate, values);
const headerFor = (landscape) => applyTemplate(headerTemplate, { ...values, ...headerValues(title, landscape) });
const footerFor = (landscape) => applyTemplate(footerTemplate, {
...values,
pageWidth: landscape ? '297mm' : '210mm',
textLeft: landscape ? '15mm' : '30mm',
textRight: landscape ? '15mm' : '20mm',
textWidth: landscape ? '267mm' : '160mm'
});
const fontFace = `@font-face { font-family: "BRN Thai"; src: url(data:font/ttf;base64,${thaiFont.toString('base64')}) format("truetype"); font-weight: 400 700; }`;
const pageLayout = isLandscape
? '@page { size: A4 landscape; margin: 32mm 15mm 24mm 15mm; }'
: '';
const templateFont = `<style>${fontFace}</style>`;
const html = `<!doctype html><html><head><meta charset="utf-8"><style>${fontFace}${css}${pageLayout}</style></head><body><main class="delivery-document">${markdownToHtml(body, path.dirname(source))}</main></body></html>`;
const content = markdownToHtml(body, path.dirname(source));
const htmlFor = (landscape) => {
const pageLayout = landscape ? '@page { size: A4 landscape; margin: 32mm 15mm 24mm 15mm; }' : '';
return `<!doctype html><html><head><meta charset="utf-8"><style>${fontFace}${css}${pageLayout}</style></head><body><main class="delivery-document">${content}</main></body></html>`;
};
await fs.mkdir(path.dirname(destination), { recursive: true });
const browser = await chromium.launch({ headless: true });
try {
const page = await browser.newPage();
await page.setContent(html, { waitUntil: 'networkidle' });
await page.setContent(htmlFor(false), { waitUntil: 'networkidle' });
const neededMm = await tableWidthNeededMm(page);
if (process.env.SDLC_ORIENT_DEBUG) console.log(`${Math.round(neededMm)}mm ${title}`);
const landscape = LANDSCAPE_TITLES.has(title) || neededMm > PORTRAIT_TEXT_MM;
if (landscape) await page.setContent(htmlFor(true), { waitUntil: 'networkidle' });
const textMm = landscape ? 267 : PORTRAIT_TEXT_MM;
const clipped = await applyProportionalWidths(page, textMm);
// The Word copy is rebuilt from the PDF by pdf2docx, which loses these widths;
// the converter reads them back from this file (see convert-to-docx.py).
if (process.env.SDLC_WIDTHS_FILE) {
const sized = await page.evaluate(() => [...document.querySelectorAll('main table')]
.filter((table) => table.querySelector('colgroup'))
.map((table) => ({
header: [...table.tHead.rows[0].cells].map((cell) => cell.textContent.trim()),
widths: [...table.querySelectorAll('colgroup col')].map((col) => parseFloat(col.style.width))
})));
await fs.mkdir(path.dirname(process.env.SDLC_WIDTHS_FILE), { recursive: true });
await fs.writeFile(process.env.SDLC_WIDTHS_FILE, `${JSON.stringify(sized)}\n`);
}
for (const what of clipped) console.error(`WARNING ${path.basename(source)}: ${what} does not fit its column`);
// Unbreakable IDs can make a table wider than the page; say so rather than print it clipped.
const overflowMm = await page.evaluate((mm) => {
const main = document.querySelector('main.delivery-document');
main.style.width = `${mm}mm`; // the screen viewport is wider than the printed text block
const over = Math.max(0, ...[...main.querySelectorAll('table')].map((table) => (table.scrollWidth - main.clientWidth) / (96 / 25.4)));
main.style.width = '';
return over;
}, textMm);
if (overflowMm > 1) console.error(`WARNING ${path.basename(source)}: a table is ${overflowMm.toFixed(0)}mm wider than the page`);
await page.pdf({
path: destination,
format: 'A4',
printBackground: true,
preferCSSPageSize: true,
displayHeaderFooter: true,
headerTemplate: `${templateFont}${pdfHeader}`,
footerTemplate: `${templateFont}${pdfFooter}`
headerTemplate: `${templateFont}${headerFor(landscape)}`,
footerTemplate: `${templateFont}${footerFor(landscape)}`
});
} finally {
await browser.close();
}
}
/**
* True when a table needs more than the 160mm portrait text block. Each column
* is given its one-line width, capped at COLUMN_CAP_MM for prose columns that
* are meant to wrap; when those widths add up to more than the page, portrait
* would stack short values (IDs, dates, statuses) one word per line.
*/
const PORTRAIT_TEXT_MM = 160;
const COLUMN_CAP_MM = Number(process.env.SDLC_COLUMN_CAP_MM ?? 30);
async function tableWidthNeededMm(page) {
return page.evaluate((capMm) => {
const mm = 96 / 25.4;
const main = document.querySelector('main.delivery-document');
const probe = document.createElement('style');
probe.textContent = '.width-probe { position: absolute; left: -10000mm; width: max-content; } '
+ '.width-probe table { width: auto !important; } .width-probe th, .width-probe td { white-space: nowrap !important; }';
document.head.append(probe);
let widest = 0;
for (const table of main.querySelectorAll('table:not(.document-control)')) {
const holder = document.createElement('div');
holder.className = 'delivery-document width-probe';
holder.append(table.cloneNode(true));
document.body.append(holder);
const columns = [];
for (const row of holder.querySelector('table').rows) {
let index = 0;
for (const cell of row.cells) {
if (cell.colSpan === 1) columns[index] = Math.max(columns[index] ?? 0, cell.getBoundingClientRect().width / mm);
index += cell.colSpan;
}
}
holder.remove();
widest = Math.max(widest, columns.reduce((sum, w) => sum + Math.min(w ?? 0, capMm), 0));
}
probe.remove();
return widest;
}, COLUMN_CAP_MM);
}
/**
* Column widths proportional to content. Each column's share of the text width
* follows the average one-line width of its cells, but it never drops below the
* narrowest width that keeps its words and IDs whole, and never grows beyond its
* longest line; the widths are then fixed so the browser does not redistribute
* them. Returns the IDs or dates that still stick out of their cell.
*/
async function applyProportionalWidths(page, textMm) {
return page.evaluate(({ availMm, SHORT_COLUMN_MM, SHORT_CELL_CHARS }) => {
const mm = 96 / 25.4;
const main = document.querySelector('main.delivery-document');
const probe = document.createElement('style');
probe.textContent = '.width-probe { position: absolute; left: -10000mm; width: max-content; } '
+ '.width-probe table { width: auto !important; table-layout: auto !important; } '
+ '.width-probe.one-line th, .width-probe.one-line td { white-space: nowrap !important; } '
+ '.width-probe.narrowest table { width: 1px !important; } '
+ '.width-probe.narrowest th, .width-probe.narrowest td { overflow-wrap: normal !important; }';
document.head.append(probe);
// Per column: the width of every single-column cell's own content (plus its
// padding) in one probe layout. A cell's box is as wide as its whole column,
// so the content is measured with a range rather than the cell's rectangle.
const contentWidth = (cell) => {
const range = document.createRange();
range.selectNodeContents(cell);
const rects = [...range.getClientRects()];
const text = rects.length ? Math.max(...rects.map((r) => r.right)) - Math.min(...rects.map((r) => r.left)) : 0;
const style = getComputedStyle(cell);
// 1.2mm slack for the cell borders and glyphs that overhang their advance width.
return text + parseFloat(style.paddingLeft) + parseFloat(style.paddingRight) + 1.2 * mm;
};
const measure = (table, mode) => {
const holder = document.createElement('div');
holder.className = `delivery-document width-probe ${mode}`;
holder.append(table.cloneNode(true));
document.body.append(holder);
const columns = [];
for (const row of holder.querySelector('table').rows) {
let index = 0;
for (const cell of row.cells) {
if (cell.colSpan === 1) (columns[index] ??= []).push({ width: contentWidth(cell) / mm, head: cell.tagName === 'TH', short: cell.textContent.trim().length <= SHORT_CELL_CHARS });
index += cell.colSpan;
}
}
holder.remove();
return columns;
};
const tables = [...main.querySelectorAll('table:not(.document-control):not(.signature-table)')];
for (const table of tables) {
const line = measure(table, 'one-line');
const narrow = measure(table, 'narrowest');
const n = line.length;
if (n < 2 || [...line].some((c) => !c)) continue;
const longest = line.map((c) => Math.max(...c.map((x) => x.width)));
let floor = narrow.map((c, i) => Math.min(Math.max(...c.map((x) => x.width)), longest[i]));
// Short columns (IDs, dates, statuses) stay on one line when the page allows;
// the prose columns then share what is left in proportion to their content.
// So do short entries in any column - a status word, a list of initials.
const shortest = line.map((c) => Math.max(0, ...c.filter((x) => x.short && !x.head).map((x) => x.width)));
const oneLine = floor.map((f, i) => (longest[i] <= SHORT_COLUMN_MM ? longest[i] : Math.max(f, shortest[i])));
if (oneLine.reduce((a, b) => a + b, 0) < availMm) floor = oneLine;
const weight = line.map((c) => {
const body = c.filter((x) => !x.head);
const cells = body.length ? body : c;
return Math.max(1, cells.reduce((sum, x) => sum + x.width, 0) / cells.length);
});
let widths;
const floorSum = floor.reduce((a, b) => a + b, 0);
if (floorSum >= availMm) {
widths = floor.map((f) => (f * availMm) / floorSum);
} else {
// Water-filling: share by weight, pin columns that hit their floor or longest line, repeat.
const pinned = new Array(n).fill(null);
for (let changed = true; changed;) {
changed = false;
const free = availMm - pinned.reduce((a, w) => a + (w ?? 0), 0);
const open = [...Array(n).keys()].filter((i) => pinned[i] === null);
if (!open.length) break;
const total = open.reduce((a, i) => a + weight[i], 0);
for (const i of open) {
if ((free * weight[i]) / total < floor[i]) { pinned[i] = floor[i]; changed = true; }
}
if (changed) continue;
for (const i of open) {
if ((free * weight[i]) / total > longest[i]) { pinned[i] = longest[i]; changed = true; }
}
if (!changed) for (const i of open) pinned[i] = (free * weight[i]) / total;
}
widths = pinned;
// Every column fits on one line with room to spare: spread the rest by weight.
const used = widths.reduce((a, b) => a + b, 0);
if (used < availMm) {
const total = weight.reduce((a, b) => a + b, 0);
widths = widths.map((w, i) => w + ((availMm - used) * weight[i]) / total);
}
}
// Topic columns (CR Topic, SRS Topic, Unit Topic) share their combined width equally.
const heads = [...(table.tHead?.rows[0]?.cells ?? [])].map((cell) => cell.textContent.trim());
const topics = heads.map((h, i) => (/Topic$/i.test(h) ? i : -1)).filter((i) => i >= 0);
if (topics.length > 1 && heads.length === n) {
const even = topics.reduce((sum, i) => sum + widths[i], 0) / topics.length;
if (topics.every((i) => even >= floor[i])) for (const i of topics) widths[i] = even;
}
const colgroup = document.createElement('colgroup');
for (const w of widths) {
const col = document.createElement('col');
col.style.width = `${((w / availMm) * 100).toFixed(2)}%`;
colgroup.append(col);
}
table.prepend(colgroup);
table.style.tableLayout = 'fixed';
}
probe.remove();
// An ID or date wider than its column would print over the next cell.
main.style.width = `${availMm}mm`;
const clipped = [...main.querySelectorAll('td .nobr, th .nobr')]
.filter((span) => span.getBoundingClientRect().right > span.closest('td, th').getBoundingClientRect().right + 0.5)
.map((span) => span.textContent);
main.style.width = '';
return [...new Set(clipped)];
}, { availMm: textMm, SHORT_COLUMN_MM: 35, SHORT_CELL_CHARS: 16 });
}
main().catch((error) => { console.error(error); process.exit(1); });
+4 -3
View File
@@ -26,12 +26,13 @@ function compact(text) {
return text.normalize('NFC').toLocaleLowerCase().replace(/[^\p{L}\p{N}]/gu, '');
}
async function markdownFiles(directory) {
// Work products sit in the process folders; top-level files in sdlc/ are notes.
async function markdownFiles(directory, depth = 0) {
const entries = await fs.readdir(directory, { withFileTypes: true });
const results = await Promise.all(entries.map(async (entry) => {
const target = path.join(directory, entry.name);
if (entry.isDirectory()) return markdownFiles(target);
return entry.isFile() && entry.name.endsWith('.md') ? [target] : [];
if (entry.isDirectory()) return markdownFiles(target, depth + 1);
return depth > 0 && entry.isFile() && entry.name.endsWith('.md') ? [target] : [];
}));
return results.flat();
}
+3 -1
View File
@@ -74,7 +74,9 @@ async function main() {
const hindsight = hindsightErrors(docs, root);
if (hindsight.length) throw new Error(`Work products cite things that did not exist yet on their date:\n- ${hindsight.join('\n- ')}`);
await fs.rm(target, { recursive: true, force: true });
// Only the process folders are generated; files at the top of sdlc/ are
// working notes (e.g. the audit prep) and are neither seeded nor rendered.
for (const folder of FOLDERS) await fs.rm(path.join(target, folder), { recursive: true, force: true });
for (const folder of FOLDERS) await fs.mkdir(path.join(target, folder), { recursive: true });
for (const d of docs) {
await fs.writeFile(path.join(target, d.dir, `${d.name}.md`), d.md, 'utf8');
+21 -13
View File
@@ -1,8 +1,8 @@
// 3-Other Document: Stakeholder Register, Project Charter, Traceability Record Table, Training Report, List of Evidence.
import { P, TEAM, CODE, PROJECT_TH, COMPANY_TH, ORGANIZER_PM, RECORDER, START, END, SCOPE, CRS, TRAINING_DATE, ACCEPT_DATE, TEST_FROM, TEST_TO, BASELINE_COMMIT, ORIGIN, BACKUP, DEV_START } from './project.mjs';
import { SRS, UNITS, TEST_CASES, traceOf, srTopic, unitTopic } from './engineering.mjs';
import { P, TEAM, OTHER_STAKEHOLDERS, CODE, PROJECT_TH, COMPANY_TH, ORGANIZER_PM, RECORDER, START, END, SCOPE, CRS, TRAINING_DATE, ACCEPT_DATE, TEST_FROM, TEST_TO, BASELINE_COMMIT, ORIGIN, BACKUP, DEV_START } from './project.mjs';
import { SRS, UNITS, TEST_CASES, traceOf, srTopic, unitTopic, crStatus } from './engineering.mjs';
import { CORRECTIONS, PERIODS, MEETINGS, TASKS } from './history.mjs';
import { bud, thLong, thRangeLong, table, bullets, numbered, daysBetween } from './lib.mjs';
import { bud, thLong, thShort, thRangeLong, table, bullets, numbered, daysBetween } from './lib.mjs';
import { workProduct, head } from './format.mjs';
const OTHER = '3-Other Document';
@@ -27,12 +27,9 @@ export function stakeholderRegister() {
[2, P.ApS.th, P.ApS.id, 'Project Manager', 'วางแผนและควบคุมโครงการ ติดตามความก้าวหน้า บริหารความเสี่ยงและการเปลี่ยนแปลง', 'A (Accountable), R (Responsible)'],
[3, P.NoC.th, P.NoC.id, 'System Analyst', 'เก็บและวิเคราะห์ความต้องการ ออกแบบระบบและฐานข้อมูล จัดทำเอกสาร SRS และ Design', 'R (Responsible), C (Consult)'],
[4, P.ThS.th, P.ThS.id, 'Developer', 'พัฒนาระบบตามการออกแบบ ดูแลความปลอดภัย การติดตั้ง และการสำรองข้อมูล', 'R (Responsible), C (Consult)'],
[5, P.PaNg.th, P.PaNg.id, 'QA / Tester', 'จัดทำและดำเนินการทดสอบ ตรวจสอบ Work Products และติดตามการแก้ไขข้อบกพร่อง', 'R (Responsible), C (Consult)'],
[5, P.PaNg.th, P.PaNg.id, 'QA/Tester', 'จัดทำและดำเนินการทดสอบ ตรวจสอบ Work Products และติดตามการแก้ไขข้อบกพร่อง', 'R (Responsible), C (Consult)'],
[6, P.YaB.th, P.YaB.id, 'Document Control', 'ควบคุมรหัสเอกสาร เวอร์ชัน การอนุมัติ การจัดเก็บ และหลักฐานของโครงการ', 'R (Responsible)'],
[7, 'หัวหน้าฝ่ายคลังสินค้าและพนักงานคลัง', '—', 'ผู้ใช้งานหลัก', 'ให้ข้อมูลกระบวนการทำงาน ทดสอบการใช้งาน และใช้งานระบบจริง', 'C (Consult), R (Review)'],
[8, 'ฝ่ายขายและฝ่ายจัดซื้อ', '—', 'ผู้ใช้งาน', 'ให้ข้อมูลกระบวนการเอกสารขายและจัดซื้อ และทดสอบการใช้งาน', 'C (Consult)'],
[9, 'ฝ่ายบัญชีและการเงิน', '—', 'ผู้ใช้งาน', 'ให้ข้อมูลการวางบิล รับชำระ จ่ายชำระ และการบันทึกบัญชี', 'C (Consult)'],
[10, 'ผู้ดูแลระบบ (System Administrator)', '—', 'ผู้สนับสนุน', 'ติดตั้ง ตั้งค่า เฝ้าระวัง สำรองและกู้คืนระบบ', 'C (Consult), I (Inform)'],
...OTHER_STAKEHOLDERS.map(([name, role, duty, engagement], i) => [7 + i, name, '—', role, duty, engagement]),
], ['c', '', 'c', '', '', '']),
'หมายเหตุ: รหัสระดับการมีส่วนร่วม (Engagement Level) ได้แก่ A: Approve, R: Responsible, C: Consult, I: Inform ตามแนวทาง RACI Matrix เพื่อระบุระดับการมีส่วนร่วมในแต่ละบทบาท',
'## ช่องทางการสื่อสาร',
@@ -40,7 +37,7 @@ export function stakeholderRegister() {
[1, 'รายงานความก้าวหน้า (Progress Status Record)', 'ทุกงวดรายงาน', P.ApS.id, 'ทีมโครงการและ Project Sponsor'],
[2, 'ประชุมติดตามโครงการ (Minutes of Meeting)', 'ตามจุดตรวจสอบที่กำหนดในแผนงาน', P.ApS.id, 'ผู้เกี่ยวข้องทั้งหมด'],
[3, 'รายงานข้อบกพร่องและการแก้ไข', 'เมื่อพบข้อบกพร่อง', `${P.PaNg.id} / ${P.ThS.id}`, `${P.ApS.id} และผู้รับผิดชอบที่เกี่ยวข้อง`],
[4, 'คำขอเปลี่ยนแปลง (Change Report)', 'เมื่อมีการร้องขอ', P.ApS.id, `${P.SeV.id} และคณะพิจารณา`],
[4, 'คำขอเปลี่ยนแปลง (Change Report)', 'เมื่อมีการร้องขอ', P.ApS.id, `${P.SeV.id} (Project Sponsor)`],
], ['c']),
].join('\n\n'),
secretary: P.ApS, reviewer: P.YaB, approver: P.SeV,
@@ -117,10 +114,19 @@ export function projectCharter() {
/** Traceability Record Table — the summary matrix filed alongside the master record. */
export function traceabilityTable() {
const date = '2026-08-17';
// Evidence issued after this table is planned, not yet in hand.
const LATER = { 'Training Report': TRAINING_DATE, 'List of Evidence': END, 'Acceptance Report': ACCEPT_DATE };
const evidence = (ev) => ev.split(', ').map((name) => {
const at = LATER[name.replace(/ \(.*\)$/, '')];
return at && at > date ? `${name} (ตามแผน ${thShort(at)})` : name;
}).join(', ');
const rows = CRS.map(([id, text]) => {
const t = traceOf(id);
return [id, text, t.sr.join(', '), t.un.join(', ') || '-', t.tc, t.ev || '-', 'Passed', 'Passed'];
const status = crStatus(id, date);
const tested = t.tcs.length ? status : '-';
return [id, text, t.sr.join(', '), t.un.join(', ') || '-', t.tc, t.ev ? evidence(t.ev) : '-', tested, status];
});
const count = (s) => CRS.filter(([id]) => crStatus(id, date) === s).length;
const traced = CRS.map(([id]) => traceOf(id));
return {
dir: OTHER,
@@ -145,8 +151,10 @@ export function traceabilityTable() {
['เชื่อมโยงกับ Software Unit', traced.filter((t) => t.un.length).length],
['เชื่อมโยงกับ Test Case', traced.filter((t) => t.tcs.length).length],
['ยืนยันด้วยเอกสารส่งมอบ (ไม่มี Software Unit)', traced.filter((t) => !t.tcs.length).length],
['ผลการทดสอบหรือการตรวจสอบเอกสารผ่าน', CRS.length],
['ผลการทดสอบการยอมรับผ่าน', CRS.length],
['ผ่าน (Passed)', count('Passed')],
['ไม่ผ่าน (Failed) — ข้อบกพร่องคงค้างใน Correction Register', count('Failed')],
['ยังไม่ได้ทดสอบ (ชุดติดตั้ง Docker ส่งมอบหลังรอบทดสอบ)', count('Not tested')],
['รอดำเนินการตามแผน (Pending)', count('Pending')],
], ['', 'r']),
].join('\n\n'),
secretary: P.NoC, reviewer: P.PaNg, approver: P.SeV,
@@ -234,7 +242,7 @@ export function listOfEvidence(inventory) {
[3, `Progress Status Record (${files('3.Progress Status Record').length} Report)`, multi('3.Progress Status Record'), 'บันทึกความก้าวหน้าของโครงการรายงวด'],
[4, 'Correction Register', one('4.Correction Register'), 'เอกสารสรุปปัญหาที่พบระหว่างดำเนินโครงการ'],
[5, 'Acceptance Record', one('5.Acceptance Report'), 'บันทึกการส่งมอบงาน (Sign off Letter)'],
[6, `Change Requests (${files('6.Change Report').length} ฉบับ)`, multi('6.Change Report'), 'บันทึกขอการเปลี่ยนแปลงความต้องการ'],
[6, 'Change Request (Change Report)', multi('6.Change Report'), 'เกณฑ์และทะเบียนการพิจารณาคำขอเปลี่ยนแปลง (ไม่มีคำขอที่เข้าเกณฑ์ตลอดโครงการ)'],
[7, `Meeting Record (${files('7.Meeting Record').length} MoM Report)`, multi('7.Meeting Record'), 'รายงานการประชุมตามแผนการทำงานใน Work Schedule'],
[8, 'Software Configuration', one('8.Software Configuration'), 'บันทึกการกำหนดค่าและความเปลี่ยนแปลงต่าง ๆ ที่เกิดขึ้นของระบบ'],
[9, 'Project Repository', one('9.Project Repository'), `พื้นที่เก็บข้อมูล เอกสาร และหลักฐานทุกอย่างของโครงการ (Git Server: \`${ORIGIN}\`)`],
+10 -6
View File
@@ -1,5 +1,5 @@
// PM work products 1–2: Statement of Work, Work Schedule, Software Project Plan, Customer Requirements.
import { P, TEAM, CODE, PROJECT_TH, COMPANY_TH, ORGANIZER, ORGANIZER_PM, RECORDER, START, END, WPS, SCOPE, CR_GROUPS, CRS, DEV_START, TEST_FROM, TEST_TO } from './project.mjs';
import { P, TEAM, OTHER_STAKEHOLDERS, CODE, PROJECT_TH, COMPANY_TH, ORGANIZER, ORGANIZER_PM, RECORDER, START, END, WPS, SCOPE, CR_GROUPS, CRS, DEV_START, TEST_FROM, TEST_TO } from './project.mjs';
import { TASKS, VERIFICATION_ROUNDS, VERIFICATION_RULES } from './history.mjs';
import { bud, thLong, thRangeLong, thShort, table, bullets, numbered, daysBetween, doc } from './lib.mjs';
import { workProduct, head } from './format.mjs';
@@ -101,7 +101,7 @@ export function workSchedule() {
['เริ่มพัฒนาระบบ', thLong(DEV_START), 'เริ่ม Task 3.1 ตามแผน'],
['Baseline การพัฒนา', thLong('2026-05-29'), 'พัฒนาครบทุกโมดูลตามขอบเขต และกำหนด Git Baseline การพัฒนา'],
['ทดสอบระบบและ UAT', thRangeLong(TEST_FROM, TEST_TO), `Test Case ผ่านครบ และ UAT ผ่านครบ 14 หมวดความต้องการ (${CRS.length} รายการ)`],
['ตรวจรับส่งมอบระบบ', thLong('2026-08-17'), 'Project Sponsor ลงนามตรวจรับสิ่งส่งมอบครบทุกรายการ'],
['ตรวจรับส่งมอบระบบ', thLong('2026-08-24'), 'Project Sponsor ลงนามตรวจรับสิ่งส่งมอบครบทุกรายการ'],
['อบรมผู้ใช้งาน', thLong('2026-08-22'), 'ผู้ใช้งานทุกฝ่ายที่เกี่ยวข้องผ่านการอบรม'],
['ปิดโครงการ', thLong(END), 'ปิดโครงการอย่างเป็นทางการ'],
], ['', 'c', '']),
@@ -201,10 +201,14 @@ export function softwareProjectPlan() {
[1, `Project Manager (${P.ApS.id})`, P.ApS.th, P.ApS.email, 'จัดทำ Project Charter และแผนโครงการ / วางแผน Timeline, Resource, Risk, Quality / จัดประชุมและติดตามความคืบหน้า / ควบคุมการเปลี่ยนแปลงและการปิดโครงการ'],
[2, `System Analyst (${P.NoC.id})`, P.NoC.th, P.NoC.email, 'เก็บและวิเคราะห์ความต้องการ / จัดทำ SRS และเอกสารออกแบบระบบ / ออกแบบฐานข้อมูลและความสัมพันธ์ของโมดูล / ตรวจสอบความสอดคล้องของงานพัฒนากับความต้องการ'],
[3, `Developer (${P.ThS.id})`, P.ThS.th, P.ThS.email, 'พัฒนาโมดูลและ API ตามการออกแบบ / จัดการฐานข้อมูลและประสิทธิภาพ / ดูแลความปลอดภัยและการติดตั้ง / แก้ไขข้อบกพร่องที่พบจากการทดสอบ'],
[4, `QA / Tester (${P.PaNg.id})`, P.PaNg.th, P.PaNg.email, 'จัดทำ Test Case และ Test Procedures / ทดสอบระบบและบันทึกผล / ติดตามการแก้ไขข้อบกพร่องและทดสอบซ้ำ / สนับสนุนผู้ใช้งานช่วง UAT'],
[4, `QA/Tester (${P.PaNg.id})`, P.PaNg.th, P.PaNg.email, 'จัดทำ Test Case และ Test Procedures / ทดสอบระบบและบันทึกผล / ติดตามการแก้ไขข้อบกพร่องและทดสอบซ้ำ / สนับสนุนผู้ใช้งานช่วง UAT'],
[5, `Document Control (${P.YaB.id})`, P.YaB.th, P.YaB.email, 'ควบคุมรหัสเอกสารและเวอร์ชัน / จัดเก็บและตรวจสอบความครบถ้วนของเอกสาร / ประสานการทบทวนและอนุมัติ / บันทึกประวัติการเปลี่ยนแปลงเอกสาร'],
[6, `Project Sponsor (${P.SeV.id})`, P.SeV.th, P.SeV.email, 'อนุมัติขอบเขต งบประมาณ และการเปลี่ยนแปลง / ตัดสินใจเชิงกลยุทธ์ / ทดสอบการยอมรับและตรวจรับส่งมอบ / อนุมัติปิดโครงการ'],
], ['c']),
'### 5.2 ผู้มีส่วนได้ส่วนเสียอื่น (Other Stakeholders)',
'ผู้มีส่วนได้ส่วนเสียนอกทีมโครงการตาม Stakeholder Register ซึ่งร่วมให้ข้อมูล ทดสอบ และรับการอบรมตามแผนงาน',
table(['ลำดับ', 'ผู้มีส่วนได้ส่วนเสีย', 'บทบาท', 'การมีส่วนร่วมในโครงการ', 'ระดับการมีส่วนร่วม'],
OTHER_STAKEHOLDERS.map(([name, role, duty, engagement], i) => [i + 1, name, role, duty, engagement]), ['c']),
sec(6, 'Project Estimate'),
'### 6.1 Size of the Software Work Products',
table(['ลำดับ', 'Work Product', 'ปริมาณ (ประมาณการ)'], [
@@ -221,7 +225,7 @@ export function softwareProjectPlan() {
[1, 'Project Manager', 'ควบคุมงาน ประสานงาน ติดตามแผน', 120],
[2, 'System Analyst', 'วิเคราะห์ความต้องการ ออกแบบระบบ', 60],
[3, 'Developer', 'พัฒนาระบบและแก้ไขข้อบกพร่อง', 100],
[4, 'QA / Tester', 'วางแผนทดสอบ ทดสอบระบบ UAT', 40],
[4, 'QA/Tester', 'วางแผนทดสอบ ทดสอบระบบ UAT', 40],
[5, 'Document Control', 'ควบคุมเอกสารและหลักฐานโครงการ', 60],
], ['c', '', '', 'r']),
sec(7, 'Project Resources'),
@@ -279,7 +283,7 @@ export function softwareProjectPlan() {
'แผนรองรับสำหรับกรณีงานไม่แล้วเสร็จตามกำหนด (Non-Completed Task)',
table(['ลำดับ', 'สถานการณ์', 'สาเหตุที่พบบ่อย', 'ผลกระทบที่อาจเกิด', 'แผนรองรับเมื่อเกิดเหตุ', 'ผู้รับผิดชอบ'], [
[1, 'งานไม่เสร็จภายในกำหนด (Due Date)', 'ประเมินเวลาผิดพลาด หรือทรัพยากรไม่เพียงพอ', 'กระทบ Timeline และกำหนดส่งมอบ', 'ประชุมด่วนเพื่อประเมินสถานะ จัดลำดับความสำคัญใหม่ ปรับแผน และแจ้งลูกค้าอย่างเป็นทางการ', P.ApS.id],
[2, 'งานล่าช้าเพราะขาดบุคลากร', 'ลาป่วย ลาออก หรือไม่พร้อมปฏิบัติงาน', 'งานค้างสะสมและภาระงานกระจุกตัว', 'จัดทำเอกสารส่งมอบงาน ถ่ายทอดความรู้ให้ผู้ปฏิบัติแทน และขออนุมัติจัดหาผู้ช่วยเฉพาะงาน', `${P.ApS.id} / ฝ่ายบุคคล`],
[2, 'งานล่าช้าเพราะขาดบุคลากร', 'ลาป่วย ลาออก หรือไม่พร้อมปฏิบัติงาน', 'งานค้างสะสมและภาระงานกระจุกตัว', 'จัดทำเอกสารส่งมอบงาน ถ่ายทอดความรู้ให้ผู้ปฏิบัติแทน และขออนุมัติจัดหาผู้ช่วยเฉพาะงาน', `${P.ApS.id} / ${P.SeV.id}`],
[3, 'งานค้างเพราะรอข้อมูลจากผู้ใช้', 'ผู้ให้ข้อมูลส่งความต้องการหรือข้อมูลตั้งต้นล่าช้า', 'ไม่สามารถพัฒนาหรือทดสอบต่อได้', 'แจ้งเตือนเป็นลายลักษณ์อักษร แยกงานที่ยังไม่พร้อมออก และนัดยืนยัน Timeline ใหม่', `${P.ApS.id} / ${P.SeV.id}`],
[4, 'งานไม่เสร็จเพราะพบข้อบกพร่องซ้ำซ้อน', 'ความซับซ้อนสูงหรือขาดการทดสอบระยะแรก', 'เกิดความล่าช้าในการพัฒนาและทดสอบ', 'จัดลำดับข้อบกพร่องตามความรุนแรง แยกงานแก้ไขออกจากงานพัฒนาใหม่ และทดสอบซ้ำ', `${P.ThS.id} / ${P.PaNg.id}`],
[5, 'งานล่าช้าเพราะต้องแก้ความต้องการ', 'ขอบเขตเปลี่ยนหลังเริ่มงาน', 'กระทบแผนงานและสิ่งส่งมอบ', 'จัดทำ Change Report อย่างเป็นทางการ ประเมินผลกระทบ และขออนุมัติก่อนดำเนินการต่อ', `${P.ApS.id} / ${P.SeV.id}`],
@@ -344,7 +348,7 @@ export function customerRequirements() {
`เอกสารฉบับนี้จัดทำขึ้นเพื่อบันทึกและสรุปความต้องการของลูกค้า (Customer Requirements) ตามกระบวนการ ISO/IEC 29110 สำหรับใช้เป็นพื้นฐานในการจัดทำ Software Requirements Specification, การออกแบบระบบ, การทดสอบ และการตรวจรับ ของ${PROJECT_TH}`,
'## ผู้มีส่วนได้ส่วนเสีย (Stakeholders)',
table(['ลำดับ', 'ชื่อ', 'ตำแหน่ง', 'บทบาท'], [
[1, P.SeV.th, 'ผู้บริหาร (CEO)', 'Project Sponsor อนุมัติและตัดสินใจเชิงกลยุทธ์'],
[1, P.SeV.th, P.SeV.pos, 'Project Sponsor อนุมัติและตัดสินใจเชิงกลยุทธ์'],
[2, P.ApS.th, 'ผู้จัดการโครงการ', 'บริหารโครงการและควบคุมการเปลี่ยนแปลง'],
[3, P.NoC.th, 'นักวิเคราะห์ระบบ', 'เก็บและวิเคราะห์ความต้องการ จัดทำเอกสาร'],
[4, 'หัวหน้าฝ่ายคลังสินค้าและพนักงานคลัง', 'ผู้ใช้งานหลัก', 'ให้ข้อมูลกระบวนการรับเข้า จ่ายออก โอนย้าย และตรวจนับ'],
+37 -17
View File
@@ -4,6 +4,7 @@ import { P, CRS, CODE, PROJECT_TH, COMPANY_TH, ORGANIZER, ORGANIZER_PM, RECORDER
import { TASKS, taskById, CORRECTIONS, CHANGE_CRITERIA, CHANGE_REVIEW, LESSONS, PERIODS, MEETINGS } from './history.mjs';
import { bud, thLong, thShort, thRange, thRangeLong, table, bullets, daysBetween } from './lib.mjs';
import { workProduct, head } from './format.mjs';
import { TEST_CASES, resultCounts, crStatus } from './engineering.mjs';
const ids = (arr) => arr.map((i) => P[i].id).join(', ');
const dir3 = '1-PM Process (10 Work Product)/3.Progress Status Record';
@@ -20,7 +21,9 @@ export function progressStatusRecords() {
// detail, owner and status.
const issues = issueIds.map((iid, n) => {
const c = CORRECTIONS.find((x) => x[0] === iid);
return [n + 1, `${c[0]} : ${c[4]}`, `${c[5]} จึงดำเนินการ${c[6]} แล้วเสร็จ ${thShort(c[2])}`, P[c[7]].id, 'เสร็จสิ้น'];
return c[2]
? [n + 1, `${c[0]} : ${c[4]}`, `${c[5]} จึงดำเนินการ${c[6]} แล้วเสร็จ ${thShort(c[2])}`, P[c[7]].id, 'เสร็จสิ้น']
: [n + 1, `${c[0]} : ${c[4]}`, `${c[5]} แนวทางแก้ไข: ${c[6]}`, P[c[7]].id, 'คงค้าง'];
});
if (changeIds.length) throw new Error(`Progress record ${date} lists change requests, but the project has none.`);
const changes = [];
@@ -60,7 +63,8 @@ export function progressStatusRecords() {
/** 4. Correction Register — all issues found during the project. */
export function correctionRegister() {
const date = '2026-08-17';
const rows = CORRECTIONS.map((c, i) => [i + 1, c[0], thShort(c[1]), c[4], c[5], c[6], P[c[7]].id, thShort(c[2]), 'เสร็จสิ้น']);
const rows = CORRECTIONS.map((c, i) => [i + 1, c[0], thShort(c[1]), c[4], c[5], c[6], P[c[7]].id, c[2] ? thShort(c[2]) : '-', c[2] ? 'เสร็จสิ้น' : 'คงค้าง']);
const open = CORRECTIONS.filter((c) => !c[2]);
const bySev = (s) => CORRECTIONS.filter((c) => c[3] === s).length;
return {
dir: '1-PM Process (10 Work Product)/4.Correction Register',
@@ -82,16 +86,16 @@ export function correctionRegister() {
['ระดับความรุนแรงสูง', bySev('สูง')],
['ระดับความรุนแรงกลาง', bySev('กลาง')],
['ระดับความรุนแรงต่ำ', bySev('ต่ำ')],
['แก้ไขแล้วเสร็จและตรวจสอบผลแล้ว', CORRECTIONS.length],
['คงค้าง', 0],
['แก้ไขแล้วเสร็จและตรวจสอบผลแล้ว', CORRECTIONS.length - open.length],
['คงค้าง', open.length],
], ['', 'r']),
`ปัญหาทุกรายการได้รับการแก้ไขและตรวจสอบผลด้วย Test Case ที่เกี่ยวข้อง โดยผลการทดสอบผ่านทั้งหมดในรอบทดสอบระหว่าง ${thRangeLong(TEST_FROM, TEST_TO)} ไม่มีข้อบกพร่องระดับวิกฤตคงค้าง ณ วันตรวจรับ`,
`ปัญหาที่พบระหว่างพัฒนา ${CORRECTIONS.length - open.length} รายการได้รับการแก้ไขและตรวจสอบผลแล้ว รอบทดสอบระหว่าง ${thRangeLong(TEST_FROM, TEST_TO)} พบข้อบกพร่องระดับสูงคงค้าง ${open.length} รายการ (${open.map((c) => c[0]).join(', ')}) ซึ่งต้องแจ้งผู้รับมอบและกำหนดการแก้ไขก่อนตรวจรับ`,
'## บทเรียนและแนวทางป้องกันสำหรับโครงการถัดไป (Lessons Learned)',
'เมื่อปิดโครงการ ทีมงานทบทวนปัญหาทั้งหมดในทะเบียนนี้ จัดกลุ่มตามสาเหตุ และกำหนดแนวทางป้องกันเพื่อไม่ให้ปัญหาลักษณะเดียวกันเกิดซ้ำในโครงการถัดไป',
table(['ลำดับ', 'กลุ่มปัญหา', 'เลขที่ปัญหา', 'จำนวน', 'สิ่งที่เกิดขึ้น', 'แนวทางป้องกัน (Preventive Action)'],
LESSONS.map(([theme, issueIds, what, action], i) => [i + 1, theme, issueIds.join(', '), issueIds.length, what, action]), ['c', '', '', 'r', '', '']),
'## การเชื่อมโยงกับหลักฐานการทดสอบ',
table(['เลขที่', 'การแก้ไขอ้างอิง (Commit)', 'Test Case ที่ใช้ตรวจสอบ', 'ผลการทดสอบ'], CORRECTIONS.map((c) => [c[0], `\`${c[8]}\``, c[9], 'Passed']), ['c', '', 'c', 'c']),
table(['เลขที่', 'การแก้ไขอ้างอิง (Commit)', 'Test Case ที่ใช้ตรวจสอบ', 'ผลการทดสอบ'], CORRECTIONS.map((c) => [c[0], c[8] ? `\`${c[8]}\`` : '-', c[9], c[2] ? 'Passed' : 'Failed (คงค้าง)']), ['c', '', 'c', 'c']),
].join('\n\n'),
secretary: P.YaB, reviewer: P.ApS, approver: P.SeV,
}),
@@ -100,6 +104,10 @@ export function correctionRegister() {
/** 5. Acceptance Report — the sign-off record. */
export function acceptanceReport() {
const open = CORRECTIONS.filter((c) => !c[2]);
const count = resultCounts();
const failedCrs = CRS.filter(([id]) => crStatus(id, ACCEPT_DATE) === 'Failed');
const untestedCrs = CRS.filter(([id]) => crStatus(id, ACCEPT_DATE) === 'Not tested');
return {
dir: '1-PM Process (10 Work Product)/5.Acceptance Report',
name: `${CODE} Acceptance Report ${bud(ACCEPT_DATE)} V1.0 ApS`,
@@ -115,26 +123,27 @@ export function acceptanceReport() {
body: [
'## รายการสิ่งส่งมอบ (Work Products)',
table(['ลำดับ', 'รหัส', 'สิ่งส่งมอบ (Work Product)', 'รายละเอียด', 'ผลการตรวจรับ (Accepted / Not Accepted)'],
WPS.map((w, i) => [i + 1, w[0], w[1], w[0] === 'WP 9.0' ? `${w[2]} (Git baseline \`${BASELINE_COMMIT}\`)` : w[2], 'Accepted']), ['c', 'c', '', '', 'c']),
WPS.map((w, i) => [i + 1, w[0], w[1], w[0] === 'WP 9.0' ? `${w[2]} (Git baseline \`${BASELINE_COMMIT}\`)` : w[2], w[0] === 'WP 9.0' ? 'Accepted (มีเงื่อนไข)' : 'Accepted']), ['c', 'c', '', '', 'c']),
'## ผลการตรวจสอบตามเกณฑ์การตรวจรับ',
table(['ลำดับ', 'เกณฑ์การตรวจรับ', 'หลักฐานอ้างอิง', 'ผลการตรวจสอบ'], [
[1, 'ฟังก์ชันในขอบเขตทำงานตรงตาม Customer Requirements ครบทุกรายการ', 'Traceability Record, Test Report', 'ผ่าน'],
[2, 'ทดสอบระบบตาม Test Case ครบถ้วน', `Test Report 45 Test Case ผ่านทั้งหมด (${thRangeLong(TEST_FROM, TEST_TO)})`, 'ผ่าน'],
[3, 'ทดสอบการยอมรับโดยผู้ใช้ (UAT)', `Validation Results ครบ 14 หมวดความต้องการ (${CRS.length} รายการ) ผ่านทั้งหมด`, 'ผ่าน'],
[4, 'ไม่มีข้อบกพร่องระดับวิกฤตคงค้าง', `Correction Register ${CORRECTIONS.length} รายการ แก้ไขและตรวจสอบครบ`, 'ผ่าน'],
[1, 'ฟังก์ชันในขอบเขตทำงานตรงตาม Customer Requirements ครบทุกรายการ', `Traceability Record Table, Validation Results: ไม่ผ่าน ${failedCrs.length} รายการ ยังไม่ได้ทดสอบ ${untestedCrs.length} รายการ`, 'ไม่ผ่าน (มีเงื่อนไข)'],
[2, 'ทดสอบระบบตาม Test Case ครบถ้วน', `Test Report ผ่าน ${count.Passed} ไม่ผ่าน ${count.Failed} ยังไม่ได้ทดสอบ ${count['Not tested']} จาก ${TEST_CASES.length} Test Case (${thRangeLong(TEST_FROM, TEST_TO)})`, 'ไม่ผ่าน (มีเงื่อนไข)'],
[3, 'ทดสอบการยอมรับโดยผู้ใช้ (UAT)', `Validation Results ทดสอบครบ ${CRS.length} รายการ ผลตามข้อ 1`, 'ไม่ผ่าน (มีเงื่อนไข)'],
[4, 'ไม่มีข้อบกพร่องระดับวิกฤตคงค้าง', `Correction Register ข้อบกพร่องระดับสูงคงค้าง ${open.map((c) => c[0]).join(', ')}`, 'ไม่ผ่าน (มีเงื่อนไข)'],
[5, 'เอกสารคู่มือผู้ใช้ ผู้ดูแลระบบ และการบำรุงรักษาครบถ้วน', 'Software User Document, Product Operation Guide, Maintenance Document', 'ผ่าน'],
[6, 'Source Code และเอกสารจัดเก็บใน Repository พร้อมชุดสำรอง', 'Project Repository, Project Repository (Backup)', 'ผ่าน'],
[7, 'ผลการตรวจสอบ Work Products โดยผู้ตรวจสอบอิสระ', 'Verification Results รอบที่ 4', 'ผ่าน'],
[7, 'ผลการตรวจสอบ Work Products (Verification)', 'Verification Results รอบที่ 4: ไม่ผ่าน 2 รายการ (ผลการทดสอบ และชุดติดตั้ง Docker)', 'ไม่ผ่าน (มีเงื่อนไข)'],
], ['c', '', '', 'c']),
'## สรุปผลการตรวจรับ',
table(['รายการ', 'ผลสรุป'], [
['จำนวนสิ่งส่งมอบทั้งหมด', `${WPS.length} รายการ`],
['ตรวจรับแล้ว (Accepted)', `${WPS.length} รายการ`],
['ตรวจรับแล้ว (Accepted)', `${WPS.length - 1} รายการ`],
['ตรวจรับแบบมีเงื่อนไข', '1 รายการ (WP 9.0 Software)'],
['ไม่ผ่านการตรวจรับ (Not Accepted)', '0 รายการ'],
['ข้อบกพร่องคงค้าง', 'ไม่มี'],
['ผลการตรวจรับโดยรวม', '**Accepted** — ตรวจรับส่งมอบระบบครบถ้วน'],
['ข้อบกพร่องคงค้าง', `${open.length} รายการ (${open.map((c) => c[0]).join(', ')})`],
['ผลการตรวจรับโดยรวม', '**Accepted with conditions** — ตรวจรับพร้อมเงื่อนไขแก้ไขข้อบกพร่องคงค้าง'],
]),
`ผู้รับมอบได้ตรวจสอบสิ่งส่งมอบทั้งหมดตามเกณฑ์การตรวจรับที่กำหนดในเอกสาร Statement of Work และ Software Project Plan แล้วเห็นว่าครบถ้วนถูกต้อง จึงมีมติตรวจรับส่งมอบระบบเมื่อวันที่ ${thLong(ACCEPT_DATE)} โดยงานอบรมผู้ใช้งานและการปิดงานควบคุมปฏิบัติการจะดำเนินการให้แล้วเสร็จภายในวันที่ ${thLong(END)} ตามแผนปิดโครงการ`,
`ผู้รับมอบได้ตรวจสอบสิ่งส่งมอบทั้งหมดตามเกณฑ์การตรวจรับในเอกสาร Statement of Work และ Software Project Plan แล้ว รับทราบว่ามีข้อบกพร่องระดับสูงคงค้าง ${open.length} รายการ และชุดติดตั้ง Docker ที่ยังไม่ได้ทดสอบ จึงมีมติตรวจรับส่งมอบระบบแบบมีเงื่อนไขเมื่อวันที่ ${thLong(ACCEPT_DATE)} โดยเงื่อนไขคือ (1) แก้ไข ${open.map((c) => c[0]).join(', ')} และทดสอบซ้ำภายใต้การบำรุงรักษาแบบแก้ไข (Corrective) ตาม Maintenance Document และ (2) ทดสอบ TC-UN13.002 และ TC-UN13.003 บน Baseline ก่อนใช้ชุดติดตั้ง Docker ในสภาพแวดล้อมใช้งานจริง`,
].join('\n\n'),
secretary: P.ApS, reviewer: P.YaB, approver: P.SeV,
}),
@@ -187,6 +196,17 @@ export function changeReports() {
}];
}
/**
* Each Progress Status Record is presented at the meeting its "Next Meeting" names;
* the last one, issued at closure, at the closing meeting. The minutes attach them.
*/
const presentedAt = (period) => period[7]?.date ?? MEETINGS.at(-1).date;
const attachments = (m) => {
const records = PERIODS.filter((p) => presentedAt(p) === m.date).map((p) => thShort(p[0]));
const listed = records.length ? [`Progress Status Record ${records.join(', ')}`] : [];
return [...listed, ...(m.attachment === '-' ? [] : [m.attachment])].join(', ') || '-';
};
/** 7. Meeting Record — Minutes of Meeting per checkpoint. */
export function meetingRecords() {
return MEETINGS.map((m) => {
@@ -202,7 +222,7 @@ export function meetingRecords() {
['Task ID / Task Name', `${m.taskId} ${m.taskName}`],
['Work Period of Task', thRangeLong(t[5], t[6])],
['Meeting Date', thLong(m.date), 'Time', m.time],
['สิ่งแนบ', m.attachment],
['สิ่งแนบ', attachments(m)],
['Location', `ห้องประชุม ${COMPANY_TH}`],
['Organizer', ORGANIZER],
['Recorder', RECORDER],
+34 -27
View File
@@ -1,6 +1,6 @@
// SI work products 11–22.
import { P, CODE, PROJECT_TH, ORGANIZER, ORGANIZER_PM, RECORDER, START, END, SCOPE, CRS, crTopic, TEST_FROM, TEST_TO, ACCEPT_DATE, DEV_BASELINE, BASELINE_COMMIT, TEST_STATE_COMMIT, ORIGIN, BACKUP } from './project.mjs';
import { SR_GROUPS, SRS, srTopic, UNIT_GROUPS, UNITS, unitTopic, unitModule, TEST_CASES, traceOf } from './engineering.mjs';
import { SR_GROUPS, SRS, srTopic, UNIT_GROUPS, UNITS, unitTopic, unitModule, TEST_CASES, traceOf, testResult, resultCounts, crStatus } from './engineering.mjs';
import { CORRECTIONS } from './history.mjs';
import { figure } from './figures.mjs';
import { bud, thLong, thShort, thRange, thRangeLong, table, bullets, numbered } from './lib.mjs';
@@ -32,7 +32,7 @@ export function softwareRequirements() {
'เพื่อแปลงความต้องการของลูกค้าที่ได้รับอนุมัติแล้วให้เป็นความต้องการเชิงเทคนิคของซอฟต์แวร์ ครอบคลุมมาตรฐาน โครงสร้าง ส่วนประกอบ ความสัมพันธ์ ประสิทธิภาพ ส่วนเชื่อมต่อ ความปลอดภัย ฐานข้อมูล และการจัดการข้อผิดพลาด เพื่อใช้เป็นเอกสารอ้างอิงหลักในการออกแบบระบบ พัฒนา และทดสอบ',
'## ผู้มีส่วนได้ส่วนเสีย (Stakeholders)',
table(['ลำดับ', 'ชื่อ', 'หน่วยงาน/ตำแหน่ง', 'บทบาท'], [
[1, P.SeV.th, 'ผู้บริหาร (CEO)', 'Project Sponsor อนุมัติความต้องการ'],
[1, P.SeV.th, P.SeV.pos, 'Project Sponsor อนุมัติความต้องการ'],
[2, P.ApS.th, 'ผู้จัดการโครงการ', 'บริหารจัดการโครงการ'],
[3, P.NoC.th, 'นักวิเคราะห์ระบบ', 'จัดทำและควบคุมความต้องการซอฟต์แวร์'],
[4, P.ThS.th, 'นักพัฒนาระบบ', 'พัฒนาระบบตามความต้องการ'],
@@ -81,7 +81,7 @@ export function softwareDesign() {
figure('useCase'),
table(['Actor', 'คำอธิบาย', 'การใช้งานหลัก'], [
['Owner', 'เจ้าของบริษัท สิทธิ์สูงสุดภายในบริษัท', 'ตั้งค่าบริษัท จัดการผู้ใช้ และใช้งานทุกโมดูล'],
['Admin', 'ผู้ดูแลระบบภายในบริษัท', 'จัดการข้อมูลหลัก ผู้ใช้ สิทธิ์ และใช้งานทุกโมดูลปฏิบัติการ'],
['Admin', 'ผู้ดูแลระบบภายในบริษัท', 'จัดการข้อมูลหลักและใช้งานทุกโมดูลปฏิบัติการ (การจัดการผู้ใช้และสิทธิ์เป็นของ Owner)'],
['Staff', 'ผู้ปฏิบัติงาน', 'บันทึกรายการคลังสินค้า ขาย จัดซื้อ และการเงินตามสิทธิ์'],
['Viewer', 'ผู้ใช้แบบอ่านอย่างเดียว', 'ดูหน้าจอและรายงานที่ได้รับสิทธิ์'],
['System Scheduler', 'ผู้กระทำอัตโนมัติ (Node.js)', 'สรุปยอดสต๊อก/GL และแจ้งเตือนตามกำหนดเวลา'],
@@ -155,20 +155,21 @@ export function softwareDesign() {
};
}
/** 13. Traceability Record. */
/** 13. Traceability Record — completed once the test cases exist, so it is issued with them. */
export function traceabilityRecord() {
const date = '2026-03-06';
const date = '2026-07-31';
const rows = [];
let group = '';
for (const [id, text] of CRS) {
const g = id.slice(0, 4);
if (g !== group) { group = g; const name = SCOPEGROUP(g); rows.push([`${g}: ${name}`, '', '', '', '', '', '']); }
const t = traceOf(id);
rows.push([id, text, t.sr.join(', '), t.sr.map(srTopic).join(' / '), t.un.join(', ') || '-', t.un.map(unitTopic).join(' / ') || '-', t.ev || '-']);
rows.push([id, text, t.sr.join(', '), t.sr.map(srTopic).join(' / '), t.un.join(', ') || '-', t.un.map(unitTopic).join(' / ') || '-', t.tc]);
}
const traced = CRS.map(([id]) => traceOf(id));
const withUnits = traced.filter((t) => t.un.length).length;
const byDocument = traced.filter((t) => !t.un.length).length;
const withTests = traced.filter((t) => t.tcs.length).length;
return {
dir: '2-SI Process (12 Work Product)/13.Traceability record',
name: `${CODE} Traceability Record ${bud(date)} V1.0 NoC`,
@@ -182,20 +183,21 @@ export function traceabilityRecord() {
]),
body: [
'## วัตถุประสงค์ (Objective)',
'เอกสารที่เชื่อมโยงความต้องการของลูกค้า → ความต้องการซอฟต์แวร์ → การออกแบบ (Software Unit) ณ วันที่ออกแบบระบบ เพื่อยืนยันความครบถ้วนของ Customer Requirements ก่อนเริ่มพัฒนา และลดความเสี่ยงของความต้องการที่ตกหล่น การสอบกลับไปยัง Test Case และผลการทดสอบจะเพิ่มใน Traceability Record ฉบับส่งมอบ หลังจัดทำ Test Case and Test Procedures',
'เอกสารที่เชื่อมโยงความต้องการของลูกค้า → ความต้องการซอฟต์แวร์ → การออกแบบ (Software Unit) → Test Case จัดทำพร้อม Test Case and Test Procedures เพื่อยืนยันว่าความต้องการทุกรายการมีการออกแบบและการทดสอบรองรับก่อนเริ่มทดสอบระบบ ผลการทดสอบสรุปใน Traceability Record Table ฉบับส่งมอบ',
'## Traceability Matrix',
table(['CR ID', 'CR Topic', 'SRS ID', 'SRS Topic', 'Unit ID', 'Unit Topic', 'หลักฐานอื่น (Document)'], rows),
'หมายเหตุ: ความต้องการที่เป็นเอกสาร การอบรม หรือหลักฐานด้านคุณภาพ สอบกลับไปยังเอกสารส่งมอบที่เป็นหลักฐานในคอลัมน์ "หลักฐานอื่น" แทน Software Unit',
table(['CR ID', 'CR Topic', 'SRS ID', 'SRS Topic', 'Unit ID', 'Unit Topic', 'Test Case ID'], rows),
'หมายเหตุ: ความต้องการที่เป็นเอกสาร การอบรม หรือหลักฐานด้านคุณภาพไม่มี Software Unit และ Test Case จึงยืนยันด้วยเอกสารส่งมอบตามแผน ซึ่งสรุปไว้ใน Traceability Record Table ฉบับส่งมอบ',
'## สรุปความครอบคลุม',
table(['รายการ', 'จำนวน'], [
['ความต้องการของลูกค้าทั้งหมด (CR01–CR14)', CRS.length],
['เชื่อมโยงกับความต้องการซอฟต์แวร์ (SRS)', CRS.length],
['เชื่อมโยงกับ Software Unit', withUnits],
['ยืนยันด้วยเอกสารส่งมอบ (ไม่มี Software Unit)', byDocument],
['เชื่อมโยงกับ Test Case', withTests],
['ยืนยันด้วยเอกสารส่งมอบตามแผน (ไม่มี Software Unit)', byDocument],
['ความต้องการซอฟต์แวร์ทั้งหมด (SR01–SR09)', SRS.length],
['Software Unit ทั้งหมด', UNITS.length],
], ['', 'r']),
'ความต้องการทุกรายการเชื่อมโยงกับความต้องการซอฟต์แวร์ และเชื่อมโยงกับ Software Unit หรือเอกสารส่งมอบที่เป็นหลักฐาน Test Case ของแต่ละ Software Unit จะกำหนดใน Test Case and Test Procedures (WP 6.0) ก่อนรอบทดสอบระบบ',
'ความต้องการทุกรายการเชื่อมโยงกับความต้องการซอฟต์แวร์ ความต้องการที่มี Software Unit เชื่อมโยงกับ Test Case ใน Test Case and Test Procedures (WP 6.0) ครบทุกรายการ ส่วนที่เหลือยืนยันด้วยเอกสารส่งมอบตามแผน',
].join('\n\n'),
secretary: P.NoC, reviewer: P.YaB, approver: P.SeV,
}),
@@ -297,10 +299,14 @@ export function testReport() {
const date = '2026-08-14';
const tested = CRS.filter(([id]) => traceOf(id).tcs.length);
const byDocument = CRS.length - tested.length;
const rows = tested.map(([id, text], i) => {
const t = traceOf(id);
return [i + 1, t.tc, `ตรวจสอบ ${t.un.map(unitTopic).join(' / ')}`, id, text, 'Passed', thRangeLong(TEST_FROM, TEST_TO)];
const count = resultCounts();
const crsOf = (tc) => CRS.filter(([id]) => traceOf(id).tcs.includes(tc)).map(([id]) => id).join(', ') || '-';
const rows = TEST_CASES.map(([tc, unit], i) => {
const r = testResult(tc);
return [i + 1, tc, unitTopic(unit), crsOf(tc), r.actual, r.status, r.status === 'Not tested' ? '-' : thRangeLong(TEST_FROM, TEST_TO)];
});
const failed = TEST_CASES.filter(([tc]) => testResult(tc).status === 'Failed');
const issues = [...new Set(failed.map(([tc]) => testResult(tc).issue))];
return {
dir: '2-SI Process (12 Work Product)/16.Test Report',
name: `${CODE} Test Report ${bud(date)} V1.0 PaNg`,
@@ -322,27 +328,28 @@ export function testReport() {
[2, 'Non-Functional Test', 'ประสิทธิภาพ, ความปลอดภัย (สิทธิ์/TLS/การเข้ารหัสรหัสผ่าน), การแยกข้อมูล, การสำรองและกู้คืน'],
[3, 'Environment', 'Internal Testing Server (PHP 8, MariaDB, Node.js)'],
[4, 'ช่วงเวลา', `รอบทดสอบระบบ ${thRangeLong(TEST_FROM, TEST_TO)}`],
[5, 'Baseline ที่ใช้ทดสอบ', `สถานะ Repository ที่ใกล้เคียงที่สุด Commit \`${TEST_STATE_COMMIT}\``],
[5, 'Build ที่ใช้ทดสอบ', `Commit \`${TEST_STATE_COMMIT}\` บน Branch main (การแก้ไขโค้ดล่าสุดก่อนรอบทดสอบ)`],
], ['c']),
'## สรุปผลการทดสอบ',
table(['รายการ', 'จำนวน'], [
['Test Case ที่กำหนดทั้งหมด', TEST_CASES.length],
['ดำเนินการทดสอบ', TEST_CASES.length],
['ผ่าน (Passed)', TEST_CASES.length],
['ไม่ผ่าน (Failed)', 0],
['ค้าง (Blocked / Pending)', 0],
['ดำเนินการทดสอบ', count.Passed + count.Failed],
['ผ่าน (Passed)', count.Passed],
['ไม่ผ่าน (Failed)', count.Failed],
['ยังไม่ได้ทดสอบ (ฟังก์ชันส่งมอบหลังรอบทดสอบ)', count['Not tested']],
], ['', 'r']),
'## Verification Items',
table(['No', 'Test Case', 'Software Testing', 'Customer Req. ID', 'Customer Req. Topic', 'Status', 'Test Date'], rows, ['c', 'c', '', 'c', '', 'c', 'c']),
table(['No', 'Test Case', 'Software Unit', 'Customer Req. ID', 'Actual Result', 'Status', 'Test Date'], rows, ['c', 'c', '', '', '', 'c', 'c']),
`ความต้องการที่ยืนยันด้วย Test Case มี ${tested.length} รายการ ส่วนอีก ${byDocument} รายการเป็นความต้องการด้านเอกสาร การอบรม และหลักฐานด้านคุณภาพ ซึ่งยืนยันด้วยเอกสารส่งมอบตามที่ระบุใน Traceability Record และได้รับการยืนยันจากลูกค้าใน Validation Results`,
'## Summary',
table(['No', 'Topic', 'Detail'], [
[1, 'Test Case', `ทดสอบตาม Customer Requirements ผ่านทั้งหมด ${TEST_CASES.length} Test Case คิดเป็น 100%`],
[2, 'Pending', 'ไม่พบข้อผิดพลาด (Fail) หรือรายการค้าง (Pending) ในรอบการทดสอบนี้'],
[3, 'Conclusion', 'ผลการทดสอบสรุปได้ว่าระบบพร้อมสำหรับ UAT และการส่งมอบ เนื่องจากผลการทดสอบผ่านครบ 100% และเป็นไปตาม Project Plan'],
[1, 'Test Case', `ทดสอบ ${count.Passed + count.Failed} จาก ${TEST_CASES.length} Test Case ผ่าน ${count.Passed} ไม่ผ่าน ${count.Failed}`],
[2, 'Failed', `${failed.map(([tc]) => tc).join(', ')} บันทึกเป็นข้อบกพร่องคงค้าง ${issues.join(', ')} ใน Correction Register`],
[3, 'Not tested', `${TEST_CASES.filter(([tc]) => testResult(tc).status === 'Not tested').map(([tc]) => tc).join(', ')} ฟังก์ชันส่งมอบหลังรอบทดสอบ (Task 4.7) ต้องทดสอบก่อนใช้งาน`],
[4, 'Conclusion', 'ฟังก์ชันหลักผ่านการทดสอบ แต่ยังมีข้อบกพร่องด้านความปลอดภัยและความถูกต้องของข้อมูลคงค้าง ต้องแจ้งผู้รับมอบและกำหนดการแก้ไขก่อนตรวจรับ'],
], ['c']),
'## หลักฐานประกอบทางอ้อม',
`Correction Register บันทึกการแก้ไขข้อบกพร่องที่พบระหว่างพัฒนา ${CORRECTIONS.length} รายการ ซึ่งได้รับการแก้ไขและตรวจสอบผลด้วย Test Case ที่เกี่ยวข้องครบทุกรายการ`,
`Correction Register บันทึกข้อบกพร่อง ${CORRECTIONS.length} รายการ แก้ไขแล้ว ${CORRECTIONS.filter((c) => c[2]).length} รายการ และคงค้างจากรอบทดสอบนี้ ${CORRECTIONS.filter((c) => !c[2]).length} รายการ`,
].join('\n\n'),
secretary: P.PaNg, reviewer: P.NoC, approver: P.SeV,
}),
@@ -380,9 +387,9 @@ export function software() {
table(['ลำดับ', 'หลักฐาน', 'รายละเอียด'], [
[1, 'Source Code และ Baseline', `Commit \`${BASELINE_COMMIT}\` ควบคุมใน Repository หลักและสำรอง`],
[2, 'ชุดติดตั้งระบบ', 'Docker Compose Stack และสคริปต์ `setup.php` สำหรับสร้างฐานข้อมูล'],
[3, 'ผลการทดสอบระบบ', `Test Report ${TEST_CASES.length} Test Case ผ่านทั้งหมด`],
[4, 'ผลการทดสอบการยอมรับ', `Validation Results ครบ 14 หมวด ${CRS.length} รายการ ผ่านทั้งหมด`],
[5, 'การตรวจรับส่งมอบ', `Acceptance Report ผลการตรวจรับ Accepted เมื่อ ${thLong(ACCEPT_DATE)}`],
[3, 'ผลการทดสอบระบบ', (({ Passed: ok, Failed: bad, 'Not tested': none }) => `Test Report ${TEST_CASES.length} Test Case ผ่าน ${ok} ไม่ผ่าน ${bad} ยังไม่ได้ทดสอบ ${none}`)(resultCounts())],
[4, 'ผลการทดสอบการยอมรับ', (() => { const s = CRS.map(([id]) => crStatus(id, date)); const n = (x) => s.filter((v) => v === x).length; return `Validation Results ${CRS.length} รายการ ผ่าน ${n('Passed')} ไม่ผ่าน ${n('Failed')} ยังไม่ได้ทดสอบ ${n('Not tested')} รอดำเนินการ ${n('Pending')}`; })()],
[5, 'การตรวจรับส่งมอบ', `เสนอ Project Sponsor ตรวจรับตามแผนในวันที่ ${thLong(ACCEPT_DATE)} (Acceptance Report)`],
[6, 'คู่มือประกอบการใช้งาน', 'Software User Document, Product Operation Guide, Maintenance Document'],
], ['c']),
'## รายการอ้างอิงเพิ่มเติม',
+28 -17
View File
@@ -1,6 +1,6 @@
// SI work products 18–22: user documentation, operation guide, maintenance, verification, validation.
import { P, CODE, ORGANIZER, ORGANIZER_PM, RECORDER, START, END, SCOPE, CRS, CR_GROUPS, TEST_FROM, TEST_TO, ACCEPT_DATE, BASELINE_COMMIT, TEST_STATE_COMMIT, ORIGIN, BACKUP, OPS_CLOSE_DATE } from './project.mjs';
import { UNITS, TEST_CASES, traceOf, unitTopic } from './engineering.mjs';
import { UNITS, TEST_CASES, traceOf, unitTopic, testResult, resultCounts, crStatus, PENDING_UNTIL } from './engineering.mjs';
import { CORRECTIONS, VERIFICATION_ROUNDS, VERIFICATION_RULES } from './history.mjs';
import { bud, thLong, thRangeLong, table, bullets, numbered } from './lib.mjs';
import { workProduct, head } from './format.mjs';
@@ -311,9 +311,9 @@ export function verificationResults() {
['ลงชื่อผู้จัดทำและผู้อนุมัติเรียบร้อย', 'มีตารางลงนามครบถ้วน'],
], 'YaB'],
'WP 9.0': ['ระบบที่ผ่านการทดสอบพร้อมใช้งานจริง', [
['ระบบติดตั้งและใช้งานได้บนสภาพแวดล้อมที่กำหนด', `ติดตั้งสำเร็จและใช้งานได้ Baseline \`${BASELINE_COMMIT}\``],
['ระบบติดตั้งและใช้งานได้บนสภาพแวดล้อมที่กำหนด', `Build \`${TEST_STATE_COMMIT}\` ติดตั้งและใช้งานบน Internal Testing Server ได้ ชุดติดตั้ง Docker ใน Baseline \`${BASELINE_COMMIT}\` ยังไม่ได้ทดสอบ`, 'Failed', 'ทดสอบ TC-UN13.002 และ TC-UN13.003 บน Baseline ก่อนใช้งานจริง'],
['Source Code จัดเก็บใน Repository พร้อมชุดสำรอง', 'จัดเก็บครบทั้ง Repository หลักและสำรอง'],
['ผลการทดสอบระบบผ่านครบทุก Test Case', 'ผ่านครบ 45 Test Case'],
['ผลการทดสอบระบบผ่านครบทุก Test Case', (({ Passed: ok, Failed: bad, 'Not tested': none }) => `ผ่าน ${ok} ไม่ผ่าน ${bad} ยังไม่ได้ทดสอบ ${none} จาก ${TEST_CASES.length} Test Case`)(resultCounts()), 'Failed', 'ข้อบกพร่องคงค้างบันทึกใน Correction Register ต้องมีแผนแก้ไขก่อนตรวจรับ'],
], 'ThS'],
'WP 10.0': ['Product Operation Guide', [
['รายละเอียดและความถูกต้องของ Header ของเอกสาร', 'มีรายละเอียดครบถ้วนถูกต้อง'],
@@ -331,12 +331,16 @@ export function verificationResults() {
return ROUNDS.map((r) => {
const rows = [];
let n = 0;
let failed = 0;
for (const wp of r.wps) {
const [wpName, checks, owner] = CHECKS[wp];
rows.push([`${wp} ${wpName}`, '', '', '', '', '']);
checks.forEach((c, i) => {
n += 1;
rows.push([`VR${String(r.no).padStart(2, '0')}.${wp.replace(/\D/g, '').padStart(3, '0')}.${String(i + 1).padStart(3, '0')}`, c[0], c[1], 'Passed', P[owner].id, '']);
// An item passes unless it records its own result and recommendation.
const [item, evidence, result = 'Passed', comment = ''] = c;
if (result !== 'Passed') failed += 1;
rows.push([`VR${String(r.no).padStart(2, '0')}.${wp.replace(/\D/g, '').padStart(3, '0')}.${String(i + 1).padStart(3, '0')}`, item, evidence, result, P[owner].id, comment]);
});
}
return {
@@ -363,12 +367,12 @@ export function verificationResults() {
'## สรุปผลการตรวจสอบรอบนี้',
table(['รายการ', 'จำนวน'], [
['รายการตรวจสอบทั้งหมด', n],
['ผลผ่าน (Passed)', n],
['ผลไม่ผ่าน (Failed)', 0],
['ประเด็นคงค้าง', 0],
['ผลผ่าน (Passed)', n - failed],
['ผลไม่ผ่าน (Failed)', failed],
['ประเด็นคงค้าง', failed],
], ['', 'r']),
r.no === 4
? `รอบตรวจสอบนี้เป็นรอบสุดท้ายก่อนส่งมอบ ครอบคลุมสิ่งส่งมอบทั้งหมด ${r.wps.length} รายการ ผลการตรวจสอบผ่านทุกรายการ จึงเห็นควรให้ดำเนินการตรวจรับส่งมอบระบบตามที่บันทึกใน Acceptance Report`
? `รอบตรวจสอบนี้เป็นรอบสุดท้ายก่อนส่งมอบ ครอบคลุมสิ่งส่งมอบทั้งหมด ${r.wps.length} รายการ ผ่าน ${n - failed} รายการ ไม่ผ่าน ${failed} รายการ คือผลการทดสอบระบบที่ยังมีข้อบกพร่องคงค้าง และชุดติดตั้ง Docker ที่ยังไม่ได้ทดสอบ จึงเสนอให้ผู้รับมอบพิจารณาการตรวจรับพร้อมแผนแก้ไขข้อบกพร่องคงค้าง`
: `ผลการตรวจสอบรอบนี้ผ่านทุกรายการ ประเด็นที่พบระหว่างการตรวจสอบได้รับการแก้ไขและบันทึกใน Correction Register แล้ว การตรวจสอบรอบถัดไปจะครอบคลุมสิ่งส่งมอบที่จัดทำเพิ่มเติมในช่วงถัดไป`,
].join('\n\n'),
secretary: P.PaNg, reviewer: P.YaB, approver: P.SeV,
@@ -406,15 +410,20 @@ export function validationResults() {
const evidence = (id) => {
const t = traceOf(id);
const ev = AT_UAT[id] ?? t.ev;
return [t.tcs.length ? t.tc : '', ev].filter(Boolean).join(' / ');
const issues = [...new Set(t.tcs.map((tc) => testResult(tc).issue).filter(Boolean))];
const pending = crStatus(id, date) === 'Pending' ? PENDING_UNTIL[id][1] : '';
return [t.tcs.length ? t.tc : '', ev, issues.length ? `ข้อบกพร่องคงค้าง ${issues.join(', ')}` : '', pending].filter(Boolean).join(' / ');
};
const status = (id) => crStatus(id, date);
const rows = [];
const summary = [];
for (const [gid, gname, items] of CR_GROUPS) {
rows.push([gid, `**${gname}**`, '', '', '', '']);
for (const [id, text] of items) rows.push([id, text, METHOD[gid], evidence(id), 'Passed', P.SeV.id]);
summary.push([`${gid} ${gname}`, items.length, items.length, 0]);
for (const [id, text] of items) rows.push([id, text, METHOD[gid], evidence(id), status(id), P.SeV.id]);
const n = (s) => items.filter(([id]) => status(id) === s).length;
summary.push([`${gid} ${gname}`, items.length, n('Passed'), n('Failed'), n('Not tested') + n('Pending')]);
}
const all = (s) => CRS.filter(([id]) => status(id) === s).length;
if (Object.keys(METHOD).length !== CR_GROUPS.length) throw new Error('Every CR group needs a validation method.');
return {
dir: '2-SI Process (12 Work Product)/22.Validation Result',
@@ -438,21 +447,23 @@ export function validationResults() {
['ผู้บันทึกผล', `${P.PaNg.th} (QA/Tester)`],
['ช่วงเวลาทดสอบ', thRangeLong(TEST_FROM, TEST_TO)],
['ขอบเขตการทดสอบ', `Customer Requirements ครบ ${CR_GROUPS.length} หมวด ${CRS.length} รายการ (CR01–CR14)`],
['สถานะ Repository ที่ใกล้เคียงที่สุด', `Commit \`${TEST_STATE_COMMIT}\``],
['Build ที่ใช้ทดสอบ', `Commit \`${TEST_STATE_COMMIT}\` บน Branch main`],
['ลำดับการทดสอบ', 'ทดสอบการยอมรับคู่ขนานกับการทดสอบระบบตามมติที่ประชุม 10 ส.ค. 69 ซึ่งต่างจากเกณฑ์เข้าสู่ UAT ใน Test Case and Test Procedures'],
]),
'## ตารางสรุป Validation Result ตาม Customer Requirements',
table(['CR ID', 'Customer Requirement', 'วิธีการยืนยัน', 'หลักฐานอ้างอิง', 'Status', 'Tester'], rows, ['', '', '', '', 'c', 'c']),
'## สรุปผลการทดสอบการยอมรับรายหมวด',
table(['หมวดความต้องการ', 'จำนวนรายการ', 'ผ่าน (Passed)', 'ไม่ผ่าน (Failed)'], summary, ['', 'r', 'r', 'r']),
table(['หมวดความต้องการ', 'จำนวนรายการ', 'ผ่าน (Passed)', 'ไม่ผ่าน (Failed)', 'ยังไม่ได้ทดสอบ / รอดำเนินการ'], summary, ['', 'r', 'r', 'r', 'r']),
'## สรุปผลการทดสอบการยอมรับ',
table(['รายการ', 'จำนวน'], [
['หมวดความต้องการที่ทดสอบ', CR_GROUPS.length],
['ความต้องการที่ทดสอบทั้งหมด', CRS.length],
['ผ่าน (Passed)', CRS.length],
['ไม่ผ่าน (Failed)', 0],
['ค้างการทดสอบ', 0],
['ผ่าน (Passed)', all('Passed')],
['ไม่ผ่าน (Failed)', all('Failed')],
['ยังไม่ได้ทดสอบ (ฟังก์ชันส่งมอบหลังรอบทดสอบ)', all('Not tested')],
['รอดำเนินการตามแผน (Pending)', all('Pending')],
], ['', 'r']),
`ตัวแทนลูกค้าได้ทดสอบการยอมรับครบทุกความต้องการใน Customer Requirements บนสภาพแวดล้อมใช้งานจริง ผลการทดสอบผ่านทั้งหมด ไม่พบประเด็นที่ขัดขวางการเปิดใช้งาน จึงยืนยันความพร้อมของระบบและเสนอเข้าสู่การตรวจรับส่งมอบตามแผนในวันที่ ${thLong(ACCEPT_DATE)}`,
`ตัวแทนลูกค้าทดสอบการยอมรับครบ ${CRS.length} ความต้องการ ผ่าน ${all('Passed')} รายการ ความต้องการ ${all('Failed')} รายการไม่ผ่านเนื่องจากข้อบกพร่องคงค้างที่บันทึกใน Correction Register ${all('Not tested')} รายการขึ้นกับชุดติดตั้ง Docker ที่ส่งมอบหลังรอบทดสอบ และ ${all('Pending')} รายการรองานปฏิบัติการตามแผน ผู้รับมอบรับทราบผลและจะพิจารณาการตรวจรับพร้อมแผนแก้ไขในวันที่ ${thLong(ACCEPT_DATE)}`,
].join('\n\n'),
secretary: P.PaNg, reviewer: P.NoC, approver: P.SeV,
}),
+62 -20
View File
@@ -80,7 +80,7 @@ export const UNIT_GROUPS = [
['UN01.001', 'Identity – Register & Onboarding', 'ลงทะเบียนเจ้าของบริษัท, ยืนยันอีเมล, เชิญผู้ใช้และรับคำเชิญเข้าบริษัทที่ถูกต้อง', 'SR03:001, SR08:001', 'app/login/, UserManager.php'],
['UN01.002', 'Identity – Login & Role Guard', 'เข้าสู่ระบบด้วย Username/Password, ตรวจบทบาทและสิทธิ์แอปพลิเคชันฝั่งเซิร์ฟเวอร์ก่อนเข้าทุกหน้า', 'SR03:001, SR04:001, SR07:005', 'app/login/, UserManager.php, app/assets/utils/'],
['UN01.003', 'Identity – Password Recovery / OTP / Session', 'ลืมรหัสผ่านผ่านอีเมล, OTP, Hash รหัสผ่าน bcrypt, บล็อก Concurrent Login', 'SR03:001, SR01:004, SR07:003', 'PasswordManager.php, PasswordResetManager.php'],
['UN01.004', 'Identity – User & App-Access Administration', 'เพิ่ม/แก้ไข/ปิดใช้ผู้ใช้, กำหนดบทบาทและสิทธิ์การเข้าถึงแอปพลิเคชัน', 'SR03:001, SR08:001', 'app/setting/, UserManager.php'],
['UN01.004', 'Identity – User & App-Access Administration', 'Owner เชิญ/แก้ไขบทบาทและสิทธิ์แอปพลิเคชัน/ลบผู้ใช้ และส่งคำเชิญซ้ำ', 'SR03:001, SR08:001', 'app/setting/api/engine/manage_users.php, UserManager.php'],
]],
['UN02', 'ตั้งค่าบริษัท (Company Settings)', [
['UN02.001', 'Company – Profile & System Settings', 'แก้ไขข้อมูลบริษัท, โลโก้, ค่าตั้งค่าระบบต่อบริษัท', 'SR03:002', 'CompanyProfileManager.php, CompanySettingManager.php'],
@@ -94,7 +94,7 @@ export const UNIT_GROUPS = [
]],
['UN04', 'ควบคุมสินค้าคงคลัง (Inventory Control)', [
['UN04.001', 'ICS – Stock-in', 'รับสินค้าเข้าตามสินค้า จำนวน ตำแหน่ง เอกสารอ้างอิง สร้างความเคลื่อนไหวและยอดคงเหลือ', 'SR03:004, SR08:003', 'app/ics/, StockManager.php, StockTablesTrait.php'],
['UN04.002', 'ICS – Stock-out', 'จ่ายสินค้าออกโดยตรวจสิทธิ์และยอดคงเหลือ ปฏิเสธจำนวนเกิน', 'SR03:004, SR09:003', 'StockManager.php'],
['UN04.002', 'ICS – Stock-out', 'จ่ายสินค้าออกทั้งยอดของช่องจัดเก็บโดยตรวจสิทธิ์ ยอดคงเหลือไม่ติดลบ', 'SR03:004, SR09:003', 'StockManager.php, OrderManager.php'],
['UN04.003', 'ICS – Stock Transfer', 'โอนย้ายระหว่างตำแหน่ง ต้นทาง/ปลายทางผูกเป็นรายการเดียว', 'SR03:004', 'StockManager.php'],
['UN04.004', 'ICS – Lot / Serial / Expiry', 'บันทึกและสอบกลับ Lot, Serial Number, วันหมดอายุ', 'SR03:004, SR08:002', 'StockSourceManager.php, md_lot'],
['UN04.005', 'ICS – Barcode Label & Scan', 'พิมพ์ฉลากบาร์โค้ด SKU/ตำแหน่ง และรับค่าจากเครื่องสแกน', 'SR03:004', 'BarcodeManager.php, location_barcode_label.php'],
@@ -109,7 +109,7 @@ export const UNIT_GROUPS = [
['UN06', 'จัดซื้อ (Purchasing)', [
['UN06.001', 'Purchasing – Purchase Request', 'สร้างและอนุมัติใบขอซื้อ', 'SR03:006', 'app/po/, PurchaseRequestManager.php'],
['UN06.002', 'Purchasing – Purchase Order', 'แปลงใบขอซื้อเป็นใบสั่งซื้อ และติดตามสถานะ', 'SR03:006, SR04:002', 'PurchaseOrderManager.php'],
['UN06.003', 'Purchasing – Purchase Invoice', 'บันทึกใบแจ้งหนี้ซื้อ รับสินค้าเข้า และบันทึก GL', 'SR03:006, SR04:002', 'PurchaseOrderManager.php, StockManager.php'],
['UN06.003', 'Purchasing – Purchase Invoice', 'รับสินค้าตามใบสั่งซื้อ และบันทึกใบแจ้งหนี้ซื้อพร้อมผ่าน GL', 'SR03:006, SR04:002', 'PurchaseOrderManager.php, app/expense/purchase_invoice.php, PurchaseInvoicePosting.php'],
['UN06.004', 'Purchasing – Supplier Return', 'คืนสินค้าผู้ขาย พร้อมปรับสต๊อก/บัญชี', 'SR03:006, SR04:002', 'SupplierReturnManager.php'],
]],
['UN07', 'การเงิน (Finance)', [
@@ -117,19 +117,19 @@ export const UNIT_GROUPS = [
['UN07.002', 'Finance – Payment Billing & Payment', 'วางบิลจ่ายและบันทึกใบสำคัญจ่ายผูกกับใบแจ้งหนี้ซื้อ', 'SR03:007, SR04:002', 'PaymentBillingManager.php, PaymentManager.php'],
]],
['UN08', 'บัญชี (Accounting)', [
['UN08.001', 'Accounting – Chart of Accounts / Departments / Formulas', 'จัดการผังบัญชี แผนก และสูตรบัญชีอัตโนมัติ', 'SR03:008, SR08:002', 'app/accounting/, PostingManager.php'],
['UN08.002', 'Accounting – Journal & GL Posting', 'บันทึกสมุดรายวันและผ่านรายการ GL แบบสมดุลภายใน Transaction', 'SR03:008, SR09:003', 'app/journal/, PostingManager.php'],
['UN08.001', 'Accounting – Chart of Accounts / Departments / Formulas', 'จัดการผังบัญชี แผนก และสูตรบัญชีอัตโนมัติ', 'SR03:008, SR08:002', 'app/accounting/, GlManager.php'],
['UN08.002', 'Accounting – Journal & GL Posting', 'บันทึกสมุดรายวันและผ่านรายการ GL ตามช่วงเวลาที่เปิดผ่านรายการ', 'SR03:008, SR09:003', 'app/accounting/api/engine/save_manual_journal.php, GlManager.php, BasePosting.php, PostingWindowGuard.php'],
]],
['UN09', 'รายงานและแดชบอร์ด (Reports & Dashboard)', [
['UN09.001', 'Reports – Dashboard & Aggregates', 'แดชบอร์ดคลังสินค้าและบัญชีจากตารางสรุปยอด', 'SR03:009, SR05:001, SR05:002', 'app/dashboard/, app/ac_dashboard/, EtlStockManager.php'],
['UN09.002', 'Reports – Stock Reports', 'ภาพรวมสต๊อก, ความเคลื่อนไหว, ความจุ, สินค้าใกล้หมด, หมดอายุ, Lot', 'SR03:009', 'app/reports/, ReportManager.php'],
['UN09.003', 'Reports – Financial Reports', 'งบทดลอง, งบกำไรขาดทุน, งบดุล, VAT, สมุดรายวัน, ความเคลื่อนไหว GL', 'SR03:009', 'ReportManager.php'],
['UN09.003', 'Reports – Financial Reports', 'งบทดลอง, งบกำไรขาดทุน, งบดุล, VAT, สมุดรายวัน, ความเคลื่อนไหว GL', 'SR03:009', 'app/accounting/ (trial_balance, pl_statement, balance_sheet, vat_report, journal_listing, gl_movement)'],
['UN09.004', 'Reports – Filter / Print / Export', 'กรอง ดู พิมพ์ และส่งออกรายงานตามขอบเขตสิทธิ์', 'SR03:009, SR04:003', 'app/reports/'],
]],
['UN10', 'ควบคุมเอกสาร (Document Control)', [
['UN10.001', 'Document – Numbering', 'ออกเลขที่เอกสารตามลำดับที่กำหนด (document_number_sequences)', 'SR03:010, SR04:005, SR08:004', 'DocumentNumberManager.php'],
['UN10.002', 'Document – Lifecycle & Status', 'ควบคุมสถานะเอกสาร ปฏิเสธการเปลี่ยนสถานะที่ไม่ถูกต้อง พร้อม Confirm Dialog และข้อความข้อผิดพลาด', 'SR03:010, SR09:001, SR09:002', 'BatchActionManager.php, Manager classes'],
['UN10.003', 'Document – Audit Fields & History', 'บันทึกผู้สร้าง ผู้แก้ไข วันที่ และประวัติรายการ', 'SR03:010, SR09:001', 'td_* tables, td_bin_log'],
['UN10.003', 'Document – Audit Fields & History', 'บันทึกผู้ทำรายการ วันเวลา และข้อมูลที่ส่งในคอลัมน์ log ของรายการ และประวัติช่องจัดเก็บ', 'SR03:010, SR09:001', 'db_auth.php (log), td_* tables, td_bin_log'],
]],
['UN11', 'แจ้งเตือนและงานตามกำหนดเวลา (Notification & Scheduler)', [
['UN11.001', 'Node – Socket.IO Notification Server', 'รับเหตุการณ์จาก PHP ผ่าน Endpoint ที่ป้องกันด้วย Secret และส่งต่อไปยัง Browser ของผู้ใช้ที่เกี่ยวข้อง', 'SR03:011, SR04:004, SR06:004, SR06:005', 'nodejs/server.js'],
@@ -138,14 +138,14 @@ export const UNIT_GROUPS = [
['UN12', 'ความปลอดภัยและการแยกข้อมูล (Security & Tenant Scope)', [
['UN12.001', 'Security – Tenant Scope Guard', 'จำกัดทุก Query และ Action ให้อยู่ในบริษัท/คลังที่ผู้ใช้ได้รับอนุญาต', 'SR04:001, SR04:003, SR07:002', 'StockTablesTrait.php, Manager classes'],
['UN12.002', 'Security – Server-side Validation', 'ตรวจสอบข้อมูลนำเข้า, Prepared Statements, Escape Output ป้องกัน SQLi/XSS', 'SR07:002, SR07:004', 'Manager classes, app/assets/utils/'],
['UN12.003', 'Security – Operation Lock & Usage Guard', 'ล็อกช่วงเวลาผ่านรายการ และจำกัดโควตารายการต่อบริษัท', 'SR07:002, SR09:003', 'OperationLockManager.php, UsageGuard.php'],
['UN12.003', 'Security – Posting Window & Usage Guard', 'ปฏิเสธการผ่านรายการนอกช่วงเวลาที่เปิด และล็อกแดชบอร์ด/รายงานเมื่อใช้งานเกินแพ็กเกจ', 'SR07:002, SR09:003', 'PostingWindowGuard.php, UsageGuard.php'],
['UN12.004', 'Security – TLS & Secret Configuration', 'ค่าตั้งค่า TLS, NODE_EMIT_SECRET และ Secrets ที่สร้างตอนติดตั้ง', 'SR07:001, SR01:005', 'docker/php/config.php.template, .env'],
]],
['UN13', 'ติดตั้งและสำรองข้อมูล (Deployment & Backup)', [
['UN13.001', 'Deploy – setup.php Schema Installer', 'สร้างฐานข้อมูล wms/wms2 และตารางทั้งหมดในขั้นตอนเดียว', 'SR01:003, SR08:005, SR06:001', 'setup.php, docker/mariadb/init-wms2.sql'],
['UN13.002', 'Deploy – Docker Compose Stack', 'Container php-apache, mariadb, node/pm2 พร้อม Entrypoint สร้าง config.php', 'SR02:005, SR01:003', 'docker-compose.yml, docker/'],
['UN13.002', 'Deploy – Docker Compose Stack', 'Service db, php, node พร้อม Entrypoint สร้าง config.php', 'SR02:005, SR01:003', 'docker-compose.yml, docker/'],
['UN13.003', 'Deploy – .env Generator', 'สร้าง .env และความลับอัตโนมัติ ไม่เก็บใน Git', 'SR01:005', 'docker/init-env.sh, .env.example'],
['UN13.004', 'Deploy – Backup & Restore', 'สำรอง Git 2 Remote, mysqldump รายวัน, ขั้นตอนกู้คืน', 'SR01:003', 'Product Operation Guide, backup remote'],
['UN13.004', 'Deploy – Backup & Restore', 'สำรอง Git 2 Remote, สำรองฐานข้อมูลด้วย mysqldump และขั้นตอนกู้คืน', 'SR01:003', 'Product Operation Guide, backup remote'],
]],
];
export const UNITS = UNIT_GROUPS.flatMap(([, , rows]) => rows);
@@ -161,7 +161,7 @@ export const TEST_CASES = [
['TC-UN01.001', 'UN01.001', 'ลงทะเบียนเจ้าของบริษัทใหม่ → ยืนยันอีเมล → เชิญผู้ใช้ → ผู้ใช้กดลิงก์เชิญ', 'สร้างบริษัท/บัญชีเจ้าของสำเร็จ ผู้ใช้ที่ถูกเชิญเข้าบริษัทที่ถูกต้อง', ENV_WEB + ', SMTP ทดสอบ', 'ต้องส่งอีเมลได้จริง', '-'],
['TC-UN01.002', 'UN01.002', 'เข้าสู่ระบบด้วยบัญชี Owner/Admin/Staff/Viewer แล้วเรียกหน้าและ Action ที่ไม่ได้รับสิทธิ์', 'แต่ละบทบาทเข้าถึงได้เฉพาะหน้าจอ/คำสั่งที่อนุญาต คำสั่งที่ไม่ได้รับสิทธิ์ถูกปฏิเสธฝั่งเซิร์ฟเวอร์', ENV_WEB, 'ต้องมีบัญชีทดสอบครบ 4 บทบาท', 'TC-UN01.001'],
['TC-UN01.003', 'UN01.003', 'ขอรีเซ็ตรหัสผ่าน → กรอก OTP → ตั้งรหัสใหม่; เข้าสู่ระบบบัญชีเดิมจาก 2 อุปกรณ์', 'รีเซ็ตสำเร็จโดยไม่เปิดเผยรหัสผ่านเดิม Session ที่สองถูกปฏิเสธ', ENV_WEB + ', SMTP ทดสอบ', '-', 'TC-UN01.002'],
['TC-UN01.004', 'UN01.004', 'Admin เพิ่มผู้ใช้ เปลี่ยนบทบาท และปิดสิทธิ์แอปพลิเคชัน; Staff ลองทำรายการเดียวกัน', 'การเปลี่ยนแปลงของ Admin ถูกบันทึก Staff ถูกปฏิเสธ', ENV_WEB, '-', 'TC-UN01.002'],
['TC-UN01.004', 'UN01.004', 'Owner เชิญผู้ใช้ แก้ไขบทบาทและสิทธิ์แอปพลิเคชัน และลบผู้ใช้; Admin และ Staff ลองทำรายการเดียวกัน', 'การเปลี่ยนแปลงของ Owner ถูกบันทึก สิทธิ์แอปพลิเคชันไม่เกินสิทธิ์ของ Owner Admin และ Staff ถูกปฏิเสธ (HTTP 403)', ENV_WEB, 'บัญชี Owner, Admin, Staff ในบริษัทเดียวกัน', 'TC-UN01.002'],
['TC-UN02.001', 'UN02.001', 'แก้ไขข้อมูลบริษัทและค่าตั้งค่าระบบ', 'ค่าใหม่ถูกบันทึกและมีผลกับบริษัทนั้นเท่านั้น', ENV_WEB, '-', 'TC-UN01.002'],
['TC-UN02.002', 'UN02.002', 'ตั้งค่า SMTP แล้วส่งอีเมลทดสอบ/คำเชิญ', 'อีเมลถูกส่งผ่าน SMTP ของบริษัทและได้รับที่ปลายทาง', ENV_WEB + ', SMTP ทดสอบ', 'ต้องมีบัญชี SMTP ทดสอบ', 'TC-UN02.001'],
['TC-UN03.001', 'UN03.001', 'เพิ่ม/แก้ไข/ปิดใช้ คลังสินค้า พื้นที่ และช่องจัดเก็บ; กรอกความจุไม่ถูกต้อง', 'รายการถูกต้องถูกบันทึก ข้อมูลผิดถูกปฏิเสธพร้อมข้อความ', ENV_WEB, '-', 'TC-UN01.002'],
@@ -169,7 +169,7 @@ export const TEST_CASES = [
['TC-UN03.003', 'UN03.003', 'เพิ่มประเภทผู้ติดต่อ ลูกค้า และผู้ขาย', 'ผู้ติดต่อเลือกใช้ในเอกสารขาย/ซื้อได้', ENV_WEB, '-', 'TC-UN01.002'],
['TC-UN03.004', 'UN03.004', 'ตั้งบริษัท A เป็นคลังชั้นเดียว บริษัท B เป็นคลัง/พื้นที่/ช่อง แล้วรับสินค้าเข้า', 'ทั้งสองบริษัททำรายการได้ถูกต้องตามโครงสร้างของตน', ENV_WEB + ', 2 บริษัททดสอบ', '-', 'TC-UN03.001'],
['TC-UN04.001', 'UN04.001', 'รับสินค้าเข้า ระบุสินค้า จำนวน ตำแหน่ง เอกสารอ้างอิง; กรอกจำนวนติดลบ', 'สร้างความเคลื่อนไหวและยอดคงเหลือถูกต้อง จำนวนติดลบถูกปฏิเสธ', ENV_WEB, '-', 'TC-UN03.002'],
['TC-UN04.002', 'UN04.002', 'จ่ายสินค้าออกภายในยอดคงเหลือ และจ่ายเกินยอด', 'ยอดลดลงถูกต้อง การจ่ายเกินยอดถูกปฏิเสธ', ENV_WEB, '-', 'TC-UN04.001'],
['TC-UN04.002', 'UN04.002', 'จ่ายสินค้าออกจากช่องจัดเก็บที่มียอด; ยืนยันใบสั่งขายที่จำนวนเกินยอดคงเหลือ', 'ยอดช่องจัดเก็บลดเป็นศูนย์และบันทึกความเคลื่อนไหว ยอดไม่ติดลบ ใบสั่งขายที่เกินยอดถูกปฏิเสธพร้อมข้อความ', ENV_WEB, '-', 'TC-UN04.001'],
['TC-UN04.003', 'UN04.003', 'โอนย้ายระหว่าง 2 ตำแหน่งที่ได้รับอนุญาต', 'ต้นทางลด ปลายทางเพิ่ม ผูกเป็นรายการโอนเดียวและสอบกลับได้', ENV_WEB, '-', 'TC-UN04.001'],
['TC-UN04.004', 'UN04.004', 'รับสินค้าเข้าพร้อม Lot, Serial Number, วันหมดอายุ แล้วเปิดรายงาน Lot/หมดอายุ', 'ข้อมูลติดตามคงอยู่และแสดงในรายงานที่เกี่ยวข้อง', ENV_WEB, '-', 'TC-UN04.001'],
['TC-UN04.005', 'UN04.005', 'พิมพ์ฉลาก SKU/ตำแหน่ง แล้วสแกน (หรือป้อนค่า) ในหน้าจอรับเข้า', 'ฉลากมีรหัสที่ใช้งานได้ ค่าที่สแกนถูกยอมรับ', ENV_WEB + ', เครื่องสแกนหรือจำลอง', 'เครื่องพิมพ์/สแกน (ถ้ามี)', 'TC-UN03.002'],
@@ -186,24 +186,66 @@ export const TEST_CASES = [
['TC-UN07.002', 'UN07.002', 'วางบิลจ่ายและบันทึกใบสำคัญจ่ายกับใบแจ้งหนี้ซื้อ', 'ยอดค้างจ่ายลดลง เอกสารเชื่อมโยงกันและมีประวัติ', ENV_WEB, '-', 'TC-UN06.003'],
['TC-UN08.001', 'UN08.001', 'เพิ่มผังบัญชี แผนก และสูตรบัญชี', 'โครงสร้างบัญชีถูกบันทึกและใช้กับการผ่านรายการได้', ENV_WEB, '-', 'TC-UN01.004'],
['TC-UN08.002', 'UN08.002', 'บันทึกสมุดรายวันแบบสมดุลและแบบไม่สมดุล; จำลองความล้มเหลวระหว่างผ่านรายการ', 'รายการสมดุลถูกผ่าน GL รายการไม่สมดุลถูกปฏิเสธ ความล้มเหลวถูก Rollback ทั้งชุด', ENV_WEB, 'ต้องจำลองความล้มเหลวบน Testing Server', 'TC-UN08.001'],
['TC-UN09.001', 'UN09.001', 'เปิดแดชบอร์ดคลังสินค้าและบัญชีหลังรันงานสรุปยอด', 'ตัวเลขตรงกับตารางสรุปยอดและแสดงผลภายในเวลาที่ใช้งานได้', ENV_WEB, '-', 'TC-UN11.002'],
['TC-UN09.001', 'UN09.001', 'รันงานสรุปยอดแล้วเปิดแดชบอร์ดคลังสินค้าและบัญชี', 'ตัวเลขตรงกับตาราง etl_stock_summary และ etl_gl_summary และหน้าแดชบอร์ดแสดงผลภายใน 3 วินาที', ENV_WEB, '-', 'TC-UN11.002'],
['TC-UN09.002', 'UN09.002', 'เปิดรายงานภาพรวมสต๊อก ความเคลื่อนไหว ความจุ สินค้าใกล้หมด หมดอายุ และ Lot พร้อมตัวกรอง', 'รายงานแสดงข้อมูลที่ได้รับสิทธิ์ตามตัวกรอง', ENV_WEB, '-', 'TC-UN04.001–TC-UN04.004'],
['TC-UN09.003', 'UN09.003', 'เปิดงบทดลอง งบกำไรขาดทุน งบดุล VAT สมุดรายวัน และความเคลื่อนไหว GL', 'ยอดรวมสอดคล้องกันในงวดที่เลือก', ENV_WEB, '-', 'TC-UN08.002'],
['TC-UN09.003', 'UN09.003', 'เปิดงบทดลอง งบกำไรขาดทุน งบดุล VAT สมุดรายวัน และความเคลื่อนไหว GL ของงวดเดียวกัน', 'งบทดลองเดบิตเท่ากับเครดิต สินทรัพย์เท่ากับหนี้สินรวมส่วนของเจ้าของ และยอดในรายงานตรงกับสมุดรายวัน', ENV_WEB, '-', 'TC-UN08.002'],
['TC-UN09.004', 'UN09.004', 'กรอง ดู พิมพ์ และส่งออกรายงาน', 'ผลลัพธ์ตรงกับขอบเขตและตัวกรองที่เลือก', ENV_WEB, '-', 'TC-UN09.002'],
['TC-UN10.001', 'UN10.001', 'สร้างเอกสารควบคุมหลายฉบับติดต่อกัน', 'เลขที่เอกสารเรียงตามลำดับที่ตั้งค่า ไม่ซ้ำ', ENV_WEB, '-', 'TC-UN05.001'],
['TC-UN10.002', 'UN10.002', 'พยายามเปลี่ยนสถานะเอกสารข้ามขั้น; ลบเอกสารควบคุม', 'การเปลี่ยนสถานะที่ไม่ถูกต้องถูกปฏิเสธพร้อมข้อความ การลบต้องยืนยันก่อน', ENV_WEB, '-', 'TC-UN10.001'],
['TC-UN10.003', 'UN10.003', 'สร้างแล้วแก้ไขรายการ แล้วเปิดดูประวัติ', 'ผู้สร้าง ผู้แก้ไข วันที่ และสถานะถูกบันทึกครบ', ENV_WEB, '-', 'TC-UN03.002'],
['TC-UN10.003', 'UN10.003', 'สร้างแล้วแก้ไขรายการ แล้วเปิดดูประวัติรายการ', 'คอลัมน์ log บันทึกผู้ทำรายการ วันเวลา และข้อมูลที่ส่งครบทุกครั้ง', ENV_WEB, '-', 'TC-UN03.002'],
['TC-UN11.001', 'UN11.001', 'เปลี่ยนสถานะเอกสารที่ต้องแจ้งเตือน; เรียก Endpoint แจ้งเตือนโดยไม่มี Secret', 'ผู้ใช้ที่เกี่ยวข้องได้รับแจ้งเตือน Real-time เท่านั้น การเรียกโดยไม่มี Secret ถูกปฏิเสธ', ENV_WEB + ', ' + ENV_NODE, 'บริการ Node.js ทำงานอยู่', 'TC-UN01.002'],
['TC-UN11.002', 'UN11.002', 'รันงานสรุปยอดสต๊อก/GL และแจ้งเตือนสินค้าใกล้หมด/ใบแจ้งหนี้ค้างชำระ; ตรวจเวลาบันทึกเทียบ Asia/Bangkok', 'งานเสร็จโดยไม่ซ้ำ ผลลัพธ์อยู่ในขอบเขตสิทธิ์ เวลาถูกต้องตามเขตเวลา', ENV_NODE, 'Scheduler ทำงานอยู่', 'TC-UN04.001, TC-UN07.001'],
['TC-UN11.002', 'UN11.002', 'รันงานสรุปยอดสต๊อก/GL และแจ้งเตือนสินค้าใกล้หมด/ใบแจ้งหนี้ค้างชำระสองรอบติดกัน; ตรวจเวลาบันทึกเทียบ Asia/Bangkok', 'แถวสรุปยอดของงวดเดียวกันไม่เพิ่มซ้ำในรอบที่สอง แจ้งเตือนส่งเฉพาะบริษัทเจ้าของข้อมูล เวลาตรงกับ Asia/Bangkok', ENV_NODE, 'Scheduler ทำงานอยู่', 'TC-UN04.001, TC-UN07.001'],
['TC-UN12.001', 'UN12.001', 'ผู้ใช้บริษัท A เรียกข้อมูล/รายการของบริษัท B และคลังที่ไม่ได้รับสิทธิ์ ทั้งผ่าน UI และ URL/ API โดยตรง', 'ถูกปฏิเสธทุกช่องทาง ไม่มีข้อมูลรั่วไหลหรือถูกแก้ไข', ENV_WEB + ', 2 บริษัททดสอบ', '-', 'TC-UN01.002, TC-UN03.001'],
['TC-UN12.002', 'UN12.002', 'ส่งข้อมูลนำเข้าที่มี SQL/Script และค่าที่ไม่ถูกต้องไปยัง Action ฝั่งเซิร์ฟเวอร์', 'ถูกปฏิเสธหรือ Escape โดยไม่มีข้อมูลเปลี่ยนแปลงหรือ Script ทำงาน', ENV_WEB + ', เครื่องมือทดสอบ API', '-', 'TC-UN01.002'],
['TC-UN12.003', 'UN12.003', 'ผ่านรายการในงวดที่ล็อก; ทำรายการเกินโควตาบริษัท', 'รายการถูกปฏิเสธพร้อมข้อความ', ENV_WEB, '-', 'TC-UN08.002'],
['TC-UN12.003', 'UN12.003', 'ผ่านรายการลงวันที่นอกช่วงเวลาที่เปิด; ใช้งานเกินจำนวนรายการของแพ็กเกจแล้วเปิดแดชบอร์ด/รายงาน', 'การผ่านรายการนอกช่วงถูกปฏิเสธพร้อมข้อความ แดชบอร์ด/รายงานถูกล็อก (HTTP 402) พร้อมข้อความให้ปรับแพ็กเกจ', ENV_WEB, 'ตั้งช่วงเวลาผ่านรายการและแพ็กเกจทดสอบ', 'TC-UN08.002'],
['TC-UN12.004', 'UN12.004', 'ตรวจ Repository และสภาพแวดล้อมที่ติดตั้งว่ามีความลับหรือไฟล์ตั้งค่าเปิดเผยหรือไม่; เรียกผ่าน HTTPS', 'ไม่พบความลับใน Git หรือเข้าถึงได้จากเว็บ การเชื่อมต่อผ่าน TLS', 'Repository, Deployed Environment', '-', '-'],
['TC-UN13.001', 'UN13.001', 'ติดตั้งบนเครื่องใหม่ด้วย setup.php', 'ฐานข้อมูลและตารางครบตาม SR08 โดยไม่แก้ Schema ด้วยมือ', 'Fresh Linux Server, PHP 8, MariaDB', 'ต้องใช้เครื่องว่าง', '-'],
['TC-UN13.002', 'UN13.002', 'รัน docker compose up -d --build', 'Container php-apache, mariadb, node ทำงานและเข้าใช้ระบบได้', 'Docker Host', '-', 'TC-UN13.003'],
['TC-UN13.002', 'UN13.002', 'รัน docker compose up -d --build', 'Service db, php, node ทำงานและเข้าใช้ระบบได้', 'Docker Host', '-', 'TC-UN13.003'],
['TC-UN13.003', 'UN13.003', 'รัน docker/init-env.sh โดยเว้นค่าความลับว่าง', '.env ถูกสร้างพร้อมความลับสุ่ม และไม่ถูก Git ติดตาม', 'Docker Host', '-', '-'],
['TC-UN13.004', 'UN13.004', 'สำรองฐานข้อมูล กู้คืนสู่สภาพแวดล้อมทดสอบ และ Clone จาก Backup Remote', 'ข้อมูลและ Source Code กู้คืนได้ครบตามขั้นตอนในคู่มือ', 'Testing Server, Backup Remote', 'ตาม Product Operation Guide', 'TC-UN13.001'],
['TC-UN13.004', 'UN13.004', 'สำรองฐานข้อมูลด้วย mysqldump กู้คืนสู่สภาพแวดล้อมทดสอบ และ Clone จาก Backup Remote', 'ข้อมูลและ Source Code กู้คืนได้ครบตามขั้นตอนในคู่มือ', 'Testing Server, Backup Remote', 'ตาม Product Operation Guide', 'TC-UN13.001'],
];
/**
* Results of the system test run (10–14 Aug) on the build under test. Anything not
* listed passed as expected. A failure names the open issue in the Correction Register;
* "Not tested" marks a function delivered after the run.
*/
export const TEST_RESULTS = {
'TC-UN04.006': { status: 'Failed', issue: 'ISS-029', actual: 'อัปโหลดและตรวจชนิด/ขนาดไฟล์ถูกต้อง แต่ไฟล์แนบอยู่ใต้ app/uploads/ ที่เว็บเปิดให้เรียกโดยตรง ผู้ที่ทราบ URL เปิดไฟล์ได้โดยไม่ตรวจ Session และบริษัท' },
'TC-UN08.002': { status: 'Failed', issue: 'ISS-030', actual: 'สมุดรายวันที่ไม่สมดุลถูกปฏิเสธ แต่เมื่อการผ่าน GL ล้มเหลว เอกสารที่บันทึกแล้วไม่ถูก Rollback เพราะการผ่าน GL แยก Transaction จากเอกสาร' },
'TC-UN11.001': { status: 'Failed', issue: 'ISS-031', actual: 'การเรียก Endpoint แจ้งเตือนโดยไม่มี Secret ถูกปฏิเสธ แต่ Socket.IO รับ company_id, user_id และ role จาก Browser โดยไม่ตรวจกับ Session ผู้ที่ปลอมค่าได้รับแจ้งเตือนของบริษัทอื่น' },
'TC-UN12.001': { status: 'Failed', issue: 'ISS-031', actual: 'หน้าจอและ API ปฏิเสธการเข้าถึงข้ามบริษัทถูกต้อง แต่ช่องทาง Socket.IO ยังรับการแจ้งเตือนของบริษัทอื่นได้' },
'TC-UN12.004': { status: 'Failed', issue: 'ISS-032', actual: 'ไม่พบไฟล์ .env หรือรหัสผ่านใน Git แต่แม่แบบค่าตั้งค่ากำหนดกุญแจเข้ารหัสและ IV คงที่ เชื่อมต่อฐานข้อมูลด้วยผู้ใช้ root และไม่มีหลักฐานการตั้งค่า HTTPS/TLS' },
'TC-UN13.002': { status: 'Not tested', actual: 'ชุดติดตั้ง Docker Compose ยังไม่อยู่ใน Build ที่ทดสอบ ส่งมอบภายหลังใน Task 4.7' },
'TC-UN13.003': { status: 'Not tested', actual: 'สคริปต์ docker/init-env.sh ยังไม่อยู่ใน Build ที่ทดสอบ ส่งมอบภายหลังใน Task 4.7' },
};
export const testResult = (tcId) => TEST_RESULTS[tcId] ?? { status: 'Passed', actual: 'ผลตรงตามที่คาดหวัง' };
export const resultCounts = () => {
const counts = { Passed: 0, Failed: 0, 'Not tested': 0 };
for (const [tc] of TEST_CASES) counts[testResult(tc).status] += 1;
return counts;
};
/**
* Requirements met by an operations task or document finished after the test run:
* pending in any record dated before that date.
*/
export const PENDING_UNTIL = {
'CR08:001': ['2026-08-23', 'รอปิดรายการ OP-001 สำรองฐานข้อมูลอัตโนมัติ'],
'CR08:003': ['2026-08-23', 'รอปิดรายการ OP-002 การเฝ้าระวังและแจ้งเตือน'],
'CR11:003': ['2026-08-22', 'รอการอบรมผู้ใช้งานตามแผน'],
'CR12:004': ['2026-08-24', 'รอจัดทำ List of Evidence ตอนปิดโครงการ'],
};
/** Status of a requirement as known on `asOf`: Failed, Not tested, Pending or Passed. */
export const crStatus = (crId, asOf) => {
const t = traceOf(crId);
const results = t.tcs.map((tc) => testResult(tc).status);
if (results.includes('Failed')) return 'Failed';
if (results.includes('Not tested')) return 'Not tested';
const pending = PENDING_UNTIL[crId];
if (pending && asOf < pending[0]) return 'Pending';
return 'Passed';
};
export const tcByUnit = (unitId) => TEST_CASES.find((t) => t[1] === unitId)?.[0] ?? '';
/**
@@ -255,7 +297,7 @@ export const TRACE = {
'CR05:005': { sr: ['SR03:004'], un: ['UN04.005'] },
'CR06:001': { sr: ['SR07:001'], un: ['UN12.004'] },
'CR06:002': { sr: ['SR01:005'], un: ['UN13.003', 'UN12.004'] },
'CR06:003': { sr: ['SR07:002'], un: ['UN12.002'] },
'CR06:003': { sr: ['SR07:002'], un: ['UN12.002', 'UN12.003'] },
'CR06:004': { sr: ['SR07:005'], un: ['UN01.002'] },
'CR06:005': { sr: ['SR07:004'], un: ['UN12.002'] },
'CR06:006': { sr: ['SR07:003'], un: ['UN01.003'] },
+82 -87
View File
@@ -30,14 +30,14 @@ export const TASKS = [
['4.7', 'Stabilization', 'Demonstration Data & Delivery Package', 'เตรียมข้อมูลสาธิต ปรับแบรนด์ และชุดติดตั้ง Docker Compose', ['ThS'], '2026-08-14', '2026-08-17', 'Demo data, Docker stack', 'Completed', 'Git dd48a8b–6c39700'],
['5.1', 'Project Close', 'Final Work-product Review', 'ตรวจสอบความครบถ้วนของ Work Products และหลักฐาน', ['ApS', 'YaB'], '2026-08-10', '2026-08-17', 'List of Evidence', 'Completed', 'ตรวจรอบที่ 4 เมื่อ 17 ส.ค. 69'],
['5.2', 'Project Close', 'Training', 'อบรมผู้ใช้งานก่อนเปิดใช้งานจริง', ['ThS', 'PaNg'], '2026-08-22', '2026-08-22', 'Training Report', 'Completed', 'ผู้เข้าอบรม 6 คน'],
['5.3', 'Project Close', 'Acceptance & Project Closure', 'ส่งมอบระบบ ตรวจรับ และปิดโครงการ', ['SeV', 'ApS'], '2026-08-17', '2026-08-24', 'Acceptance Report', 'Completed', 'ตรวจรับ 17 ส.ค. 69 ปิดโครงการ 24 ส.ค. 69'],
['5.3', 'Project Close', 'Acceptance & Project Closure', 'ส่งมอบระบบ ตรวจรับ และปิดโครงการ', ['SeV', 'ApS'], '2026-08-17', '2026-08-24', 'Acceptance Report', 'Completed', 'ตรวจรับและปิดโครงการ 24 ส.ค. 69'],
];
export const taskById = (id) => TASKS.find((t) => t[0] === id);
/** Correction Register: [id, detected, corrected, severity, problem, cause, action, ownerId, commit, testCase] */
export const CORRECTIONS = [
['ISS-001', '2026-03-03', '2026-03-09', 'สูง', 'ผลตรวจความปลอดภัยเบื้องต้นพบจุดที่ต้องแก้ไข', 'ยังไม่ได้ใช้แนวปฏิบัติ Secure Coding ครบทุกจุด', 'แก้ไขตามผลตรวจความปลอดภัยทั้งชุด', 'ThS', '7cb78d0', 'TC-UN12.002'],
['ISS-002', '2026-04-18', '2026-04-24', 'สูง', 'คำสั่งบางรายการทำให้ความถูกต้องของข้อมูลเสียหาย', 'ไม่มีการตรวจสอบความสัมพันธ์ของข้อมูลก่อนทำรายการ', 'ถอดคำสั่งที่มีปัญหาและเพิ่มการตรวจสอบความถูกต้อง', 'ThS', '92d116f', 'TC-UN08.002'],
['ISS-002', '2026-04-18', '2026-04-24', 'สูง', 'คำสั่งบางรายการทำให้ความถูกต้องของข้อมูลเสียหาย', 'ไม่มีการตรวจสอบความสัมพันธ์ของข้อมูลก่อนทำรายการ', 'ถอดคำสั่งที่มีปัญหาและเพิ่มการตรวจสอบความถูกต้อง', 'ThS', '92d116f', 'TC-UN04.002'],
['ISS-003', '2026-04-30', '2026-05-06', 'สูง', 'การตรวจสิทธิ์ตามบทบาทยังไม่ครอบคลุมทุกหน้าจอ', 'พัฒนาสิทธิ์แบบเพิ่มทีละส่วนระหว่างพัฒนา', 'เพิ่ม Role Guard ให้ครบและปรับปรุงเอกสารประกอบ', 'ThS', '2eb6a1a', 'TC-UN01.002'],
['ISS-004', '2026-05-01', '2026-05-07', 'สูง', 'พบช่องโหว่ด้านความปลอดภัยที่ยังไม่ได้ปิด', 'การตรวจสอบสิทธิ์บางเส้นทางยังไม่สมบูรณ์', 'ปิดช่องโหว่ที่พบตามผลการตรวจสอบ', 'ThS', 'a75d37e', 'TC-UN12.002'],
['ISS-005', '2026-05-02', '2026-05-08', 'กลาง', 'ชื่อตัวแปร/ฟังก์ชันไม่สอดคล้องกัน และ Session ทำงานผิดพลาด', 'พัฒนาเป็นช่วง ๆ ทำให้การตั้งชื่อไม่เป็นมาตรฐานเดียวกัน', 'ปรับชื่อให้เป็นมาตรฐานเดียวกันและแก้ปัญหา Session', 'ThS', '304848d', 'TC-UN01.003'],
@@ -45,25 +45,29 @@ export const CORRECTIONS = [
['ISS-007', '2026-05-06', '2026-05-12', 'กลาง', 'ขั้นตอน Onboarding ทำงานไม่ครบตามที่ออกแบบ', 'เงื่อนไขการรับคำเชิญไม่ครอบคลุมทุกกรณี', 'แก้ไขขั้นตอน Onboarding ให้ทำงานถูกต้อง', 'ThS', '8f1c5c4', 'TC-UN01.001'],
['ISS-008', '2026-05-07', '2026-05-13', 'กลาง', 'การลงทะเบียนผู้ใช้ใหม่ล้มเหลวในบางกรณี', 'การตรวจสอบข้อมูลลงทะเบียนไม่ครบถ้วน', 'แก้ไขขั้นตอนการลงทะเบียน', 'ThS', '4433ef1', 'TC-UN01.001'],
['ISS-009', '2026-05-07', '2026-05-13', 'สูง', 'การตรวจสอบสิทธิ์ระดับฐานข้อมูลทำงานไม่ถูกต้อง', 'การกำหนดสิทธิ์เชื่อมต่อฐานข้อมูลไม่สอดคล้องกับบทบาท', 'แก้ไขการตรวจสอบสิทธิ์ db_auth', 'ThS', '8cf1d93', 'TC-UN12.001'],
['ISS-010', '2026-05-14', '2026-05-20', 'กลาง', 'รูปแบบการเขียนโค้ดในแต่ละโมดูลไม่สอดคล้องกัน', 'พัฒนาหลายโมดูลคู่ขนานกัน', 'ทบทวนและปรับรูปแบบการเขียนโค้ดให้สอดคล้องกัน', 'ThS', '91f8bb8', 'TC-UN13.001'],
['ISS-011', '2026-05-15', '2026-05-21', 'สูง', 'การเชิญผู้ใช้ สิทธิ์แอปพลิเคชัน และโควตารายการยังไม่รัดกุม', 'ควบคุมสิทธิ์และโควตาถูกเพิ่มภายหลัง', 'แก้ไขการเชิญผู้ใช้ เพิ่มการควบคุมสิทธิ์และโควตารายการ', 'ThS', '6eeebfe', 'TC-UN12.003'],
['ISS-010', '2026-05-14', '2026-05-20', 'กลาง', 'รูปแบบการเขียนโค้ดในแต่ละโมดูลไม่สอดคล้องกัน', 'พัฒนาหลายโมดูลคู่ขนานกัน', 'ทบทวนและปรับรูปแบบการเขียนโค้ดให้สอดคล้องกัน', 'ThS', '91f8bb8', 'ทดสอบถดถอยทั้งชุด'],
['ISS-011', '2026-05-15', '2026-05-21', 'สูง', 'การเชิญผู้ใช้ สิทธิ์แอปพลิเคชัน และโควตารายการยังไม่รัดกุม', 'ควบคุมสิทธิ์และโควตาถูกเพิ่มภายหลัง', 'แก้ไขการเชิญผู้ใช้ เพิ่มการควบคุมสิทธิ์และโควตารายการ', 'ThS', '6eeebfe', 'TC-UN01.001, TC-UN01.004, TC-UN12.003'],
['ISS-012', '2026-05-15', '2026-05-21', 'สูง', 'StockManager แปลงค่า Lot/Serial ผิดพลาด ทำให้สอบกลับไม่ถูกต้อง', 'การแปลงชนิดข้อมูลไม่รองรับค่าที่เป็นข้อความ', 'แก้ไขการแปลงค่า Lot/Serial และเพิ่มชุดทดสอบวงจรเอกสาร', 'ThS', '94032dd', 'TC-UN04.004'],
['ISS-013', '2026-05-15', '2026-05-21', 'กลาง', 'ยังพบข้อบกพร่องของขั้นตอน Onboarding เพิ่มเติม', 'เส้นทางการรับคำเชิญมีหลายเงื่อนไข', 'แก้ไขข้อบกพร่องของ Onboarding ที่พบเพิ่ม', 'ThS', 'b76dc67', 'TC-UN01.001'],
['ISS-014', '2026-05-17', '2026-05-23', 'กลาง', 'เส้นทางไฟล์ (File Path) ไม่ถูกต้องในบางสภาพแวดล้อม', 'สมมติฐานเส้นทางติดตั้งไม่ตรงกัน', 'แก้ไขเส้นทางไฟล์ให้ถูกต้องทั้งระบบ', 'ThS', '59037b5, f4ef776', 'TC-UN13.001'],
['ISS-015', '2026-05-17', '2026-05-23', 'สูง', 'ผลตรวจโค้ดพบปัญหา require_once, ขั้นตอนจ่ายออก และ Role Guard', 'การควบคุมข้ามโมดูลไม่สอดคล้องกัน', 'แก้ไข require_once, ขั้นตอนจ่ายออก และเพิ่ม Role Guard', 'ThS', 'b07882e', 'TC-UN01.002'],
['ISS-015', '2026-05-17', '2026-05-23', 'สูง', 'ผลตรวจโค้ดพบปัญหา require_once, ขั้นตอนจ่ายออก และ Role Guard', 'การควบคุมข้ามโมดูลไม่สอดคล้องกัน', 'แก้ไข require_once, ขั้นตอนจ่ายออก และเพิ่ม Role Guard', 'ThS', 'b07882e', 'TC-UN01.002, TC-UN04.002'],
['ISS-016', '2026-05-18', '2026-05-24', 'สูง', 'อนุญาตให้เข้าสู่ระบบพร้อมกันหลาย Session', 'ยังไม่มีการบังคับ Session เดียวต่อบัญชี', 'บล็อกการเข้าสู่ระบบซ้ำซ้อนและปรับนโยบายยืนยันตัวตนตามบทบาท', 'ThS', '2930973', 'TC-UN01.003'],
['ISS-017', '2026-05-19', '2026-05-25', 'สูง', 'ยังพบช่องว่างของการควบคุมการเข้าสู่ระบบ', 'บางเส้นทางเข้าสู่ระบบไม่ผ่านการตรวจสอบเดียวกัน', 'ปิดช่องว่างการเข้าสู่ระบบที่พบ', 'ThS', '4733c78', 'TC-UN01.002'],
['ISS-018', '2026-05-20', '2026-05-26', 'สูง', 'ขั้นตอน Onboarding ผู้ใช้ที่ถูกเชิญต้องเพิ่มความปลอดภัย', 'มีหลายเส้นทางที่ต้องบังคับใช้การควบคุมเดียวกัน', 'ดำเนินการ Security Hardening รายการ C1–N7', 'ThS', 'b4b1f5c', 'TC-UN01.001'],
['ISS-019', '2026-05-20', '2026-05-26', 'สูง', 'วงจรชีวิตเอกสารในระบบไม่ตรงกับข้อกำหนด', 'ข้อกำหนดและการพัฒนาเปลี่ยนแปลงไม่พร้อมกัน', 'ปรับข้อกำหนดและแก้โค้ดรายการ C2/M9', 'ThS', 'cb36d3b', 'TC-UN10.002'],
['ISS-020', '2026-05-20', '2026-05-26', 'สูง', 'ข้อมูลหลักมีช่องว่างระหว่างข้อกำหนดและการพัฒนา', 'ข้อกำหนดข้อมูลหลักถูกปรับระหว่างพัฒนา', 'ปรับข้อกำหนดและแก้โค้ดรายการ C1/C2/M3–M6', 'ThS', 'dfeb575', 'TC-UN03.001'],
['ISS-021', '2026-05-21', '2026-05-27', 'กลาง', 'งานตามกำหนดเวลาของ Node.js ทำงานผิดพลาด', 'การตั้งค่า Cron ของบริการ Node.js ไม่ถูกต้อง', 'แก้ไขการทำงานของ Cron บน Node.js', 'ThS', 'f3c0e3c, 714b70d', 'TC-UN11.002'],
['ISS-022', '2026-05-21', '2026-05-27', 'กลาง', 'เอกสารประกอบภายในโครงการยังไม่ครบถ้วน', 'เอกสารถูกปรับตามการพัฒนาอย่างต่อเนื่อง', 'ทบทวนเอกสารทั้งหมดและแก้ไขส่วนที่ขาด', 'ThS', '99ae35d', 'TC-UN13.001'],
['ISS-023', '2026-05-22', '2026-05-28', 'กลาง', 'การเปลี่ยนคำว่า Rack เป็น Bin ไม่ครบทุกจุด และ ReportManager ผิดพลาด', 'เปลี่ยนชื่อไม่ครบทุกไฟล์และป้ายกำกับหน้าจอ', 'เปลี่ยนชื่อไฟล์ ป้ายกำกับ และแก้คุณสมบัติของ ReportManager', 'ThS', '5df6736', 'TC-UN03.001'],
['ISS-022', '2026-05-21', '2026-05-27', 'กลาง', 'เอกสารประกอบภายในโครงการยังไม่ครบถ้วน', 'เอกสารถูกปรับตามการพัฒนาอย่างต่อเนื่อง', 'ทบทวนเอกสารทั้งหมดและแก้ไขส่วนที่ขาด', 'ThS', '99ae35d', 'ทบทวนเอกสาร'],
['ISS-023', '2026-05-28', '2026-05-28', 'กลาง', 'การเปลี่ยนคำว่า Rack เป็น Bin ไม่ครบทุกจุด และ ReportManager ผิดพลาด', 'เปลี่ยนชื่อไม่ครบทุกไฟล์และป้ายกำกับหน้าจอ', 'เปลี่ยนชื่อไฟล์ ป้ายกำกับ และแก้คุณสมบัติของ ReportManager', 'ThS', '5df6736', 'TC-UN03.001'],
['ISS-024', '2026-05-22', '2026-05-28', 'สูง', 'การเข้าถึงตารางสต๊อกยังไม่จำกัดตามคลังของบริษัท', 'ขอบเขตสิทธิ์คลังสินค้ายังไม่ครบทุกคำสั่ง', 'จำกัดการเข้าถึงตารางสต๊อกตามคลังที่บริษัทได้รับสิทธิ์', 'ThS', '9a50238', 'TC-UN12.001'],
['ISS-025', '2026-05-22', '2026-05-28', 'ต่ำ', 'แถบเลื่อนแนวนอนเกินขอบหน้าจอจากระยะขอบ Sidebar', 'ค่าระยะขอบเกินความกว้างของ Viewport', 'แก้ไขการจัดวาง Sidebar ให้อยู่ในขอบหน้าจอ', 'ThS', 'ed3dd2f', 'TC-UN01.002'],
['ISS-025', '2026-05-22', '2026-05-28', 'ต่ำ', 'แถบเลื่อนแนวนอนเกินขอบหน้าจอจากระยะขอบ Sidebar', 'ค่าระยะขอบเกินความกว้างของ Viewport', 'แก้ไขการจัดวาง Sidebar ให้อยู่ในขอบหน้าจอ', 'ThS', 'ed3dd2f', 'ตรวจหน้าจอ'],
['ISS-026', '2026-05-22', '2026-05-28', 'สูง', 'ระบบยังรับการเข้าสู่ระบบใหม่ขณะบัญชีมี Session ทำงานอยู่', 'การบังคับ Session เดียวยังไม่ครอบคลุมทุกกรณี', 'ปฏิเสธการเข้าสู่ระบบใหม่เมื่อบัญชีมี Session ทำงานอยู่', 'ThS', 'fda211b', 'TC-UN01.003'],
['ISS-027', '2026-05-23', '2026-05-29', 'ต่ำ', 'เมธอดของคลาสมีการทำงานซ้ำซ้อน', 'พัฒนาเพิ่มทีละส่วนทำให้เกิดความซ้ำซ้อน', 'ลดความซ้ำซ้อนของเมธอดในคลาส', 'ThS', 'a0677d6', 'TC-UN13.001'],
['ISS-028', '2026-07-28', '2026-08-03', 'สูง', 'การเข้าสู่ระบบและค่าตั้งค่าสภาพแวดล้อมผิดพลาดหลัง Baseline', 'ค่าตั้งค่าของสภาพแวดล้อมใหม่ไม่ตรงกับที่ระบบคาดหวัง', 'แก้ไขการเข้าสู่ระบบและค่าตั้งค่าสภาพแวดล้อม', 'ThS', 'b2c4374', 'TC-UN12.004'],
['ISS-027', '2026-05-23', '2026-05-29', 'ต่ำ', 'เมธอดของคลาสมีการทำงานซ้ำซ้อน', 'พัฒนาเพิ่มทีละส่วนทำให้เกิดความซ้ำซ้อน', 'ลดความซ้ำซ้อนของเมธอดในคลาส', 'ThS', 'a0677d6', 'ทดสอบถดถอยทั้งชุด'],
['ISS-028', '2026-07-28', '2026-08-03', 'สูง', 'การเข้าสู่ระบบและค่าตั้งค่าสภาพแวดล้อมผิดพลาดหลัง Baseline', 'ค่าตั้งค่าของสภาพแวดล้อมใหม่ไม่ตรงกับที่ระบบคาดหวัง', 'แก้ไขการเข้าสู่ระบบและค่าตั้งค่าสภาพแวดล้อม', 'ThS', 'b2c4374', 'TC-UN01.002'],
['ISS-029', '2026-08-11', null, 'สูง', 'ไฟล์แนบเปิดได้จาก URL โดยตรงโดยไม่ตรวจ Session และบริษัท', 'ไฟล์แนบเก็บใต้โฟลเดอร์ที่เว็บเปิดให้เรียกได้', 'ย้ายไฟล์แนบออกนอก Web Root และให้ดาวน์โหลดผ่านสคริปต์ที่ตรวจ Session และบริษัท', 'ThS', null, 'TC-UN04.006'],
['ISS-030', '2026-08-12', null, 'สูง', 'เอกสารที่บันทึกแล้วไม่ถูก Rollback เมื่อการผ่าน GL ล้มเหลว', 'การผ่าน GL ทำใน Transaction แยกหลังบันทึกเอกสาร', 'รวมการบันทึกเอกสาร สต๊อก และ GL ไว้ใน Transaction เดียวตาม CR13:001', 'ThS', null, 'TC-UN08.002'],
['ISS-031', '2026-08-12', null, 'สูง', 'Socket.IO รับรหัสบริษัท ผู้ใช้ และบทบาทจาก Browser ทำให้รับแจ้งเตือนของบริษัทอื่นได้', 'การเชื่อมต่อ Socket.IO ไม่ตรวจกับ Session ของผู้ใช้', 'ตรวจ Session ฝั่งเซิร์ฟเวอร์ก่อนเข้าห้องแจ้งเตือน และไม่รับค่าบริษัท/บทบาทจาก Browser', 'ThS', null, 'TC-UN11.001, TC-UN12.001'],
['ISS-032', '2026-08-13', null, 'สูง', 'แม่แบบค่าตั้งค่ากำหนดกุญแจเข้ารหัสและ IV คงที่ และเชื่อมต่อฐานข้อมูลด้วยผู้ใช้ root', 'ค่าความลับบางส่วนยังไม่ได้แยกออกจาก Source Code', 'ย้ายกุญแจและ IV ไปเป็นความลับต่อการติดตั้ง ใช้ผู้ใช้ฐานข้อมูลสิทธิ์จำกัด และตั้งค่า HTTPS/TLS', 'ThS', null, 'TC-UN12.004'],
];
/**
@@ -78,15 +82,15 @@ export const CHANGE_CRITERIA = [
];
/** Adjustments reviewed against CHANGE_CRITERIA: [date, topic, requesterId, assessment, handledAs, reference]. */
export const CHANGE_REVIEW = [
['2026-05-23', 'ปรับคำเรียกตำแหน่งจัดเก็บจาก Rack เป็น Bin ทั้งระบบ', 'NoC',
['2026-05-18', 'ปรับคำเรียกตำแหน่งจัดเก็บจาก Rack เป็น Bin ทั้งระบบ', 'NoC',
'เป็นการปรับคำเรียกบนหน้าจอให้ตรงกับการปฏิบัติงานจริง ไม่เพิ่มหรือลดฟังก์ชัน ไม่กระทบ Man-day และส่งมอบได้แม้ไม่ปรับ',
'มติที่ประชุม ดำเนินการภายใน Task 3.9', 'Meeting Record 23 พ.ค. 69, Correction Register ISS-023'],
['2026-08-03', 'เตรียมข้อมูลสาธิตสำหรับการตรวจรับและการสาธิตระบบ', 'ApS',
'มติที่ประชุม ดำเนินการภายใน Task 3.9', 'Meeting Record 18 พ.ค. 69, Correction Register ISS-023'],
['2026-08-10', 'เตรียมข้อมูลสาธิตสำหรับการตรวจรับและการสาธิตระบบ', 'ApS',
'เป็นข้อมูลตั้งต้นสำหรับตรวจรับ ไม่ใช่สิ่งส่งมอบ และต้องแยกจากข้อมูลใช้งานจริงตาม CR07:004',
'งานเตรียมการส่งมอบ Task 4.7', 'Meeting Record 3 ส.ค. 69, 14 ส.ค. 69'],
['2026-08-14', 'ปรับอัตลักษณ์องค์กรและจัดทำชุดติดตั้ง Docker Compose', 'SeV',
'งานเตรียมการส่งมอบ Task 4.7', 'Meeting Record 10 ส.ค. 69, 19 ส.ค. 69'],
['2026-08-10', 'ปรับอัตลักษณ์องค์กรและจัดทำชุดติดตั้ง Docker Compose', 'SeV',
'ชุดติดตั้ง Docker Compose อยู่ในความต้องการ CR10:002 ตั้งแต่ Baseline และการปรับอัตลักษณ์องค์กรไม่เปลี่ยนแปลงฟังก์ชันหรือสิ่งส่งมอบ',
'งานเตรียมการส่งมอบ Task 4.7', 'Meeting Record 14 ส.ค. 69, 17 ส.ค. 69'],
'งานเตรียมการส่งมอบ Task 4.7', 'Meeting Record 10 ส.ค. 69, 24 ส.ค. 69'],
];
/**
@@ -97,7 +101,7 @@ export const VERIFICATION_ROUNDS = [
{ v: 'V0.1', date: '2026-03-17', no: 1, time: '09:00 – 12:00 น.', hours: 3, topic: 'ตรวจสอบเอกสารวางแผนโครงการและความต้องการ', wps: ['WP 1.0', 'WP 2.0'] },
{ v: 'V0.2', date: '2026-05-29', no: 2, time: '09:00 – 12:00 น.', hours: 3, topic: 'ตรวจสอบเอกสารความต้องการซอฟต์แวร์และการออกแบบ', wps: ['WP 3.0', 'WP 4.0', 'WP 5.0'] },
{ v: 'V0.3', date: '2026-07-31', no: 3, time: '09:00 – 12:00 น.', hours: 3, topic: 'ตรวจสอบชุดทดสอบและการสอบกลับ', wps: ['WP 6.0'] },
{ v: 'V1.0', date: '2026-08-17', no: 4, time: '09:00 – 16:00 น.', hours: 6, topic: 'User Acceptance Test (UAT) และตรวจสอบเอกสารส่งมอบทั้งหมด', wps: ['WP 1.0', 'WP 2.0', 'WP 3.0', 'WP 4.0', 'WP 5.0', 'WP 6.0', 'WP 7.0', 'WP 8.0', 'WP 9.0', 'WP 10.0', 'WP 11.0'] },
{ v: 'V1.0', date: '2026-08-17', no: 4, time: '09:00 – 16:00 น.', hours: 6, topic: 'ทบทวนผลการทดสอบระบบและ UAT และตรวจสอบเอกสารส่งมอบทั้งหมด', wps: ['WP 1.0', 'WP 2.0', 'WP 3.0', 'WP 4.0', 'WP 5.0', 'WP 6.0', 'WP 7.0', 'WP 8.0', 'WP 9.0', 'WP 10.0', 'WP 11.0'] },
];
/** Rules every verification round follows (plan section and each Verification Results record). */
export const VERIFICATION_RULES = [
@@ -112,7 +116,7 @@ export const VERIFICATION_RULES = [
* [theme, issue ids, what happened, preventive action for the next project]
*/
export const LESSONS = [
['ความปลอดภัยและการควบคุมสิทธิ์', ['ISS-001', 'ISS-003', 'ISS-004', 'ISS-006', 'ISS-009', 'ISS-011', 'ISS-015', 'ISS-016', 'ISS-017', 'ISS-018', 'ISS-024', 'ISS-026'],
['ความปลอดภัยและการควบคุมสิทธิ์', ['ISS-001', 'ISS-003', 'ISS-004', 'ISS-006', 'ISS-009', 'ISS-011', 'ISS-015', 'ISS-016', 'ISS-017', 'ISS-018', 'ISS-024', 'ISS-026', 'ISS-029', 'ISS-031', 'ISS-032'],
'ข้อบกพร่องกลุ่มใหญ่ที่สุดเกิดจากการเพิ่ม Role Guard การตรวจสิทธิ์ และการจำกัด Session ทีละส่วนระหว่างพัฒนา',
'กำหนด Secure Coding Checklist และรูปแบบการตรวจสิทธิ์กลางตั้งแต่ช่วงออกแบบ และทบทวนโค้ดด้านความปลอดภัยทุกโมดูลก่อนรวมเข้า Baseline'],
['ขั้นตอน Onboarding และการลงทะเบียน', ['ISS-007', 'ISS-008', 'ISS-013'],
@@ -121,7 +125,7 @@ export const LESSONS = [
['ความสอดคล้องระหว่างข้อกำหนดกับการพัฒนา', ['ISS-019', 'ISS-020', 'ISS-022'],
'ข้อกำหนดและเอกสารถูกปรับระหว่างพัฒนา ทำให้เกิดช่องว่างกับโค้ด',
'ปรับข้อกำหนดและ Traceability Record ในรอบเดียวกับการแก้โค้ด และตรวจความสอดคล้องในทุกรอบ Verification'],
['ความถูกต้องของข้อมูลสต๊อกและบัญชี', ['ISS-002', 'ISS-012'],
['ความถูกต้องของข้อมูลสต๊อกและบัญชี', ['ISS-002', 'ISS-012', 'ISS-030'],
'การแปลงชนิดข้อมูลและการตรวจสอบความสัมพันธ์ของข้อมูลไม่ครอบคลุม',
'ตรวจสอบข้อมูลนำเข้าและความสัมพันธ์ฝั่งเซิร์ฟเวอร์ทุกคำสั่งที่เปลี่ยนสต๊อกหรือ GL และเพิ่มชุดทดสอบวงจรเอกสาร'],
['มาตรฐานโค้ดและการตั้งค่าสภาพแวดล้อม', ['ISS-005', 'ISS-010', 'ISS-014', 'ISS-021', 'ISS-023', 'ISS-025', 'ISS-027', 'ISS-028'],
@@ -134,21 +138,21 @@ export const LESSONS = [
* [date, from, to, weekNo, taskRows[[taskId, status, pct, remark]], issues[ISS ids], changes[CH ids], next{date,topic}]
*/
export const PERIODS = [
['2026-01-09', '2026-01-05', '2026-01-09', 1, [['1.1', 'Completed', 100, 'เปิดโครงการอย่างเป็นทางการ ชี้แจงขอบเขตและทีมงานครบทุกบทบาท'], ['1.2', 'In Progress', 40, 'อยู่ระหว่างรวบรวมรายชื่อผู้มีส่วนได้ส่วนเสียและระดับการมีส่วนร่วม']], [], [], { date: '2026-01-16', topic: 'ทบทวนรายชื่อผู้มีส่วนได้ส่วนเสียและเริ่มกำหนดวัตถุประสงค์โครงการ' }],
['2026-01-23', '2026-01-12', '2026-01-23', 2, [['1.2', 'Completed', 100, 'จัดทำ Stakeholder Register ครบ 6 บทบาทหลักและผู้ใช้งานที่เกี่ยวข้อง'], ['1.3', 'Completed', 100, 'กำหนดวัตถุประสงค์ ขอบเขตระบบ 11 ระบบงาน และเกณฑ์ความสำเร็จ'], ['1.4', 'Completed', 100, 'Project Charter ได้รับอนุมัติจาก Project Sponsor'], ['2.1', 'In Progress', 30, 'เริ่มเก็บความต้องการลูกค้าจากฝ่ายคลังสินค้า ขาย และบัญชี']], [], [], { date: '2026-02-06', topic: 'สรุปความต้องการลูกค้าและทบทวนร่างแผนโครงการ' }],
['2026-02-06', '2026-01-26', '2026-02-06', 3, [['2.1', 'Completed', 100, `สรุปความต้องการลูกค้า ${CRS.length} รายการ จัดหมวด CR01–CR14 และได้ผลสรุป A ทุกรายการ`], ['2.2', 'In Progress', 60, 'อยู่ระหว่างจัดทำ Software Project Plan และ Work Schedule']], [], [], { date: '2026-02-13', topic: 'ทบทวน Software Project Plan, Work Schedule และแผนความเสี่ยง' }],
['2026-02-18', '2026-02-09', '2026-02-18', 4, [['2.2', 'Completed', 100, 'จัดทำแผนโครงการ ทรัพยากร ความเสี่ยง R1–R8 และแผนสำรองงานที่ไม่แล้วเสร็จ'], ['2.3', 'Completed', 100, 'ตั้ง Baseline ความต้องการและแผนงาน อนุมัติเริ่มพัฒนา 19 ก.พ. 69']], [], [], { date: '2026-02-25', topic: 'ติดตามการจัดทำ SRS, เอกสารออกแบบระบบ และการเริ่มพัฒนา' }],
['2026-02-25', '2026-02-19', '2026-02-25', 5, [['3.1', 'Completed', 100, 'จัดทำ SRS (SR01–SR09) และเอกสารออกแบบระบบ เริ่มพัฒนาโครงสร้างแอปพลิเคชัน'], ['4.2', 'In Progress', 20, 'เริ่มควบคุม Source Code บน Git และกำหนด main เป็น Baseline']], [], [], { date: '2026-03-17', topic: 'ติดตามผลการแก้ไขด้านความปลอดภัยและการออกแบบฐานข้อมูลสต๊อก' }],
['2026-03-17', '2026-03-09', '2026-03-17', 6, [['3.2', 'Completed', 100, 'ป้องกันไฟล์ตั้งค่า แก้ผลตรวจความปลอดภัย ออกแบบฐานข้อมูลสต๊อกและโมดูล ICS'], ['4.3', 'In Progress', 10, 'เริ่มตรวจสอบเอกสารความต้องการและออกแบบรอบที่ 1']], ['ISS-001'], [], { date: '2026-04-29', topic: 'ติดตามการพัฒนาโมดูลคลังสินค้า สินค้า และรายงาน' }],
['2026-04-29', '2026-04-09', '2026-04-29', 7, [['3.3', 'Completed', 100, 'พัฒนาแดชบอร์ดสต๊อก สินค้า ความจุคลัง Lot/Serial/Expiry และรายงานครบตามแผน'], ['3.4', 'In Progress', 50, 'อยู่ระหว่างพัฒนาเข้าสู่ระบบ ลงทะเบียน และ Onboarding']], ['ISS-002'], [], { date: '2026-05-08', topic: 'ติดตามการพัฒนาระบบสิทธิ์ ใบสั่งขาย และบาร์โค้ด' }],
['2026-05-08', '2026-04-30', '2026-05-08', 8, [['3.4', 'In Progress', 80, 'พัฒนาการเข้าสู่ระบบและสิทธิ์ตามบทบาทเกือบครบ เหลือขั้นตอนกู้คืนรหัสผ่าน'], ['3.5', 'Completed', 100, 'พัฒนาใบสั่งขาย ใบรับคืน ใบแจ้งหนี้ คลังหลายชั้น และบาร์โค้ดครบตามแผน'], ['3.9', 'In Progress', 25, 'เริ่มทบทวน Role Guard และการรวมศูนย์การตรวจสิทธิ์']], ['ISS-003', 'ISS-004', 'ISS-005'], [], { date: '2026-05-13', topic: 'ติดตามการเตรียมใช้งานจริง สคริปต์ติดตั้ง และการเริ่มโมดูลบัญชี' }],
['2026-05-13', '2026-05-11', '2026-05-13', 9, [['3.4', 'Completed', 100, 'พัฒนาการเข้าสู่ระบบ Onboarding และกู้คืนรหัสผ่านครบถ้วน'], ['3.6', 'Completed', 100, 'จัดทำ Base URL อัตโนมัติ สคริปต์ติดตั้งฐานข้อมูล และข้อมูลทดสอบ'], ['3.7', 'In Progress', 20, 'เริ่มพัฒนาโมดูลบัญชีและผังบัญชี']], ['ISS-006', 'ISS-007', 'ISS-008', 'ISS-009'], [], { date: '2026-05-23', topic: 'ติดตามการพัฒนาบัญชี การเงิน และการควบคุมสิทธิ์ผู้ใช้' }],
['2026-05-23', '2026-05-20', '2026-05-23', 10, [['3.7', 'Completed', 100, 'พัฒนาผังบัญชี GL สมุดรายวัน รายงานบัญชี ใบวางบิลและใบเสร็จครบตามแผน'], ['3.8', 'In Progress', 45, 'อยู่ระหว่างพัฒนา Socket.IO และตารางสรุปยอด'], ['3.9', 'In Progress', 60, 'เพิ่มการเชิญผู้ใช้ การควบคุมสิทธิ์แอปพลิเคชัน และโควตารายการ']], ['ISS-010', 'ISS-011', 'ISS-012', 'ISS-013', 'ISS-014', 'ISS-015'], [], { date: '2026-05-29', topic: 'สรุปผลการพัฒนาและตั้ง Baseline การพัฒนา' }],
['2026-05-29', '2026-05-24', '2026-05-29', 11, [['3.8', 'Completed', 100, 'พัฒนาบริการแจ้งเตือน Real-time ตารางสรุปยอด และงานตามกำหนดเวลาครบถ้วน'], ['3.9', 'Completed', 100, 'ทบทวนความปลอดภัย ขอบเขตบริษัท วงจรเอกสาร และปิดข้อบกพร่องที่พบ'], ['3.10', 'Completed', 100, 'ลดความซ้ำซ้อนของคลาสและตั้ง Baseline การพัฒนาเมื่อ 29 พ.ค. 69']], ['ISS-016', 'ISS-017', 'ISS-018', 'ISS-019', 'ISS-020', 'ISS-021', 'ISS-022', 'ISS-023', 'ISS-024', 'ISS-025', 'ISS-026', 'ISS-027'], [], { date: '2026-08-03', topic: 'ติดตามการตรวจสอบเอกสาร การจัดทำคู่มือ และการแก้ไขหลัง Baseline' }],
['2026-08-03', '2026-06-01', '2026-08-03', 12, [['4.3', 'In Progress', 70, 'ตรวจสอบเอกสารความต้องการ ออกแบบ และการสอบกลับ รอบที่ 1–3'], ['4.5', 'In Progress', 80, 'จัดทำคู่มือผู้ใช้ คู่มือผู้ดูแลระบบ และคู่มือบำรุงรักษา'], ['4.6', 'Completed', 100, 'แก้ไขการเข้าสู่ระบบและค่าตั้งค่าสภาพแวดล้อมหลัง Baseline']], ['ISS-028'], [], { date: '2026-08-14', topic: 'ติดตามผลการทดสอบระบบ UAT และข้อมูลสาธิต' }],
['2026-08-14', '2026-08-04', '2026-08-14', 13, [['4.4', 'Completed', 100, `ทดสอบระบบ 45 Test Case และ UAT ครบ 14 หมวดความต้องการ (${CRS.length} รายการ) ผ่านทั้งหมดระหว่าง 10–14 ส.ค. 69`], ['4.7', 'In Progress', 60, 'จัดทำข้อมูลสาธิตเสร็จ อยู่ระหว่างปรับแบรนด์และชุดติดตั้ง Docker Compose'], ['5.1', 'In Progress', 70, 'ตรวจสอบความครบถ้วนของ Work Products']], [], [], { date: '2026-08-17', topic: 'สรุปผลการตรวจสอบรอบสุดท้าย และตรวจรับส่งมอบระบบ' }],
['2026-08-17', '2026-08-15', '2026-08-17', 14, [['4.3', 'Completed', 100, 'ตรวจสอบ Work Products รอบที่ 4 ครบทุกรายการ ผลผ่านทั้งหมด'], ['4.5', 'Completed', 100, 'คู่มือผู้ใช้ คู่มือผู้ดูแลระบบ และคู่มือบำรุงรักษาเสร็จสมบูรณ์'], ['4.7', 'Completed', 100, 'ปรับแบรนด์และชุดติดตั้ง Docker Compose เสร็จ Baseline 6c39700'], ['5.1', 'Completed', 100, 'จัดทำ List of Evidence ครบทุก Work Product'], ['5.3', 'In Progress', 80, 'ลูกค้าตรวจรับระบบเมื่อ 17 ส.ค. 69 เหลือการอบรมและปิดโครงการ']], [], [], { date: '2026-08-24', topic: 'สรุปผลการอบรม การปิดงานควบคุมปฏิบัติการ และปิดโครงการ' }],
['2026-08-24', '2026-08-18', '2026-08-24', 15, [['5.2', 'Completed', 100, 'อบรมผู้ใช้งาน 6 คน เมื่อ 22 ส.ค. 69 ผ่านการทดลองใช้งานครบทุกขั้นตอน'], ['5.3', 'Completed', 100, 'ปิดงานควบคุมปฏิบัติการ 23 ส.ค. 69 และปิดโครงการอย่างเป็นทางการ 24 ส.ค. 69'], ['4.1', 'Completed', 100, 'บันทึกความก้าวหน้าและรายงานการประชุมครบตลอดโครงการ'], ['4.2', 'Completed', 100, 'ควบคุมเวอร์ชันและสำรองข้อมูลครบ']], [], [], null],
['2026-01-09', '2026-01-05', '2026-01-09', 1, [['1.1', 'Completed', 100, 'เปิดโครงการอย่างเป็นทางการ ชี้แจงขอบเขตและทีมงานครบทุกบทบาท'], ['1.2', 'In Progress', 40, 'อยู่ระหว่างรวบรวมรายชื่อผู้มีส่วนได้ส่วนเสียและระดับการมีส่วนร่วม']], [], [], { date: '2026-01-23', topic: 'พิจารณาอนุมัติกฎบัตรโครงการและทบทวนผู้มีส่วนได้ส่วนเสีย' }],
['2026-01-23', '2026-01-12', '2026-01-23', 2, [['1.2', 'Completed', 100, 'จัดทำ Stakeholder Register ครบ 6 บทบาทหลักและผู้ใช้งานที่เกี่ยวข้อง'], ['1.3', 'Completed', 100, 'กำหนดวัตถุประสงค์ ขอบเขตระบบ 11 ระบบงาน และเกณฑ์ความสำเร็จ'], ['1.4', 'Completed', 100, 'Project Charter ได้รับอนุมัติจาก Project Sponsor'], ['2.1', 'In Progress', 30, 'เริ่มเก็บความต้องการลูกค้าจากฝ่ายคลังสินค้า ขาย และบัญชี']], [], [], { date: '2026-02-18', topic: 'ทบทวนความต้องการลูกค้า แผนโครงการ และตั้ง Baseline' }],
['2026-02-06', '2026-01-26', '2026-02-06', 3, [['2.1', 'Completed', 100, `สรุปความต้องการลูกค้า ${CRS.length} รายการ จัดหมวด CR01–CR14 และได้ผลสรุป A ทุกรายการ`], ['2.2', 'In Progress', 60, 'อยู่ระหว่างจัดทำ Software Project Plan และ Work Schedule']], [], [], { date: '2026-02-18', topic: 'ทบทวนความต้องการลูกค้า แผนโครงการ และตั้ง Baseline' }],
['2026-02-18', '2026-02-09', '2026-02-18', 4, [['2.2', 'Completed', 100, 'จัดทำแผนโครงการ ทรัพยากร ความเสี่ยง R1–R8 และแผนสำรองงานที่ไม่แล้วเสร็จ'], ['2.3', 'Completed', 100, 'ตั้ง Baseline ความต้องการและแผนงาน อนุมัติเริ่มพัฒนา 19 ก.พ. 69']], [], [], { date: '2026-03-23', topic: 'สรุปผลการแก้ไขด้านความปลอดภัยและการออกแบบฐานข้อมูลสต๊อก' }],
['2026-02-25', '2026-02-19', '2026-02-25', 5, [['3.1', 'Completed', 100, 'จัดทำ SRS (SR01–SR09) และเอกสารออกแบบระบบ เริ่มพัฒนาโครงสร้างแอปพลิเคชัน'], ['4.2', 'In Progress', 20, 'เริ่มควบคุม Source Code บน Git และกำหนด main เป็น Baseline']], [], [], { date: '2026-03-23', topic: 'สรุปผลการแก้ไขด้านความปลอดภัยและการออกแบบฐานข้อมูลสต๊อก' }],
['2026-03-17', '2026-03-09', '2026-03-17', 6, [['3.2', 'Completed', 100, 'ป้องกันไฟล์ตั้งค่า แก้ผลตรวจความปลอดภัย ออกแบบฐานข้อมูลสต๊อกและโมดูล ICS'], ['4.3', 'In Progress', 10, 'เริ่มตรวจสอบเอกสารความต้องการและออกแบบรอบที่ 1']], ['ISS-001'], [], { date: '2026-03-23', topic: 'สรุปผลการแก้ไขด้านความปลอดภัยและการออกแบบฐานข้อมูลสต๊อก' }],
['2026-04-29', '2026-04-09', '2026-04-29', 7, [['3.3', 'Completed', 100, 'พัฒนาแดชบอร์ดสต๊อก สินค้า ความจุคลัง Lot/Serial/Expiry และรายงานครบตามแผน'], ['3.4', 'In Progress', 50, 'อยู่ระหว่างพัฒนาเข้าสู่ระบบ ลงทะเบียน และ Onboarding']], ['ISS-002'], [], { date: '2026-05-04', topic: 'สรุปผลการพัฒนาโมดูลคลังสินค้า สินค้า และรายงาน' }],
['2026-05-08', '2026-04-30', '2026-05-08', 8, [['3.4', 'In Progress', 80, 'พัฒนาการเข้าสู่ระบบและสิทธิ์ตามบทบาทเกือบครบ เหลือขั้นตอนกู้คืนรหัสผ่าน'], ['3.5', 'Completed', 100, 'พัฒนาใบสั่งขาย ใบรับคืน ใบแจ้งหนี้ คลังหลายชั้น และบาร์โค้ดครบตามแผน'], ['3.9', 'In Progress', 25, 'เริ่มทบทวน Role Guard และการรวมศูนย์การตรวจสิทธิ์']], ['ISS-003', 'ISS-004', 'ISS-005'], [], { date: '2026-05-18', topic: 'ติดตามการเตรียมใช้งานจริง สคริปต์ติดตั้ง และการเริ่มโมดูลบัญชี' }],
['2026-05-13', '2026-05-11', '2026-05-13', 9, [['3.4', 'Completed', 100, 'พัฒนาการเข้าสู่ระบบ Onboarding และกู้คืนรหัสผ่านครบถ้วน'], ['3.6', 'Completed', 100, 'จัดทำ Base URL อัตโนมัติ สคริปต์ติดตั้งฐานข้อมูล และข้อมูลทดสอบ'], ['3.7', 'In Progress', 20, 'เริ่มพัฒนาโมดูลบัญชีและผังบัญชี']], ['ISS-006', 'ISS-007', 'ISS-008', 'ISS-009'], [], { date: '2026-05-18', topic: 'ติดตามการเตรียมใช้งานจริง สคริปต์ติดตั้ง และการเริ่มโมดูลบัญชี' }],
['2026-05-25', '2026-05-20', '2026-05-23', 10, [['3.7', 'Completed', 100, 'พัฒนาผังบัญชี GL สมุดรายวัน รายงานบัญชี ใบวางบิลและใบเสร็จครบตามแผน'], ['3.8', 'In Progress', 45, 'อยู่ระหว่างพัฒนา Socket.IO และตารางสรุปยอด'], ['3.9', 'In Progress', 60, 'เพิ่มการเชิญผู้ใช้ การควบคุมสิทธิ์แอปพลิเคชัน และโควตารายการ']], ['ISS-010', 'ISS-011', 'ISS-012', 'ISS-013', 'ISS-014', 'ISS-015'], [], { date: '2026-05-28', topic: 'ติดตามการพัฒนาบัญชี การเงิน และการควบคุมสิทธิ์ผู้ใช้' }],
['2026-05-29', '2026-05-24', '2026-05-29', 11, [['3.8', 'Completed', 100, 'พัฒนาบริการแจ้งเตือน Real-time ตารางสรุปยอด และงานตามกำหนดเวลาครบถ้วน'], ['3.9', 'Completed', 100, 'ทบทวนความปลอดภัย ขอบเขตบริษัท วงจรเอกสาร และปิดข้อบกพร่องที่พบ'], ['3.10', 'Completed', 100, 'ลดความซ้ำซ้อนของคลาสและตั้ง Baseline การพัฒนาเมื่อ 29 พ.ค. 69']], ['ISS-016', 'ISS-017', 'ISS-018', 'ISS-019', 'ISS-020', 'ISS-021', 'ISS-022', 'ISS-023', 'ISS-024', 'ISS-025', 'ISS-026', 'ISS-027'], [], { date: '2026-06-04', topic: 'สรุปผลการพัฒนาและตั้ง Baseline การพัฒนา' }],
['2026-08-03', '2026-06-01', '2026-08-03', 12, [['4.3', 'In Progress', 70, 'ตรวจสอบเอกสารความต้องการ ออกแบบ และการสอบกลับ รอบที่ 1–3'], ['4.5', 'In Progress', 80, 'จัดทำคู่มือผู้ใช้ คู่มือผู้ดูแลระบบ และคู่มือบำรุงรักษา'], ['4.6', 'Completed', 100, 'แก้ไขการเข้าสู่ระบบและค่าตั้งค่าสภาพแวดล้อมหลัง Baseline']], ['ISS-028'], [], { date: '2026-08-10', topic: 'ติดตามการแก้ไขหลัง Baseline การตรวจสอบเอกสาร และแผนการทดสอบ' }],
['2026-08-14', '2026-08-04', '2026-08-14', 13, [['4.4', 'Completed', 100, `ทดสอบระบบ 45 Test Case ผ่าน 38 ไม่ผ่าน 5 ยังไม่ได้ทดสอบ 2 และ UAT ครบ 14 หมวดความต้องการ (${CRS.length} รายการ) ระหว่าง 10–14 ส.ค. 69 บันทึกข้อบกพร่องคงค้าง ISS-029–ISS-032`], ['4.7', 'In Progress', 60, 'จัดทำข้อมูลสาธิตเสร็จ อยู่ระหว่างปรับแบรนด์และชุดติดตั้ง Docker Compose'], ['5.1', 'In Progress', 70, 'ตรวจสอบความครบถ้วนของ Work Products']], ['ISS-029', 'ISS-030', 'ISS-031', 'ISS-032'], [], { date: '2026-08-19', topic: 'สรุปผลการทดสอบระบบ UAT และข้อมูลสาธิต' }],
['2026-08-17', '2026-08-15', '2026-08-17', 14, [['4.3', 'Completed', 100, 'ตรวจสอบ Work Products รอบที่ 4 ครบทุกรายการ ไม่ผ่าน 2 รายการ คือผลการทดสอบที่มีข้อบกพร่องคงค้าง และชุดติดตั้ง Docker ที่ยังไม่ได้ทดสอบ'], ['4.5', 'Completed', 100, 'คู่มือผู้ใช้ คู่มือผู้ดูแลระบบ และคู่มือบำรุงรักษาเสร็จสมบูรณ์'], ['4.7', 'Completed', 100, 'ปรับแบรนด์และชุดติดตั้ง Docker Compose เสร็จ Baseline 6c39700'], ['5.1', 'Completed', 100, 'จัดทำ List of Evidence ครบทุก Work Product'], ['5.3', 'In Progress', 20, 'ส่งมอบสิ่งส่งมอบครบ นัดตรวจรับและปิดโครงการ 24 ส.ค. 69']], [], [], { date: '2026-08-24', topic: 'ตรวจรับส่งมอบระบบและปิดโครงการอย่างเป็นทางการ' }],
['2026-08-24', '2026-08-18', '2026-08-24', 15, [['5.2', 'Completed', 100, 'อบรมผู้ใช้งาน 6 คน เมื่อ 22 ส.ค. 69 ผ่านการทดลองใช้งานครบทุกขั้นตอน'], ['5.3', 'Completed', 100, 'ปิดงานควบคุมปฏิบัติการ 23 ส.ค. 69 ตรวจรับระบบและปิดโครงการอย่างเป็นทางการ 24 ส.ค. 69'], ['4.1', 'Completed', 100, 'บันทึกความก้าวหน้าและรายงานการประชุมครบตลอดโครงการ'], ['4.2', 'Completed', 100, 'ควบคุมเวอร์ชันและสำรองข้อมูลครบ']], [], [], null],
];
/**
@@ -163,7 +167,7 @@ export const MEETINGS = [
agenda: [['แนะนำสมาชิกทีมโครงการ', 'สร้างความเข้าใจบทบาทและหน้าที่'], ['ทบทวนความจำเป็นและวัตถุประสงค์ของระบบคลังสินค้า', 'กำหนดเป้าหมายร่วมกัน'], ['ภาพรวมระบบงาน 11 ระบบที่จะพัฒนา', 'เห็นขอบเขตและแผนการพัฒนา'], ['สรุปแผนโครงการ 232 วัน', 'ยืนยัน Timeline 5 ม.ค. – 24 ส.ค. 69'], ['กำหนดรอบการประชุมและการรายงานความก้าวหน้า', 'ตั้งรอบประชุม ผู้รับผิดชอบ และเอกสารอ้างอิง']],
discussion: [['วัตถุประสงค์โครงการ', 'พัฒนาระบบบริหารจัดการคลังสินค้าแบบรวมศูนย์ รองรับหลายบริษัทและหลายคลัง', ['SeV', 'ApS'], 'ยืนยันแล้ว'], ['ระยะเวลาโครงการ', 'ระยะเวลา 232 วัน ตั้งแต่ 5 ม.ค. – 24 ส.ค. 69', ['ApS'], 'อนุมัติ'], ['ทรัพยากรและทีมงาน', 'ทีมหลัก 6 คน ครบบทบาท PM, SA, Developer, QA, Document Control และ Project Sponsor', ['ApS', 'YaB'], 'ผ่านการอนุมัติ'], ['Project Repository', 'ใช้ Git Server เป็นศูนย์กลางเก็บ Source Code และเอกสารโครงการ', ['ThS', 'YaB'], 'ดำเนินการแล้ว']],
actions: [['จัดทำ Stakeholder Register (Task 1.2)', ['ApS', 'YaB'], '2026-01-16', 'Scheduled'], ['เตรียม Repository และโครงสร้างโฟลเดอร์เอกสารโครงการ', ['ThS'], '2026-01-16', 'Scheduled']],
next: { date: '2026-01-16', topic: 'ทบทวนรายชื่อผู้มีส่วนได้ส่วนเสียและกำหนดวัตถุประสงค์โครงการ' },
next: { date: '2026-01-23', topic: 'พิจารณาอนุมัติกฎบัตรโครงการและทบทวนผู้มีส่วนได้ส่วนเสีย' },
},
{
date: '2026-01-23', phase: 'Project Initiation', taskId: '1.4', taskName: 'Approve Project Charter', time: '09:00 – 11:00 น.',
@@ -172,7 +176,7 @@ export const MEETINGS = [
agenda: [['พิจารณาอนุมัติกฎบัตรโครงการ (Project Charter)', 'ยืนยันวัตถุประสงค์ ขอบเขต และงบประมาณ'], ['ทบทวน Stakeholder Register', 'ยืนยันบทบาทและระดับการมีส่วนร่วม'], ['แผนการเก็บความต้องการลูกค้า', 'กำหนดผู้ให้ข้อมูลและกรอบเวลา']],
discussion: [['อนุมัติกฎบัตรโครงการ', 'Project Sponsor อนุมัติวัตถุประสงค์ ขอบเขต 11 ระบบงาน และเกณฑ์ความสำเร็จ', ['SeV'], 'อนุมัติ'], ['ผู้มีส่วนได้ส่วนเสีย', 'ยืนยันผู้รับผิดชอบตาม RACI ครบทุกบทบาท', ['ApS', 'YaB'], 'ยืนยันแล้ว'], ['การเก็บความต้องการ', 'มอบหมาย System Analyst สัมภาษณ์ฝ่ายคลังสินค้า ขาย จัดซื้อ และบัญชี', ['NoC'], 'ดำเนินการ']],
actions: [['เก็บและสรุปความต้องการลูกค้า (Task 2.1)', ['NoC', 'ApS'], '2026-02-06', 'Scheduled'], ['จัดทำร่าง Software Project Plan และ Work Schedule (Task 2.2)', ['ApS'], '2026-02-13', 'Scheduled']],
next: { date: '2026-02-06', topic: 'สรุปความต้องการลูกค้าและทบทวนร่างแผนโครงการ' },
next: { date: '2026-02-18', topic: 'ทบทวนความต้องการลูกค้า แผนโครงการ และตั้ง Baseline' },
},
{
date: '2026-02-18', phase: 'Project Planning', taskId: '2.3', taskName: 'Baseline Requirements & Schedule', time: '09:00 – 11:00 น.',
@@ -181,87 +185,78 @@ export const MEETINGS = [
agenda: [['ทบทวนความต้องการลูกค้า CR01–CR14', 'ยืนยันความครบถ้วนและความเป็นไปได้'], ['ทบทวน Software Project Plan และ Work Schedule', 'ยืนยันแผนงาน ทรัพยากร และความเสี่ยง'], ['อนุมัติเริ่มพัฒนาระบบ', 'ยืนยันวันเริ่มพัฒนาและการควบคุม Source Code']],
discussion: [['ความต้องการลูกค้า', `ความต้องการ ${CRS.length} รายการ จัดหมวด CR01–CR14 ผลสรุปยอมรับ (A) ทุกรายการ`, ['NoC', 'SeV'], 'อนุมัติ'], ['แผนโครงการและความเสี่ยง', 'ยืนยันแผน 232 วัน ความเสี่ยง R1–R8 และแผนสำรองงานที่ไม่แล้วเสร็จ', ['ApS'], 'อนุมัติ'], ['การควบคุม Source Code', 'กำหนด main เป็น Baseline หลัก ไม่นำค่าตั้งค่าและความลับเข้า Git', ['ThS', 'YaB'], 'บังคับใช้ทันที']],
actions: [['เริ่มพัฒนาระบบตาม Task 3.1 ในวันที่ 19 ก.พ. 69', ['ThS', 'NoC'], '2026-02-25', 'Scheduled'], ['จัดทำ SRS และเอกสารออกแบบระบบ', ['NoC', 'ThS'], '2026-02-25', 'Scheduled'], ['รายงานความก้าวหน้าตามรหัสงานใน Work Schedule', ['ApS'], '2026-08-24', 'Ongoing']],
next: { date: '2026-02-25', topic: 'ติดตามการจัดทำ SRS เอกสารออกแบบ และการเริ่มพัฒนาระบบ' },
next: { date: '2026-03-23', topic: 'สรุปผลการแก้ไขด้านความปลอดภัยและการออกแบบฐานข้อมูลสต๊อก' },
},
{
date: '2026-03-17', phase: 'Project Execution', taskId: '3.2', taskName: 'Security & Database Foundation', time: '09:00 – 11:00 น.',
attachment: 'Progress Status Record 17 มี.ค. 69',
date: '2026-03-23', phase: 'Project Execution', taskId: '3.2', taskName: 'Security & Database Foundation', time: '09:00 – 11:00 น.',
attachment: '-',
progress: [['Phase 3: Project Execution', 'กำลังดำเนินการ', 25, 'พัฒนาฐานข้อมูลสต๊อกและโมดูล ICS เบื้องต้น'], ['รวมความคืบหน้าทั้งโครงการ', 'ตามแผน', 30, 'การพัฒนาเป็นไปตามแผน']],
agenda: [['ผลการแก้ไขตามผลตรวจความปลอดภัย', 'ยืนยันการปิดข้อบกพร่อง ISS-001'], ['การออกแบบฐานข้อมูลสต๊อกและโมดูล ICS', 'ทบทวนโครงสร้างตารางและความสัมพันธ์'], ['แผนพัฒนาโมดูลคลังสินค้าและรายงาน', 'ยืนยันลำดับงานช่วงเดือนเมษายน']],
discussion: [['ความปลอดภัยของระบบ', 'ถอดไฟล์ตั้งค่าออกจาก Git และแก้ไขตามผลตรวจความปลอดภัยครบถ้วน', ['ThS'], 'เสร็จสิ้น'], ['ฐานข้อมูลสต๊อก', 'ยืนยันโครงสร้างตาราง td_stock และตารางข้อมูลหลักที่เกี่ยวข้อง', ['NoC', 'ThS'], 'ยืนยันแล้ว'], ['แผนงานถัดไป', 'พัฒนาแดชบอร์ดสต๊อก สินค้า ความจุคลัง และรายงาน', ['ThS'], 'ดำเนินการ']],
actions: [['บันทึกการแก้ไข ISS-001 ใน Correction Register', ['YaB'], '2026-03-20', 'Scheduled'], ['พัฒนาโมดูลคลังสินค้าและรายงาน (Task 3.3)', ['ThS'], '2026-04-29', 'Scheduled']],
next: { date: '2026-04-29', topic: 'ติดตามการพัฒนาโมดูลคลังสินค้า สินค้า และรายงาน' },
actions: [['บันทึกการแก้ไข ISS-001 ใน Correction Register', ['YaB'], '2026-03-25', 'Scheduled'], ['พัฒนาโมดูลคลังสินค้าและรายงาน (Task 3.3)', ['ThS'], '2026-04-29', 'Scheduled']],
next: { date: '2026-05-04', topic: 'ติดตามการพัฒนาโมดูลคลังสินค้า สินค้า และรายงาน' },
},
{
date: '2026-04-29', phase: 'Project Execution', taskId: '3.3', taskName: 'Inventory & Warehouse Modules', time: '09:00 – 11:00 น.',
attachment: 'Progress Status Record 29 เม.ย. 69',
date: '2026-05-04', phase: 'Project Execution', taskId: '3.3', taskName: 'Inventory & Warehouse Modules', time: '09:00 – 11:00 น.',
attachment: '-',
progress: [['Phase 3: Project Execution', 'กำลังดำเนินการ', 50, 'พัฒนาโมดูลคลังสินค้า สินค้า Lot/Serial/Expiry และรายงานเสร็จ'], ['รวมความคืบหน้าทั้งโครงการ', 'ตามแผน', 45, 'เข้าสู่การพัฒนาระบบสิทธิ์และเอกสารธุรกิจ']],
agenda: [['ผลการพัฒนาโมดูลคลังสินค้าและรายงาน', 'ตรวจสอบความครบถ้วนตาม CR01:005–CR01:012'], ['ประเด็นความถูกต้องของข้อมูล', 'พิจารณาการแก้ไข ISS-002'], ['แผนพัฒนาระบบสิทธิ์ ใบสั่งขาย และบาร์โค้ด', 'ยืนยันลำดับงานช่วงต้นเดือนพฤษภาคม']],
discussion: [['โมดูลคลังสินค้า', 'พัฒนาแดชบอร์ดสต๊อก ความจุคลัง Lot/Serial/Expiry และรายงานครบตามความต้องการ', ['ThS', 'NoC'], 'เสร็จสิ้น'], ['ความถูกต้องของข้อมูล', 'ถอดคำสั่งที่กระทบความถูกต้องและเพิ่มการตรวจสอบก่อนทำรายการ', ['ThS'], 'เสร็จสิ้น'], ['ระบบสิทธิ์ผู้ใช้', 'มอบหมายพัฒนา Role Guard และรวมศูนย์การตรวจสิทธิ์', ['ThS'], 'ดำเนินการ']],
actions: [['บันทึกการแก้ไข ISS-002 ใน Correction Register', ['YaB'], '2026-05-01', 'Scheduled'], ['พัฒนาระบบเข้าสู่ระบบ สิทธิ์ และเอกสารขาย (Task 3.4, 3.5)', ['ThS'], '2026-05-12', 'Scheduled']],
next: { date: '2026-05-08', topic: 'ติดตามการพัฒนาระบบสิทธิ์ ใบสั่งขาย และบาร์โค้ด' },
actions: [['บันทึกการแก้ไข ISS-002 ใน Correction Register', ['YaB'], '2026-05-06', 'Scheduled'], ['พัฒนาระบบเข้าสู่ระบบ สิทธิ์ และเอกสารขาย (Task 3.4, 3.5)', ['ThS'], '2026-05-12', 'Scheduled']],
next: { date: '2026-05-18', topic: 'ติดตามการพัฒนาระบบสิทธิ์ ใบสั่งขาย และบาร์โค้ด' },
},
{
date: '2026-05-13', phase: 'Project Execution', taskId: '3.6', taskName: 'Production Preparation & Setup', time: '09:00 – 11:00 น.',
attachment: 'Progress Status Record 13 พ.ค. 69',
date: '2026-05-18', phase: 'Project Execution', taskId: '3.6', taskName: 'Production Preparation & Setup', time: '09:00 – 11:00 น.',
attachment: '-',
progress: [['Phase 3: Project Execution', 'กำลังดำเนินการ', 70, 'ระบบสิทธิ์ เอกสารขาย และสคริปต์ติดตั้งเสร็จ'], ['รวมความคืบหน้าทั้งโครงการ', 'ตามแผน', 60, 'เริ่มพัฒนาโมดูลบัญชีและการเงิน']],
agenda: [['ผลการพัฒนาระบบสิทธิ์และเอกสารขาย', 'ตรวจสอบความครบถ้วนตาม CR01:001–CR01:004 และ CR01:013'], ['ประเด็นความปลอดภัยและ Onboarding ที่พบ', 'พิจารณาการแก้ไข ISS-003 ถึง ISS-009'], ['แผนพัฒนาบัญชีและการเงิน', 'ยืนยันลำดับงานช่วงกลางเดือนพฤษภาคม']],
discussion: [['ระบบสิทธิ์และ Onboarding', 'แก้ไขข้อบกพร่องการลงทะเบียน Onboarding และการตรวจสิทธิ์ครบทุกรายการที่พบ', ['ThS'], 'เสร็จสิ้น'], ['การเตรียมใช้งานจริง', 'จัดทำ Base URL อัตโนมัติและสคริปต์ติดตั้งฐานข้อมูลในขั้นตอนเดียว', ['ThS'], 'เสร็จสิ้น'], ['บัญชีและการเงิน', 'มอบหมายพัฒนาผังบัญชี GL สมุดรายวัน และเอกสารการเงิน', ['ThS', 'NoC'], 'ดำเนินการ']],
actions: [['บันทึกการแก้ไข ISS-003 ถึง ISS-009 ใน Correction Register', ['YaB'], '2026-05-15', 'Scheduled'], ['พัฒนาโมดูลบัญชีและการเงิน (Task 3.7)', ['ThS'], '2026-05-23', 'Scheduled']],
next: { date: '2026-05-23', topic: 'ติดตามการพัฒนาบัญชี การเงิน และการควบคุมสิทธิ์ผู้ใช้' },
agenda: [['ผลการพัฒนาระบบสิทธิ์และเอกสารขาย', 'ตรวจสอบความครบถ้วนตาม CR01:001–CR01:004 และ CR01:013'], ['ประเด็นความปลอดภัยและ Onboarding ที่พบ', 'พิจารณาการแก้ไข ISS-003 ถึง ISS-009'], ['คำเรียกตำแหน่งจัดเก็บ (Rack / Bin)', 'กำหนดคำเรียกที่ใช้ในหน้าจอให้ตรงกับการปฏิบัติงานจริง'], ['แผนพัฒนาบัญชีและการเงิน', 'ยืนยันลำดับงานช่วงกลางเดือนพฤษภาคม']],
discussion: [['ระบบสิทธิ์และ Onboarding', 'แก้ไขข้อบกพร่องการลงทะเบียน Onboarding และการตรวจสิทธิ์ครบทุกรายการที่พบ', ['ThS'], 'เสร็จสิ้น'], ['การเตรียมใช้งานจริง', 'จัดทำ Base URL อัตโนมัติและสคริปต์ติดตั้งฐานข้อมูลในขั้นตอนเดียว', ['ThS'], 'เสร็จสิ้น'], ['คำเรียกตำแหน่งจัดเก็บ', 'ที่ประชุมมีมติให้ใช้คำว่า Bin แทน Rack ทั้งระบบให้ตรงกับคำที่พนักงานคลังใช้จริง เป็นการปรับคำเรียกบนหน้าจอ ไม่เพิ่มหรือลดฟังก์ชันและไม่กระทบ Man-day จึงไม่ถือเป็นคำขอเปลี่ยนแปลง', ['SeV', 'ApS', 'NoC'], 'มติที่ประชุม'], ['บัญชีและการเงิน', 'มอบหมายพัฒนาผังบัญชี GL สมุดรายวัน และเอกสารการเงิน', ['ThS', 'NoC'], 'ดำเนินการ']],
actions: [['บันทึกการแก้ไข ISS-003 ถึง ISS-009 ใน Correction Register', ['YaB'], '2026-05-20', 'Scheduled'], ['ปรับคำเรียก Rack เป็น Bin ตามมติที่ประชุม', ['ThS'], '2026-05-27', 'Scheduled'], ['พัฒนาโมดูลบัญชีและการเงิน (Task 3.7)', ['ThS'], '2026-05-23', 'Scheduled']],
next: { date: '2026-05-28', topic: 'ติดตามการพัฒนาบัญชี การเงิน และการควบคุมสิทธิ์ผู้ใช้' },
},
{
date: '2026-05-23', phase: 'Project Execution', taskId: '3.7', taskName: 'Accounting & Finance Workflows', time: '09:00 – 11:00 น.',
attachment: 'Progress Status Record 23 พ.ค. 69',
date: '2026-05-28', phase: 'Project Execution', taskId: '3.7', taskName: 'Accounting & Finance Workflows', time: '09:00 – 11:00 น.',
attachment: '-',
progress: [['Phase 3: Project Execution', 'กำลังดำเนินการ', 85, 'บัญชีและการเงินเสร็จ เหลือบริการแจ้งเตือนและการทบทวนความปลอดภัย'], ['รวมความคืบหน้าทั้งโครงการ', 'ตามแผน', 72, 'เตรียมเข้าสู่ Baseline การพัฒนา']],
agenda: [['ผลการพัฒนาบัญชีและการเงิน', 'ตรวจสอบความครบถ้วนตาม CR01:015–CR01:017'], ['คำเรียกตำแหน่งจัดเก็บ (Rack / Bin)', 'กำหนดคำเรียกที่ใช้ในหน้าจอให้ตรงกับการปฏิบัติงานจริง'], ['แผนงานก่อนตั้ง Baseline การพัฒนา', 'ยืนยันงานที่ต้องเสร็จภายใน 29 พ.ค. 69']],
discussion: [['บัญชีและการเงิน', 'พัฒนาผังบัญชี GL สมุดรายวัน รายงานบัญชี ใบวางบิลและใบเสร็จครบตามความต้องการ', ['ThS', 'NoC'], 'เสร็จสิ้น'], ['คำเรียกตำแหน่งจัดเก็บ', 'ที่ประชุมมีมติให้ใช้คำว่า Bin แทน Rack ทั้งระบบให้ตรงกับคำที่พนักงานคลังใช้จริง เป็นการปรับคำเรียกบนหน้าจอ ไม่เพิ่มหรือลดฟังก์ชันและไม่กระทบ Man-day จึงไม่ถือเป็นคำขอเปลี่ยนแปลง', ['SeV', 'ApS', 'NoC'], 'มติที่ประชุม'], ['ความปลอดภัยและโควตารายการ', 'เพิ่มการควบคุมสิทธิ์แอปพลิเคชันและโควตารายการต่อบริษัท', ['ThS'], 'ดำเนินการ']],
actions: [['ปรับคำเรียก Rack เป็น Bin ตามมติที่ประชุม', ['ThS'], '2026-05-27', 'Scheduled'], ['ทบทวนความปลอดภัยและวงจรเอกสารก่อนตั้ง Baseline (Task 3.9)', ['ThS', 'PaNg'], '2026-05-28', 'Scheduled'], ['บันทึกการแก้ไข ISS-010 ถึง ISS-015 ใน Correction Register', ['YaB'], '2026-05-25', 'Scheduled']],
next: { date: '2026-05-29', topic: 'สรุปผลการพัฒนาและตั้ง Baseline การพัฒนา' },
agenda: [['ผลการพัฒนาบัญชีและการเงิน', 'ตรวจสอบความครบถ้วนตาม CR01:015–CR01:017'], ['แผนงานก่อนตั้ง Baseline การพัฒนา', 'ยืนยันงานที่ต้องเสร็จภายใน 29 พ.ค. 69']],
discussion: [['บัญชีและการเงิน', 'พัฒนาผังบัญชี GL สมุดรายวัน รายงานบัญชี ใบวางบิลและใบเสร็จครบตามความต้องการ', ['ThS', 'NoC'], 'เสร็จสิ้น'], ['ความปลอดภัยและโควตารายการ', 'เพิ่มการควบคุมสิทธิ์แอปพลิเคชันและโควตารายการต่อบริษัท', ['ThS'], 'ดำเนินการ']],
actions: [['ทบทวนความปลอดภัยและวงจรเอกสารก่อนตั้ง Baseline (Task 3.9)', ['ThS', 'PaNg'], '2026-05-28', 'Scheduled'], ['บันทึกการแก้ไข ISS-010 ถึง ISS-015 ใน Correction Register', ['YaB'], '2026-06-01', 'Scheduled']],
next: { date: '2026-06-04', topic: 'สรุปผลการพัฒนาและตั้ง Baseline การพัฒนา' },
},
{
date: '2026-05-29', phase: 'Project Execution', taskId: '3.10', taskName: 'Refactor & Development Baseline', time: '09:00 – 11:00 น.',
attachment: 'Progress Status Record 29 พ.ค. 69',
date: '2026-06-04', phase: 'Project Execution', taskId: '3.10', taskName: 'Refactor & Development Baseline', time: '09:00 – 11:00 น.',
attachment: '-',
progress: [['Phase 3: Project Execution', 'เสร็จสมบูรณ์', 100, 'พัฒนาครบทุกโมดูลและตั้ง Baseline การพัฒนา (Git a0677d6)'], ['Phase 4: Verification & Validation', 'กำลังดำเนินการ', 10, 'เริ่มตรวจสอบเอกสารและจัดทำคู่มือ'], ['รวมความคืบหน้าทั้งโครงการ', 'ตามแผน', 78, 'เข้าสู่ขั้นตอนตรวจสอบและจัดทำเอกสาร']],
agenda: [['สรุปผลการพัฒนา Task 3.1–3.10', 'ยืนยันความครบถ้วนของการพัฒนา'], ['สรุปผลการทบทวนความปลอดภัยและวงจรเอกสาร', 'ยืนยันการปิดข้อบกพร่อง ISS-016 ถึง ISS-027'], ['แผนการตรวจสอบ ทดสอบ และจัดทำเอกสาร', 'กำหนดผู้รับผิดชอบและกรอบเวลา']],
discussion: [['Baseline การพัฒนา', 'พัฒนาครบทุกโมดูลตามขอบเขต และลดความซ้ำซ้อนของคลาสเรียบร้อย ตั้ง Baseline เมื่อ 29 พ.ค. 69', ['ThS'], 'เสร็จสิ้น'], ['ความปลอดภัยและการแยกข้อมูล', 'ปิดข้อบกพร่องด้านการเข้าสู่ระบบ ขอบเขตบริษัท และวงจรเอกสารครบทุกรายการที่พบ', ['ThS', 'PaNg'], 'เสร็จสิ้น'], ['ขั้นตอนถัดไป', 'ตรวจสอบ Work Products จัดทำคู่มือ และเตรียมชุดทดสอบระบบ', ['PaNg', 'NoC', 'YaB'], 'ดำเนินการ']],
actions: [['จัดทำ Test Case และ Test Procedures ครบทุกหน่วยงาน', ['PaNg', 'NoC'], '2026-07-31', 'Scheduled'], ['จัดทำคู่มือผู้ใช้ คู่มือผู้ดูแลระบบ และคู่มือบำรุงรักษา (Task 4.5)', ['ThS', 'YaB'], '2026-08-17', 'Scheduled'], ['ตรวจสอบ Work Products รอบที่ 1–3 (Task 4.3)', ['PaNg', 'NoC'], '2026-08-15', 'Scheduled']],
next: { date: '2026-08-03', topic: 'ติดตามการตรวจสอบเอกสาร การจัดทำคู่มือ และการแก้ไขหลัง Baseline' },
actions: [['จัดทำ Test Case และ Test Procedures ครบทุกหน่วยงาน', ['PaNg', 'NoC'], '2026-07-31', 'Scheduled'], ['จัดทำคู่มือผู้ใช้ คู่มือผู้ดูแลระบบ และคู่มือบำรุงรักษา (Task 4.5)', ['ThS', 'YaB'], '2026-08-17', 'Scheduled'], ['ตรวจสอบ Work Products รอบที่ 1–3 (Task 4.3)', ['PaNg', 'NoC'], '2026-07-31', 'Scheduled']],
next: { date: '2026-08-10', topic: 'ติดตามการตรวจสอบเอกสาร การจัดทำคู่มือ และการแก้ไขหลัง Baseline' },
},
{
date: '2026-08-03', phase: 'Stabilization', taskId: '4.6', taskName: 'Post-baseline Corrections', time: '09:00 – 11:00 น.',
attachment: 'Progress Status Record 3 ส.ค. 69',
date: '2026-08-10', phase: 'Stabilization', taskId: '4.6', taskName: 'Post-baseline Corrections', time: '09:00 – 11:00 น.',
attachment: '-',
progress: [['Phase 4: Verification & Validation', 'กำลังดำเนินการ', 70, 'ตรวจสอบเอกสารรอบที่ 1–3 และจัดทำคู่มือใกล้เสร็จ'], ['รวมความคืบหน้าทั้งโครงการ', 'ตามแผน', 85, 'เตรียมทดสอบระบบและ UAT']],
agenda: [['ผลการแก้ไขหลัง Baseline การพัฒนา', 'ยืนยันการปิดข้อบกพร่อง ISS-028'], ['ความคืบหน้าการตรวจสอบเอกสารและคู่มือ', 'ทบทวนผลการตรวจสอบรอบที่ 1–3'], ['แผนการทดสอบระบบและ UAT', 'ยืนยันสภาพแวดล้อม ผู้ทดสอบ และกรอบเวลา']],
discussion: [['การแก้ไขหลัง Baseline', 'แก้ไขการเข้าสู่ระบบและค่าตั้งค่าสภาพแวดล้อมที่พบหลังตั้ง Baseline เรียบร้อย', ['ThS'], 'เสร็จสิ้น'], ['การตรวจสอบเอกสาร', 'ตรวจสอบความต้องการ ออกแบบ และการสอบกลับรอบที่ 1–3 พบประเด็นที่ต้องปรับปรุงและแก้ไขแล้ว', ['PaNg', 'NoC'], 'ดำเนินการ'], ['แผนการทดสอบ', 'กำหนดทดสอบระบบบน Internal Testing Server โดย QA และ UAT บนสภาพแวดล้อมจริงโดยตัวแทนลูกค้า ระหว่าง 10–14 ส.ค. 69', ['PaNg', 'SeV'], 'อนุมัติ']],
actions: [['บันทึกการแก้ไข ISS-028 ใน Correction Register', ['YaB'], '2026-08-05', 'Scheduled'], ['เตรียมข้อมูลสาธิตสำหรับการทดสอบและ UAT', ['ThS'], '2026-08-14', 'Scheduled'], ['ดำเนินการทดสอบระบบและ UAT (Task 4.4)', ['PaNg', 'SeV'], '2026-08-14', 'Scheduled']],
next: { date: '2026-08-14', topic: 'สรุปผลการทดสอบระบบ UAT และข้อมูลสาธิต' },
agenda: [['ผลการแก้ไขหลัง Baseline การพัฒนา', 'ยืนยันการปิดข้อบกพร่อง ISS-028'], ['ความคืบหน้าการตรวจสอบเอกสารและคู่มือ', 'ทบทวนผลการตรวจสอบรอบที่ 1–3'], ['แผนการทดสอบระบบและ UAT', 'ยืนยันสภาพแวดล้อม ผู้ทดสอบ และกรอบเวลา'], ['ข้อมูลสาธิต อัตลักษณ์องค์กร และชุดติดตั้ง (Task 4.7)', 'พิจารณาตามเกณฑ์การเปลี่ยนแปลงและกำหนดผู้รับผิดชอบ']],
discussion: [['การแก้ไขหลัง Baseline', 'แก้ไขการเข้าสู่ระบบและค่าตั้งค่าสภาพแวดล้อมที่พบหลังตั้ง Baseline เรียบร้อย', ['ThS'], 'เสร็จสิ้น'], ['การตรวจสอบเอกสาร', 'ตรวจสอบความต้องการ ออกแบบ และการสอบกลับรอบที่ 1–3 พบประเด็นที่ต้องปรับปรุงและแก้ไขแล้ว', ['PaNg', 'NoC'], 'ดำเนินการ'], ['แผนการทดสอบ', 'กำหนดทดสอบระบบบน Internal Testing Server โดย QA และ UAT บนสภาพแวดล้อมจริงโดยตัวแทนลูกค้า ระหว่าง 10–14 ส.ค. 69', ['PaNg', 'SeV'], 'อนุมัติ'], ['ข้อมูลสาธิตและชุดส่งมอบ', 'Project Sponsor ขอให้เตรียมข้อมูลสาธิตสำหรับการตรวจรับ ปรับอัตลักษณ์องค์กร และจัดทำชุดติดตั้ง Docker Compose ตามความต้องการ CR10:002 ที่ประชุมพิจารณาตามเกณฑ์การเปลี่ยนแปลงแล้ว ไม่เพิ่มหรือลดสิ่งส่งมอบและไม่กระทบ Man-day จึงดำเนินการเป็นงานเตรียมการส่งมอบตาม Task 4.7', ['SeV', 'ApS'], 'มติที่ประชุม']],
actions: [['บันทึกการแก้ไข ISS-028 ใน Correction Register', ['YaB'], '2026-08-12', 'Scheduled'], ['เตรียมข้อมูลสาธิตสำหรับการทดสอบและ UAT', ['ThS'], '2026-08-14', 'Scheduled'], ['ดำเนินการทดสอบระบบและ UAT (Task 4.4)', ['PaNg', 'SeV'], '2026-08-14', 'Scheduled'], ['ปรับอัตลักษณ์องค์กรและจัดทำชุดติดตั้ง Docker Compose (Task 4.7)', ['ThS'], '2026-08-17', 'Scheduled']],
next: { date: '2026-08-19', topic: 'สรุปผลการทดสอบระบบ UAT และข้อมูลสาธิต' },
},
{
date: '2026-08-14', phase: 'Validation', taskId: '4.4', taskName: 'System Test & UAT', time: '09:00 – 11:00 น.',
date: '2026-08-19', phase: 'Validation', taskId: '4.4', taskName: 'System Test & UAT', time: '09:00 – 11:00 น.',
attachment: 'Test Report, Validation Results',
progress: [['Phase 4: Verification & Validation', 'เสร็จสมบูรณ์', 100, `ทดสอบ 45 Test Case และ UAT ครบ 14 หมวดความต้องการ (${CRS.length} รายการ) ผ่านทั้งหมด`], ['Phase 5: Project Close', 'กำลังดำเนินการ', 60, 'อยู่ระหว่างตรวจสอบ Work Products และเตรียมชุดส่งมอบ'], ['รวมความคืบหน้าทั้งโครงการ', 'ตามแผน', 92, 'เตรียมตรวจรับและปิดโครงการ']],
progress: [['Phase 4: Verification & Validation', 'กำลังดำเนินการ', 90, `ทดสอบ 45 Test Case ผ่าน 38 ไม่ผ่าน 5 ยังไม่ได้ทดสอบ 2 และ UAT ครบ 14 หมวดความต้องการ (${CRS.length} รายการ) มีข้อบกพร่องคงค้าง ISS-029–ISS-032`], ['Phase 5: Project Close', 'กำลังดำเนินการ', 60, 'อยู่ระหว่างเตรียมอบรมผู้ใช้งาน ตรวจรับ และปิดโครงการ'], ['รวมความคืบหน้าทั้งโครงการ', 'ตามแผน', 92, 'เตรียมตรวจรับและปิดโครงการ']],
agenda: [['สรุปผลการทดสอบระบบ 45 Test Case', 'ยืนยันผลการทดสอบและข้อบกพร่องคงค้าง'], ['สรุปผลการทดสอบการยอมรับ (UAT) ตาม Customer Requirements CR01–CR14', 'ยืนยันความพร้อมใช้งานจริง'], ['ข้อมูลสาธิตสำหรับการตรวจรับ (Task 4.7)', 'ยืนยันความพร้อมของข้อมูลตั้งต้น'], ['แผนงานปิดโครงการ', 'ยืนยันงานที่เหลือถึง 24 ส.ค. 69']],
discussion: [['ผลการทดสอบระบบ', 'ทดสอบ 45 Test Case บน Internal Testing Server ผ่านทั้งหมด ไม่พบข้อบกพร่องคงค้าง', ['PaNg'], 'ผ่าน'], ['ผลการทดสอบการยอมรับ', `ตัวแทนลูกค้าทดสอบการยอมรับครบ 14 หมวดความต้องการ (${CRS.length} รายการ) บนสภาพแวดล้อมใช้งานจริง ผ่านทั้งหมด`, ['SeV'], 'ผ่าน'], ['ข้อมูลสาธิต', 'จัดเตรียมข้อมูลสาธิตสำหรับการตรวจรับรายงานและขั้นตอนการทำงานเสร็จแล้ว เป็นงานเตรียมการส่งมอบตาม Task 4.7 ไม่เปลี่ยนแปลงขอบเขตหรือสิ่งส่งมอบ', ['ApS', 'SeV'], 'เสร็จสิ้น'], ['งานที่เหลือ', 'ตรวจสอบ Work Products รอบสุดท้าย เตรียมชุดติดตั้ง ตรวจรับ อบรม และปิดโครงการ', ['ApS', 'YaB'], 'ดำเนินการ']],
actions: [['ตรวจสอบ Work Products รอบที่ 4 และจัดทำ List of Evidence', ['YaB', 'ApS'], '2026-08-17', 'Scheduled'], ['จัดทำชุดติดตั้ง Docker Compose (CR10:002) และปรับแบรนด์ (Task 4.7)', ['ThS'], '2026-08-17', 'Scheduled'], ['นัดตรวจรับส่งมอบระบบกับ Project Sponsor', ['ApS'], '2026-08-17', 'Scheduled']],
next: { date: '2026-08-17', topic: 'สรุปผลการตรวจสอบรอบสุดท้ายและตรวจรับส่งมอบระบบ' },
discussion: [['ผลการทดสอบระบบ', 'ทดสอบ 45 Test Case บน Internal Testing Server (Build b2c4374) ผ่าน 38 ไม่ผ่าน 5 ยังไม่ได้ทดสอบ 2 ข้อบกพร่องระดับสูงที่พบ ได้แก่ ไฟล์แนบเปิดได้จาก URL (ISS-029) การผ่าน GL ไม่อยู่ใน Transaction เดียวกับเอกสาร (ISS-030) Socket.IO ไม่ตรวจ Session (ISS-031) และค่าความลับในแม่แบบค่าตั้งค่า (ISS-032) ส่วนชุดติดตั้ง Docker ยังไม่ได้ทดสอบ', ['PaNg'], 'ไม่ผ่าน'], ['ผลการทดสอบการยอมรับ', `ตัวแทนลูกค้าทดสอบการยอมรับครบ 14 หมวดความต้องการ (${CRS.length} รายการ) บนสภาพแวดล้อมใช้งานจริง ความต้องการที่เกี่ยวข้องกับข้อบกพร่องคงค้างและชุดติดตั้ง Docker ยังไม่ผ่าน ผู้รับมอบรับทราบผลและขอแผนแก้ไขก่อนตรวจรับ`, ['SeV'], 'รับทราบ'], ['ข้อมูลสาธิต', 'จัดเตรียมข้อมูลสาธิตสำหรับการตรวจรับรายงานและขั้นตอนการทำงานเสร็จแล้ว เป็นงานเตรียมการส่งมอบตาม Task 4.7 ไม่เปลี่ยนแปลงขอบเขตหรือสิ่งส่งมอบ', ['ApS', 'SeV'], 'เสร็จสิ้น'], ['งานที่เหลือ', 'อบรมผู้ใช้งาน ปิดงานควบคุมปฏิบัติการ ตรวจรับส่งมอบ และปิดโครงการ', ['ApS', 'YaB'], 'ดำเนินการ']],
actions: [['จัดทำแผนแก้ไขข้อบกพร่องคงค้าง ISS-029–ISS-032 และการทดสอบชุดติดตั้ง Docker เสนอผู้รับมอบ', ['ApS', 'ThS'], '2026-08-24', 'Scheduled'], ['จัดอบรมผู้ใช้งานก่อนเปิดใช้งานจริง (Task 5.2)', ['ThS', 'PaNg'], '2026-08-22', 'Scheduled'], ['ปิดงานควบคุมการสำรองข้อมูลและการเฝ้าระวังระบบ', ['ThS'], '2026-08-23', 'Scheduled'], ['นัดตรวจรับส่งมอบระบบและปิดโครงการกับ Project Sponsor', ['ApS'], '2026-08-24', 'Scheduled']],
next: { date: '2026-08-24', topic: 'ตรวจรับส่งมอบระบบและปิดโครงการอย่างเป็นทางการ' },
},
{
date: '2026-08-17', phase: 'Project Close', taskId: '5.3', taskName: 'Acceptance & Project Closure', time: '09:00 – 11:00 น.',
attachment: 'Acceptance Report, List of Evidence, Verification Results',
progress: [['Phase 3: Project Execution', 'เสร็จสมบูรณ์', 100, 'พัฒนาระบบครบตามขอบเขต Baseline 6c39700'], ['Phase 4: Verification & Validation', 'เสร็จสมบูรณ์', 100, 'ตรวจสอบ ทดสอบ และ UAT ผ่านครบทุกรายการ'], ['Phase 5: Project Close', 'กำลังดำเนินการ', 80, 'ตรวจรับระบบแล้ว เหลือการอบรมและปิดงานควบคุมปฏิบัติการ'], ['รวมความคืบหน้าทั้งโครงการ', 'ตามแผน', 96, 'ตรวจรับระบบเรียบร้อย']],
agenda: [['นำเสนอผลการดำเนินโครงการและสิ่งส่งมอบ', 'สรุปผลงานทั้งหมดเทียบกับแผนและผลลัพธ์จริง'], ['ตรวจสอบความครบถ้วนของเอกสารและหลักฐาน', 'ยืนยันเอกสารครบตามมาตรฐาน ISO/IEC 29110'], ['ชุดติดตั้งและอัตลักษณ์องค์กร (Task 4.7)', 'ยืนยันความพร้อมของชุดส่งมอบ'], ['พิจารณาตรวจรับส่งมอบระบบ', 'ลงมติผลการตรวจรับ']],
discussion: [['ผลการดำเนินโครงการ', 'พัฒนาระบบครบตามขอบเขต 11 ระบบงาน ทดสอบและ UAT ผ่านทั้งหมด ไม่มีข้อบกพร่องระดับวิกฤตคงค้าง', ['ApS', 'SeV'], 'ผ่านการรับรอง'], ['ความครบถ้วนของเอกสาร', 'ตรวจสอบ Work Products รอบที่ 4 ครบทุกรายการ ผลผ่านทั้งหมด และจัดทำ List of Evidence เรียบร้อย', ['YaB', 'PaNg'], 'สมบูรณ์'], ['ชุดติดตั้งและอัตลักษณ์องค์กร', 'จัดทำชุดติดตั้ง Docker Compose ตามความต้องการ CR10:002 และปรับอัตลักษณ์องค์กรในหน้าจอและเอกสารเสร็จแล้ว เป็นงานเตรียมการส่งมอบตาม Task 4.7 ไม่เปลี่ยนแปลงขอบเขตหรือสิ่งส่งมอบ', ['SeV', 'ApS'], 'เสร็จสิ้น'], ['ผลการตรวจรับ', 'Project Sponsor ตรวจรับสิ่งส่งมอบทั้งหมด ผลการตรวจรับ Accepted', ['SeV'], 'ตรวจรับแล้ว']],
actions: [['จัดอบรมผู้ใช้งานก่อนเปิดใช้งานจริง (Task 5.2)', ['ThS', 'PaNg'], '2026-08-22', 'Scheduled'], ['ปิดงานควบคุมการสำรองข้อมูลและการเฝ้าระวังระบบ', ['ThS'], '2026-08-23', 'Scheduled'], ['ปิดโครงการและส่งมอบเอกสารเข้าระบบควบคุมเอกสาร', ['ApS', 'YaB'], '2026-08-24', 'Scheduled']],
next: { date: '2026-08-24', topic: 'สรุปผลการอบรม การปิดงานควบคุมปฏิบัติการ และปิดโครงการอย่างเป็นทางการ' },
},
{
date: '2026-08-24', phase: 'Project Close', taskId: '5.3', taskName: 'Final Report & Sign-off', time: '09:00 – 11:00 น.',
attachment: 'Training Report, Product Operation Guide, Project Repository (Backup)',
progress: [['Phase 1: Project Initiation', 'เสร็จสมบูรณ์', 100, 'เปิดโครงการและอนุมัติกฎบัตรเรียบร้อย'], ['Phase 2: Project Planning', 'เสร็จสมบูรณ์', 100, 'อนุมัติแผนโครงการและ Baseline ความต้องการ'], ['Phase 3: Project Execution', 'เสร็จสมบูรณ์', 100, 'พัฒนาระบบครบตามขอบเขตและเปิดใช้งานจริง'], ['Phase 4: Verification & Validation', 'เสร็จสมบูรณ์', 100, 'ตรวจสอบ ทดสอบ และ UAT ผ่านครบทุกรายการ'], ['Phase 5: Project Close', 'เสร็จสมบูรณ์', 100, 'อบรม ปิดงานควบคุมปฏิบัติการ และปิดโครงการเรียบร้อย'], ['รวมความคืบหน้าทั้งโครงการ', 'ตามแผน', 100, 'โครงการเสร็จสมบูรณ์ ปิดโครงการอย่างเป็นทางการ']],
agenda: [['สรุปผลการอบรมผู้ใช้งาน', 'ยืนยันความพร้อมของผู้ใช้ก่อนเปิดใช้งานจริง'], ['สรุปการปิดงานควบคุมการสำรองข้อมูลและการเฝ้าระวัง', 'ยืนยันการปิดรายการ OP-001 และ OP-002'], ['สรุปการสำรอง Repository และเอกสารโครงการ', 'ยืนยันการสำรองและกู้คืนได้จริง'], ['ปิดโครงการอย่างเป็นทางการ', 'ลงมติปิดโครงการและส่งมอบเอกสาร']],
discussion: [['ผลการอบรม', 'อบรมผู้ใช้งาน 6 คน เมื่อ 22 ส.ค. 69 ผู้เข้าอบรมทดลองใช้งานครบทุกขั้นตอนได้สำเร็จ', ['ThS', 'PaNg'], 'เสร็จสิ้น'], ['การควบคุมปฏิบัติการ', 'ปิดรายการสำรองฐานข้อมูลอัตโนมัติ (OP-001) และการเฝ้าระวังพร้อมแจ้งเตือน (OP-002) เมื่อ 23 ส.ค. 69', ['ThS'], 'เสร็จสิ้น'], ['การสำรองข้อมูลโครงการ', 'ซิงก์ Branch `main` ไปยัง Backup Remote และทดสอบเรียกคืนสำเร็จ', ['ThS', 'YaB'], 'เสร็จสิ้น'], ['การปิดโครงการ', 'ที่ประชุมมีมติปิดโครงการอย่างเป็นทางการเมื่อ 24 ส.ค. 69', ['SeV', 'ApS'], 'ปิดโครงการ']],
actions: [['จัดเก็บเอกสารโครงการทั้งหมดเข้าระบบควบคุมเอกสาร', ['YaB'], '2026-08-24', 'เสร็จสมบูรณ์'], ['ติดตามการตรวจนับสต๊อกสิ้นเดือนแรกหลังเปิดใช้งานจริง', ['ApS'], '2026-09-30', 'Scheduled']],
date: '2026-08-24', phase: 'Project Close', taskId: '5.3', taskName: 'Acceptance & Project Closure', time: '09:00 – 12:00 น.',
attachment: 'Acceptance Report, List of Evidence, Verification Results, Training Report, Product Operation Guide, Project Repository (Backup)',
progress: [['Phase 1: Project Initiation', 'เสร็จสมบูรณ์', 100, 'เปิดโครงการและอนุมัติกฎบัตรเรียบร้อย'], ['Phase 2: Project Planning', 'เสร็จสมบูรณ์', 100, 'อนุมัติแผนโครงการและ Baseline ความต้องการ'], ['Phase 3: Project Execution', 'เสร็จสมบูรณ์', 100, 'พัฒนาระบบครบตามขอบเขต Baseline 6c39700'], ['Phase 4: Verification & Validation', 'เสร็จสมบูรณ์', 100, 'ตรวจสอบ ทดสอบ และ UAT ครบตามแผน ผ่าน 38 ไม่ผ่าน 5 ยังไม่ได้ทดสอบ 2 ข้อบกพร่องคงค้างส่งต่อการบำรุงรักษา'], ['Phase 5: Project Close', 'เสร็จสมบูรณ์', 100, 'ตรวจรับ อบรม ปิดงานควบคุมปฏิบัติการ และปิดโครงการเรียบร้อย'], ['รวมความคืบหน้าทั้งโครงการ', 'ตามแผน', 100, 'โครงการเสร็จสมบูรณ์ ปิดโครงการอย่างเป็นทางการ']],
agenda: [['นำเสนอผลการดำเนินโครงการและสิ่งส่งมอบ', 'สรุปผลงานทั้งหมดเทียบกับแผนและผลลัพธ์จริง'], ['ตรวจสอบความครบถ้วนของเอกสารและหลักฐาน', 'ยืนยันเอกสารครบตามมาตรฐาน ISO/IEC 29110'], ['ชุดติดตั้งและอัตลักษณ์องค์กร (Task 4.7)', 'ยืนยันความพร้อมของชุดส่งมอบ'], ['พิจารณาตรวจรับส่งมอบระบบ', 'ลงมติผลการตรวจรับ'], ['สรุปผลการอบรมผู้ใช้งาน', 'ยืนยันความพร้อมของผู้ใช้ก่อนเปิดใช้งานจริง'], ['สรุปการปิดงานควบคุมการสำรองข้อมูลและการเฝ้าระวัง', 'ยืนยันการปิดรายการ OP-001 และ OP-002'], ['สรุปการสำรอง Repository และเอกสารโครงการ', 'ยืนยันการสำรองและกู้คืนได้จริง'], ['ปิดโครงการอย่างเป็นทางการ', 'ลงมติปิดโครงการและส่งมอบเอกสาร']],
discussion: [['ผลการดำเนินโครงการ', 'พัฒนาระบบครบตามขอบเขต 11 ระบบงาน ทดสอบ 45 Test Case ผ่าน 38 ไม่ผ่าน 5 ยังไม่ได้ทดสอบ 2 มีข้อบกพร่องระดับสูงคงค้าง ISS-029–ISS-032 และชุดติดตั้ง Docker ที่ยังไม่ได้ทดสอบ', ['ApS', 'SeV'], 'รับทราบ'], ['ความครบถ้วนของเอกสาร', 'ตรวจสอบ Work Products รอบที่ 4 ครบทุกรายการ ไม่ผ่าน 2 รายการตามผลการทดสอบ และจัดทำ List of Evidence เรียบร้อย', ['YaB', 'PaNg'], 'สมบูรณ์'], ['ชุดติดตั้งและอัตลักษณ์องค์กร', 'จัดทำชุดติดตั้ง Docker Compose ตามความต้องการ CR10:002 และปรับอัตลักษณ์องค์กรในหน้าจอและเอกสารเสร็จแล้ว เป็นงานเตรียมการส่งมอบตาม Task 4.7 ไม่เปลี่ยนแปลงขอบเขตหรือสิ่งส่งมอบ', ['SeV', 'ApS'], 'เสร็จสิ้น'], ['ผลการตรวจรับ', 'Project Sponsor ตรวจรับสิ่งส่งมอบแบบมีเงื่อนไข (Accepted with conditions): แก้ไข ISS-029–ISS-032 และทดสอบซ้ำภายใต้การบำรุงรักษาแบบแก้ไข และทดสอบ TC-UN13.002–TC-UN13.003 ก่อนใช้ชุดติดตั้ง Docker ในสภาพแวดล้อมใช้งานจริง', ['SeV'], 'ตรวจรับแบบมีเงื่อนไข'], ['ผลการอบรม', 'อบรมผู้ใช้งาน 6 คน เมื่อ 22 ส.ค. 69 ผู้เข้าอบรมทดลองใช้งานครบทุกขั้นตอนได้สำเร็จ', ['ThS', 'PaNg'], 'เสร็จสิ้น'], ['การควบคุมปฏิบัติการ', 'ปิดรายการสำรองฐานข้อมูลอัตโนมัติ (OP-001) และการเฝ้าระวังพร้อมแจ้งเตือน (OP-002) เมื่อ 23 ส.ค. 69', ['ThS'], 'เสร็จสิ้น'], ['การสำรองข้อมูลโครงการ', 'ซิงก์ Branch `main` ไปยัง Backup Remote และทดสอบเรียกคืนสำเร็จ', ['ThS', 'YaB'], 'เสร็จสิ้น'], ['การปิดโครงการ', 'ที่ประชุมมีมติปิดโครงการอย่างเป็นทางการเมื่อ 24 ส.ค. 69', ['SeV', 'ApS'], 'ปิดโครงการ']],
actions: [['แก้ไขข้อบกพร่องคงค้าง ISS-029–ISS-032 และทดสอบซ้ำภายใต้การบำรุงรักษาแบบแก้ไข', ['ThS', 'PaNg'], '2026-09-30', 'Scheduled'], ['ทดสอบชุดติดตั้ง Docker (TC-UN13.002–TC-UN13.003) ก่อนใช้งานจริง', ['ThS', 'PaNg'], '2026-09-30', 'Scheduled'], ['จัดเก็บเอกสารโครงการทั้งหมดเข้าระบบควบคุมเอกสาร', ['YaB'], '2026-08-24', 'เสร็จสมบูรณ์'], ['ติดตามการตรวจนับสต๊อกสิ้นเดือนแรกหลังเปิดใช้งานจริง', ['ApS'], '2026-09-30', 'Scheduled']],
next: null,
},
];
+9 -2
View File
@@ -9,12 +9,12 @@ export const DEV_START = '2026-02-19';
export const DEV_BASELINE = '2026-05-29';
export const TEST_FROM = '2026-08-10';
export const TEST_TO = '2026-08-14';
export const ACCEPT_DATE = '2026-08-17';
export const ACCEPT_DATE = '2026-08-24';
export const TRAINING_DATE = '2026-08-22';
export const OPS_CLOSE_DATE = '2026-08-23';
export const RELEASE_DATE = '2026-08-17'; // date stamped on the consolidated V1.0 issue
export const BASELINE_COMMIT = '6c39700'; // delivered software baseline on main
export const TEST_STATE_COMMIT = 'dd48a8b'; // closest retained repository state at end of test window
export const TEST_STATE_COMMIT = 'b2c4374'; // build under test 10–14 Aug: last code change before the run (demo data dd48a8b followed on 14 Aug)
export const TAG = 'sdlc-v1.0-final';
export const ORIGIN = 'git@188.166.228.62:nok/wms-app.git';
export const BACKUP = 'git@github.com:thanakorninbox-dev/wms-app.git';
@@ -28,6 +28,13 @@ export const P = {
YaB: { id: 'YaB', th: 'คุณเยาวลักษณ์ บางชมภู', en: 'Yaowalak Bangchomphoo', role: 'Document Control', pos: 'เจ้าหน้าที่ควบคุมเอกสาร', email: 'yaowalak.b@brn.co.th' },
};
export const TEAM = [P.SeV, P.ApS, P.NoC, P.ThS, P.PaNg, P.YaB];
/** Stakeholders outside the project team: [name, role, responsibility, engagement]. */
export const OTHER_STAKEHOLDERS = [
['หัวหน้าฝ่ายคลังสินค้าและพนักงานคลัง', 'ผู้ใช้งานหลัก', 'ให้ข้อมูลกระบวนการทำงาน ทดสอบการใช้งาน และใช้งานระบบจริง', 'C (Consult), R (Review)'],
['ฝ่ายขายและฝ่ายจัดซื้อ', 'ผู้ใช้งาน', 'ให้ข้อมูลกระบวนการเอกสารขายและจัดซื้อ และทดสอบการใช้งาน', 'C (Consult)'],
['ฝ่ายบัญชีและการเงิน', 'ผู้ใช้งาน', 'ให้ข้อมูลการวางบิล รับชำระ จ่ายชำระ และการบันทึกบัญชี', 'C (Consult)'],
['ผู้ดูแลระบบ (System Administrator)', 'ผู้สนับสนุน', 'ติดตั้ง ตั้งค่า เฝ้าระวัง สำรองและกู้คืนระบบ', 'C (Consult), I (Inform)'],
];
export const ORGANIZER = `${P.ApS.th} (Project Manager) , ${P.NoC.th} (System Analyst)`;
export const ORGANIZER_PM = `${P.ApS.th} (Project Manager)`;
export const RECORDER = `${P.YaB.th} (Document Control)`;
+43 -2
View File
@@ -4,7 +4,7 @@
// Planned dates in the future are fine; facts that only came into being later are not.
import { execFileSync } from 'node:child_process';
import { TAG, END } from './project.mjs';
import { CORRECTIONS } from './history.mjs';
import { CORRECTIONS, MEETINGS, PERIODS, taskById } from './history.mjs';
/** Document date from its name: "... 25690817 V1.0 ..." -> 2026-08-17 */
export function docDate(name) {
@@ -79,5 +79,46 @@ export function hindsightErrors(docs, root) {
}
for (const what of found) errors.push(`${d.name}: ${what}`);
}
return errors;
// Meetings are held on working days, at least MEETING_LAG days after the commits and corrections
// they review, so the work has settled before it is reported as done.
const MEETING_LAG = 5;
const lagged = (iso) => new Date(Date.parse(`${iso}T00:00:00Z`) - MEETING_LAG * 864e5).toISOString().slice(0, 10);
const weekend = (iso) => [0, 6].includes(new Date(`${iso}T00:00:00Z`).getUTCDay());
const corrected = new Map(CORRECTIONS.map((c) => [c[0], c[2]]));
for (const m of MEETINGS) {
const where = `Minutes Of Meeting ${m.date}`;
const latest = lagged(m.date);
if (weekend(m.date)) errors.push(`${where}: held on a weekend`);
// The next meeting named in minutes is the next meeting that has minutes.
const following = MEETINGS.find((x) => x.date > m.date)?.date ?? null;
if ((m.next?.date ?? null) !== following) errors.push(`${where}: next meeting ${m.next?.date ?? 'none'} should be ${following ?? 'none'}`);
const text = JSON.stringify(m);
const hashes = new Set([...text.matchAll(/(?<![\w/.-])([0-9a-f]{7,40})(?![\w/.-])/g)].map((x) => x[1]));
const range = taskById(m.taskId)[9].match(/\bGit (\w+)(?:–(\w+))?/);
if (range) hashes.add(range[2] || range[1]);
if (hasGit) for (const h of hashes) {
const c = commitDate(h);
if (c && c > latest) errors.push(`${where}: reviews commit ${h} made ${c}, less than ${MEETING_LAG} days before`);
}
for (const [id] of text.matchAll(/\bISS-\d{3}\b/g)) {
if (corrected.get(id) > latest) errors.push(`${where}: confirms ${id} corrected ${corrected.get(id)}, less than ${MEETING_LAG} days before`);
}
}
// Every Progress Status Record is presented at a meeting that has minutes: the one
// its "Next Meeting" names, held MEETING_LAG days or more after the period's last commit.
// Only the closing record has no next meeting; the closing meeting presents it.
const meetingDates = new Set(MEETINGS.map((m) => m.date));
PERIODS.forEach(([date, from, to, no, , , , next], i) => {
const where = `Progress Status Record ${no} (${date})`;
if (weekend(date)) errors.push(`${where}: dated on a weekend`);
if (!next) {
if (i !== PERIODS.length - 1) errors.push(`${where}: names no next meeting`);
return;
}
if (!(next.date > date && meetingDates.has(next.date))) errors.push(`${where}: next meeting ${next.date} has no minutes`);
const last = hasGit ? git(root, ['log', 'main', '-1', '--format=%ad', '--date=short', `--since=${from} 00:00`, `--until=${to} 23:59:59`]) : null;
if (last && last > lagged(next.date)) errors.push(`${where}: next meeting ${next.date} is less than ${MEETING_LAG} days after commit on ${last}`);
});
return [...new Set(errors)];
}
+1 -1
View File
@@ -9,7 +9,7 @@ PACKAGE_DIR="$(realpath -m "${1:-$ROOT_DIR/sdlc-delivery}")"
[[ -d "$PACKAGE_DIR" ]] || { echo "Delivery package not found: $PACKAGE_DIR" >&2; exit 2; }
expected="$(find "$SOURCE_DIR" -type f -name '*.md' | wc -l | tr -d ' ')"
expected="$(find "$SOURCE_DIR" -mindepth 2 -type f -name '*.md' | wc -l | tr -d ' ')"
produced="$(find "$PACKAGE_DIR" -type f -name '*.pdf' | wc -l | tr -d ' ')"
[[ "$expected" == "$produced" ]] || { echo "Expected $expected PDFs, found $produced." >&2; exit 1; }
BIN
View File
Binary file not shown.

Some files were not shown because too many files have changed in this diff Show More