Compare commits

1 Commits
Author SHA1 Message Date
Thanakorn de6ff85fa1 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.
2026-09-24 08:45:20 +07:00
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"
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
echo "Installing the local PDF renderer..."
npm install --prefix "$TOOL_DIR"
npx --prefix "$TOOL_DIR" playwright install chromium
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"
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. */
table.dense { font-size: 7.2pt; }
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):
"""Drop the letterhead logo left loose in the body.
The work products carry no figures of their own, but the converter draws
each bullet as a small image, so only the logo is removed - anything that
size would be furniture, anything smaller is a list marker.
The converter draws each bullet as a small image, so only the logo is
removed: it is square and at least LOGO_MIN_PX, whereas anything smaller is
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 = {
relationship_id
for relationship_id, part in document.part.related_parts.items()
if getattr(part, 'image', None) is not None
and max(part.image.px_width, part.image.px_height) >= LOGO_MIN_PX
if getattr(part, 'image', None) is not None and is_logo(part.image)
}
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 { readFileSync } from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { chromium } from 'playwright';
@@ -101,7 +102,7 @@ function renderParagraph(lines) {
}).join('');
}
function markdownToHtml(markdown) {
function markdownToHtml(markdown, baseDir) {
const lines = markdown.replaceAll('\r\n', '\n').split('\n');
const output = [];
let index = 0;
@@ -110,6 +111,15 @@ function markdownToHtml(markdown) {
const line = lines[index];
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)) {
const table = [];
while (index < lines.length && isTableLine(lines[index])) table.push(lines[index++]);
@@ -149,7 +159,7 @@ function markdownToHtml(markdown) {
}
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 html = `<p${isApproval ? ' class="approval-fields"' : ''}>${renderParagraph(paragraph)}</p>`;
// 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; }'
: '';
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 });
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))
.replace(/^<!--\s*footer:.*?-->\s*$/m, '')
.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
// 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.
+1
View File
@@ -16,6 +16,7 @@ Approval signature tables on every document.
| `history.mjs` | Work Schedule, reporting periods, meetings, Correction Register, Change Reports |
| `lib.mjs` | Buddhist-calendar dates, Markdown tables, signature blocks |
| `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 |
| `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 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.
const FOLDERS = [
@@ -71,7 +72,16 @@ async function main() {
await fs.rm(target, { recursive: true, force: true });
for (const folder of FOLDERS) await fs.mkdir(path.join(target, folder), { recursive: true });
for (const d of docs) await fs.writeFile(path.join(target, d.dir, `${d.name}.md`), d.md, 'utf8');
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 = [];
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.
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 { 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 { workProduct, head } from './format.mjs';
@@ -119,8 +119,9 @@ export function traceabilityTable() {
const date = '2026-08-17';
const rows = CRS.map(([id, text]) => {
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 {
dir: OTHER,
name: `${CODE} TRACEABILITY-RECORD ${bud(date)} V1.0 NoC`,
@@ -136,14 +137,15 @@ export function traceabilityTable() {
'## วัตถุประสงค์',
'ตารางนี้สรุปเส้นทางการสอบกลับของความต้องการทุกรายการ ตั้งแต่ความต้องการของลูกค้าจนถึงผลการทดสอบและผลการทดสอบการยอมรับ เพื่อใช้ตรวจสอบความครบถ้วนได้ในหน้าเดียว โดยรายละเอียดหัวข้อของแต่ละรหัสอยู่ในเอกสาร 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(['รายการ', 'จำนวน'], [
['ความต้องการของลูกค้าทั้งหมด', CRS.length],
['เชื่อมโยงกับความต้องการซอฟต์แวร์', CRS.length],
['เชื่อมโยงกับ Software Unit', CRS.length],
['เชื่อมโยงกับ Test Case', CRS.length],
['ผลการทดสอบผ่าน', CRS.length],
['เชื่อมโยงกับ Software Unit', traced.filter((t) => t.un.length).length],
['เชื่อมโยงกับ Test Case', traced.filter((t) => t.tcs.length).length],
['ยืนยันด้วยเอกสารส่งมอบ (ไม่มี Software Unit)', traced.filter((t) => !t.tcs.length).length],
['ผลการทดสอบหรือการตรวจสอบเอกสารผ่าน', CRS.length],
['ผลการทดสอบการยอมรับผ่าน', CRS.length],
], ['', 'r']),
].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.
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 { workProduct, head } from './format.mjs';
@@ -97,7 +97,7 @@ export function workSchedule() {
['ตั้ง Baseline ความต้องการและแผนงาน', thLong('2026-02-18'), 'อนุมัติ Customer Requirements และ Work Schedule'],
['เริ่มพัฒนาระบบ', thLong(DEV_START), 'เริ่ม Task 3.1 ตามแผน'],
['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-22'), 'อบรมผู้ใช้งาน 6 คน'],
['ปิดโครงการ', thLong(END), 'ปิดโครงการอย่างเป็นทางการ'],
@@ -185,11 +185,11 @@ export function softwareProjectPlan() {
'ตรวจสอบย้อนกลับ (Audit) และตรวจรับงานได้อย่างเป็นทางการ',
]),
table(['ขั้นตอน', 'แนวทางที่ใช้จริงในโครงการ'], [
['Requirements', 'เก็บความต้องการและตั้ง Baseline ก่อนเริ่มพัฒนา การเปลี่ยนแปลงภายหลังผ่าน Change Report (CH-001–CH-003)'],
['Requirements', 'เก็บความต้องการและตั้ง Baseline ก่อนเริ่มพัฒนา การเปลี่ยนแปลงภายหลังต้องพิจารณาตามเกณฑ์ใน Change Report ก่อนดำเนินการ'],
['Design', 'ออกแบบสถาปัตยกรรม โครงสร้างฐานข้อมูล และ Software Unit ก่อนพัฒนาแต่ละโมดูล'],
['Implementation', 'พัฒนาเป็นโมดูลต่อเนื่องระหว่าง 19 ก.พ. – 29 พ.ค. 69 พร้อมทบทวนและแก้ไขระหว่างทาง'],
['Verification', 'ตรวจสอบ Work Products 4 รอบ ควบคู่กับการพัฒนา และรอบสุดท้ายก่อนส่งมอบ'],
['Validation', 'ทดสอบระบบและ UAT ระหว่าง 10–14 ส.ค. 69 บนสภาพแวดล้อมที่กำหนด'],
['Verification', 'ตรวจสอบ Work Products 4 รอบ ประมาณทุก 2 เดือน และรอบสุดท้ายก่อนส่งมอบ ตามแผนในหัวข้อ 8.1'],
['Validation', 'ทดสอบระบบและ UAT ระหว่าง 10–14 ส.ค. 69 บนสภาพแวดล้อมที่กำหนด โดยใช้ Customer Requirements ทุกรายการเป็นตัวตั้ง'],
['Closure', 'ตรวจรับ อบรม ปิดงานควบคุมปฏิบัติการ และปิดโครงการภายใน 24 ส.ค. 69'],
]),
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'],
['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'),
'วัตถุประสงค์ของแผนบริหารความเสี่ยง คือระบุแนวทางจัดการความเสี่ยงของโครงการ เพื่อให้โครงการบรรลุเป้าหมายภายในเวลา งบประมาณ และคุณภาพที่กำหนด',
table(['ลำดับ', 'หมวด', 'รายการ', 'รายละเอียด'], [
@@ -309,8 +322,8 @@ export function customerRequirements() {
const date = '2026-02-06';
const rows = [];
for (const [gid, gname, items] of CR_GROUPS) {
rows.push([`${gid}: ${gname}`, '', '', '']);
for (const [id, text, remark] of items) rows.push([id, text, 'A', remark]);
rows.push([`${gid}: ${gname}`, '', '']);
for (const [id, text] of items) rows.push([id, text, 'A']);
}
return {
dir: '1-PM Process (10 Work Product)/2.Project Plan/3-Customer Requirement',
@@ -336,7 +349,7 @@ export function customerRequirements() {
[6, 'ฝ่ายบัญชีและการเงิน', 'ผู้ใช้งาน', 'ให้ข้อมูลการวางบิล รับชำระ จ่ายชำระ และการบันทึกบัญชี'],
], ['c']),
'## ความต้องการเชิงหน้าที่และไม่ใช่หน้าที่ (Functional and Non-Functional Requirements)',
table(['ID', 'Topic', 'Result*', 'Remark'], rows, ['', '', 'c', '']),
table(['ID', 'Topic', 'Result*'], rows, ['', '', 'c']),
'## Remark',
bullets(['\\* Result : A = Accepted, U = Unaccepted, N/A = Not Applicable', 'เอกสารฉบับนี้รวมความต้องการที่ได้จากการทบทวนขอบเขตงานร่วมกับลูกค้า', 'การเปลี่ยนแปลงความต้องการหลังอนุมัติต้องดำเนินการผ่าน Change Report']),
].join('\n\n'),
+46 -59
View File
@@ -1,7 +1,7 @@
// PM work products 3–10: Progress Status Record, Correction Register, Acceptance Report,
// 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 { TASKS, taskById, CORRECTIONS, CHANGES, PERIODS, MEETINGS } from './history.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, CHANGE_CRITERIA, CHANGE_REVIEW, LESSONS, PERIODS, MEETINGS } from './history.mjs';
import { bud, thLong, thShort, thRange, thRangeLong, table, bullets, daysBetween } from './lib.mjs';
import { workProduct, head } from './format.mjs';
@@ -22,10 +22,8 @@ export function progressStatusRecords() {
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, 'เสร็จสิ้น'];
});
const changes = changeIds.map((cid) => {
const c = CHANGES.find((x) => x.id === cid);
return [c.id, c.titleTh, 'อนุมัติ', c.impact[0][1]];
});
if (changeIds.length) throw new Error(`Progress record ${date} lists change requests, but the project has none.`);
const changes = [];
return {
dir: dir3,
name: `${CODE} Progress Status Record ${bud(date)} V1.0 ApS`,
@@ -88,6 +86,10 @@ export function correctionRegister() {
['คงค้าง', 0],
], ['', 'r']),
`ปัญหาทุกรายการได้รับการแก้ไขและตรวจสอบผลด้วย 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']),
].join('\n\n'),
@@ -118,7 +120,7 @@ export function acceptanceReport() {
table(['ลำดับ', 'เกณฑ์การตรวจรับ', 'หลักฐานอ้างอิง', 'ผลการตรวจสอบ'], [
[1, 'ฟังก์ชันในขอบเขตทำงานตรงตาม Customer Requirements ครบทุกรายการ', 'Traceability Record, Test Report', 'ผ่าน'],
[2, 'ทดสอบระบบตาม Test Case ครบถ้วน', `Test Report 45 Test Case ผ่านทั้งหมด (${thRangeLong(TEST_FROM, TEST_TO)})`, 'ผ่าน'],
[3, 'ทดสอบการยอมรับโดยผู้ใช้ (UAT)', 'Validation Results 12 สถานการณ์ ผ่านทั้งหมด', 'ผ่าน'],
[3, 'ทดสอบการยอมรับโดยผู้ใช้ (UAT)', `Validation Results ครบ 14 หมวดความต้องการ (${CRS.length} รายการ) ผ่านทั้งหมด`, 'ผ่าน'],
[4, 'ไม่มีข้อบกพร่องระดับวิกฤตคงค้าง', `Correction Register ${CORRECTIONS.length} รายการ แก้ไขและตรวจสอบครบ`, 'ผ่าน'],
[5, 'เอกสารคู่มือผู้ใช้ ผู้ดูแลระบบ และการบำรุงรักษาครบถ้วน', 'Software User Document, Product Operation Guide, Maintenance Document', 'ผ่าน'],
[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() {
return CHANGES.map((c) => ({
const date = ACCEPT_DATE;
return [{
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({
type: 'Change Report', footer: 'CR',
rows: head('Change Report', `${bud(c.date)} V1.0 ${c.requester.id}`, [
['Change ID', c.id, 'วันที่ร้องขอ', thLong(c.date)],
['ผู้ร้องขอ (Requester)', `${c.requester.th} (${c.requester.role})`],
['หน่วยงาน/ฝ่าย', c.unit],
['ประเภทการเปลี่ยนแปลง', `☒ ${c.type} ☒ ${c.typeChecked} ☐ Schedule ☐ Cost ☐ Resource ☐ Other`],
['ความเร่งด่วน (Priority)', `☐ High ☒ ${c.priority} ☐ Low`],
rows: head('Change Report', `${bud(date)} V1.0 ApS`, [
['Title', 'ทะเบียนการควบคุมการเปลี่ยนแปลง (Change Control Register)'],
['Project Period', thRangeLong(START, END)],
['Organizer', ORGANIZER_PM],
['Recorder', RECORDER],
]),
body: [
'## ส่วนที่ 1 : ข้อมูลทั่วไป',
table(['หัวข้อ', 'รายละเอียด'], [
['ชื่อการเปลี่ยนแปลง', c.titleTh],
['โครงการ', PROJECT_TH],
['ระยะที่เกิดการเปลี่ยนแปลง', `ช่วงดำเนินโครงการ (${thLong(c.date)})`],
['เอกสารที่เกี่ยวข้อง', 'Customer Requirements, Software Project Plan, Traceability Record'],
]),
'## ส่วนที่ 2 : รายละเอียดการเปลี่ยนแปลง',
table(['ลำดับ', 'รายการ', 'รายละเอียด'], [
[1, 'คำอธิบายการเปลี่ยนแปลงที่ต้องการ (Description)', c.description],
[2, 'เหตุผลของการเปลี่ยนแปลง (Justification)', c.justification],
], ['c']),
'## ส่วนที่ 3 : การประเมินผลกระทบ (Impact Analysis)',
table(['ลำดับ', 'รายการ', 'รายละเอียด'], c.impact.map((r, i) => [i + 1, r[0], r[1]]), ['c']),
'## ส่วนที่ 4 : การพิจารณาอนุมัติ (Change Advisory Board Approval)',
table(['ลำดับ', 'ชื่อผู้พิจารณา', 'ตำแหน่ง', 'ความคิดเห็น / ลงนาม'], [
[1, P.ApS.th, 'Project Manager', 'เห็นควรอนุมัติ ผลกระทบอยู่ในวิสัยที่ควบคุมได้'],
[2, P.NoC.th, 'System Analyst', 'สอดคล้องกับความต้องการและการออกแบบ'],
[3, P.PaNg.th, 'QA / Tester', 'ต้องทดสอบซ้ำในส่วนที่ได้รับผลกระทบ'],
[4, P.SeV.th, 'Project Sponsor', 'อนุมัติ'],
], ['c']),
'## ส่วนที่ 5 : สรุปผลการพิจารณา',
table(['หัวข้อ', 'รายละเอียด'], [
['สถานะ', '☒ อนุมัติ (Approved) ☐ ไม่อนุมัติ (Rejected)'],
['การอ้างอิงการดำเนินการ', `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)"',
'หากมีข้อสงสัยในระดับผลกระทบ ให้ใช้ระดับที่สูงกว่าเพื่อความปลอดภัยของโครงการ',
'## วัตถุประสงค์ (Objective)',
'เอกสารนี้บันทึกการควบคุมการเปลี่ยนแปลงของโครงการ ตั้งแต่ตั้ง Baseline ความต้องการเมื่อ 18 กุมภาพันธ์ 2569 จนถึงการตรวจรับส่งมอบ โดยพิจารณาทุกรายการปรับปรุงที่มีการเสนอตามเกณฑ์ในส่วนที่ 1 รายการที่เข้าเกณฑ์ต้องจัดทำคำขอเปลี่ยนแปลง ประเมินผลกระทบ และขออนุมัติก่อนดำเนินการ ส่วนรายการที่ไม่เข้าเกณฑ์ดำเนินการเป็นมติที่ประชุมหรืองานในแผน',
'## ส่วนที่ 1 : เกณฑ์การพิจารณาคำขอเปลี่ยนแปลง (Change Criteria)',
'รายการใดจะถือเป็นคำขอเปลี่ยนแปลงเมื่อเข้าเกณฑ์ทั้ง 3 ข้อ หากตัดรายการนั้นออกแล้วยังส่งมอบงานได้ตามที่ตกลงไว้ จะไม่ถือเป็นคำขอเปลี่ยนแปลง',
table(['ลำดับ', 'เกณฑ์', 'คำอธิบาย'], CHANGE_CRITERIA.map(([name, detail], i) => [i + 1, name, detail]), ['c']),
'## ส่วนที่ 2 : ทะเบียนคำขอเปลี่ยนแปลง (Change Request Log)',
table(['Change ID', 'หัวข้อ', 'ผู้ร้องขอ', 'ผลกระทบ', 'สถานะ'], [['-', 'ไม่มีคำขอเปลี่ยนแปลงที่เข้าเกณฑ์ตลอดโครงการ', '-', '-', '-']], ['c', '', 'c', '', 'c']),
'## ส่วนที่ 3 : รายการปรับปรุงที่พิจารณาแล้วไม่เข้าเกณฑ์',
table(['ลำดับ', 'วันที่พิจารณา', 'รายการ', 'ผู้เสนอ', 'ผลการพิจารณา', 'การดำเนินการ', 'อ้างอิง'],
CHANGE_REVIEW.map(([when, topic, who, assessment, handled, ref], i) => [i + 1, thShort(when), topic, P[who].id, `ไม่เข้าเกณฑ์ — ${assessment}`, handled, ref]), ['c', 'c', '', 'c', '', '', '']),
'## ส่วนที่ 4 : สรุปผลการควบคุมการเปลี่ยนแปลง',
table(['รายการ', 'จำนวน'], [
['รายการปรับปรุงที่เสนอและพิจารณา', CHANGE_REVIEW.length],
['คำขอเปลี่ยนแปลงที่เข้าเกณฑ์', 0],
['คำขอเปลี่ยนแปลงที่อนุมัติ', 0],
['ผลกระทบต่อ Man-day กำหนดส่งมอบ และค่าใช้จ่าย', 'ไม่มี'],
], ['', 'r']),
'ขอบเขตงาน สิ่งส่งมอบ และ Customer Requirements ที่ตั้ง Baseline ไม่มีการเปลี่ยนแปลงตลอดโครงการ รายการปรับปรุงที่พิจารณาทั้งหมดดำเนินการภายในแผนงานเดิมโดยไม่กระทบ Man-day และกำหนดส่งมอบ',
'## ภาคผนวก A : เกณฑ์การประเมินระดับความรุนแรงของการเปลี่ยนแปลง',
'ใช้ประเมินระดับความรุนแรง (Priority Level) เมื่อมีคำขอเปลี่ยนแปลงที่เข้าเกณฑ์ในส่วนที่ 1',
table(['ระดับ (Level)', 'คำจำกัดความ (Definition)', 'ตัวอย่างสถานการณ์ (Examples)'], [
['High', 'กระทบระบบหลัก งบประมาณ หรือแผนงานอย่างมีนัยสำคัญ', 'เพิ่มโมดูลใหม่ เปลี่ยนโครงสร้างฐานข้อมูลหลัก'],
['Medium', 'กระทบฟังก์ชันย่อยหรือส่วนติดต่อผู้ใช้งานบางส่วน และกระทบ Man-day', 'รองรับ 2 ภาษา เพิ่มรายงานใหม่'],
['Low', 'กระทบเล็กน้อยต่อ Man-day ไม่เปลี่ยนการทำงานหลัก', 'เพิ่มฟิลด์ในเอกสาร'],
]),
].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. */
@@ -266,7 +253,7 @@ export function softwareConfiguration() {
['**เอกสารส่งมอบครั้งที่ 2 (พัฒนาและทดสอบ)**', '', ''],
['WP 3.0', `เอกสาร ${CODE} Software Requirements Specification`, '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 7.0', `เอกสาร ${CODE} Validation Results`, '1.0'],
['WP 8.0', `เอกสาร ${CODE} Software User Document`, '1.0'],
@@ -296,7 +283,7 @@ export function softwareConfiguration() {
['สิทธิ์การเข้าถึง', 'ยืนยันตัวตนด้วย 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'),
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 { SR_GROUPS, SRS, srTopic, UNIT_GROUPS, UNITS, unitTopic, unitModule, TEST_CASES, traceOf } from './engineering.mjs';
import { CORRECTIONS } from './history.mjs';
import { figure } from './figures.mjs';
import { bud, thLong, thShort, thRangeLong, table, bullets, numbered } from './lib.mjs';
import { workProduct, head } from './format.mjs';
@@ -72,8 +73,12 @@ export function softwareDesign() {
]),
body: [
'## HIGH LEVEL DESIGN',
'### High Level Architecture Diagram',
'ภาพรวมสถาปัตยกรรมของระบบ แบ่งเป็นผู้ใช้งาน ชั้น Client, Web, Data, Service และบริการภายนอก พร้อมช่องทางการเชื่อมต่อระหว่างกัน ทุกคำขอจากหน้าจอผ่านการตรวจสอบของ db_auth ก่อนถึงตรรกะธุรกิจ',
figure('architecture'),
'### Use Case Diagram',
'ผู้ใช้งานของระบบและกลุ่มการใช้งานหลัก',
'ผู้ใช้งานของระบบและกลุ่มการใช้งานหลัก บทบาทที่สูงกว่าได้รับสิทธิ์การใช้งานของบทบาทที่ต่ำกว่าทั้งหมด (Viewer → Staff → Admin → Owner)',
figure('useCase'),
table(['Actor', 'คำอธิบาย', 'การใช้งานหลัก'], [
['Owner', 'เจ้าของบริษัท สิทธิ์สูงสุดภายในบริษัท', 'ตั้งค่าบริษัท จัดการผู้ใช้ และใช้งานทุกโมดูล'],
['Admin', 'ผู้ดูแลระบบภายในบริษัท', 'จัดการข้อมูลหลัก ผู้ใช้ สิทธิ์ และใช้งานทุกโมดูลปฏิบัติการ'],
@@ -83,6 +88,8 @@ export function softwareDesign() {
['SMTP Service', 'ผู้กระทำภายนอก', 'ส่งอีเมล Onboarding กู้คืนรหัสผ่าน และแจ้งเตือน'],
]),
'### Component Diagram',
'โมดูลของระบบ (Software Unit กลุ่ม UN01–UN13) และความสัมพันธ์การเรียกใช้ระหว่างโมดูล',
figure('component'),
'โครงสร้างส่วนประกอบของระบบแบ่งตามชั้นการทำงาน',
table(['ชั้น (Layer)', 'ส่วนประกอบ', 'อ้างอิง SR'], [
['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'],
]),
'### Deployment Diagram',
'การติดตั้งบนสภาพแวดล้อมใช้งานจริงด้วย Docker Compose และทางเลือกการติดตั้งแบบ Manual',
figure('deployment'),
'การติดตั้งระบบแบ่งเป็น 4 ชั้นบริการ',
table(['ชั้นการติดตั้ง (Tier)', 'ส่วนประกอบ', 'อ้างอิง SR'], [
['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'],
]),
'### User Interface Design',
'ผังหน้าจอหลักของระบบแบ่งตามโมดูลการใช้งาน',
'ผังหน้าจอหลักของระบบแบ่งตามโมดูลการใช้งาน ทุกหน้าจอใช้โครงร่างเดียวกัน ได้แก่ เมนูด้านซ้ายที่แสดงตามสิทธิ์ของผู้ใช้ แถบด้านบนสำหรับเลือกบริษัทและบัญชีผู้ใช้ และพื้นที่ทำงานด้านขวา',
table(['กลุ่มหน้าจอ', 'หน้าจอ'], [
['Login & Onboarding', 'เข้าสู่ระบบ / ลงทะเบียนบริษัท / รับคำเชิญเข้าร่วมบริษัท / ลืมรหัสผ่าน / ยืนยัน OTP'],
['Dashboard', 'แดชบอร์ดคลังสินค้า / แดชบอร์ดบัญชีและการเงิน'],
@@ -113,6 +122,14 @@ export function softwareDesign() {
['Reports', 'รายงานสต๊อก / ความเคลื่อนไหว / ความจุคลัง / สินค้าใกล้หมด / สินค้าหมดอายุ / รายงานการเงิน'],
['Settings', 'ข้อมูลบริษัท / SMTP / ผู้ใช้งานและสิทธิ์ / การตั้งค่าระบบ'],
]),
'### Mockup Wireframe',
'Wireframe แสดงโครงร่างหน้าจอหลักที่ใช้เป็นแม่แบบในการพัฒนา หน้าจอเอกสารขาย จัดซื้อ และการเงินใช้รูปแบบหน้ารายการ (รูปที่ 7) และหน้าบันทึก (รูปที่ 8) เดียวกัน',
figure('wfLogin'),
figure('wfDashboard'),
figure('wfList'),
figure('wfDocument'),
figure('wfStockIn'),
figure('wfReport'),
'## Software Baseline',
'หัวข้อนี้ระบุสิ่งที่ถูกกำหนดเป็น Baseline ของซอฟต์แวร์ วันที่กำหนด และผู้อนุมัติ เพื่อให้ตรวจสอบเส้นทาง Baseline → Configuration → Change ได้ในเอกสารชุดเดียวกัน',
table(['รายการที่กำหนดเป็น Baseline', 'ค่าที่ควบคุม', 'ที่จัดเก็บ'], [
@@ -128,7 +145,7 @@ export function softwareDesign() {
['ผู้อนุมัติ', `${P.SeV.th} (Project Sponsor) พร้อมการตรวจรับเมื่อ ${thLong(ACCEPT_DATE)}`],
['เอกสารควบคุมที่เกี่ยวข้อง', 'Software Configuration (WP 8) ระบุรายการ Configuration Item และการควบคุมเวอร์ชัน'],
['การสอบกลับ', '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',
table(['ID', 'Description', 'Functional Interfaces Detail', 'References', 'Files'], unitRows),
@@ -143,12 +160,16 @@ export function traceabilityRecord() {
const date = '2026-03-06';
const rows = [];
let group = '';
for (const [id, text, remark] of CRS) {
for (const [id, text] of CRS) {
const g = id.slice(0, 4);
if (g !== group) { group = g; const name = SCOPEGROUP(g); rows.push([`${g}: ${name}`, '', '', '', '', '', '']); }
if (g !== group) { group = g; const name = SCOPEGROUP(g); rows.push([`${g}: ${name}`, '', '', '', '', '', '', '']); }
const t = traceOf(id);
rows.push([id, text, t.sr.join(', '), t.sr.map(srTopic).join(' / '), t.un.join(', '), t.un.map(unitTopic).join(' / '), t.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 {
dir: '2-SI Process (12 Work Product)/13.Traceability record',
name: `${CODE} Traceability Record ${bud(date)} V1.0 NoC`,
@@ -164,13 +185,16 @@ export function traceabilityRecord() {
'## วัตถุประสงค์ (Objective)',
'เอกสารที่เชื่อมโยงทุกขั้นตอนของโครงการ ตั้งแต่ความต้องการของลูกค้า → ความต้องการซอฟต์แวร์ → การออกแบบ → การทดสอบ เพื่อยืนยันความครบถ้วนของ Customer Requirements และลดความเสี่ยงของข้อบกพร่องและความต้องการที่ตกหล่น',
'## 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(['รายการ', 'จำนวน'], [
['ความต้องการของลูกค้าทั้งหมด (CR01–CR14)', CRS.length],
['เชื่อมโยงกับความต้องการซอฟต์แวร์ (SRS)', CRS.length],
['เชื่อมโยงกับ Software Unit', CRS.length],
['เชื่อมโยงกับ Test Case', CRS.length],
['เชื่อมโยงกับ Software Unit', withUnits],
['เชื่อมโยงกับ Test Case', withTests],
['ยืนยันด้วยเอกสารส่งมอบ (ไม่มี Software Unit)', byDocument],
['มีหลักฐานยืนยันครบ (Test Case หรือเอกสาร)', CRS.length],
['ความต้องการซอฟต์แวร์ทั้งหมด (SR01–SR09)', SRS.length],
['Software Unit ทั้งหมด', UNITS.length],
['Test Case ทั้งหมด', TEST_CASES.length],
@@ -275,9 +299,11 @@ export function testCases() {
/** 16. Test Report. */
export function testReport() {
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);
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 {
dir: '2-SI Process (12 Work Product)/16.Test Report',
@@ -312,6 +338,7 @@ export function testReport() {
], ['', 'r']),
'## Verification Items',
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',
table(['No', 'Topic', 'Detail'], [
[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 หลักและสำรอง`],
[2, 'ชุดติดตั้งระบบ', 'Docker Compose Stack และสคริปต์ `setup.php` สำหรับสร้างฐานข้อมูล'],
[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)}`],
[6, 'คู่มือประกอบการใช้งาน', 'Software User Document, Product Operation Guide, Maintenance Document'],
], ['c']),
+62 -45
View File
@@ -1,7 +1,7 @@
// 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 { 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 { 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. */
export function verificationResults() {
const 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 ROUNDS = VERIFICATION_ROUNDS;
const CHECKS = {
'WP 1.0': ['Software Project Plan', [
['รายละเอียดและความถูกต้องของ Header ของเอกสาร', 'มีรายละเอียดใน Header ครบถ้วน'],
@@ -279,8 +274,8 @@ export function verificationResults() {
], 'NoC'],
'WP 4.0': ['Software Design', [
['รายละเอียดและความถูกต้องของ Header ของเอกสาร', 'มีรายละเอียดครบถ้วนถูกต้อง'],
['ใน High Level Design มี Use Case, Component และ Deployment Diagram', 'มีครบทั้ง 3 ส่วน'],
['ใน User Interface Design มีผังหน้าจอที่จะใช้ในระบบ', 'มีผังหน้าจอครบทุกกลุ่มโมดูล'],
['ใน High Level Design มี Architecture, Use Case, Component และ Deployment Diagram', 'มีแผนภาพครบทั้ง 4 แบบ'],
['ใน User Interface Design มีผังหน้าจอที่จะใช้ในระบบ', 'มีผังหน้าจอครบทุกกลุ่มโมดูล พร้อม Mockup Wireframe ของหน้าจอหลัก 6 รูปแบบ'],
['มีหัวข้อ Software Baseline ระบุสิ่งที่ถูกกำหนดเป็น Baseline วันที่ และผู้อนุมัติ', 'มีหัวข้อ Software Baseline ครบถ้วน พร้อมอ้างอิง Software Configuration และ Traceability Record'],
['ใน Software Unit มีรหัสกำกับแต่ละหน่วย เช่น UN01.001 อย่างเป็นระเบียบ', 'มีรหัสครบ 45 หน่วย'],
['ใน Software Unit มี Description และ Functional Interfaces Detail ครบทุกรายการ', 'มีรายละเอียดครบทุกหน่วย'],
@@ -288,10 +283,10 @@ export function verificationResults() {
['ลงชื่อผู้จัดทำและผู้อนุมัติเรียบร้อย', 'มีตารางลงนามครบถ้วน'],
], 'NoC'],
'WP 5.0': ['Change Report', [
['รายละเอียดและความถูกต้องของ Header ของเอกสาร', 'มีรายละเอียดครบทั้ง 3 ฉบับ'],
['ส่วน Requesting เขียนรายละเอียดถูกต้องครบถ้วนทุกฉบับ', 'มีรายละเอียดครบทุกฉบับ'],
['ส่วน Impact Analysis ได้รับการประเมินผลกระทบครบทุกฉบับ', 'มีการประเมินผลกระทบ 5 ด้านครบทุกฉบับ'],
['มีการลงนามอนุมัติโดยคณะพิจารณา (CAB) ครบทุกฉบับ', 'มีตารางพิจารณาและลงนามครบถ้วน'],
['รายละเอียดและความถูกต้องของ Header ของเอกสาร', 'มีรายละเอียดครบถ้วนถูกต้อง'],
['ระบุเกณฑ์การพิจารณาว่ารายการใดเป็นคำขอเปลี่ยนแปลง', 'มีเกณฑ์ 3 ข้อ ครอบคลุมความจำเป็นต่อการส่งมอบ ขอบเขต และ Man-day'],
['รายการปรับปรุงที่เสนอได้รับการพิจารณาตามเกณฑ์และบันทึกผลครบทุกรายการ', 'มีผลการพิจารณาและการดำเนินการครบทุกรายการ'],
['ลงชื่อผู้จัดทำ ผู้ตรวจสอบ และผู้อนุมัติครบถ้วน', 'มีตารางลงนามครบถ้วน'],
], 'ApS'],
'WP 6.0': ['Test Case and Test Procedures', [
['รายละเอียดและความถูกต้องของ Header ของเอกสาร', 'มีรายละเอียดครบถ้วนถูกต้อง'],
@@ -305,8 +300,9 @@ export function verificationResults() {
], 'PaNg'],
'WP 7.0': ['Validation Results', [
['รายละเอียดและความถูกต้องของ Header ของเอกสาร', 'มีรายละเอียดครบถ้วนถูกต้อง'],
['ระบุสถานการณ์ทดสอบการยอมรับครบตามความต้องการของลูกค้า', 'มี 12 สถานการณ์ครอบคลุมทุกระบบงาน'],
['ระบุผลการทดสอบและผู้ทดสอบครบทุกรายการ', 'มีผลและผู้ทดสอบครบทุกสถานการณ์'],
['ใช้ Customer Requirements เป็นตัวตั้งของการทดสอบการยอมรับ', `ครอบคลุมครบ 14 หมวด ${CRS.length} รายการ`],
['ระบุวิธีการยืนยันและหลักฐานอ้างอิงของแต่ละความต้องการ', 'มีวิธีการยืนยันและหลักฐานครบทุกรายการ'],
['ระบุผลการทดสอบและผู้ทดสอบครบทุกรายการ', 'มีผลและผู้ทดสอบครบทุกรายการ'],
['ลงชื่อผู้ทดสอบฝ่ายลูกค้าเรียบร้อย', 'มีตารางลงนามครบถ้วน'],
], 'PaNg'],
'WP 8.0': ['Software User Document', [
@@ -352,21 +348,17 @@ export function verificationResults() {
rows: head('Verification Results', `${bud(r.date)} ${r.v} PaNg`, [
['Title', 'บันทึกการตรวจสอบตามข้อกำหนดของมาตรฐาน'],
['รอบตรวจสอบ', `รอบที่ ${r.no} (${thLong(r.date)})`, 'หัวข้อ', r.topic],
['เวลาตรวจสอบ', r.time, 'ระยะเวลา', `${r.hours} ชั่วโมง`],
['Organizer', ORGANIZER],
['Recorder', RECORDER],
]),
body: [
'## วัตถุประสงค์ (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',
table(['รหัส', 'สิ่งส่งมอบที่ตรวจสอบในรอบนี้'], r.wps.map((wp) => [wp, CHECKS[wp][0]]), ['c']),
'## Risk & Constraints Note',
numbered([
'เจ้าหน้าที่ควบคุมเอกสารต้องตรวจสอบแต่ละรายการอย่างละเอียด และต้องมีหลักฐานเพียงพอที่จะสรุปผลว่าผ่าน',
'ผู้ปฏิบัติงานที่เกี่ยวข้องควรร่วมรับฟังผลการตรวจสอบ เพื่อให้แก้ไขได้ทันทีเมื่อพบประเด็น',
'การตรวจสอบแต่ละครั้งไม่ควรถูกขัดจังหวะ ซึ่งจะทำให้การตรวจสอบเลื่อนออกไป',
'ห้ามให้ผู้อื่นตรวจสอบแทนผู้ที่ได้รับมอบหมาย',
]),
numbered(VERIFICATION_RULES),
'## ผลการตรวจสอบ',
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() {
const date = '2026-08-14';
const SCENARIOS = [
['การลงทะเบียนและกำหนดสิทธิ์ผู้ใช้งาน', ['CR01:001', 'CR01:002', 'CR01:003', 'CR01:004'], 'ผู้ใช้เข้าสู่บริษัทที่ถูกต้องและเห็นเฉพาะเมนูที่ได้รับสิทธิ์'],
['การตั้งค่าข้อมูลหลักคลังสินค้าและสินค้า', ['CR01:005', 'CR01:006'], 'ตั้งค่าคลังสินค้า พื้นที่ ช่องจัดเก็บ และสินค้าได้ครบตามโครงสร้างที่ใช้จริง'],
['การรับสินค้าเข้าคลัง', ['CR01:007'], 'บันทึกรับเข้าแล้วยอดคงเหลือและความเคลื่อนไหวถูกต้อง'],
['การจ่ายสินค้าออกจากคลัง', ['CR01:008'], 'จ่ายออกภายในยอดคงเหลือได้ และระบบปฏิเสธการจ่ายเกินยอด'],
['การโอนย้ายสินค้าระหว่างตำแหน่ง', ['CR01:009'], 'ยอดต้นทางและปลายทางสมดุลและสอบกลับได้เป็นรายการเดียว'],
['การติดตาม Lot, Serial Number และวันหมดอายุ', ['CR01:010', 'CR01:012'], 'ข้อมูลติดตามคงอยู่และแสดงในรายงานที่เกี่ยวข้อง พร้อมพิมพ์บาร์โค้ดได้'],
['กระบวนการขาย', ['CR01:013'], 'ใบเสนอราคา ใบสั่งขาย ใบแจ้งหนี้ และใบรับคืน ทำงานตามลำดับสถานะที่กำหนด'],
['กระบวนการจัดซื้อ', ['CR01:014'], 'ใบขอซื้อ ใบสั่งซื้อ ใบแจ้งหนี้ซื้อ และใบคืนผู้ขาย ทำงานตามลำดับสถานะที่กำหนด'],
['งานการเงินและบัญชี', ['CR01:015', 'CR01:016'], 'ใบเสร็จ ใบสำคัญจ่าย และรายการบัญชีสมดุลและตรวจสอบได้'],
['รายงานคลังสินค้าและรายงานการเงิน', ['CR01:011', 'CR01:017', 'CR01:020'], 'รายงานแสดงผลตรงตามตัวกรองและขอบเขตสิทธิ์'],
['การแจ้งเตือนและงานตามกำหนดเวลา', ['CR01:021', 'CR01:022'], 'ผู้ใช้ที่เกี่ยวข้องได้รับการแจ้งเตือนโดยไม่ซ้ำซ้อน'],
['การแยกข้อมูลระหว่างบริษัทและคลังสินค้า', ['CR01:024', 'CR08:004'], 'การเข้าถึงข้อมูลข้ามบริษัทหรือคลังที่ไม่ได้รับสิทธิ์ถูกปฏิเสธ'],
];
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]);
// How the customer confirmed each requirement group during the UAT on 10–14 Aug.
const METHOD = {
CR01: 'ทดลองใช้งานตามขั้นตอนธุรกิจจริงบนระบบ',
CR02: 'ใช้งานประจำวันบนสภาพแวดล้อมจริงและสังเกตเวลาตอบสนอง',
CR03: 'ใช้งานผ่าน Browser ที่กำหนด รับอีเมลและการแจ้งเตือนจริง',
CR04: 'ตรวจสอบสภาพแวดล้อมที่ติดตั้งและการเข้าสู่ระบบ',
CR05: 'ทดลองใช้งานบน Desktop และ Tablet/Mobile',
CR06: 'ตรวจสอบ HTTPS การจำกัดสิทธิ์ และการเข้าสู่ระบบซ้ำซ้อน',
CR07: 'ตรวจสอบสภาพแวดล้อมจริง เขตเวลา และการแยกข้อมูลสาธิต',
CR08: 'ตรวจสอบการสำรอง/กู้คืน การเฝ้าระวัง และการแยกข้อมูลหลายบริษัท',
CR09: 'ทบทวนโครงสร้างซอฟต์แวร์และเอกสารการบำรุงรักษา',
CR10: 'สาธิตการติดตั้งตามขั้นตอนในคู่มือ',
CR11: 'ทบทวนคู่มือ แผนการอบรม และช่องทางสนับสนุน',
CR12: 'ตรวจสอบเทคโนโลยี การควบคุม Source Code และเอกสารตามมาตรฐาน',
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 {
dir: '2-SI Process (12 Work Product)/22.Validation Result',
name: `${CODE} Validation Results ${bud(date)} V1.0 PaNg`,
@@ -418,25 +431,29 @@ export function validationResults() {
]),
body: [
'## วัตถุประสงค์ (Objective)',
'เพื่อยืนยันว่าระบบที่พัฒนาตรงตามความต้องการของลูกค้า (Customer Requirements) สามารถใช้งานได้จริง มีประสิทธิภาพ มีความปลอดภัย และพร้อมเปิดใช้งานจริง (Go-Live)',
'เพื่อยืนยันว่าระบบที่พัฒนาตรงตามความต้องการของลูกค้า (Customer Requirements) สามารถใช้งานได้จริง มีประสิทธิภาพ มีความปลอดภัย และพร้อมเปิดใช้งานจริง (Go-Live) โดยใช้ความต้องการของลูกค้าทุกรายการเป็นตัวตั้งของการทดสอบการยอมรับ และอ้างอิง Test Case หรือเอกสารส่งมอบเป็นหลักฐานประกอบ',
'## สภาพแวดล้อมและผู้ทดสอบ',
table(['หัวข้อ', 'รายละเอียด'], [
['สภาพแวดล้อมที่ใช้ทดสอบ', 'สภาพแวดล้อมใช้งานจริงของลูกค้า'],
['ผู้ทดสอบ', `${P.SeV.th} (Project Sponsor / ตัวแทนลูกค้า)`],
['ผู้บันทึกผล', `${P.PaNg.th} (QA/Tester)`],
['ช่วงเวลาทดสอบ', thRangeLong(TEST_FROM, TEST_TO)],
['ขอบเขตการทดสอบ', `Customer Requirements ครบ ${CR_GROUPS.length} หมวด ${CRS.length} รายการ (CR01–CR14)`],
['สถานะ Repository ที่ใกล้เคียงที่สุด', `Commit \`${TEST_STATE_COMMIT}\``],
]),
'## ตารางสรุป Validation Result',
table(['No', 'Test Case', 'CR ID', 'Customer Req. Topic', 'ผลลัพธ์ที่คาดหวัง', 'Status', 'Tester'], rows, ['c', '', '', '', '', 'c', 'c']),
'## ตารางสรุป Validation Result ตาม Customer Requirements',
table(['CR ID', 'Customer Requirement', 'วิธีการยืนยัน', 'หลักฐานอ้างอิง', 'Status', 'Tester'], rows, ['', '', '', '', 'c', 'c']),
'## สรุปผลการทดสอบการยอมรับรายหมวด',
table(['หมวดความต้องการ', 'จำนวนรายการ', 'ผ่าน (Passed)', 'ไม่ผ่าน (Failed)'], summary, ['', 'r', 'r', 'r']),
'## สรุปผลการทดสอบการยอมรับ',
table(['รายการ', 'จำนวน'], [
['สถานการณ์ทดสอบทั้งหมด', SCENARIOS.length],
['ผ่าน (Passed)', SCENARIOS.length],
['หมวดความต้องการที่ทดสอบ', CR_GROUPS.length],
['ความต้องการที่ทดสอบทั้งหมด', CRS.length],
['ผ่าน (Passed)', CRS.length],
['ไม่ผ่าน (Failed)', 0],
['ค้างการทดสอบ', 0],
], ['', 'r']),
`ตัวแทนลูกค้าได้ทดสอบการยอมรับครบทุกสถานการณ์บนสภาพแวดล้อมใช้งานจริง ผลการทดสอบผ่านทั้งหมด ไม่พบประเด็นที่ขัดขวางการเปิดใช้งาน จึงยืนยันความพร้อมของระบบสำหรับการตรวจรับส่งมอบ ซึ่งบันทึกไว้ใน Acceptance Report เมื่อ ${thLong(ACCEPT_DATE)}`,
`ตัวแทนลูกค้าได้ทดสอบการยอมรับครบทุกความต้องการใน Customer Requirements บนสภาพแวดล้อมใช้งานจริง ผลการทดสอบผ่านทั้งหมด ไม่พบประเด็นที่ขัดขวางการเปิดใช้งาน จึงยืนยันความพร้อมของระบบสำหรับการตรวจรับส่งมอบ ซึ่งบันทึกไว้ใน Acceptance Report เมื่อ ${thLong(ACCEPT_DATE)}`,
].join('\n\n'),
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] ?? '';
/** 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 = {
'CR01:001': { sr: ['SR03:001', 'SR08:001'], un: ['UN01.001'] },
'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'] },
'CR08:001': { 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: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: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'] },
'CR10:001': { sr: ['SR01:003', 'SR08:005'], un: ['UN13.001'] },
'CR10:002': { sr: ['SR02:005'], un: ['UN13.002'] },
'CR10:003': { sr: ['SR01:005'], un: ['UN13.003'] },
'CR10:004': { sr: ['SR01:001'], un: ['UN13.001'] },
'CR11:001': { sr: ['SR01:001'], un: ['UN01.002'] },
'CR11:002': { sr: ['SR01:001'], un: ['UN13.001'] },
'CR11:003': { sr: ['SR01:001'], un: ['UN01.002'] },
'CR11:004': { sr: ['SR01:001'], un: ['UN13.004'] },
'CR10:004': { sr: ['SR01:001'], un: ['UN13.001', 'UN13.002'], ev: 'Product Operation Guide (ขั้นตอนติดตั้ง)' },
'CR11:001': { sr: ['SR01:001'], un: [], ev: 'Software User Document (WP 8.0)' },
'CR11:002': { sr: ['SR01:001'], un: [], ev: 'Product Operation Guide (WP 10.0)' },
'CR11:003': { sr: ['SR01:001'], un: [], ev: 'Training Report' },
'CR11:004': { sr: ['SR01:001'], un: [], ev: 'Maintenance Document (SLA)' },
'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: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:002': { sr: ['SR09:003'], un: ['UN04.002'] },
'CR13:003': { sr: ['SR01:003'], un: ['UN13.004'] },
'CR13:004': { sr: ['SR09:002'], un: ['UN10.002'] },
'CR14:001': { sr: ['SR01:001'], un: ['UN01.002'] },
'CR14:002': { sr: ['SR01:001'], un: ['UN01.002'] },
'CR14:003': { sr: ['SR01:001'], un: ['UN01.002'] },
'CR14:004': { sr: ['SR07:002'], un: ['UN12.001'] },
'CR14:001': { sr: ['SR01:001'], un: [], ev: 'Test Report' },
'CR14:002': { sr: ['SR01:001'], un: [], ev: 'Traceability Record' },
'CR14:003': { sr: ['SR01:001'], un: [], ev: 'Validation Results, Acceptance Report' },
'CR14:004': { sr: ['SR07:002'], un: ['UN12.001', 'UN12.002'], ev: 'Test Report, Correction Register' },
};
export const traceOf = (crId) => {
const t = TRACE[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) {
const t = traceOf(id);
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}`);
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.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.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.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'],
@@ -66,56 +66,67 @@ export const CORRECTIONS = [
['ISS-028', '2026-07-28', '2026-08-03', 'สูง', 'การเข้าสู่ระบบและค่าตั้งค่าสภาพแวดล้อมผิดพลาดหลัง Baseline', 'ค่าตั้งค่าของสภาพแวดล้อมใหม่ไม่ตรงกับที่ระบบคาดหวัง', 'แก้ไขการเข้าสู่ระบบและค่าตั้งค่าสภาพแวดล้อม', 'ThS', 'b2c4374', 'TC-UN12.004'],
];
/** Change Reports: one document each. */
export const CHANGES = [
{
id: 'CH-001', date: '2026-05-21', file: 'Rack to Bin Rename',
titleTh: 'เปลี่ยนคำเรียกตำแหน่งจัดเก็บจาก Rack เป็น Bin',
requester: P.NoC, unit: 'ฝ่ายวิเคราะห์ระบบ / ตัวแทนผู้ใช้งานคลังสินค้า',
type: 'Scope', typeChecked: 'Quality', priority: 'Medium',
description: 'เปลี่ยนคำเรียกตำแหน่งจัดเก็บสินค้าจาก "Rack" เป็น "Bin" ทั้งระบบ ครอบคลุมป้ายกำกับหน้าจอ, custom.js, เครื่องมือบาร์โค้ด (BarcodeManager.php, location_barcode_label.php, retrieve_rack.php) และคลาสจัดการคลังสินค้า สต๊อก ใบสั่งขาย รายงาน ใบรับคืน ใบคืนผู้ขาย สินค้า และการตั้งค่าบริษัท',
justification: 'ให้คำที่ใช้ในระบบตรงกับคำที่พนักงานคลังสินค้าใช้จริงในการปฏิบัติงาน ลดความสับสนระหว่างการใช้งานและการอบรม',
impact: [
['ผลกระทบต่อ Scope', 'แก้ไข 14 ไฟล์ ประมาณ 1,000 บรรทัด ครอบคลุมคลาสจัดการ 8 คลาสและเครื่องมือบาร์โค้ด ไม่มีการเพิ่มฟังก์ชันใหม่'],
['ผลกระทบต่อ Schedule', 'ดำเนินการภายในช่วงพัฒนาปัจจุบัน ไม่กระทบกำหนดส่งมอบ'],
['ผลกระทบต่อ Budget/Cost', 'ไม่มีค่าใช้จ่ายเพิ่ม ใช้ทรัพยากรทีมพัฒนาเดิม'],
['ผลกระทบต่อ Resource', 'ผู้พัฒนา 1 คน'],
['ผลกระทบต่อ Quality/Deliverables', 'เป็นการเปลี่ยนคำเรียกเท่านั้น ไม่เปลี่ยนพฤติกรรมของระบบ ข้อบกพร่องที่เกิดจากการเปลี่ยนไม่ครบให้บันทึกใน Correction Register (ISS-023)'],
],
commit: '8f57ab5', result: 'ดำเนินการแล้วเสร็จ และตรวจสอบผลด้วย TC-UN03.001 ผ่านเมื่อ 10–14 ส.ค. 69',
},
{
id: 'CH-002', date: '2026-08-08', file: 'Demo Data Population',
titleTh: 'เพิ่มชุดข้อมูลสาธิตสำหรับการตรวจรับและสาธิตระบบ',
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: 'ระบบที่ติดตั้งใหม่ไม่มีประวัติรายการ ทำให้ตรวจรับรายงานและขั้นตอนการทำงานไม่ได้ จึงต้องมีข้อมูลตัวอย่างที่ใกล้เคียงการใช้งานจริงสำหรับการตรวจรับและการสาธิต',
impact: [
['ผลกระทบต่อ Scope', 'แก้ไข 9 ไฟล์ เพิ่มประมาณ 2,900 บรรทัด เป็นสคริปต์สร้างข้อมูลใหม่ ไม่แก้ตรรกะหลักของระบบ'],
['ผลกระทบต่อ Schedule', 'ดำเนินการภายในช่วงเตรียมส่งมอบตามแผน'],
['ผลกระทบต่อ Budget/Cost', 'ไม่มีค่าใช้จ่ายเพิ่ม'],
['ผลกระทบต่อ Resource', 'ผู้พัฒนา 1 คน'],
['ผลกระทบต่อ Quality/Deliverables', 'เป็นการเพิ่มข้อมูลสาธิตเท่านั้น ข้อมูลสาธิตต้องแยกจากข้อมูลใช้งานจริงตาม CR07:004'],
],
commit: 'dd48a8b', result: 'ดำเนินการแล้วเสร็จเมื่อ 14 ส.ค. 69 และใช้เป็นข้อมูลตั้งต้นของการทดสอบและ UAT',
},
{
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 ไฟล์'],
['ผลกระทบต่อ Schedule', 'ดำเนินการภายในช่วงเตรียมส่งมอบ 15–17 ส.ค. 69'],
['ผลกระทบต่อ Budget/Cost', 'ไม่มีค่าใช้จ่ายเพิ่ม'],
['ผลกระทบต่อ Resource', 'ผู้พัฒนา 1 คน'],
['ผลกระทบต่อ Quality/Deliverables', 'ภาพหัวเอกสารถูกใช้ในเอกสารส่งมอบ ชุด Docker Compose สร้างค่าความลับจาก .env ขณะเริ่ม Container ตาม CR10:003'],
],
commit: '63cea23, 136084f, 6c39700', result: 'ดำเนินการแล้วเสร็จเมื่อ 17 ส.ค. 69 และตรวจสอบผลด้วย TC-UN13.002 และ TC-UN13.003',
},
/**
* 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.
* Adjustments that fail that test are handled as decisions or tasks and listed here as reviewed.
*/
export const CHANGE_CRITERIA = [
['เป็นสิ่งจำเป็นต่อการส่งมอบ', 'หากไม่ดำเนินการ จะส่งมอบสิ่งส่งมอบ (Work Product) หรือความต้องการที่ตกลงไว้ไม่ได้'],
['เปลี่ยนแปลงขอบเขตหรือสิ่งส่งมอบ', 'เพิ่ม ลด หรือแก้ไขความต้องการ (CR) หรือสิ่งส่งมอบที่ตั้ง Baseline แล้ว เช่น เพิ่มโมดูล หรือรองรับ 2 ภาษา'],
['กระทบทรัพยากรของโครงการ', 'กระทบ Man-day กำหนดส่งมอบ หรือค่าใช้จ่าย จึงต้องประเมินผลกระทบและขออนุมัติก่อนดำเนินการ'],
];
/** Adjustments reviewed against CHANGE_CRITERIA: [date, topic, requesterId, assessment, handledAs, reference]. */
export const CHANGE_REVIEW = [
['2026-05-23', 'ปรับคำเรียกตำแหน่งจัดเก็บจาก Rack เป็น Bin ทั้งระบบ', 'NoC',
'เป็นการปรับคำเรียกบนหน้าจอให้ตรงกับการปฏิบัติงานจริง ไม่เพิ่มหรือลดฟังก์ชัน ไม่กระทบ Man-day และส่งมอบได้แม้ไม่ปรับ',
'มติที่ประชุม ดำเนินการภายใน Task 3.9', 'Meeting Record 23 พ.ค. 69, Correction Register ISS-023'],
['2026-08-03', 'เตรียมข้อมูลสาธิตสำหรับการตรวจรับและการสาธิตระบบ', 'ApS',
'เป็นข้อมูลตั้งต้นสำหรับตรวจรับ ไม่ใช่สิ่งส่งมอบ และต้องแยกจากข้อมูลใช้งานจริงตาม CR07:004',
'งานเตรียมการส่งมอบ Task 4.7', 'Meeting Record 3 ส.ค. 69, 14 ส.ค. 69'],
['2026-08-14', 'ปรับอัตลักษณ์องค์กรและจัดทำชุดติดตั้ง Docker Compose', 'SeV',
'ชุดติดตั้ง Docker Compose อยู่ในความต้องการ CR10:002 ตั้งแต่ Baseline และการปรับอัตลักษณ์องค์กรไม่เปลี่ยนแปลงฟังก์ชันหรือสิ่งส่งมอบ',
'งานเตรียมการส่งมอบ Task 4.7', 'Meeting Record 14 ส.ค. 69, 17 ส.ค. 69'],
];
/**
* Verification rounds, planned in the Software Project Plan and recorded in Verification Results.
* The 232-day project is reviewed about every two months, with a final round before delivery.
*/
export const VERIFICATION_ROUNDS = [
{ v: 'V0.1', date: '2026-03-17', no: 1, time: '09:00 – 12:00 น.', hours: 3, topic: 'ตรวจสอบเอกสารวางแผนโครงการและความต้องการ', wps: ['WP 1.0', 'WP 2.0'] },
{ v: 'V0.2', date: '2026-05-29', no: 2, time: '09:00 – 12:00 น.', hours: 3, topic: 'ตรวจสอบเอกสารความต้องการซอฟต์แวร์และการออกแบบ', wps: ['WP 3.0', 'WP 4.0', 'WP 5.0'] },
{ v: 'V0.3', date: '2026-07-31', no: 3, time: '09:00 – 12:00 น.', hours: 3, topic: 'ตรวจสอบชุดทดสอบและการสอบกลับ', wps: ['WP 6.0'] },
{ v: 'V1.0', date: '2026-08-17', no: 4, time: '09:00 – 16:00 น.', hours: 6, topic: 'User Acceptance Test (UAT) และตรวจสอบเอกสารส่งมอบทั้งหมด', wps: ['WP 1.0', 'WP 2.0', 'WP 3.0', 'WP 4.0', 'WP 5.0', 'WP 6.0', 'WP 7.0', 'WP 8.0', 'WP 9.0', 'WP 10.0', 'WP 11.0'] },
];
/** Rules every verification round follows (plan section and each Verification Results record). */
export const VERIFICATION_RULES = [
'เจ้าหน้าที่ควบคุมเอกสารต้องตรวจสอบแต่ละรายการอย่างละเอียด และต้องมีหลักฐานเพียงพอที่จะสรุปผลว่าผ่าน',
'ผู้ปฏิบัติงานที่เกี่ยวข้องควรร่วมรับฟังผลการตรวจสอบ เพื่อให้แก้ไขได้ทันทีเมื่อพบประเด็น',
'การตรวจสอบแต่ละครั้งไม่ควรถูกขัดจังหวะ ซึ่งจะทำให้การตรวจสอบเลื่อนออกไป',
'ห้ามให้ผู้อื่นตรวจสอบแทนผู้ที่ได้รับมอบหมาย',
];
/**
* Lessons drawn from the Correction Register at project close: what the team changes next time.
* [theme, issue ids, what happened, preventive action for the next project]
*/
export const LESSONS = [
['ความปลอดภัยและการควบคุมสิทธิ์', ['ISS-001', 'ISS-003', 'ISS-004', 'ISS-006', 'ISS-009', 'ISS-011', 'ISS-015', 'ISS-016', 'ISS-017', 'ISS-018', 'ISS-024', 'ISS-026'],
'ข้อบกพร่องกลุ่มใหญ่ที่สุดเกิดจากการเพิ่ม Role Guard การตรวจสิทธิ์ และการจำกัด Session ทีละส่วนระหว่างพัฒนา',
'กำหนด 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-13', '2026-05-11', '2026-05-13', 9, [['3.4', 'Completed', 100, 'พัฒนาการเข้าสู่ระบบ Onboarding และกู้คืนรหัสผ่านครบถ้วน'], ['3.6', 'Completed', 100, 'จัดทำ Base URL อัตโนมัติ สคริปต์ติดตั้งฐานข้อมูล และข้อมูลทดสอบ'], ['3.7', 'In Progress', 20, 'เริ่มพัฒนาโมดูลบัญชีและผังบัญชี']], ['ISS-006', 'ISS-007', 'ISS-008', 'ISS-009'], [], { date: '2026-05-23', topic: 'ติดตามการพัฒนาบัญชี การเงิน และการควบคุมสิทธิ์ผู้ใช้' }],
['2026-05-23', '2026-05-20', '2026-05-23', 10, [['3.7', 'Completed', 100, 'พัฒนาผังบัญชี GL สมุดรายวัน รายงานบัญชี ใบวางบิลและใบเสร็จครบตามแผน'], ['3.8', 'In Progress', 45, 'อยู่ระหว่างพัฒนา Socket.IO และตารางสรุปยอด'], ['3.9', 'In Progress', 60, 'เพิ่มการเชิญผู้ใช้ การควบคุมสิทธิ์แอปพลิเคชัน และโควตารายการ']], ['ISS-010', 'ISS-011', 'ISS-012', 'ISS-013', 'ISS-014', 'ISS-015'], [], { date: '2026-05-29', topic: 'สรุปผลการพัฒนาและตั้ง Baseline การพัฒนา' }],
['2026-05-29', '2026-05-24', '2026-05-29', 11, [['3.8', 'Completed', 100, 'พัฒนาบริการแจ้งเตือน Real-time ตารางสรุปยอด และงานตามกำหนดเวลาครบถ้วน'], ['3.9', 'Completed', 100, 'ทบทวนความปลอดภัย ขอบเขตบริษัท วงจรเอกสาร และปิดข้อบกพร่องที่พบ'], ['3.10', 'Completed', 100, 'ลดความซ้ำซ้อนของคลาสและตั้ง Baseline การพัฒนาเมื่อ 29 พ.ค. 69']], ['ISS-016', 'ISS-017', 'ISS-018', 'ISS-019', 'ISS-020', 'ISS-021', 'ISS-022', 'ISS-023', 'ISS-024', 'ISS-025', 'ISS-026', 'ISS-027'], ['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-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-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-14', '2026-08-04', '2026-08-14', 13, [['4.4', 'Completed', 100, `ทดสอบระบบ 45 Test Case และ UAT ครบ 14 หมวดความต้องการ (${CRS.length} รายการ) ผ่านทั้งหมดระหว่าง 10–14 ส.ค. 69`], ['4.7', 'In Progress', 60, 'จัดทำข้อมูลสาธิตเสร็จ อยู่ระหว่างปรับแบรนด์และชุดติดตั้ง Docker Compose'], ['5.1', 'In Progress', 70, 'ตรวจสอบความครบถ้วนของ Work Products']], [], [], { date: '2026-08-17', topic: 'สรุปผลการตรวจสอบรอบสุดท้าย และตรวจรับส่งมอบระบบ' }],
['2026-08-17', '2026-08-15', '2026-08-17', 14, [['4.3', 'Completed', 100, 'ตรวจสอบ Work Products รอบที่ 4 ครบทุกรายการ ผลผ่านทั้งหมด'], ['4.5', 'Completed', 100, 'คู่มือผู้ใช้ คู่มือผู้ดูแลระบบ และคู่มือบำรุงรักษาเสร็จสมบูรณ์'], ['4.7', 'Completed', 100, 'ปรับแบรนด์และชุดติดตั้ง Docker Compose เสร็จ Baseline 6c39700'], ['5.1', 'Completed', 100, 'จัดทำ List of Evidence ครบทุก Work Product'], ['5.3', 'In Progress', 80, 'ลูกค้าตรวจรับระบบเมื่อ 17 ส.ค. 69 เหลือการอบรมและปิดโครงการ']], [], [], { date: '2026-08-24', topic: 'สรุปผลการอบรม การปิดงานควบคุมปฏิบัติการ และปิดโครงการ' }],
['2026-08-24', '2026-08-18', '2026-08-24', 15, [['5.2', 'Completed', 100, 'อบรมผู้ใช้งาน 6 คน เมื่อ 22 ส.ค. 69 ผ่านการทดลองใช้งานครบทุกขั้นตอน'], ['5.3', 'Completed', 100, 'ปิดงานควบคุมปฏิบัติการ 23 ส.ค. 69 และปิดโครงการอย่างเป็นทางการ 24 ส.ค. 69'], ['4.1', 'Completed', 100, 'บันทึกความก้าวหน้าและรายงานการประชุมครบตลอดโครงการ'], ['4.2', 'Completed', 100, 'ควบคุมเวอร์ชันและสำรองข้อมูลครบ พร้อม 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 น.',
attachment: 'Progress Status Record 23 พ.ค. 69, Change Report CH-001',
attachment: 'Progress Status Record 23 พ.ค. 69',
progress: [['Phase 3: Project Execution', 'กำลังดำเนินการ', 85, 'บัญชีและการเงินเสร็จ เหลือบริการแจ้งเตือนและการทบทวนความปลอดภัย'], ['รวมความคืบหน้าทั้งโครงการ', 'ตามแผน', 72, 'เตรียมเข้าสู่ Baseline การพัฒนา']],
agenda: [['ผลการพัฒนาบัญชีและการเงิน', 'ตรวจสอบความครบถ้วนตาม CR01:015–CR01:017'], ['พิจารณาคำขอเปลี่ยนแปลง CH-001 (Rack เป็น Bin)', 'ประเมินผลกระทบและอนุมัติ'], ['แผนงานก่อนตั้ง Baseline การพัฒนา', 'ยืนยันงานที่ต้องเสร็จภายใน 29 พ.ค. 69']],
discussion: [['บัญชีและการเงิน', 'พัฒนาผังบัญชี GL สมุดรายวัน รายงานบัญชี ใบวางบิลและใบเสร็จครบตามความต้องการ', ['ThS', 'NoC'], 'เสร็จสิ้น'], ['คำขอเปลี่ยนแปลง CH-001', 'เปลี่ยนคำเรียกตำแหน่งจัดเก็บจาก Rack เป็น Bin ให้ตรงกับคำที่พนักงานคลังใช้จริง ประเมินผลกระทบระดับ Medium ไม่กระทบกำหนดส่งมอบ', ['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']],
agenda: [['ผลการพัฒนาบัญชีและการเงิน', 'ตรวจสอบความครบถ้วนตาม CR01:015–CR01:017'], ['คำเรียกตำแหน่งจัดเก็บ (Rack / Bin)', 'กำหนดคำเรียกที่ใช้ในหน้าจอให้ตรงกับการปฏิบัติงานจริง'], ['แผนงานก่อนตั้ง Baseline การพัฒนา', 'ยืนยันงานที่ต้องเสร็จภายใน 29 พ.ค. 69']],
discussion: [['บัญชีและการเงิน', 'พัฒนาผังบัญชี GL สมุดรายวัน รายงานบัญชี ใบวางบิลและใบเสร็จครบตามความต้องการ', ['ThS', 'NoC'], 'เสร็จสิ้น'], ['คำเรียกตำแหน่งจัดเก็บ', 'ที่ประชุมมีมติให้ใช้คำว่า Bin แทน Rack ทั้งระบบให้ตรงกับคำที่พนักงานคลังใช้จริง เป็นการปรับคำเรียกบนหน้าจอ ไม่เพิ่มหรือลดฟังก์ชันและไม่กระทบ Man-day จึงไม่ถือเป็นคำขอเปลี่ยนแปลง', ['SeV', 'ApS', 'NoC'], 'มติที่ประชุม'], ['ความปลอดภัยและโควตารายการ', 'เพิ่มการควบคุมสิทธิ์แอปพลิเคชันและโควตารายการต่อบริษัท', ['ThS'], 'ดำเนินการ']],
actions: [['ปรับคำเรียก Rack เป็น Bin ตามมติที่ประชุม', ['ThS'], '2026-05-27', 'Scheduled'], ['ทบทวนความปลอดภัยและวงจรเอกสารก่อนตั้ง Baseline (Task 3.9)', ['ThS', 'PaNg'], '2026-05-28', 'Scheduled'], ['บันทึกการแก้ไข ISS-010 ถึง ISS-015 ใน Correction Register', ['YaB'], '2026-05-25', 'Scheduled']],
next: { date: '2026-05-29', topic: 'สรุปผลการพัฒนาและตั้ง Baseline การพัฒนา' },
},
{
@@ -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 น.',
attachment: 'Test Report, Validation Results, Change Report CH-002',
progress: [['Phase 4: Verification & Validation', 'เสร็จสมบูรณ์', 100, 'ทดสอบ 45 Test Case และ UAT 12 สถานการณ์ ผ่านทั้งหมด'], ['Phase 5: Project Close', 'กำลังดำเนินการ', 60, 'อยู่ระหว่างตรวจสอบ Work Products และเตรียมชุดส่งมอบ'], ['รวมความคืบหน้าทั้งโครงการ', 'ตามแผน', 92, 'เตรียมตรวจรับและปิดโครงการ']],
agenda: [['สรุปผลการทดสอบระบบ 45 Test Case', 'ยืนยันผลการทดสอบและข้อบกพร่องคงค้าง'], ['สรุปผลการทดสอบการยอมรับ (UAT) 12 สถานการณ์', 'ยืนยันความพร้อมใช้งานจริง'], ['พิจารณาคำขอเปลี่ยนแปลง CH-002 (ข้อมูลสาธิต)', 'ประเมินผลกระทบและอนุมัติ'], ['แผนงานปิดโครงการ', 'ยืนยันงานที่เหลือถึง 24 ส.ค. 69']],
discussion: [['ผลการทดสอบระบบ', 'ทดสอบ 45 Test Case บน Internal Testing Server ผ่านทั้งหมด ไม่พบข้อบกพร่องคงค้าง', ['PaNg'], 'ผ่าน'], ['ผลการทดสอบการยอมรับ', 'ตัวแทนลูกค้าทดสอบ 12 สถานการณ์บนสภาพแวดล้อมใช้งานจริง ผ่านทั้งหมด', ['SeV'], 'ผ่าน'], ['คำขอเปลี่ยนแปลง CH-002', 'เพิ่มชุดข้อมูลสาธิตเพื่อใช้ตรวจรับรายงานและขั้นตอนการทำงาน ประเมินผลกระทบระดับ Medium', ['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']],
attachment: 'Test Report, Validation Results',
progress: [['Phase 4: Verification & Validation', 'เสร็จสมบูรณ์', 100, `ทดสอบ 45 Test Case และ UAT ครบ 14 หมวดความต้องการ (${CRS.length} รายการ) ผ่านทั้งหมด`], ['Phase 5: Project Close', 'กำลังดำเนินการ', 60, 'อยู่ระหว่างตรวจสอบ Work Products และเตรียมชุดส่งมอบ'], ['รวมความคืบหน้าทั้งโครงการ', 'ตามแผน', 92, 'เตรียมตรวจรับและปิดโครงการ']],
agenda: [['สรุปผลการทดสอบระบบ 45 Test Case', 'ยืนยันผลการทดสอบและข้อบกพร่องคงค้าง'], ['สรุปผลการทดสอบการยอมรับ (UAT) ตาม Customer Requirements CR01–CR14', 'ยืนยันความพร้อมใช้งานจริง'], ['ข้อมูลสาธิตสำหรับการตรวจรับ (Task 4.7)', 'ยืนยันความพร้อมของข้อมูลตั้งต้น'], ['แผนงานปิดโครงการ', 'ยืนยันงานที่เหลือถึง 24 ส.ค. 69']],
discussion: [['ผลการทดสอบระบบ', 'ทดสอบ 45 Test Case บน Internal Testing Server ผ่านทั้งหมด ไม่พบข้อบกพร่องคงค้าง', ['PaNg'], 'ผ่าน'], ['ผลการทดสอบการยอมรับ', `ตัวแทนลูกค้าทดสอบการยอมรับครบ 14 หมวดความต้องการ (${CRS.length} รายการ) บนสภาพแวดล้อมใช้งานจริง ผ่านทั้งหมด`, ['SeV'], 'ผ่าน'], ['ข้อมูลสาธิต', 'จัดเตรียมข้อมูลสาธิตสำหรับการตรวจรับรายงานและขั้นตอนการทำงานเสร็จแล้ว เป็นงานเตรียมการส่งมอบตาม Task 4.7 ไม่เปลี่ยนแปลงขอบเขตหรือสิ่งส่งมอบ', ['ApS', 'SeV'], 'เสร็จสิ้น'], ['งานที่เหลือ', 'ตรวจสอบ Work Products รอบสุดท้าย เตรียมชุดติดตั้ง ตรวจรับ อบรม และปิดโครงการ', ['ApS', 'YaB'], 'ดำเนินการ']],
actions: [['ตรวจสอบ Work Products รอบที่ 4 และจัดทำ List of Evidence', ['YaB', 'ApS'], '2026-08-17', 'Scheduled'], ['จัดทำชุดติดตั้ง Docker Compose (CR10:002) และปรับแบรนด์ (Task 4.7)', ['ThS'], '2026-08-17', 'Scheduled'], ['นัดตรวจรับส่งมอบระบบกับ Project Sponsor', ['ApS'], '2026-08-17', 'Scheduled']],
next: { date: '2026-08-17', topic: 'สรุปผลการตรวจสอบรอบสุดท้ายและตรวจรับส่งมอบระบบ' },
},
{
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, 'ตรวจรับระบบเรียบร้อย']],
agenda: [['นำเสนอผลการดำเนินโครงการและสิ่งส่งมอบ', 'สรุปผลงานทั้งหมดเทียบกับแผนและผลลัพธ์จริง'], ['ตรวจสอบความครบถ้วนของเอกสารและหลักฐาน', 'ยืนยันเอกสารครบตามมาตรฐาน ISO/IEC 29110'], ['พิจารณาคำขอเปลี่ยนแปลง CH-003', 'ประเมินผลกระทบและอนุมัติ'], ['พิจารณาตรวจรับส่งมอบระบบ', 'ลงมติผลการตรวจรับ']],
discussion: [['ผลการดำเนินโครงการ', 'พัฒนาระบบครบตามขอบเขต 11 ระบบงาน ทดสอบและ UAT ผ่านทั้งหมด ไม่มีข้อบกพร่องระดับวิกฤตคงค้าง', ['ApS', 'SeV'], 'ผ่านการรับรอง'], ['ความครบถ้วนของเอกสาร', 'ตรวจสอบ Work Products รอบที่ 4 ครบทุกรายการ ผลผ่านทั้งหมด และจัดทำ List of Evidence เรียบร้อย', ['YaB', 'PaNg'], 'สมบูรณ์'], ['คำขอเปลี่ยนแปลง CH-003', 'ปรับอัตลักษณ์องค์กรและเพิ่มชุดติดตั้ง Docker Compose ประเมินผลกระทบระดับ Medium', ['SeV', 'ApS'], 'อนุมัติ'], ['ผลการตรวจรับ', 'Project Sponsor ตรวจรับสิ่งส่งมอบทั้งหมด ผลการตรวจรับ Accepted', ['SeV'], 'ตรวจรับแล้ว']],
agenda: [['นำเสนอผลการดำเนินโครงการและสิ่งส่งมอบ', 'สรุปผลงานทั้งหมดเทียบกับแผนและผลลัพธ์จริง'], ['ตรวจสอบความครบถ้วนของเอกสารและหลักฐาน', 'ยืนยันเอกสารครบตามมาตรฐาน ISO/IEC 29110'], ['ชุดติดตั้งและอัตลักษณ์องค์กร (Task 4.7)', 'ยืนยันความพร้อมของชุดส่งมอบ'], ['พิจารณาตรวจรับส่งมอบระบบ', 'ลงมติผลการตรวจรับ']],
discussion: [['ผลการดำเนินโครงการ', 'พัฒนาระบบครบตามขอบเขต 11 ระบบงาน ทดสอบและ UAT ผ่านทั้งหมด ไม่มีข้อบกพร่องระดับวิกฤตคงค้าง', ['ApS', 'SeV'], 'ผ่านการรับรอง'], ['ความครบถ้วนของเอกสาร', 'ตรวจสอบ Work Products รอบที่ 4 ครบทุกรายการ ผลผ่านทั้งหมด และจัดทำ List of Evidence เรียบร้อย', ['YaB', 'PaNg'], 'สมบูรณ์'], ['ชุดติดตั้งและอัตลักษณ์องค์กร', 'จัดทำชุดติดตั้ง Docker Compose ตามความต้องการ CR10:002 และปรับอัตลักษณ์องค์กรในหน้าจอและเอกสารเสร็จแล้ว เป็นงานเตรียมการส่งมอบตาม Task 4.7 ไม่เปลี่ยนแปลงขอบเขตหรือสิ่งส่งมอบ', ['SeV', 'ApS'], 'เสร็จสิ้น'], ['ผลการตรวจรับ', 'Project Sponsor ตรวจรับสิ่งส่งมอบทั้งหมด ผลการตรวจรับ Accepted', ['SeV'], 'ตรวจรับแล้ว']],
actions: [['จัดอบรมผู้ใช้งานก่อนเปิดใช้งานจริง (Task 5.2)', ['ThS', 'PaNg'], '2026-08-22', 'Scheduled'], ['ปิดงานควบคุมการสำรองข้อมูลและการเฝ้าระวังระบบ', ['ThS'], '2026-08-23', 'Scheduled'], ['ปิดโครงการและส่งมอบเอกสารเข้าระบบควบคุมเอกสาร', ['ApS', 'YaB'], '2026-08-24', 'Scheduled']],
next: { date: '2026-08-24', topic: 'สรุปผลการอบรม การปิดงานควบคุมปฏิบัติการ และปิดโครงการอย่างเป็นทางการ' },
},
@@ -254,3 +265,10 @@ export const MEETINGS = [
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 3.0', `เอกสาร ${CODE} Software Requirements Specification`, 'ส่งเอกสารจำนวน 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 7.0', `เอกสาร ${CODE} Validation Results`, 'ส่งเอกสารจำนวน 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']],
];
/**
* 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.
*/
/** Customer Requirements (CR01–CR14). Each: [id, text]. */
export const CR_GROUPS = [
['CR01', 'Feature & Functional Characteristics', [
['CR01:001', 'ระบบต้องรองรับการลงทะเบียนเจ้าของบริษัทและการเชิญผู้ใช้งานเข้าร่วมบริษัท (Onboarding)', 'FR-001'],
['CR01:002', 'ระบบต้องยืนยันตัวตนผู้ใช้และบังคับสิทธิ์ตามบทบาท Owner, Admin, Staff และ Viewer', 'FR-002'],
['CR01:003', 'ระบบต้องรองรับการกู้คืนรหัสผ่าน การควบคุม Session และการยืนยัน OTP ตามที่กำหนด', 'FR-003'],
['CR01:004', 'ระบบต้องให้ผู้ดูแลจัดการข้อมูลบริษัท, SMTP, การตั้งค่าระบบ, ผู้ใช้งาน และสิทธิ์การเข้าถึงแอปพลิเคชัน', 'FR-004'],
['CR01:005', 'ระบบต้องจัดการข้อมูลคลังสินค้า, พื้นที่/ช่องจัดเก็บ, หมวดสินค้า, สินค้า, ประเภทผู้ติดต่อ และผู้ติดต่อ', 'FR-005'],
['CR01:006', 'ระบบต้องรองรับโครงสร้างตำแหน่งจัดเก็บทั้งแบบคลังเดียวและแบบหลายชั้น (คลัง/พื้นที่/ช่อง)', 'FR-006'],
['CR01:007', 'ระบบต้องบันทึกการรับสินค้าเข้า (Stock-in) ระบุสินค้า จำนวน ตำแหน่ง เอกสารอ้างอิง และข้อมูลติดตาม', 'FR-007'],
['CR01:008', 'ระบบต้องบันทึกการจ่ายสินค้าออก (Stock-out) โดยตรวจสอบสิทธิ์และยอดคงเหลือก่อนจ่าย', 'FR-008'],
['CR01:009', 'ระบบต้องโอนย้ายสินค้าระหว่างตำแหน่งจัดเก็บที่ได้รับอนุญาตโดยยอดต้นทาง/ปลายทางสมดุลกัน', 'FR-009'],
['CR01:010', 'ระบบต้องติดตาม Lot, Serial Number และวันหมดอายุของสินค้าที่เกี่ยวข้อง', 'FR-010'],
['CR01:011', 'ระบบต้องแสดงภาพรวมสต๊อก, ประวัติความเคลื่อนไหว, ความจุ/การใช้พื้นที่, สินค้าใกล้หมด, สินค้าหมดอายุ และข้อมูล Lot', 'FR-011'],
['CR01:012', 'ระบบต้องพิมพ์บาร์โค้ดสินค้า (SKU) และตำแหน่งจัดเก็บ และรองรับการสแกนในหน้าจอที่กำหนด', 'FR-012'],
['CR01:013', 'ระบบต้องสร้างและจัดการใบเสนอราคา, ใบสั่งขาย, ใบแจ้งหนี้, ใบรับคืน และใบลดหนี้', 'FR-013'],
['CR01:014', 'ระบบต้องสร้างและจัดการใบขอซื้อ, ใบสั่งซื้อ, ใบแจ้งหนี้ซื้อ และใบคืนสินค้าผู้ขาย', 'FR-014'],
['CR01:015', 'ระบบต้องสร้างและจัดการใบวางบิลรับ, ใบเสร็จรับเงิน, ใบวางบิลจ่าย และใบสำคัญจ่าย', 'FR-015'],
['CR01:016', 'ระบบต้องจัดการผังบัญชี, แผนก, สูตรบัญชี, สมุดรายวัน และบัญชีแยกประเภท', 'FR-016'],
['CR01:017', 'ระบบต้องจัดทำรายงานงบทดลอง, งบกำไรขาดทุน, งบดุล, ภาษีมูลค่าเพิ่ม, สมุดรายวัน และความเคลื่อนไหว GL', 'FR-017'],
['CR01:018', 'ระบบต้องออกเลขที่เอกสารอัตโนมัติและควบคุมสถานะ/วงจรชีวิตของเอกสาร', 'FR-018'],
['CR01:019', 'ระบบต้องรองรับการแนบไฟล์ที่อนุญาตกับรายการที่กำหนด', 'FR-019'],
['CR01:020', 'ระบบต้องให้ผู้ใช้กรอง ดู พิมพ์ และส่งออกรายงานปฏิบัติการและรายงานผู้บริหาร', 'FR-020'],
['CR01:021', 'ระบบต้องแจ้งเตือนผู้ใช้ที่เกี่ยวข้องเมื่อสถานะเอกสารเปลี่ยนหรือมีเหตุการณ์ปฏิบัติการ', 'FR-021'],
['CR01:022', 'ระบบต้องสรุปยอดสต๊อก/GL และแจ้งเตือนสินค้าใกล้หมดและใบแจ้งหนี้ค้างชำระตามกำหนดเวลา', 'FR-022'],
['CR01:023', 'ระบบต้องเก็บผู้สร้าง ผู้แก้ไข สถานะ และประวัติรายการเพื่อการตรวจสอบ', 'FR-023'],
['CR01:024', 'ระบบต้องจำกัดข้อมูลบริษัทและคลังสินค้าให้เฉพาะผู้ใช้ที่ได้รับอนุญาตในบริบทปัจจุบัน', 'FR-024'],
['CR01:001', 'ระบบต้องรองรับการลงทะเบียนเจ้าของบริษัทและการเชิญผู้ใช้งานเข้าร่วมบริษัท (Onboarding)'],
['CR01:002', 'ระบบต้องยืนยันตัวตนผู้ใช้และบังคับสิทธิ์ตามบทบาท Owner, Admin, Staff และ Viewer'],
['CR01:003', 'ระบบต้องรองรับการกู้คืนรหัสผ่าน การควบคุม Session และการยืนยัน OTP ตามที่กำหนด'],
['CR01:004', 'ระบบต้องให้ผู้ดูแลจัดการข้อมูลบริษัท, SMTP, การตั้งค่าระบบ, ผู้ใช้งาน และสิทธิ์การเข้าถึงแอปพลิเคชัน'],
['CR01:005', 'ระบบต้องจัดการข้อมูลคลังสินค้า, พื้นที่/ช่องจัดเก็บ, หมวดสินค้า, สินค้า, ประเภทผู้ติดต่อ และผู้ติดต่อ'],
['CR01:006', 'ระบบต้องรองรับโครงสร้างตำแหน่งจัดเก็บทั้งแบบคลังเดียวและแบบหลายชั้น (คลัง/พื้นที่/ช่อง)'],
['CR01:007', 'ระบบต้องบันทึกการรับสินค้าเข้า (Stock-in) ระบุสินค้า จำนวน ตำแหน่ง เอกสารอ้างอิง และข้อมูลติดตาม'],
['CR01:008', 'ระบบต้องบันทึกการจ่ายสินค้าออก (Stock-out) โดยตรวจสอบสิทธิ์และยอดคงเหลือก่อนจ่าย'],
['CR01:009', 'ระบบต้องโอนย้ายสินค้าระหว่างตำแหน่งจัดเก็บที่ได้รับอนุญาตโดยยอดต้นทาง/ปลายทางสมดุลกัน'],
['CR01:010', 'ระบบต้องติดตาม Lot, Serial Number และวันหมดอายุของสินค้าที่เกี่ยวข้อง'],
['CR01:011', 'ระบบต้องแสดงภาพรวมสต๊อก, ประวัติความเคลื่อนไหว, ความจุ/การใช้พื้นที่, สินค้าใกล้หมด, สินค้าหมดอายุ และข้อมูล Lot'],
['CR01:012', 'ระบบต้องพิมพ์บาร์โค้ดสินค้า (SKU) และตำแหน่งจัดเก็บ และรองรับการสแกนในหน้าจอที่กำหนด'],
['CR01:013', 'ระบบต้องสร้างและจัดการใบเสนอราคา, ใบสั่งขาย, ใบแจ้งหนี้, ใบรับคืน และใบลดหนี้'],
['CR01:014', 'ระบบต้องสร้างและจัดการใบขอซื้อ, ใบสั่งซื้อ, ใบแจ้งหนี้ซื้อ และใบคืนสินค้าผู้ขาย'],
['CR01:015', 'ระบบต้องสร้างและจัดการใบวางบิลรับ, ใบเสร็จรับเงิน, ใบวางบิลจ่าย และใบสำคัญจ่าย'],
['CR01:016', 'ระบบต้องจัดการผังบัญชี, แผนก, สูตรบัญชี, สมุดรายวัน และบัญชีแยกประเภท'],
['CR01:017', 'ระบบต้องจัดทำรายงานงบทดลอง, งบกำไรขาดทุน, งบดุล, ภาษีมูลค่าเพิ่ม, สมุดรายวัน และความเคลื่อนไหว GL'],
['CR01:018', 'ระบบต้องออกเลขที่เอกสารอัตโนมัติและควบคุมสถานะ/วงจรชีวิตของเอกสาร'],
['CR01:019', 'ระบบต้องรองรับการแนบไฟล์ที่อนุญาตกับรายการที่กำหนด'],
['CR01:020', 'ระบบต้องให้ผู้ใช้กรอง ดู พิมพ์ และส่งออกรายงานปฏิบัติการและรายงานผู้บริหาร'],
['CR01:021', 'ระบบต้องแจ้งเตือนผู้ใช้ที่เกี่ยวข้องเมื่อสถานะเอกสารเปลี่ยนหรือมีเหตุการณ์ปฏิบัติการ'],
['CR01:022', 'ระบบต้องสรุปยอดสต๊อก/GL และแจ้งเตือนสินค้าใกล้หมดและใบแจ้งหนี้ค้างชำระตามกำหนดเวลา'],
['CR01:023', 'ระบบต้องเก็บผู้สร้าง ผู้แก้ไข สถานะ และประวัติรายการเพื่อการตรวจสอบ'],
['CR01:024', 'ระบบต้องจำกัดข้อมูลบริษัทและคลังสินค้าให้เฉพาะผู้ใช้ที่ได้รับอนุญาตในบริบทปัจจุบัน'],
]],
['CR02', 'Performance Considerations', [
['CR02:001', 'ระบบต้องตอบสนองงานประจำวัน (เปิดหน้าจอ, ค้นหา, สร้างเอกสาร) ภายในเวลาที่ใช้งานได้จริงบนสภาพแวดล้อมที่ตกลง', 'NFR-006'],
['CR02:002', 'ระบบต้องมีตารางสรุปยอด (Aggregate) เพื่อให้แดชบอร์ดและรายงานแสดงผลได้โดยไม่ต้องคำนวณใหม่ทุกครั้ง', 'NFR-006'],
['CR02:003', 'ระบบต้องรองรับปริมาณข้อมูลและผู้ใช้พร้อมกันในระดับที่ตกลงสำหรับสภาพแวดล้อมใช้งานจริง', 'NFR-006'],
['CR02:001', 'ระบบต้องตอบสนองงานประจำวัน (เปิดหน้าจอ, ค้นหา, สร้างเอกสาร) ภายในเวลาที่ใช้งานได้จริงบนสภาพแวดล้อมที่ตกลง'],
['CR02:002', 'ระบบต้องมีตารางสรุปยอด (Aggregate) เพื่อให้แดชบอร์ดและรายงานแสดงผลได้โดยไม่ต้องคำนวณใหม่ทุกครั้ง'],
['CR02:003', 'ระบบต้องรองรับปริมาณข้อมูลและผู้ใช้พร้อมกันในระดับที่ตกลงสำหรับสภาพแวดล้อมใช้งานจริง'],
]],
['CR03', 'Interface Considerations', [
['CR03:001', 'ระบบต้องเชื่อมต่อฐานข้อมูล MySQL/MariaDB 2 ฐาน (wms สำหรับผู้ใช้/บริษัท และ wms2 สำหรับคลัง/บัญชี)', 'Operational context'],
['CR03:002', 'ระบบต้องใช้งานผ่าน Web Browser มาตรฐาน (Chrome, Edge, Firefox) ได้', 'NFR-008'],
['CR03:003', 'ระบบต้องส่งเหตุการณ์ไปยังบริการ Node.js ผ่าน Endpoint ภายในที่ป้องกันด้วย Secret', 'NFR-002'],
['CR03:004', 'Browser ต้องเชื่อมต่อ Socket.IO Endpoint สาธารณะเพื่อรับการแจ้งเตือนแบบ Real-time', 'FR-021'],
['CR03:005', 'ระบบต้องส่งอีเมล Onboarding, กู้คืนรหัสผ่าน และแจ้งเตือนผ่าน SMTP ที่ตั้งค่าต่อบริษัท', 'FR-004'],
['CR03:001', 'ระบบต้องเชื่อมต่อฐานข้อมูล MySQL/MariaDB 2 ฐาน (wms สำหรับผู้ใช้/บริษัท และ wms2 สำหรับคลัง/บัญชี)'],
['CR03:002', 'ระบบต้องใช้งานผ่าน Web Browser มาตรฐาน (Chrome, Edge, Firefox) ได้'],
['CR03:003', 'ระบบต้องส่งเหตุการณ์ไปยังบริการ Node.js ผ่าน Endpoint ภายในที่ป้องกันด้วย Secret'],
['CR03:004', 'Browser ต้องเชื่อมต่อ Socket.IO Endpoint สาธารณะเพื่อรับการแจ้งเตือนแบบ Real-time'],
['CR03:005', 'ระบบต้องส่งอีเมล Onboarding, กู้คืนรหัสผ่าน และแจ้งเตือนผ่าน SMTP ที่ตั้งค่าต่อบริษัท'],
]],
['CR04', 'Required System Characteristics', [
['CR04:001', 'ระบบต้องพัฒนาบนสถาปัตยกรรม Web-based Application', 'Operational context'],
['CR04:002', 'ระบบต้องเก็บข้อมูลแบบ Relational Database และรักษา Referential Integrity', 'Data requirements'],
['CR04:003', 'ระบบต้องรองรับการเข้าสู่ระบบด้วย Username/Password และกำหนดบทบาทผู้ใช้', 'FR-002'],
['CR04:004', 'ระบบต้องทำงานบน PHP 8 ขึ้นไป, MySQL/MariaDB และ Node.js', 'NFR-008'],
['CR04:001', 'ระบบต้องพัฒนาบนสถาปัตยกรรม Web-based Application'],
['CR04:002', 'ระบบต้องเก็บข้อมูลแบบ Relational Database และรักษา Referential Integrity'],
['CR04:003', 'ระบบต้องรองรับการเข้าสู่ระบบด้วย Username/Password และกำหนดบทบาทผู้ใช้'],
['CR04:004', 'ระบบต้องทำงานบน PHP 8 ขึ้นไป, MySQL/MariaDB และ Node.js'],
]],
['CR05', 'Human Engineering Considerations', [
['CR05:001', 'UI ต้องเป็น Responsive ใช้งานได้ทั้งบน Desktop และอุปกรณ์หน้าคลังสินค้า (Tablet/Mobile)', 'NFR-005'],
['CR05:002', 'เมนูและปุ่มคำสั่งต้องแสดงตามบทบาทและสิทธิ์การเข้าถึงของผู้ใช้', 'Interface requirements'],
['CR05:003', 'ระบบต้องแสดงผลการตรวจสอบข้อมูล สถานะ ความสำเร็จ และข้อผิดพลาดอย่างชัดเจน', 'Interface requirements'],
['CR05:004', 'ระบบต้องมีขั้นตอนยืนยันก่อนลบ ยกเลิก หรือทำรายการที่ย้อนกลับไม่ได้', 'Usability practice'],
['CR05:005', 'ระบบต้องพิมพ์เอกสารธุรกิจและฉลากบาร์โค้ดในรูปแบบที่ใช้งานได้', 'Interface requirements'],
['CR05:001', 'UI ต้องเป็น Responsive ใช้งานได้ทั้งบน Desktop และอุปกรณ์หน้าคลังสินค้า (Tablet/Mobile)'],
['CR05:002', 'เมนูและปุ่มคำสั่งต้องแสดงตามบทบาทและสิทธิ์การเข้าถึงของผู้ใช้'],
['CR05:003', 'ระบบต้องแสดงผลการตรวจสอบข้อมูล สถานะ ความสำเร็จ และข้อผิดพลาดอย่างชัดเจน'],
['CR05:004', 'ระบบต้องมีขั้นตอนยืนยันก่อนลบ ยกเลิก หรือทำรายการที่ย้อนกลับไม่ได้'],
['CR05:005', 'ระบบต้องพิมพ์เอกสารธุรกิจและฉลากบาร์โค้ดในรูปแบบที่ใช้งานได้'],
]],
['CR06', 'Security Considerations', [
['CR06:001', 'การเชื่อมต่อในสภาพแวดล้อมใช้งานจริงต้องเข้ารหัสด้วย HTTPS/TLS', 'NFR-001'],
['CR06:002', 'ค่าตั้งค่าและความลับของระบบต้องไม่ถูกเก็บใน Source Control และไม่เข้าถึงได้จากเว็บสาธารณะ', 'NFR-001'],
['CR06:003', 'การทำงานฝั่งเซิร์ฟเวอร์ต้องตรวจสอบข้อมูลนำเข้า ยืนยันตัวตน ตรวจสิทธิ์ และจำกัดขอบเขตบริษัท/คลัง', 'NFR-002'],
['CR06:004', 'ระบบต้องจำกัดสิทธิ์การเข้าถึงตามบทบาท (Role-based Access Control) ทั้งใน UI และฝั่งเซิร์ฟเวอร์', 'FR-002'],
['CR06:005', 'ระบบต้องป้องกัน SQL Injection และ Cross-Site Scripting', 'NFR-002'],
['CR06:006', 'ระบบต้องบล็อกการเข้าสู่ระบบซ้ำซ้อน (Concurrent Login) ของบัญชีเดียวกัน', 'FR-003'],
['CR06:001', 'การเชื่อมต่อในสภาพแวดล้อมใช้งานจริงต้องเข้ารหัสด้วย HTTPS/TLS'],
['CR06:002', 'ค่าตั้งค่าและความลับของระบบต้องไม่ถูกเก็บใน Source Control และไม่เข้าถึงได้จากเว็บสาธารณะ'],
['CR06:003', 'การทำงานฝั่งเซิร์ฟเวอร์ต้องตรวจสอบข้อมูลนำเข้า ยืนยันตัวตน ตรวจสิทธิ์ และจำกัดขอบเขตบริษัท/คลัง'],
['CR06:004', 'ระบบต้องจำกัดสิทธิ์การเข้าถึงตามบทบาท (Role-based Access Control) ทั้งใน UI และฝั่งเซิร์ฟเวอร์'],
['CR06:005', 'ระบบต้องป้องกัน SQL Injection และ Cross-Site Scripting'],
['CR06:006', 'ระบบต้องบล็อกการเข้าสู่ระบบซ้ำซ้อน (Concurrent Login) ของบัญชีเดียวกัน'],
]],
['CR07', 'Environmental Considerations', [
['CR07:001', 'ระบบต้องทำงานบน Linux Server ในรูปแบบติดตั้งเอง (LAMP) หรือ Docker Compose', 'NFR-004'],
['CR07:002', 'ระบบต้องใช้เขตเวลา Asia/Bangkok อย่างสม่ำเสมอทั้งแอปพลิเคชันและงานตามกำหนดเวลา', 'NFR-010'],
['CR07:003', 'ระบบต้องใช้งานได้บนอุปกรณ์ Desktop และอุปกรณ์พกพาผ่าน Web Browser', 'NFR-005'],
['CR07:004', 'ระบบต้องแยกข้อมูลสาธิต/ทดสอบออกจากข้อมูลใช้งานจริงได้', 'Data requirements'],
['CR07:001', 'ระบบต้องทำงานบน Linux Server ในรูปแบบติดตั้งเอง (LAMP) หรือ Docker Compose'],
['CR07:002', 'ระบบต้องใช้เขตเวลา Asia/Bangkok อย่างสม่ำเสมอทั้งแอปพลิเคชันและงานตามกำหนดเวลา'],
['CR07:003', 'ระบบต้องใช้งานได้บนอุปกรณ์ Desktop และอุปกรณ์พกพาผ่าน Web Browser'],
['CR07:004', 'ระบบต้องแยกข้อมูลสาธิต/ทดสอบออกจากข้อมูลใช้งานจริงได้'],
]],
['CR08', 'Operational Considerations', [
['CR08:001', 'ระบบต้องมีการสำรองฐานข้อมูลอัตโนมัติรายวัน', 'NFR-004'],
['CR08:002', 'ระบบต้องกู้คืนข้อมูลจากชุดสำรองได้ตามขั้นตอนที่จัดทำเป็นเอกสาร', 'NFR-004'],
['CR08:003', 'ระบบต้องมีการเฝ้าระวังสถานะบริการ Web, Database และ Node.js พร้อมแจ้งเตือนเมื่อขัดข้อง', 'NFR-004'],
['CR08:004', 'ระบบต้องรองรับหลายบริษัท (Multi-company) และหลายคลังสินค้าภายใต้การแยกข้อมูล', 'FR-024'],
['CR08:005', 'งานตามกำหนดเวลาต้องทำงานสำเร็จโดยไม่สร้างผลลัพธ์ซ้ำหรือเกินสิทธิ์', 'FR-022'],
['CR08:001', 'ระบบต้องมีการสำรองฐานข้อมูลอัตโนมัติรายวัน'],
['CR08:002', 'ระบบต้องกู้คืนข้อมูลจากชุดสำรองได้ตามขั้นตอนที่จัดทำเป็นเอกสาร'],
['CR08:003', 'ระบบต้องมีการเฝ้าระวังสถานะบริการ Web, Database และ Node.js พร้อมแจ้งเตือนเมื่อขัดข้อง'],
['CR08:004', 'ระบบต้องรองรับหลายบริษัท (Multi-company) และหลายคลังสินค้าภายใต้การแยกข้อมูล'],
['CR08:005', 'งานตามกำหนดเวลาต้องทำงานสำเร็จโดยไม่สร้างผลลัพธ์ซ้ำหรือเกินสิทธิ์'],
]],
['CR09', 'Maintenance Considerations', [
['CR09:001', 'ซอฟต์แวร์ต้องมีโครงสร้างแบบโมดูล (Manager Classes / API) เพื่อให้แก้ไขได้โดยไม่กระทบส่วนอื่น', 'NFR-007'],
['CR09:002', 'ระบบต้องมีแม่แบบค่าตั้งค่า (Configuration Template) และควบคุมเวอร์ชันด้วย Git', 'NFR-007'],
['CR09:003', 'ระบบต้องมีเอกสารคู่มือการบำรุงรักษาและประวัติข้อบกพร่องที่แก้ไขแล้ว', 'NFR-004'],
['CR09:004', 'การเปลี่ยนแปลงโครงสร้างฐานข้อมูลต้องสะท้อนใน setup.php และตาราง schema_migrations', 'NFR-007'],
['CR09:001', 'ซอฟต์แวร์ต้องมีโครงสร้างแบบโมดูล (Manager Classes / API) เพื่อให้แก้ไขได้โดยไม่กระทบส่วนอื่น'],
['CR09:002', 'ระบบต้องมีแม่แบบค่าตั้งค่า (Configuration Template) และควบคุมเวอร์ชันด้วย Git'],
['CR09:003', 'ระบบต้องมีเอกสารคู่มือการบำรุงรักษาและประวัติข้อบกพร่องที่แก้ไขแล้ว'],
['CR09:004', 'การเปลี่ยนแปลงโครงสร้างฐานข้อมูลต้องสะท้อนใน setup.php และตาราง schema_migrations'],
]],
['CR10', 'Installation Considerations', [
['CR10:001', 'ระบบต้องติดตั้งฐานข้อมูลได้ในขั้นตอนเดียวผ่าน setup.php', 'NFR-004'],
['CR10:002', 'ระบบต้องติดตั้งแบบ Container ได้ด้วยคำสั่ง docker compose up -d --build', 'NFR-004'],
['CR10:003', 'ระบบต้องมีสคริปต์สร้างไฟล์ .env และค่าความลับอัตโนมัติ (docker/init-env.sh)', 'NFR-001'],
['CR10:004', 'ระบบต้องมีเอกสารขั้นตอนการติดตั้งและตั้งค่า', 'NFR-004'],
['CR10:001', 'ระบบต้องติดตั้งฐานข้อมูลได้ในขั้นตอนเดียวผ่าน setup.php'],
['CR10:002', 'ระบบต้องติดตั้งแบบ Container ได้ด้วยคำสั่ง docker compose up -d --build'],
['CR10:003', 'ระบบต้องมีสคริปต์สร้างไฟล์ .env และค่าความลับอัตโนมัติ (docker/init-env.sh)'],
['CR10:004', 'ระบบต้องมีเอกสารขั้นตอนการติดตั้งและตั้งค่า'],
]],
['CR11', 'Support Considerations', [
['CR11:001', 'ต้องมีคู่มือผู้ใช้งาน (Software User Document)', 'Documentation'],
['CR11:002', 'ต้องมีคู่มือปฏิบัติงานสำหรับผู้ดูแลระบบ (Product Operation Guide)', 'Documentation'],
['CR11:003', 'ต้องมีการอบรมผู้ใช้งานก่อนเปิดใช้งานจริง', 'Training'],
['CR11:004', 'ต้องมีช่องทางสนับสนุนและระดับการให้บริการ (SLA) หลังส่งมอบ', 'Maintenance'],
['CR11:001', 'ต้องมีคู่มือผู้ใช้งาน (Software User Document)'],
['CR11:002', 'ต้องมีคู่มือปฏิบัติงานสำหรับผู้ดูแลระบบ (Product Operation Guide)'],
['CR11:003', 'ต้องมีการอบรมผู้ใช้งานก่อนเปิดใช้งานจริง'],
['CR11:004', 'ต้องมีช่องทางสนับสนุนและระดับการให้บริการ (SLA) หลังส่งมอบ'],
]],
['CR12', 'Design Constraints', [
['CR12:001', 'ระบบต้องพัฒนาด้วย PHP, MariaDB และ Node.js ตามที่องค์กรอนุมัติ', 'NFR-008'],
['CR12:002', 'ระบบต้องควบคุม Source Code ด้วย Git โดยมี main เป็น Baseline หลัก', 'Configuration'],
['CR12:003', 'รหัสผ่านผู้ใช้ต้องเข้ารหัสด้วย bcrypt และไม่เก็บหรือบันทึกเป็นข้อความธรรมดา', 'NFR-002'],
['CR12:004', 'โครงการต้องจัดทำ Work Products ตามมาตรฐาน ISO/IEC 29110 Basic Profile', 'NFR-009'],
['CR12:001', 'ระบบต้องพัฒนาด้วย PHP, MariaDB และ Node.js ตามที่องค์กรอนุมัติ'],
['CR12:002', 'ระบบต้องควบคุม Source Code ด้วย Git โดยมี main เป็น Baseline หลัก'],
['CR12:003', 'รหัสผ่านผู้ใช้ต้องเข้ารหัสด้วย bcrypt และไม่เก็บหรือบันทึกเป็นข้อความธรรมดา'],
['CR12:004', 'โครงการต้องจัดทำ Work Products ตามมาตรฐาน ISO/IEC 29110 Basic Profile'],
]],
['CR13', 'Safety and Reliability Considerations', [
['CR13:001', 'การเปลี่ยนแปลงฐานข้อมูลที่เกี่ยวข้องกัน (เอกสาร + สต๊อก + GL) ต้องเป็น Transaction เดียวกัน', 'NFR-003'],
['CR13:002', 'ระบบต้องป้องกันยอดสต๊อกติดลบและความเคลื่อนไหวซ้ำซ้อน', 'NFR-003'],
['CR13:003', 'ระบบต้องรองรับการสำรองและกู้คืน (Backup & Recovery) ทั้ง Source Code และฐานข้อมูล', 'NFR-004'],
['CR13:004', 'ระบบต้องจัดการข้อผิดพลาดโดยแสดงข้อความที่เข้าใจได้แทน Fatal Error', 'NFR-006'],
['CR13:001', 'การเปลี่ยนแปลงฐานข้อมูลที่เกี่ยวข้องกัน (เอกสาร + สต๊อก + GL) ต้องเป็น Transaction เดียวกัน'],
['CR13:002', 'ระบบต้องป้องกันยอดสต๊อกติดลบและความเคลื่อนไหวซ้ำซ้อน'],
['CR13:003', 'ระบบต้องรองรับการสำรองและกู้คืน (Backup & Recovery) ทั้ง Source Code และฐานข้อมูล'],
['CR13:004', 'ระบบต้องจัดการข้อผิดพลาดโดยแสดงข้อความที่เข้าใจได้แทน Fatal Error'],
]],
['CR14', 'Quality Expectations', [
['CR14:001', 'ระบบต้องผ่านการทดสอบตาม Test Case ที่กำหนดก่อนส่งมอบ', 'Acceptance'],
['CR14:002', 'ทุกความต้องการต้องสอบกลับได้ถึงการออกแบบ ส่วนประกอบ และหลักฐานการทดสอบ', 'NFR-009'],
['CR14:003', 'ระบบต้องผ่านการทดสอบการยอมรับ (UAT) โดยตัวแทนลูกค้าบนสภาพแวดล้อมใช้งานจริง', 'Acceptance'],
['CR14:004', 'ต้องไม่มีข้อบกพร่องระดับวิกฤตค้างอยู่ในด้านความปลอดภัย การแยกข้อมูล และความถูกต้องของสต๊อก/บัญชี', 'Acceptance'],
['CR14:001', 'ระบบต้องผ่านการทดสอบตาม Test Case ที่กำหนดก่อนส่งมอบ'],
['CR14:002', 'ทุกความต้องการต้องสอบกลับได้ถึงการออกแบบ ส่วนประกอบ และหลักฐานการทดสอบ'],
['CR14:003', 'ระบบต้องผ่านการทดสอบการยอมรับ (UAT) โดยตัวแทนลูกค้าบนสภาพแวดล้อมใช้งานจริง'],
['CR14:004', 'ต้องไม่มีข้อบกพร่องระดับวิกฤตค้างอยู่ในด้านความปลอดภัย การแยกข้อมูล และความถูกต้องของสต๊อก/บัญชี'],
]],
];
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