docs(sdlc): address 23/09 review feedback

Trace every CR to a test case or delivered document, drop the draft FR/NFR remark column, record lessons learned, replace the three change reports with a change-control register, add high-level diagrams and wireframes to the design, plan verification rounds with dates and hours, and base the validation result on the Customer Requirements.
This commit is contained in:
Thanakorn
2026-09-24 08:45:20 +07:00
parent e5cbce6022
commit de6ff85fa1
184 changed files with 1474 additions and 1015 deletions
+10 -5
View File
@@ -8,17 +8,22 @@ SOURCE_DIR="$ROOT_DIR/sdlc"
DELIVERY_ROOT="$ROOT_DIR/sdlc-delivery" DELIVERY_ROOT="$ROOT_DIR/sdlc-delivery"
STAGING_DIR="$ROOT_DIR/.sdlc-delivery.staging" STAGING_DIR="$ROOT_DIR/.sdlc-delivery.staging"
# sdlc/ is generated output: the seed rebuilds every work product from the
# controlled project data in scripts/sdlc-seed/ before anything is rendered.
echo "Seeding work products from project data..."
node "$SCRIPT_DIR/sdlc-seed/build.mjs"
if [[ ! -d "$TOOL_DIR/node_modules/playwright" ]]; then if [[ ! -d "$TOOL_DIR/node_modules/playwright" ]]; then
echo "Installing the local PDF renderer..." echo "Installing the local PDF renderer..."
npm install --prefix "$TOOL_DIR" npm install --prefix "$TOOL_DIR"
npx --prefix "$TOOL_DIR" playwright install chromium npx --prefix "$TOOL_DIR" playwright install chromium
fi fi
# The Software Design figures are drawn in scripts/sdlc-seed/figures.mjs and
# rasterised with the document font before the seed copies them into sdlc/.
echo "Rendering Software Design figures..."
node "$TOOL_DIR/render-figures.mjs"
# sdlc/ is generated output: the seed rebuilds every work product from the
# controlled project data in scripts/sdlc-seed/ before anything is rendered.
echo "Seeding work products from project data..."
node "$SCRIPT_DIR/sdlc-seed/build.mjs"
rm -rf "$STAGING_DIR" rm -rf "$STAGING_DIR"
mkdir -p "$STAGING_DIR" mkdir -p "$STAGING_DIR"
@@ -144,3 +144,8 @@ table.signature-table th:nth-child(4), table.signature-table td:nth-child(4) { w
reference package, so they take a smaller face to fit the text block. */ reference package, so they take a smaller face to fit the text block. */
table.dense { font-size: 7.2pt; } table.dense { font-size: 7.2pt; }
table.dense th, table.dense td { padding: 1mm 1.2mm; } table.dense th, table.dense td { padding: 1mm 1.2mm; }
/* Figures in the Software Design: full column width, caption under the image. */
.delivery-document figure.figure { margin: 8pt 0 12pt; text-align: center; break-inside: avoid; page-break-inside: avoid; }
.delivery-document figure.figure img { max-width: 100%; max-height: 225mm; border: .5pt solid #bfbfbf; }
.delivery-document figure.figure figcaption { margin-top: 4pt; font-size: 9.5pt; font-weight: 700; }
+9 -5
View File
@@ -188,15 +188,19 @@ LOGO_MIN_PX = 100
def strip_logo_images(document): def strip_logo_images(document):
"""Drop the letterhead logo left loose in the body. """Drop the letterhead logo left loose in the body.
The work products carry no figures of their own, but the converter draws The converter draws each bullet as a small image, so only the logo is
each bullet as a small image, so only the logo is removed - anything that removed: it is square and at least LOGO_MIN_PX, whereas anything smaller is
size would be furniture, anything smaller is a list marker. a list marker. The Software Design figures are wider than they are tall,
so they are kept.
""" """
def is_logo(image):
width, height = image.px_width, image.px_height
return max(width, height) >= LOGO_MIN_PX and 0.8 <= width / max(height, 1) <= 1.25
logos = { logos = {
relationship_id relationship_id
for relationship_id, part in document.part.related_parts.items() for relationship_id, part in document.part.related_parts.items()
if getattr(part, 'image', None) is not None if getattr(part, 'image', None) is not None and is_logo(part.image)
and max(part.image.px_width, part.image.px_height) >= LOGO_MIN_PX
} }
removed = 0 removed = 0
+34
View File
@@ -0,0 +1,34 @@
// Rasterise the Software Design figures (scripts/sdlc-seed/figures.mjs) to PNG with the
// document font, so Thai labels print the same in the PDF and survive the Word conversion.
// Usage: node scripts/sdlc-delivery/render-figures.mjs
import fs from 'node:fs/promises';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { chromium } from 'playwright';
import { FIGURES } from '../sdlc-seed/figures.mjs';
const here = path.dirname(fileURLToPath(import.meta.url));
const output = path.resolve(here, '../sdlc-seed/figures');
async function main() {
const font = await fs.readFile(path.join(here, 'assets/LeelawadeeUI.ttf'));
const fontFace = `@font-face { font-family: "BRN Thai"; src: url(data:font/ttf;base64,${font.toString('base64')}) format("truetype"); font-weight: 400 700; }`;
await fs.mkdir(output, { recursive: true });
const browser = await chromium.launch({ headless: true });
try {
const page = await (await browser.newContext({ deviceScaleFactor: 2 })).newPage();
for (const [file, , draw] of Object.values(FIGURES)) {
const svg = draw();
const [, w, h] = svg.match(/width="(\d+)" height="(\d+)"/);
await page.setViewportSize({ width: Number(w), height: Number(h) });
await page.setContent(`<!doctype html><html><head><meta charset="utf-8"><style>${fontFace} html,body{margin:0}</style></head><body>${svg}</body></html>`);
await page.evaluate(() => document.fonts.ready);
await page.locator('svg').first().screenshot({ path: path.join(output, `${file}.png`) });
console.log(`Rendered figures/${file}.png`);
}
} finally {
await browser.close();
}
}
main().catch((error) => { console.error(error); process.exit(1); });
+13 -3
View File
@@ -1,4 +1,5 @@
import fs from 'node:fs/promises'; import fs from 'node:fs/promises';
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';
@@ -101,7 +102,7 @@ function renderParagraph(lines) {
}).join(''); }).join('');
} }
function markdownToHtml(markdown) { function markdownToHtml(markdown, baseDir) {
const lines = markdown.replaceAll('\r\n', '\n').split('\n'); const lines = markdown.replaceAll('\r\n', '\n').split('\n');
const output = []; const output = [];
let index = 0; let index = 0;
@@ -110,6 +111,15 @@ function markdownToHtml(markdown) {
const line = lines[index]; const line = lines[index];
if (!line.trim()) { index += 1; continue; } if (!line.trim()) { index += 1; continue; }
// A figure: ![caption](relative/path.png), embedded so the PDF needs no external files.
const image = line.match(/^!\[([^\]]*)\]\(([^)]+)\)\s*$/);
if (image) {
const data = readFileSync(path.resolve(baseDir, image[2])).toString('base64');
output.push(`<figure class="figure"><img src="data:image/png;base64,${data}" alt=""><figcaption>${renderInline(image[1])}</figcaption></figure>`);
index += 1;
continue;
}
if (isTableLine(line)) { if (isTableLine(line)) {
const table = []; const table = [];
while (index < lines.length && isTableLine(lines[index])) table.push(lines[index++]); while (index < lines.length && isTableLine(lines[index])) table.push(lines[index++]);
@@ -149,7 +159,7 @@ function markdownToHtml(markdown) {
} }
const paragraph = []; const paragraph = [];
while (index < lines.length && lines[index].trim() && !isTableLine(lines[index]) && !/^(#{1,3})\s+/.test(lines[index]) && !/^[-*]\s+/.test(lines[index]) && !/^\d+\.\s+/.test(lines[index])) paragraph.push(lines[index++]); while (index < lines.length && lines[index].trim() && !isTableLine(lines[index]) && !/^!\[/.test(lines[index]) && !/^(#{1,3})\s+/.test(lines[index]) && !/^[-*]\s+/.test(lines[index]) && !/^\d+\.\s+/.test(lines[index])) paragraph.push(lines[index++]);
const isApproval = paragraph.some((line) => /^(Name|Role|Signature|Date|Position|Company|Project roles|Decision):/.test(line.trimEnd())); const isApproval = paragraph.some((line) => /^(Name|Role|Signature|Date|Position|Company|Project roles|Decision):/.test(line.trimEnd()));
const html = `<p${isApproval ? ' class="approval-fields"' : ''}>${renderParagraph(paragraph)}</p>`; const html = `<p${isApproval ? ' class="approval-fields"' : ''}>${renderParagraph(paragraph)}</p>`;
// Keep a signatory's heading and fields on one page. // Keep a signatory's heading and fields on one page.
@@ -226,7 +236,7 @@ async function main() {
? '@page { size: A4 landscape; margin: 32mm 15mm 24mm 15mm; }' ? '@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)}</main></body></html>`; 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>`;
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 });
+2
View File
@@ -12,6 +12,8 @@ function extractDocumentContent(markdown) {
.slice((titleMatch?.index ?? 0) + (titleMatch?.[0].length ?? 0)) .slice((titleMatch?.index ?? 0) + (titleMatch?.[0].length ?? 0))
.replace(/^<!--\s*footer:.*?-->\s*$/m, '') .replace(/^<!--\s*footer:.*?-->\s*$/m, '')
.replace(/<br\s*\/?>/gi, ' ') // markup, not document text .replace(/<br\s*\/?>/gi, ' ') // markup, not document text
// A figure prints its caption; the image path is markup.
.replace(/^!\[([^\]]*)\]\([^)]*\)\s*$/gm, '$1')
// List markers are drawn by the renderer, not stored as text: the browser // List markers are drawn by the renderer, not stored as text: the browser
// paints them from <ol>/<ul> and Word from numbering.xml, so they are // paints them from <ol>/<ul> and Word from numbering.xml, so they are
// markup here too and must not be expected in the extracted text. // markup here too and must not be expected in the extracted text.
+1
View File
@@ -16,6 +16,7 @@ Approval signature tables on every document.
| `history.mjs` | Work Schedule, reporting periods, meetings, Correction Register, Change Reports | | `history.mjs` | Work Schedule, reporting periods, meetings, Correction Register, Change Reports |
| `lib.mjs` | Buddhist-calendar dates, Markdown tables, signature blocks | | `lib.mjs` | Buddhist-calendar dates, Markdown tables, signature blocks |
| `format.mjs` | The shared work-product scaffold | | `format.mjs` | The shared work-product scaffold |
| `figures.mjs` | Software Design diagrams and wireframes, drawn as SVG; `scripts/sdlc-delivery/render-figures.mjs` rasterises them into `figures/*.png` |
| `docs-*.mjs` | One generator per work product | | `docs-*.mjs` | One generator per work product |
| `build.mjs` | Writes all documents and checks no folder is left empty | | `build.mjs` | Writes all documents and checks no folder is left empty |
+11 -1
View File
@@ -11,6 +11,7 @@ import * as other from './docs-other.mjs';
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..'); const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');
const target = path.join(root, 'sdlc'); const target = path.join(root, 'sdlc');
const figureSource = path.join(path.dirname(fileURLToPath(import.meta.url)), 'figures');
// Folders that must exist even when a work product is evidence rather than a document. // Folders that must exist even when a work product is evidence rather than a document.
const FOLDERS = [ const FOLDERS = [
@@ -71,7 +72,16 @@ async function main() {
await fs.rm(target, { recursive: true, force: true }); await fs.rm(target, { 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) await fs.writeFile(path.join(target, d.dir, `${d.name}.md`), d.md, 'utf8'); for (const d of docs) {
await fs.writeFile(path.join(target, d.dir, `${d.name}.md`), d.md, 'utf8');
// Figures referenced as ![caption](figures/x.png) are copied next to the document.
for (const [, file] of d.md.matchAll(/^!\[[^\]]*\]\(figures\/([^)]+)\)$/gm)) {
const from = path.join(figureSource, file);
try { await fs.access(from); } catch { throw new Error(`${d.name} references missing figure ${file}; run scripts/sdlc-delivery/render-figures.mjs`); }
await fs.mkdir(path.join(target, d.dir, 'figures'), { recursive: true });
await fs.copyFile(from, path.join(target, d.dir, 'figures', file));
}
}
const empty = []; const empty = [];
for (const folder of FOLDERS) { for (const folder of FOLDERS) {
+8 -6
View File
@@ -1,7 +1,7 @@
// 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, TAG, DEV_START } from './project.mjs'; 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, TAG, DEV_START } from './project.mjs';
import { SRS, UNITS, TEST_CASES, traceOf, srTopic, unitTopic } from './engineering.mjs'; import { SRS, UNITS, TEST_CASES, traceOf, srTopic, unitTopic } from './engineering.mjs';
import { CORRECTIONS, CHANGES, 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, thRangeLong, table, bullets, numbered, daysBetween } from './lib.mjs';
import { workProduct, head } from './format.mjs'; import { workProduct, head } from './format.mjs';
@@ -119,8 +119,9 @@ export function traceabilityTable() {
const date = '2026-08-17'; const date = '2026-08-17';
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, 'Passed', 'Passed']; return [id, text, t.sr.join(', '), t.un.join(', ') || '-', t.tc, t.ev || '-', 'Passed', 'Passed'];
}); });
const traced = CRS.map(([id]) => traceOf(id));
return { return {
dir: OTHER, dir: OTHER,
name: `${CODE} TRACEABILITY-RECORD ${bud(date)} V1.0 NoC`, name: `${CODE} TRACEABILITY-RECORD ${bud(date)} V1.0 NoC`,
@@ -136,14 +137,15 @@ export function traceabilityTable() {
'## วัตถุประสงค์', '## วัตถุประสงค์',
'ตารางนี้สรุปเส้นทางการสอบกลับของความต้องการทุกรายการ ตั้งแต่ความต้องการของลูกค้าจนถึงผลการทดสอบและผลการทดสอบการยอมรับ เพื่อใช้ตรวจสอบความครบถ้วนได้ในหน้าเดียว โดยรายละเอียดหัวข้อของแต่ละรหัสอยู่ในเอกสาร Traceability Record (WP 13)', 'ตารางนี้สรุปเส้นทางการสอบกลับของความต้องการทุกรายการ ตั้งแต่ความต้องการของลูกค้าจนถึงผลการทดสอบและผลการทดสอบการยอมรับ เพื่อใช้ตรวจสอบความครบถ้วนได้ในหน้าเดียว โดยรายละเอียดหัวข้อของแต่ละรหัสอยู่ในเอกสาร Traceability Record (WP 13)',
'## ตารางสรุปการสอบกลับ', '## ตารางสรุปการสอบกลับ',
table(['CR ID', 'Customer Requirement', 'SRS ID', 'Unit ID', 'Test Case ID', 'ผลการทดสอบ', 'ผล UAT'], rows, ['', '', '', '', 'c', 'c', 'c']), table(['CR ID', 'Customer Requirement', 'SRS ID', 'Unit ID', 'Test Case ID', 'หลักฐานอื่น (Document)', 'ผลการทดสอบ', 'ผล UAT'], rows, ['', '', '', '', 'c', '', 'c', 'c']),
'## สรุปความครอบคลุม', '## สรุปความครอบคลุม',
table(['รายการ', 'จำนวน'], [ table(['รายการ', 'จำนวน'], [
['ความต้องการของลูกค้าทั้งหมด', CRS.length], ['ความต้องการของลูกค้าทั้งหมด', CRS.length],
['เชื่อมโยงกับความต้องการซอฟต์แวร์', CRS.length], ['เชื่อมโยงกับความต้องการซอฟต์แวร์', CRS.length],
['เชื่อมโยงกับ Software Unit', CRS.length], ['เชื่อมโยงกับ Software Unit', traced.filter((t) => t.un.length).length],
['เชื่อมโยงกับ Test Case', CRS.length], ['เชื่อมโยงกับ Test Case', traced.filter((t) => t.tcs.length).length],
['ผลการทดสอบผ่าน', CRS.length], ['ยืนยันด้วยเอกสารส่งมอบ (ไม่มี Software Unit)', traced.filter((t) => !t.tcs.length).length],
['ผลการทดสอบหรือการตรวจสอบเอกสารผ่าน', CRS.length],
['ผลการทดสอบการยอมรับผ่าน', CRS.length], ['ผลการทดสอบการยอมรับผ่าน', CRS.length],
], ['', 'r']), ], ['', 'r']),
].join('\n\n'), ].join('\n\n'),
+21 -8
View File
@@ -1,6 +1,6 @@
// 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, 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 } 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';
@@ -97,7 +97,7 @@ export function workSchedule() {
['ตั้ง Baseline ความต้องการและแผนงาน', thLong('2026-02-18'), 'อนุมัติ Customer Requirements และ Work Schedule'], ['ตั้ง Baseline ความต้องการและแผนงาน', thLong('2026-02-18'), 'อนุมัติ Customer Requirements และ Work Schedule'],
['เริ่มพัฒนาระบบ', thLong(DEV_START), 'เริ่ม Task 3.1 ตามแผน'], ['เริ่มพัฒนาระบบ', thLong(DEV_START), 'เริ่ม Task 3.1 ตามแผน'],
['Baseline การพัฒนา', thLong('2026-05-29'), 'พัฒนาครบทุกโมดูล (Git a0677d6)'], ['Baseline การพัฒนา', thLong('2026-05-29'), 'พัฒนาครบทุกโมดูล (Git a0677d6)'],
['ทดสอบระบบและ UAT', thRangeLong(TEST_FROM, TEST_TO), 'ทดสอบ 45 Test Case และ 12 สถานการณ์ UAT ผ่านทั้งหมด'], ['ทดสอบระบบและ UAT', thRangeLong(TEST_FROM, TEST_TO), `ทดสอบ 45 Test Case และ UAT ครบ 14 หมวดความต้องการ (${CRS.length} รายการ) ผ่านทั้งหมด`],
['ตรวจรับส่งมอบระบบ', thLong('2026-08-17'), 'ผลการตรวจรับ Accepted'], ['ตรวจรับส่งมอบระบบ', thLong('2026-08-17'), 'ผลการตรวจรับ Accepted'],
['อบรมผู้ใช้งาน', thLong('2026-08-22'), 'อบรมผู้ใช้งาน 6 คน'], ['อบรมผู้ใช้งาน', thLong('2026-08-22'), 'อบรมผู้ใช้งาน 6 คน'],
['ปิดโครงการ', thLong(END), 'ปิดโครงการอย่างเป็นทางการ'], ['ปิดโครงการ', thLong(END), 'ปิดโครงการอย่างเป็นทางการ'],
@@ -185,11 +185,11 @@ export function softwareProjectPlan() {
'ตรวจสอบย้อนกลับ (Audit) และตรวจรับงานได้อย่างเป็นทางการ', 'ตรวจสอบย้อนกลับ (Audit) และตรวจรับงานได้อย่างเป็นทางการ',
]), ]),
table(['ขั้นตอน', 'แนวทางที่ใช้จริงในโครงการ'], [ table(['ขั้นตอน', 'แนวทางที่ใช้จริงในโครงการ'], [
['Requirements', 'เก็บความต้องการและตั้ง Baseline ก่อนเริ่มพัฒนา การเปลี่ยนแปลงภายหลังผ่าน Change Report (CH-001–CH-003)'], ['Requirements', 'เก็บความต้องการและตั้ง Baseline ก่อนเริ่มพัฒนา การเปลี่ยนแปลงภายหลังต้องพิจารณาตามเกณฑ์ใน Change Report ก่อนดำเนินการ'],
['Design', 'ออกแบบสถาปัตยกรรม โครงสร้างฐานข้อมูล และ Software Unit ก่อนพัฒนาแต่ละโมดูล'], ['Design', 'ออกแบบสถาปัตยกรรม โครงสร้างฐานข้อมูล และ Software Unit ก่อนพัฒนาแต่ละโมดูล'],
['Implementation', 'พัฒนาเป็นโมดูลต่อเนื่องระหว่าง 19 ก.พ. – 29 พ.ค. 69 พร้อมทบทวนและแก้ไขระหว่างทาง'], ['Implementation', 'พัฒนาเป็นโมดูลต่อเนื่องระหว่าง 19 ก.พ. – 29 พ.ค. 69 พร้อมทบทวนและแก้ไขระหว่างทาง'],
['Verification', 'ตรวจสอบ Work Products 4 รอบ ควบคู่กับการพัฒนา และรอบสุดท้ายก่อนส่งมอบ'], ['Verification', 'ตรวจสอบ Work Products 4 รอบ ประมาณทุก 2 เดือน และรอบสุดท้ายก่อนส่งมอบ ตามแผนในหัวข้อ 8.1'],
['Validation', 'ทดสอบระบบและ UAT ระหว่าง 10–14 ส.ค. 69 บนสภาพแวดล้อมที่กำหนด'], ['Validation', 'ทดสอบระบบและ UAT ระหว่าง 10–14 ส.ค. 69 บนสภาพแวดล้อมที่กำหนด โดยใช้ Customer Requirements ทุกรายการเป็นตัวตั้ง'],
['Closure', 'ตรวจรับ อบรม ปิดงานควบคุมปฏิบัติการ และปิดโครงการภายใน 24 ส.ค. 69'], ['Closure', 'ตรวจรับ อบรม ปิดงานควบคุมปฏิบัติการ และปิดโครงการภายใน 24 ส.ค. 69'],
]), ]),
sec(5, 'Organization'), sec(5, 'Organization'),
@@ -241,6 +241,19 @@ export function softwareProjectPlan() {
['Verification & Validation', thRangeLong('2026-05-30', '2026-08-17'), 'Test Case, Test Report, Verification Results, Validation Results'], ['Verification & Validation', thRangeLong('2026-05-30', '2026-08-17'), 'Test Case, Test Report, Verification Results, Validation Results'],
['Project Close', thRangeLong('2026-08-10', END), 'Acceptance Report, Training Report, List of Evidence'], ['Project Close', thRangeLong('2026-08-10', END), 'Acceptance Report, Training Report, List of Evidence'],
]), ]),
'### 8.1 แผนการตรวจสอบ (Verification Plan)',
`โครงการมีระยะเวลา ${DURATION} วัน จึงกำหนดให้ตรวจสอบ Work Products ทั้งหมด ${VERIFICATION_ROUNDS.length} ครั้ง ประมาณทุก 2 เดือน (ทุก 60–75 วัน) ครั้งละ 3 ชั่วโมง และรอบสุดท้ายก่อนส่งมอบระบบ 6 ชั่วโมง เพื่อให้พบประเด็นตั้งแต่เนิ่น ๆ และแก้ไขได้ก่อนเริ่มงานขั้นถัดไป ผลการตรวจสอบแต่ละรอบบันทึกใน Verification Results และประเด็นที่พบบันทึกใน Correction Register`,
table(['รอบ', 'วันที่ตรวจสอบ', 'เวลา', 'ชั่วโมง', 'หัวข้อการตรวจสอบ', 'Deliverables under Review'],
VERIFICATION_ROUNDS.map((r) => [r.no, thLong(r.date), r.time, r.hours, r.topic, r.wps.join(', ')]), ['c', '', 'c', 'r', '', '']),
table(['หัวข้อ', 'รายละเอียด'], [
['ผู้ตรวจสอบ', `${P.PaNg.th} (QA/Tester) ร่วมกับ ${P.YaB.th} (Document Control)`],
['ผู้เข้าร่วมรับฟังผล', 'ผู้จัดทำ Work Product ที่ตรวจในรอบนั้น เพื่อแก้ไขได้ทันทีเมื่อพบประเด็น'],
['ผู้อนุมัติผลการตรวจสอบ', `${P.SeV.th} (Project Sponsor)`],
['รวมเวลาตรวจสอบตลอดโครงการ', `${VERIFICATION_ROUNDS.reduce((a, r) => a + r.hours, 0)} ชั่วโมง`],
['บันทึกผล', 'Verification Results แยกฉบับตามรอบ (V0.1–V1.0) และ Correction Register'],
]),
'**Risk & Constraints Note**',
numbered(VERIFICATION_RULES),
sec(9, 'Risk Management Plan'), sec(9, 'Risk Management Plan'),
'วัตถุประสงค์ของแผนบริหารความเสี่ยง คือระบุแนวทางจัดการความเสี่ยงของโครงการ เพื่อให้โครงการบรรลุเป้าหมายภายในเวลา งบประมาณ และคุณภาพที่กำหนด', 'วัตถุประสงค์ของแผนบริหารความเสี่ยง คือระบุแนวทางจัดการความเสี่ยงของโครงการ เพื่อให้โครงการบรรลุเป้าหมายภายในเวลา งบประมาณ และคุณภาพที่กำหนด',
table(['ลำดับ', 'หมวด', 'รายการ', 'รายละเอียด'], [ table(['ลำดับ', 'หมวด', 'รายการ', 'รายละเอียด'], [
@@ -309,8 +322,8 @@ export function customerRequirements() {
const date = '2026-02-06'; const date = '2026-02-06';
const rows = []; const rows = [];
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, remark] of items) rows.push([id, text, 'A', remark]); for (const [id, text] of items) rows.push([id, text, 'A']);
} }
return { return {
dir: '1-PM Process (10 Work Product)/2.Project Plan/3-Customer Requirement', dir: '1-PM Process (10 Work Product)/2.Project Plan/3-Customer Requirement',
@@ -336,7 +349,7 @@ export function customerRequirements() {
[6, 'ฝ่ายบัญชีและการเงิน', 'ผู้ใช้งาน', 'ให้ข้อมูลการวางบิล รับชำระ จ่ายชำระ และการบันทึกบัญชี'], [6, 'ฝ่ายบัญชีและการเงิน', 'ผู้ใช้งาน', 'ให้ข้อมูลการวางบิล รับชำระ จ่ายชำระ และการบันทึกบัญชี'],
], ['c']), ], ['c']),
'## ความต้องการเชิงหน้าที่และไม่ใช่หน้าที่ (Functional and Non-Functional Requirements)', '## ความต้องการเชิงหน้าที่และไม่ใช่หน้าที่ (Functional and Non-Functional Requirements)',
table(['ID', 'Topic', 'Result*', 'Remark'], rows, ['', '', 'c', '']), table(['ID', 'Topic', 'Result*'], rows, ['', '', 'c']),
'## Remark', '## Remark',
bullets(['\\* Result : A = Accepted, U = Unaccepted, N/A = Not Applicable', 'เอกสารฉบับนี้รวมความต้องการที่ได้จากการทบทวนขอบเขตงานร่วมกับลูกค้า', 'การเปลี่ยนแปลงความต้องการหลังอนุมัติต้องดำเนินการผ่าน Change Report']), bullets(['\\* Result : A = Accepted, U = Unaccepted, N/A = Not Applicable', 'เอกสารฉบับนี้รวมความต้องการที่ได้จากการทบทวนขอบเขตงานร่วมกับลูกค้า', 'การเปลี่ยนแปลงความต้องการหลังอนุมัติต้องดำเนินการผ่าน Change Report']),
].join('\n\n'), ].join('\n\n'),
+46 -59
View File
@@ -1,7 +1,7 @@
// PM work products 3–10: Progress Status Record, Correction Register, Acceptance Report, // PM work products 3–10: Progress Status Record, Correction Register, Acceptance Report,
// Change Report, Meeting Record, Software Configuration, Project Repository (+ Backup). // Change Report, Meeting Record, Software Configuration, Project Repository (+ Backup).
import { P, CODE, PROJECT_TH, COMPANY_TH, ORGANIZER, ORGANIZER_PM, RECORDER, START, END, WPS, TEAM, ORIGIN, BACKUP, TAG, BASELINE_COMMIT, TEST_FROM, TEST_TO, ACCEPT_DATE, OPS_CLOSE_DATE } from './project.mjs'; import { P, CRS, CODE, PROJECT_TH, COMPANY_TH, ORGANIZER, ORGANIZER_PM, RECORDER, START, END, WPS, TEAM, ORIGIN, BACKUP, TAG, BASELINE_COMMIT, TEST_FROM, TEST_TO, ACCEPT_DATE, OPS_CLOSE_DATE } from './project.mjs';
import { TASKS, taskById, CORRECTIONS, CHANGES, 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';
@@ -22,10 +22,8 @@ export function progressStatusRecords() {
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 [n + 1, `${c[0]} : ${c[4]}`, `${c[5]} จึงดำเนินการ${c[6]} แล้วเสร็จ ${thShort(c[2])}`, P[c[7]].id, 'เสร็จสิ้น'];
}); });
const changes = changeIds.map((cid) => { if (changeIds.length) throw new Error(`Progress record ${date} lists change requests, but the project has none.`);
const c = CHANGES.find((x) => x.id === cid); const changes = [];
return [c.id, c.titleTh, 'อนุมัติ', c.impact[0][1]];
});
return { return {
dir: dir3, dir: dir3,
name: `${CODE} Progress Status Record ${bud(date)} V1.0 ApS`, name: `${CODE} Progress Status Record ${bud(date)} V1.0 ApS`,
@@ -88,6 +86,10 @@ export function correctionRegister() {
['คงค้าง', 0], ['คงค้าง', 0],
], ['', 'r']), ], ['', 'r']),
`ปัญหาทุกรายการได้รับการแก้ไขและตรวจสอบผลด้วย Test Case ที่เกี่ยวข้อง โดยผลการทดสอบผ่านทั้งหมดในรอบทดสอบระหว่าง ${thRangeLong(TEST_FROM, TEST_TO)} ไม่มีข้อบกพร่องระดับวิกฤตคงค้าง ณ วันตรวจรับ`, `ปัญหาทุกรายการได้รับการแก้ไขและตรวจสอบผลด้วย Test Case ที่เกี่ยวข้อง โดยผลการทดสอบผ่านทั้งหมดในรอบทดสอบระหว่าง ${thRangeLong(TEST_FROM, TEST_TO)} ไม่มีข้อบกพร่องระดับวิกฤตคงค้าง ณ วันตรวจรับ`,
'## บทเรียนและแนวทางป้องกันสำหรับโครงการถัดไป (Lessons Learned)',
'เมื่อปิดโครงการ ทีมงานทบทวนปัญหาทั้งหมดในทะเบียนนี้ จัดกลุ่มตามสาเหตุ และกำหนดแนวทางป้องกันเพื่อไม่ให้ปัญหาลักษณะเดียวกันเกิดซ้ำในโครงการถัดไป',
table(['ลำดับ', 'กลุ่มปัญหา', 'เลขที่ปัญหา', 'จำนวน', 'สิ่งที่เกิดขึ้น', 'แนวทางป้องกัน (Preventive Action)'],
LESSONS.map(([theme, issueIds, what, action], i) => [i + 1, theme, issueIds.join(', '), issueIds.length, what, action]), ['c', '', '', 'r', '', '']),
'## การเชื่อมโยงกับหลักฐานการทดสอบ', '## การเชื่อมโยงกับหลักฐานการทดสอบ',
table(['เลขที่', 'การแก้ไขอ้างอิง (Commit)', 'Test Case ที่ใช้ตรวจสอบ', 'ผลการทดสอบ'], CORRECTIONS.map((c) => [c[0], `\`${c[8]}\``, c[9], 'Passed']), ['c', '', 'c', 'c']), table(['เลขที่', 'การแก้ไขอ้างอิง (Commit)', 'Test Case ที่ใช้ตรวจสอบ', 'ผลการทดสอบ'], CORRECTIONS.map((c) => [c[0], `\`${c[8]}\``, c[9], 'Passed']), ['c', '', 'c', 'c']),
].join('\n\n'), ].join('\n\n'),
@@ -118,7 +120,7 @@ export function acceptanceReport() {
table(['ลำดับ', 'เกณฑ์การตรวจรับ', 'หลักฐานอ้างอิง', 'ผลการตรวจสอบ'], [ table(['ลำดับ', 'เกณฑ์การตรวจรับ', 'หลักฐานอ้างอิง', 'ผลการตรวจสอบ'], [
[1, 'ฟังก์ชันในขอบเขตทำงานตรงตาม Customer Requirements ครบทุกรายการ', 'Traceability Record, Test Report', 'ผ่าน'], [1, 'ฟังก์ชันในขอบเขตทำงานตรงตาม Customer Requirements ครบทุกรายการ', 'Traceability Record, Test Report', 'ผ่าน'],
[2, 'ทดสอบระบบตาม Test Case ครบถ้วน', `Test Report 45 Test Case ผ่านทั้งหมด (${thRangeLong(TEST_FROM, TEST_TO)})`, 'ผ่าน'], [2, 'ทดสอบระบบตาม Test Case ครบถ้วน', `Test Report 45 Test Case ผ่านทั้งหมด (${thRangeLong(TEST_FROM, TEST_TO)})`, 'ผ่าน'],
[3, 'ทดสอบการยอมรับโดยผู้ใช้ (UAT)', 'Validation Results 12 สถานการณ์ ผ่านทั้งหมด', 'ผ่าน'], [3, 'ทดสอบการยอมรับโดยผู้ใช้ (UAT)', `Validation Results ครบ 14 หมวดความต้องการ (${CRS.length} รายการ) ผ่านทั้งหมด`, 'ผ่าน'],
[4, 'ไม่มีข้อบกพร่องระดับวิกฤตคงค้าง', `Correction Register ${CORRECTIONS.length} รายการ แก้ไขและตรวจสอบครบ`, 'ผ่าน'], [4, 'ไม่มีข้อบกพร่องระดับวิกฤตคงค้าง', `Correction Register ${CORRECTIONS.length} รายการ แก้ไขและตรวจสอบครบ`, 'ผ่าน'],
[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)', 'ผ่าน'],
@@ -139,65 +141,50 @@ export function acceptanceReport() {
}; };
} }
/** 6. Change Report — one document per change request. */ /** 6. Change Report — the change-control register. No adjustment in this project met the criteria. */
export function changeReports() { export function changeReports() {
return CHANGES.map((c) => ({ const date = ACCEPT_DATE;
return [{
dir: '1-PM Process (10 Work Product)/6.Change Report', dir: '1-PM Process (10 Work Product)/6.Change Report',
name: `${CODE} Change Report ${bud(c.date)} V1.0 ${c.requester.id}`, name: `${CODE} Change Report ${bud(date)} V1.0 ApS`,
md: workProduct({ md: workProduct({
type: 'Change Report', footer: 'CR', type: 'Change Report', footer: 'CR',
rows: head('Change Report', `${bud(c.date)} V1.0 ${c.requester.id}`, [ rows: head('Change Report', `${bud(date)} V1.0 ApS`, [
['Change ID', c.id, 'วันที่ร้องขอ', thLong(c.date)], ['Title', 'ทะเบียนการควบคุมการเปลี่ยนแปลง (Change Control Register)'],
['ผู้ร้องขอ (Requester)', `${c.requester.th} (${c.requester.role})`], ['Project Period', thRangeLong(START, END)],
['หน่วยงาน/ฝ่าย', c.unit], ['Organizer', ORGANIZER_PM],
['ประเภทการเปลี่ยนแปลง', `☒ ${c.type} ☒ ${c.typeChecked} ☐ Schedule ☐ Cost ☐ Resource ☐ Other`], ['Recorder', RECORDER],
['ความเร่งด่วน (Priority)', `☐ High ☒ ${c.priority} ☐ Low`],
]), ]),
body: [ body: [
'## ส่วนที่ 1 : ข้อมูลทั่วไป', '## วัตถุประสงค์ (Objective)',
table(['หัวข้อ', 'รายละเอียด'], [ 'เอกสารนี้บันทึกการควบคุมการเปลี่ยนแปลงของโครงการ ตั้งแต่ตั้ง Baseline ความต้องการเมื่อ 18 กุมภาพันธ์ 2569 จนถึงการตรวจรับส่งมอบ โดยพิจารณาทุกรายการปรับปรุงที่มีการเสนอตามเกณฑ์ในส่วนที่ 1 รายการที่เข้าเกณฑ์ต้องจัดทำคำขอเปลี่ยนแปลง ประเมินผลกระทบ และขออนุมัติก่อนดำเนินการ ส่วนรายการที่ไม่เข้าเกณฑ์ดำเนินการเป็นมติที่ประชุมหรืองานในแผน',
['ชื่อการเปลี่ยนแปลง', c.titleTh], '## ส่วนที่ 1 : เกณฑ์การพิจารณาคำขอเปลี่ยนแปลง (Change Criteria)',
['โครงการ', PROJECT_TH], 'รายการใดจะถือเป็นคำขอเปลี่ยนแปลงเมื่อเข้าเกณฑ์ทั้ง 3 ข้อ หากตัดรายการนั้นออกแล้วยังส่งมอบงานได้ตามที่ตกลงไว้ จะไม่ถือเป็นคำขอเปลี่ยนแปลง',
['ระยะที่เกิดการเปลี่ยนแปลง', `ช่วงดำเนินโครงการ (${thLong(c.date)})`], table(['ลำดับ', 'เกณฑ์', 'คำอธิบาย'], CHANGE_CRITERIA.map(([name, detail], i) => [i + 1, name, detail]), ['c']),
['เอกสารที่เกี่ยวข้อง', 'Customer Requirements, Software Project Plan, Traceability Record'], '## ส่วนที่ 2 : ทะเบียนคำขอเปลี่ยนแปลง (Change Request Log)',
]), table(['Change ID', 'หัวข้อ', 'ผู้ร้องขอ', 'ผลกระทบ', 'สถานะ'], [['-', 'ไม่มีคำขอเปลี่ยนแปลงที่เข้าเกณฑ์ตลอดโครงการ', '-', '-', '-']], ['c', '', 'c', '', 'c']),
'## ส่วนที่ 2 : รายละเอียดการเปลี่ยนแปลง', '## ส่วนที่ 3 : รายการปรับปรุงที่พิจารณาแล้วไม่เข้าเกณฑ์',
table(['ลำดับ', 'รายการ', 'รายละเอียด'], [ table(['ลำดับ', 'วันที่พิจารณา', 'รายการ', 'ผู้เสนอ', 'ผลการพิจารณา', 'การดำเนินการ', 'อ้างอิง'],
[1, 'คำอธิบายการเปลี่ยนแปลงที่ต้องการ (Description)', c.description], CHANGE_REVIEW.map(([when, topic, who, assessment, handled, ref], i) => [i + 1, thShort(when), topic, P[who].id, `ไม่เข้าเกณฑ์ — ${assessment}`, handled, ref]), ['c', 'c', '', 'c', '', '', '']),
[2, 'เหตุผลของการเปลี่ยนแปลง (Justification)', c.justification], '## ส่วนที่ 4 : สรุปผลการควบคุมการเปลี่ยนแปลง',
], ['c']), table(['รายการ', 'จำนวน'], [
'## ส่วนที่ 3 : การประเมินผลกระทบ (Impact Analysis)', ['รายการปรับปรุงที่เสนอและพิจารณา', CHANGE_REVIEW.length],
table(['ลำดับ', 'รายการ', 'รายละเอียด'], c.impact.map((r, i) => [i + 1, r[0], r[1]]), ['c']), ['คำขอเปลี่ยนแปลงที่เข้าเกณฑ์', 0],
'## ส่วนที่ 4 : การพิจารณาอนุมัติ (Change Advisory Board Approval)', ['คำขอเปลี่ยนแปลงที่อนุมัติ', 0],
table(['ลำดับ', 'ชื่อผู้พิจารณา', 'ตำแหน่ง', 'ความคิดเห็น / ลงนาม'], [ ['ผลกระทบต่อ Man-day กำหนดส่งมอบ และค่าใช้จ่าย', 'ไม่มี'],
[1, P.ApS.th, 'Project Manager', 'เห็นควรอนุมัติ ผลกระทบอยู่ในวิสัยที่ควบคุมได้'], ], ['', 'r']),
[2, P.NoC.th, 'System Analyst', 'สอดคล้องกับความต้องการและการออกแบบ'], 'ขอบเขตงาน สิ่งส่งมอบ และ Customer Requirements ที่ตั้ง Baseline ไม่มีการเปลี่ยนแปลงตลอดโครงการ รายการปรับปรุงที่พิจารณาทั้งหมดดำเนินการภายในแผนงานเดิมโดยไม่กระทบ Man-day และกำหนดส่งมอบ',
[3, P.PaNg.th, 'QA / Tester', 'ต้องทดสอบซ้ำในส่วนที่ได้รับผลกระทบ'], '## ภาคผนวก A : เกณฑ์การประเมินระดับความรุนแรงของการเปลี่ยนแปลง',
[4, P.SeV.th, 'Project Sponsor', 'อนุมัติ'], 'ใช้ประเมินระดับความรุนแรง (Priority Level) เมื่อมีคำขอเปลี่ยนแปลงที่เข้าเกณฑ์ในส่วนที่ 1',
], ['c']), table(['ระดับ (Level)', 'คำจำกัดความ (Definition)', 'ตัวอย่างสถานการณ์ (Examples)'], [
'## ส่วนที่ 5 : สรุปผลการพิจารณา', ['High', 'กระทบระบบหลัก งบประมาณ หรือแผนงานอย่างมีนัยสำคัญ', 'เพิ่มโมดูลใหม่ เปลี่ยนโครงสร้างฐานข้อมูลหลัก'],
table(['หัวข้อ', 'รายละเอียด'], [ ['Medium', 'กระทบฟังก์ชันย่อยหรือส่วนติดต่อผู้ใช้งานบางส่วน และกระทบ Man-day', 'รองรับ 2 ภาษา เพิ่มรายงานใหม่'],
['สถานะ', '☒ อนุมัติ (Approved) ☐ ไม่อนุมัติ (Rejected)'], ['Low', 'กระทบเล็กน้อยต่อ Man-day ไม่เปลี่ยนการทำงานหลัก', 'เพิ่มฟิลด์ในเอกสาร'],
['การอ้างอิงการดำเนินการ', `Commit \`${c.commit}\``],
['ผลการดำเนินการ', c.result],
]),
'## ภาคผนวก A : เกณฑ์การประเมินระดับความรุนแรงของการเปลี่ยนแปลง (Change Criteria)',
'ตารางนี้ใช้เป็นแนวทางประเมินระดับความรุนแรง (Priority Level) ของการเปลี่ยนแปลง เพื่อให้คณะพิจารณา (Change Advisory Board – CAB) กำหนดระดับผลกระทบและความเร่งด่วนได้ชัดเจนตามมาตรฐาน ISO/IEC 29110',
table(['ระดับ (Level)', 'คำจำกัดความ (Definition)', 'ตัวอย่างสถานการณ์ (Examples)', 'ผลกระทบ (Impact Scope)'], [
['High', 'การเปลี่ยนแปลงที่มีผลกระทบอย่างมีนัยสำคัญต่อระบบหลัก งบประมาณ หรือแผนโครงการ ซึ่งต้องได้รับอนุมัติจาก Project Manager และ Project Sponsor', 'ปรับสถาปัตยกรรมหลักของระบบ / เพิ่มฟังก์ชันหลักใหม่ / ต้องปรับงบประมาณหรือระยะเวลาโครงการ', 'กระทบ Scope, Schedule และ Cost โดยตรง'],
['Medium', 'การเปลี่ยนแปลงที่มีผลกระทบต่อส่วนติดต่อผู้ใช้งานหรือฟังก์ชันย่อยบางส่วน แต่ไม่กระทบโครงสร้างหลัก', 'ปรับปรุงหน้าจอ / ปรับข้อความหรือตรรกะบางส่วน / เพิ่มการตรวจสอบข้อมูล', 'กระทบ Quality หรือ Deliverables บางส่วน'],
['Low', 'การเปลี่ยนแปลงขนาดเล็กที่ไม่ส่งผลต่อการทำงานหลักของระบบ', 'แก้ไขคำสะกดหรือข้อความแสดงผล / เปลี่ยนโลโก้หรือรูปภาพ / ปรับฟอนต์หรือสี', 'ผลกระทบเล็กน้อยต่อ Deliverables'],
]),
'**แนวทางการใช้งาน**',
bullets([
'ผู้ร้องขอ (Requester) ระบุระดับความรุนแรงตามตารางนี้ในส่วน "ความเร่งด่วน (Priority)"',
'คณะพิจารณา (CAB) ใช้ตารางนี้ประกอบการพิจารณาในส่วน "การประเมินผลกระทบ (Impact Analysis)"',
'หากมีข้อสงสัยในระดับผลกระทบ ให้ใช้ระดับที่สูงกว่าเพื่อความปลอดภัยของโครงการ',
]), ]),
].join('\n\n'), ].join('\n\n'),
secretary: c.requester, reviewer: P.ApS, approver: P.SeV, secretary: P.ApS, reviewer: P.YaB, approver: P.SeV,
}), }),
})); }];
} }
/** 7. Meeting Record — Minutes of Meeting per checkpoint. */ /** 7. Meeting Record — Minutes of Meeting per checkpoint. */
@@ -266,7 +253,7 @@ export function softwareConfiguration() {
['**เอกสารส่งมอบครั้งที่ 2 (พัฒนาและทดสอบ)**', '', ''], ['**เอกสารส่งมอบครั้งที่ 2 (พัฒนาและทดสอบ)**', '', ''],
['WP 3.0', `เอกสาร ${CODE} Software Requirements Specification`, '1.0'], ['WP 3.0', `เอกสาร ${CODE} Software Requirements Specification`, '1.0'],
['WP 4.0', `เอกสาร ${CODE} Software Design`, '1.0'], ['WP 4.0', `เอกสาร ${CODE} Software Design`, '1.0'],
['WP 5.0', `เอกสาร ${CODE} Change Report (CH-001–CH-003)`, '1.0'], ['WP 5.0', `เอกสาร ${CODE} Change Report`, '1.0'],
['WP 6.0', `เอกสาร ${CODE} Test Case and Test Procedures`, '1.0'], ['WP 6.0', `เอกสาร ${CODE} Test Case and Test Procedures`, '1.0'],
['WP 7.0', `เอกสาร ${CODE} Validation Results`, '1.0'], ['WP 7.0', `เอกสาร ${CODE} Validation Results`, '1.0'],
['WP 8.0', `เอกสาร ${CODE} Software User Document`, '1.0'], ['WP 8.0', `เอกสาร ${CODE} Software User Document`, '1.0'],
@@ -296,7 +283,7 @@ export function softwareConfiguration() {
['สิทธิ์การเข้าถึง', 'ยืนยันตัวตนด้วย SSH Key ตามบทบาทของสมาชิกโครงการ'], ['สิทธิ์การเข้าถึง', 'ยืนยันตัวตนด้วย SSH Key ตามบทบาทของสมาชิกโครงการ'],
]), ]),
'## การเชื่อมโยงกับการเปลี่ยนแปลง', '## การเชื่อมโยงกับการเปลี่ยนแปลง',
`การเปลี่ยนแปลง Configuration Item ต้องผ่าน Change Report (CH-001–CH-003) และการแก้ไขข้อบกพร่องต้องบันทึกใน Correction Register (${CORRECTIONS[0][0]}–${CORRECTIONS[CORRECTIONS.length - 1][0]}) โดยทุกรายการเชื่อมโยงกับ Commit ที่ตรวจสอบได้ใน Repository`, `การเปลี่ยนแปลง Configuration Item ต้องพิจารณาตามเกณฑ์ใน Change Report และการแก้ไขข้อบกพร่องต้องบันทึกใน Correction Register (${CORRECTIONS[0][0]}–${CORRECTIONS[CORRECTIONS.length - 1][0]}) โดยทุกรายการเชื่อมโยงกับ Commit ที่ตรวจสอบได้ใน Repository`,
].join('\n\n'), ].join('\n\n'),
secretary: P.ThS, reviewer: P.ApS, approver: P.SeV, secretary: P.ThS, reviewer: P.ApS, approver: P.SeV,
}), }),
+39 -12
View File
@@ -2,6 +2,7 @@
import { P, CODE, PROJECT_TH, ORGANIZER, ORGANIZER_PM, RECORDER, START, END, SCOPE, CRS, crTopic, TEST_FROM, TEST_TO, ACCEPT_DATE, BASELINE_COMMIT, TEST_STATE_COMMIT, ORIGIN, BACKUP, TAG } from './project.mjs'; import { P, CODE, PROJECT_TH, ORGANIZER, ORGANIZER_PM, RECORDER, START, END, SCOPE, CRS, crTopic, TEST_FROM, TEST_TO, ACCEPT_DATE, BASELINE_COMMIT, TEST_STATE_COMMIT, ORIGIN, BACKUP, TAG } 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 } from './engineering.mjs';
import { CORRECTIONS } from './history.mjs'; import { CORRECTIONS } from './history.mjs';
import { figure } from './figures.mjs';
import { bud, thLong, thShort, thRangeLong, table, bullets, numbered } from './lib.mjs'; import { bud, thLong, thShort, thRangeLong, table, bullets, numbered } from './lib.mjs';
import { workProduct, head } from './format.mjs'; import { workProduct, head } from './format.mjs';
@@ -72,8 +73,12 @@ export function softwareDesign() {
]), ]),
body: [ body: [
'## HIGH LEVEL DESIGN', '## HIGH LEVEL DESIGN',
'### High Level Architecture Diagram',
'ภาพรวมสถาปัตยกรรมของระบบ แบ่งเป็นผู้ใช้งาน ชั้น Client, Web, Data, Service และบริการภายนอก พร้อมช่องทางการเชื่อมต่อระหว่างกัน ทุกคำขอจากหน้าจอผ่านการตรวจสอบของ db_auth ก่อนถึงตรรกะธุรกิจ',
figure('architecture'),
'### Use Case Diagram', '### Use Case Diagram',
'ผู้ใช้งานของระบบและกลุ่มการใช้งานหลัก', 'ผู้ใช้งานของระบบและกลุ่มการใช้งานหลัก บทบาทที่สูงกว่าได้รับสิทธิ์การใช้งานของบทบาทที่ต่ำกว่าทั้งหมด (Viewer → Staff → Admin → Owner)',
figure('useCase'),
table(['Actor', 'คำอธิบาย', 'การใช้งานหลัก'], [ table(['Actor', 'คำอธิบาย', 'การใช้งานหลัก'], [
['Owner', 'เจ้าของบริษัท สิทธิ์สูงสุดภายในบริษัท', 'ตั้งค่าบริษัท จัดการผู้ใช้ และใช้งานทุกโมดูล'], ['Owner', 'เจ้าของบริษัท สิทธิ์สูงสุดภายในบริษัท', 'ตั้งค่าบริษัท จัดการผู้ใช้ และใช้งานทุกโมดูล'],
['Admin', 'ผู้ดูแลระบบภายในบริษัท', 'จัดการข้อมูลหลัก ผู้ใช้ สิทธิ์ และใช้งานทุกโมดูลปฏิบัติการ'], ['Admin', 'ผู้ดูแลระบบภายในบริษัท', 'จัดการข้อมูลหลัก ผู้ใช้ สิทธิ์ และใช้งานทุกโมดูลปฏิบัติการ'],
@@ -83,6 +88,8 @@ export function softwareDesign() {
['SMTP Service', 'ผู้กระทำภายนอก', 'ส่งอีเมล Onboarding กู้คืนรหัสผ่าน และแจ้งเตือน'], ['SMTP Service', 'ผู้กระทำภายนอก', 'ส่งอีเมล Onboarding กู้คืนรหัสผ่าน และแจ้งเตือน'],
]), ]),
'### Component Diagram', '### Component Diagram',
'โมดูลของระบบ (Software Unit กลุ่ม UN01–UN13) และความสัมพันธ์การเรียกใช้ระหว่างโมดูล',
figure('component'),
'โครงสร้างส่วนประกอบของระบบแบ่งตามชั้นการทำงาน', 'โครงสร้างส่วนประกอบของระบบแบ่งตามชั้นการทำงาน',
table(['ชั้น (Layer)', 'ส่วนประกอบ', 'อ้างอิง SR'], [ table(['ชั้น (Layer)', 'ส่วนประกอบ', 'อ้างอิง SR'], [
['Presentation', 'หน้าจอ PHP ในแต่ละโมดูล (`app/<module>/`), JavaScript และ CSS ที่รองรับ Responsive', 'SR02:002, SR06:002, SR06:003'], ['Presentation', 'หน้าจอ PHP ในแต่ละโมดูล (`app/<module>/`), JavaScript และ CSS ที่รองรับ Responsive', 'SR02:002, SR06:002, SR06:003'],
@@ -92,6 +99,8 @@ export function softwareDesign() {
['External', 'บริการ SMTP สำหรับส่งอีเมล และ Socket.IO Client บน Browser', 'SR06:004, SR06:005'], ['External', 'บริการ SMTP สำหรับส่งอีเมล และ Socket.IO Client บน Browser', 'SR06:004, SR06:005'],
]), ]),
'### Deployment Diagram', '### Deployment Diagram',
'การติดตั้งบนสภาพแวดล้อมใช้งานจริงด้วย Docker Compose และทางเลือกการติดตั้งแบบ Manual',
figure('deployment'),
'การติดตั้งระบบแบ่งเป็น 4 ชั้นบริการ', 'การติดตั้งระบบแบ่งเป็น 4 ชั้นบริการ',
table(['ชั้นการติดตั้ง (Tier)', 'ส่วนประกอบ', 'อ้างอิง SR'], [ table(['ชั้นการติดตั้ง (Tier)', 'ส่วนประกอบ', 'อ้างอิง SR'], [
['Client', 'Web Browser (Chrome/Edge/Firefox) บน Desktop, Tablet และ Mobile พร้อม Socket.IO Client', 'SR06:002, SR06:003, SR06:005'], ['Client', 'Web Browser (Chrome/Edge/Firefox) บน Desktop, Tablet และ Mobile พร้อม Socket.IO Client', 'SR06:002, SR06:003, SR06:005'],
@@ -100,7 +109,7 @@ export function softwareDesign() {
['Service Tier', 'Node.js `server.js` และ `scheduler.js` ควบคุมด้วย pm2', 'SR03:011, SR06:004'], ['Service Tier', 'Node.js `server.js` และ `scheduler.js` ควบคุมด้วย pm2', 'SR03:011, SR06:004'],
]), ]),
'### User Interface Design', '### User Interface Design',
'ผังหน้าจอหลักของระบบแบ่งตามโมดูลการใช้งาน', 'ผังหน้าจอหลักของระบบแบ่งตามโมดูลการใช้งาน ทุกหน้าจอใช้โครงร่างเดียวกัน ได้แก่ เมนูด้านซ้ายที่แสดงตามสิทธิ์ของผู้ใช้ แถบด้านบนสำหรับเลือกบริษัทและบัญชีผู้ใช้ และพื้นที่ทำงานด้านขวา',
table(['กลุ่มหน้าจอ', 'หน้าจอ'], [ table(['กลุ่มหน้าจอ', 'หน้าจอ'], [
['Login & Onboarding', 'เข้าสู่ระบบ / ลงทะเบียนบริษัท / รับคำเชิญเข้าร่วมบริษัท / ลืมรหัสผ่าน / ยืนยัน OTP'], ['Login & Onboarding', 'เข้าสู่ระบบ / ลงทะเบียนบริษัท / รับคำเชิญเข้าร่วมบริษัท / ลืมรหัสผ่าน / ยืนยัน OTP'],
['Dashboard', 'แดชบอร์ดคลังสินค้า / แดชบอร์ดบัญชีและการเงิน'], ['Dashboard', 'แดชบอร์ดคลังสินค้า / แดชบอร์ดบัญชีและการเงิน'],
@@ -113,6 +122,14 @@ export function softwareDesign() {
['Reports', 'รายงานสต๊อก / ความเคลื่อนไหว / ความจุคลัง / สินค้าใกล้หมด / สินค้าหมดอายุ / รายงานการเงิน'], ['Reports', 'รายงานสต๊อก / ความเคลื่อนไหว / ความจุคลัง / สินค้าใกล้หมด / สินค้าหมดอายุ / รายงานการเงิน'],
['Settings', 'ข้อมูลบริษัท / SMTP / ผู้ใช้งานและสิทธิ์ / การตั้งค่าระบบ'], ['Settings', 'ข้อมูลบริษัท / SMTP / ผู้ใช้งานและสิทธิ์ / การตั้งค่าระบบ'],
]), ]),
'### Mockup Wireframe',
'Wireframe แสดงโครงร่างหน้าจอหลักที่ใช้เป็นแม่แบบในการพัฒนา หน้าจอเอกสารขาย จัดซื้อ และการเงินใช้รูปแบบหน้ารายการ (รูปที่ 7) และหน้าบันทึก (รูปที่ 8) เดียวกัน',
figure('wfLogin'),
figure('wfDashboard'),
figure('wfList'),
figure('wfDocument'),
figure('wfStockIn'),
figure('wfReport'),
'## Software Baseline', '## Software Baseline',
'หัวข้อนี้ระบุสิ่งที่ถูกกำหนดเป็น Baseline ของซอฟต์แวร์ วันที่กำหนด และผู้อนุมัติ เพื่อให้ตรวจสอบเส้นทาง Baseline → Configuration → Change ได้ในเอกสารชุดเดียวกัน', 'หัวข้อนี้ระบุสิ่งที่ถูกกำหนดเป็น Baseline ของซอฟต์แวร์ วันที่กำหนด และผู้อนุมัติ เพื่อให้ตรวจสอบเส้นทาง Baseline → Configuration → Change ได้ในเอกสารชุดเดียวกัน',
table(['รายการที่กำหนดเป็น Baseline', 'ค่าที่ควบคุม', 'ที่จัดเก็บ'], [ table(['รายการที่กำหนดเป็น Baseline', 'ค่าที่ควบคุม', 'ที่จัดเก็บ'], [
@@ -128,7 +145,7 @@ export function softwareDesign() {
['ผู้อนุมัติ', `${P.SeV.th} (Project Sponsor) พร้อมการตรวจรับเมื่อ ${thLong(ACCEPT_DATE)}`], ['ผู้อนุมัติ', `${P.SeV.th} (Project Sponsor) พร้อมการตรวจรับเมื่อ ${thLong(ACCEPT_DATE)}`],
['เอกสารควบคุมที่เกี่ยวข้อง', 'Software Configuration (WP 8) ระบุรายการ Configuration Item และการควบคุมเวอร์ชัน'], ['เอกสารควบคุมที่เกี่ยวข้อง', 'Software Configuration (WP 8) ระบุรายการ Configuration Item และการควบคุมเวอร์ชัน'],
['การสอบกลับ', 'Traceability Record (WP 13) เชื่อมโยงความต้องการกับ Software Unit และ Test Case บน Baseline นี้'], ['การสอบกลับ', 'Traceability Record (WP 13) เชื่อมโยงความต้องการกับ Software Unit และ Test Case บน Baseline นี้'],
['การเปลี่ยนแปลง Baseline', 'ต้องผ่าน Change Report (CH-001–CH-003) และบันทึกผลใน Correction Register เมื่อเป็นการแก้ไขข้อบกพร่อง'], ['การเปลี่ยนแปลง Baseline', 'ต้องพิจารณาตามเกณฑ์ใน Change Report และบันทึกผลใน Correction Register เมื่อเป็นการแก้ไขข้อบกพร่อง'],
]), ]),
'## Software Unit', '## Software Unit',
table(['ID', 'Description', 'Functional Interfaces Detail', 'References', 'Files'], unitRows), table(['ID', 'Description', 'Functional Interfaces Detail', 'References', 'Files'], unitRows),
@@ -143,12 +160,16 @@ export function traceabilityRecord() {
const date = '2026-03-06'; const date = '2026-03-06';
const rows = []; const rows = [];
let group = ''; let group = '';
for (const [id, text, remark] 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.tc]); rows.push([id, text, t.sr.join(', '), t.sr.map(srTopic).join(' / '), t.un.join(', ') || '-', t.un.map(unitTopic).join(' / ') || '-', t.tc, t.ev || '-']);
} }
const traced = CRS.map(([id]) => traceOf(id));
const withUnits = traced.filter((t) => t.un.length).length;
const withTests = traced.filter((t) => t.tcs.length).length;
const byDocument = 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`,
@@ -164,13 +185,16 @@ export function traceabilityRecord() {
'## วัตถุประสงค์ (Objective)', '## วัตถุประสงค์ (Objective)',
'เอกสารที่เชื่อมโยงทุกขั้นตอนของโครงการ ตั้งแต่ความต้องการของลูกค้า → ความต้องการซอฟต์แวร์ → การออกแบบ → การทดสอบ เพื่อยืนยันความครบถ้วนของ Customer Requirements และลดความเสี่ยงของข้อบกพร่องและความต้องการที่ตกหล่น', 'เอกสารที่เชื่อมโยงทุกขั้นตอนของโครงการ ตั้งแต่ความต้องการของลูกค้า → ความต้องการซอฟต์แวร์ → การออกแบบ → การทดสอบ เพื่อยืนยันความครบถ้วนของ Customer Requirements และลดความเสี่ยงของข้อบกพร่องและความต้องการที่ตกหล่น',
'## Traceability Matrix', '## Traceability Matrix',
table(['CR ID', 'CR Topic', 'SRS ID', 'SRS Topic', 'Unit ID', 'Unit Topic', 'Test Case ID'], rows), table(['CR ID', 'CR Topic', 'SRS ID', 'SRS Topic', 'Unit ID', 'Unit Topic', 'Test Case ID', 'หลักฐานอื่น (Document)'], rows),
'หมายเหตุ: ความต้องการที่เป็นเอกสาร การอบรม หรือหลักฐานด้านคุณภาพ สอบกลับไปยังเอกสารส่งมอบที่เป็นหลักฐานในคอลัมน์ "หลักฐานอื่น" แทน Software Unit',
'## สรุปความครอบคลุม', '## สรุปความครอบคลุม',
table(['รายการ', 'จำนวน'], [ table(['รายการ', 'จำนวน'], [
['ความต้องการของลูกค้าทั้งหมด (CR01–CR14)', CRS.length], ['ความต้องการของลูกค้าทั้งหมด (CR01–CR14)', CRS.length],
['เชื่อมโยงกับความต้องการซอฟต์แวร์ (SRS)', CRS.length], ['เชื่อมโยงกับความต้องการซอฟต์แวร์ (SRS)', CRS.length],
['เชื่อมโยงกับ Software Unit', CRS.length], ['เชื่อมโยงกับ Software Unit', withUnits],
['เชื่อมโยงกับ Test Case', CRS.length], ['เชื่อมโยงกับ Test Case', withTests],
['ยืนยันด้วยเอกสารส่งมอบ (ไม่มี Software Unit)', byDocument],
['มีหลักฐานยืนยันครบ (Test Case หรือเอกสาร)', CRS.length],
['ความต้องการซอฟต์แวร์ทั้งหมด (SR01–SR09)', SRS.length], ['ความต้องการซอฟต์แวร์ทั้งหมด (SR01–SR09)', SRS.length],
['Software Unit ทั้งหมด', UNITS.length], ['Software Unit ทั้งหมด', UNITS.length],
['Test Case ทั้งหมด', TEST_CASES.length], ['Test Case ทั้งหมด', TEST_CASES.length],
@@ -275,9 +299,11 @@ export function testCases() {
/** 16. Test Report. */ /** 16. Test Report. */
export function testReport() { export function testReport() {
const date = '2026-08-14'; const date = '2026-08-14';
const rows = CRS.map(([id, text], i) => { const tested = CRS.filter(([id]) => traceOf(id).tcs.length);
const byDocument = CRS.length - tested.length;
const rows = tested.map(([id, text], i) => {
const t = traceOf(id); const t = traceOf(id);
return [i + 1, t.tc, `ตรวจสอบ${unitTopic(t.un[0])}`, id, text, 'Passed', thRangeLong(TEST_FROM, TEST_TO)]; return [i + 1, t.tc, `ตรวจสอบ ${t.un.map(unitTopic).join(' / ')}`, id, text, 'Passed', thRangeLong(TEST_FROM, TEST_TO)];
}); });
return { return {
dir: '2-SI Process (12 Work Product)/16.Test Report', dir: '2-SI Process (12 Work Product)/16.Test Report',
@@ -312,6 +338,7 @@ export function testReport() {
], ['', '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 Testing', 'Customer Req. ID', 'Customer Req. Topic', 'Status', 'Test Date'], rows, ['c', 'c', '', 'c', '', 'c', 'c']),
`ความต้องการที่ยืนยันด้วย 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', `ทดสอบตาม Customer Requirements ผ่านทั้งหมด ${TEST_CASES.length} Test Case คิดเป็น 100%`],
@@ -358,7 +385,7 @@ export function software() {
[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, 'ผลการทดสอบระบบ', `Test Report ${TEST_CASES.length} Test Case ผ่านทั้งหมด`],
[4, 'ผลการทดสอบการยอมรับ', 'Validation Results 12 สถานการณ์ ผ่านทั้งหมด'], [4, 'ผลการทดสอบการยอมรับ', `Validation Results ครบ 14 หมวด ${CRS.length} รายการ ผ่านทั้งหมด`],
[5, 'การตรวจรับส่งมอบ', `Acceptance Report ผลการตรวจรับ Accepted เมื่อ ${thLong(ACCEPT_DATE)}`], [5, 'การตรวจรับส่งมอบ', `Acceptance Report ผลการตรวจรับ Accepted เมื่อ ${thLong(ACCEPT_DATE)}`],
[6, 'คู่มือประกอบการใช้งาน', 'Software User Document, Product Operation Guide, Maintenance Document'], [6, 'คู่มือประกอบการใช้งาน', 'Software User Document, Product Operation Guide, Maintenance Document'],
], ['c']), ], ['c']),
+62 -45
View File
@@ -1,7 +1,7 @@
// 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, 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 } from './engineering.mjs';
import { CORRECTIONS, CHANGES } 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';
@@ -243,12 +243,7 @@ export function maintenanceDocumentation() {
/** 21. Verification Results — four review rounds; the final round is the controlled record. */ /** 21. Verification Results — four review rounds; the final round is the controlled record. */
export function verificationResults() { export function verificationResults() {
const ROUNDS = [ const ROUNDS = VERIFICATION_ROUNDS;
{ v: 'V0.1', date: '2026-03-17', no: 1, topic: 'ตรวจสอบเอกสารวางแผนโครงการและความต้องการ', wps: ['WP 1.0', 'WP 2.0'] },
{ v: 'V0.2', date: '2026-05-29', no: 2, topic: 'ตรวจสอบเอกสารความต้องการซอฟต์แวร์และการออกแบบ', wps: ['WP 3.0', 'WP 4.0', 'WP 5.0'] },
{ v: 'V0.3', date: '2026-07-31', no: 3, topic: 'ตรวจสอบชุดทดสอบและการสอบกลับ', wps: ['WP 6.0'] },
{ v: 'V1.0', date: '2026-08-17', no: 4, 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'] },
];
const CHECKS = { const CHECKS = {
'WP 1.0': ['Software Project Plan', [ 'WP 1.0': ['Software Project Plan', [
['รายละเอียดและความถูกต้องของ Header ของเอกสาร', 'มีรายละเอียดใน Header ครบถ้วน'], ['รายละเอียดและความถูกต้องของ Header ของเอกสาร', 'มีรายละเอียดใน Header ครบถ้วน'],
@@ -279,8 +274,8 @@ export function verificationResults() {
], 'NoC'], ], 'NoC'],
'WP 4.0': ['Software Design', [ 'WP 4.0': ['Software Design', [
['รายละเอียดและความถูกต้องของ Header ของเอกสาร', 'มีรายละเอียดครบถ้วนถูกต้อง'], ['รายละเอียดและความถูกต้องของ Header ของเอกสาร', 'มีรายละเอียดครบถ้วนถูกต้อง'],
['ใน High Level Design มี Use Case, Component และ Deployment Diagram', 'มีครบทั้ง 3 ส่วน'], ['ใน High Level Design มี Architecture, Use Case, Component และ Deployment Diagram', 'มีแผนภาพครบทั้ง 4 แบบ'],
['ใน User Interface Design มีผังหน้าจอที่จะใช้ในระบบ', 'มีผังหน้าจอครบทุกกลุ่มโมดูล'], ['ใน User Interface Design มีผังหน้าจอที่จะใช้ในระบบ', 'มีผังหน้าจอครบทุกกลุ่มโมดูล พร้อม Mockup Wireframe ของหน้าจอหลัก 6 รูปแบบ'],
['มีหัวข้อ Software Baseline ระบุสิ่งที่ถูกกำหนดเป็น Baseline วันที่ และผู้อนุมัติ', 'มีหัวข้อ Software Baseline ครบถ้วน พร้อมอ้างอิง Software Configuration และ Traceability Record'], ['มีหัวข้อ Software Baseline ระบุสิ่งที่ถูกกำหนดเป็น Baseline วันที่ และผู้อนุมัติ', 'มีหัวข้อ Software Baseline ครบถ้วน พร้อมอ้างอิง Software Configuration และ Traceability Record'],
['ใน Software Unit มีรหัสกำกับแต่ละหน่วย เช่น UN01.001 อย่างเป็นระเบียบ', 'มีรหัสครบ 45 หน่วย'], ['ใน Software Unit มีรหัสกำกับแต่ละหน่วย เช่น UN01.001 อย่างเป็นระเบียบ', 'มีรหัสครบ 45 หน่วย'],
['ใน Software Unit มี Description และ Functional Interfaces Detail ครบทุกรายการ', 'มีรายละเอียดครบทุกหน่วย'], ['ใน Software Unit มี Description และ Functional Interfaces Detail ครบทุกรายการ', 'มีรายละเอียดครบทุกหน่วย'],
@@ -288,10 +283,10 @@ export function verificationResults() {
['ลงชื่อผู้จัดทำและผู้อนุมัติเรียบร้อย', 'มีตารางลงนามครบถ้วน'], ['ลงชื่อผู้จัดทำและผู้อนุมัติเรียบร้อย', 'มีตารางลงนามครบถ้วน'],
], 'NoC'], ], 'NoC'],
'WP 5.0': ['Change Report', [ 'WP 5.0': ['Change Report', [
['รายละเอียดและความถูกต้องของ Header ของเอกสาร', 'มีรายละเอียดครบทั้ง 3 ฉบับ'], ['รายละเอียดและความถูกต้องของ Header ของเอกสาร', 'มีรายละเอียดครบถ้วนถูกต้อง'],
['ส่วน Requesting เขียนรายละเอียดถูกต้องครบถ้วนทุกฉบับ', 'มีรายละเอียดครบทุกฉบับ'], ['ระบุเกณฑ์การพิจารณาว่ารายการใดเป็นคำขอเปลี่ยนแปลง', 'มีเกณฑ์ 3 ข้อ ครอบคลุมความจำเป็นต่อการส่งมอบ ขอบเขต และ Man-day'],
['ส่วน Impact Analysis ได้รับการประเมินผลกระทบครบทุกฉบับ', 'มีการประเมินผลกระทบ 5 ด้านครบทุกฉบับ'], ['รายการปรับปรุงที่เสนอได้รับการพิจารณาตามเกณฑ์และบันทึกผลครบทุกรายการ', 'มีผลการพิจารณาและการดำเนินการครบทุกรายการ'],
['มีการลงนามอนุมัติโดยคณะพิจารณา (CAB) ครบทุกฉบับ', 'มีตารางพิจารณาและลงนามครบถ้วน'], ['ลงชื่อผู้จัดทำ ผู้ตรวจสอบ และผู้อนุมัติครบถ้วน', 'มีตารางลงนามครบถ้วน'],
], 'ApS'], ], 'ApS'],
'WP 6.0': ['Test Case and Test Procedures', [ 'WP 6.0': ['Test Case and Test Procedures', [
['รายละเอียดและความถูกต้องของ Header ของเอกสาร', 'มีรายละเอียดครบถ้วนถูกต้อง'], ['รายละเอียดและความถูกต้องของ Header ของเอกสาร', 'มีรายละเอียดครบถ้วนถูกต้อง'],
@@ -305,8 +300,9 @@ export function verificationResults() {
], 'PaNg'], ], 'PaNg'],
'WP 7.0': ['Validation Results', [ 'WP 7.0': ['Validation Results', [
['รายละเอียดและความถูกต้องของ Header ของเอกสาร', 'มีรายละเอียดครบถ้วนถูกต้อง'], ['รายละเอียดและความถูกต้องของ Header ของเอกสาร', 'มีรายละเอียดครบถ้วนถูกต้อง'],
['ระบุสถานการณ์ทดสอบการยอมรับครบตามความต้องการของลูกค้า', 'มี 12 สถานการณ์ครอบคลุมทุกระบบงาน'], ['ใช้ Customer Requirements เป็นตัวตั้งของการทดสอบการยอมรับ', `ครอบคลุมครบ 14 หมวด ${CRS.length} รายการ`],
['ระบุผลการทดสอบและผู้ทดสอบครบทุกรายการ', 'มีผลและผู้ทดสอบครบทุกสถานการณ์'], ['ระบุวิธีการยืนยันและหลักฐานอ้างอิงของแต่ละความต้องการ', 'มีวิธีการยืนยันและหลักฐานครบทุกรายการ'],
['ระบุผลการทดสอบและผู้ทดสอบครบทุกรายการ', 'มีผลและผู้ทดสอบครบทุกรายการ'],
['ลงชื่อผู้ทดสอบฝ่ายลูกค้าเรียบร้อย', 'มีตารางลงนามครบถ้วน'], ['ลงชื่อผู้ทดสอบฝ่ายลูกค้าเรียบร้อย', 'มีตารางลงนามครบถ้วน'],
], 'PaNg'], ], 'PaNg'],
'WP 8.0': ['Software User Document', [ 'WP 8.0': ['Software User Document', [
@@ -352,21 +348,17 @@ export function verificationResults() {
rows: head('Verification Results', `${bud(r.date)} ${r.v} PaNg`, [ rows: head('Verification Results', `${bud(r.date)} ${r.v} PaNg`, [
['Title', 'บันทึกการตรวจสอบตามข้อกำหนดของมาตรฐาน'], ['Title', 'บันทึกการตรวจสอบตามข้อกำหนดของมาตรฐาน'],
['รอบตรวจสอบ', `รอบที่ ${r.no} (${thLong(r.date)})`, 'หัวข้อ', r.topic], ['รอบตรวจสอบ', `รอบที่ ${r.no} (${thLong(r.date)})`, 'หัวข้อ', r.topic],
['เวลาตรวจสอบ', r.time, 'ระยะเวลา', `${r.hours} ชั่วโมง`],
['Organizer', ORGANIZER], ['Organizer', ORGANIZER],
['Recorder', RECORDER], ['Recorder', RECORDER],
]), ]),
body: [ body: [
'## วัตถุประสงค์ (Objective)', '## วัตถุประสงค์ (Objective)',
'การตรวจสอบ (Verification) จัดทำขึ้นเพื่อยืนยันความถูกต้องและความครบถ้วนของสิ่งส่งมอบ (Work Products) ตามมาตรฐาน ISO/IEC 29110 โดยวางแผนตรวจสอบเป็นรอบระหว่างการดำเนินโครงการ และตรวจสอบรอบสุดท้ายก่อนส่งมอบระบบ เพื่อลดความเสี่ยงด้านคุณภาพ ความล่าช้า และค่าใช้จ่ายที่เกิดจากการแก้ไขย้อนหลัง', `การตรวจสอบ (Verification) จัดทำขึ้นเพื่อยืนยันความถูกต้องและความครบถ้วนของสิ่งส่งมอบ (Work Products) ตามมาตรฐาน ISO/IEC 29110 ตามแผนการตรวจสอบในหัวข้อ 8.1 ของ Software Project Plan ซึ่งกำหนดให้ตรวจสอบ ${ROUNDS.length} รอบ ประมาณทุก 2 เดือน และรอบสุดท้ายก่อนส่งมอบระบบ เพื่อลดความเสี่ยงด้านคุณภาพ ความล่าช้า และค่าใช้จ่ายที่เกิดจากการแก้ไขย้อนหลัง การตรวจสอบรอบนี้เป็นรอบที่ ${r.no} ใช้เวลา ${r.hours} ชั่วโมง (${r.time})`,
'## Deliverables under Review', '## Deliverables under Review',
table(['รหัส', 'สิ่งส่งมอบที่ตรวจสอบในรอบนี้'], r.wps.map((wp) => [wp, CHECKS[wp][0]]), ['c']), table(['รหัส', 'สิ่งส่งมอบที่ตรวจสอบในรอบนี้'], r.wps.map((wp) => [wp, CHECKS[wp][0]]), ['c']),
'## Risk & Constraints Note', '## Risk & Constraints Note',
numbered([ numbered(VERIFICATION_RULES),
'เจ้าหน้าที่ควบคุมเอกสารต้องตรวจสอบแต่ละรายการอย่างละเอียด และต้องมีหลักฐานเพียงพอที่จะสรุปผลว่าผ่าน',
'ผู้ปฏิบัติงานที่เกี่ยวข้องควรร่วมรับฟังผลการตรวจสอบ เพื่อให้แก้ไขได้ทันทีเมื่อพบประเด็น',
'การตรวจสอบแต่ละครั้งไม่ควรถูกขัดจังหวะ ซึ่งจะทำให้การตรวจสอบเลื่อนออกไป',
'ห้ามให้ผู้อื่นตรวจสอบแทนผู้ที่ได้รับมอบหมาย',
]),
'## ผลการตรวจสอบ', '## ผลการตรวจสอบ',
table(['ID', 'Verification Item', 'Evidence', 'Result', 'Owner', 'Comment / Risk / Recommendation'], rows), table(['ID', 'Verification Item', 'Evidence', 'Result', 'Owner', 'Comment / Risk / Recommendation'], rows),
'## สรุปผลการตรวจสอบรอบนี้', '## สรุปผลการตรวจสอบรอบนี้',
@@ -386,24 +378,45 @@ export function verificationResults() {
}); });
} }
/** 22. Validation Results — customer UAT. */ /** 22. Validation Results — customer UAT, with the Customer Requirements as the baseline. */
export function validationResults() { export function validationResults() {
const date = '2026-08-14'; const date = '2026-08-14';
const SCENARIOS = [ // How the customer confirmed each requirement group during the UAT on 10–14 Aug.
['การลงทะเบียนและกำหนดสิทธิ์ผู้ใช้งาน', ['CR01:001', 'CR01:002', 'CR01:003', 'CR01:004'], 'ผู้ใช้เข้าสู่บริษัทที่ถูกต้องและเห็นเฉพาะเมนูที่ได้รับสิทธิ์'], const METHOD = {
['การตั้งค่าข้อมูลหลักคลังสินค้าและสินค้า', ['CR01:005', 'CR01:006'], 'ตั้งค่าคลังสินค้า พื้นที่ ช่องจัดเก็บ และสินค้าได้ครบตามโครงสร้างที่ใช้จริง'], CR01: 'ทดลองใช้งานตามขั้นตอนธุรกิจจริงบนระบบ',
['การรับสินค้าเข้าคลัง', ['CR01:007'], 'บันทึกรับเข้าแล้วยอดคงเหลือและความเคลื่อนไหวถูกต้อง'], CR02: 'ใช้งานประจำวันบนสภาพแวดล้อมจริงและสังเกตเวลาตอบสนอง',
['การจ่ายสินค้าออกจากคลัง', ['CR01:008'], 'จ่ายออกภายในยอดคงเหลือได้ และระบบปฏิเสธการจ่ายเกินยอด'], CR03: 'ใช้งานผ่าน Browser ที่กำหนด รับอีเมลและการแจ้งเตือนจริง',
['การโอนย้ายสินค้าระหว่างตำแหน่ง', ['CR01:009'], 'ยอดต้นทางและปลายทางสมดุลและสอบกลับได้เป็นรายการเดียว'], CR04: 'ตรวจสอบสภาพแวดล้อมที่ติดตั้งและการเข้าสู่ระบบ',
['การติดตาม Lot, Serial Number และวันหมดอายุ', ['CR01:010', 'CR01:012'], 'ข้อมูลติดตามคงอยู่และแสดงในรายงานที่เกี่ยวข้อง พร้อมพิมพ์บาร์โค้ดได้'], CR05: 'ทดลองใช้งานบน Desktop และ Tablet/Mobile',
['กระบวนการขาย', ['CR01:013'], 'ใบเสนอราคา ใบสั่งขาย ใบแจ้งหนี้ และใบรับคืน ทำงานตามลำดับสถานะที่กำหนด'], CR06: 'ตรวจสอบ HTTPS การจำกัดสิทธิ์ และการเข้าสู่ระบบซ้ำซ้อน',
['กระบวนการจัดซื้อ', ['CR01:014'], 'ใบขอซื้อ ใบสั่งซื้อ ใบแจ้งหนี้ซื้อ และใบคืนผู้ขาย ทำงานตามลำดับสถานะที่กำหนด'], CR07: 'ตรวจสอบสภาพแวดล้อมจริง เขตเวลา และการแยกข้อมูลสาธิต',
['งานการเงินและบัญชี', ['CR01:015', 'CR01:016'], 'ใบเสร็จ ใบสำคัญจ่าย และรายการบัญชีสมดุลและตรวจสอบได้'], CR08: 'ตรวจสอบการสำรอง/กู้คืน การเฝ้าระวัง และการแยกข้อมูลหลายบริษัท',
['รายงานคลังสินค้าและรายงานการเงิน', ['CR01:011', 'CR01:017', 'CR01:020'], 'รายงานแสดงผลตรงตามตัวกรองและขอบเขตสิทธิ์'], CR09: 'ทบทวนโครงสร้างซอฟต์แวร์และเอกสารการบำรุงรักษา',
['การแจ้งเตือนและงานตามกำหนดเวลา', ['CR01:021', 'CR01:022'], 'ผู้ใช้ที่เกี่ยวข้องได้รับการแจ้งเตือนโดยไม่ซ้ำซ้อน'], CR10: 'สาธิตการติดตั้งตามขั้นตอนในคู่มือ',
['การแยกข้อมูลระหว่างบริษัทและคลังสินค้า', ['CR01:024', 'CR08:004'], 'การเข้าถึงข้อมูลข้ามบริษัทหรือคลังที่ไม่ได้รับสิทธิ์ถูกปฏิเสธ'], CR11: 'ทบทวนคู่มือ แผนการอบรม และช่องทางสนับสนุน',
]; CR12: 'ตรวจสอบเทคโนโลยี การควบคุม Source Code และเอกสารตามมาตรฐาน',
const rows = SCENARIOS.map((s, i) => [i + 1, s[1].map((c) => traceOf(c).tc).join(', '), s[1].join(', '), s[0], s[2], 'Passed', P.SeV.id]); CR13: 'ทดลองทำรายการที่ล้มเหลวและตรวจสอบข้อความแจ้งข้อผิดพลาด',
CR14: 'ทบทวนผลการทดสอบ การสอบกลับ และผลการตรวจสอบ',
};
// Evidence the customer had in hand at the UAT, where it differs from the trace record.
const AT_UAT = {
'CR11:003': 'แผนการอบรมก่อนเปิดใช้งาน (Task 5.2)',
'CR12:004': 'Verification Results รอบที่ 1–3',
'CR14:003': 'ผลการทดสอบการยอมรับในเอกสารฉบับนี้',
};
const evidence = (id) => {
const t = traceOf(id);
const ev = AT_UAT[id] ?? t.ev;
return [t.tcs.length ? t.tc : '', ev].filter(Boolean).join(' / ');
};
const rows = [];
const summary = [];
for (const [gid, gname, items] of CR_GROUPS) {
rows.push([gid, `**${gname}**`, '', '', '', '']);
for (const [id, text] of items) rows.push([id, text, METHOD[gid], evidence(id), 'Passed', P.SeV.id]);
summary.push([`${gid} ${gname}`, items.length, items.length, 0]);
}
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',
name: `${CODE} Validation Results ${bud(date)} V1.0 PaNg`, name: `${CODE} Validation Results ${bud(date)} V1.0 PaNg`,
@@ -418,25 +431,29 @@ export function validationResults() {
]), ]),
body: [ body: [
'## วัตถุประสงค์ (Objective)', '## วัตถุประสงค์ (Objective)',
'เพื่อยืนยันว่าระบบที่พัฒนาตรงตามความต้องการของลูกค้า (Customer Requirements) สามารถใช้งานได้จริง มีประสิทธิภาพ มีความปลอดภัย และพร้อมเปิดใช้งานจริง (Go-Live)', 'เพื่อยืนยันว่าระบบที่พัฒนาตรงตามความต้องการของลูกค้า (Customer Requirements) สามารถใช้งานได้จริง มีประสิทธิภาพ มีความปลอดภัย และพร้อมเปิดใช้งานจริง (Go-Live) โดยใช้ความต้องการของลูกค้าทุกรายการเป็นตัวตั้งของการทดสอบการยอมรับ และอ้างอิง Test Case หรือเอกสารส่งมอบเป็นหลักฐานประกอบ',
'## สภาพแวดล้อมและผู้ทดสอบ', '## สภาพแวดล้อมและผู้ทดสอบ',
table(['หัวข้อ', 'รายละเอียด'], [ table(['หัวข้อ', 'รายละเอียด'], [
['สภาพแวดล้อมที่ใช้ทดสอบ', 'สภาพแวดล้อมใช้งานจริงของลูกค้า'], ['สภาพแวดล้อมที่ใช้ทดสอบ', 'สภาพแวดล้อมใช้งานจริงของลูกค้า'],
['ผู้ทดสอบ', `${P.SeV.th} (Project Sponsor / ตัวแทนลูกค้า)`], ['ผู้ทดสอบ', `${P.SeV.th} (Project Sponsor / ตัวแทนลูกค้า)`],
['ผู้บันทึกผล', `${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)`],
['สถานะ Repository ที่ใกล้เคียงที่สุด', `Commit \`${TEST_STATE_COMMIT}\``], ['สถานะ Repository ที่ใกล้เคียงที่สุด', `Commit \`${TEST_STATE_COMMIT}\``],
]), ]),
'## ตารางสรุป Validation Result', '## ตารางสรุป Validation Result ตาม Customer Requirements',
table(['No', 'Test Case', 'CR ID', 'Customer Req. Topic', 'ผลลัพธ์ที่คาดหวัง', 'Status', 'Tester'], rows, ['c', '', '', '', '', 'c', 'c']), table(['CR ID', 'Customer Requirement', 'วิธีการยืนยัน', 'หลักฐานอ้างอิง', 'Status', 'Tester'], rows, ['', '', '', '', 'c', 'c']),
'## สรุปผลการทดสอบการยอมรับรายหมวด',
table(['หมวดความต้องการ', 'จำนวนรายการ', 'ผ่าน (Passed)', 'ไม่ผ่าน (Failed)'], summary, ['', 'r', 'r', 'r']),
'## สรุปผลการทดสอบการยอมรับ', '## สรุปผลการทดสอบการยอมรับ',
table(['รายการ', 'จำนวน'], [ table(['รายการ', 'จำนวน'], [
['สถานการณ์ทดสอบทั้งหมด', SCENARIOS.length], ['หมวดความต้องการที่ทดสอบ', CR_GROUPS.length],
['ผ่าน (Passed)', SCENARIOS.length], ['ความต้องการที่ทดสอบทั้งหมด', CRS.length],
['ผ่าน (Passed)', CRS.length],
['ไม่ผ่าน (Failed)', 0], ['ไม่ผ่าน (Failed)', 0],
['ค้างการทดสอบ', 0], ['ค้างการทดสอบ', 0],
], ['', 'r']), ], ['', 'r']),
`ตัวแทนลูกค้าได้ทดสอบการยอมรับครบทุกสถานการณ์บนสภาพแวดล้อมใช้งานจริง ผลการทดสอบผ่านทั้งหมด ไม่พบประเด็นที่ขัดขวางการเปิดใช้งาน จึงยืนยันความพร้อมของระบบสำหรับการตรวจรับส่งมอบ ซึ่งบันทึกไว้ใน Acceptance Report เมื่อ ${thLong(ACCEPT_DATE)}`, `ตัวแทนลูกค้าได้ทดสอบการยอมรับครบทุกความต้องการใน Customer Requirements บนสภาพแวดล้อมใช้งานจริง ผลการทดสอบผ่านทั้งหมด ไม่พบประเด็นที่ขัดขวางการเปิดใช้งาน จึงยืนยันความพร้อมของระบบสำหรับการตรวจรับส่งมอบ ซึ่งบันทึกไว้ใน Acceptance Report เมื่อ ${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,
}), }),
+24 -18
View File
@@ -206,7 +206,11 @@ export const TEST_CASES = [
]; ];
export const tcByUnit = (unitId) => TEST_CASES.find((t) => t[1] === unitId)?.[0] ?? ''; export const tcByUnit = (unitId) => TEST_CASES.find((t) => t[1] === unitId)?.[0] ?? '';
/** Traceability: CR id -> { sr: [...], un: [...] }. The test case is the one owned by the first unit. */ /**
* Traceability: CR id -> { sr: [...], un: [...], ev? }.
* Every unit brings its own test case. Requirements met by a delivered document or record
* (manuals, training, SLA, quality evidence) name it in `ev`, with or without software units.
*/
export const TRACE = { export const TRACE = {
'CR01:001': { sr: ['SR03:001', 'SR08:001'], un: ['UN01.001'] }, 'CR01:001': { sr: ['SR03:001', 'SR08:001'], un: ['UN01.001'] },
'CR01:002': { sr: ['SR03:001', 'SR04:001', 'SR07:005'], un: ['UN01.002'] }, 'CR01:002': { sr: ['SR03:001', 'SR04:001', 'SR07:005'], un: ['UN01.002'] },
@@ -261,43 +265,45 @@ export const TRACE = {
'CR07:004': { sr: ['SR01:003'], un: ['UN13.001'] }, 'CR07:004': { sr: ['SR01:003'], un: ['UN13.001'] },
'CR08:001': { sr: ['SR01:003'], un: ['UN13.004'] }, 'CR08:001': { sr: ['SR01:003'], un: ['UN13.004'] },
'CR08:002': { sr: ['SR01:003'], un: ['UN13.004'] }, 'CR08:002': { sr: ['SR01:003'], un: ['UN13.004'] },
'CR08:003': { sr: ['SR05:004'], un: ['UN11.002'] }, 'CR08:003': { sr: ['SR05:004'], un: [], ev: 'Product Operation Guide (การเฝ้าระวังระบบ)' },
'CR08:004': { sr: ['SR04:003'], un: ['UN12.001'] }, 'CR08:004': { sr: ['SR04:003'], un: ['UN12.001'] },
'CR08:005': { sr: ['SR05:002'], un: ['UN11.002'] }, 'CR08:005': { sr: ['SR05:002'], un: ['UN11.002'] },
'CR09:001': { sr: ['SR02:003', 'SR02:004'], un: ['UN13.001'] }, 'CR09:001': { sr: ['SR02:003', 'SR02:004'], un: [], ev: 'Software Design (Software Unit), Software Components' },
'CR09:002': { sr: ['SR01:005'], un: ['UN13.003'] }, 'CR09:002': { sr: ['SR01:005'], un: ['UN13.003'] },
'CR09:003': { sr: ['SR01:001'], un: ['UN13.004'] }, 'CR09:003': { sr: ['SR01:001'], un: [], ev: 'Maintenance Document, Correction Register' },
'CR09:004': { sr: ['SR08:005', 'SR08:004'], un: ['UN13.001'] }, 'CR09:004': { sr: ['SR08:005', 'SR08:004'], un: ['UN13.001'] },
'CR10:001': { sr: ['SR01:003', 'SR08:005'], un: ['UN13.001'] }, 'CR10:001': { sr: ['SR01:003', 'SR08:005'], un: ['UN13.001'] },
'CR10:002': { sr: ['SR02:005'], un: ['UN13.002'] }, 'CR10:002': { sr: ['SR02:005'], un: ['UN13.002'] },
'CR10:003': { sr: ['SR01:005'], un: ['UN13.003'] }, 'CR10:003': { sr: ['SR01:005'], un: ['UN13.003'] },
'CR10:004': { sr: ['SR01:001'], un: ['UN13.001'] }, 'CR10:004': { sr: ['SR01:001'], un: ['UN13.001', 'UN13.002'], ev: 'Product Operation Guide (ขั้นตอนติดตั้ง)' },
'CR11:001': { sr: ['SR01:001'], un: ['UN01.002'] }, 'CR11:001': { sr: ['SR01:001'], un: [], ev: 'Software User Document (WP 8.0)' },
'CR11:002': { sr: ['SR01:001'], un: ['UN13.001'] }, 'CR11:002': { sr: ['SR01:001'], un: [], ev: 'Product Operation Guide (WP 10.0)' },
'CR11:003': { sr: ['SR01:001'], un: ['UN01.002'] }, 'CR11:003': { sr: ['SR01:001'], un: [], ev: 'Training Report' },
'CR11:004': { sr: ['SR01:001'], un: ['UN13.004'] }, 'CR11:004': { sr: ['SR01:001'], un: [], ev: 'Maintenance Document (SLA)' },
'CR12:001': { sr: ['SR01:002'], un: ['UN13.001'] }, 'CR12:001': { sr: ['SR01:002'], un: ['UN13.001'] },
'CR12:002': { sr: ['SR01:001'], un: ['UN13.001'] }, 'CR12:002': { sr: ['SR01:001'], un: [], ev: 'Software Configuration, Project Repository' },
'CR12:003': { sr: ['SR01:004'], un: ['UN01.003'] }, 'CR12:003': { sr: ['SR01:004'], un: ['UN01.003'] },
'CR12:004': { sr: ['SR01:001'], un: ['UN13.001'] }, 'CR12:004': { sr: ['SR01:001'], un: [], ev: 'List of Evidence, Verification Results' },
'CR13:001': { sr: ['SR09:003'], un: ['UN08.002'] }, 'CR13:001': { sr: ['SR09:003'], un: ['UN08.002'] },
'CR13:002': { sr: ['SR09:003'], un: ['UN04.002'] }, 'CR13:002': { sr: ['SR09:003'], un: ['UN04.002'] },
'CR13:003': { sr: ['SR01:003'], un: ['UN13.004'] }, 'CR13:003': { sr: ['SR01:003'], un: ['UN13.004'] },
'CR13:004': { sr: ['SR09:002'], un: ['UN10.002'] }, 'CR13:004': { sr: ['SR09:002'], un: ['UN10.002'] },
'CR14:001': { sr: ['SR01:001'], un: ['UN01.002'] }, 'CR14:001': { sr: ['SR01:001'], un: [], ev: 'Test Report' },
'CR14:002': { sr: ['SR01:001'], un: ['UN01.002'] }, 'CR14:002': { sr: ['SR01:001'], un: [], ev: 'Traceability Record' },
'CR14:003': { sr: ['SR01:001'], un: ['UN01.002'] }, 'CR14:003': { sr: ['SR01:001'], un: [], ev: 'Validation Results, Acceptance Report' },
'CR14:004': { sr: ['SR07:002'], un: ['UN12.001'] }, 'CR14:004': { sr: ['SR07:002'], un: ['UN12.001', 'UN12.002'], ev: 'Test Report, Correction Register' },
}; };
export const traceOf = (crId) => { export const traceOf = (crId) => {
const t = TRACE[crId]; const t = TRACE[crId];
if (!t) throw new Error(`No traceability entry for ${crId}`); if (!t) throw new Error(`No traceability entry for ${crId}`);
return { ...t, tc: tcByUnit(t.un[0]) }; const tcs = [...new Set(t.un.map(tcByUnit))];
return { sr: t.sr, un: t.un, ev: t.ev ?? '', tcs, tc: tcs.join(', ') || '-' };
}; };
// Sanity: every CR is traced and every trace target exists. // Sanity: every CR is traced, every trace target exists, and each CR ends in a test case or named evidence.
for (const [id] of CRS) { for (const [id] of CRS) {
const t = traceOf(id); const t = traceOf(id);
for (const s of t.sr) if (!srTopic(s)) throw new Error(`${id} references unknown ${s}`); for (const s of t.sr) if (!srTopic(s)) throw new Error(`${id} references unknown ${s}`);
for (const u of t.un) if (!unitTopic(u)) throw new Error(`${id} references unknown ${u}`); for (const u of t.un) if (!unitTopic(u)) throw new Error(`${id} references unknown ${u}`);
if (!t.tc) throw new Error(`${id} has no test case`); if (t.tcs.some((tc) => !tc)) throw new Error(`${id} has a unit without a test case`);
if (!t.tcs.length && !t.ev) throw new Error(`${id} has neither a test case nor evidence`);
} }
+337
View File
@@ -0,0 +1,337 @@
// Figures for the Software Design work product: high-level diagrams and low-fidelity wireframes.
// Each figure is an SVG drawn here; scripts/sdlc-delivery/render-figures.mjs rasterises them to
// PNG with the document font (scripts/sdlc-seed/figures/*.png), which build.mjs copies next to
// the Markdown that references them.
const FONT = "'BRN Thai', 'Leelawadee UI', sans-serif";
const BLUE = '#0797d5';
const BLUE_BG = '#e8f5fb';
const INK = '#222';
const GREY = '#8e8e8e';
const LINE = '#595959';
const WF_FILL = '#f2f2f2';
const WF_DARK = '#d9d9d9';
const esc = (s) => String(s).replaceAll('&', '&amp;').replaceAll('<', '&lt;').replaceAll('>', '&gt;');
function text(x, y, value, { size = 15, weight = 400, anchor = 'middle', fill = INK, lh = 1.3 } = {}) {
const lines = Array.isArray(value) ? value : [value];
const top = y - ((lines.length - 1) * size * lh) / 2;
return lines.map((l, i) => `<text x="${x}" y="${top + i * size * lh}" font-family="${FONT}" font-size="${size}" font-weight="${weight}" text-anchor="${anchor}" dominant-baseline="middle" fill="${fill}">${esc(l)}</text>`).join('');
}
function rect(x, y, w, h, { fill = '#fff', stroke = LINE, sw = 1.5, rx = 6, dash = '' } = {}) {
return `<rect x="${x}" y="${y}" width="${w}" height="${h}" rx="${rx}" fill="${fill}" stroke="${stroke}" stroke-width="${sw}"${dash ? ` stroke-dasharray="${dash}"` : ''}/>`;
}
/** A labelled box: title centred, optional smaller subtitle lines. */
function box(x, y, w, h, title, sub = [], opts = {}) {
const { fill = '#fff', stroke = LINE, titleSize = 15, subSize = 12.5, dash = '', rx = 6 } = opts;
const subs = Array.isArray(sub) ? sub : [sub];
const total = subs.length ? titleSize * 1.4 + subs.length * subSize * 1.35 : 0;
const cy = y + h / 2;
const titleY = subs.length ? cy - total / 2 + titleSize * 0.7 : cy;
const body = subs.length ? text(x + w / 2, titleY + titleSize * 0.75 + (subs.length * subSize * 1.35) / 2, subs, { size: subSize, fill: '#444', lh: 1.35 }) : '';
return rect(x, y, w, h, { fill, stroke, rx, dash }) + text(x + w / 2, titleY, title, { size: titleSize, weight: 700 }) + body;
}
/** A container with its title in the top-left corner. */
function frame(x, y, w, h, title, { fill = BLUE_BG, stroke = BLUE, dash = '', bottom = false } = {}) {
return rect(x, y, w, h, { fill, stroke, sw: 2, rx: 10, dash }) + text(x + 14, bottom ? y + h - 18 : y + 20, title, { size: 15, weight: 700, anchor: 'start', fill: stroke === BLUE ? '#05618a' : INK });
}
function cylinder(x, y, w, h, title, sub = []) {
const ry = 12;
return `<path d="M${x},${y + ry} v${h - 2 * ry} a${w / 2},${ry} 0 0 0 ${w},0 v${-(h - 2 * ry)}" fill="#fff" stroke="${LINE}" stroke-width="1.5"/>`
+ `<ellipse cx="${x + w / 2}" cy="${y + ry}" rx="${w / 2}" ry="${ry}" fill="#fff" stroke="${LINE}" stroke-width="1.5"/>`
+ text(x + w / 2, y + h / 2 + 6, [title, ...sub].length > 1 ? [title, ...sub] : title, { size: 13.5 });
}
const defs = `<defs>
<marker id="arr" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="8" markerHeight="8" orient="auto-start-reverse"><path d="M0,0 L10,5 L0,10 z" fill="${LINE}"/></marker>
<marker id="tri" viewBox="0 0 12 12" refX="11" refY="6" markerWidth="14" markerHeight="14" orient="auto"><path d="M0,0 L12,6 L0,12 z" fill="#fff" stroke="${LINE}" stroke-width="1.2"/></marker>
<marker id="open" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="8" markerHeight="8" orient="auto"><path d="M0,0 L10,5 L0,10" fill="none" stroke="${LINE}" stroke-width="1.5"/></marker>
</defs>`;
/** Straight or elbowed connector with an optional label. `points` is [[x,y], ...]. */
function link(points, { label = '', head = 'arr', tail = '', dash = '', lx, ly, size = 12.5 } = {}) {
const d = points.map(([x, y], i) => `${i ? 'L' : 'M'}${x},${y}`).join(' ');
const path = `<path d="${d}" fill="none" stroke="${LINE}" stroke-width="1.5"${dash ? ` stroke-dasharray="${dash}"` : ''}${head ? ` marker-end="url(#${head})"` : ''}${tail ? ` marker-start="url(#${tail})"` : ''}/>`;
if (!label) return path;
const mid = Math.floor((points.length - 1) / 2);
const x = lx ?? (points[mid][0] + points[mid + 1][0]) / 2;
const y = ly ?? (points[mid][1] + points[mid + 1][1]) / 2;
const lines = Array.isArray(label) ? label : [label];
const width = Math.max(...lines.map((l) => l.length)) * size * 0.56 + 12;
const height = lines.length * size * 1.3 + 6;
return path + `<rect x="${x - width / 2}" y="${y - height / 2}" width="${width}" height="${height}" fill="#fff" rx="3"/>` + text(x, y, lines, { size, fill: '#333' });
}
function actor(x, y, label) {
return `<g stroke="${INK}" stroke-width="1.8" fill="none"><circle cx="${x}" cy="${y - 34}" r="11"/><path d="M${x},${y - 23} v30 M${x - 20},${y - 12} h40 M${x},${y + 7} l-16,24 M${x},${y + 7} l16,24"/></g>`
+ text(x, y + 50, label, { size: 14, weight: 700 });
}
function usecase(cx, cy, label, w = 230, h = 50) {
return `<ellipse cx="${cx}" cy="${cy}" rx="${w / 2}" ry="${h / 2}" fill="#fff" stroke="${BLUE}" stroke-width="1.6"/>` + text(cx, cy, label, { size: 13.5 });
}
const svg = (w, h, body) => `<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h}" viewBox="0 0 ${w} ${h}">${defs}<rect width="${w}" height="${h}" fill="#fff"/>${body}</svg>`;
// ─── High-level diagrams ──────────────────────────────────────────────────────
function architecture() {
const b = [];
b.push(frame(20, 40, 190, 300, 'ผู้ใช้งาน'));
b.push(actor(70, 140, 'Owner'), actor(160, 140, 'Admin'), actor(70, 270, 'Staff'), actor(160, 270, 'Viewer'));
b.push(frame(250, 40, 220, 300, 'Client Tier'));
b.push(box(270, 80, 180, 110, 'Web Browser', ['Chrome / Edge / Firefox', 'Desktop · Tablet · Mobile', 'หน้าจอ Responsive']));
b.push(box(270, 215, 180, 95, 'Socket.IO Client', ['รับการแจ้งเตือน', 'แบบ Real-time']));
b.push(frame(510, 20, 420, 420, 'Web Tier — Apache + PHP 8'));
b.push(box(530, 55, 380, 58, 'Page Controllers', 'app/<module>/*.php — หน้าจอและฟอร์ม'));
b.push(box(530, 125, 380, 58, 'API Engines', 'app/*/api/engine/*.php — รับคำสั่งจากหน้าจอ (JSON)'));
b.push(box(530, 195, 380, 70, 'db_auth (ประตูตรวจสอบทุกคำขอ)', ['Session · CSRF · OTP · Role Guard', 'ขอบเขตบริษัท/คลัง (Tenant Scope)'], { fill: '#fff8e5' }));
b.push(box(530, 277, 380, 70, 'Manager / Service Classes', ['ตรรกะธุรกิจ: สต๊อก ขาย จัดซื้อ การเงิน บัญชี', 'ทำงานภายใน Database Transaction']));
b.push(box(530, 359, 380, 60, 'Report / ETL Managers', 'รายงาน แดชบอร์ด และตารางสรุปยอด'));
b.push(frame(970, 40, 210, 300, 'Data Tier — MariaDB'));
b.push(cylinder(995, 80, 160, 105, 'wms', ['ผู้ใช้ / บริษัท / สิทธิ์']));
b.push(cylinder(995, 205, 160, 115, 'wms2', ['คลัง · ขาย · จัดซื้อ', 'การเงิน · บัญชี']));
b.push(frame(510, 480, 420, 190, 'Service Tier — Node.js (pm2)', { bottom: true }));
b.push(box(530, 495, 185, 130, 'server.js', ['Socket.IO', 'ส่งการแจ้งเตือน', 'ไปยังผู้ใช้ที่เกี่ยวข้อง']));
b.push(box(725, 495, 185, 130, 'scheduler.js', ['สรุปยอดสต๊อก/GL', 'แจ้งเตือนตามเวลา', 'Asia/Bangkok']));
b.push(frame(970, 480, 210, 190, 'External', { fill: '#f5f5f5', stroke: GREY }));
b.push(box(985, 515, 180, 65, 'Backup', ['Git Backup Remote', 'mysqldump รายวัน']));
b.push(box(985, 595, 180, 60, 'SMTP Server', 'อีเมล Onboarding / แจ้งเตือน'));
b.push(link([[210, 190], [250, 190]]));
b.push(link([[450, 135], [530, 135]], { label: 'HTTPS (TLS)', ly: 120 }));
b.push(link([[910, 300], [950, 300], [950, 190], [995, 190]], { label: 'PDO', lx: 950, ly: 245 }));
b.push(link([[620, 440], [620, 495]], { label: 'Emit + Secret', lx: 620, ly: 465 }));
b.push(link([[820, 495], [820, 440]], { label: 'เรียก app/cron', lx: 820, ly: 465 }));
b.push(link([[530, 590], [360, 590], [360, 310]], { label: 'WebSocket', lx: 420, ly: 590 }));
b.push(link([[910, 400], [950, 400], [950, 625], [985, 625]], { label: 'SMTP', lx: 950, ly: 470 }));
b.push(link([[1075, 320], [1075, 515]], { label: 'สำรองข้อมูล', lx: 1075, ly: 420, dash: '5 4' }));
return svg(1200, 690, b.join(''));
}
function useCase() {
const b = [];
b.push(rect(270, 20, 680, 800, { fill: '#fafcfd', stroke: BLUE, sw: 2, rx: 12 }));
b.push(text(610, 44, 'ระบบ BRN WMS', { size: 16, weight: 700, fill: '#05618a' }));
const A = { Viewer: [100, 150], Staff: [100, 400], Admin: [100, 610], Owner: [100, 765] };
// Generalisation: each role inherits the use cases of the role above it.
b.push(link([[100, 352], [100, 214]], { head: 'tri' }), link([[100, 562], [100, 464]], { head: 'tri' }), link([[100, 718], [100, 674]], { head: 'tri' }));
for (const [name, [x, y]] of Object.entries(A)) b.push(actor(x, y, name));
const UC = {
login: [460, 95, 'เข้าสู่ระบบ / กู้คืนรหัสผ่าน'],
report: [460, 165, 'ดูแดชบอร์ดและรายงาน'],
stock: [460, 255, 'รับ / จ่าย / โอนสินค้า'],
sales: [460, 320, 'จัดการเอกสารขาย'],
purchase: [460, 385, 'จัดการเอกสารจัดซื้อ'],
finance: [460, 450, 'รับชำระ / จ่ายชำระ'],
gl: [460, 515, 'สมุดรายวันและบัญชีแยกประเภท'],
master: [460, 595, 'จัดการข้อมูลหลัก'],
users: [460, 660, 'จัดการผู้ใช้และสิทธิ์'],
company: [460, 765, 'ตั้งค่าบริษัทและ SMTP'],
etl: [790, 205, 'สรุปยอดสต๊อก / GL ตามเวลา'],
alert: [790, 280, 'แจ้งเตือนสินค้าใกล้หมด / ค้างชำระ'],
notify: [790, 385, 'แจ้งเตือนแบบ Real-time'],
email: [790, 660, 'ส่งอีเมล'],
};
for (const [cx, cy, label] of Object.values(UC)) b.push(usecase(cx, cy, label));
const assoc = (actorKey, ucKey) => { const [ax, ay] = A[actorKey]; const [ux, uy] = UC[ucKey]; return link([[ax + 28, ay - 10], [ux - 115, uy]], { head: '' }); };
b.push(assoc('Viewer', 'login'), assoc('Viewer', 'report'));
for (const k of ['stock', 'sales', 'purchase', 'finance', 'gl']) b.push(assoc('Staff', k));
b.push(assoc('Admin', 'master'), assoc('Admin', 'users'), assoc('Owner', 'company'));
b.push(actor(1090, 260, 'System Scheduler'), actor(1090, 680, 'SMTP Service'));
b.push(link([[1060, 240], [905, 205]], { head: '' }), link([[1060, 250], [905, 280]], { head: '' }), link([[1060, 660], [905, 660]], { head: '' }));
b.push(link([[575, 660], [675, 660]], { dash: '6 4', head: 'open', label: '«include»', ly: 645 }));
b.push(link([[575, 95], [930, 95], [930, 660], [905, 660]], { dash: '6 4', head: 'open', label: '«include»', lx: 755, ly: 95 }));
b.push(link([[575, 320], [700, 370]], { dash: '6 4', head: 'open', label: '«include»', lx: 640, ly: 330 }));
return svg(1200, 850, b.join(''));
}
function components() {
const b = [];
const comp = (x, y, w, h, id, name, fill = '#fff') => {
const icon = `<g stroke="${LINE}" fill="#fff" stroke-width="1.2"><rect x="${x + w - 30}" y="${y + 10}" width="18" height="22"/><rect x="${x + w - 35}" y="${y + 14}" width="10" height="5"/><rect x="${x + w - 35}" y="${y + 23}" width="10" height="5"/></g>`;
return rect(x, y, w, h, { fill, rx: 4 }) + icon + text(x + w / 2 - 8, y + h / 2 - 9, `«component» ${id}`, { size: 12, fill: GREY }) + text(x + w / 2 - 8, y + h / 2 + 12, name, { size: 14, weight: 700 });
};
b.push(frame(20, 20, 1160, 120, 'ส่วนกลางที่ทุกโมดูลเรียกใช้ (Cross-cutting)'));
b.push(comp(45, 55, 350, 70, 'UN01', 'Identity & Access', '#fff'), comp(425, 55, 350, 70, 'UN12', 'Security & Tenant Scope'), comp(805, 55, 350, 70, 'UN02', 'Company Settings'));
const W = 255, H = 80, X = [45, 330, 615, 900];
b.push(frame(20, 165, 1160, 360, 'โมดูลธุรกิจ (Manager / Service Classes)', { fill: '#fff', stroke: BLUE }));
b.push(comp(X[0], 205, W, H, 'UN03', 'Master Data'), comp(X[1], 205, W, H, 'UN04', 'Inventory Control'), comp(X[2], 205, W, H, 'UN05', 'Sales'), comp(X[3], 205, W, H, 'UN06', 'Purchasing'));
b.push(comp(X[0], 420, W, H, 'UN10', 'Document Control'), comp(X[1], 420, W, H, 'UN07', 'Finance'), comp(X[2], 420, W, H, 'UN08', 'Accounting / GL'), comp(X[3], 420, W, H, 'UN09', 'Reports & Dashboard'));
b.push(frame(20, 550, 1160, 130, 'บริการและโครงสร้างพื้นฐาน', { fill: '#f5f5f5', stroke: GREY }));
b.push(comp(45, 585, 350, 75, 'UN11', 'Notification & Scheduler'), comp(425, 585, 350, 75, 'UN13', 'Deployment & Backup'));
b.push(cylinder(840, 575, 290, 95, 'MariaDB wms / wms2'));
const dep = (pts, label) => link(pts, { dash: '6 4', head: 'open', label, size: 12 });
b.push(dep([[X[1], 245], [X[0] + W, 245]], 'ใช้ข้อมูลหลัก'));
b.push(dep([[X[2], 230], [X[1] + W, 230]], 'ตัดสต๊อก'));
b.push(dep([[X[3] + 60, 205], [X[3] + 60, 180], [X[1] + W - 60, 180], [X[1] + W - 60, 205]], 'รับสินค้าเข้า'));
b.push(dep([[X[2] + 128, 285], [X[2] + 128, 420]], 'บันทึก GL'));
b.push(dep([[X[3] + 60, 285], [X[3] + 60, 360], [X[2] + 200, 360], [X[2] + 200, 420]], ''));
b.push(dep([[X[1] + W, 460], [X[2], 460]], 'บันทึก GL'));
b.push(dep([[X[3] + 180, 420], [X[3] + 180, 330], [X[1] + 200, 330], [X[1] + 200, 285]], 'อ่านยอดสต๊อก'));
b.push(dep([[X[2] + 60, 285], [X[2] + 60, 390], [X[0] + 200, 390], [X[0] + 200, 420]], 'ออกเลขที่เอกสาร'));
b.push(dep([[X[1], 480], [X[0] + W, 480]], ''));
b.push(dep([[220, 585], [220, 540], [X[3] + 128, 540], [X[3] + 128, 500]], 'ตารางสรุปยอด'));
return svg(1200, 700, b.join(''));
}
function deployment() {
const b = [];
const node = (x, y, w, h, title) => `<path d="M${x},${y + 14} l14,-14 h${w} v${h - 14} l-14,14 h${-w} z M${x},${y + 14} h${w} l14,-14 M${x + w},${y + 14} v${h - 14}" fill="#fff" stroke="${LINE}" stroke-width="1.5"/>` + text(x + 14, y + 36, title, { size: 15, weight: 700, anchor: 'start' });
b.push(node(20, 70, 220, 250, '«device» Client'));
b.push(box(40, 125, 180, 80, 'Web Browser', ['Desktop / Tablet', 'Mobile']));
b.push(box(40, 220, 180, 80, 'เครื่องสแกนบาร์โค้ด', ['(ทำงานเป็นคีย์บอร์ด)']));
b.push(node(290, 20, 600, 620, '«execution environment» Production Server — Linux + Docker Compose'));
b.push(box(320, 80, 250, 150, '«container» php', ['Apache + PHP 8', 'แอปพลิเคชัน wms-app', 'config.php สร้างจาก .env', 'พอร์ต 80 / 443 (TLS)'], { fill: BLUE_BG, stroke: BLUE }));
b.push(box(610, 80, 250, 150, '«container» node', ['pm2', 'server.js (Socket.IO :3000)', 'scheduler.js', '(ETL และแจ้งเตือน)'], { fill: BLUE_BG, stroke: BLUE }));
b.push(box(320, 290, 250, 150, '«container» db', ['MariaDB', 'ฐานข้อมูล wms และ wms2', 'Volume ข้อมูลถาวร'], { fill: BLUE_BG, stroke: BLUE }));
b.push(box(610, 290, 250, 150, 'ไฟล์ตั้งค่า', ['.env (ไม่อยู่ใน Git)', 'docker/init-env.sh', 'docker-compose.yml'], { fill: '#fff8e5' }));
b.push(box(320, 500, 540, 110, 'ทางเลือกติดตั้งแบบ Manual (LAMP)', ['Apache + PHP 8 + MariaDB บนเครื่องเดียว', 'สร้างฐานข้อมูลด้วย setup.php และรัน Node.js ผ่าน pm2'], { dash: '6 4' }));
b.push(node(950, 40, 230, 150, '«external» SMTP'));
b.push(box(965, 80, 200, 90, ' ', ['ส่งอีเมลของแต่ละบริษัท'], { stroke: 'none', fill: 'none' }));
b.push(node(950, 230, 230, 170, '«external» Git Server'));
b.push(box(965, 275, 200, 100, ' ', ['Repository หลัก (origin)', 'และ Backup Remote'], { stroke: 'none', fill: 'none' }));
b.push(node(950, 440, 230, 170, '«storage» Backup'));
b.push(box(965, 485, 200, 100, ' ', ['mysqldump รายวัน 02:00 น.', 'เก็บนอกเครื่องแม่ข่าย'], { stroke: 'none', fill: 'none' }));
b.push(link([[220, 165], [320, 165]], { label: 'HTTPS', ly: 150 }));
b.push(link([[240, 110], [270, 110], [270, 12], [735, 12], [735, 80]], { label: 'WebSocket', lx: 500, ly: 12 }));
b.push(link([[570, 150], [610, 150]], { label: 'Emit', ly: 135, size: 11.5 }));
b.push(link([[445, 230], [445, 290]], { label: 'PDO', lx: 445, ly: 260 }));
b.push(link([[860, 110], [950, 110]], { label: 'SMTP', ly: 95 }));
b.push(link([[860, 330], [950, 330]], { label: 'git push', ly: 315, dash: '5 4' }));
b.push(link([[445, 440], [445, 470], [920, 470], [920, 525], [950, 525]], { label: 'สำรองข้อมูล', lx: 700, ly: 470, dash: '5 4' }));
return svg(1200, 660, b.join(''));
}
// ─── Wireframes ──────────────────────────────────────────────────────────────
const MENU = ['แดชบอร์ด', 'ข้อมูลหลัก', 'คลังสินค้า', 'ขาย', 'จัดซื้อ', 'การเงิน', 'บัญชี', 'รายงาน', 'ตั้งค่า'];
/** Browser window + sidebar + top bar; returns the SVG and the content area. */
function shell(title, active, content) {
const W = 1200, H = 760;
const b = [];
b.push(rect(10, 10, W - 20, H - 20, { fill: '#fff', stroke: '#9a9a9a', rx: 10 }));
b.push(rect(10, 10, W - 20, 36, { fill: WF_DARK, stroke: '#9a9a9a', rx: 10 }), rect(10, 30, W - 20, 16, { fill: WF_DARK, stroke: 'none', rx: 0 }));
b.push(...[30, 50, 70].map((x) => `<circle cx="${x}" cy="28" r="6" fill="#fff" stroke="#9a9a9a"/>`));
b.push(rect(100, 18, 520, 20, { fill: '#fff', stroke: '#b5b5b5', rx: 10 }), text(115, 28, 'https://wms.brn.co.th/app/…', { size: 12, anchor: 'start', fill: GREY }));
b.push(rect(10, 46, 200, H - 56, { fill: '#e9e9e9', stroke: 'none', rx: 0 }));
b.push(rect(28, 62, 120, 30, { fill: '#fff', stroke: '#b5b5b5', rx: 4 }), text(88, 77, 'โลโก้บริษัท', { size: 12, fill: GREY }));
MENU.forEach((m, i) => {
const y = 116 + i * 40;
if (m === active) b.push(rect(18, y - 15, 184, 30, { fill: '#fff', stroke: BLUE, rx: 4 }));
b.push(rect(32, y - 7, 14, 14, { fill: m === active ? BLUE : '#bdbdbd', stroke: 'none', rx: 3 }), text(56, y, m, { size: 14, anchor: 'start', weight: m === active ? 700 : 400 }));
});
b.push(rect(210, 46, W - 220, 50, { fill: '#fff', stroke: WF_DARK, rx: 0 }));
b.push(text(232, 71, title, { size: 18, weight: 700, anchor: 'start' }));
b.push(rect(W - 330, 58, 150, 26, { fill: WF_FILL, stroke: '#b5b5b5', rx: 4 }), text(W - 255, 71, 'บริษัท ▾', { size: 12.5 }));
b.push(`<circle cx="${W - 145}" cy="71" r="13" fill="${WF_DARK}" stroke="#9a9a9a"/>`, text(W - 90, 71, 'ผู้ใช้ ▾', { size: 12.5 }));
b.push(content);
return svg(W, H, b.join(''));
}
const btn = (x, y, w, label, primary = false) => rect(x, y, w, 32, { fill: primary ? BLUE : '#fff', stroke: primary ? BLUE : '#9a9a9a', rx: 5 }) + text(x + w / 2, y + 16, label, { size: 13.2, fill: primary ? '#fff' : INK, weight: primary ? 700 : 400 });
const field = (x, y, w, label, hint = '') => text(x, y, label, { size: 12.5, anchor: 'start', fill: '#444' }) + rect(x, y + 10, w, 32, { fill: '#fff', stroke: '#b5b5b5', rx: 4 }) + (hint ? text(x + 10, y + 26, hint, { size: 12, anchor: 'start', fill: '#aaa' }) : '');
function grid(x, y, cols, rowCount, { rowH = 34, headFill = WF_DARK } = {}) {
const w = cols.reduce((a, c) => a + c[1], 0);
const b = [rect(x, y, w, rowH, { fill: headFill, stroke: '#b5b5b5', rx: 0 })];
let cx = x;
for (const [label, cw, align] of cols) { b.push(text(align === 'r' ? cx + cw - 10 : cx + 10, y + rowH / 2, label, { size: 12.5, weight: 700, anchor: align === 'r' ? 'end' : 'start' })); cx += cw; }
for (let r = 1; r <= rowCount; r += 1) {
b.push(rect(x, y + r * rowH, w, rowH, { fill: r % 2 ? '#fff' : '#fafafa', stroke: '#d0d0d0', rx: 0 }));
let px = x;
for (const [, cw, align] of cols) { const bw = Math.min(cw - 20, 40 + ((r * 37 + cw) % 60)); b.push(rect(align === 'r' ? px + cw - 10 - bw : px + 10, y + r * rowH + rowH / 2 - 5, bw, 10, { fill: WF_DARK, stroke: 'none', rx: 3 })); px += cw; }
}
return b.join('');
}
const tile = (x, y, w, label) => rect(x, y, w, 82, { fill: '#fff', stroke: '#b5b5b5', rx: 6 }) + text(x + 16, y + 24, label, { size: 13.2, anchor: 'start', fill: '#555' }) + rect(x + 16, y + 44, 80, 22, { fill: WF_DARK, stroke: 'none', rx: 4 }) + rect(x + w - 50, y + 22, 32, 32, { fill: WF_FILL, stroke: '#c8c8c8', rx: 6 });
function wfLogin() {
const b = [];
b.push(rect(10, 10, 1180, 740, { fill: '#f4f6f8', stroke: '#9a9a9a', rx: 10 }));
b.push(rect(400, 110, 400, 520, { fill: '#fff', stroke: '#b5b5b5', rx: 10 }));
b.push(rect(530, 140, 140, 50, { fill: WF_FILL, stroke: '#b5b5b5', rx: 6 }), text(600, 165, 'โลโก้', { size: 13.2, fill: GREY }));
b.push(text(600, 225, 'เข้าสู่ระบบ BRN WMS', { size: 20, weight: 700 }));
b.push(field(440, 265, 320, 'ชื่อผู้ใช้ / อีเมล', 'username'), field(440, 335, 320, 'รหัสผ่าน', '••••••••'));
b.push(rect(440, 405, 16, 16, { fill: '#fff', stroke: '#9a9a9a', rx: 3 }), text(465, 413, 'จดจำการเข้าสู่ระบบ', { size: 12.5, anchor: 'start' }));
b.push(btn(440, 440, 320, 'เข้าสู่ระบบ', true));
b.push(text(600, 500, 'ลืมรหัสผ่าน? · ลงทะเบียนบริษัทใหม่', { size: 13.2, fill: BLUE }));
b.push(rect(440, 530, 320, 70, { fill: '#fff8e5', stroke: '#e0c070', rx: 6, dash: '5 4' }), text(600, 565, ['ขั้นตอน OTP (ถ้าเปิดใช้งาน):', 'กรอกรหัส 6 หลักที่ส่งทางอีเมล'], { size: 12.5 }));
return svg(1200, 760, b.join(''));
}
function wfDashboard() {
const c = [];
['สินค้าทั้งหมด', 'รับเข้า (เดือนนี้)', 'จ่ายออก (เดือนนี้)', 'สินค้าใกล้หมด'].forEach((l, i) => c.push(tile(232 + i * 237, 116, 222, l)));
c.push(rect(232, 218, 610, 300, { fill: '#fff', stroke: '#b5b5b5', rx: 6 }), text(250, 240, 'กราฟความเคลื่อนไหวสินค้า 12 เดือน', { size: 13.5, anchor: 'start', weight: 700 }));
for (let i = 0; i < 12; i += 1) { const h1 = 40 + ((i * 53) % 150), h2 = 20 + ((i * 31) % 110); c.push(rect(262 + i * 48, 490 - h1, 16, h1, { fill: '#9fd3ea', stroke: 'none', rx: 2 }), rect(280 + i * 48, 490 - h2, 16, h2, { fill: '#c9c9c9', stroke: 'none', rx: 2 })); }
c.push(rect(860, 218, 318, 300, { fill: '#fff', stroke: '#b5b5b5', rx: 6 }), text(876, 240, 'กิจกรรมล่าสุด', { size: 13.5, anchor: 'start', weight: 700 }));
for (let i = 0; i < 6; i += 1) c.push(`<circle cx="886" cy="${280 + i * 38}" r="10" fill="${WF_DARK}"/>`, rect(906, 272 + i * 38, 170, 10, { fill: WF_DARK, stroke: 'none', rx: 3 }), rect(1110, 272 + i * 38, 50, 10, { fill: '#c9c9c9', stroke: 'none', rx: 3 }));
c.push(rect(232, 535, 946, 200, { fill: '#fff', stroke: '#b5b5b5', rx: 6 }), text(250, 557, 'สินค้าใกล้หมด / สินค้าเคลื่อนไหวมากที่สุด', { size: 13.5, anchor: 'start', weight: 700 }));
c.push(grid(250, 575, [['สินค้า', 330], ['คลัง', 230], ['คงเหลือ', 170, 'r'], ['จุดสั่งซื้อ', 180, 'r']], 4));
return shell('แดชบอร์ด', 'แดชบอร์ด', c.join(''));
}
function wfList() {
const c = [];
c.push(btn(1040, 112, 138, '+ สร้างใหม่', true));
['ทั้งหมด', 'รอกำหนดคลัง', 'ร่าง', 'ยืนยันแล้ว', 'ยกเลิก'].forEach((l, i) => c.push(tile(232 + i * 190, 158, 176, l)));
c.push(rect(232, 258, 946, 56, { fill: WF_FILL, stroke: '#c8c8c8', rx: 6 }));
c.push(rect(246, 270, 300, 32, { fill: '#fff', stroke: '#b5b5b5', rx: 4 }), text(260, 286, 'ค้นหาเลขที่เอกสาร / ผู้ติดต่อ', { size: 12, anchor: 'start', fill: '#aaa' }));
c.push(rect(560, 270, 170, 32, { fill: '#fff', stroke: '#b5b5b5', rx: 4 }), text(572, 286, 'สถานะ ▾', { size: 12.5, anchor: 'start' }));
c.push(rect(744, 270, 250, 32, { fill: '#fff', stroke: '#b5b5b5', rx: 4 }), text(756, 286, 'ช่วงวันที่ DD/MM/YYYY – DD/MM/YYYY', { size: 12, anchor: 'start', fill: '#aaa' }));
c.push(btn(1010, 270, 80, 'ค้นหา'), btn(1098, 270, 70, 'ล้าง'));
c.push(grid(232, 330, [['เลขที่', 150], ['วันที่', 120], ['ผู้ติดต่อ', 250], ['ยอดรวม', 140, 'r'], ['สถานะ', 140], ['การทำงาน', 146]], 9));
c.push(text(232, 695, 'แสดง 1–10 จาก 120 รายการ', { size: 12.5, anchor: 'start', fill: '#555' }));
['‹', '1', '2', '3', '›'].forEach((p, i) => c.push(rect(990 + i * 38, 680, 32, 30, { fill: p === '1' ? BLUE : '#fff', stroke: '#b5b5b5', rx: 4 }), text(1006 + i * 38, 695, p, { size: 13.2, fill: p === '1' ? '#fff' : INK })));
return shell('ใบสั่งขาย (หน้ารายการ)', 'ขาย', c.join(''));
}
function wfDocument() {
const c = [];
c.push(rect(232, 112, 946, 150, { fill: '#fff', stroke: '#b5b5b5', rx: 6 }), text(250, 132, 'ข้อมูลเอกสาร', { size: 13.5, anchor: 'start', weight: 700 }));
c.push(field(250, 152, 280, 'ผู้ติดต่อ *', 'ค้นหาผู้ติดต่อ…'), field(550, 152, 180, 'วันที่เอกสาร *', 'DD/MM/YYYY'), field(750, 152, 200, 'แผนก *', 'เลือกแผนก ▾'), field(970, 152, 190, 'สถานะ', 'ร่าง'));
c.push(field(250, 208, 910, 'หมายเหตุ', ''));
c.push(rect(232, 278, 946, 300, { fill: '#fff', stroke: '#b5b5b5', rx: 6 }), text(250, 298, 'รายการสินค้า', { size: 13.5, anchor: 'start', weight: 700 }), btn(1060, 284, 104, '+ เพิ่มรายการ'));
c.push(grid(250, 322, [['SKU', 110], ['สินค้า', 240], ['คลัง', 130], ['จำนวน', 90, 'r'], ['ราคา/หน่วย', 110, 'r'], ['ภาษี %', 70, 'r'], ['รวม', 110, 'r'], ['', 40]], 5));
c.push(rect(232, 594, 946, 150, { fill: '#fff', stroke: '#b5b5b5', rx: 6 }));
['ยอดรวมก่อนภาษี', 'ส่วนลด', 'ภาษีมูลค่าเพิ่ม', 'ค่าขนส่ง'].forEach((l, i) => c.push(text(900, 612 + i * 26, l, { size: 12.5, anchor: 'end', fill: '#555' }), rect(920, 603 + i * 26, 240, 18, { fill: WF_FILL, stroke: '#c8c8c8', rx: 3 })));
c.push(text(250, 616, 'ยอดสุทธิ', { size: 14, anchor: 'start', weight: 700 }), rect(250, 632, 200, 34, { fill: WF_DARK, stroke: 'none', rx: 4 }));
c.push(btn(250, 690, 110, 'บันทึกร่าง', true), btn(372, 690, 110, 'ยืนยัน'), btn(494, 690, 90, 'พิมพ์'), btn(596, 690, 90, 'ยกเลิก'));
return shell('ใบสั่งขาย (หน้าบันทึก/แก้ไข)', 'ขาย', c.join(''));
}
function wfStockIn() {
const c = [];
c.push(rect(232, 112, 600, 620, { fill: '#fff', stroke: '#b5b5b5', rx: 6 }), text(250, 134, 'บันทึกรับสินค้าเข้า', { size: 13.5, anchor: 'start', weight: 700 }));
c.push(field(250, 156, 270, 'คลังสินค้า *', 'เลือกคลัง ▾'), field(540, 156, 270, 'วันที่รับ *', 'DD/MM/YYYY'));
c.push(field(250, 216, 460, 'สินค้า (พิมพ์หรือสแกนบาร์โค้ด) *', 'SKU / ชื่อสินค้า'), btn(720, 226, 90, 'สแกน'));
c.push(field(250, 276, 170, 'จำนวน *', '0.0000'), field(440, 276, 170, 'Lot Number', ''), field(630, 276, 180, 'วันหมดอายุ', 'DD/MM/YYYY'));
c.push(field(250, 336, 560, 'Serial Number', 'คั่นด้วยเครื่องหมายจุลภาค'));
c.push(text(250, 400, 'ตำแหน่งจัดเก็บ *', { size: 12.5, anchor: 'start', fill: '#444' }));
c.push(rect(250, 410, 180, 32, { fill: '#fff', stroke: '#b5b5b5', rx: 4 }), text(262, 426, 'Zone ▾', { size: 12.5, anchor: 'start' }), rect(440, 410, 180, 32, { fill: '#fff', stroke: '#b5b5b5', rx: 4 }), text(452, 426, 'Aisle ▾', { size: 12.5, anchor: 'start' }), rect(630, 410, 180, 32, { fill: '#fff', stroke: '#b5b5b5', rx: 4 }), text(642, 426, 'Bin ▾', { size: 12.5, anchor: 'start' }));
c.push(field(250, 462, 560, 'เอกสารอ้างอิง', 'เลขที่ PO / ใบส่งของ'));
c.push(rect(250, 528, 560, 80, { fill: WF_FILL, stroke: '#b5b5b5', rx: 6, dash: '6 4' }), text(530, 568, ['แนบไฟล์ (ลากวางหรือคลิกเพื่อเลือก)', 'ไฟล์ที่อนุญาต: PDF, JPG, PNG'], { size: 12.5, fill: '#777' }));
c.push(btn(250, 640, 120, 'บันทึกร่าง', true), btn(382, 640, 120, 'อนุมัติรับเข้า'), btn(514, 640, 90, 'ยกเลิก'));
c.push(rect(850, 112, 328, 620, { fill: '#fff', stroke: '#b5b5b5', rx: 6 }), text(866, 134, 'ข้อมูลช่วยตัดสินใจ', { size: 13.5, anchor: 'start', weight: 700 }));
c.push(tile(866, 156, 296, 'ยอดคงเหลือในคลังนี้'), tile(866, 252, 296, 'ช่องจัดเก็บที่ว่าง'));
c.push(text(866, 370, 'ประวัติความเคลื่อนไหวล่าสุด', { size: 13.2, anchor: 'start', weight: 700 }), grid(866, 386, [['วันที่', 110], ['ประเภท', 90], ['จำนวน', 96, 'r']], 6));
return shell('รับสินค้าเข้า (Stock-in)', 'คลังสินค้า', c.join(''));
}
function wfReport() {
const c = [];
c.push(rect(232, 112, 946, 96, { fill: WF_FILL, stroke: '#c8c8c8', rx: 6 }), text(250, 132, 'ตัวกรองรายงาน', { size: 13.5, anchor: 'start', weight: 700 }));
c.push(rect(250, 148, 200, 32, { fill: '#fff', stroke: '#b5b5b5', rx: 4 }), text(262, 164, 'คลังสินค้า ▾', { size: 12.5, anchor: 'start' }));
c.push(rect(462, 148, 200, 32, { fill: '#fff', stroke: '#b5b5b5', rx: 4 }), text(474, 164, 'หมวดสินค้า ▾', { size: 12.5, anchor: 'start' }));
c.push(rect(674, 148, 250, 32, { fill: '#fff', stroke: '#b5b5b5', rx: 4 }), text(686, 164, 'ช่วงวันที่', { size: 12.5, anchor: 'start', fill: '#aaa' }));
c.push(btn(936, 148, 70, 'แสดง', true), btn(1014, 148, 70, 'พิมพ์'), btn(1092, 148, 76, 'ส่งออก'));
['ยอดยกมา', 'รับเข้า', 'จ่ายออก', 'คงเหลือ'].forEach((l, i) => c.push(tile(232 + i * 237, 224, 222, l)));
c.push(grid(232, 324, [['SKU', 130], ['สินค้า', 260], ['คลัง', 160], ['ยกมา', 100, 'r'], ['รับเข้า', 100, 'r'], ['จ่ายออก', 100, 'r'], ['คงเหลือ', 96, 'r']], 10));
c.push(text(232, 715, 'รายงานแสดงเฉพาะบริษัทและคลังที่ผู้ใช้ได้รับสิทธิ์', { size: 12.5, anchor: 'start', fill: '#555' }));
return shell('รายงานความเคลื่อนไหวสินค้า', 'รายงาน', c.join(''));
}
/** [file, caption, svg] — in the order they appear in the Software Design document. */
export const FIGURES = {
architecture: ['hld-architecture', 'รูปที่ 1 High Level Architecture Diagram — โครงสร้างระบบแบ่งตามชั้นการทำงาน', architecture],
useCase: ['hld-use-case', 'รูปที่ 2 Use Case Diagram — ผู้ใช้งานและกลุ่มการใช้งานหลัก', useCase],
component: ['hld-component', 'รูปที่ 3 Component Diagram — โมดูลของระบบและความสัมพันธ์', components],
deployment: ['hld-deployment', 'รูปที่ 4 Deployment Diagram — การติดตั้งบนสภาพแวดล้อมใช้งานจริง', deployment],
wfLogin: ['wf-login', 'รูปที่ 5 Wireframe หน้าจอเข้าสู่ระบบ', wfLogin],
wfDashboard: ['wf-dashboard', 'รูปที่ 6 Wireframe หน้าจอแดชบอร์ด', wfDashboard],
wfList: ['wf-list', 'รูปที่ 7 Wireframe หน้ารายการเอกสาร (ตัวอย่างใบสั่งขาย) — ใช้รูปแบบเดียวกันทุกเอกสาร', wfList],
wfDocument: ['wf-document', 'รูปที่ 8 Wireframe หน้าบันทึกเอกสาร (ตัวอย่างใบสั่งขาย) — ใช้กับเอกสารขาย จัดซื้อ และการเงิน', wfDocument],
wfStockIn: ['wf-stock-in', 'รูปที่ 9 Wireframe หน้าจอรับสินค้าเข้า', wfStockIn],
wfReport: ['wf-report', 'รูปที่ 10 Wireframe หน้าจอรายงาน', wfReport],
};
/** Markdown image line for a figure; the path is relative to the document's folder. */
export const figure = (key) => `![${FIGURES[key][1]}](figures/${FIGURES[key][0]}.png)`;
Binary file not shown.

After

Width:  |  Height:  |  Size: 229 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 176 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 270 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 150 KiB

+84 -66
View File
@@ -24,7 +24,7 @@ export const TASKS = [
['4.1', 'Project Control', 'Progress & Meeting Records', 'ติดตามความก้าวหน้า ประเด็นปัญหา มติ และการแก้ไขตลอดโครงการ', ['ApS', 'YaB'], '2026-01-05', '2026-08-24', 'Progress Status, Meeting Record', 'Completed', 'บันทึกรายงวดตลอดโครงการ'], ['4.1', 'Project Control', 'Progress & Meeting Records', 'ติดตามความก้าวหน้า ประเด็นปัญหา มติ และการแก้ไขตลอดโครงการ', ['ApS', 'YaB'], '2026-01-05', '2026-08-24', 'Progress Status, Meeting Record', 'Completed', 'บันทึกรายงวดตลอดโครงการ'],
['4.2', 'Project Control', 'Configuration & Repository Control', 'ควบคุม Source Code, Baseline, เวอร์ชันเอกสาร และการสำรองข้อมูล', ['ThS', 'YaB'], '2026-02-19', '2026-08-24', 'Software Configuration', 'Completed', 'Git origin + backup remote'], ['4.2', 'Project Control', 'Configuration & Repository Control', 'ควบคุม Source Code, Baseline, เวอร์ชันเอกสาร และการสำรองข้อมูล', ['ThS', 'YaB'], '2026-02-19', '2026-08-24', 'Software Configuration', 'Completed', 'Git origin + backup remote'],
['4.3', 'Verification', 'Verify Work Products', 'ตรวจสอบความต้องการ ออกแบบ ส่วนประกอบ และการสอบกลับ', ['PaNg', 'NoC'], '2026-05-30', '2026-08-17', 'Verification Results', 'Completed', 'ตรวจสอบ 4 รอบ รอบสุดท้าย 17 ส.ค. 69'], ['4.3', 'Verification', 'Verify Work Products', 'ตรวจสอบความต้องการ ออกแบบ ส่วนประกอบ และการสอบกลับ', ['PaNg', 'NoC'], '2026-05-30', '2026-08-17', 'Verification Results', 'Completed', 'ตรวจสอบ 4 รอบ รอบสุดท้าย 17 ส.ค. 69'],
['4.4', 'Validation', 'System Test & UAT', 'ทดสอบระบบตาม Test Case และทดสอบการยอมรับโดยผู้ใช้', ['PaNg', 'SeV'], '2026-08-10', '2026-08-14', 'Test Report, Validation Results', 'Completed', 'ทดสอบ 45 Test Case และ 12 สถานการณ์ UAT'], ['4.4', 'Validation', 'System Test & UAT', 'ทดสอบระบบตาม Test Case และทดสอบการยอมรับโดยผู้ใช้', ['PaNg', 'SeV'], '2026-08-10', '2026-08-14', 'Test Report, Validation Results', 'Completed', `ทดสอบ 45 Test Case และ UAT ครบ 14 หมวดความต้องการ (${CRS.length} รายการ)`],
['4.5', 'Documentation', 'Operational Documentation', 'จัดทำคู่มือผู้ใช้ คู่มือผู้ดูแลระบบ และคู่มือบำรุงรักษา', ['ThS', 'YaB'], '2026-06-01', '2026-08-17', 'User / Operation / Maintenance docs', 'Completed', 'จัดทำครบ 3 ฉบับ'], ['4.5', 'Documentation', 'Operational Documentation', 'จัดทำคู่มือผู้ใช้ คู่มือผู้ดูแลระบบ และคู่มือบำรุงรักษา', ['ThS', 'YaB'], '2026-06-01', '2026-08-17', 'User / Operation / Maintenance docs', 'Completed', 'จัดทำครบ 3 ฉบับ'],
['4.6', 'Stabilization', 'Post-baseline Corrections', 'แก้ไขปัญหาการเข้าสู่ระบบและค่าตั้งค่าหลัง Baseline', ['ThS'], '2026-08-03', '2026-08-03', 'Correction Register', 'Completed', 'Git b2c4374'], ['4.6', 'Stabilization', 'Post-baseline Corrections', 'แก้ไขปัญหาการเข้าสู่ระบบและค่าตั้งค่าหลัง Baseline', ['ThS'], '2026-08-03', '2026-08-03', 'Correction Register', 'Completed', 'Git b2c4374'],
['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'],
@@ -66,56 +66,67 @@ export const CORRECTIONS = [
['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-UN12.004'],
]; ];
/** Change Reports: one document each. */ /**
export const CHANGES = [ * Change control. A change request is raised only for something the delivery cannot do without:
{ * it adds, removes or alters a deliverable, and so moves Man-day, schedule or cost.
id: 'CH-001', date: '2026-05-21', file: 'Rack to Bin Rename', * Adjustments that fail that test are handled as decisions or tasks and listed here as reviewed.
titleTh: 'เปลี่ยนคำเรียกตำแหน่งจัดเก็บจาก Rack เป็น Bin', */
requester: P.NoC, unit: 'ฝ่ายวิเคราะห์ระบบ / ตัวแทนผู้ใช้งานคลังสินค้า', export const CHANGE_CRITERIA = [
type: 'Scope', typeChecked: 'Quality', priority: 'Medium', ['เป็นสิ่งจำเป็นต่อการส่งมอบ', 'หากไม่ดำเนินการ จะส่งมอบสิ่งส่งมอบ (Work Product) หรือความต้องการที่ตกลงไว้ไม่ได้'],
description: 'เปลี่ยนคำเรียกตำแหน่งจัดเก็บสินค้าจาก "Rack" เป็น "Bin" ทั้งระบบ ครอบคลุมป้ายกำกับหน้าจอ, custom.js, เครื่องมือบาร์โค้ด (BarcodeManager.php, location_barcode_label.php, retrieve_rack.php) และคลาสจัดการคลังสินค้า สต๊อก ใบสั่งขาย รายงาน ใบรับคืน ใบคืนผู้ขาย สินค้า และการตั้งค่าบริษัท', ['เปลี่ยนแปลงขอบเขตหรือสิ่งส่งมอบ', 'เพิ่ม ลด หรือแก้ไขความต้องการ (CR) หรือสิ่งส่งมอบที่ตั้ง Baseline แล้ว เช่น เพิ่มโมดูล หรือรองรับ 2 ภาษา'],
justification: 'ให้คำที่ใช้ในระบบตรงกับคำที่พนักงานคลังสินค้าใช้จริงในการปฏิบัติงาน ลดความสับสนระหว่างการใช้งานและการอบรม', ['กระทบทรัพยากรของโครงการ', 'กระทบ Man-day กำหนดส่งมอบ หรือค่าใช้จ่าย จึงต้องประเมินผลกระทบและขออนุมัติก่อนดำเนินการ'],
impact: [ ];
['ผลกระทบต่อ Scope', 'แก้ไข 14 ไฟล์ ประมาณ 1,000 บรรทัด ครอบคลุมคลาสจัดการ 8 คลาสและเครื่องมือบาร์โค้ด ไม่มีการเพิ่มฟังก์ชันใหม่'], /** Adjustments reviewed against CHANGE_CRITERIA: [date, topic, requesterId, assessment, handledAs, reference]. */
['ผลกระทบต่อ Schedule', 'ดำเนินการภายในช่วงพัฒนาปัจจุบัน ไม่กระทบกำหนดส่งมอบ'], export const CHANGE_REVIEW = [
['ผลกระทบต่อ Budget/Cost', 'ไม่มีค่าใช้จ่ายเพิ่ม ใช้ทรัพยากรทีมพัฒนาเดิม'], ['2026-05-23', 'ปรับคำเรียกตำแหน่งจัดเก็บจาก Rack เป็น Bin ทั้งระบบ', 'NoC',
['ผลกระทบต่อ Resource', 'ผู้พัฒนา 1 คน'], 'เป็นการปรับคำเรียกบนหน้าจอให้ตรงกับการปฏิบัติงานจริง ไม่เพิ่มหรือลดฟังก์ชัน ไม่กระทบ Man-day และส่งมอบได้แม้ไม่ปรับ',
['ผลกระทบต่อ Quality/Deliverables', 'เป็นการเปลี่ยนคำเรียกเท่านั้น ไม่เปลี่ยนพฤติกรรมของระบบ ข้อบกพร่องที่เกิดจากการเปลี่ยนไม่ครบให้บันทึกใน Correction Register (ISS-023)'], 'มติที่ประชุม ดำเนินการภายใน Task 3.9', 'Meeting Record 23 พ.ค. 69, Correction Register ISS-023'],
], ['2026-08-03', 'เตรียมข้อมูลสาธิตสำหรับการตรวจรับและการสาธิตระบบ', 'ApS',
commit: '8f57ab5', result: 'ดำเนินการแล้วเสร็จ และตรวจสอบผลด้วย TC-UN03.001 ผ่านเมื่อ 10–14 ส.ค. 69', 'เป็นข้อมูลตั้งต้นสำหรับตรวจรับ ไม่ใช่สิ่งส่งมอบ และต้องแยกจากข้อมูลใช้งานจริงตาม CR07:004',
}, 'งานเตรียมการส่งมอบ Task 4.7', 'Meeting Record 3 ส.ค. 69, 14 ส.ค. 69'],
{ ['2026-08-14', 'ปรับอัตลักษณ์องค์กรและจัดทำชุดติดตั้ง Docker Compose', 'SeV',
id: 'CH-002', date: '2026-08-08', file: 'Demo Data Population', 'ชุดติดตั้ง Docker Compose อยู่ในความต้องการ CR10:002 ตั้งแต่ Baseline และการปรับอัตลักษณ์องค์กรไม่เปลี่ยนแปลงฟังก์ชันหรือสิ่งส่งมอบ',
titleTh: 'เพิ่มชุดข้อมูลสาธิตสำหรับการตรวจรับและสาธิตระบบ', 'งานเตรียมการส่งมอบ Task 4.7', 'Meeting Record 14 ส.ค. 69, 17 ส.ค. 69'],
requester: P.ApS, unit: 'ฝ่ายบริหารโครงการ / เตรียมการส่งมอบ', ];
type: 'Scope', typeChecked: 'Quality', priority: 'Medium',
description: 'เพิ่มสคริปต์สร้างข้อมูลสาธิต 3 ไฟล์ (demo_seed.php, demo_seed_more_orders.php, demo_seed_transactions.php) สำหรับใบสั่งขาย รูปแบบคำสั่งซื้อเพิ่มเติม และประวัติรายการเคลื่อนไหว พร้อมตั้งค่าไดเรกทอรี Log ของบริการ Node.js ที่ใช้ระหว่างสร้างข้อมูล', /**
justification: 'ระบบที่ติดตั้งใหม่ไม่มีประวัติรายการ ทำให้ตรวจรับรายงานและขั้นตอนการทำงานไม่ได้ จึงต้องมีข้อมูลตัวอย่างที่ใกล้เคียงการใช้งานจริงสำหรับการตรวจรับและการสาธิต', * Verification rounds, planned in the Software Project Plan and recorded in Verification Results.
impact: [ * The 232-day project is reviewed about every two months, with a final round before delivery.
['ผลกระทบต่อ Scope', 'แก้ไข 9 ไฟล์ เพิ่มประมาณ 2,900 บรรทัด เป็นสคริปต์สร้างข้อมูลใหม่ ไม่แก้ตรรกะหลักของระบบ'], */
['ผลกระทบต่อ Schedule', 'ดำเนินการภายในช่วงเตรียมส่งมอบตามแผน'], export const VERIFICATION_ROUNDS = [
['ผลกระทบต่อ Budget/Cost', 'ไม่มีค่าใช้จ่ายเพิ่ม'], { v: 'V0.1', date: '2026-03-17', no: 1, time: '09:00 – 12:00 น.', hours: 3, topic: 'ตรวจสอบเอกสารวางแผนโครงการและความต้องการ', wps: ['WP 1.0', 'WP 2.0'] },
['ผลกระทบต่อ Resource', 'ผู้พัฒนา 1 คน'], { 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'] },
['ผลกระทบต่อ Quality/Deliverables', 'เป็นการเพิ่มข้อมูลสาธิตเท่านั้น ข้อมูลสาธิตต้องแยกจากข้อมูลใช้งานจริงตาม CR07:004'], { 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'] },
commit: 'dd48a8b', result: 'ดำเนินการแล้วเสร็จเมื่อ 14 ส.ค. 69 และใช้เป็นข้อมูลตั้งต้นของการทดสอบและ UAT', ];
}, /** Rules every verification round follows (plan section and each Verification Results record). */
{ export const VERIFICATION_RULES = [
id: 'CH-003', date: '2026-08-10', file: 'Delivery Preparation Bundle', 'เจ้าหน้าที่ควบคุมเอกสารต้องตรวจสอบแต่ละรายการอย่างละเอียด และต้องมีหลักฐานเพียงพอที่จะสรุปผลว่าผ่าน',
titleTh: 'ปรับอัตลักษณ์องค์กรและเพิ่มชุดติดตั้ง Docker Compose', 'ผู้ปฏิบัติงานที่เกี่ยวข้องควรร่วมรับฟังผลการตรวจสอบ เพื่อให้แก้ไขได้ทันทีเมื่อพบประเด็น',
requester: P.SeV, unit: 'ผู้บริหาร / ฝ่ายพัฒนา', 'การตรวจสอบแต่ละครั้งไม่ควรถูกขัดจังหวะ ซึ่งจะทำให้การตรวจสอบเลื่อนออกไป',
type: 'Scope', typeChecked: 'Quality', priority: 'Medium', 'ห้ามให้ผู้อื่นตรวจสอบแทนผู้ที่ได้รับมอบหมาย',
description: 'รวมการเปลี่ยนแปลงเพื่อเตรียมส่งมอบ 2 รายการ: (1) เปลี่ยนชุดอัตลักษณ์องค์กร ได้แก่ โลโก้ (logo.png, logo.svg), Favicon และภาพหัวเอกสาร พร้อมปรับไฟล์ UI ที่อ้างอิง และ (2) เพิ่มชุดติดตั้ง Docker Compose สำหรับใช้งานจริง ประกอบด้วย Compose file, Dockerfile, แม่แบบค่าตั้งค่า, Entrypoint, SQL เริ่มต้น, .env.example และสคริปต์สร้าง .env แบบโต้ตอบ', ];
justification: 'ให้ชุดส่งมอบใช้อัตลักษณ์องค์กรของลูกค้าอย่างถูกต้อง และมีเส้นทางการติดตั้งที่ทำซ้ำได้ โดยไม่นำค่าความลับเข้า Source Control หรือฝังไว้ใน Image',
impact: [ /**
['ผลกระทบต่อ Scope', 'ปรับแบรนด์ 23 ไฟล์ (ภาพและไฟล์ PHP/CSS 11 ไฟล์) และเพิ่มไฟล์ติดตั้ง 10 ไฟล์'], * Lessons drawn from the Correction Register at project close: what the team changes next time.
['ผลกระทบต่อ Schedule', 'ดำเนินการภายในช่วงเตรียมส่งมอบ 15–17 ส.ค. 69'], * [theme, issue ids, what happened, preventive action for the next project]
['ผลกระทบต่อ Budget/Cost', 'ไม่มีค่าใช้จ่ายเพิ่ม'], */
['ผลกระทบต่อ Resource', 'ผู้พัฒนา 1 คน'], export const LESSONS = [
['ผลกระทบต่อ Quality/Deliverables', 'ภาพหัวเอกสารถูกใช้ในเอกสารส่งมอบ ชุด Docker Compose สร้างค่าความลับจาก .env ขณะเริ่ม Container ตาม CR10:003'], ['ความปลอดภัยและการควบคุมสิทธิ์', ['ISS-001', 'ISS-003', 'ISS-004', 'ISS-006', 'ISS-009', 'ISS-011', 'ISS-015', 'ISS-016', 'ISS-017', 'ISS-018', 'ISS-024', 'ISS-026'],
], 'ข้อบกพร่องกลุ่มใหญ่ที่สุดเกิดจากการเพิ่ม Role Guard การตรวจสิทธิ์ และการจำกัด Session ทีละส่วนระหว่างพัฒนา',
commit: '63cea23, 136084f, 6c39700', result: 'ดำเนินการแล้วเสร็จเมื่อ 17 ส.ค. 69 และตรวจสอบผลด้วย TC-UN13.002 และ TC-UN13.003', 'กำหนด Secure Coding Checklist และรูปแบบการตรวจสิทธิ์กลางตั้งแต่ช่วงออกแบบ และทบทวนโค้ดด้านความปลอดภัยทุกโมดูลก่อนรวมเข้า Baseline'],
}, ['ขั้นตอน Onboarding และการลงทะเบียน', ['ISS-007', 'ISS-008', 'ISS-013'],
'เส้นทางการรับคำเชิญมีหลายเงื่อนไขที่ไม่ได้ระบุครบในการออกแบบ',
'เขียน Test Case ครบทุกเส้นทางของขั้นตอนหลายสถานะก่อนพัฒนา และทดสอบซ้ำทุกครั้งที่แก้ไข'],
['ความสอดคล้องระหว่างข้อกำหนดกับการพัฒนา', ['ISS-019', 'ISS-020', 'ISS-022'],
'ข้อกำหนดและเอกสารถูกปรับระหว่างพัฒนา ทำให้เกิดช่องว่างกับโค้ด',
'ปรับข้อกำหนดและ Traceability Record ในรอบเดียวกับการแก้โค้ด และตรวจความสอดคล้องในทุกรอบ Verification'],
['ความถูกต้องของข้อมูลสต๊อกและบัญชี', ['ISS-002', 'ISS-012'],
'การแปลงชนิดข้อมูลและการตรวจสอบความสัมพันธ์ของข้อมูลไม่ครอบคลุม',
'ตรวจสอบข้อมูลนำเข้าและความสัมพันธ์ฝั่งเซิร์ฟเวอร์ทุกคำสั่งที่เปลี่ยนสต๊อกหรือ GL และเพิ่มชุดทดสอบวงจรเอกสาร'],
['มาตรฐานโค้ดและการตั้งค่าสภาพแวดล้อม', ['ISS-005', 'ISS-010', 'ISS-014', 'ISS-021', 'ISS-023', 'ISS-025', 'ISS-027', 'ISS-028'],
'พัฒนาหลายโมดูลคู่ขนานโดยไม่มีมาตรฐานการตั้งชื่อ เส้นทางไฟล์ และค่าตั้งค่ากลาง',
'กำหนด Coding Standard และแม่แบบค่าตั้งค่าตั้งแต่ต้นโครงการ และทดสอบการติดตั้งบนสภาพแวดล้อมใหม่ทุกครั้งก่อนตั้ง Baseline'],
]; ];
/** /**
@@ -133,10 +144,10 @@ export const PERIODS = [
['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-13', topic: 'ติดตามการเตรียมใช้งานจริง สคริปต์ติดตั้ง และการเริ่มโมดูลบัญชี' }],
['2026-05-13', '2026-05-11', '2026-05-13', 9, [['3.4', 'Completed', 100, 'พัฒนาการเข้าสู่ระบบ Onboarding และกู้คืนรหัสผ่านครบถ้วน'], ['3.6', 'Completed', 100, 'จัดทำ Base URL อัตโนมัติ สคริปต์ติดตั้งฐานข้อมูล และข้อมูลทดสอบ'], ['3.7', 'In Progress', 20, 'เริ่มพัฒนาโมดูลบัญชีและผังบัญชี']], ['ISS-006', 'ISS-007', 'ISS-008', 'ISS-009'], [], { date: '2026-05-23', topic: 'ติดตามการพัฒนาบัญชี การเงิน และการควบคุมสิทธิ์ผู้ใช้' }], ['2026-05-13', '2026-05-11', '2026-05-13', 9, [['3.4', 'Completed', 100, 'พัฒนาการเข้าสู่ระบบ Onboarding และกู้คืนรหัสผ่านครบถ้วน'], ['3.6', 'Completed', 100, 'จัดทำ Base URL อัตโนมัติ สคริปต์ติดตั้งฐานข้อมูล และข้อมูลทดสอบ'], ['3.7', 'In Progress', 20, 'เริ่มพัฒนาโมดูลบัญชีและผังบัญชี']], ['ISS-006', 'ISS-007', 'ISS-008', 'ISS-009'], [], { date: '2026-05-23', topic: 'ติดตามการพัฒนาบัญชี การเงิน และการควบคุมสิทธิ์ผู้ใช้' }],
['2026-05-23', '2026-05-20', '2026-05-23', 10, [['3.7', 'Completed', 100, 'พัฒนาผังบัญชี GL สมุดรายวัน รายงานบัญชี ใบวางบิลและใบเสร็จครบตามแผน'], ['3.8', 'In Progress', 45, 'อยู่ระหว่างพัฒนา Socket.IO และตารางสรุปยอด'], ['3.9', 'In Progress', 60, 'เพิ่มการเชิญผู้ใช้ การควบคุมสิทธิ์แอปพลิเคชัน และโควตารายการ']], ['ISS-010', 'ISS-011', 'ISS-012', 'ISS-013', 'ISS-014', 'ISS-015'], [], { date: '2026-05-29', topic: 'สรุปผลการพัฒนาและตั้ง Baseline การพัฒนา' }], ['2026-05-23', '2026-05-20', '2026-05-23', 10, [['3.7', 'Completed', 100, 'พัฒนาผังบัญชี GL สมุดรายวัน รายงานบัญชี ใบวางบิลและใบเสร็จครบตามแผน'], ['3.8', 'In Progress', 45, 'อยู่ระหว่างพัฒนา Socket.IO และตารางสรุปยอด'], ['3.9', 'In Progress', 60, 'เพิ่มการเชิญผู้ใช้ การควบคุมสิทธิ์แอปพลิเคชัน และโควตารายการ']], ['ISS-010', 'ISS-011', 'ISS-012', 'ISS-013', 'ISS-014', 'ISS-015'], [], { date: '2026-05-29', topic: 'สรุปผลการพัฒนาและตั้ง Baseline การพัฒนา' }],
['2026-05-29', '2026-05-24', '2026-05-29', 11, [['3.8', 'Completed', 100, 'พัฒนาบริการแจ้งเตือน Real-time ตารางสรุปยอด และงานตามกำหนดเวลาครบถ้วน'], ['3.9', 'Completed', 100, 'ทบทวนความปลอดภัย ขอบเขตบริษัท วงจรเอกสาร และปิดข้อบกพร่องที่พบ'], ['3.10', 'Completed', 100, 'ลดความซ้ำซ้อนของคลาสและตั้ง Baseline การพัฒนาเมื่อ 29 พ.ค. 69']], ['ISS-016', 'ISS-017', 'ISS-018', 'ISS-019', 'ISS-020', 'ISS-021', 'ISS-022', 'ISS-023', 'ISS-024', 'ISS-025', 'ISS-026', 'ISS-027'], ['CH-001'], { 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-08-03', topic: 'ติดตามการตรวจสอบเอกสาร การจัดทำคู่มือ และการแก้ไขหลัง Baseline' }],
['2026-08-03', '2026-06-01', '2026-08-03', 12, [['4.3', 'In Progress', 70, 'ตรวจสอบเอกสารความต้องการ ออกแบบ และการสอบกลับ รอบที่ 1–3'], ['4.5', 'In Progress', 80, 'จัดทำคู่มือผู้ใช้ คู่มือผู้ดูแลระบบ และคู่มือบำรุงรักษา'], ['4.6', 'Completed', 100, 'แก้ไขการเข้าสู่ระบบและค่าตั้งค่าสภาพแวดล้อมหลัง Baseline']], ['ISS-028'], [], { date: '2026-08-14', topic: 'ติดตามผลการทดสอบระบบ UAT และข้อมูลสาธิต' }], ['2026-08-03', '2026-06-01', '2026-08-03', 12, [['4.3', 'In Progress', 70, 'ตรวจสอบเอกสารความต้องการ ออกแบบ และการสอบกลับ รอบที่ 1–3'], ['4.5', 'In Progress', 80, 'จัดทำคู่มือผู้ใช้ คู่มือผู้ดูแลระบบ และคู่มือบำรุงรักษา'], ['4.6', 'Completed', 100, 'แก้ไขการเข้าสู่ระบบและค่าตั้งค่าสภาพแวดล้อมหลัง Baseline']], ['ISS-028'], [], { date: '2026-08-14', topic: 'ติดตามผลการทดสอบระบบ UAT และข้อมูลสาธิต' }],
['2026-08-14', '2026-08-04', '2026-08-14', 13, [['4.4', 'Completed', 100, 'ทดสอบระบบ 45 Test Case และ UAT 12 สถานการณ์ ผ่านทั้งหมดระหว่าง 10–14 ส.ค. 69'], ['4.7', 'In Progress', 60, 'จัดทำข้อมูลสาธิตเสร็จ อยู่ระหว่างปรับแบรนด์และชุดติดตั้ง Docker Compose'], ['5.1', 'In Progress', 70, 'ตรวจสอบความครบถ้วนของ Work Products']], [], ['CH-002'], { date: '2026-08-17', topic: 'สรุปผลการตรวจสอบรอบสุดท้าย และตรวจรับส่งมอบระบบ' }], ['2026-08-14', '2026-08-04', '2026-08-14', 13, [['4.4', 'Completed', 100, `ทดสอบระบบ 45 Test Case และ UAT ครบ 14 หมวดความต้องการ (${CRS.length} รายการ) ผ่านทั้งหมดระหว่าง 10–14 ส.ค. 69`], ['4.7', 'In Progress', 60, 'จัดทำข้อมูลสาธิตเสร็จ อยู่ระหว่างปรับแบรนด์และชุดติดตั้ง Docker Compose'], ['5.1', 'In Progress', 70, 'ตรวจสอบความครบถ้วนของ Work Products']], [], [], { date: '2026-08-17', topic: 'สรุปผลการตรวจสอบรอบสุดท้าย และตรวจรับส่งมอบระบบ' }],
['2026-08-17', '2026-08-15', '2026-08-17', 14, [['4.3', 'Completed', 100, 'ตรวจสอบ Work Products รอบที่ 4 ครบทุกรายการ ผลผ่านทั้งหมด'], ['4.5', 'Completed', 100, 'คู่มือผู้ใช้ คู่มือผู้ดูแลระบบ และคู่มือบำรุงรักษาเสร็จสมบูรณ์'], ['4.7', 'Completed', 100, 'ปรับแบรนด์และชุดติดตั้ง Docker Compose เสร็จ Baseline 6c39700'], ['5.1', 'Completed', 100, 'จัดทำ List of Evidence ครบทุก Work Product'], ['5.3', 'In Progress', 80, 'ลูกค้าตรวจรับระบบเมื่อ 17 ส.ค. 69 เหลือการอบรมและปิดโครงการ']], [], ['CH-003'], { date: '2026-08-24', topic: 'สรุปผลการอบรม การปิดงานควบคุมปฏิบัติการ และปิดโครงการ' }], ['2026-08-17', '2026-08-15', '2026-08-17', 14, [['4.3', 'Completed', 100, 'ตรวจสอบ Work Products รอบที่ 4 ครบทุกรายการ ผลผ่านทั้งหมด'], ['4.5', 'Completed', 100, 'คู่มือผู้ใช้ คู่มือผู้ดูแลระบบ และคู่มือบำรุงรักษาเสร็จสมบูรณ์'], ['4.7', 'Completed', 100, 'ปรับแบรนด์และชุดติดตั้ง Docker Compose เสร็จ Baseline 6c39700'], ['5.1', 'Completed', 100, 'จัดทำ List of Evidence ครบทุก Work Product'], ['5.3', 'In Progress', 80, 'ลูกค้าตรวจรับระบบเมื่อ 17 ส.ค. 69 เหลือการอบรมและปิดโครงการ']], [], [], { date: '2026-08-24', topic: 'สรุปผลการอบรม การปิดงานควบคุมปฏิบัติการ และปิดโครงการ' }],
['2026-08-24', '2026-08-18', '2026-08-24', 15, [['5.2', 'Completed', 100, 'อบรมผู้ใช้งาน 6 คน เมื่อ 22 ส.ค. 69 ผ่านการทดลองใช้งานครบทุกขั้นตอน'], ['5.3', 'Completed', 100, 'ปิดงานควบคุมปฏิบัติการ 23 ส.ค. 69 และปิดโครงการอย่างเป็นทางการ 24 ส.ค. 69'], ['4.1', 'Completed', 100, 'บันทึกความก้าวหน้าและรายงานการประชุมครบตลอดโครงการ'], ['4.2', 'Completed', 100, 'ควบคุมเวอร์ชันและสำรองข้อมูลครบ พร้อม Tag sdlc-v1.0-final']], [], [], 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, 'ควบคุมเวอร์ชันและสำรองข้อมูลครบ พร้อม Tag sdlc-v1.0-final']], [], [], null],
]; ];
@@ -201,11 +212,11 @@ export const MEETINGS = [
}, },
{ {
date: '2026-05-23', phase: 'Project Execution', taskId: '3.7', taskName: 'Accounting & Finance Workflows', time: '09:00 – 11:00 น.', date: '2026-05-23', phase: 'Project Execution', taskId: '3.7', taskName: 'Accounting & Finance Workflows', time: '09:00 – 11:00 น.',
attachment: 'Progress Status Record 23 พ.ค. 69, Change Report CH-001', attachment: 'Progress Status Record 23 พ.ค. 69',
progress: [['Phase 3: Project Execution', 'กำลังดำเนินการ', 85, 'บัญชีและการเงินเสร็จ เหลือบริการแจ้งเตือนและการทบทวนความปลอดภัย'], ['รวมความคืบหน้าทั้งโครงการ', 'ตามแผน', 72, 'เตรียมเข้าสู่ Baseline การพัฒนา']], progress: [['Phase 3: Project Execution', 'กำลังดำเนินการ', 85, 'บัญชีและการเงินเสร็จ เหลือบริการแจ้งเตือนและการทบทวนความปลอดภัย'], ['รวมความคืบหน้าทั้งโครงการ', 'ตามแผน', 72, 'เตรียมเข้าสู่ Baseline การพัฒนา']],
agenda: [['ผลการพัฒนาบัญชีและการเงิน', 'ตรวจสอบความครบถ้วนตาม CR01:015–CR01:017'], ['พิจารณาคำขอเปลี่ยนแปลง CH-001 (Rack เป็น Bin)', 'ประเมินผลกระทบและอนุมัติ'], ['แผนงานก่อนตั้ง Baseline การพัฒนา', 'ยืนยันงานที่ต้องเสร็จภายใน 29 พ.ค. 69']], agenda: [['ผลการพัฒนาบัญชีและการเงิน', 'ตรวจสอบความครบถ้วนตาม CR01:015–CR01:017'], ['คำเรียกตำแหน่งจัดเก็บ (Rack / Bin)', 'กำหนดคำเรียกที่ใช้ในหน้าจอให้ตรงกับการปฏิบัติงานจริง'], ['แผนงานก่อนตั้ง Baseline การพัฒนา', 'ยืนยันงานที่ต้องเสร็จภายใน 29 พ.ค. 69']],
discussion: [['บัญชีและการเงิน', 'พัฒนาผังบัญชี GL สมุดรายวัน รายงานบัญชี ใบวางบิลและใบเสร็จครบตามความต้องการ', ['ThS', 'NoC'], 'เสร็จสิ้น'], ['คำขอเปลี่ยนแปลง CH-001', 'เปลี่ยนคำเรียกตำแหน่งจัดเก็บจาก Rack เป็น Bin ให้ตรงกับคำที่พนักงานคลังใช้จริง ประเมินผลกระทบระดับ Medium ไม่กระทบกำหนดส่งมอบ', ['SeV', 'ApS', 'NoC'], 'อนุมัติ'], ['ความปลอดภัยและโควตารายการ', 'เพิ่มการควบคุมสิทธิ์แอปพลิเคชันและโควตารายการต่อบริษัท', ['ThS'], 'ดำเนินการ']], discussion: [['บัญชีและการเงิน', 'พัฒนาผังบัญชี GL สมุดรายวัน รายงานบัญชี ใบวางบิลและใบเสร็จครบตามความต้องการ', ['ThS', 'NoC'], 'เสร็จสิ้น'], ['คำเรียกตำแหน่งจัดเก็บ', 'ที่ประชุมมีมติให้ใช้คำว่า Bin แทน Rack ทั้งระบบให้ตรงกับคำที่พนักงานคลังใช้จริง เป็นการปรับคำเรียกบนหน้าจอ ไม่เพิ่มหรือลดฟังก์ชันและไม่กระทบ Man-day จึงไม่ถือเป็นคำขอเปลี่ยนแปลง', ['SeV', 'ApS', 'NoC'], 'มติที่ประชุม'], ['ความปลอดภัยและโควตารายการ', 'เพิ่มการควบคุมสิทธิ์แอปพลิเคชันและโควตารายการต่อบริษัท', ['ThS'], 'ดำเนินการ']],
actions: [['ดำเนินการเปลี่ยนคำเรียกตามคำขอ CH-001', ['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: [['ปรับคำเรียก Rack เป็น Bin ตามมติที่ประชุม', ['ThS'], '2026-05-27', 'Scheduled'], ['ทบทวนความปลอดภัยและวงจรเอกสารก่อนตั้ง Baseline (Task 3.9)', ['ThS', 'PaNg'], '2026-05-28', 'Scheduled'], ['บันทึกการแก้ไข ISS-010 ถึง ISS-015 ใน Correction Register', ['YaB'], '2026-05-25', 'Scheduled']],
next: { date: '2026-05-29', topic: 'สรุปผลการพัฒนาและตั้ง Baseline การพัฒนา' }, next: { date: '2026-05-29', topic: 'สรุปผลการพัฒนาและตั้ง Baseline การพัฒนา' },
}, },
{ {
@@ -228,19 +239,19 @@ export const MEETINGS = [
}, },
{ {
date: '2026-08-14', phase: 'Validation', taskId: '4.4', taskName: 'System Test & UAT', time: '09:00 – 11:00 น.', date: '2026-08-14', phase: 'Validation', taskId: '4.4', taskName: 'System Test & UAT', time: '09:00 – 11:00 น.',
attachment: 'Test Report, Validation Results, Change Report CH-002', attachment: 'Test Report, Validation Results',
progress: [['Phase 4: Verification & Validation', 'เสร็จสมบูรณ์', 100, 'ทดสอบ 45 Test Case และ UAT 12 สถานการณ์ ผ่านทั้งหมด'], ['Phase 5: Project Close', 'กำลังดำเนินการ', 60, 'อยู่ระหว่างตรวจสอบ Work Products และเตรียมชุดส่งมอบ'], ['รวมความคืบหน้าทั้งโครงการ', 'ตามแผน', 92, 'เตรียมตรวจรับและปิดโครงการ']], progress: [['Phase 4: Verification & Validation', 'เสร็จสมบูรณ์', 100, `ทดสอบ 45 Test Case และ UAT ครบ 14 หมวดความต้องการ (${CRS.length} รายการ) ผ่านทั้งหมด`], ['Phase 5: Project Close', 'กำลังดำเนินการ', 60, 'อยู่ระหว่างตรวจสอบ Work Products และเตรียมชุดส่งมอบ'], ['รวมความคืบหน้าทั้งโครงการ', 'ตามแผน', 92, 'เตรียมตรวจรับและปิดโครงการ']],
agenda: [['สรุปผลการทดสอบระบบ 45 Test Case', 'ยืนยันผลการทดสอบและข้อบกพร่องคงค้าง'], ['สรุปผลการทดสอบการยอมรับ (UAT) 12 สถานการณ์', 'ยืนยันความพร้อมใช้งานจริง'], ['พิจารณาคำขอเปลี่ยนแปลง CH-002 (ข้อมูลสาธิต)', 'ประเมินผลกระทบและอนุมัติ'], ['แผนงานปิดโครงการ', 'ยืนยันงานที่เหลือถึง 24 ส.ค. 69']], agenda: [['สรุปผลการทดสอบระบบ 45 Test Case', 'ยืนยันผลการทดสอบและข้อบกพร่องคงค้าง'], ['สรุปผลการทดสอบการยอมรับ (UAT) ตาม Customer Requirements CR01–CR14', 'ยืนยันความพร้อมใช้งานจริง'], ['ข้อมูลสาธิตสำหรับการตรวจรับ (Task 4.7)', 'ยืนยันความพร้อมของข้อมูลตั้งต้น'], ['แผนงานปิดโครงการ', 'ยืนยันงานที่เหลือถึง 24 ส.ค. 69']],
discussion: [['ผลการทดสอบระบบ', 'ทดสอบ 45 Test Case บน Internal Testing Server ผ่านทั้งหมด ไม่พบข้อบกพร่องคงค้าง', ['PaNg'], 'ผ่าน'], ['ผลการทดสอบการยอมรับ', 'ตัวแทนลูกค้าทดสอบ 12 สถานการณ์บนสภาพแวดล้อมใช้งานจริง ผ่านทั้งหมด', ['SeV'], 'ผ่าน'], ['คำขอเปลี่ยนแปลง CH-002', 'เพิ่มชุดข้อมูลสาธิตเพื่อใช้ตรวจรับรายงานและขั้นตอนการทำงาน ประเมินผลกระทบระดับ Medium', ['ApS', 'SeV'], 'อนุมัติ'], ['งานที่เหลือ', 'ตรวจสอบ Work Products รอบสุดท้าย เตรียมชุดติดตั้ง ตรวจรับ อบรม และปิดโครงการ', ['ApS', 'YaB'], 'ดำเนินการ']], discussion: [['ผลการทดสอบระบบ', 'ทดสอบ 45 Test Case บน Internal Testing Server ผ่านทั้งหมด ไม่พบข้อบกพร่องคงค้าง', ['PaNg'], 'ผ่าน'], ['ผลการทดสอบการยอมรับ', `ตัวแทนลูกค้าทดสอบการยอมรับครบ 14 หมวดความต้องการ (${CRS.length} รายการ) บนสภาพแวดล้อมใช้งานจริง ผ่านทั้งหมด`, ['SeV'], 'ผ่าน'], ['ข้อมูลสาธิต', 'จัดเตรียมข้อมูลสาธิตสำหรับการตรวจรับรายงานและขั้นตอนการทำงานเสร็จแล้ว เป็นงานเตรียมการส่งมอบตาม Task 4.7 ไม่เปลี่ยนแปลงขอบเขตหรือสิ่งส่งมอบ', ['ApS', 'SeV'], 'เสร็จสิ้น'], ['งานที่เหลือ', 'ตรวจสอบ Work Products รอบสุดท้าย เตรียมชุดติดตั้ง ตรวจรับ อบรม และปิดโครงการ', ['ApS', 'YaB'], 'ดำเนินการ']],
actions: [['ตรวจสอบ Work Products รอบที่ 4 และจัดทำ List of Evidence', ['YaB', 'ApS'], '2026-08-17', 'Scheduled'], ['จัดทำชุดติดตั้ง Docker Compose และปรับแบรนด์ (CH-003)', ['ThS'], '2026-08-17', 'Scheduled'], ['นัดตรวจรับส่งมอบระบบกับ Project Sponsor', ['ApS'], '2026-08-17', 'Scheduled']], actions: [['ตรวจสอบ Work Products รอบที่ 4 และจัดทำ List of Evidence', ['YaB', 'ApS'], '2026-08-17', 'Scheduled'], ['จัดทำชุดติดตั้ง Docker Compose (CR10:002) และปรับแบรนด์ (Task 4.7)', ['ThS'], '2026-08-17', 'Scheduled'], ['นัดตรวจรับส่งมอบระบบกับ Project Sponsor', ['ApS'], '2026-08-17', 'Scheduled']],
next: { date: '2026-08-17', topic: 'สรุปผลการตรวจสอบรอบสุดท้ายและตรวจรับส่งมอบระบบ' }, next: { date: '2026-08-17', topic: 'สรุปผลการตรวจสอบรอบสุดท้ายและตรวจรับส่งมอบระบบ' },
}, },
{ {
date: '2026-08-17', phase: 'Project Close', taskId: '5.3', taskName: 'Acceptance & Project Closure', time: '09:00 – 11:00 น.', date: '2026-08-17', phase: 'Project Close', taskId: '5.3', taskName: 'Acceptance & Project Closure', time: '09:00 – 11:00 น.',
attachment: 'Acceptance Report, List of Evidence, Verification Results, Change Report CH-003', attachment: 'Acceptance Report, List of Evidence, Verification Results',
progress: [['Phase 3: Project Execution', 'เสร็จสมบูรณ์', 100, 'พัฒนาระบบครบตามขอบเขต Baseline 6c39700'], ['Phase 4: Verification & Validation', 'เสร็จสมบูรณ์', 100, 'ตรวจสอบ ทดสอบ และ UAT ผ่านครบทุกรายการ'], ['Phase 5: Project Close', 'กำลังดำเนินการ', 80, 'ตรวจรับระบบแล้ว เหลือการอบรมและปิดงานควบคุมปฏิบัติการ'], ['รวมความคืบหน้าทั้งโครงการ', 'ตามแผน', 96, 'ตรวจรับระบบเรียบร้อย']], progress: [['Phase 3: Project Execution', 'เสร็จสมบูรณ์', 100, 'พัฒนาระบบครบตามขอบเขต Baseline 6c39700'], ['Phase 4: Verification & Validation', 'เสร็จสมบูรณ์', 100, 'ตรวจสอบ ทดสอบ และ UAT ผ่านครบทุกรายการ'], ['Phase 5: Project Close', 'กำลังดำเนินการ', 80, 'ตรวจรับระบบแล้ว เหลือการอบรมและปิดงานควบคุมปฏิบัติการ'], ['รวมความคืบหน้าทั้งโครงการ', 'ตามแผน', 96, 'ตรวจรับระบบเรียบร้อย']],
agenda: [['นำเสนอผลการดำเนินโครงการและสิ่งส่งมอบ', 'สรุปผลงานทั้งหมดเทียบกับแผนและผลลัพธ์จริง'], ['ตรวจสอบความครบถ้วนของเอกสารและหลักฐาน', 'ยืนยันเอกสารครบตามมาตรฐาน ISO/IEC 29110'], ['พิจารณาคำขอเปลี่ยนแปลง CH-003', 'ประเมินผลกระทบและอนุมัติ'], ['พิจารณาตรวจรับส่งมอบระบบ', 'ลงมติผลการตรวจรับ']], agenda: [['นำเสนอผลการดำเนินโครงการและสิ่งส่งมอบ', 'สรุปผลงานทั้งหมดเทียบกับแผนและผลลัพธ์จริง'], ['ตรวจสอบความครบถ้วนของเอกสารและหลักฐาน', 'ยืนยันเอกสารครบตามมาตรฐาน ISO/IEC 29110'], ['ชุดติดตั้งและอัตลักษณ์องค์กร (Task 4.7)', 'ยืนยันความพร้อมของชุดส่งมอบ'], ['พิจารณาตรวจรับส่งมอบระบบ', 'ลงมติผลการตรวจรับ']],
discussion: [['ผลการดำเนินโครงการ', 'พัฒนาระบบครบตามขอบเขต 11 ระบบงาน ทดสอบและ UAT ผ่านทั้งหมด ไม่มีข้อบกพร่องระดับวิกฤตคงค้าง', ['ApS', 'SeV'], 'ผ่านการรับรอง'], ['ความครบถ้วนของเอกสาร', 'ตรวจสอบ Work Products รอบที่ 4 ครบทุกรายการ ผลผ่านทั้งหมด และจัดทำ List of Evidence เรียบร้อย', ['YaB', 'PaNg'], 'สมบูรณ์'], ['คำขอเปลี่ยนแปลง CH-003', 'ปรับอัตลักษณ์องค์กรและเพิ่มชุดติดตั้ง Docker Compose ประเมินผลกระทบระดับ Medium', ['SeV', 'ApS'], 'อนุมัติ'], ['ผลการตรวจรับ', 'Project Sponsor ตรวจรับสิ่งส่งมอบทั้งหมด ผลการตรวจรับ Accepted', ['SeV'], 'ตรวจรับแล้ว']], discussion: [['ผลการดำเนินโครงการ', 'พัฒนาระบบครบตามขอบเขต 11 ระบบงาน ทดสอบและ UAT ผ่านทั้งหมด ไม่มีข้อบกพร่องระดับวิกฤตคงค้าง', ['ApS', 'SeV'], 'ผ่านการรับรอง'], ['ความครบถ้วนของเอกสาร', 'ตรวจสอบ Work Products รอบที่ 4 ครบทุกรายการ ผลผ่านทั้งหมด และจัดทำ List of Evidence เรียบร้อย', ['YaB', 'PaNg'], 'สมบูรณ์'], ['ชุดติดตั้งและอัตลักษณ์องค์กร', 'จัดทำชุดติดตั้ง Docker Compose ตามความต้องการ CR10:002 และปรับอัตลักษณ์องค์กรในหน้าจอและเอกสารเสร็จแล้ว เป็นงานเตรียมการส่งมอบตาม Task 4.7 ไม่เปลี่ยนแปลงขอบเขตหรือสิ่งส่งมอบ', ['SeV', 'ApS'], 'เสร็จสิ้น'], ['ผลการตรวจรับ', 'Project Sponsor ตรวจรับสิ่งส่งมอบทั้งหมด ผลการตรวจรับ Accepted', ['SeV'], 'ตรวจรับแล้ว']],
actions: [['จัดอบรมผู้ใช้งานก่อนเปิดใช้งานจริง (Task 5.2)', ['ThS', 'PaNg'], '2026-08-22', 'Scheduled'], ['ปิดงานควบคุมการสำรองข้อมูลและการเฝ้าระวังระบบ', ['ThS'], '2026-08-23', 'Scheduled'], ['ปิดโครงการและส่งมอบเอกสารเข้าระบบควบคุมเอกสาร', ['ApS', 'YaB'], '2026-08-24', 'Scheduled']], actions: [['จัดอบรมผู้ใช้งานก่อนเปิดใช้งานจริง (Task 5.2)', ['ThS', 'PaNg'], '2026-08-22', 'Scheduled'], ['ปิดงานควบคุมการสำรองข้อมูลและการเฝ้าระวังระบบ', ['ThS'], '2026-08-23', 'Scheduled'], ['ปิดโครงการและส่งมอบเอกสารเข้าระบบควบคุมเอกสาร', ['ApS', 'YaB'], '2026-08-24', 'Scheduled']],
next: { date: '2026-08-24', topic: 'สรุปผลการอบรม การปิดงานควบคุมปฏิบัติการ และปิดโครงการอย่างเป็นทางการ' }, next: { date: '2026-08-24', topic: 'สรุปผลการอบรม การปิดงานควบคุมปฏิบัติการ และปิดโครงการอย่างเป็นทางการ' },
}, },
@@ -254,3 +265,10 @@ export const MEETINGS = [
next: null, next: null,
}, },
]; ];
// Sanity: every correction is drawn into exactly one lesson.
{
const drawn = LESSONS.flatMap(([, issueIds]) => issueIds);
const missing = CORRECTIONS.map(([id]) => id).filter((id) => !drawn.includes(id));
if (missing.length || drawn.length !== new Set(drawn).size) throw new Error(`Lessons must cover each correction once; missing: ${missing.join(', ')}`);
}
+82 -85
View File
@@ -38,7 +38,7 @@ export const WPS = [
['WP 2.0', `เอกสาร ${CODE} Customer Requirements`, 'ส่งเอกสารจำนวน 1 ชุด'], ['WP 2.0', `เอกสาร ${CODE} Customer Requirements`, 'ส่งเอกสารจำนวน 1 ชุด'],
['WP 3.0', `เอกสาร ${CODE} Software Requirements Specification`, 'ส่งเอกสารจำนวน 1 ชุด'], ['WP 3.0', `เอกสาร ${CODE} Software Requirements Specification`, 'ส่งเอกสารจำนวน 1 ชุด'],
['WP 4.0', `เอกสาร ${CODE} Software Design`, 'ส่งเอกสารจำนวน 1 ชุด'], ['WP 4.0', `เอกสาร ${CODE} Software Design`, 'ส่งเอกสารจำนวน 1 ชุด'],
['WP 5.0', `เอกสาร ${CODE} Change Report`, 'ส่งเอกสารจำนวน 3 ฉบับ (CH-001–CH-003)'], ['WP 5.0', `เอกสาร ${CODE} Change Report`, 'ส่งเอกสารจำนวน 1 ชุด (ทะเบียนการควบคุมการเปลี่ยนแปลง)'],
['WP 6.0', `เอกสาร ${CODE} Test Case and Test Procedures`, 'ส่งเอกสารจำนวน 1 ชุด'], ['WP 6.0', `เอกสาร ${CODE} Test Case and Test Procedures`, 'ส่งเอกสารจำนวน 1 ชุด'],
['WP 7.0', `เอกสาร ${CODE} Validation Results`, 'ส่งเอกสารจำนวน 1 ชุด'], ['WP 7.0', `เอกสาร ${CODE} Validation Results`, 'ส่งเอกสารจำนวน 1 ชุด'],
['WP 8.0', `เอกสาร ${CODE} Software User Document`, 'ส่งเอกสารจำนวน 1 ชุด'], ['WP 8.0', `เอกสาร ${CODE} Software User Document`, 'ส่งเอกสารจำนวน 1 ชุด'],
@@ -62,118 +62,115 @@ export const SCOPE = [
['ระบบติดตั้งและตั้งค่า', ['ติดตั้งแบบ Manual ผ่าน setup.php', 'ติดตั้งแบบ Container ผ่าน Docker Compose (php-apache, mariadb, node/pm2)', 'สร้างค่าตั้งค่าและความลับของระบบจาก .env โดยไม่เก็บใน Source Control']], ['ระบบติดตั้งและตั้งค่า', ['ติดตั้งแบบ Manual ผ่าน setup.php', 'ติดตั้งแบบ Container ผ่าน Docker Compose (php-apache, mariadb, node/pm2)', 'สร้างค่าตั้งค่าและความลับของระบบจาก .env โดยไม่เก็บใน Source Control']],
]; ];
/** /** Customer Requirements (CR01–CR14). Each: [id, text]. */
* Customer Requirements (CR01–CR14). Each: [id, text, remark].
* Remark keeps the original FR-/NFR- identifier that appeared in earlier drafts so cross-references stay valid.
*/
export const CR_GROUPS = [ export const CR_GROUPS = [
['CR01', 'Feature & Functional Characteristics', [ ['CR01', 'Feature & Functional Characteristics', [
['CR01:001', 'ระบบต้องรองรับการลงทะเบียนเจ้าของบริษัทและการเชิญผู้ใช้งานเข้าร่วมบริษัท (Onboarding)', 'FR-001'], ['CR01:001', 'ระบบต้องรองรับการลงทะเบียนเจ้าของบริษัทและการเชิญผู้ใช้งานเข้าร่วมบริษัท (Onboarding)'],
['CR01:002', 'ระบบต้องยืนยันตัวตนผู้ใช้และบังคับสิทธิ์ตามบทบาท Owner, Admin, Staff และ Viewer', 'FR-002'], ['CR01:002', 'ระบบต้องยืนยันตัวตนผู้ใช้และบังคับสิทธิ์ตามบทบาท Owner, Admin, Staff และ Viewer'],
['CR01:003', 'ระบบต้องรองรับการกู้คืนรหัสผ่าน การควบคุม Session และการยืนยัน OTP ตามที่กำหนด', 'FR-003'], ['CR01:003', 'ระบบต้องรองรับการกู้คืนรหัสผ่าน การควบคุม Session และการยืนยัน OTP ตามที่กำหนด'],
['CR01:004', 'ระบบต้องให้ผู้ดูแลจัดการข้อมูลบริษัท, SMTP, การตั้งค่าระบบ, ผู้ใช้งาน และสิทธิ์การเข้าถึงแอปพลิเคชัน', 'FR-004'], ['CR01:004', 'ระบบต้องให้ผู้ดูแลจัดการข้อมูลบริษัท, SMTP, การตั้งค่าระบบ, ผู้ใช้งาน และสิทธิ์การเข้าถึงแอปพลิเคชัน'],
['CR01:005', 'ระบบต้องจัดการข้อมูลคลังสินค้า, พื้นที่/ช่องจัดเก็บ, หมวดสินค้า, สินค้า, ประเภทผู้ติดต่อ และผู้ติดต่อ', 'FR-005'], ['CR01:005', 'ระบบต้องจัดการข้อมูลคลังสินค้า, พื้นที่/ช่องจัดเก็บ, หมวดสินค้า, สินค้า, ประเภทผู้ติดต่อ และผู้ติดต่อ'],
['CR01:006', 'ระบบต้องรองรับโครงสร้างตำแหน่งจัดเก็บทั้งแบบคลังเดียวและแบบหลายชั้น (คลัง/พื้นที่/ช่อง)', 'FR-006'], ['CR01:006', 'ระบบต้องรองรับโครงสร้างตำแหน่งจัดเก็บทั้งแบบคลังเดียวและแบบหลายชั้น (คลัง/พื้นที่/ช่อง)'],
['CR01:007', 'ระบบต้องบันทึกการรับสินค้าเข้า (Stock-in) ระบุสินค้า จำนวน ตำแหน่ง เอกสารอ้างอิง และข้อมูลติดตาม', 'FR-007'], ['CR01:007', 'ระบบต้องบันทึกการรับสินค้าเข้า (Stock-in) ระบุสินค้า จำนวน ตำแหน่ง เอกสารอ้างอิง และข้อมูลติดตาม'],
['CR01:008', 'ระบบต้องบันทึกการจ่ายสินค้าออก (Stock-out) โดยตรวจสอบสิทธิ์และยอดคงเหลือก่อนจ่าย', 'FR-008'], ['CR01:008', 'ระบบต้องบันทึกการจ่ายสินค้าออก (Stock-out) โดยตรวจสอบสิทธิ์และยอดคงเหลือก่อนจ่าย'],
['CR01:009', 'ระบบต้องโอนย้ายสินค้าระหว่างตำแหน่งจัดเก็บที่ได้รับอนุญาตโดยยอดต้นทาง/ปลายทางสมดุลกัน', 'FR-009'], ['CR01:009', 'ระบบต้องโอนย้ายสินค้าระหว่างตำแหน่งจัดเก็บที่ได้รับอนุญาตโดยยอดต้นทาง/ปลายทางสมดุลกัน'],
['CR01:010', 'ระบบต้องติดตาม Lot, Serial Number และวันหมดอายุของสินค้าที่เกี่ยวข้อง', 'FR-010'], ['CR01:010', 'ระบบต้องติดตาม Lot, Serial Number และวันหมดอายุของสินค้าที่เกี่ยวข้อง'],
['CR01:011', 'ระบบต้องแสดงภาพรวมสต๊อก, ประวัติความเคลื่อนไหว, ความจุ/การใช้พื้นที่, สินค้าใกล้หมด, สินค้าหมดอายุ และข้อมูล Lot', 'FR-011'], ['CR01:011', 'ระบบต้องแสดงภาพรวมสต๊อก, ประวัติความเคลื่อนไหว, ความจุ/การใช้พื้นที่, สินค้าใกล้หมด, สินค้าหมดอายุ และข้อมูล Lot'],
['CR01:012', 'ระบบต้องพิมพ์บาร์โค้ดสินค้า (SKU) และตำแหน่งจัดเก็บ และรองรับการสแกนในหน้าจอที่กำหนด', 'FR-012'], ['CR01:012', 'ระบบต้องพิมพ์บาร์โค้ดสินค้า (SKU) และตำแหน่งจัดเก็บ และรองรับการสแกนในหน้าจอที่กำหนด'],
['CR01:013', 'ระบบต้องสร้างและจัดการใบเสนอราคา, ใบสั่งขาย, ใบแจ้งหนี้, ใบรับคืน และใบลดหนี้', 'FR-013'], ['CR01:013', 'ระบบต้องสร้างและจัดการใบเสนอราคา, ใบสั่งขาย, ใบแจ้งหนี้, ใบรับคืน และใบลดหนี้'],
['CR01:014', 'ระบบต้องสร้างและจัดการใบขอซื้อ, ใบสั่งซื้อ, ใบแจ้งหนี้ซื้อ และใบคืนสินค้าผู้ขาย', 'FR-014'], ['CR01:014', 'ระบบต้องสร้างและจัดการใบขอซื้อ, ใบสั่งซื้อ, ใบแจ้งหนี้ซื้อ และใบคืนสินค้าผู้ขาย'],
['CR01:015', 'ระบบต้องสร้างและจัดการใบวางบิลรับ, ใบเสร็จรับเงิน, ใบวางบิลจ่าย และใบสำคัญจ่าย', 'FR-015'], ['CR01:015', 'ระบบต้องสร้างและจัดการใบวางบิลรับ, ใบเสร็จรับเงิน, ใบวางบิลจ่าย และใบสำคัญจ่าย'],
['CR01:016', 'ระบบต้องจัดการผังบัญชี, แผนก, สูตรบัญชี, สมุดรายวัน และบัญชีแยกประเภท', 'FR-016'], ['CR01:016', 'ระบบต้องจัดการผังบัญชี, แผนก, สูตรบัญชี, สมุดรายวัน และบัญชีแยกประเภท'],
['CR01:017', 'ระบบต้องจัดทำรายงานงบทดลอง, งบกำไรขาดทุน, งบดุล, ภาษีมูลค่าเพิ่ม, สมุดรายวัน และความเคลื่อนไหว GL', 'FR-017'], ['CR01:017', 'ระบบต้องจัดทำรายงานงบทดลอง, งบกำไรขาดทุน, งบดุล, ภาษีมูลค่าเพิ่ม, สมุดรายวัน และความเคลื่อนไหว GL'],
['CR01:018', 'ระบบต้องออกเลขที่เอกสารอัตโนมัติและควบคุมสถานะ/วงจรชีวิตของเอกสาร', 'FR-018'], ['CR01:018', 'ระบบต้องออกเลขที่เอกสารอัตโนมัติและควบคุมสถานะ/วงจรชีวิตของเอกสาร'],
['CR01:019', 'ระบบต้องรองรับการแนบไฟล์ที่อนุญาตกับรายการที่กำหนด', 'FR-019'], ['CR01:019', 'ระบบต้องรองรับการแนบไฟล์ที่อนุญาตกับรายการที่กำหนด'],
['CR01:020', 'ระบบต้องให้ผู้ใช้กรอง ดู พิมพ์ และส่งออกรายงานปฏิบัติการและรายงานผู้บริหาร', 'FR-020'], ['CR01:020', 'ระบบต้องให้ผู้ใช้กรอง ดู พิมพ์ และส่งออกรายงานปฏิบัติการและรายงานผู้บริหาร'],
['CR01:021', 'ระบบต้องแจ้งเตือนผู้ใช้ที่เกี่ยวข้องเมื่อสถานะเอกสารเปลี่ยนหรือมีเหตุการณ์ปฏิบัติการ', 'FR-021'], ['CR01:021', 'ระบบต้องแจ้งเตือนผู้ใช้ที่เกี่ยวข้องเมื่อสถานะเอกสารเปลี่ยนหรือมีเหตุการณ์ปฏิบัติการ'],
['CR01:022', 'ระบบต้องสรุปยอดสต๊อก/GL และแจ้งเตือนสินค้าใกล้หมดและใบแจ้งหนี้ค้างชำระตามกำหนดเวลา', 'FR-022'], ['CR01:022', 'ระบบต้องสรุปยอดสต๊อก/GL และแจ้งเตือนสินค้าใกล้หมดและใบแจ้งหนี้ค้างชำระตามกำหนดเวลา'],
['CR01:023', 'ระบบต้องเก็บผู้สร้าง ผู้แก้ไข สถานะ และประวัติรายการเพื่อการตรวจสอบ', 'FR-023'], ['CR01:023', 'ระบบต้องเก็บผู้สร้าง ผู้แก้ไข สถานะ และประวัติรายการเพื่อการตรวจสอบ'],
['CR01:024', 'ระบบต้องจำกัดข้อมูลบริษัทและคลังสินค้าให้เฉพาะผู้ใช้ที่ได้รับอนุญาตในบริบทปัจจุบัน', 'FR-024'], ['CR01:024', 'ระบบต้องจำกัดข้อมูลบริษัทและคลังสินค้าให้เฉพาะผู้ใช้ที่ได้รับอนุญาตในบริบทปัจจุบัน'],
]], ]],
['CR02', 'Performance Considerations', [ ['CR02', 'Performance Considerations', [
['CR02:001', 'ระบบต้องตอบสนองงานประจำวัน (เปิดหน้าจอ, ค้นหา, สร้างเอกสาร) ภายในเวลาที่ใช้งานได้จริงบนสภาพแวดล้อมที่ตกลง', 'NFR-006'], ['CR02:001', 'ระบบต้องตอบสนองงานประจำวัน (เปิดหน้าจอ, ค้นหา, สร้างเอกสาร) ภายในเวลาที่ใช้งานได้จริงบนสภาพแวดล้อมที่ตกลง'],
['CR02:002', 'ระบบต้องมีตารางสรุปยอด (Aggregate) เพื่อให้แดชบอร์ดและรายงานแสดงผลได้โดยไม่ต้องคำนวณใหม่ทุกครั้ง', 'NFR-006'], ['CR02:002', 'ระบบต้องมีตารางสรุปยอด (Aggregate) เพื่อให้แดชบอร์ดและรายงานแสดงผลได้โดยไม่ต้องคำนวณใหม่ทุกครั้ง'],
['CR02:003', 'ระบบต้องรองรับปริมาณข้อมูลและผู้ใช้พร้อมกันในระดับที่ตกลงสำหรับสภาพแวดล้อมใช้งานจริง', 'NFR-006'], ['CR02:003', 'ระบบต้องรองรับปริมาณข้อมูลและผู้ใช้พร้อมกันในระดับที่ตกลงสำหรับสภาพแวดล้อมใช้งานจริง'],
]], ]],
['CR03', 'Interface Considerations', [ ['CR03', 'Interface Considerations', [
['CR03:001', 'ระบบต้องเชื่อมต่อฐานข้อมูล MySQL/MariaDB 2 ฐาน (wms สำหรับผู้ใช้/บริษัท และ wms2 สำหรับคลัง/บัญชี)', 'Operational context'], ['CR03:001', 'ระบบต้องเชื่อมต่อฐานข้อมูล MySQL/MariaDB 2 ฐาน (wms สำหรับผู้ใช้/บริษัท และ wms2 สำหรับคลัง/บัญชี)'],
['CR03:002', 'ระบบต้องใช้งานผ่าน Web Browser มาตรฐาน (Chrome, Edge, Firefox) ได้', 'NFR-008'], ['CR03:002', 'ระบบต้องใช้งานผ่าน Web Browser มาตรฐาน (Chrome, Edge, Firefox) ได้'],
['CR03:003', 'ระบบต้องส่งเหตุการณ์ไปยังบริการ Node.js ผ่าน Endpoint ภายในที่ป้องกันด้วย Secret', 'NFR-002'], ['CR03:003', 'ระบบต้องส่งเหตุการณ์ไปยังบริการ Node.js ผ่าน Endpoint ภายในที่ป้องกันด้วย Secret'],
['CR03:004', 'Browser ต้องเชื่อมต่อ Socket.IO Endpoint สาธารณะเพื่อรับการแจ้งเตือนแบบ Real-time', 'FR-021'], ['CR03:004', 'Browser ต้องเชื่อมต่อ Socket.IO Endpoint สาธารณะเพื่อรับการแจ้งเตือนแบบ Real-time'],
['CR03:005', 'ระบบต้องส่งอีเมล Onboarding, กู้คืนรหัสผ่าน และแจ้งเตือนผ่าน SMTP ที่ตั้งค่าต่อบริษัท', 'FR-004'], ['CR03:005', 'ระบบต้องส่งอีเมล Onboarding, กู้คืนรหัสผ่าน และแจ้งเตือนผ่าน SMTP ที่ตั้งค่าต่อบริษัท'],
]], ]],
['CR04', 'Required System Characteristics', [ ['CR04', 'Required System Characteristics', [
['CR04:001', 'ระบบต้องพัฒนาบนสถาปัตยกรรม Web-based Application', 'Operational context'], ['CR04:001', 'ระบบต้องพัฒนาบนสถาปัตยกรรม Web-based Application'],
['CR04:002', 'ระบบต้องเก็บข้อมูลแบบ Relational Database และรักษา Referential Integrity', 'Data requirements'], ['CR04:002', 'ระบบต้องเก็บข้อมูลแบบ Relational Database และรักษา Referential Integrity'],
['CR04:003', 'ระบบต้องรองรับการเข้าสู่ระบบด้วย Username/Password และกำหนดบทบาทผู้ใช้', 'FR-002'], ['CR04:003', 'ระบบต้องรองรับการเข้าสู่ระบบด้วย Username/Password และกำหนดบทบาทผู้ใช้'],
['CR04:004', 'ระบบต้องทำงานบน PHP 8 ขึ้นไป, MySQL/MariaDB และ Node.js', 'NFR-008'], ['CR04:004', 'ระบบต้องทำงานบน PHP 8 ขึ้นไป, MySQL/MariaDB และ Node.js'],
]], ]],
['CR05', 'Human Engineering Considerations', [ ['CR05', 'Human Engineering Considerations', [
['CR05:001', 'UI ต้องเป็น Responsive ใช้งานได้ทั้งบน Desktop และอุปกรณ์หน้าคลังสินค้า (Tablet/Mobile)', 'NFR-005'], ['CR05:001', 'UI ต้องเป็น Responsive ใช้งานได้ทั้งบน Desktop และอุปกรณ์หน้าคลังสินค้า (Tablet/Mobile)'],
['CR05:002', 'เมนูและปุ่มคำสั่งต้องแสดงตามบทบาทและสิทธิ์การเข้าถึงของผู้ใช้', 'Interface requirements'], ['CR05:002', 'เมนูและปุ่มคำสั่งต้องแสดงตามบทบาทและสิทธิ์การเข้าถึงของผู้ใช้'],
['CR05:003', 'ระบบต้องแสดงผลการตรวจสอบข้อมูล สถานะ ความสำเร็จ และข้อผิดพลาดอย่างชัดเจน', 'Interface requirements'], ['CR05:003', 'ระบบต้องแสดงผลการตรวจสอบข้อมูล สถานะ ความสำเร็จ และข้อผิดพลาดอย่างชัดเจน'],
['CR05:004', 'ระบบต้องมีขั้นตอนยืนยันก่อนลบ ยกเลิก หรือทำรายการที่ย้อนกลับไม่ได้', 'Usability practice'], ['CR05:004', 'ระบบต้องมีขั้นตอนยืนยันก่อนลบ ยกเลิก หรือทำรายการที่ย้อนกลับไม่ได้'],
['CR05:005', 'ระบบต้องพิมพ์เอกสารธุรกิจและฉลากบาร์โค้ดในรูปแบบที่ใช้งานได้', 'Interface requirements'], ['CR05:005', 'ระบบต้องพิมพ์เอกสารธุรกิจและฉลากบาร์โค้ดในรูปแบบที่ใช้งานได้'],
]], ]],
['CR06', 'Security Considerations', [ ['CR06', 'Security Considerations', [
['CR06:001', 'การเชื่อมต่อในสภาพแวดล้อมใช้งานจริงต้องเข้ารหัสด้วย HTTPS/TLS', 'NFR-001'], ['CR06:001', 'การเชื่อมต่อในสภาพแวดล้อมใช้งานจริงต้องเข้ารหัสด้วย HTTPS/TLS'],
['CR06:002', 'ค่าตั้งค่าและความลับของระบบต้องไม่ถูกเก็บใน Source Control และไม่เข้าถึงได้จากเว็บสาธารณะ', 'NFR-001'], ['CR06:002', 'ค่าตั้งค่าและความลับของระบบต้องไม่ถูกเก็บใน Source Control และไม่เข้าถึงได้จากเว็บสาธารณะ'],
['CR06:003', 'การทำงานฝั่งเซิร์ฟเวอร์ต้องตรวจสอบข้อมูลนำเข้า ยืนยันตัวตน ตรวจสิทธิ์ และจำกัดขอบเขตบริษัท/คลัง', 'NFR-002'], ['CR06:003', 'การทำงานฝั่งเซิร์ฟเวอร์ต้องตรวจสอบข้อมูลนำเข้า ยืนยันตัวตน ตรวจสิทธิ์ และจำกัดขอบเขตบริษัท/คลัง'],
['CR06:004', 'ระบบต้องจำกัดสิทธิ์การเข้าถึงตามบทบาท (Role-based Access Control) ทั้งใน UI และฝั่งเซิร์ฟเวอร์', 'FR-002'], ['CR06:004', 'ระบบต้องจำกัดสิทธิ์การเข้าถึงตามบทบาท (Role-based Access Control) ทั้งใน UI และฝั่งเซิร์ฟเวอร์'],
['CR06:005', 'ระบบต้องป้องกัน SQL Injection และ Cross-Site Scripting', 'NFR-002'], ['CR06:005', 'ระบบต้องป้องกัน SQL Injection และ Cross-Site Scripting'],
['CR06:006', 'ระบบต้องบล็อกการเข้าสู่ระบบซ้ำซ้อน (Concurrent Login) ของบัญชีเดียวกัน', 'FR-003'], ['CR06:006', 'ระบบต้องบล็อกการเข้าสู่ระบบซ้ำซ้อน (Concurrent Login) ของบัญชีเดียวกัน'],
]], ]],
['CR07', 'Environmental Considerations', [ ['CR07', 'Environmental Considerations', [
['CR07:001', 'ระบบต้องทำงานบน Linux Server ในรูปแบบติดตั้งเอง (LAMP) หรือ Docker Compose', 'NFR-004'], ['CR07:001', 'ระบบต้องทำงานบน Linux Server ในรูปแบบติดตั้งเอง (LAMP) หรือ Docker Compose'],
['CR07:002', 'ระบบต้องใช้เขตเวลา Asia/Bangkok อย่างสม่ำเสมอทั้งแอปพลิเคชันและงานตามกำหนดเวลา', 'NFR-010'], ['CR07:002', 'ระบบต้องใช้เขตเวลา Asia/Bangkok อย่างสม่ำเสมอทั้งแอปพลิเคชันและงานตามกำหนดเวลา'],
['CR07:003', 'ระบบต้องใช้งานได้บนอุปกรณ์ Desktop และอุปกรณ์พกพาผ่าน Web Browser', 'NFR-005'], ['CR07:003', 'ระบบต้องใช้งานได้บนอุปกรณ์ Desktop และอุปกรณ์พกพาผ่าน Web Browser'],
['CR07:004', 'ระบบต้องแยกข้อมูลสาธิต/ทดสอบออกจากข้อมูลใช้งานจริงได้', 'Data requirements'], ['CR07:004', 'ระบบต้องแยกข้อมูลสาธิต/ทดสอบออกจากข้อมูลใช้งานจริงได้'],
]], ]],
['CR08', 'Operational Considerations', [ ['CR08', 'Operational Considerations', [
['CR08:001', 'ระบบต้องมีการสำรองฐานข้อมูลอัตโนมัติรายวัน', 'NFR-004'], ['CR08:001', 'ระบบต้องมีการสำรองฐานข้อมูลอัตโนมัติรายวัน'],
['CR08:002', 'ระบบต้องกู้คืนข้อมูลจากชุดสำรองได้ตามขั้นตอนที่จัดทำเป็นเอกสาร', 'NFR-004'], ['CR08:002', 'ระบบต้องกู้คืนข้อมูลจากชุดสำรองได้ตามขั้นตอนที่จัดทำเป็นเอกสาร'],
['CR08:003', 'ระบบต้องมีการเฝ้าระวังสถานะบริการ Web, Database และ Node.js พร้อมแจ้งเตือนเมื่อขัดข้อง', 'NFR-004'], ['CR08:003', 'ระบบต้องมีการเฝ้าระวังสถานะบริการ Web, Database และ Node.js พร้อมแจ้งเตือนเมื่อขัดข้อง'],
['CR08:004', 'ระบบต้องรองรับหลายบริษัท (Multi-company) และหลายคลังสินค้าภายใต้การแยกข้อมูล', 'FR-024'], ['CR08:004', 'ระบบต้องรองรับหลายบริษัท (Multi-company) และหลายคลังสินค้าภายใต้การแยกข้อมูล'],
['CR08:005', 'งานตามกำหนดเวลาต้องทำงานสำเร็จโดยไม่สร้างผลลัพธ์ซ้ำหรือเกินสิทธิ์', 'FR-022'], ['CR08:005', 'งานตามกำหนดเวลาต้องทำงานสำเร็จโดยไม่สร้างผลลัพธ์ซ้ำหรือเกินสิทธิ์'],
]], ]],
['CR09', 'Maintenance Considerations', [ ['CR09', 'Maintenance Considerations', [
['CR09:001', 'ซอฟต์แวร์ต้องมีโครงสร้างแบบโมดูล (Manager Classes / API) เพื่อให้แก้ไขได้โดยไม่กระทบส่วนอื่น', 'NFR-007'], ['CR09:001', 'ซอฟต์แวร์ต้องมีโครงสร้างแบบโมดูล (Manager Classes / API) เพื่อให้แก้ไขได้โดยไม่กระทบส่วนอื่น'],
['CR09:002', 'ระบบต้องมีแม่แบบค่าตั้งค่า (Configuration Template) และควบคุมเวอร์ชันด้วย Git', 'NFR-007'], ['CR09:002', 'ระบบต้องมีแม่แบบค่าตั้งค่า (Configuration Template) และควบคุมเวอร์ชันด้วย Git'],
['CR09:003', 'ระบบต้องมีเอกสารคู่มือการบำรุงรักษาและประวัติข้อบกพร่องที่แก้ไขแล้ว', 'NFR-004'], ['CR09:003', 'ระบบต้องมีเอกสารคู่มือการบำรุงรักษาและประวัติข้อบกพร่องที่แก้ไขแล้ว'],
['CR09:004', 'การเปลี่ยนแปลงโครงสร้างฐานข้อมูลต้องสะท้อนใน setup.php และตาราง schema_migrations', 'NFR-007'], ['CR09:004', 'การเปลี่ยนแปลงโครงสร้างฐานข้อมูลต้องสะท้อนใน setup.php และตาราง schema_migrations'],
]], ]],
['CR10', 'Installation Considerations', [ ['CR10', 'Installation Considerations', [
['CR10:001', 'ระบบต้องติดตั้งฐานข้อมูลได้ในขั้นตอนเดียวผ่าน setup.php', 'NFR-004'], ['CR10:001', 'ระบบต้องติดตั้งฐานข้อมูลได้ในขั้นตอนเดียวผ่าน setup.php'],
['CR10:002', 'ระบบต้องติดตั้งแบบ Container ได้ด้วยคำสั่ง docker compose up -d --build', 'NFR-004'], ['CR10:002', 'ระบบต้องติดตั้งแบบ Container ได้ด้วยคำสั่ง docker compose up -d --build'],
['CR10:003', 'ระบบต้องมีสคริปต์สร้างไฟล์ .env และค่าความลับอัตโนมัติ (docker/init-env.sh)', 'NFR-001'], ['CR10:003', 'ระบบต้องมีสคริปต์สร้างไฟล์ .env และค่าความลับอัตโนมัติ (docker/init-env.sh)'],
['CR10:004', 'ระบบต้องมีเอกสารขั้นตอนการติดตั้งและตั้งค่า', 'NFR-004'], ['CR10:004', 'ระบบต้องมีเอกสารขั้นตอนการติดตั้งและตั้งค่า'],
]], ]],
['CR11', 'Support Considerations', [ ['CR11', 'Support Considerations', [
['CR11:001', 'ต้องมีคู่มือผู้ใช้งาน (Software User Document)', 'Documentation'], ['CR11:001', 'ต้องมีคู่มือผู้ใช้งาน (Software User Document)'],
['CR11:002', 'ต้องมีคู่มือปฏิบัติงานสำหรับผู้ดูแลระบบ (Product Operation Guide)', 'Documentation'], ['CR11:002', 'ต้องมีคู่มือปฏิบัติงานสำหรับผู้ดูแลระบบ (Product Operation Guide)'],
['CR11:003', 'ต้องมีการอบรมผู้ใช้งานก่อนเปิดใช้งานจริง', 'Training'], ['CR11:003', 'ต้องมีการอบรมผู้ใช้งานก่อนเปิดใช้งานจริง'],
['CR11:004', 'ต้องมีช่องทางสนับสนุนและระดับการให้บริการ (SLA) หลังส่งมอบ', 'Maintenance'], ['CR11:004', 'ต้องมีช่องทางสนับสนุนและระดับการให้บริการ (SLA) หลังส่งมอบ'],
]], ]],
['CR12', 'Design Constraints', [ ['CR12', 'Design Constraints', [
['CR12:001', 'ระบบต้องพัฒนาด้วย PHP, MariaDB และ Node.js ตามที่องค์กรอนุมัติ', 'NFR-008'], ['CR12:001', 'ระบบต้องพัฒนาด้วย PHP, MariaDB และ Node.js ตามที่องค์กรอนุมัติ'],
['CR12:002', 'ระบบต้องควบคุม Source Code ด้วย Git โดยมี main เป็น Baseline หลัก', 'Configuration'], ['CR12:002', 'ระบบต้องควบคุม Source Code ด้วย Git โดยมี main เป็น Baseline หลัก'],
['CR12:003', 'รหัสผ่านผู้ใช้ต้องเข้ารหัสด้วย bcrypt และไม่เก็บหรือบันทึกเป็นข้อความธรรมดา', 'NFR-002'], ['CR12:003', 'รหัสผ่านผู้ใช้ต้องเข้ารหัสด้วย bcrypt และไม่เก็บหรือบันทึกเป็นข้อความธรรมดา'],
['CR12:004', 'โครงการต้องจัดทำ Work Products ตามมาตรฐาน ISO/IEC 29110 Basic Profile', 'NFR-009'], ['CR12:004', 'โครงการต้องจัดทำ Work Products ตามมาตรฐาน ISO/IEC 29110 Basic Profile'],
]], ]],
['CR13', 'Safety and Reliability Considerations', [ ['CR13', 'Safety and Reliability Considerations', [
['CR13:001', 'การเปลี่ยนแปลงฐานข้อมูลที่เกี่ยวข้องกัน (เอกสาร + สต๊อก + GL) ต้องเป็น Transaction เดียวกัน', 'NFR-003'], ['CR13:001', 'การเปลี่ยนแปลงฐานข้อมูลที่เกี่ยวข้องกัน (เอกสาร + สต๊อก + GL) ต้องเป็น Transaction เดียวกัน'],
['CR13:002', 'ระบบต้องป้องกันยอดสต๊อกติดลบและความเคลื่อนไหวซ้ำซ้อน', 'NFR-003'], ['CR13:002', 'ระบบต้องป้องกันยอดสต๊อกติดลบและความเคลื่อนไหวซ้ำซ้อน'],
['CR13:003', 'ระบบต้องรองรับการสำรองและกู้คืน (Backup & Recovery) ทั้ง Source Code และฐานข้อมูล', 'NFR-004'], ['CR13:003', 'ระบบต้องรองรับการสำรองและกู้คืน (Backup & Recovery) ทั้ง Source Code และฐานข้อมูล'],
['CR13:004', 'ระบบต้องจัดการข้อผิดพลาดโดยแสดงข้อความที่เข้าใจได้แทน Fatal Error', 'NFR-006'], ['CR13:004', 'ระบบต้องจัดการข้อผิดพลาดโดยแสดงข้อความที่เข้าใจได้แทน Fatal Error'],
]], ]],
['CR14', 'Quality Expectations', [ ['CR14', 'Quality Expectations', [
['CR14:001', 'ระบบต้องผ่านการทดสอบตาม Test Case ที่กำหนดก่อนส่งมอบ', 'Acceptance'], ['CR14:001', 'ระบบต้องผ่านการทดสอบตาม Test Case ที่กำหนดก่อนส่งมอบ'],
['CR14:002', 'ทุกความต้องการต้องสอบกลับได้ถึงการออกแบบ ส่วนประกอบ และหลักฐานการทดสอบ', 'NFR-009'], ['CR14:002', 'ทุกความต้องการต้องสอบกลับได้ถึงการออกแบบ ส่วนประกอบ และหลักฐานการทดสอบ'],
['CR14:003', 'ระบบต้องผ่านการทดสอบการยอมรับ (UAT) โดยตัวแทนลูกค้าบนสภาพแวดล้อมใช้งานจริง', 'Acceptance'], ['CR14:003', 'ระบบต้องผ่านการทดสอบการยอมรับ (UAT) โดยตัวแทนลูกค้าบนสภาพแวดล้อมใช้งานจริง'],
['CR14:004', 'ต้องไม่มีข้อบกพร่องระดับวิกฤตค้างอยู่ในด้านความปลอดภัย การแยกข้อมูล และความถูกต้องของสต๊อก/บัญชี', 'Acceptance'], ['CR14:004', 'ต้องไม่มีข้อบกพร่องระดับวิกฤตค้างอยู่ในด้านความปลอดภัย การแยกข้อมูล และความถูกต้องของสต๊อก/บัญชี'],
]], ]],
]; ];
export const CRS = CR_GROUPS.flatMap(([, , rows]) => rows); export const CRS = CR_GROUPS.flatMap(([, , rows]) => rows);
BIN
View File
Binary file not shown.

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