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/ notes/
docs/ docs/
SESSION.php 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" rm -rf "$STAGING_DIR"
mkdir -p "$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 while IFS= read -r -d '' source; do
relative="${source#"$ROOT_DIR/sdlc/"}" relative="${source#"$ROOT_DIR/sdlc/"}"
destination="$STAGING_DIR/${relative%.md}.pdf" destination="$STAGING_DIR/${relative%.md}.pdf"
echo "Rendering ${relative%.md}.pdf" echo "Rendering ${relative%.md}.pdf"
node "$TOOL_DIR/render-sdlc.mjs" "$source" "$destination" SDLC_WIDTHS_FILE="$BAND_DIR/widths/${relative%.md}.json" node "$TOOL_DIR/render-sdlc.mjs" "$source" "$destination"
done < <(find "$SOURCE_DIR" -type f -name '*.md' -print0 | sort -z) 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. # 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 # 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 # handed to the converter, which then only needs Python with pdf2docx: the local
# interpreter ($PYTHON) when it has it, otherwise a throwaway python container. # 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..." 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..." echo "Converting PDFs to Word..."
PYTHON="${PYTHON:-python3}" PYTHON="${PYTHON:-python3}"
+6 -5
View File
@@ -1,7 +1,8 @@
<!-- Repeating page footer: rule, standard reference at the left, document <!-- Repeating page footer: rule, standard reference at the left, document
abbreviation at the right, matching the audited reference package. --> 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;"> It spans the text block: 30–190mm portrait, 15–282mm landscape. -->
<div style="border-top:.75pt solid #111; left:30mm; position:absolute; top:0; width:160mm;"></div> <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="color:#111; font-size:9pt; left:30mm; position:absolute; top:1.2mm;">{{standard}}</div> <div style="border-top:.75pt solid #111; left:{{textLeft}}; position:absolute; top:0; width:{{textWidth}};"></div>
<div style="color:#111; font-size:9pt; position:absolute; right:20mm; text-align:right; top:1.2mm;">{{footerTag}}</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> </div>
+4 -4
View File
@@ -1,12 +1,12 @@
<!-- Repeating page header reproducing the corporate identity of the audited <!-- Repeating page header reproducing the corporate identity of the audited
reference package. Every position is in millimetres measured from the 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 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 55.4mm, and the address block beneath it. The Latin face is Liberation
Sans, metric-compatible with the Arial of the reference. --> 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="-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:64.7mm;"></div> <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: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="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;"> <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="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> <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-collapse: collapse;
border: .5pt solid var(--brn-line); border: .5pt solid var(--brn-line);
break-inside: auto; break-inside: auto;
font-size: 9pt; font-size: 8.5pt;
margin: 0 0 5mm; margin: 0 0 5mm;
table-layout: auto; table-layout: auto;
width: 100%; 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 { 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 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; } .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. # Page box measured from the audited reference package, as in sdlc-delivery.css.
BODY_TOP_MM = 32 BODY_TOP_MM = 32
FOOTER_FROM_BOTTOM_MM = 24.2 # footer rule at 272.8mm on a 297mm page 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' STANDARD = 'ISO/IEC 29110-4-1:2018'
RULE_SIZE = '4' # .5pt table rule, as printed RULE_SIZE = '4' # .5pt table rule, as printed
RULE_COLOUR = '595959' # --brn-line from sdlc-delivery.css RULE_COLOUR = '595959' # --brn-line from sdlc-delivery.css
@@ -702,6 +701,68 @@ def build_table(document, rows):
return table 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): def restore_dropped_tables(document, markdown):
"""Put back tables the converter left out, where the source places them. """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.left_indent = -section.left_margin
header.paragraph_format.space_before = Pt(0) header.paragraph_format.space_before = Pt(0)
header.paragraph_format.space_after = 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] footer = section.footer.paragraphs[0]
drop_style(footer) drop_style(footer)
@@ -789,7 +850,7 @@ def install_letterhead(document, band, footer_tag):
style_run(footer.add_run(f'\t{footer_tag}'), 9) 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') docx = pdf.with_suffix('.docx')
converter = Converter(str(pdf)) converter = Converter(str(pdf))
try: try:
@@ -806,6 +867,8 @@ def convert(pdf, source, band, footer_tag):
markdown = source.read_text(encoding='utf8') markdown = source.read_text(encoding='utf8')
restore_text(document, markdown, bullets) restore_text(document, markdown, bullets)
restore_dropped_tables(document, markdown) 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: for paragraph in bullets:
if paragraph.getparent() is not None: if paragraph.getparent() is not None:
make_bullet(document, paragraph) make_bullet(document, paragraph)
@@ -816,9 +879,9 @@ def convert(pdf, source, band, footer_tag):
return docx return docx
def render_bands(source_root, band_root): def render_bands(source_root, band_root, pdf_root):
subprocess.run( 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 check=True
) )
return json.loads((Path(band_root) / 'bands.json').read_text(encoding='utf8')) 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]) source_root, delivery_root = Path(argv[0]), Path(argv[1])
temporary = None temporary = None
if len(argv) > 2: 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: else:
temporary = tempfile.TemporaryDirectory() 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 = [] failures = []
for index, (relative, band) in enumerate(sorted(bands.items()), start=1): for index, (relative, band) in enumerate(sorted(bands.items()), start=1):
pdf = delivery_root / Path(relative).with_suffix('.pdf') pdf = delivery_root / Path(relative).with_suffix('.pdf')
try: 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)}') print(f'[{index}/{len(bands)}] {docx.relative_to(delivery_root)}')
except Exception as error: # noqa: BLE001 - reported, not swallowed except Exception as error: # noqa: BLE001 - reported, not swallowed
failures.append((pdf, error)) 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 path from 'node:path';
import { fileURLToPath } from 'node:url'; import { fileURLToPath } from 'node:url';
import { chromium } from 'playwright'; import { chromium } from 'playwright';
import { PAGE_MM, headerValues, isLandscapePdf } from './letterhead.mjs';
const here = path.dirname(fileURLToPath(import.meta.url)); const here = path.dirname(fileURLToPath(import.meta.url));
const root = path.resolve(here, '../..'); const root = path.resolve(here, '../..');
const assets = path.join(here, 'assets'); 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 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 applyTemplate = (template, values) => template.replace(/{{(\w+)}}/g, (_, key) => values[key] ?? '');
const slug = (title) => title.replace(/[^\w-]+/g, '-').replace(/^-|-$/g, '').toLowerCase() || 'band'; 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 entries = await fs.readdir(directory, { withFileTypes: true });
const results = await Promise.all(entries.map(async (entry) => { const results = await Promise.all(entries.map(async (entry) => {
const target = path.join(directory, entry.name); const target = path.join(directory, entry.name);
if (entry.isDirectory()) return markdownFiles(target); if (entry.isDirectory()) return markdownFiles(target, depth + 1);
return entry.isFile() && entry.name.endsWith('.md') ? [target] : []; return depth > 0 && entry.isFile() && entry.name.endsWith('.md') ? [target] : [];
})); }));
return results.flat(); return results.flat();
} }
async function main() { async function main() {
const [sourceRoot, outputRoot] = process.argv.slice(2); // PDF_DIRECTORY holds the rendered PDFs: each band matches its PDF's orientation.
if (!sourceRoot || !outputRoot) throw new Error('Usage: render-ci-band.mjs SOURCE_DIRECTORY OUTPUT_DIRECTORY'); 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([ const [template, logo] = await Promise.all([
fs.readFile(path.join(assets, 'header.html'), 'utf8'), 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 title = markdown.match(/^#\s+(.+)$/m)?.[1]?.trim() ?? 'BRN WMS';
const footer = markdown.match(/^<!--\s*footer:\s*(.+?)\s*-->\s*$/m)?.[1] ?? ''; const footer = markdown.match(/^<!--\s*footer:\s*(.+?)\s*-->\s*$/m)?.[1] ?? '';
if (!rendered.has(title)) { const relative = path.relative(sourceRoot, source);
const file = path.join(outputRoot, `${slug(title)}.png`); 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( await page.setContent(
'<!doctype html><meta charset="utf-8"><body style="margin:0;background:#fff">' + '<!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">` + `<div id="band" style="width:${widthMm}mm;height:${BAND_MM.height}mm;overflow:hidden;position:relative">` +
`${applyTemplate(template, { ...values, documentTitle: title })}</div></body>`, `${applyTemplate(template, { ...values, ...headerValues(title, landscape) })}</div></body>`,
{ waitUntil: 'networkidle' } { waitUntil: 'networkidle' }
); );
await page.locator('#band').screenshot({ type: 'png', path: file }); 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 { } finally {
await browser.close(); await browser.close();
+218 -14
View File
@@ -3,6 +3,7 @@ import { readFileSync } from 'node:fs';
import path from 'node:path'; import path from 'node:path';
import { fileURLToPath } from 'node:url'; import { fileURLToPath } from 'node:url';
import { chromium } from 'playwright'; import { chromium } from 'playwright';
import { headerValues } from './letterhead.mjs';
const here = path.dirname(fileURLToPath(import.meta.url)); const here = path.dirname(fileURLToPath(import.meta.url));
const root = path.resolve(here, '../..'); const root = path.resolve(here, '../..');
@@ -13,7 +14,15 @@ const escapeHtml = (value) => value
.replaceAll('<', '&lt;') .replaceAll('<', '&lt;')
.replaceAll('>', '&gt;'); .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) 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 // 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. // several files on separate lines the way the reference package does.
.replaceAll('&lt;br&gt;', '<br>') .replaceAll('&lt;br&gt;', '<br>')
@@ -93,7 +102,7 @@ function renderTable(lines, className = '') {
const attr = classFor(column); const attr = classFor(column);
return `<${tag}${attr ? ` class="${attr}"` : ''}>${renderInline(cell)}</${tag}>`; return `<${tag}${attr ? ` class="${attr}"` : ''}>${renderInline(cell)}</${tag}>`;
}).join(''); }).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) 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 class="group"><td colspan="${header.length}">${renderInline(row[0])}</td></tr>`
: `<tr>${cells('td', row)}</tr>`).join('')}</tbody></table>`; : `<tr>${cells('td', row)}</tr>`).join('')}</tbody></table>`;
@@ -218,13 +227,13 @@ async function main() {
fs.readFile(path.join(assets, 'LeelawadeeUI.ttf')) fs.readFile(path.join(assets, 'LeelawadeeUI.ttf'))
]); ]);
const { title, footer, metadata, body } = extractDocument(markdown); const { title, footer, metadata, body } = extractDocument(markdown);
// The reference package prints every work product portrait except the // Work products print portrait unless a table needs the width: the evidence
// evidence index and the summary traceability matrix. // 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 LANDSCAPE_TITLES = new Set(['List of Evidence', 'Traceability Record Table']);
const isLandscape = LANDSCAPE_TITLES.has(title);
const values = { const values = {
logoPath: `data:image/svg+xml;base64,${logo.toString('base64')}`, logoPath: `data:image/svg+xml;base64,${logo.toString('base64')}`,
documentTitle: title,
documentType: metadata['Document No'] ?? title, documentType: metadata['Document No'] ?? title,
ciBlue: '#0797d5', ciBlue: '#0797d5',
ciGrey: '#8e8e8e', // ribbon grey measured from the audited reference documents ciGrey: '#8e8e8e', // ribbon grey measured from the audited reference documents
@@ -236,32 +245,227 @@ async function main() {
pageNumber: '<span class="pageNumber"></span>', pageNumber: '<span class="pageNumber"></span>',
totalPages: '<span class="totalPages"></span>' totalPages: '<span class="totalPages"></span>'
}; };
const pdfHeader = applyTemplate(headerTemplate, values); const headerFor = (landscape) => applyTemplate(headerTemplate, { ...values, ...headerValues(title, landscape) });
const pdfFooter = applyTemplate(footerTemplate, values); 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 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 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 }); await fs.mkdir(path.dirname(destination), { recursive: true });
const browser = await chromium.launch({ headless: true }); const browser = await chromium.launch({ headless: true });
try { try {
const page = await browser.newPage(); 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({ await page.pdf({
path: destination, path: destination,
format: 'A4', format: 'A4',
printBackground: true, printBackground: true,
preferCSSPageSize: true, preferCSSPageSize: true,
displayHeaderFooter: true, displayHeaderFooter: true,
headerTemplate: `${templateFont}${pdfHeader}`, headerTemplate: `${templateFont}${headerFor(landscape)}`,
footerTemplate: `${templateFont}${pdfFooter}` footerTemplate: `${templateFont}${footerFor(landscape)}`
}); });
} finally { } finally {
await browser.close(); 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); }); 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, ''); 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 entries = await fs.readdir(directory, { withFileTypes: true });
const results = await Promise.all(entries.map(async (entry) => { const results = await Promise.all(entries.map(async (entry) => {
const target = path.join(directory, entry.name); const target = path.join(directory, entry.name);
if (entry.isDirectory()) return markdownFiles(target); if (entry.isDirectory()) return markdownFiles(target, depth + 1);
return entry.isFile() && entry.name.endsWith('.md') ? [target] : []; return depth > 0 && entry.isFile() && entry.name.endsWith('.md') ? [target] : [];
})); }));
return results.flat(); return results.flat();
} }
+3 -1
View File
@@ -74,7 +74,9 @@ async function main() {
const hindsight = hindsightErrors(docs, root); 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- ')}`); 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 folder of FOLDERS) await fs.mkdir(path.join(target, folder), { recursive: true });
for (const d of docs) { for (const d of docs) {
await fs.writeFile(path.join(target, d.dir, `${d.name}.md`), d.md, 'utf8'); await fs.writeFile(path.join(target, d.dir, `${d.name}.md`), d.md, 'utf8');
+20 -12
View File
@@ -1,8 +1,8 @@
// 3-Other Document: Stakeholder Register, Project Charter, Traceability Record Table, Training Report, List of Evidence. // 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 { 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 } from './engineering.mjs'; import { SRS, UNITS, TEST_CASES, traceOf, srTopic, unitTopic, crStatus } from './engineering.mjs';
import { CORRECTIONS, PERIODS, MEETINGS, TASKS } from './history.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'; import { workProduct, head } from './format.mjs';
const OTHER = '3-Other Document'; const OTHER = '3-Other Document';
@@ -29,10 +29,7 @@ export function stakeholderRegister() {
[4, P.ThS.th, P.ThS.id, 'Developer', 'พัฒนาระบบตามการออกแบบ ดูแลความปลอดภัย การติดตั้ง และการสำรองข้อมูล', '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)'], [6, P.YaB.th, P.YaB.id, 'Document Control', 'ควบคุมรหัสเอกสาร เวอร์ชัน การอนุมัติ การจัดเก็บ และหลักฐานของโครงการ', 'R (Responsible)'],
[7, 'หัวหน้าฝ่ายคลังสินค้าและพนักงานคลัง', '—', 'ผู้ใช้งานหลัก', 'ให้ข้อมูลกระบวนการทำงาน ทดสอบการใช้งาน และใช้งานระบบจริง', 'C (Consult), R (Review)'], ...OTHER_STAKEHOLDERS.map(([name, role, duty, engagement], i) => [7 + i, name, '—', role, duty, engagement]),
[8, 'ฝ่ายขายและฝ่ายจัดซื้อ', '—', 'ผู้ใช้งาน', 'ให้ข้อมูลกระบวนการเอกสารขายและจัดซื้อ และทดสอบการใช้งาน', 'C (Consult)'],
[9, 'ฝ่ายบัญชีและการเงิน', '—', 'ผู้ใช้งาน', 'ให้ข้อมูลการวางบิล รับชำระ จ่ายชำระ และการบันทึกบัญชี', 'C (Consult)'],
[10, 'ผู้ดูแลระบบ (System Administrator)', '—', 'ผู้สนับสนุน', 'ติดตั้ง ตั้งค่า เฝ้าระวัง สำรองและกู้คืนระบบ', 'C (Consult), I (Inform)'],
], ['c', '', 'c', '', '', '']), ], ['c', '', 'c', '', '', '']),
'หมายเหตุ: รหัสระดับการมีส่วนร่วม (Engagement Level) ได้แก่ A: Approve, R: Responsible, C: Consult, I: Inform ตามแนวทาง RACI Matrix เพื่อระบุระดับการมีส่วนร่วมในแต่ละบทบาท', 'หมายเหตุ: รหัสระดับการมีส่วนร่วม (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'], [1, 'รายงานความก้าวหน้า (Progress Status Record)', 'ทุกงวดรายงาน', P.ApS.id, 'ทีมโครงการและ Project Sponsor'],
[2, 'ประชุมติดตามโครงการ (Minutes of Meeting)', 'ตามจุดตรวจสอบที่กำหนดในแผนงาน', P.ApS.id, 'ผู้เกี่ยวข้องทั้งหมด'], [2, 'ประชุมติดตามโครงการ (Minutes of Meeting)', 'ตามจุดตรวจสอบที่กำหนดในแผนงาน', P.ApS.id, 'ผู้เกี่ยวข้องทั้งหมด'],
[3, 'รายงานข้อบกพร่องและการแก้ไข', 'เมื่อพบข้อบกพร่อง', `${P.PaNg.id} / ${P.ThS.id}`, `${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']), ], ['c']),
].join('\n\n'), ].join('\n\n'),
secretary: P.ApS, reviewer: P.YaB, approver: P.SeV, 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. */ /** Traceability Record Table — the summary matrix filed alongside the master record. */
export function traceabilityTable() { export function traceabilityTable() {
const date = '2026-08-17'; 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 rows = CRS.map(([id, text]) => {
const t = traceOf(id); 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)); const traced = CRS.map(([id]) => traceOf(id));
return { return {
dir: OTHER, dir: OTHER,
@@ -145,8 +151,10 @@ export function traceabilityTable() {
['เชื่อมโยงกับ Software Unit', traced.filter((t) => t.un.length).length], ['เชื่อมโยงกับ Software Unit', traced.filter((t) => t.un.length).length],
['เชื่อมโยงกับ Test Case', traced.filter((t) => t.tcs.length).length], ['เชื่อมโยงกับ Test Case', traced.filter((t) => t.tcs.length).length],
['ยืนยันด้วยเอกสารส่งมอบ (ไม่มี Software Unit)', traced.filter((t) => !t.tcs.length).length], ['ยืนยันด้วยเอกสารส่งมอบ (ไม่มี Software Unit)', traced.filter((t) => !t.tcs.length).length],
['ผลการทดสอบหรือการตรวจสอบเอกสารผ่าน', CRS.length], ['ผ่าน (Passed)', count('Passed')],
['ผลการทดสอบการยอมรับผ่าน', CRS.length], ['ไม่ผ่าน (Failed) — ข้อบกพร่องคงค้างใน Correction Register', count('Failed')],
['ยังไม่ได้ทดสอบ (ชุดติดตั้ง Docker ส่งมอบหลังรอบทดสอบ)', count('Not tested')],
['รอดำเนินการตามแผน (Pending)', count('Pending')],
], ['', 'r']), ], ['', 'r']),
].join('\n\n'), ].join('\n\n'),
secretary: P.NoC, reviewer: P.PaNg, approver: P.SeV, 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'), 'บันทึกความก้าวหน้าของโครงการรายงวด'], [3, `Progress Status Record (${files('3.Progress Status Record').length} Report)`, multi('3.Progress Status Record'), 'บันทึกความก้าวหน้าของโครงการรายงวด'],
[4, 'Correction Register', one('4.Correction Register'), 'เอกสารสรุปปัญหาที่พบระหว่างดำเนินโครงการ'], [4, 'Correction Register', one('4.Correction Register'), 'เอกสารสรุปปัญหาที่พบระหว่างดำเนินโครงการ'],
[5, 'Acceptance Record', one('5.Acceptance Report'), 'บันทึกการส่งมอบงาน (Sign off Letter)'], [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'], [7, `Meeting Record (${files('7.Meeting Record').length} MoM Report)`, multi('7.Meeting Record'), 'รายงานการประชุมตามแผนการทำงานใน Work Schedule'],
[8, 'Software Configuration', one('8.Software Configuration'), 'บันทึกการกำหนดค่าและความเปลี่ยนแปลงต่าง ๆ ที่เกิดขึ้นของระบบ'], [8, 'Software Configuration', one('8.Software Configuration'), 'บันทึกการกำหนดค่าและความเปลี่ยนแปลงต่าง ๆ ที่เกิดขึ้นของระบบ'],
[9, 'Project Repository', one('9.Project Repository'), `พื้นที่เก็บข้อมูล เอกสาร และหลักฐานทุกอย่างของโครงการ (Git Server: \`${ORIGIN}\`)`], [9, 'Project Repository', one('9.Project Repository'), `พื้นที่เก็บข้อมูล เอกสาร และหลักฐานทุกอย่างของโครงการ (Git Server: \`${ORIGIN}\`)`],
+8 -4
View File
@@ -1,5 +1,5 @@
// PM work products 1–2: Statement of Work, Work Schedule, Software Project Plan, Customer Requirements. // 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 { TASKS, VERIFICATION_ROUNDS, VERIFICATION_RULES } from './history.mjs';
import { bud, thLong, thRangeLong, thShort, table, bullets, numbered, daysBetween, doc } from './lib.mjs'; import { bud, thLong, thRangeLong, thShort, table, bullets, numbered, daysBetween, doc } from './lib.mjs';
import { workProduct, head } from './format.mjs'; import { workProduct, head } from './format.mjs';
@@ -101,7 +101,7 @@ export function workSchedule() {
['เริ่มพัฒนาระบบ', thLong(DEV_START), 'เริ่ม Task 3.1 ตามแผน'], ['เริ่มพัฒนาระบบ', thLong(DEV_START), 'เริ่ม Task 3.1 ตามแผน'],
['Baseline การพัฒนา', thLong('2026-05-29'), 'พัฒนาครบทุกโมดูลตามขอบเขต และกำหนด Git Baseline การพัฒนา'], ['Baseline การพัฒนา', thLong('2026-05-29'), 'พัฒนาครบทุกโมดูลตามขอบเขต และกำหนด Git Baseline การพัฒนา'],
['ทดสอบระบบและ UAT', thRangeLong(TEST_FROM, TEST_TO), `Test Case ผ่านครบ และ UAT ผ่านครบ 14 หมวดความต้องการ (${CRS.length} รายการ)`], ['ทดสอบระบบและ 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('2026-08-22'), 'ผู้ใช้งานทุกฝ่ายที่เกี่ยวข้องผ่านการอบรม'],
['ปิดโครงการ', thLong(END), 'ปิดโครงการอย่างเป็นทางการ'], ['ปิดโครงการ', thLong(END), 'ปิดโครงการอย่างเป็นทางการ'],
], ['', 'c', '']), ], ['', 'c', '']),
@@ -205,6 +205,10 @@ export function softwareProjectPlan() {
[5, `Document Control (${P.YaB.id})`, P.YaB.th, P.YaB.email, 'ควบคุมรหัสเอกสารและเวอร์ชัน / จัดเก็บและตรวจสอบความครบถ้วนของเอกสาร / ประสานการทบทวนและอนุมัติ / บันทึกประวัติการเปลี่ยนแปลงเอกสาร'], [5, `Document Control (${P.YaB.id})`, P.YaB.th, P.YaB.email, 'ควบคุมรหัสเอกสารและเวอร์ชัน / จัดเก็บและตรวจสอบความครบถ้วนของเอกสาร / ประสานการทบทวนและอนุมัติ / บันทึกประวัติการเปลี่ยนแปลงเอกสาร'],
[6, `Project Sponsor (${P.SeV.id})`, P.SeV.th, P.SeV.email, 'อนุมัติขอบเขต งบประมาณ และการเปลี่ยนแปลง / ตัดสินใจเชิงกลยุทธ์ / ทดสอบการยอมรับและตรวจรับส่งมอบ / อนุมัติปิดโครงการ'], [6, `Project Sponsor (${P.SeV.id})`, P.SeV.th, P.SeV.email, 'อนุมัติขอบเขต งบประมาณ และการเปลี่ยนแปลง / ตัดสินใจเชิงกลยุทธ์ / ทดสอบการยอมรับและตรวจรับส่งมอบ / อนุมัติปิดโครงการ'],
], ['c']), ], ['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'), sec(6, 'Project Estimate'),
'### 6.1 Size of the Software Work Products', '### 6.1 Size of the Software Work Products',
table(['ลำดับ', 'Work Product', 'ปริมาณ (ประมาณการ)'], [ table(['ลำดับ', 'Work Product', 'ปริมาณ (ประมาณการ)'], [
@@ -279,7 +283,7 @@ export function softwareProjectPlan() {
'แผนรองรับสำหรับกรณีงานไม่แล้วเสร็จตามกำหนด (Non-Completed Task)', 'แผนรองรับสำหรับกรณีงานไม่แล้วเสร็จตามกำหนด (Non-Completed Task)',
table(['ลำดับ', 'สถานการณ์', 'สาเหตุที่พบบ่อย', 'ผลกระทบที่อาจเกิด', 'แผนรองรับเมื่อเกิดเหตุ', 'ผู้รับผิดชอบ'], [ table(['ลำดับ', 'สถานการณ์', 'สาเหตุที่พบบ่อย', 'ผลกระทบที่อาจเกิด', 'แผนรองรับเมื่อเกิดเหตุ', 'ผู้รับผิดชอบ'], [
[1, 'งานไม่เสร็จภายในกำหนด (Due Date)', 'ประเมินเวลาผิดพลาด หรือทรัพยากรไม่เพียงพอ', 'กระทบ Timeline และกำหนดส่งมอบ', 'ประชุมด่วนเพื่อประเมินสถานะ จัดลำดับความสำคัญใหม่ ปรับแผน และแจ้งลูกค้าอย่างเป็นทางการ', P.ApS.id], [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}`], [3, 'งานค้างเพราะรอข้อมูลจากผู้ใช้', 'ผู้ให้ข้อมูลส่งความต้องการหรือข้อมูลตั้งต้นล่าช้า', 'ไม่สามารถพัฒนาหรือทดสอบต่อได้', 'แจ้งเตือนเป็นลายลักษณ์อักษร แยกงานที่ยังไม่พร้อมออก และนัดยืนยัน Timeline ใหม่', `${P.ApS.id} / ${P.SeV.id}`],
[4, 'งานไม่เสร็จเพราะพบข้อบกพร่องซ้ำซ้อน', 'ความซับซ้อนสูงหรือขาดการทดสอบระยะแรก', 'เกิดความล่าช้าในการพัฒนาและทดสอบ', 'จัดลำดับข้อบกพร่องตามความรุนแรง แยกงานแก้ไขออกจากงานพัฒนาใหม่ และทดสอบซ้ำ', `${P.ThS.id} / ${P.PaNg.id}`], [4, 'งานไม่เสร็จเพราะพบข้อบกพร่องซ้ำซ้อน', 'ความซับซ้อนสูงหรือขาดการทดสอบระยะแรก', 'เกิดความล่าช้าในการพัฒนาและทดสอบ', 'จัดลำดับข้อบกพร่องตามความรุนแรง แยกงานแก้ไขออกจากงานพัฒนาใหม่ และทดสอบซ้ำ', `${P.ThS.id} / ${P.PaNg.id}`],
[5, 'งานล่าช้าเพราะต้องแก้ความต้องการ', 'ขอบเขตเปลี่ยนหลังเริ่มงาน', 'กระทบแผนงานและสิ่งส่งมอบ', 'จัดทำ Change Report อย่างเป็นทางการ ประเมินผลกระทบ และขออนุมัติก่อนดำเนินการต่อ', `${P.ApS.id} / ${P.SeV.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}`, `เอกสารฉบับนี้จัดทำขึ้นเพื่อบันทึกและสรุปความต้องการของลูกค้า (Customer Requirements) ตามกระบวนการ ISO/IEC 29110 สำหรับใช้เป็นพื้นฐานในการจัดทำ Software Requirements Specification, การออกแบบระบบ, การทดสอบ และการตรวจรับ ของ${PROJECT_TH}`,
'## ผู้มีส่วนได้ส่วนเสีย (Stakeholders)', '## ผู้มีส่วนได้ส่วนเสีย (Stakeholders)',
table(['ลำดับ', 'ชื่อ', 'ตำแหน่ง', 'บทบาท'], [ table(['ลำดับ', 'ชื่อ', 'ตำแหน่ง', 'บทบาท'], [
[1, P.SeV.th, 'ผู้บริหาร (CEO)', 'Project Sponsor อนุมัติและตัดสินใจเชิงกลยุทธ์'], [1, P.SeV.th, P.SeV.pos, 'Project Sponsor อนุมัติและตัดสินใจเชิงกลยุทธ์'],
[2, P.ApS.th, 'ผู้จัดการโครงการ', 'บริหารโครงการและควบคุมการเปลี่ยนแปลง'], [2, P.ApS.th, 'ผู้จัดการโครงการ', 'บริหารโครงการและควบคุมการเปลี่ยนแปลง'],
[3, P.NoC.th, 'นักวิเคราะห์ระบบ', 'เก็บและวิเคราะห์ความต้องการ จัดทำเอกสาร'], [3, P.NoC.th, 'นักวิเคราะห์ระบบ', 'เก็บและวิเคราะห์ความต้องการ จัดทำเอกสาร'],
[4, 'หัวหน้าฝ่ายคลังสินค้าและพนักงานคลัง', 'ผู้ใช้งานหลัก', 'ให้ข้อมูลกระบวนการรับเข้า จ่ายออก โอนย้าย และตรวจนับ'], [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 { 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 { bud, thLong, thShort, thRange, thRangeLong, table, bullets, daysBetween } from './lib.mjs';
import { workProduct, head } from './format.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 ids = (arr) => arr.map((i) => P[i].id).join(', ');
const dir3 = '1-PM Process (10 Work Product)/3.Progress Status Record'; const dir3 = '1-PM Process (10 Work Product)/3.Progress Status Record';
@@ -20,7 +21,9 @@ export function progressStatusRecords() {
// detail, owner and status. // detail, owner and status.
const issues = issueIds.map((iid, n) => { const issues = issueIds.map((iid, n) => {
const c = CORRECTIONS.find((x) => x[0] === iid); 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.`); if (changeIds.length) throw new Error(`Progress record ${date} lists change requests, but the project has none.`);
const changes = []; const changes = [];
@@ -60,7 +63,8 @@ export function progressStatusRecords() {
/** 4. Correction Register — all issues found during the project. */ /** 4. Correction Register — all issues found during the project. */
export function correctionRegister() { export function correctionRegister() {
const date = '2026-08-17'; 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; const bySev = (s) => CORRECTIONS.filter((c) => c[3] === s).length;
return { return {
dir: '1-PM Process (10 Work Product)/4.Correction Register', dir: '1-PM Process (10 Work Product)/4.Correction Register',
@@ -82,16 +86,16 @@ export function correctionRegister() {
['ระดับความรุนแรงสูง', bySev('สูง')], ['ระดับความรุนแรงสูง', bySev('สูง')],
['ระดับความรุนแรงกลาง', bySev('กลาง')], ['ระดับความรุนแรงกลาง', bySev('กลาง')],
['ระดับความรุนแรงต่ำ', bySev('ต่ำ')], ['ระดับความรุนแรงต่ำ', bySev('ต่ำ')],
['แก้ไขแล้วเสร็จและตรวจสอบผลแล้ว', CORRECTIONS.length], ['แก้ไขแล้วเสร็จและตรวจสอบผลแล้ว', CORRECTIONS.length - open.length],
['คงค้าง', 0], ['คงค้าง', open.length],
], ['', 'r']), ], ['', '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)', '## บทเรียนและแนวทางป้องกันสำหรับโครงการถัดไป (Lessons Learned)',
'เมื่อปิดโครงการ ทีมงานทบทวนปัญหาทั้งหมดในทะเบียนนี้ จัดกลุ่มตามสาเหตุ และกำหนดแนวทางป้องกันเพื่อไม่ให้ปัญหาลักษณะเดียวกันเกิดซ้ำในโครงการถัดไป', 'เมื่อปิดโครงการ ทีมงานทบทวนปัญหาทั้งหมดในทะเบียนนี้ จัดกลุ่มตามสาเหตุ และกำหนดแนวทางป้องกันเพื่อไม่ให้ปัญหาลักษณะเดียวกันเกิดซ้ำในโครงการถัดไป',
table(['ลำดับ', 'กลุ่มปัญหา', 'เลขที่ปัญหา', 'จำนวน', 'สิ่งที่เกิดขึ้น', 'แนวทางป้องกัน (Preventive Action)'], table(['ลำดับ', 'กลุ่มปัญหา', 'เลขที่ปัญหา', 'จำนวน', 'สิ่งที่เกิดขึ้น', 'แนวทางป้องกัน (Preventive Action)'],
LESSONS.map(([theme, issueIds, what, action], i) => [i + 1, theme, issueIds.join(', '), issueIds.length, what, action]), ['c', '', '', 'r', '', '']), 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'), ].join('\n\n'),
secretary: P.YaB, reviewer: P.ApS, approver: P.SeV, secretary: P.YaB, reviewer: P.ApS, approver: P.SeV,
}), }),
@@ -100,6 +104,10 @@ export function correctionRegister() {
/** 5. Acceptance Report — the sign-off record. */ /** 5. Acceptance Report — the sign-off record. */
export function acceptanceReport() { 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 { return {
dir: '1-PM Process (10 Work Product)/5.Acceptance Report', dir: '1-PM Process (10 Work Product)/5.Acceptance Report',
name: `${CODE} Acceptance Report ${bud(ACCEPT_DATE)} V1.0 ApS`, name: `${CODE} Acceptance Report ${bud(ACCEPT_DATE)} V1.0 ApS`,
@@ -115,26 +123,27 @@ export function acceptanceReport() {
body: [ body: [
'## รายการสิ่งส่งมอบ (Work Products)', '## รายการสิ่งส่งมอบ (Work Products)',
table(['ลำดับ', 'รหัส', 'สิ่งส่งมอบ (Work Product)', 'รายละเอียด', 'ผลการตรวจรับ (Accepted / Not Accepted)'], 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(['ลำดับ', 'เกณฑ์การตรวจรับ', 'หลักฐานอ้างอิง', 'ผลการตรวจสอบ'], [ table(['ลำดับ', 'เกณฑ์การตรวจรับ', 'หลักฐานอ้างอิง', 'ผลการตรวจสอบ'], [
[1, 'ฟังก์ชันในขอบเขตทำงานตรงตาม Customer Requirements ครบทุกรายการ', 'Traceability Record, Test Report', 'ผ่าน'], [1, 'ฟังก์ชันในขอบเขตทำงานตรงตาม Customer Requirements ครบทุกรายการ', `Traceability Record Table, Validation Results: ไม่ผ่าน ${failedCrs.length} รายการ ยังไม่ได้ทดสอบ ${untestedCrs.length} รายการ`, 'ไม่ผ่าน (มีเงื่อนไข)'],
[2, 'ทดสอบระบบตาม Test Case ครบถ้วน', `Test Report 45 Test Case ผ่านทั้งหมด (${thRangeLong(TEST_FROM, TEST_TO)})`, 'ผ่าน'], [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 ครบ 14 หมวดความต้องการ (${CRS.length} รายการ) ผ่านทั้งหมด`, 'ผ่าน'], [3, 'ทดสอบการยอมรับโดยผู้ใช้ (UAT)', `Validation Results ทดสอบครบ ${CRS.length} รายการ ผลตามข้อ 1`, 'ไม่ผ่าน (มีเงื่อนไข)'],
[4, 'ไม่มีข้อบกพร่องระดับวิกฤตคงค้าง', `Correction Register ${CORRECTIONS.length} รายการ แก้ไขและตรวจสอบครบ`, 'ผ่าน'], [4, 'ไม่มีข้อบกพร่องระดับวิกฤตคงค้าง', `Correction Register ข้อบกพร่องระดับสูงคงค้าง ${open.map((c) => c[0]).join(', ')}`, 'ไม่ผ่าน (มีเงื่อนไข)'],
[5, 'เอกสารคู่มือผู้ใช้ ผู้ดูแลระบบ และการบำรุงรักษาครบถ้วน', 'Software User Document, Product Operation Guide, Maintenance Document', 'ผ่าน'], [5, 'เอกสารคู่มือผู้ใช้ ผู้ดูแลระบบ และการบำรุงรักษาครบถ้วน', 'Software User Document, Product Operation Guide, Maintenance Document', 'ผ่าน'],
[6, 'Source Code และเอกสารจัดเก็บใน Repository พร้อมชุดสำรอง', 'Project Repository, Project Repository (Backup)', 'ผ่าน'], [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']), ], ['c', '', '', 'c']),
'## สรุปผลการตรวจรับ', '## สรุปผลการตรวจรับ',
table(['รายการ', 'ผลสรุป'], [ table(['รายการ', 'ผลสรุป'], [
['จำนวนสิ่งส่งมอบทั้งหมด', `${WPS.length} รายการ`], ['จำนวนสิ่งส่งมอบทั้งหมด', `${WPS.length} รายการ`],
['ตรวจรับแล้ว (Accepted)', `${WPS.length} รายการ`], ['ตรวจรับแล้ว (Accepted)', `${WPS.length - 1} รายการ`],
['ตรวจรับแบบมีเงื่อนไข', '1 รายการ (WP 9.0 Software)'],
['ไม่ผ่านการตรวจรับ (Not Accepted)', '0 รายการ'], ['ไม่ผ่านการตรวจรับ (Not Accepted)', '0 รายการ'],
['ข้อบกพร่องคงค้าง', 'ไม่มี'], ['ข้อบกพร่องคงค้าง', `${open.length} รายการ (${open.map((c) => c[0]).join(', ')})`],
['ผลการตรวจรับโดยรวม', '**Accepted** — ตรวจรับส่งมอบระบบครบถ้วน'], ['ผลการตรวจรับโดยรวม', '**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'), ].join('\n\n'),
secretary: P.ApS, reviewer: P.YaB, approver: P.SeV, 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. */ /** 7. Meeting Record — Minutes of Meeting per checkpoint. */
export function meetingRecords() { export function meetingRecords() {
return MEETINGS.map((m) => { return MEETINGS.map((m) => {
@@ -202,7 +222,7 @@ export function meetingRecords() {
['Task ID / Task Name', `${m.taskId} ${m.taskName}`], ['Task ID / Task Name', `${m.taskId} ${m.taskName}`],
['Work Period of Task', thRangeLong(t[5], t[6])], ['Work Period of Task', thRangeLong(t[5], t[6])],
['Meeting Date', thLong(m.date), 'Time', m.time], ['Meeting Date', thLong(m.date), 'Time', m.time],
['สิ่งแนบ', m.attachment], ['สิ่งแนบ', attachments(m)],
['Location', `ห้องประชุม ${COMPANY_TH}`], ['Location', `ห้องประชุม ${COMPANY_TH}`],
['Organizer', ORGANIZER], ['Organizer', ORGANIZER],
['Recorder', RECORDER], ['Recorder', RECORDER],
+34 -27
View File
@@ -1,6 +1,6 @@
// SI work products 11–22. // 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 { 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 { CORRECTIONS } from './history.mjs';
import { figure } from './figures.mjs'; import { figure } from './figures.mjs';
import { bud, thLong, thShort, thRange, thRangeLong, table, bullets, numbered } from './lib.mjs'; import { bud, thLong, thShort, thRange, thRangeLong, table, bullets, numbered } from './lib.mjs';
@@ -32,7 +32,7 @@ export function softwareRequirements() {
'เพื่อแปลงความต้องการของลูกค้าที่ได้รับอนุมัติแล้วให้เป็นความต้องการเชิงเทคนิคของซอฟต์แวร์ ครอบคลุมมาตรฐาน โครงสร้าง ส่วนประกอบ ความสัมพันธ์ ประสิทธิภาพ ส่วนเชื่อมต่อ ความปลอดภัย ฐานข้อมูล และการจัดการข้อผิดพลาด เพื่อใช้เป็นเอกสารอ้างอิงหลักในการออกแบบระบบ พัฒนา และทดสอบ', 'เพื่อแปลงความต้องการของลูกค้าที่ได้รับอนุมัติแล้วให้เป็นความต้องการเชิงเทคนิคของซอฟต์แวร์ ครอบคลุมมาตรฐาน โครงสร้าง ส่วนประกอบ ความสัมพันธ์ ประสิทธิภาพ ส่วนเชื่อมต่อ ความปลอดภัย ฐานข้อมูล และการจัดการข้อผิดพลาด เพื่อใช้เป็นเอกสารอ้างอิงหลักในการออกแบบระบบ พัฒนา และทดสอบ',
'## ผู้มีส่วนได้ส่วนเสีย (Stakeholders)', '## ผู้มีส่วนได้ส่วนเสีย (Stakeholders)',
table(['ลำดับ', 'ชื่อ', 'หน่วยงาน/ตำแหน่ง', 'บทบาท'], [ table(['ลำดับ', 'ชื่อ', 'หน่วยงาน/ตำแหน่ง', 'บทบาท'], [
[1, P.SeV.th, 'ผู้บริหาร (CEO)', 'Project Sponsor อนุมัติความต้องการ'], [1, P.SeV.th, P.SeV.pos, 'Project Sponsor อนุมัติความต้องการ'],
[2, P.ApS.th, 'ผู้จัดการโครงการ', 'บริหารจัดการโครงการ'], [2, P.ApS.th, 'ผู้จัดการโครงการ', 'บริหารจัดการโครงการ'],
[3, P.NoC.th, 'นักวิเคราะห์ระบบ', 'จัดทำและควบคุมความต้องการซอฟต์แวร์'], [3, P.NoC.th, 'นักวิเคราะห์ระบบ', 'จัดทำและควบคุมความต้องการซอฟต์แวร์'],
[4, P.ThS.th, 'นักพัฒนาระบบ', 'พัฒนาระบบตามความต้องการ'], [4, P.ThS.th, 'นักพัฒนาระบบ', 'พัฒนาระบบตามความต้องการ'],
@@ -81,7 +81,7 @@ export function softwareDesign() {
figure('useCase'), figure('useCase'),
table(['Actor', 'คำอธิบาย', 'การใช้งานหลัก'], [ table(['Actor', 'คำอธิบาย', 'การใช้งานหลัก'], [
['Owner', 'เจ้าของบริษัท สิทธิ์สูงสุดภายในบริษัท', 'ตั้งค่าบริษัท จัดการผู้ใช้ และใช้งานทุกโมดูล'], ['Owner', 'เจ้าของบริษัท สิทธิ์สูงสุดภายในบริษัท', 'ตั้งค่าบริษัท จัดการผู้ใช้ และใช้งานทุกโมดูล'],
['Admin', 'ผู้ดูแลระบบภายในบริษัท', 'จัดการข้อมูลหลัก ผู้ใช้ สิทธิ์ และใช้งานทุกโมดูลปฏิบัติการ'], ['Admin', 'ผู้ดูแลระบบภายในบริษัท', 'จัดการข้อมูลหลักและใช้งานทุกโมดูลปฏิบัติการ (การจัดการผู้ใช้และสิทธิ์เป็นของ Owner)'],
['Staff', 'ผู้ปฏิบัติงาน', 'บันทึกรายการคลังสินค้า ขาย จัดซื้อ และการเงินตามสิทธิ์'], ['Staff', 'ผู้ปฏิบัติงาน', 'บันทึกรายการคลังสินค้า ขาย จัดซื้อ และการเงินตามสิทธิ์'],
['Viewer', 'ผู้ใช้แบบอ่านอย่างเดียว', 'ดูหน้าจอและรายงานที่ได้รับสิทธิ์'], ['Viewer', 'ผู้ใช้แบบอ่านอย่างเดียว', 'ดูหน้าจอและรายงานที่ได้รับสิทธิ์'],
['System Scheduler', 'ผู้กระทำอัตโนมัติ (Node.js)', 'สรุปยอดสต๊อก/GL และแจ้งเตือนตามกำหนดเวลา'], ['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() { export function traceabilityRecord() {
const date = '2026-03-06'; const date = '2026-07-31';
const rows = []; const rows = [];
let group = ''; let group = '';
for (const [id, text] of CRS) { for (const [id, text] of CRS) {
const g = id.slice(0, 4); const g = id.slice(0, 4);
if (g !== group) { group = g; const name = SCOPEGROUP(g); rows.push([`${g}: ${name}`, '', '', '', '', '', '']); } if (g !== group) { group = g; const name = SCOPEGROUP(g); rows.push([`${g}: ${name}`, '', '', '', '', '', '']); }
const t = traceOf(id); 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 traced = CRS.map(([id]) => traceOf(id));
const withUnits = traced.filter((t) => t.un.length).length; const withUnits = traced.filter((t) => t.un.length).length;
const byDocument = 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 { return {
dir: '2-SI Process (12 Work Product)/13.Traceability record', dir: '2-SI Process (12 Work Product)/13.Traceability record',
name: `${CODE} Traceability Record ${bud(date)} V1.0 NoC`, name: `${CODE} Traceability Record ${bud(date)} V1.0 NoC`,
@@ -182,20 +183,21 @@ export function traceabilityRecord() {
]), ]),
body: [ body: [
'## วัตถุประสงค์ (Objective)', '## วัตถุประสงค์ (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', '## Traceability Matrix',
table(['CR ID', 'CR Topic', 'SRS ID', 'SRS Topic', 'Unit ID', 'Unit Topic', 'หลักฐานอื่น (Document)'], rows), table(['CR ID', 'CR Topic', 'SRS ID', 'SRS Topic', 'Unit ID', 'Unit Topic', 'Test Case ID'], rows),
'หมายเหตุ: ความต้องการที่เป็นเอกสาร การอบรม หรือหลักฐานด้านคุณภาพ สอบกลับไปยังเอกสารส่งมอบที่เป็นหลักฐานในคอลัมน์ "หลักฐานอื่น" แทน Software Unit', 'หมายเหตุ: ความต้องการที่เป็นเอกสาร การอบรม หรือหลักฐานด้านคุณภาพไม่มี Software Unit และ Test Case จึงยืนยันด้วยเอกสารส่งมอบตามแผน ซึ่งสรุปไว้ใน Traceability Record Table ฉบับส่งมอบ',
'## สรุปความครอบคลุม', '## สรุปความครอบคลุม',
table(['รายการ', 'จำนวน'], [ table(['รายการ', 'จำนวน'], [
['ความต้องการของลูกค้าทั้งหมด (CR01–CR14)', CRS.length], ['ความต้องการของลูกค้าทั้งหมด (CR01–CR14)', CRS.length],
['เชื่อมโยงกับความต้องการซอฟต์แวร์ (SRS)', CRS.length], ['เชื่อมโยงกับความต้องการซอฟต์แวร์ (SRS)', CRS.length],
['เชื่อมโยงกับ Software Unit', withUnits], ['เชื่อมโยงกับ Software Unit', withUnits],
['ยืนยันด้วยเอกสารส่งมอบ (ไม่มี Software Unit)', byDocument], ['เชื่อมโยงกับ Test Case', withTests],
['ยืนยันด้วยเอกสารส่งมอบตามแผน (ไม่มี Software Unit)', byDocument],
['ความต้องการซอฟต์แวร์ทั้งหมด (SR01–SR09)', SRS.length], ['ความต้องการซอฟต์แวร์ทั้งหมด (SR01–SR09)', SRS.length],
['Software Unit ทั้งหมด', UNITS.length], ['Software Unit ทั้งหมด', UNITS.length],
], ['', 'r']), ], ['', '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'), ].join('\n\n'),
secretary: P.NoC, reviewer: P.YaB, approver: P.SeV, secretary: P.NoC, reviewer: P.YaB, approver: P.SeV,
}), }),
@@ -297,10 +299,14 @@ export function testReport() {
const date = '2026-08-14'; const date = '2026-08-14';
const tested = CRS.filter(([id]) => traceOf(id).tcs.length); const tested = CRS.filter(([id]) => traceOf(id).tcs.length);
const byDocument = CRS.length - tested.length; const byDocument = CRS.length - tested.length;
const rows = tested.map(([id, text], i) => { const count = resultCounts();
const t = traceOf(id); const crsOf = (tc) => CRS.filter(([id]) => traceOf(id).tcs.includes(tc)).map(([id]) => id).join(', ') || '-';
return [i + 1, t.tc, `ตรวจสอบ ${t.un.map(unitTopic).join(' / ')}`, id, text, 'Passed', thRangeLong(TEST_FROM, TEST_TO)]; 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 { return {
dir: '2-SI Process (12 Work Product)/16.Test Report', dir: '2-SI Process (12 Work Product)/16.Test Report',
name: `${CODE} Test Report ${bud(date)} V1.0 PaNg`, name: `${CODE} Test Report ${bud(date)} V1.0 PaNg`,
@@ -322,27 +328,28 @@ export function testReport() {
[2, 'Non-Functional Test', 'ประสิทธิภาพ, ความปลอดภัย (สิทธิ์/TLS/การเข้ารหัสรหัสผ่าน), การแยกข้อมูล, การสำรองและกู้คืน'], [2, 'Non-Functional Test', 'ประสิทธิภาพ, ความปลอดภัย (สิทธิ์/TLS/การเข้ารหัสรหัสผ่าน), การแยกข้อมูล, การสำรองและกู้คืน'],
[3, 'Environment', 'Internal Testing Server (PHP 8, MariaDB, Node.js)'], [3, 'Environment', 'Internal Testing Server (PHP 8, MariaDB, Node.js)'],
[4, 'ช่วงเวลา', `รอบทดสอบระบบ ${thRangeLong(TEST_FROM, TEST_TO)}`], [4, 'ช่วงเวลา', `รอบทดสอบระบบ ${thRangeLong(TEST_FROM, TEST_TO)}`],
[5, 'Baseline ที่ใช้ทดสอบ', `สถานะ Repository ที่ใกล้เคียงที่สุด Commit \`${TEST_STATE_COMMIT}\``], [5, 'Build ที่ใช้ทดสอบ', `Commit \`${TEST_STATE_COMMIT}\` บน Branch main (การแก้ไขโค้ดล่าสุดก่อนรอบทดสอบ)`],
], ['c']), ], ['c']),
'## สรุปผลการทดสอบ', '## สรุปผลการทดสอบ',
table(['รายการ', 'จำนวน'], [ table(['รายการ', 'จำนวน'], [
['Test Case ที่กำหนดทั้งหมด', TEST_CASES.length], ['Test Case ที่กำหนดทั้งหมด', TEST_CASES.length],
['ดำเนินการทดสอบ', TEST_CASES.length], ['ดำเนินการทดสอบ', count.Passed + count.Failed],
['ผ่าน (Passed)', TEST_CASES.length], ['ผ่าน (Passed)', count.Passed],
['ไม่ผ่าน (Failed)', 0], ['ไม่ผ่าน (Failed)', count.Failed],
['ค้าง (Blocked / Pending)', 0], ['ยังไม่ได้ทดสอบ (ฟังก์ชันส่งมอบหลังรอบทดสอบ)', count['Not tested']],
], ['', 'r']), ], ['', 'r']),
'## Verification Items', '## 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`, `ความต้องการที่ยืนยันด้วย Test Case มี ${tested.length} รายการ ส่วนอีก ${byDocument} รายการเป็นความต้องการด้านเอกสาร การอบรม และหลักฐานด้านคุณภาพ ซึ่งยืนยันด้วยเอกสารส่งมอบตามที่ระบุใน Traceability Record และได้รับการยืนยันจากลูกค้าใน Validation Results`,
'## Summary', '## Summary',
table(['No', 'Topic', 'Detail'], [ table(['No', 'Topic', 'Detail'], [
[1, 'Test Case', `ทดสอบตาม Customer Requirements ผ่านทั้งหมด ${TEST_CASES.length} Test Case คิดเป็น 100%`], [1, 'Test Case', `ทดสอบ ${count.Passed + count.Failed} จาก ${TEST_CASES.length} Test Case ผ่าน ${count.Passed} ไม่ผ่าน ${count.Failed}`],
[2, 'Pending', 'ไม่พบข้อผิดพลาด (Fail) หรือรายการค้าง (Pending) ในรอบการทดสอบนี้'], [2, 'Failed', `${failed.map(([tc]) => tc).join(', ')} บันทึกเป็นข้อบกพร่องคงค้าง ${issues.join(', ')} ใน Correction Register`],
[3, 'Conclusion', 'ผลการทดสอบสรุปได้ว่าระบบพร้อมสำหรับ UAT และการส่งมอบ เนื่องจากผลการทดสอบผ่านครบ 100% และเป็นไปตาม Project Plan'], [3, 'Not tested', `${TEST_CASES.filter(([tc]) => testResult(tc).status === 'Not tested').map(([tc]) => tc).join(', ')} ฟังก์ชันส่งมอบหลังรอบทดสอบ (Task 4.7) ต้องทดสอบก่อนใช้งาน`],
[4, 'Conclusion', 'ฟังก์ชันหลักผ่านการทดสอบ แต่ยังมีข้อบกพร่องด้านความปลอดภัยและความถูกต้องของข้อมูลคงค้าง ต้องแจ้งผู้รับมอบและกำหนดการแก้ไขก่อนตรวจรับ'],
], ['c']), ], ['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'), ].join('\n\n'),
secretary: P.PaNg, reviewer: P.NoC, approver: P.SeV, secretary: P.PaNg, reviewer: P.NoC, approver: P.SeV,
}), }),
@@ -380,9 +387,9 @@ export function software() {
table(['ลำดับ', 'หลักฐาน', 'รายละเอียด'], [ table(['ลำดับ', 'หลักฐาน', 'รายละเอียด'], [
[1, 'Source Code และ Baseline', `Commit \`${BASELINE_COMMIT}\` ควบคุมใน Repository หลักและสำรอง`], [1, 'Source Code และ Baseline', `Commit \`${BASELINE_COMMIT}\` ควบคุมใน Repository หลักและสำรอง`],
[2, 'ชุดติดตั้งระบบ', 'Docker Compose Stack และสคริปต์ `setup.php` สำหรับสร้างฐานข้อมูล'], [2, 'ชุดติดตั้งระบบ', 'Docker Compose Stack และสคริปต์ `setup.php` สำหรับสร้างฐานข้อมูล'],
[3, 'ผลการทดสอบระบบ', `Test Report ${TEST_CASES.length} Test Case ผ่านทั้งหมด`], [3, 'ผลการทดสอบระบบ', (({ Passed: ok, Failed: bad, 'Not tested': none }) => `Test Report ${TEST_CASES.length} Test Case ผ่าน ${ok} ไม่ผ่าน ${bad} ยังไม่ได้ทดสอบ ${none}`)(resultCounts())],
[4, 'ผลการทดสอบการยอมรับ', `Validation Results ครบ 14 หมวด ${CRS.length} รายการ ผ่านทั้งหมด`], [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, 'การตรวจรับส่งมอบ', `Acceptance Report ผลการตรวจรับ Accepted เมื่อ ${thLong(ACCEPT_DATE)}`], [5, 'การตรวจรับส่งมอบ', `เสนอ Project Sponsor ตรวจรับตามแผนในวันที่ ${thLong(ACCEPT_DATE)} (Acceptance Report)`],
[6, 'คู่มือประกอบการใช้งาน', 'Software User Document, Product Operation Guide, Maintenance Document'], [6, 'คู่มือประกอบการใช้งาน', 'Software User Document, Product Operation Guide, Maintenance Document'],
], ['c']), ], ['c']),
'## รายการอ้างอิงเพิ่มเติม', '## รายการอ้างอิงเพิ่มเติม',
+28 -17
View File
@@ -1,6 +1,6 @@
// SI work products 18–22: user documentation, operation guide, maintenance, verification, validation. // 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 { 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 { CORRECTIONS, VERIFICATION_ROUNDS, VERIFICATION_RULES } from './history.mjs';
import { bud, thLong, thRangeLong, table, bullets, numbered } from './lib.mjs'; import { bud, thLong, thRangeLong, table, bullets, numbered } from './lib.mjs';
import { workProduct, head } from './format.mjs'; import { workProduct, head } from './format.mjs';
@@ -311,9 +311,9 @@ export function verificationResults() {
['ลงชื่อผู้จัดทำและผู้อนุมัติเรียบร้อย', 'มีตารางลงนามครบถ้วน'], ['ลงชื่อผู้จัดทำและผู้อนุมัติเรียบร้อย', 'มีตารางลงนามครบถ้วน'],
], 'YaB'], ], 'YaB'],
'WP 9.0': ['ระบบที่ผ่านการทดสอบพร้อมใช้งานจริง', [ '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 หลักและสำรอง'], ['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'], ], 'ThS'],
'WP 10.0': ['Product Operation Guide', [ 'WP 10.0': ['Product Operation Guide', [
['รายละเอียดและความถูกต้องของ Header ของเอกสาร', 'มีรายละเอียดครบถ้วนถูกต้อง'], ['รายละเอียดและความถูกต้องของ Header ของเอกสาร', 'มีรายละเอียดครบถ้วนถูกต้อง'],
@@ -331,12 +331,16 @@ export function verificationResults() {
return ROUNDS.map((r) => { return ROUNDS.map((r) => {
const rows = []; const rows = [];
let n = 0; let n = 0;
let failed = 0;
for (const wp of r.wps) { for (const wp of r.wps) {
const [wpName, checks, owner] = CHECKS[wp]; const [wpName, checks, owner] = CHECKS[wp];
rows.push([`${wp} ${wpName}`, '', '', '', '', '']); rows.push([`${wp} ${wpName}`, '', '', '', '', '']);
checks.forEach((c, i) => { checks.forEach((c, i) => {
n += 1; 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 { return {
@@ -363,12 +367,12 @@ export function verificationResults() {
'## สรุปผลการตรวจสอบรอบนี้', '## สรุปผลการตรวจสอบรอบนี้',
table(['รายการ', 'จำนวน'], [ table(['รายการ', 'จำนวน'], [
['รายการตรวจสอบทั้งหมด', n], ['รายการตรวจสอบทั้งหมด', n],
['ผลผ่าน (Passed)', n], ['ผลผ่าน (Passed)', n - failed],
['ผลไม่ผ่าน (Failed)', 0], ['ผลไม่ผ่าน (Failed)', failed],
['ประเด็นคงค้าง', 0], ['ประเด็นคงค้าง', failed],
], ['', 'r']), ], ['', 'r']),
r.no === 4 r.no === 4
? `รอบตรวจสอบนี้เป็นรอบสุดท้ายก่อนส่งมอบ ครอบคลุมสิ่งส่งมอบทั้งหมด ${r.wps.length} รายการ ผลการตรวจสอบผ่านทุกรายการ จึงเห็นควรให้ดำเนินการตรวจรับส่งมอบระบบตามที่บันทึกใน Acceptance Report` ? `รอบตรวจสอบนี้เป็นรอบสุดท้ายก่อนส่งมอบ ครอบคลุมสิ่งส่งมอบทั้งหมด ${r.wps.length} รายการ ผ่าน ${n - failed} รายการ ไม่ผ่าน ${failed} รายการ คือผลการทดสอบระบบที่ยังมีข้อบกพร่องคงค้าง และชุดติดตั้ง Docker ที่ยังไม่ได้ทดสอบ จึงเสนอให้ผู้รับมอบพิจารณาการตรวจรับพร้อมแผนแก้ไขข้อบกพร่องคงค้าง`
: `ผลการตรวจสอบรอบนี้ผ่านทุกรายการ ประเด็นที่พบระหว่างการตรวจสอบได้รับการแก้ไขและบันทึกใน Correction Register แล้ว การตรวจสอบรอบถัดไปจะครอบคลุมสิ่งส่งมอบที่จัดทำเพิ่มเติมในช่วงถัดไป`, : `ผลการตรวจสอบรอบนี้ผ่านทุกรายการ ประเด็นที่พบระหว่างการตรวจสอบได้รับการแก้ไขและบันทึกใน Correction Register แล้ว การตรวจสอบรอบถัดไปจะครอบคลุมสิ่งส่งมอบที่จัดทำเพิ่มเติมในช่วงถัดไป`,
].join('\n\n'), ].join('\n\n'),
secretary: P.PaNg, reviewer: P.YaB, approver: P.SeV, secretary: P.PaNg, reviewer: P.YaB, approver: P.SeV,
@@ -406,15 +410,20 @@ export function validationResults() {
const evidence = (id) => { const evidence = (id) => {
const t = traceOf(id); const t = traceOf(id);
const ev = AT_UAT[id] ?? t.ev; 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 rows = [];
const summary = []; const summary = [];
for (const [gid, gname, items] of CR_GROUPS) { for (const [gid, gname, items] of CR_GROUPS) {
rows.push([gid, `**${gname}**`, '', '', '', '']); rows.push([gid, `**${gname}**`, '', '', '', '']);
for (const [id, text] of items) rows.push([id, text, METHOD[gid], evidence(id), 'Passed', P.SeV.id]); for (const [id, text] of items) rows.push([id, text, METHOD[gid], evidence(id), status(id), P.SeV.id]);
summary.push([`${gid} ${gname}`, items.length, items.length, 0]); 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.'); if (Object.keys(METHOD).length !== CR_GROUPS.length) throw new Error('Every CR group needs a validation method.');
return { return {
dir: '2-SI Process (12 Work Product)/22.Validation Result', dir: '2-SI Process (12 Work Product)/22.Validation Result',
@@ -438,21 +447,23 @@ export function validationResults() {
['ผู้บันทึกผล', `${P.PaNg.th} (QA/Tester)`], ['ผู้บันทึกผล', `${P.PaNg.th} (QA/Tester)`],
['ช่วงเวลาทดสอบ', thRangeLong(TEST_FROM, TEST_TO)], ['ช่วงเวลาทดสอบ', thRangeLong(TEST_FROM, TEST_TO)],
['ขอบเขตการทดสอบ', `Customer Requirements ครบ ${CR_GROUPS.length} หมวด ${CRS.length} รายการ (CR01–CR14)`], ['ขอบเขตการทดสอบ', `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', '## ตารางสรุป Validation Result ตาม Customer Requirements',
table(['CR ID', 'Customer Requirement', 'วิธีการยืนยัน', 'หลักฐานอ้างอิง', 'Status', 'Tester'], rows, ['', '', '', '', 'c', 'c']), 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(['รายการ', 'จำนวน'], [ table(['รายการ', 'จำนวน'], [
['หมวดความต้องการที่ทดสอบ', CR_GROUPS.length], ['หมวดความต้องการที่ทดสอบ', CR_GROUPS.length],
['ความต้องการที่ทดสอบทั้งหมด', CRS.length], ['ความต้องการที่ทดสอบทั้งหมด', CRS.length],
['ผ่าน (Passed)', CRS.length], ['ผ่าน (Passed)', all('Passed')],
['ไม่ผ่าน (Failed)', 0], ['ไม่ผ่าน (Failed)', all('Failed')],
['ค้างการทดสอบ', 0], ['ยังไม่ได้ทดสอบ (ฟังก์ชันส่งมอบหลังรอบทดสอบ)', all('Not tested')],
['รอดำเนินการตามแผน (Pending)', all('Pending')],
], ['', 'r']), ], ['', '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'), ].join('\n\n'),
secretary: P.PaNg, reviewer: P.NoC, approver: P.SeV, 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.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.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.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', 'ตั้งค่าบริษัท (Company Settings)', [
['UN02.001', 'Company – Profile & System Settings', 'แก้ไขข้อมูลบริษัท, โลโก้, ค่าตั้งค่าระบบต่อบริษัท', 'SR03:002', 'CompanyProfileManager.php, CompanySettingManager.php'], ['UN02.001', 'Company – Profile & System Settings', 'แก้ไขข้อมูลบริษัท, โลโก้, ค่าตั้งค่าระบบต่อบริษัท', 'SR03:002', 'CompanyProfileManager.php, CompanySettingManager.php'],
@@ -94,7 +94,7 @@ export const UNIT_GROUPS = [
]], ]],
['UN04', 'ควบคุมสินค้าคงคลัง (Inventory Control)', [ ['UN04', 'ควบคุมสินค้าคงคลัง (Inventory Control)', [
['UN04.001', 'ICS – Stock-in', 'รับสินค้าเข้าตามสินค้า จำนวน ตำแหน่ง เอกสารอ้างอิง สร้างความเคลื่อนไหวและยอดคงเหลือ', 'SR03:004, SR08:003', 'app/ics/, StockManager.php, StockTablesTrait.php'], ['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.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.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'], ['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', 'จัดซื้อ (Purchasing)', [
['UN06.001', 'Purchasing – Purchase Request', 'สร้างและอนุมัติใบขอซื้อ', 'SR03:006', 'app/po/, PurchaseRequestManager.php'], ['UN06.001', 'Purchasing – Purchase Request', 'สร้างและอนุมัติใบขอซื้อ', 'SR03:006', 'app/po/, PurchaseRequestManager.php'],
['UN06.002', 'Purchasing – Purchase Order', 'แปลงใบขอซื้อเป็นใบสั่งซื้อ และติดตามสถานะ', 'SR03:006, SR04:002', 'PurchaseOrderManager.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'], ['UN06.004', 'Purchasing – Supplier Return', 'คืนสินค้าผู้ขาย พร้อมปรับสต๊อก/บัญชี', 'SR03:006, SR04:002', 'SupplierReturnManager.php'],
]], ]],
['UN07', 'การเงิน (Finance)', [ ['UN07', 'การเงิน (Finance)', [
@@ -117,19 +117,19 @@ export const UNIT_GROUPS = [
['UN07.002', 'Finance – Payment Billing & Payment', 'วางบิลจ่ายและบันทึกใบสำคัญจ่ายผูกกับใบแจ้งหนี้ซื้อ', 'SR03:007, SR04:002', 'PaymentBillingManager.php, PaymentManager.php'], ['UN07.002', 'Finance – Payment Billing & Payment', 'วางบิลจ่ายและบันทึกใบสำคัญจ่ายผูกกับใบแจ้งหนี้ซื้อ', 'SR03:007, SR04:002', 'PaymentBillingManager.php, PaymentManager.php'],
]], ]],
['UN08', 'บัญชี (Accounting)', [ ['UN08', 'บัญชี (Accounting)', [
['UN08.001', 'Accounting – Chart of Accounts / Departments / Formulas', 'จัดการผังบัญชี แผนก และสูตรบัญชีอัตโนมัติ', 'SR03:008, SR08:002', 'app/accounting/, 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 แบบสมดุลภายใน Transaction', 'SR03:008, SR09:003', 'app/journal/, PostingManager.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', 'รายงานและแดชบอร์ด (Reports & Dashboard)', [
['UN09.001', 'Reports – Dashboard & Aggregates', 'แดชบอร์ดคลังสินค้าและบัญชีจากตารางสรุปยอด', 'SR03:009, SR05:001, SR05:002', 'app/dashboard/, app/ac_dashboard/, EtlStockManager.php'], ['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.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/'], ['UN09.004', 'Reports – Filter / Print / Export', 'กรอง ดู พิมพ์ และส่งออกรายงานตามขอบเขตสิทธิ์', 'SR03:009, SR04:003', 'app/reports/'],
]], ]],
['UN10', 'ควบคุมเอกสาร (Document Control)', [ ['UN10', 'ควบคุมเอกสาร (Document Control)', [
['UN10.001', 'Document – Numbering', 'ออกเลขที่เอกสารตามลำดับที่กำหนด (document_number_sequences)', 'SR03:010, SR04:005, SR08:004', 'DocumentNumberManager.php'], ['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.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', 'แจ้งเตือนและงานตามกำหนดเวลา (Notification & Scheduler)', [
['UN11.001', 'Node – Socket.IO Notification Server', 'รับเหตุการณ์จาก PHP ผ่าน Endpoint ที่ป้องกันด้วย Secret และส่งต่อไปยัง Browser ของผู้ใช้ที่เกี่ยวข้อง', 'SR03:011, SR04:004, SR06:004, SR06:005', 'nodejs/server.js'], ['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', 'ความปลอดภัยและการแยกข้อมูล (Security & Tenant Scope)', [
['UN12.001', 'Security – Tenant Scope Guard', 'จำกัดทุก Query และ Action ให้อยู่ในบริษัท/คลังที่ผู้ใช้ได้รับอนุญาต', 'SR04:001, SR04:003, SR07:002', 'StockTablesTrait.php, Manager classes'], ['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.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'], ['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', 'ติดตั้งและสำรองข้อมูล (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.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.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); 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.001', 'UN01.001', 'ลงทะเบียนเจ้าของบริษัทใหม่ → ยืนยันอีเมล → เชิญผู้ใช้ → ผู้ใช้กดลิงก์เชิญ', 'สร้างบริษัท/บัญชีเจ้าของสำเร็จ ผู้ใช้ที่ถูกเชิญเข้าบริษัทที่ถูกต้อง', ENV_WEB + ', SMTP ทดสอบ', 'ต้องส่งอีเมลได้จริง', '-'],
['TC-UN01.002', 'UN01.002', 'เข้าสู่ระบบด้วยบัญชี Owner/Admin/Staff/Viewer แล้วเรียกหน้าและ Action ที่ไม่ได้รับสิทธิ์', 'แต่ละบทบาทเข้าถึงได้เฉพาะหน้าจอ/คำสั่งที่อนุญาต คำสั่งที่ไม่ได้รับสิทธิ์ถูกปฏิเสธฝั่งเซิร์ฟเวอร์', ENV_WEB, 'ต้องมีบัญชีทดสอบครบ 4 บทบาท', 'TC-UN01.001'], ['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.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.001', 'UN02.001', 'แก้ไขข้อมูลบริษัทและค่าตั้งค่าระบบ', 'ค่าใหม่ถูกบันทึกและมีผลกับบริษัทนั้นเท่านั้น', ENV_WEB, '-', 'TC-UN01.002'],
['TC-UN02.002', 'UN02.002', 'ตั้งค่า SMTP แล้วส่งอีเมลทดสอบ/คำเชิญ', 'อีเมลถูกส่งผ่าน SMTP ของบริษัทและได้รับที่ปลายทาง', ENV_WEB + ', SMTP ทดสอบ', 'ต้องมีบัญชี SMTP ทดสอบ', 'TC-UN02.001'], ['TC-UN02.002', 'UN02.002', 'ตั้งค่า SMTP แล้วส่งอีเมลทดสอบ/คำเชิญ', 'อีเมลถูกส่งผ่าน SMTP ของบริษัทและได้รับที่ปลายทาง', ENV_WEB + ', SMTP ทดสอบ', 'ต้องมีบัญชี SMTP ทดสอบ', 'TC-UN02.001'],
['TC-UN03.001', 'UN03.001', 'เพิ่ม/แก้ไข/ปิดใช้ คลังสินค้า พื้นที่ และช่องจัดเก็บ; กรอกความจุไม่ถูกต้อง', 'รายการถูกต้องถูกบันทึก ข้อมูลผิดถูกปฏิเสธพร้อมข้อความ', ENV_WEB, '-', 'TC-UN01.002'], ['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.003', 'UN03.003', 'เพิ่มประเภทผู้ติดต่อ ลูกค้า และผู้ขาย', 'ผู้ติดต่อเลือกใช้ในเอกสารขาย/ซื้อได้', ENV_WEB, '-', 'TC-UN01.002'],
['TC-UN03.004', 'UN03.004', 'ตั้งบริษัท A เป็นคลังชั้นเดียว บริษัท B เป็นคลัง/พื้นที่/ช่อง แล้วรับสินค้าเข้า', 'ทั้งสองบริษัททำรายการได้ถูกต้องตามโครงสร้างของตน', ENV_WEB + ', 2 บริษัททดสอบ', '-', 'TC-UN03.001'], ['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.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.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.004', 'UN04.004', 'รับสินค้าเข้าพร้อม Lot, Serial Number, วันหมดอายุ แล้วเปิดรายงาน Lot/หมดอายุ', 'ข้อมูลติดตามคงอยู่และแสดงในรายงานที่เกี่ยวข้อง', ENV_WEB, '-', 'TC-UN04.001'],
['TC-UN04.005', 'UN04.005', 'พิมพ์ฉลาก SKU/ตำแหน่ง แล้วสแกน (หรือป้อนค่า) ในหน้าจอรับเข้า', 'ฉลากมีรหัสที่ใช้งานได้ ค่าที่สแกนถูกยอมรับ', ENV_WEB + ', เครื่องสแกนหรือจำลอง', 'เครื่องพิมพ์/สแกน (ถ้ามี)', 'TC-UN03.002'], ['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-UN07.002', 'UN07.002', 'วางบิลจ่ายและบันทึกใบสำคัญจ่ายกับใบแจ้งหนี้ซื้อ', 'ยอดค้างจ่ายลดลง เอกสารเชื่อมโยงกันและมีประวัติ', ENV_WEB, '-', 'TC-UN06.003'],
['TC-UN08.001', 'UN08.001', 'เพิ่มผังบัญชี แผนก และสูตรบัญชี', 'โครงสร้างบัญชีถูกบันทึกและใช้กับการผ่านรายการได้', ENV_WEB, '-', 'TC-UN01.004'], ['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-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.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-UN09.004', 'UN09.004', 'กรอง ดู พิมพ์ และส่งออกรายงาน', 'ผลลัพธ์ตรงกับขอบเขตและตัวกรองที่เลือก', ENV_WEB, '-', 'TC-UN09.002'],
['TC-UN10.001', 'UN10.001', 'สร้างเอกสารควบคุมหลายฉบับติดต่อกัน', 'เลขที่เอกสารเรียงตามลำดับที่ตั้งค่า ไม่ซ้ำ', ENV_WEB, '-', 'TC-UN05.001'], ['TC-UN10.001', 'UN10.001', 'สร้างเอกสารควบคุมหลายฉบับติดต่อกัน', 'เลขที่เอกสารเรียงตามลำดับที่ตั้งค่า ไม่ซ้ำ', ENV_WEB, '-', 'TC-UN05.001'],
['TC-UN10.002', 'UN10.002', 'พยายามเปลี่ยนสถานะเอกสารข้ามขั้น; ลบเอกสารควบคุม', 'การเปลี่ยนสถานะที่ไม่ถูกต้องถูกปฏิเสธพร้อมข้อความ การลบต้องยืนยันก่อน', ENV_WEB, '-', 'TC-UN10.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.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.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.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-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.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.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] ?? ''; 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'] }, 'CR05:005': { sr: ['SR03:004'], un: ['UN04.005'] },
'CR06:001': { sr: ['SR07:001'], un: ['UN12.004'] }, 'CR06:001': { sr: ['SR07:001'], un: ['UN12.004'] },
'CR06:002': { sr: ['SR01:005'], un: ['UN13.003', '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:004': { sr: ['SR07:005'], un: ['UN01.002'] },
'CR06:005': { sr: ['SR07:004'], un: ['UN12.002'] }, 'CR06:005': { sr: ['SR07:004'], un: ['UN12.002'] },
'CR06:006': { sr: ['SR07:003'], un: ['UN01.003'] }, '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'], ['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.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.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); export const taskById = (id) => TASKS.find((t) => t[0] === id);
/** Correction Register: [id, detected, corrected, severity, problem, cause, action, ownerId, commit, testCase] */ /** Correction Register: [id, detected, corrected, severity, problem, cause, action, ownerId, commit, testCase] */
export const CORRECTIONS = [ export const CORRECTIONS = [
['ISS-001', '2026-03-03', '2026-03-09', 'สูง', 'ผลตรวจความปลอดภัยเบื้องต้นพบจุดที่ต้องแก้ไข', 'ยังไม่ได้ใช้แนวปฏิบัติ Secure Coding ครบทุกจุด', 'แก้ไขตามผลตรวจความปลอดภัยทั้งชุด', 'ThS', '7cb78d0', 'TC-UN12.002'], ['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-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-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'], ['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-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-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-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-010', '2026-05-14', '2026-05-20', 'กลาง', 'รูปแบบการเขียนโค้ดในแต่ละโมดูลไม่สอดคล้องกัน', 'พัฒนาหลายโมดูลคู่ขนานกัน', 'ทบทวนและปรับรูปแบบการเขียนโค้ดให้สอดคล้องกัน', 'ThS', '91f8bb8', 'ทดสอบถดถอยทั้งชุด'],
['ISS-011', '2026-05-15', '2026-05-21', 'สูง', 'การเชิญผู้ใช้ สิทธิ์แอปพลิเคชัน และโควตารายการยังไม่รัดกุม', 'ควบคุมสิทธิ์และโควตาถูกเพิ่มภายหลัง', 'แก้ไขการเชิญผู้ใช้ เพิ่มการควบคุมสิทธิ์และโควตารายการ', 'ThS', '6eeebfe', 'TC-UN12.003'], ['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-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-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-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-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-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-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-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-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-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-022', '2026-05-21', '2026-05-27', 'กลาง', 'เอกสารประกอบภายในโครงการยังไม่ครบถ้วน', 'เอกสารถูกปรับตามการพัฒนาอย่างต่อเนื่อง', 'ทบทวนเอกสารทั้งหมดและแก้ไขส่วนที่ขาด', 'ThS', '99ae35d', 'ทบทวนเอกสาร'],
['ISS-023', '2026-05-22', '2026-05-28', 'กลาง', 'การเปลี่ยนคำว่า Rack เป็น Bin ไม่ครบทุกจุด และ ReportManager ผิดพลาด', 'เปลี่ยนชื่อไม่ครบทุกไฟล์และป้ายกำกับหน้าจอ', 'เปลี่ยนชื่อไฟล์ ป้ายกำกับ และแก้คุณสมบัติของ ReportManager', 'ThS', '5df6736', 'TC-UN03.001'], ['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-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-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-027', '2026-05-23', '2026-05-29', 'ต่ำ', 'เมธอดของคลาสมีการทำงานซ้ำซ้อน', 'พัฒนาเพิ่มทีละส่วนทำให้เกิดความซ้ำซ้อน', 'ลดความซ้ำซ้อนของเมธอดในคลาส', 'ThS', 'a0677d6', 'ทดสอบถดถอยทั้งชุด'],
['ISS-028', '2026-07-28', '2026-08-03', 'สูง', 'การเข้าสู่ระบบและค่าตั้งค่าสภาพแวดล้อมผิดพลาดหลัง Baseline', 'ค่าตั้งค่าของสภาพแวดล้อมใหม่ไม่ตรงกับที่ระบบคาดหวัง', 'แก้ไขการเข้าสู่ระบบและค่าตั้งค่าสภาพแวดล้อม', 'ThS', 'b2c4374', 'TC-UN12.004'], ['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]. */ /** Adjustments reviewed against CHANGE_CRITERIA: [date, topic, requesterId, assessment, handledAs, reference]. */
export const CHANGE_REVIEW = [ export const CHANGE_REVIEW = [
['2026-05-23', 'ปรับคำเรียกตำแหน่งจัดเก็บจาก Rack เป็น Bin ทั้งระบบ', 'NoC', ['2026-05-18', 'ปรับคำเรียกตำแหน่งจัดเก็บจาก Rack เป็น Bin ทั้งระบบ', 'NoC',
'เป็นการปรับคำเรียกบนหน้าจอให้ตรงกับการปฏิบัติงานจริง ไม่เพิ่มหรือลดฟังก์ชัน ไม่กระทบ Man-day และส่งมอบได้แม้ไม่ปรับ', 'เป็นการปรับคำเรียกบนหน้าจอให้ตรงกับการปฏิบัติงานจริง ไม่เพิ่มหรือลดฟังก์ชัน ไม่กระทบ Man-day และส่งมอบได้แม้ไม่ปรับ',
'มติที่ประชุม ดำเนินการภายใน Task 3.9', 'Meeting Record 23 พ.ค. 69, Correction Register ISS-023'], 'มติที่ประชุม ดำเนินการภายใน Task 3.9', 'Meeting Record 18 พ.ค. 69, Correction Register ISS-023'],
['2026-08-03', 'เตรียมข้อมูลสาธิตสำหรับการตรวจรับและการสาธิตระบบ', 'ApS', ['2026-08-10', 'เตรียมข้อมูลสาธิตสำหรับการตรวจรับและการสาธิตระบบ', 'ApS',
'เป็นข้อมูลตั้งต้นสำหรับตรวจรับ ไม่ใช่สิ่งส่งมอบ และต้องแยกจากข้อมูลใช้งานจริงตาม CR07:004', 'เป็นข้อมูลตั้งต้นสำหรับตรวจรับ ไม่ใช่สิ่งส่งมอบ และต้องแยกจากข้อมูลใช้งานจริงตาม CR07:004',
'งานเตรียมการส่งมอบ Task 4.7', 'Meeting Record 3 ส.ค. 69, 14 ส.ค. 69'], 'งานเตรียมการส่งมอบ Task 4.7', 'Meeting Record 10 ส.ค. 69, 19 ส.ค. 69'],
['2026-08-14', 'ปรับอัตลักษณ์องค์กรและจัดทำชุดติดตั้ง Docker Compose', 'SeV', ['2026-08-10', 'ปรับอัตลักษณ์องค์กรและจัดทำชุดติดตั้ง Docker Compose', 'SeV',
'ชุดติดตั้ง Docker Compose อยู่ในความต้องการ CR10:002 ตั้งแต่ Baseline และการปรับอัตลักษณ์องค์กรไม่เปลี่ยนแปลงฟังก์ชันหรือสิ่งส่งมอบ', 'ชุดติดตั้ง 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.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.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: '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). */ /** Rules every verification round follows (plan section and each Verification Results record). */
export const VERIFICATION_RULES = [ export const VERIFICATION_RULES = [
@@ -112,7 +116,7 @@ export const VERIFICATION_RULES = [
* [theme, issue ids, what happened, preventive action for the next project] * [theme, issue ids, what happened, preventive action for the next project]
*/ */
export const LESSONS = [ 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 ทีละส่วนระหว่างพัฒนา', 'ข้อบกพร่องกลุ่มใหญ่ที่สุดเกิดจากการเพิ่ม Role Guard การตรวจสิทธิ์ และการจำกัด Session ทีละส่วนระหว่างพัฒนา',
'กำหนด Secure Coding Checklist และรูปแบบการตรวจสิทธิ์กลางตั้งแต่ช่วงออกแบบ และทบทวนโค้ดด้านความปลอดภัยทุกโมดูลก่อนรวมเข้า Baseline'], 'กำหนด Secure Coding Checklist และรูปแบบการตรวจสิทธิ์กลางตั้งแต่ช่วงออกแบบ และทบทวนโค้ดด้านความปลอดภัยทุกโมดูลก่อนรวมเข้า Baseline'],
['ขั้นตอน Onboarding และการลงทะเบียน', ['ISS-007', 'ISS-008', 'ISS-013'], ['ขั้นตอน Onboarding และการลงทะเบียน', ['ISS-007', 'ISS-008', 'ISS-013'],
@@ -121,7 +125,7 @@ export const LESSONS = [
['ความสอดคล้องระหว่างข้อกำหนดกับการพัฒนา', ['ISS-019', 'ISS-020', 'ISS-022'], ['ความสอดคล้องระหว่างข้อกำหนดกับการพัฒนา', ['ISS-019', 'ISS-020', 'ISS-022'],
'ข้อกำหนดและเอกสารถูกปรับระหว่างพัฒนา ทำให้เกิดช่องว่างกับโค้ด', 'ข้อกำหนดและเอกสารถูกปรับระหว่างพัฒนา ทำให้เกิดช่องว่างกับโค้ด',
'ปรับข้อกำหนดและ Traceability Record ในรอบเดียวกับการแก้โค้ด และตรวจความสอดคล้องในทุกรอบ Verification'], 'ปรับข้อกำหนดและ Traceability Record ในรอบเดียวกับการแก้โค้ด และตรวจความสอดคล้องในทุกรอบ Verification'],
['ความถูกต้องของข้อมูลสต๊อกและบัญชี', ['ISS-002', 'ISS-012'], ['ความถูกต้องของข้อมูลสต๊อกและบัญชี', ['ISS-002', 'ISS-012', 'ISS-030'],
'การแปลงชนิดข้อมูลและการตรวจสอบความสัมพันธ์ของข้อมูลไม่ครอบคลุม', 'การแปลงชนิดข้อมูลและการตรวจสอบความสัมพันธ์ของข้อมูลไม่ครอบคลุม',
'ตรวจสอบข้อมูลนำเข้าและความสัมพันธ์ฝั่งเซิร์ฟเวอร์ทุกคำสั่งที่เปลี่ยนสต๊อกหรือ GL และเพิ่มชุดทดสอบวงจรเอกสาร'], 'ตรวจสอบข้อมูลนำเข้าและความสัมพันธ์ฝั่งเซิร์ฟเวอร์ทุกคำสั่งที่เปลี่ยนสต๊อกหรือ GL และเพิ่มชุดทดสอบวงจรเอกสาร'],
['มาตรฐานโค้ดและการตั้งค่าสภาพแวดล้อม', ['ISS-005', 'ISS-010', 'ISS-014', 'ISS-021', 'ISS-023', 'ISS-025', 'ISS-027', 'ISS-028'], ['มาตรฐานโค้ดและการตั้งค่าสภาพแวดล้อม', ['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}] * [date, from, to, weekNo, taskRows[[taskId, status, pct, remark]], issues[ISS ids], changes[CH ids], next{date,topic}]
*/ */
export const PERIODS = [ 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-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-06', 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-13', topic: 'ทบทวน Software Project Plan, Work Schedule และแผนความเสี่ยง' }], ['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-02-25', topic: 'ติดตามการจัดทำ SRS, เอกสารออกแบบระบบ และการเริ่มพัฒนา' }], ['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-17', 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-04-29', 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-08', 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-13', 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-23', 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-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-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-08-03', 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-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-14', topic: 'ติดตามผลการทดสอบระบบ UAT และข้อมูลสาธิต' }], ['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 และ 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-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 ครบทุกรายการ ผลผ่านทั้งหมด'], ['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-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], ['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'], ['กำหนดรอบการประชุมและการรายงานความก้าวหน้า', 'ตั้งรอบประชุม ผู้รับผิดชอบ และเอกสารอ้างอิง']], 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'], 'ดำเนินการแล้ว']], 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']], 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 น.', 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', 'ยืนยันบทบาทและระดับการมีส่วนร่วม'], ['แผนการเก็บความต้องการลูกค้า', 'กำหนดผู้ให้ข้อมูลและกรอบเวลา']], agenda: [['พิจารณาอนุมัติกฎบัตรโครงการ (Project Charter)', 'ยืนยันวัตถุประสงค์ ขอบเขต และงบประมาณ'], ['ทบทวน Stakeholder Register', 'ยืนยันบทบาทและระดับการมีส่วนร่วม'], ['แผนการเก็บความต้องการลูกค้า', 'กำหนดผู้ให้ข้อมูลและกรอบเวลา']],
discussion: [['อนุมัติกฎบัตรโครงการ', 'Project Sponsor อนุมัติวัตถุประสงค์ ขอบเขต 11 ระบบงาน และเกณฑ์ความสำเร็จ', ['SeV'], 'อนุมัติ'], ['ผู้มีส่วนได้ส่วนเสีย', 'ยืนยันผู้รับผิดชอบตาม RACI ครบทุกบทบาท', ['ApS', 'YaB'], 'ยืนยันแล้ว'], ['การเก็บความต้องการ', 'มอบหมาย System Analyst สัมภาษณ์ฝ่ายคลังสินค้า ขาย จัดซื้อ และบัญชี', ['NoC'], 'ดำเนินการ']], 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']], 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 น.', 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']], 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'], 'บังคับใช้ทันที']], 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']], 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 น.', date: '2026-03-23', phase: 'Project Execution', taskId: '3.2', taskName: 'Security & Database Foundation', time: '09:00 – 11:00 น.',
attachment: 'Progress Status Record 17 มี.ค. 69', attachment: '-',
progress: [['Phase 3: Project Execution', 'กำลังดำเนินการ', 25, 'พัฒนาฐานข้อมูลสต๊อกและโมดูล ICS เบื้องต้น'], ['รวมความคืบหน้าทั้งโครงการ', 'ตามแผน', 30, 'การพัฒนาเป็นไปตามแผน']], progress: [['Phase 3: Project Execution', 'กำลังดำเนินการ', 25, 'พัฒนาฐานข้อมูลสต๊อกและโมดูล ICS เบื้องต้น'], ['รวมความคืบหน้าทั้งโครงการ', 'ตามแผน', 30, 'การพัฒนาเป็นไปตามแผน']],
agenda: [['ผลการแก้ไขตามผลตรวจความปลอดภัย', 'ยืนยันการปิดข้อบกพร่อง ISS-001'], ['การออกแบบฐานข้อมูลสต๊อกและโมดูล ICS', 'ทบทวนโครงสร้างตารางและความสัมพันธ์'], ['แผนพัฒนาโมดูลคลังสินค้าและรายงาน', 'ยืนยันลำดับงานช่วงเดือนเมษายน']], agenda: [['ผลการแก้ไขตามผลตรวจความปลอดภัย', 'ยืนยันการปิดข้อบกพร่อง ISS-001'], ['การออกแบบฐานข้อมูลสต๊อกและโมดูล ICS', 'ทบทวนโครงสร้างตารางและความสัมพันธ์'], ['แผนพัฒนาโมดูลคลังสินค้าและรายงาน', 'ยืนยันลำดับงานช่วงเดือนเมษายน']],
discussion: [['ความปลอดภัยของระบบ', 'ถอดไฟล์ตั้งค่าออกจาก Git และแก้ไขตามผลตรวจความปลอดภัยครบถ้วน', ['ThS'], 'เสร็จสิ้น'], ['ฐานข้อมูลสต๊อก', 'ยืนยันโครงสร้างตาราง td_stock และตารางข้อมูลหลักที่เกี่ยวข้อง', ['NoC', 'ThS'], 'ยืนยันแล้ว'], ['แผนงานถัดไป', 'พัฒนาแดชบอร์ดสต๊อก สินค้า ความจุคลัง และรายงาน', ['ThS'], 'ดำเนินการ']], 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']], actions: [['บันทึกการแก้ไข ISS-001 ใน Correction Register', ['YaB'], '2026-03-25', 'Scheduled'], ['พัฒนาโมดูลคลังสินค้าและรายงาน (Task 3.3)', ['ThS'], '2026-04-29', 'Scheduled']],
next: { date: '2026-04-29', topic: 'ติดตามการพัฒนาโมดูลคลังสินค้า สินค้า และรายงาน' }, 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 น.', date: '2026-05-04', phase: 'Project Execution', taskId: '3.3', taskName: 'Inventory & Warehouse Modules', time: '09:00 – 11:00 น.',
attachment: 'Progress Status Record 29 เม.ย. 69', attachment: '-',
progress: [['Phase 3: Project Execution', 'กำลังดำเนินการ', 50, 'พัฒนาโมดูลคลังสินค้า สินค้า Lot/Serial/Expiry และรายงานเสร็จ'], ['รวมความคืบหน้าทั้งโครงการ', 'ตามแผน', 45, 'เข้าสู่การพัฒนาระบบสิทธิ์และเอกสารธุรกิจ']], progress: [['Phase 3: Project Execution', 'กำลังดำเนินการ', 50, 'พัฒนาโมดูลคลังสินค้า สินค้า Lot/Serial/Expiry และรายงานเสร็จ'], ['รวมความคืบหน้าทั้งโครงการ', 'ตามแผน', 45, 'เข้าสู่การพัฒนาระบบสิทธิ์และเอกสารธุรกิจ']],
agenda: [['ผลการพัฒนาโมดูลคลังสินค้าและรายงาน', 'ตรวจสอบความครบถ้วนตาม CR01:005–CR01:012'], ['ประเด็นความถูกต้องของข้อมูล', 'พิจารณาการแก้ไข ISS-002'], ['แผนพัฒนาระบบสิทธิ์ ใบสั่งขาย และบาร์โค้ด', 'ยืนยันลำดับงานช่วงต้นเดือนพฤษภาคม']], agenda: [['ผลการพัฒนาโมดูลคลังสินค้าและรายงาน', 'ตรวจสอบความครบถ้วนตาม CR01:005–CR01:012'], ['ประเด็นความถูกต้องของข้อมูล', 'พิจารณาการแก้ไข ISS-002'], ['แผนพัฒนาระบบสิทธิ์ ใบสั่งขาย และบาร์โค้ด', 'ยืนยันลำดับงานช่วงต้นเดือนพฤษภาคม']],
discussion: [['โมดูลคลังสินค้า', 'พัฒนาแดชบอร์ดสต๊อก ความจุคลัง Lot/Serial/Expiry และรายงานครบตามความต้องการ', ['ThS', 'NoC'], 'เสร็จสิ้น'], ['ความถูกต้องของข้อมูล', 'ถอดคำสั่งที่กระทบความถูกต้องและเพิ่มการตรวจสอบก่อนทำรายการ', ['ThS'], 'เสร็จสิ้น'], ['ระบบสิทธิ์ผู้ใช้', 'มอบหมายพัฒนา Role Guard และรวมศูนย์การตรวจสิทธิ์', ['ThS'], 'ดำเนินการ']], 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']], actions: [['บันทึกการแก้ไข ISS-002 ใน Correction Register', ['YaB'], '2026-05-06', 'Scheduled'], ['พัฒนาระบบเข้าสู่ระบบ สิทธิ์ และเอกสารขาย (Task 3.4, 3.5)', ['ThS'], '2026-05-12', 'Scheduled']],
next: { date: '2026-05-08', topic: 'ติดตามการพัฒนาระบบสิทธิ์ ใบสั่งขาย และบาร์โค้ด' }, 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 น.', date: '2026-05-18', phase: 'Project Execution', taskId: '3.6', taskName: 'Production Preparation & Setup', time: '09:00 – 11:00 น.',
attachment: 'Progress Status Record 13 พ.ค. 69', attachment: '-',
progress: [['Phase 3: Project Execution', 'กำลังดำเนินการ', 70, 'ระบบสิทธิ์ เอกสารขาย และสคริปต์ติดตั้งเสร็จ'], ['รวมความคืบหน้าทั้งโครงการ', 'ตามแผน', 60, 'เริ่มพัฒนาโมดูลบัญชีและการเงิน']], progress: [['Phase 3: Project Execution', 'กำลังดำเนินการ', 70, 'ระบบสิทธิ์ เอกสารขาย และสคริปต์ติดตั้งเสร็จ'], ['รวมความคืบหน้าทั้งโครงการ', 'ตามแผน', 60, 'เริ่มพัฒนาโมดูลบัญชีและการเงิน']],
agenda: [['ผลการพัฒนาระบบสิทธิ์และเอกสารขาย', 'ตรวจสอบความครบถ้วนตาม CR01:001–CR01:004 และ CR01:013'], ['ประเด็นความปลอดภัยและ Onboarding ที่พบ', 'พิจารณาการแก้ไข ISS-003 ถึง ISS-009'], ['แผนพัฒนาบัญชีและการเงิน', 'ยืนยันลำดับงานช่วงกลางเดือนพฤษภาคม']], agenda: [['ผลการพัฒนาระบบสิทธิ์และเอกสารขาย', 'ตรวจสอบความครบถ้วนตาม CR01:001–CR01:004 และ CR01:013'], ['ประเด็นความปลอดภัยและ Onboarding ที่พบ', 'พิจารณาการแก้ไข ISS-003 ถึง ISS-009'], ['คำเรียกตำแหน่งจัดเก็บ (Rack / Bin)', 'กำหนดคำเรียกที่ใช้ในหน้าจอให้ตรงกับการปฏิบัติงานจริง'], ['แผนพัฒนาบัญชีและการเงิน', 'ยืนยันลำดับงานช่วงกลางเดือนพฤษภาคม']],
discussion: [['ระบบสิทธิ์และ Onboarding', 'แก้ไขข้อบกพร่องการลงทะเบียน Onboarding และการตรวจสิทธิ์ครบทุกรายการที่พบ', ['ThS'], 'เสร็จสิ้น'], ['การเตรียมใช้งานจริง', 'จัดทำ Base URL อัตโนมัติและสคริปต์ติดตั้งฐานข้อมูลในขั้นตอนเดียว', ['ThS'], 'เสร็จสิ้น'], ['บัญชีและการเงิน', 'มอบหมายพัฒนาผังบัญชี GL สมุดรายวัน และเอกสารการเงิน', ['ThS', 'NoC'], 'ดำเนินการ']], 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-15', 'Scheduled'], ['พัฒนาโมดูลบัญชีและการเงิน (Task 3.7)', ['ThS'], '2026-05-23', 'Scheduled']], 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-23', topic: 'ติดตามการพัฒนาบัญชี การเงิน และการควบคุมสิทธิ์ผู้ใช้' }, 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 น.', date: '2026-05-28', phase: 'Project Execution', taskId: '3.7', taskName: 'Accounting & Finance Workflows', time: '09:00 – 11:00 น.',
attachment: 'Progress Status Record 23 พ.ค. 69', attachment: '-',
progress: [['Phase 3: Project Execution', 'กำลังดำเนินการ', 85, 'บัญชีและการเงินเสร็จ เหลือบริการแจ้งเตือนและการทบทวนความปลอดภัย'], ['รวมความคืบหน้าทั้งโครงการ', 'ตามแผน', 72, 'เตรียมเข้าสู่ Baseline การพัฒนา']], progress: [['Phase 3: Project Execution', 'กำลังดำเนินการ', 85, 'บัญชีและการเงินเสร็จ เหลือบริการแจ้งเตือนและการทบทวนความปลอดภัย'], ['รวมความคืบหน้าทั้งโครงการ', 'ตามแผน', 72, 'เตรียมเข้าสู่ Baseline การพัฒนา']],
agenda: [['ผลการพัฒนาบัญชีและการเงิน', 'ตรวจสอบความครบถ้วนตาม CR01:015–CR01:017'], ['คำเรียกตำแหน่งจัดเก็บ (Rack / Bin)', 'กำหนดคำเรียกที่ใช้ในหน้าจอให้ตรงกับการปฏิบัติงานจริง'], ['แผนงานก่อนตั้ง Baseline การพัฒนา', 'ยืนยันงานที่ต้องเสร็จภายใน 29 พ.ค. 69']], agenda: [['ผลการพัฒนาบัญชีและการเงิน', 'ตรวจสอบความครบถ้วนตาม CR01:015–CR01:017'], ['แผนงานก่อนตั้ง Baseline การพัฒนา', 'ยืนยันงานที่ต้องเสร็จภายใน 29 พ.ค. 69']],
discussion: [['บัญชีและการเงิน', 'พัฒนาผังบัญชี GL สมุดรายวัน รายงานบัญชี ใบวางบิลและใบเสร็จครบตามความต้องการ', ['ThS', 'NoC'], 'เสร็จสิ้น'], ['คำเรียกตำแหน่งจัดเก็บ', 'ที่ประชุมมีมติให้ใช้คำว่า Bin แทน Rack ทั้งระบบให้ตรงกับคำที่พนักงานคลังใช้จริง เป็นการปรับคำเรียกบนหน้าจอ ไม่เพิ่มหรือลดฟังก์ชันและไม่กระทบ Man-day จึงไม่ถือเป็นคำขอเปลี่ยนแปลง', ['SeV', 'ApS', 'NoC'], 'มติที่ประชุม'], ['ความปลอดภัยและโควตารายการ', 'เพิ่มการควบคุมสิทธิ์แอปพลิเคชันและโควตารายการต่อบริษัท', ['ThS'], 'ดำเนินการ']], discussion: [['บัญชีและการเงิน', 'พัฒนาผังบัญชี GL สมุดรายวัน รายงานบัญชี ใบวางบิลและใบเสร็จครบตามความต้องการ', ['ThS', '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']], 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-05-29', topic: 'สรุปผลการพัฒนาและตั้ง Baseline การพัฒนา' }, 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 น.', date: '2026-06-04', phase: 'Project Execution', taskId: '3.10', taskName: 'Refactor & Development Baseline', time: '09:00 – 11:00 น.',
attachment: 'Progress Status Record 29 พ.ค. 69', attachment: '-',
progress: [['Phase 3: Project Execution', 'เสร็จสมบูรณ์', 100, 'พัฒนาครบทุกโมดูลและตั้ง Baseline การพัฒนา (Git a0677d6)'], ['Phase 4: Verification & Validation', 'กำลังดำเนินการ', 10, 'เริ่มตรวจสอบเอกสารและจัดทำคู่มือ'], ['รวมความคืบหน้าทั้งโครงการ', 'ตามแผน', 78, 'เข้าสู่ขั้นตอนตรวจสอบและจัดทำเอกสาร']], 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'], ['แผนการตรวจสอบ ทดสอบ และจัดทำเอกสาร', 'กำหนดผู้รับผิดชอบและกรอบเวลา']], agenda: [['สรุปผลการพัฒนา Task 3.1–3.10', 'ยืนยันความครบถ้วนของการพัฒนา'], ['สรุปผลการทบทวนความปลอดภัยและวงจรเอกสาร', 'ยืนยันการปิดข้อบกพร่อง ISS-016 ถึง ISS-027'], ['แผนการตรวจสอบ ทดสอบ และจัดทำเอกสาร', 'กำหนดผู้รับผิดชอบและกรอบเวลา']],
discussion: [['Baseline การพัฒนา', 'พัฒนาครบทุกโมดูลตามขอบเขต และลดความซ้ำซ้อนของคลาสเรียบร้อย ตั้ง Baseline เมื่อ 29 พ.ค. 69', ['ThS'], 'เสร็จสิ้น'], ['ความปลอดภัยและการแยกข้อมูล', 'ปิดข้อบกพร่องด้านการเข้าสู่ระบบ ขอบเขตบริษัท และวงจรเอกสารครบทุกรายการที่พบ', ['ThS', 'PaNg'], 'เสร็จสิ้น'], ['ขั้นตอนถัดไป', 'ตรวจสอบ Work Products จัดทำคู่มือ และเตรียมชุดทดสอบระบบ', ['PaNg', 'NoC', 'YaB'], 'ดำเนินการ']], 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']], 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-03', topic: 'ติดตามการตรวจสอบเอกสาร การจัดทำคู่มือ และการแก้ไขหลัง Baseline' }, 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 น.', date: '2026-08-10', phase: 'Stabilization', taskId: '4.6', taskName: 'Post-baseline Corrections', time: '09:00 – 11:00 น.',
attachment: 'Progress Status Record 3 ส.ค. 69', attachment: '-',
progress: [['Phase 4: Verification & Validation', 'กำลังดำเนินการ', 70, 'ตรวจสอบเอกสารรอบที่ 1–3 และจัดทำคู่มือใกล้เสร็จ'], ['รวมความคืบหน้าทั้งโครงการ', 'ตามแผน', 85, 'เตรียมทดสอบระบบและ UAT']], progress: [['Phase 4: Verification & Validation', 'กำลังดำเนินการ', 70, 'ตรวจสอบเอกสารรอบที่ 1–3 และจัดทำคู่มือใกล้เสร็จ'], ['รวมความคืบหน้าทั้งโครงการ', 'ตามแผน', 85, 'เตรียมทดสอบระบบและ UAT']],
agenda: [['ผลการแก้ไขหลัง Baseline การพัฒนา', 'ยืนยันการปิดข้อบกพร่อง ISS-028'], ['ความคืบหน้าการตรวจสอบเอกสารและคู่มือ', 'ทบทวนผลการตรวจสอบรอบที่ 1–3'], ['แผนการทดสอบระบบและ 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'], 'อนุมัติ']], 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-05', 'Scheduled'], ['เตรียมข้อมูลสาธิตสำหรับการทดสอบและ UAT', ['ThS'], '2026-08-14', 'Scheduled'], ['ดำเนินการทดสอบระบบและ UAT (Task 4.4)', ['PaNg', 'SeV'], '2026-08-14', 'Scheduled']], 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-14', topic: 'สรุปผลการทดสอบระบบ UAT และข้อมูลสาธิต' }, 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', 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']], 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'], 'ดำเนินการ']], 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: [['ตรวจสอบ 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']], 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-17', topic: 'สรุปผลการตรวจสอบรอบสุดท้ายและตรวจรับส่งมอบระบบ' }, 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 น.', 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', attachment: 'Acceptance Report, List of Evidence, Verification Results, Training Report, Product Operation Guide, Project Repository (Backup)',
progress: [['Phase 3: Project Execution', 'เสร็จสมบูรณ์', 100, 'พัฒนาระบบครบตามขอบเขต Baseline 6c39700'], ['Phase 4: Verification & Validation', 'เสร็จสมบูรณ์', 100, 'ตรวจสอบ ทดสอบ และ UAT ผ่านครบทุกรายการ'], ['Phase 5: Project Close', 'กำลังดำเนินการ', 80, 'ตรวจรับระบบแล้ว เหลือการอบรมและปิดงานควบคุมปฏิบัติการ'], ['รวมความคืบหน้าทั้งโครงการ', 'ตามแผน', 96, 'ตรวจรับระบบเรียบร้อย']], 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)', 'ยืนยันความพร้อมของชุดส่งมอบ'], ['พิจารณาตรวจรับส่งมอบระบบ', 'ลงมติผลการตรวจรับ']], agenda: [['นำเสนอผลการดำเนินโครงการและสิ่งส่งมอบ', 'สรุปผลงานทั้งหมดเทียบกับแผนและผลลัพธ์จริง'], ['ตรวจสอบความครบถ้วนของเอกสารและหลักฐาน', 'ยืนยันเอกสารครบตามมาตรฐาน ISO/IEC 29110'], ['ชุดติดตั้งและอัตลักษณ์องค์กร (Task 4.7)', 'ยืนยันความพร้อมของชุดส่งมอบ'], ['พิจารณาตรวจรับส่งมอบระบบ', 'ลงมติผลการตรวจรับ'], ['สรุปผลการอบรมผู้ใช้งาน', 'ยืนยันความพร้อมของผู้ใช้ก่อนเปิดใช้งานจริง'], ['สรุปการปิดงานควบคุมการสำรองข้อมูลและการเฝ้าระวัง', 'ยืนยันการปิดรายการ OP-001 และ OP-002'], ['สรุปการสำรอง Repository และเอกสารโครงการ', 'ยืนยันการสำรองและกู้คืนได้จริง'], ['ปิดโครงการอย่างเป็นทางการ', 'ลงมติปิดโครงการและส่งมอบเอกสาร']],
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'], 'ตรวจรับแล้ว']], 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: [['จัดอบรมผู้ใช้งานก่อนเปิดใช้งานจริง (Task 5.2)', ['ThS', 'PaNg'], '2026-08-22', 'Scheduled'], ['ปิดงานควบคุมการสำรองข้อมูลและการเฝ้าระวังระบบ', ['ThS'], '2026-08-23', 'Scheduled'], ['ปิดโครงการและส่งมอบเอกสารเข้าระบบควบคุมเอกสาร', ['ApS', 'YaB'], '2026-08-24', 'Scheduled']], 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: { 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']],
next: null, 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 DEV_BASELINE = '2026-05-29';
export const TEST_FROM = '2026-08-10'; export const TEST_FROM = '2026-08-10';
export const TEST_TO = '2026-08-14'; 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 TRAINING_DATE = '2026-08-22';
export const OPS_CLOSE_DATE = '2026-08-23'; 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 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 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 TAG = 'sdlc-v1.0-final';
export const ORIGIN = 'git@188.166.228.62:nok/wms-app.git'; export const ORIGIN = 'git@188.166.228.62:nok/wms-app.git';
export const BACKUP = 'git@github.com:thanakorninbox-dev/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' }, 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]; 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 = `${P.ApS.th} (Project Manager) , ${P.NoC.th} (System Analyst)`;
export const ORGANIZER_PM = `${P.ApS.th} (Project Manager)`; export const ORGANIZER_PM = `${P.ApS.th} (Project Manager)`;
export const RECORDER = `${P.YaB.th} (Document Control)`; 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. // Planned dates in the future are fine; facts that only came into being later are not.
import { execFileSync } from 'node:child_process'; import { execFileSync } from 'node:child_process';
import { TAG, END } from './project.mjs'; 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 */ /** Document date from its name: "... 25690817 V1.0 ..." -> 2026-08-17 */
export function docDate(name) { export function docDate(name) {
@@ -79,5 +79,46 @@ export function hindsightErrors(docs, root) {
} }
for (const what of found) errors.push(`${d.name}: ${what}`); 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; } [[ -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 ' ')" produced="$(find "$PACKAGE_DIR" -type f -name '*.pdf' | wc -l | tr -d ' ')"
[[ "$expected" == "$produced" ]] || { echo "Expected $expected PDFs, found $produced." >&2; exit 1; } [[ "$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