27 Commits
Author SHA1 Message Date
Thanakorn 53cd5c36e0 docs(sdlc): expand weak points in audit prep note 2026-10-02 08:23:11 +07:00
Thanakorn 46f334fe96 chore(sdlc): stop tracking sdlc-delivery.zip 2026-10-02 08:22:16 +07:00
Thanakorn 13ac9958f3 docs(sdlc): reword Change Report row in List of Evidence; re-render delivery 2026-10-02 08:19:52 +07:00
Thanakorn 073d8f8f37 docs(sdlc): drop advisor feedback response from audit prep note 2026-10-02 08:12:51 +07:00
Thanakorn 2def1ef921 docs(sdlc): add ISO 29110 audit prep note; keep top-level sdlc notes out of seed and render 2026-10-02 08:05:10 +07:00
Thanakorn 4f1c2a0ad1 docs(sdlc): merge group heading rows across the full table width 2026-09-28 13:53:21 +07:00
Thanakorn 31f2629ddf docs(sdlc): cite only main and commits up to project end; guard enforces it 2026-09-25 15:46:19 +07:00
Thanakorn 7b5cc192d9 docs(sdlc): drop perfective maintenance; say Git Repository, not branch sdlc 2026-09-25 15:34:08 +07:00
Thanakorn fe9c7e612f build(sdlc): convert the rendered PDFs to Word in build-sdlc-delivery.sh
Uses local pdf2docx when present, else a throwaway python container.
2026-09-25 15:21:55 +07:00
ThanakornandClaude Opus 5.5 6c857005c0 docs(sdlc): write each work product as of its own date
- SOW, plan and design no longer cite the Aug baseline commit or the package tag
- Work Schedule status derived from its date; Test Case has no results yet
- build fails when a document cites a later commit, tag, ISS or ID (timeline.mjs)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-25 14:52:21 +07:00
Thanakorn be020e332b build(sdlc): seed data, figures and rendered package for 23/09 feedback 2026-09-24 09:19:05 +07:00
Thanakorn 6c1d66d8de docs(sdlc): update work products for 23/09 review feedback 2026-09-24 09:19:05 +07:00
Thanakorn e5cbce6022 Update PHP image, vhost config and SDLC delivery package 2026-09-14 12:48:12 +07:00
Thanakorn 6c166aa88c Fix Word fonts, bullets and footer tab; restore dropped text 2026-09-14 09:54:02 +07:00
Thanakorn 0c11eda88e Draw the Word tables the way the PDF prints them 2026-09-11 17:30:45 +07:00
Thanakorn 7473115fe2 Give the Word package a real page header for the CI band 2026-09-11 17:11:49 +07:00
Thanakorn 75ff043036 Add Word rendering of the SDLC delivery package 2026-09-11 16:47:46 +07:00
Thanakorn f1f805c3a0 docs(sdlc): correct header CI to the reference measurements 2026-09-09 14:11:27 +07:00
Thanakorn 97dc634c19 docs(sdlc): match reference PDF layout and company CI 2026-09-09 13:54:45 +07:00
Thanakorn 9553d94ec6 docs(sdlc): reformat work products to audited reference layout 2026-09-09 08:09:54 +07:00
Thanakorn f2da2f51b5 docs(delivery): rebuild final SDLC package 2026-08-26 13:17:58 +07:00
Thanakorn 358c33c7d1 docs(sdlc): close training and operational actions 2026-08-26 13:10:06 +07:00
Thanakorn 45b2415beb docs(sdlc): finalize ISO 29110 evidence package 2026-08-26 12:35:57 +07:00
Thanakorn c7821b9d2d docs(sdlc): regenerate delivery package 2026-08-19 14:14:07 +07:00
Thanakorn 7d211be7e5 docs(sdlc): align V1.0 audit evidence 2026-08-19 14:11:00 +07:00
Thanakorn bbc3654180 Fix SDLC delivery rendering and regenerate package 2026-08-19 10:18:32 +07:00
Thanakorn 849158059b SDLC docs feedback and additional adjustment 2026-08-19 10:02:21 +07:00
296 changed files with 9913 additions and 5269 deletions
+3
View File
@@ -16,3 +16,6 @@ lib/zxcvbn-php-master/vendor/sebastian/
notes/ notes/
docs/ docs/
SESSION.php SESSION.php
# SDLC delivery zip is packaged by hand, outside the repo
sdlc-delivery.zip
+2
View File
@@ -2,6 +2,8 @@ FROM php:8.3-apache
RUN apt-get update && apt-get install -y --no-install-recommends \ RUN apt-get update && apt-get install -y --no-install-recommends \
libzip-dev libicu-dev libonig-dev default-mysql-client gettext-base \ libzip-dev libicu-dev libonig-dev default-mysql-client gettext-base \
libpng-dev libjpeg-dev libfreetype6-dev \
&& docker-php-ext-configure gd --with-jpeg --with-freetype \
&& docker-php-ext-install pdo_mysql mysqli mbstring gd zip intl sockets exif opcache \ && docker-php-ext-install pdo_mysql mysqli mbstring gd zip intl sockets exif opcache \
&& a2enmod rewrite \ && a2enmod rewrite \
&& apt-get clean && rm -rf /var/lib/apt/lists/* && apt-get clean && rm -rf /var/lib/apt/lists/*
+23
View File
@@ -0,0 +1,23 @@
# Serves the app at the vhost root instead of under /wms-app/.
# Mounted over the image's default site by docker-compose.override.yml.
<VirtualHost *:80>
DocumentRoot /var/www/html/wms-app
<Directory /var/www/html/wms-app>
Options -Indexes +FollowSymLinks
AllowOverride All
Require all granted
</Directory>
# Never serve deploy artefacts or seeders over HTTP.
<FilesMatch "^(\.env|docker-compose(\.override)?\.yml|.*\.log|setup\.php|demo_seed.*\.php|finish-deploy\.sh)$">
Require all denied
</FilesMatch>
# nginx terminates TLS upstream; let PHP know the real scheme so the app
# does not emit http:// URLs into an https page.
SetEnvIf X-Forwarded-Proto "^https$" HTTPS=on
ErrorLog ${APACHE_LOG_DIR}/error.log
CustomLog ${APACHE_LOG_DIR}/access.log combined
</VirtualHost>
-143
View File
@@ -1,143 +0,0 @@
<?php
declare(strict_types=1);
if ($argc !== 2) {
fwrite(STDERR, "Usage: php adjust_docx_whitespace.php <document.docx>\n");
exit(2);
}
$path = $argv[1];
$wordNs = 'http://schemas.openxmlformats.org/wordprocessingml/2006/main';
$zip = new ZipArchive();
if ($zip->open($path) !== true) {
throw new RuntimeException("Cannot open DOCX: {$path}");
}
$documentXml = $zip->getFromName('word/document.xml');
if ($documentXml === false) {
throw new RuntimeException('word/document.xml is missing');
}
$doc = new DOMDocument('1.0', 'UTF-8');
$doc->preserveWhiteSpace = false;
$doc->formatOutput = false;
if (!$doc->loadXML($documentXml)) {
throw new RuntimeException('word/document.xml is invalid');
}
$xpath = new DOMXPath($doc);
$xpath->registerNamespace('w', $wordNs);
function attr(DOMElement $element, string $name, string $value, string $ns): void
{
$element->setAttributeNS($ns, 'w:' . $name, $value);
}
function child(DOMDocument $doc, DOMElement $parent, string $localName, string $ns): DOMElement
{
foreach ($parent->childNodes as $node) {
if ($node instanceof DOMElement && $node->namespaceURI === $ns && $node->localName === $localName) {
return $node;
}
}
$element = $doc->createElementNS($ns, 'w:' . $localName);
$parent->appendChild($element);
return $element;
}
foreach ($xpath->query('//w:body/w:p | //w:body/w:tbl//w:p') as $paragraph) {
if (!$paragraph instanceof DOMElement) {
continue;
}
$text = '';
foreach ($xpath->query('.//w:t', $paragraph) as $textNode) {
$text .= $textNode->textContent;
}
$text = trim(preg_replace('/\s+/u', ' ', $text) ?? '');
$pPrNode = $xpath->query('./w:pPr', $paragraph)->item(0);
if (!$pPrNode instanceof DOMElement) {
$pPrNode = $doc->createElementNS($wordNs, 'w:pPr');
$paragraph->insertBefore($pPrNode, $paragraph->firstChild);
}
$spacing = child($doc, $pPrNode, 'spacing', $wordNs);
$before = '0';
$after = '30';
$line = '320';
if ($text === '') {
$after = '20';
$line = '120';
} elseif (preg_match('/^[1-7]\.\s/u', $text)) {
$before = '120';
$after = '45';
$line = '330';
} elseif (preg_match('/^[1-7]\.\d\s/u', $text)) {
$before = '70';
$after = '30';
$line = '320';
} elseif (str_starts_with($text, '•')) {
$after = '15';
$line = '310';
} elseif (str_starts_with($text, 'วันที่ 5 มกราคม')) {
$before = '40';
$after = '100';
$line = '330';
} elseif (str_starts_with($text, 'เรื่อง') || str_starts_with($text, 'เรียน')) {
$after = '55';
$line = '330';
} elseif (str_starts_with($text, 'ลงชื่อ')) {
$before = '160';
$after = '10';
$line = '310';
} elseif (str_starts_with($text, '(') || str_starts_with($text, 'ตำแหน่ง') || str_starts_with($text, 'วันที่ _') || str_starts_with($text, 'ผู้มีอำนาจ')) {
$after = '10';
$line = '310';
} elseif (str_starts_with($text, 'บริษัท บี.อาร์.เอ็น.') || str_starts_with($text, 'เอกสารฉบับนี้') || str_starts_with($text, 'ผลส่งมอบประกอบ') || str_starts_with($text, 'การเปลี่ยนแปลง') || str_starts_with($text, 'จึงจัดทำ')) {
$after = '55';
$line = '330';
}
attr($spacing, 'before', $before, $wordNs);
attr($spacing, 'after', $after, $wordNs);
attr($spacing, 'line', $line, $wordNs);
attr($spacing, 'lineRule', 'auto', $wordNs);
}
foreach ($xpath->query('//w:sectPr/w:pgMar') as $margin) {
if (!$margin instanceof DOMElement) {
continue;
}
attr($margin, 'top', '850', $wordNs);
attr($margin, 'bottom', '850', $wordNs);
attr($margin, 'left', '1080', $wordNs);
attr($margin, 'right', '1080', $wordNs);
attr($margin, 'header', '400', $wordNs);
attr($margin, 'footer', '400', $wordNs);
}
foreach ($xpath->query('//w:tbl/w:tblPr') as $tableProperties) {
if (!$tableProperties instanceof DOMElement) {
continue;
}
$margins = child($doc, $tableProperties, 'tblCellMar', $wordNs);
foreach (['top' => '45', 'left' => '70', 'bottom' => '45', 'right' => '70'] as $side => $width) {
$edge = child($doc, $margins, $side, $wordNs);
attr($edge, 'w', $width, $wordNs);
attr($edge, 'type', 'dxa', $wordNs);
}
}
$updatedXml = $doc->saveXML();
if ($updatedXml === false) {
throw new RuntimeException('Cannot serialize adjusted document XML');
}
if (!$zip->addFromString('word/document.xml', $updatedXml)) {
throw new RuntimeException('Cannot update word/document.xml');
}
$zip->close();
echo $path . PHP_EOL;
+39 -1
View File
@@ -14,6 +14,16 @@ if [[ ! -d "$TOOL_DIR/node_modules/playwright" ]]; then
npx --prefix "$TOOL_DIR" playwright install chromium npx --prefix "$TOOL_DIR" playwright install chromium
fi fi
# The Software Design figures are drawn in scripts/sdlc-seed/figures.mjs and
# rasterised with the document font before the seed copies them into sdlc/.
echo "Rendering Software Design figures..."
node "$TOOL_DIR/render-figures.mjs"
# sdlc/ is generated output: the seed rebuilds every work product from the
# controlled project data in scripts/sdlc-seed/ before anything is rendered.
echo "Seeding work products from project data..."
node "$SCRIPT_DIR/sdlc-seed/build.mjs"
rm -rf "$STAGING_DIR" rm -rf "$STAGING_DIR"
mkdir -p "$STAGING_DIR" mkdir -p "$STAGING_DIR"
@@ -22,7 +32,35 @@ while IFS= read -r -d '' source; do
destination="$STAGING_DIR/${relative%.md}.pdf" destination="$STAGING_DIR/${relative%.md}.pdf"
echo "Rendering ${relative%.md}.pdf" echo "Rendering ${relative%.md}.pdf"
node "$TOOL_DIR/render-sdlc.mjs" "$source" "$destination" node "$TOOL_DIR/render-sdlc.mjs" "$source" "$destination"
done < <(find "$SOURCE_DIR" -type f -name '*.md' -print0 | sort -z) done < <(find "$SOURCE_DIR" -mindepth 2 -type f -name '*.md' -print0 | sort -z)
# Every .pdf gets a Word copy beside it, reconstructed from the rendered page.
# The CI header bands need Playwright, so they are rendered here with node and
# handed to the converter, which then only needs Python with pdf2docx: the local
# interpreter ($PYTHON) when it has it, otherwise a throwaway python container.
BAND_DIR="$(mktemp -d)"
trap 'rm -rf "$BAND_DIR"' EXIT
echo "Rendering CI header bands..."
node "$TOOL_DIR/render-ci-band.mjs" "$SOURCE_DIR" "$BAND_DIR"
echo "Converting PDFs to Word..."
PYTHON="${PYTHON:-python3}"
if "$PYTHON" -c 'import pdf2docx, docx' 2>/dev/null; then
"$PYTHON" "$TOOL_DIR/convert-to-docx.py" "$SOURCE_DIR" "$STAGING_DIR" "$BAND_DIR"
elif command -v docker >/dev/null 2>&1; then
PY_CACHE="${XDG_CACHE_HOME:-$HOME/.cache}/sdlc-delivery-python"
mkdir -p "$PY_CACHE"
docker run --rm --user "$(id -u):$(id -g)" -e HOME=/tmp -e PYTHONPATH=/py \
-v "$ROOT_DIR:$ROOT_DIR" -v "$BAND_DIR:$BAND_DIR:ro" -v "$PY_CACHE:/py" \
-w "$ROOT_DIR" python:3.12-slim sh -c '
python -c "import pdf2docx, docx" 2>/dev/null ||
pip install --quiet --disable-pip-version-check --target /py -r "$1"
shift; python "$@"' _ \
"$TOOL_DIR/requirements.txt" "$TOOL_DIR/convert-to-docx.py" "$SOURCE_DIR" "$STAGING_DIR" "$BAND_DIR"
else
echo "pdf2docx is not available: pip install -r $TOOL_DIR/requirements.txt, or install docker." >&2
exit 1
fi
echo "Verifying staged delivery package..." echo "Verifying staged delivery package..."
"$SCRIPT_DIR/verify-sdlc-delivery.sh" "$STAGING_DIR" "$SCRIPT_DIR/verify-sdlc-delivery.sh" "$STAGING_DIR"
-117
View File
@@ -1,117 +0,0 @@
<?php
declare(strict_types=1);
/*
* Applies the approved print layout from the first formatted Progress Status
* Record to every generated Progress Status Record HTML file. Markdown source
* files are deliberately not read or changed.
*
* Usage:
* php scripts/format_progress_status_html.php
*/
$root = dirname(__DIR__);
$directory = $root . '/sdlc/1-PM Process (10 Work Product)/3.Progess Status Record';
$templatePath = $directory . '/200-WMS-26-001-00 Progress Status Record 25690123 V1.0 ApS.html';
if (!is_file($templatePath)) {
fwrite(STDERR, "Template Progress Status Record HTML was not found.\n");
exit(1);
}
$template = file_get_contents($templatePath);
if ($template === false || !preg_match('~(/\* BRN Progress Status Record print layout.*?</style>)~s', $template, $match)) {
fwrite(STDERR, "The template does not contain the approved Progress Status Record CSS.\n");
exit(1);
}
// The CSS must work for every record number, not just record 1.
$layoutCss = str_replace(
'#progress-status-record-1-of-13',
'h1[id^="progress-status-record-"]',
$match[1]
);
/** @return string */
function approvalCard(string $id, string $title, string $paragraph): string
{
$lines = preg_split('~<br\s*/?>\s*~i', trim($paragraph)) ?: [];
$items = [];
foreach ($lines as $line) {
$line = trim($line);
if ($line === '') {
continue;
}
if (preg_match('~^(Signature|Date):\s*_*\s*$~i', strip_tags($line), $label)) {
$items[] = '<p class="sign-row"><strong>' . htmlspecialchars($label[1], ENT_QUOTES, 'UTF-8') . ':</strong><span class="write-line"></span></p>';
} else {
$items[] = '<p>' . $line . '</p>';
}
}
return '<div class="approval-card"><h3 id="' . $id . '">' . $title . '</h3>' . implode("\n", $items) . '</div>';
}
$files = glob($directory . '/*.html') ?: [];
$formatted = 0;
$skipped = 0;
foreach ($files as $path) {
$html = file_get_contents($path);
if ($html === false) {
fwrite(STDERR, "Could not read: $path\n");
continue;
}
if (str_contains($html, '/* BRN Progress Status Record print layout')) {
$skipped++;
continue;
}
$html = preg_replace('~</head>~', "<style>\n$layoutCss\n</style>\n</head>", $html, 1, $headCount);
if ($headCount !== 1) {
fwrite(STDERR, "Skipped malformed HTML: " . basename($path) . "\n");
continue;
}
$header = '<header class="psr-letterhead"><img class="document-header-image" src="../../../app/assets/images/brn-document-header-left.png" alt="BRN corporate identity"><div class="psr-title-band">Progress Status Record</div></header>';
$html = preg_replace(
'~(<h1 id="progress-status-record-[^"]+">.*?</h1>\s*)<table>~s',
$header . "\n" . '$1<table class="document-control">',
$html,
1,
$titleCount
);
if ($titleCount !== 1) {
fwrite(STDERR, "Skipped unsupported record structure: " . basename($path) . "\n");
continue;
}
if (preg_match('~<h2 id="8-approval">8\. Approval</h2>(.*?)(?=</body>)~s', $html, $approvalMatch)) {
preg_match_all('~<h3 id="([^"]+)">(.*?)</h3>\s*<p>(.*?)</p>~s', $approvalMatch[1], $blocks, PREG_SET_ORDER);
if (count($blocks) === 3) {
$cards = [];
foreach ($blocks as $block) {
$cards[] = approvalCard($block[1], $block[2], $block[3]);
}
$approval = '<section class="approval-page"><h2 id="8-approval">8. Approval</h2><div class="approval-grid">'
. implode("\n", $cards)
. '</div></section>';
$html = str_replace($approvalMatch[0], $approval, $html);
}
}
$date = 'Progress Status Record';
if (preg_match('~<td>Report date</td>\s*<td>(.*?)</td>~s', $html, $dateMatch)) {
$date = trim(strip_tags($dateMatch[1]));
}
$footer = '<footer class="psr-footer">200-WMS-26-001-00 · BRN WMS · Progress Status Record · '
. htmlspecialchars($date, ENT_QUOTES, 'UTF-8') . ' · V1.0 Final</footer>';
$html = str_replace('</body>', $footer . "\n</body>", $html);
if (file_put_contents($path, $html) === false) {
fwrite(STDERR, "Could not write: $path\n");
continue;
}
$formatted++;
}
printf("Formatted: %d; already formatted: %d\n", $formatted, $skipped);
@@ -1,182 +0,0 @@
<?php
declare(strict_types=1);
$directory = __DIR__ . '/../sdlc/1-PM Process (10 Work Product)/3.Progess Status Record';
$records = [
['25690123','5–23 January 2026','Project initiation','Establish the project need, stakeholders, objectives, scope, and authority.','Formal project start and scope boundary were established retrospectively from the agreed lifecycle.','Agreed reconstructed','Statement of Work; Work Schedule','Not rated — reconstructed','Historical initiation records were not created contemporaneously.','Complete customer requirements and project planning baseline.'],
['25690218','12 January–18 February 2026','Requirements and planning baseline','Collect customer requirements and define schedule, resources, risks, controls, and work-product responsibilities.','Customer Requirements, Work Schedule, and Software Project Plan were reconstructed from agreed scope and implementation evidence.','Document reconstructed','Project Plan work products','Not rated — reconstructed','Exact elicitation dates and contemporaneous approvals are unavailable.','Begin controlled software implementation on 19 February 2026.'],
['25690225','19–25 February 2026','Application initialization','Initialize the repository and establish initial application and file-upload capability.','WMS repository initialized; verification commit and dropzone/file-upload work completed.','Git evidenced','`9a50080`, `42a3876`, `8625652`, `1843308`','Green','No unresolved blocker is evidenced in the repository for this period.','Protect configuration, address security findings, and establish the stock database foundation.'],
['25690317','9–17 March 2026','Security, database, and ICS foundation','Protect local configuration, address initial security findings, design the stock database, and introduce inventory-control modules.','Configuration was removed from tracking, security-audit fixes were applied, stock database design was committed, and ICS modules were introduced.','Git evidenced','`93d903c`, `7cb78d0`, `2e558a5`, `a4f474b`','Green','The period contains a gap in commit activity before the documented security/database work.','Develop stock visibility, warehouse structure, product controls, and operational reports.'],
['25690429','9–29 April 2026','Inventory, warehouse, and access foundation','Implement stock dashboard, products, warehouse capacity, traceability, reports, settings, authentication, and reusable managers.','Dashboard, product files, OOP utilities, capacity/occupancy, lot/serial/expiry, reports, settings, login/onboarding, and manager classes were implemented.','Git evidenced','`3b8f94f` through `db5c47b`','Green','Rapid module growth increased the need for consistent authorization and lifecycle review.','Complete document approval logic, orders, warehouse layers, barcode, and role guards.'],
['25690508','30 April–8 May 2026','Orders, barcode, and security controls','Implement approval logic, customer order flows, flexible warehouse layers, barcode operations, and centralized role guards.','Password scoring, stock approval, orders/returns/invoices, switchable warehouse layers, barcode functions, role guards, security fixes, and naming/session corrections were completed.','Git evidenced','30-Apr through 08-May commits','Green','Security gaps were actively corrected during implementation; formal correction records remain to be linked.','Prepare production setup and introduce accounting capabilities.'],
['25690513','11–13 May 2026','Production preparation and accounting foundation','Prepare deployment, automate setup, correct onboarding/recovery, populate test data, and introduce accounting.','Production preparation, dynamic base URL, automated setup, password recovery, onboarding fixes, test data, dashboard updates, and accounting modules were committed.','Git evidenced','11–13 May commits','Green','Multiple fixes and a revert occurred during integration; results require traceability to tests.','Integrate accounting workflows, document control, access limits, and supporting services.'],
['25690523','20–23 May 2026','Accounting integration and supporting services','Integrate accounting, user invitation/access controls, transaction limits, document flows, Node.js, aggregates, and reports.','Accounting workflows, access controls, setup script, soft delete, Socket service, GL aggregation, accounting reports, journal batching, and GL automation were implemented.','Git evidenced','20–23 May commits','Green','Integration breadth increased regression and tenant-isolation risk.','Complete security hardening, notification control, sequencing, scheduler, tenant scoping, and final review.'],
['25690529','24–29 May 2026','Development baseline','Harden security and integrity, close known implementation gaps, stabilize services, and establish the substantially complete development baseline.','Session controls, aggregates, notifications, security/lifecycle reviews, transaction limits, document sequencing, role guards, scheduler fixes, bin naming, tenant scoping, and final refactoring were completed.','Git evidenced','24–29 May commits; final `a0677d6`','Green','Formal verification, validation, and acceptance evidence remained to be consolidated after development.','Conduct verification, validation, documentation, and delivery preparation.'],
['25690731','30 May–31 July 2026','Verification, validation, and documentation','Verify requirements/design/software, validate intended use, prepare operational documents, and consolidate delivery evidence.','This activity period is part of the agreed timeline, but contemporaneous Git evidence is unavailable; assurance and documentation records are being reconstructed from the delivered system.','Document reconstructed','SRS, Design, Traceability, Test, Guide, Verification, and Validation work products','Amber','Missing contemporaneous evidence creates an audit and acceptance gap.','Complete stabilization corrections, finalize evidence, and obtain stakeholder review.'],
['25690803','3 August 2026','Stabilization correction','Correct login and environment-configuration issues identified after the development baseline.','Login and configuration corrections were committed.','Git evidenced','`b2c4374`','Green','The correction requires linkage to the Correction Register and verification evidence.','Prepare representative demonstration data and complete final delivery checks.'],
['25690814','4–14 August 2026','Demonstration and completion boundary','Prepare controlled demonstration data, complete final checks, and reach the agreed project-completion boundary.','Demonstration data was committed on 14 August 2026 and the agreed formal completion date was reached.','Git evidenced / agreed boundary','`dd48a8b`; agreed completion date','Green','Formal signatures and some controlled ISO/IEC 29110 evidence remained open at completion.','Consolidate remaining work products and obtain Project Sponsor authorization.'],
['25690817','15–17 August 2026','Evidence consolidation and authorization preparation','Complete missing controlled work products, align roles, and prepare records for review and authorization.','PM work products were converted to reviewable Markdown, project identities and roles were aligned, and missing evidence was identified for subsequent completion.','Document reconstructed','Current `sdlc/` repository and Git status','Amber','Verification, validation, repository-backup, and acceptance evidence and signatures remain open.','Complete remaining PM/SI work products and obtain Project Sponsor review and authorization.'],
];
$tasksByDate = [
'25690123' => [
['1.1','Identify project need','09-Jan-2026','09-Jan-2026','100%','Statement of Work','Reconstructed completed'],
['1.2','Identify stakeholders and objectives','16-Jan-2026','16-Jan-2026','100%','Project role confirmation','Reconstructed completed'],
['1.3','Approve project scope','23-Jan-2026','Pending signature','90%','Statement of Work V1.0 Final','Approval pending'],
],
'25690218' => [
['2.1','Collect customer requirements','06-Feb-2026','06-Feb-2026','100%','Customer Requirements V1.0 Final','Reconstructed completed'],
['2.2','Prepare Software Project Plan','13-Feb-2026','13-Feb-2026','100%','Software Project Plan V1.0 Final','Reconstructed completed'],
['2.3','Baseline requirements and schedule','18-Feb-2026','18-Feb-2026','100%','Work Schedule and Project Plan','Reconstructed completed; approval pending'],
],
'25690225' => [
['3.1','Initialize WMS application','25-Feb-2026','25-Feb-2026','100%','Git `9a50080`–`1843308`','Completed'],
],
'25690317' => [
['3.2','Security and database foundation','17-Mar-2026','17-Mar-2026','100%','Git `93d903c`–`a4f474b`','Completed'],
],
'25690429' => [
['3.3','Inventory and warehouse modules','29-Apr-2026','29-Apr-2026','100%','Git `3b8f94f`–`db5c47b`','Completed'],
],
'25690508' => [
['3.4','Authentication and onboarding','12-May-2026','In progress at 08-May','70%','Commits through 08-May-2026','In progress; carried forward'],
['3.5','Order and barcode workflows','08-May-2026','08-May-2026','100%','02–08 May commits','Completed'],
],
'25690513' => [
['3.4','Authentication and onboarding','12-May-2026','12-May-2026','100%','Login/onboarding and recovery commits','Completed'],
['3.6','Production preparation and setup','13-May-2026','13-May-2026','100%','11–13 May commits','Completed'],
['3.7','Accounting and finance workflows','23-May-2026','In progress at 13-May','20%','Accounting foundation commits','In progress; carried forward'],
],
'25690523' => [
['3.7','Accounting and finance workflows','23-May-2026','23-May-2026','100%','13–23 May accounting commits','Completed'],
['3.8','Real-time services and scheduled jobs','27-May-2026','In progress at 23-May','45%','Node.js and GL aggregate commits','In progress; carried forward'],
['3.9','Security hardening and lifecycle review','28-May-2026','In progress at 23-May','35%','Access, flow, and role-guard commits','In progress; carried forward'],
],
'25690529' => [
['3.8','Real-time services and scheduled jobs','27-May-2026','27-May-2026','100%','22–27 May Node.js/scheduler commits','Completed'],
['3.9','Security hardening and lifecycle review','28-May-2026','28-May-2026','100%','21–28 May hardening/review commits','Completed'],
['3.10','Refactor and development baseline','29-May-2026','29-May-2026','100%','Git `a0677d6`','Completed'],
],
'25690731' => [
['4.3','Verify requirements, design, and implementation','31-Jul-2026','Evidence consolidation pending','80%','Reconstructed verification work products','In progress; evidence gap'],
['4.4','Customer-oriented system validation','07-Aug-2026','In progress at 31-Jul','85%','Reconstructed validation activities','In progress; carried forward'],
['4.5','Prepare operational documentation','07-Aug-2026','In progress at 31-Jul','85%','Configuration and draft work products','In progress; carried forward'],
],
'25690803' => [
['4.6','Configuration and login corrections','03-Aug-2026','03-Aug-2026','100%','Git `b2c4374`','Completed; formal correction closure pending'],
],
'25690814' => [
['4.4','Customer-oriented system validation','07-Aug-2026','Evidence consolidation pending','90%','Validation work products','In progress; approval pending'],
['4.5','Prepare operational documentation','07-Aug-2026','Evidence consolidation pending','90%','Operational documentation work products','In progress; approval pending'],
['4.7','Prepare demonstration data','14-Aug-2026','14-Aug-2026','100%','Git `dd48a8b`','Completed'],
['5.1','Final repository and work-product review','14-Aug-2026','In progress','70%','Repository and SDLC gap review','In progress; carried forward'],
['5.2','Acceptance and project closure','14-Aug-2026','Pending signature','75%','Completion boundary reached','Acceptance pending'],
],
'25690817' => [
['5.1','Final repository and work-product review','14-Aug-2026','In progress at 17-Aug','80%','BRN WMS PM work products and gap list','In progress'],
['5.2','Acceptance and project closure','14-Aug-2026','Pending evidence and signature','75%','Acceptance evidence not yet authorized','Acceptance pending'],
],
];
foreach ($records as $index => $record) {
[$date,$period,$milestone,$planned,$completed,$evidenceClass,$evidence,$rag,$issue,$next] = $record;
$number = $index + 1;
$taskRows = '';
foreach ($tasksByDate[$date] as $task) {
$taskRows .= '| ' . implode(' | ', $task) . " |\n";
}
$content = <<<MD
# Progress Status Record {$number} of 13
| Document field | Value |
|---|---|
| Document | Progress Status Record |
| Project | BRN WMS |
| Project code | 200-WMS-26-001-00 |
| Reporting period | {$period} |
| Report date | {$date} |
| Actual preparation date | 17 August 2026 |
| Release | {$date} V1.0 Final |
| Prepared by | คุณอภิรัชต์ สุภัทรประทีป — Project Manager |
| Technical evidence | ธนกร สถิตวิทยากุล — Developer / System Analyst |
| Status | Final — ready for review and authorization |
## 1. Retrospective-record disclosure
This record was prepared retrospectively on 17 August 2026. It covers the historical reporting period shown above and does not claim to have been authored or committed on the historical report date. Statements are limited to the evidence classification and references identified below.
## 2. Period objective
**Milestone:** {$milestone}
**Planned outcome:** {$planned}
## 3. Task progress
**Period summary:** {$completed}
| Task ID | Task | Planned finish | Actual / evidence date | Completion | Evidence | Status |
|---|---|---:|---:|---:|---|---|
{$taskRows}
| Evidence field | Value |
|---|---|
| Evidence classification | {$evidenceClass} |
| Evidence reference | {$evidence} |
| Overall period status | {$rag} |
## 4. Schedule status
Work is assessed against the agreed project boundaries of 5 January–14 August 2026. No unsupported numeric variance is asserted for this period. Where the evidence is reconstructed, schedule status remains explicitly unrated rather than represented as contemporaneously measured.
## 5. Issues, risks, and corrective action
**Issue or risk:** {$issue}
**Control:** Preserve the stated evidence basis, link applicable defects to the Correction Register, update traceability and tests, and obtain the required review or authorization without backdating records or signatures.
## 6. Changes
Technical commits in this period are implementation evidence. They must be classified separately as in-scope implementation, defect correction, or an authorized baseline change before being entered in the Change Report.
## 7. Next-period plan
{$next}
## 8. Approval
### Prepared by
Name: คุณอภิรัชต์ สุภัทรประทีป
Role: Project Manager
Signature: ______________________________________________
Date: ___________________________________________________
### Technical evidence provided by
Name: ธนกร สถิตวิทยากุล
Role: Developer / System Analyst
Signature: ______________________________________________
Date: ___________________________________________________
### Reviewed and authorized by
Name: คุณเสรี วิริยะสกุลธรณ์
Project roles: Project Sponsor / Customer Representative / Authorized Approver
Position: กรรมการผู้จัดการ
Company: บริษัท บี.อาร์.เอ็น. เอ็นเตอร์ไพรส์ จำกัด
Signature: ______________________________________________
Date: ___________________________________________________
MD;
$filename = "200-WMS-26-001-00 Progress Status Record {$date} V1.0 ApS.md";
if (file_put_contents($directory . '/' . $filename, $content . PHP_EOL) === false) {
throw new RuntimeException('Unable to write ' . $filename);
}
}
require __DIR__ . '/normalize_document_dates.php';
echo count($records) . " progress status records generated.\n";
-80
View File
@@ -1,80 +0,0 @@
<?php
declare(strict_types=1);
$base = __DIR__ . '/../sdlc/1-PM Process (10 Work Product)/2.Project Plan/';
$w = 'http://schemas.openxmlformats.org/wordprocessingml/2006/main';
function e(string $s): string { return htmlspecialchars($s, ENT_XML1 | ENT_QUOTES, 'UTF-8'); }
function r(string $s, bool $b = false, int $sz = 28): string {
return '<w:r><w:rPr>' . ($b ? '<w:b/><w:bCs/>' : '') . '<w:rFonts w:ascii="TH Sarabun New" w:hAnsi="TH Sarabun New" w:eastAsia="TH Sarabun New" w:cs="TH Sarabun New"/><w:sz w:val="'.$sz.'"/><w:szCs w:val="'.$sz.'"/></w:rPr><w:t xml:space="preserve">'.e($s).'</w:t></w:r>';
}
function p(string $s = '', string $a = 'left', bool $b = false, int $sz = 28, int $before = 0, int $after = 40): string {
return '<w:p><w:pPr><w:jc w:val="'.$a.'"/><w:spacing w:before="'.$before.'" w:after="'.$after.'" w:line="320" w:lineRule="auto"/></w:pPr>'.r($s,$b,$sz).'</w:p>';
}
function h(string $no, string $title): string { return p($no.' '.$title,'left',true,31,120,45); }
function b(string $s): string { return '<w:p><w:pPr><w:ind w:left="650" w:hanging="300"/><w:spacing w:after="20" w:line="310" w:lineRule="auto"/></w:pPr>'.r('• '.$s,false,28).'</w:p>'; }
function pb(): string { return '<w:p><w:r><w:br w:type="page"/></w:r></w:p>'; }
function tr(array $cells, bool $head = false, array $widths = []): string {
$x='<w:tr>';
foreach($cells as $i=>$v){$width=$widths[$i]??2200;$x.='<w:tc><w:tcPr><w:tcW w:w="'.$width.'" w:type="dxa"/>'.($head?'<w:shd w:fill="D9EAF7"/>':'').'</w:tcPr>'.p((string)$v,'left',$head,25,0,15).'</w:tc>';}
return $x.'</w:tr>';
}
function tbl(array $rows, array $widths, bool $firstHead = true): string {
$x='<w:tbl><w:tblPr><w:tblW w:w="9000" w:type="dxa"/><w:tblBorders><w:top w:val="single" w:sz="4" w:color="8AA4B5"/><w:left w:val="single" w:sz="4" w:color="8AA4B5"/><w:bottom w:val="single" w:sz="4" w:color="8AA4B5"/><w:right w:val="single" w:sz="4" w:color="8AA4B5"/><w:insideH w:val="single" w:sz="4" w:color="B7C9D6"/><w:insideV w:val="single" w:sz="4" w:color="B7C9D6"/></w:tblBorders></w:tblPr><w:tblGrid>';
foreach($widths as $wd)$x.='<w:gridCol w:w="'.$wd.'"/>';$x.='</w:tblGrid>';
foreach($rows as $i=>$row)$x.=tr($row,$firstHead&&$i===0,$widths);
return $x.'</w:tbl>';
}
function top(string $label): string {
return '<w:tbl><w:tblPr><w:tblW w:w="9000" w:type="dxa"/></w:tblPr><w:tblGrid><w:gridCol w:w="6000"/><w:gridCol w:w="3000"/></w:tblGrid><w:tr><w:tc><w:tcPr><w:tcW w:w="6000" w:type="dxa"/></w:tcPr>'.p('B.R.N. ENTERPRISE CO., LTD.','left',true,29,0,0).p('1011 Supalai Grand Tower, 7th Floor, Unit 6-7, Rama 3 Road, Bangkok 10120','left',false,18,0,0).'</w:tc><w:tc><w:tcPr><w:tcW w:w="3000" w:type="dxa"/><w:shd w:fill="0798D1"/><w:vAlign w:val="center"/></w:tcPr>'.p($label,'center',false,29,0,0).'</w:tc></w:tr></w:tbl>';
}
function meta(string $doc, string $title): string {
return tbl([
['Document No',$doc,'Release, Version','05/01/26 V1.0 Final'],
['Project Name','โครงการพัฒนาระบบบริหารจัดการคลังสินค้า BRN WMS','Project Code','200-WMS-26-001-00'],
['Title',$title,'Project Period','5 มกราคม – 14 สิงหาคม 2569'],
['Prepared by','คุณอภิรัชต์ สุภัทรประทีป (Project Manager)','Approval','Pending authorized signature'],
],[1600,3500,1600,2300],false);
}
function saveDoc(string $path,string $title,string $body,string $code): void {
$document='<?xml version="1.0" encoding="UTF-8" standalone="yes"?><w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"><w:body>'.$body.'<w:sectPr><w:pgSz w:w="11906" w:h="16838"/><w:pgMar w:top="850" w:right="1080" w:bottom="850" w:left="1080" w:header="400" w:footer="400"/><w:footerReference w:type="default" r:id="rId1"/></w:sectPr></w:body></w:document>';
$ct='<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"><Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/><Default Extension="xml" ContentType="application/xml"/><Override PartName="/word/document.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"/><Override PartName="/word/footer1.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.footer+xml"/><Override PartName="/docProps/core.xml" ContentType="application/vnd.openxmlformats-package.core-properties+xml"/></Types>';
$rels='<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="word/document.xml"/><Relationship Id="rId2" Type="http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties" Target="docProps/core.xml"/></Relationships>';
$drels='<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/footer" Target="footer1.xml"/></Relationships>';
$footer='<?xml version="1.0" encoding="UTF-8" standalone="yes"?><w:ftr xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">'.p('ISO/IEC 29110 | 200-WMS-26-001-00 | '.$code.' | V1.0 Final','center',false,18,0,0).'</w:ftr>';
$core='<?xml version="1.0" encoding="UTF-8" standalone="yes"?><cp:coreProperties xmlns:cp="http://schemas.openxmlformats.org/package/2006/metadata/core-properties" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:dcterms="http://purl.org/dc/terms/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"><dc:title>'.e($title).'</dc:title><dc:creator>คุณอภิรัชต์ สุภัทรประทีป (Project Manager)</dc:creator><dc:description>Editable ISO/IEC 29110 controlled final document ready for authorization.</dc:description><dcterms:created xsi:type="dcterms:W3CDTF">2026-01-05T00:00:00+07:00</dcterms:created></cp:coreProperties>';
$z=new ZipArchive();if($z->open($path,ZipArchive::CREATE|ZipArchive::OVERWRITE)!==true)throw new RuntimeException('Cannot create '.$path);
foreach(['[Content_Types].xml'=>$ct,'_rels/.rels'=>$rels,'word/document.xml'=>$document,'word/_rels/document.xml.rels'=>$drels,'word/footer1.xml'=>$footer,'docProps/core.xml'=>$core] as $n=>$v)$z->addFromString($n,$v);$z->close();
}
$plan = top('Software Project Plan').p().meta('Software Project Plan','แผนการดำเนินโครงการ').h('1.','วัตถุประสงค์และภาพรวม')
.p('โครงการ BRN WMS จัดทำระบบเว็บแบบหลายบริษัทและหลายคลัง เพื่อควบคุมข้อมูลหลัก สินค้าคงคลัง เอกสารซื้อขาย การเงินและบัญชี รายงาน และการแจ้งเตือน โดยบริหารงานตามกระบวนการ Project Management และ Software Implementation ของ ISO/IEC 29110 Basic Profile.')
.h('2.','ขอบเขตและผลส่งมอบ').b('ซอฟต์แวร์ BRN WMS และฐานข้อมูลสำหรับ Authentication/Company และ WMS/Accounting').b('โมดูลผู้ใช้ บริษัท ผู้ติดต่อ สินค้า คลัง/ตำแหน่งจัดเก็บ Stock In/Out/Transfer Lot Serial Expiry และ Barcode').b('โมดูล Quotation, Order, Return, Purchase, Invoice, Receipt, Payment, General Ledger และรายงาน').b('Node.js Socket.IO สำหรับแจ้งเตือน และ Scheduler สำหรับ Stock/GL maintenance และ alerts').b('Work Products ISO/IEC 29110 จำนวน 22 รายการและ Project Repository ที่ควบคุมเวอร์ชัน')
.h('3.','แนวทางดำเนินโครงการ').tbl([['Phase','Period','Primary output'],['Initiation and planning','05-Jan–18-Feb-2026','SOW, Customer Requirements, Project Plan'],['Implementation','19-Feb–29-May-2026','Requirements, Design, Components, Tests, Software'],['Verification, validation and documentation','30-May–07-Aug-2026','Test/Verification/Validation results and guides'],['Stabilization and closure','03-Aug–14-Aug-2026','Corrections, demo data, acceptance and repository closure']],[2600,2500,3900])
.h('4.','ทีมงานและความรับผิดชอบ').tbl([['Role','Responsibility'],['Project Sponsor / Customer Representative / Authorized Approver — คุณเสรี วิริยะสกุลธรณ์','Represent customer needs and authorize scope, resources, changes and acceptance.'],['Project Manager — คุณอภิรัชต์ สุภัทรประทีป','Plan, monitor, communicate, control risks/issues and close the project.'],['Developer / System Analyst — ธนกร สถิตวิทยากุล','Analyze, design, implement, configure, correct and maintain technical records aligned with Git evidence.'],['Tester / Reviewer','Prepare and execute tests; verify work products and record results.'],['Document Control','Control identifiers, versions, approvals, repository and evidence.']],[3200,5800])
.h('5.','ทรัพยากรและสภาพแวดล้อม').b('PHP 8+, MySQL/MariaDB, Nginx or compatible web server, Node.js, npm and PM2').b('Git main branch as source baseline; configuration secrets excluded from version control').b('Two logical databases: identity/company data and WMS/accounting operational data').b('Development and deployment in Asia/Bangkok time zone with controlled configuration')
.h('6.','การบริหารความเสี่ยง').tbl([['Risk','Response / Control'],['Reconstructed pre-development evidence','Mark inferred records clearly and obtain retrospective review/approval.'],['Unauthorized access or tenant-data exposure','Role guards, company/warehouse scoping, session controls and security review.'],['Inventory inconsistency','Transactions, validation, locking, approval flow, stock summaries and reconciliation tests.'],['Configuration or secret exposure','Ignored local configuration, templates, deployment guide and restricted web access.'],['Insufficient acceptance evidence','Trace requirements to tests and obtain authorized signatures before baselining.'],['Scope expansion','Record Change Report, assess impact and authorize before implementation.']],[3600,5400])
.h('7.','คุณภาพ การทวนสอบ และการตรวจสอบความใช้ได้').b('Every approved requirement receives a unique ID and forward/backward traceability.').b('Reviews cover requirements, design, code/configuration, test records and operational documentation.').b('Critical failures prevent acceptance; other defects require disposition in the Correction Register.').b('Validation uses representative warehouse, order, finance and role-based workflows with controlled demo data.')
.h('8.','การควบคุมการเปลี่ยนแปลงและการกำหนดค่า').b('Configuration items include source code, database/setup scripts, Node.js services, controlled documents and releases.').b('Changes are requested, analyzed, approved, implemented, tested and closed with linked evidence.').b('Document filenames use project code, work-product name, Buddhist date, version and status/author indicator.')
.h('9.','การสื่อสารและการติดตาม').tbl([['Record','Frequency / trigger','Owner'],['Progress Status Record','Weekly during active work','Project Manager'],['Meeting Record','At project/review/decision meetings','Recorder'],['Correction Register','When an issue or defect is identified','Project Manager / Tester'],['Change Report','When baseline scope/configuration changes','Project Manager'],['Repository review','At baselines and closure','Document Control']],[3400,3300,2300])
.h('10.','เกณฑ์การส่งมอบและปิดโครงการ').b('Scope functions pass their linked test cases and no unresolved critical defect remains.').b('Installation, user, operation and maintenance information is available and reviewed.').b('Software and all required work products are stored in the controlled repository.').b('Verification, validation and acceptance results are reviewed and authorized.').p('Approval signature: ____________________________________','center',false,28,180,20).p('Name/position: _______________________________________','center').p('Date: ________________________________________________','center');
$reqs = [
['FR-001','Identity','Register/onboard company owner and invited users.','Must'],['FR-002','Identity','Authenticate users and apply role-based access for Owner, Admin, Staff and Viewer.','Must'],['FR-003','Identity','Support password recovery, session control and applicable OTP verification.','Must'],['FR-004','Administration','Maintain company profile, SMTP, system settings, users and application access.','Must'],
['FR-005','Master data','Maintain warehouses, storage areas/bins, product categories, products and contacts.','Must'],['FR-006','Master data','Support simple and layered warehouse-location models.','Should'],['FR-007','Inventory','Record stock-in with product, quantity, warehouse/bin and traceability attributes.','Must'],['FR-008','Inventory','Record stock-out with authorization and balance validation.','Must'],['FR-009','Inventory','Transfer stock between controlled warehouse locations.','Must'],['FR-010','Inventory','Track lot, serial number and expiry-date information where applicable.','Must'],['FR-011','Inventory','Display stock overview, movement history, capacity/occupancy and low-stock information.','Must'],['FR-012','Inventory','Generate SKU and location barcode labels and support scanning workflows.','Should'],
['FR-013','Sales','Create and manage quotations, sales orders, invoices, returns and credit notes.','Must'],['FR-014','Purchasing','Create and manage purchase requests, purchase orders, supplier invoices and supplier returns.','Must'],['FR-015','Finance','Create and manage receipt/payment billing, receipts and payments.','Must'],['FR-016','Accounting','Maintain chart of accounts, departments, formulas, journals and GL entries.','Must'],['FR-017','Accounting','Produce trial balance, profit/loss, balance sheet, VAT and GL movement reports.','Must'],['FR-018','Documents','Generate controlled document numbers and manage document lifecycle/status.','Must'],['FR-019','Documents','Attach permitted files to supported records.','Should'],['FR-020','Reports','Filter, view and export operational and management reports.','Must'],
['FR-021','Notifications','Notify authorized users of relevant status transitions and operational alerts.','Should'],['FR-022','Scheduler','Maintain stock/GL summaries and produce low-stock/overdue-invoice alerts.','Should'],['FR-023','Audit','Preserve creator/updater/status and transaction history required for review.','Must'],['FR-024','Isolation','Restrict company and warehouse data to the authorized user context.','Must'],
['NFR-001','Security','Protect secrets from source control and direct web access.','Must'],['NFR-002','Security','Validate input, authorize server-side actions and prevent cross-tenant access.','Must'],['NFR-003','Integrity','Execute related database updates transactionally and prevent invalid negative/duplicate movements.','Must'],['NFR-004','Availability','Provide documented installation, configuration, backup and recovery procedures.','Must'],['NFR-005','Usability','Provide responsive browser UI suitable for desktop and warehouse-floor devices.','Should'],['NFR-006','Performance','Provide practical response times for daily operations and use aggregate tables for dashboards/reports.','Should'],['NFR-007','Maintainability','Use modular managers/APIs, centralized helpers, configuration templates and version control.','Should'],['NFR-008','Compatibility','Run on PHP 8+, MySQL/MariaDB and current standards-based browsers.','Must'],['NFR-009','Traceability','Link each approved requirement to design, component and verification evidence.','Must'],['NFR-010','Time','Use Asia/Bangkok consistently for application and scheduled jobs.','Must'],
];
$cr = top('Customer Requirements').p().meta('Customer Requirements','เอกสารบันทึกและสรุปความต้องการของลูกค้า').h('1.','วัตถุประสงค์').p('เอกสารฉบับนี้กำหนดความต้องการระดับลูกค้าและผู้ใช้ของ BRN WMS เพื่อเป็นฐานสำหรับ Software Requirements Specification, Software Design, Traceability Record, Test Cases และ Acceptance. ข้อมูลถูกรวบรวมย้อนหลังจากขอบเขตที่ตกลง ซอร์สโค้ด ฐานข้อมูล และประวัติ Git โดยธนกร สถิตวิทยากุล ในบทบาท Developer / System Analyst และต้องได้รับการทบทวนและอนุมัติโดยคุณเสรี วิริยะสกุลธรณ์ ในบทบาท Project Sponsor / Customer Representative / Authorized Approver ก่อน Baseline.')
.h('2.','ผู้มีส่วนได้ส่วนเสีย').tbl([['Stakeholder','Interest / role'],['Authorized Sponsor','Approve scope, resources, changes and final acceptance.'],['Warehouse Manager / Staff','Accurate receiving, issuing, transfer, stock status, traceability and reports.'],['Sales and Purchasing Users','Controlled quotation/order/purchase/invoice/return workflows.'],['Finance and Accounting Users','Billing, receipts, payments, journals, GL and financial reports.'],['System Administrator','Company configuration, user access, deployment, monitoring and recovery.'],['Auditor / Management','Traceable transactions, controlled records and management information.']],[3400,5600])
.h('3.','สมมติฐานและข้อจำกัด').b('The solution is a browser-based application deployed on a controlled PHP/MySQL environment.').b('External ERP integration, legacy-data migration, hardware procurement and custom third-party services are outside scope unless changed formally.').b('Customer representatives provide representative data, review requirements and participate in validation and acceptance.').b('Requirements marked Must are acceptance-critical; Should requirements may be accepted with documented disposition.')
.h('4.','รายการความต้องการ');
$reqRows=[['ID','Area','Requirement','Priority']];foreach($reqs as $q)$reqRows[]=$q;$cr.=tbl($reqRows,[1300,1700,5100,900]).h('5.','เกณฑ์การยอมรับความต้องการ').b('Each Must requirement is implemented, traced to one or more test cases and passes verification/validation.').b('No critical security, tenant-isolation, inventory-integrity or transaction defect remains open.').b('Any exception or deferred Should requirement is recorded and authorized.').b('Customer review confirms that representative end-to-end workflows support intended warehouse operations.').h('6.','การอนุมัติ Baseline').p('Customer representative signature: ______________________________','center',false,28,180,20).p('Name/position: ______________________________________________','center').p('Date: ________________________________________________________','center');
saveDoc($base.'2-Software Project Plan/200-WMS-26-001-00 Software Project Plan 25690105 V1.0 Final.docx','200-WMS-26-001-00 Software Project Plan',$plan,'PP');
saveDoc($base.'3-Customer Requirement/200-WMS-26-001-00 Customer Requirements 25690112 V1.0 Final.docx','200-WMS-26-001-00 Customer Requirements',$cr,'CR');
echo "Project Plan documents generated.\n";
require __DIR__ . '/normalize_document_dates.php';
-192
View File
@@ -1,192 +0,0 @@
<?php
declare(strict_types=1);
/**
* Generate the ISO/IEC 29110 Statement of Work for BRN WMS.
*
* The document is generated as native OOXML so it remains editable in Word.
*/
$output = __DIR__ . '/../sdlc/1-PM Process (10 Work Product)/1.Statement of Work/'
. '200-WMS-26-001-00 Statement of Work 25690105 V1.0 Final.docx';
function xml(string $value): string
{
return htmlspecialchars($value, ENT_XML1 | ENT_QUOTES, 'UTF-8');
}
function run(string $text, bool $bold = false, int $size = 30): string
{
$boldXml = $bold ? '<w:b/><w:bCs/>' : '';
return '<w:r><w:rPr>' . $boldXml
. '<w:rFonts w:ascii="TH Sarabun New" w:hAnsi="TH Sarabun New" w:eastAsia="TH Sarabun New" w:cs="TH Sarabun New"/>'
. '<w:sz w:val="' . $size . '"/><w:szCs w:val="' . $size . '"/>'
. '<w:lang w:val="th-TH" w:eastAsia="th-TH" w:bidi="th-TH"/>'
. '</w:rPr><w:t xml:space="preserve">' . xml($text) . '</w:t></w:r>';
}
function para(string $text = '', string $align = 'left', bool $bold = false, int $size = 30, int $before = 0, int $after = 80, int $indent = 0): string
{
return '<w:p><w:pPr><w:jc w:val="' . $align . '"/>'
. '<w:spacing w:before="' . $before . '" w:after="' . $after . '" w:line="360" w:lineRule="auto"/>'
. ($indent > 0 ? '<w:ind w:firstLine="' . $indent . '"/>' : '')
. '</w:pPr>' . run($text, $bold, $size) . '</w:p>';
}
function bullet(string $text): string
{
return '<w:p><w:pPr><w:ind w:left="720" w:hanging="360"/><w:spacing w:after="50" w:line="340" w:lineRule="auto"/></w:pPr>'
. run('• ' . $text, false, 29) . '</w:p>';
}
function tableRow(string $label, string $value): string
{
return '<w:tr>'
. '<w:tc><w:tcPr><w:tcW w:w="2200" w:type="dxa"/><w:shd w:fill="D9EAF7"/></w:tcPr>'
. para($label, 'left', true, 27, 0, 20) . '</w:tc>'
. '<w:tc><w:tcPr><w:tcW w:w="6800" w:type="dxa"/></w:tcPr>'
. para($value, 'left', false, 27, 0, 20) . '</w:tc>'
. '</w:tr>';
}
$header = '<w:tbl><w:tblPr><w:tblW w:w="9000" w:type="dxa"/><w:tblLayout w:type="fixed"/></w:tblPr><w:tblGrid><w:gridCol w:w="6100"/><w:gridCol w:w="2900"/></w:tblGrid>'
. '<w:tr><w:tc><w:tcPr><w:tcW w:w="6100" w:type="dxa"/></w:tcPr>'
. para('B.R.N. ENTERPRISE CO., LTD.', 'left', true, 29, 0, 0)
. para('1011 Supalai Grand Tower, 7th Floor, Unit 6-7, Rama 3 Road,', 'left', false, 19, 0, 0)
. para('Chongnonsi, Yannawa, Bangkok 10120', 'left', false, 19, 0, 0)
. '</w:tc><w:tc><w:tcPr><w:tcW w:w="2900" w:type="dxa"/><w:shd w:fill="0798D1"/><w:vAlign w:val="center"/></w:tcPr>'
. para('Statement of Work', 'center', false, 29, 0, 0)
. '</w:tc></w:tr></w:tbl>';
$meta = '<w:tbl><w:tblPr><w:tblW w:w="9000" w:type="dxa"/><w:tblBorders>'
. '<w:top w:val="single" w:sz="4" w:color="9E9E9E"/><w:left w:val="single" w:sz="4" w:color="9E9E9E"/>'
. '<w:bottom w:val="single" w:sz="4" w:color="9E9E9E"/><w:right w:val="single" w:sz="4" w:color="9E9E9E"/>'
. '<w:insideH w:val="single" w:sz="4" w:color="9E9E9E"/><w:insideV w:val="single" w:sz="4" w:color="9E9E9E"/>'
. '</w:tblBorders></w:tblPr><w:tblGrid><w:gridCol w:w="2200"/><w:gridCol w:w="6800"/></w:tblGrid>'
. tableRow('รหัสโครงการ', '200-WMS-26-001-00')
. tableRow('ชื่อโครงการ', 'โครงการพัฒนาระบบบริหารจัดการคลังสินค้า BRN WMS')
. tableRow('ระยะเวลาโครงการ', '5 มกราคม 2569 – 14 สิงหาคม 2569')
. tableRow('เอกสาร', 'Statement of Work · Version 1.0 · สถานะ Final')
. '</w:tbl>';
$body = $header
. para('', 'left', false, 10, 0, 80)
. para('วันที่ 5 มกราคม 2569', 'center', false, 30, 80, 180)
. para('เรื่อง ขอบเขตการดำเนินงานโครงการพัฒนาระบบบริหารจัดการคลังสินค้า BRN WMS', 'left', true, 30, 0, 100)
. para('เรียน ผู้บริหารและผู้มีส่วนเกี่ยวข้องในโครงการ', 'left', true, 30, 0, 180)
. para('บริษัท บี.อาร์.เอ็น. เอ็นเตอร์ไพรส์ จำกัด มีความประสงค์ดำเนินโครงการพัฒนาระบบบริหารจัดการคลังสินค้า BRN WMS เพื่อเพิ่มความถูกต้องและความรวดเร็วของงานคลังสินค้า ทำให้สามารถติดตามสินค้าคงคลัง การเคลื่อนไหวสินค้า คำสั่งซื้อ เอกสารทางธุรกิจ และข้อมูลบัญชีที่เกี่ยวข้องได้อย่างเป็นระบบ โดยใช้ข้อมูลแบบรวมศูนย์และกำหนดสิทธิ์การเข้าถึงตามบทบาทผู้ใช้งาน', 'both', false, 30, 0, 120, 720)
. para('เอกสารฉบับนี้กำหนดขอบเขต ผลส่งมอบ เกณฑ์การยอมรับ หน้าที่ความรับผิดชอบ และกรอบระยะเวลาของโครงการตามแนวทาง ISO/IEC 29110 โดยมีรายละเอียดดังต่อไปนี้', 'both', false, 30, 0, 180, 720)
. $meta
. para('1. วัตถุประสงค์', 'left', true, 32, 180, 80)
. bullet('พัฒนาระบบเว็บสำหรับควบคุมสินค้าคงคลังและการปฏิบัติงานคลังสินค้าแบบหลายคลัง')
. bullet('สนับสนุนการรับเข้า เบิกจ่าย โอนย้าย ปรับปรุง และตรวจสอบยอดคงเหลือ พร้อมการติดตาม Lot, Serial Number และวันหมดอายุ')
. bullet('ลดความผิดพลาดจากการทำงานด้วยมือและเพิ่มความสามารถในการตรวจสอบย้อนหลัง')
. bullet('สนับสนุนการบริหารคำสั่งซื้อ จัดซื้อ คืนสินค้า ใบแจ้งหนี้ รายงาน และรายการบัญชีที่เกี่ยวข้อง')
. bullet('จัดให้มีการรักษาความมั่นคงปลอดภัย การแยกข้อมูลรายบริษัท การกำหนดสิทธิ์ และการแจ้งเตือนแบบเรียลไทม์')
. para('2. ขอบเขตงาน', 'left', true, 32, 150, 80)
. para('2.1 งานวิเคราะห์และออกแบบ', 'left', true, 29, 0, 50)
. bullet('รวบรวมและวิเคราะห์ความต้องการของผู้ใช้ กำหนดกระบวนการทำงาน ข้อมูล และกฎทางธุรกิจ')
. bullet('ออกแบบสถาปัตยกรรมระบบ ฐานข้อมูล ส่วนติดต่อผู้ใช้ การเชื่อมต่อบริการ และมาตรการความมั่นคงปลอดภัย')
. para('2.2 งานพัฒนาระบบ', 'left', true, 29, 80, 50)
. bullet('ข้อมูลหลัก: บริษัท ผู้ใช้ ผู้ติดต่อ สินค้า คลังสินค้า โซน ทางเดิน ตำแหน่งจัดเก็บ และหน่วยนับ')
. bullet('สินค้าคงคลัง: รับเข้า จ่ายออก โอนย้าย ปรับปรุงยอด ตรวจนับ ยอดคงเหลือ และประวัติการเคลื่อนไหว')
. bullet('เอกสารธุรกิจ: Sales Order, Purchase Order, Return, Invoice และลำดับเลขที่เอกสาร')
. bullet('การเงินและบัญชี: รายรับ รายจ่าย สมุดรายวัน รายการบัญชี และรายงานที่ระบบรองรับ')
. bullet('Dashboard, รายงาน, การส่งออกข้อมูล, Barcode/Label และการแนบไฟล์')
. bullet('การยืนยันตัวตน การกำหนดบทบาท Owner/Admin/Staff/Viewer การจำกัดข้อมูลตามบริษัท และการควบคุม session')
. bullet('บริการแจ้งเตือนแบบเรียลไทม์และงานตามกำหนดเวลาด้วย Node.js/Socket.IO')
. para('2.3 งานทดสอบและส่งมอบ', 'left', true, 29, 80, 50)
. bullet('จัดทำและดำเนินการทดสอบตามความต้องการ บันทึกผล แก้ไขข้อบกพร่อง และทดสอบยืนยันผล')
. bullet('จัดเตรียมคู่มือผู้ใช้ คู่มือการติดตั้ง/ปฏิบัติการ และเอกสารบำรุงรักษา')
. bullet('จัดเตรียมหลักฐานการทวนสอบ การตรวจสอบความใช้ได้ และการยอมรับระบบ')
. para('3. ผลส่งมอบ', 'left', true, 32, 150, 80)
. para('ผลส่งมอบประกอบด้วยซอฟต์แวร์ BRN WMS ซอร์สโค้ด สคริปต์การติดตั้งและฐานข้อมูล การกำหนดค่า คู่มือ และ Work Products ของกระบวนการ Project Management และ Software Implementation ตาม ISO/IEC 29110 รวม 22 รายการ โดยจัดเก็บใน Project Repository ภายใต้การควบคุมเวอร์ชัน', 'both', false, 30, 0, 100, 720)
. para('4. ข้อยกเว้นและข้อสมมติ', 'left', true, 32, 120, 80)
. bullet('ไม่รวมการจัดหาเครื่องแม่ข่าย อุปกรณ์เครือข่าย เครื่องสแกน Barcode หรือบริการจากบุคคลภายนอก เว้นแต่ได้รับอนุมัติเพิ่มเติม')
. bullet('การย้ายข้อมูลเดิม การเชื่อมต่อ ERP/บริการภายนอก และการปรับแต่งนอกขอบเขตต้องผ่านกระบวนการ Change Request')
. bullet('ผู้มีส่วนเกี่ยวข้องต้องให้ข้อมูล ทบทวนเอกสาร และเข้าร่วมการทดสอบ/ยอมรับตามกำหนด')
. para('5. แผนงานและจุดควบคุม', 'left', true, 32, 150, 80)
. bullet('เริ่มต้นและวางแผนโครงการ: 5 มกราคม – 18 กุมภาพันธ์ 2569')
. bullet('พัฒนาและทดสอบภายใน: 19 กุมภาพันธ์ – 29 พฤษภาคม 2569')
. bullet('ทดสอบการยอมรับ จัดทำเอกสาร ส่งมอบ และปรับเสถียรภาพ: 30 พฤษภาคม – 14 สิงหาคม 2569')
. bullet('วันเสร็จสิ้นโครงการอย่างเป็นทางการ: 14 สิงหาคม 2569')
. para('6. เกณฑ์การยอมรับ', 'left', true, 32, 150, 80)
. bullet('ฟังก์ชันที่อยู่ในขอบเขตผ่าน Test Cases และเชื่อมโยงกับความต้องการใน Traceability Record')
. bullet('ข้อบกพร่องระดับร้ายแรงที่ขัดขวางการใช้งานได้รับการแก้ไขหรือมีแนวทางที่ผู้มีอำนาจยอมรับ')
. bullet('เอกสารการติดตั้ง การใช้งาน การปฏิบัติการ และการบำรุงรักษาพร้อมใช้งาน')
. bullet('ผลการ Verification, Validation และ Acceptance ได้รับการทบทวนและลงนามโดยผู้มีอำนาจ')
. para('7. การบริหารการเปลี่ยนแปลง', 'left', true, 32, 150, 80)
. para('การเปลี่ยนแปลงขอบเขต กำหนดการ หรือผลส่งมอบต้องบันทึกใน Change Report ประเมินผลกระทบ และได้รับอนุมัติก่อนดำเนินการ การแก้ไขข้อบกพร่องให้บันทึกใน Correction Register และเชื่อมโยงกับหลักฐานการทดสอบที่เกี่ยวข้อง', 'both', false, 30, 0, 120, 720)
. para('จึงจัดทำ Statement of Work ฉบับนี้เพื่อใช้เป็นกรอบดำเนินงานและขอให้ผู้เกี่ยวข้องพิจารณาอนุมัติตามอำนาจหน้าที่', 'both', false, 30, 80, 240, 720)
. para('ลงชื่อ ____________________________________', 'center', false, 30, 240, 20)
. para('(คุณเสรี วิริยะสกุลธรณ์)', 'center', false, 30, 0, 20)
. para('กรรมการผู้จัดการ · Project Sponsor / Customer Representative / Authorized Approver', 'center', false, 26, 0, 20)
. para('บริษัท บี.อาร์.เอ็น. เอ็นเตอร์ไพรส์ จำกัด', 'center', false, 30, 0, 20)
. para('วันที่ ____________________________________', 'center', false, 30, 0, 80);
$document = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
. '<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">'
. '<w:body>' . $body
. '<w:sectPr><w:pgSz w:w="11906" w:h="16838"/><w:pgMar w:top="1080" w:right="1080" w:bottom="1080" w:left="1260" w:header="500" w:footer="500"/>'
. '<w:footerReference w:type="default" r:id="rId1"/></w:sectPr></w:body></w:document>';
$contentTypes = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
. '<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">'
. '<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>'
. '<Default Extension="xml" ContentType="application/xml"/>'
. '<Override PartName="/word/document.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"/>'
. '<Override PartName="/word/footer1.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.footer+xml"/>'
. '<Override PartName="/docProps/core.xml" ContentType="application/vnd.openxmlformats-package.core-properties+xml"/>'
. '<Override PartName="/docProps/app.xml" ContentType="application/vnd.openxmlformats-officedocument.extended-properties+xml"/>'
. '</Types>';
$rels = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
. '<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">'
. '<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="word/document.xml"/>'
. '<Relationship Id="rId2" Type="http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties" Target="docProps/core.xml"/>'
. '<Relationship Id="rId3" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties" Target="docProps/app.xml"/>'
. '</Relationships>';
$documentRels = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
. '<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">'
. '<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/footer" Target="footer1.xml"/>'
. '</Relationships>';
$footer = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
. '<w:ftr xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">'
. '<w:p><w:pPr><w:jc w:val="center"/></w:pPr>'
. run('200-WMS-26-001-00 · Statement of Work · V1.0 · Final', false, 18)
. '</w:p></w:ftr>';
$core = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
. '<cp:coreProperties xmlns:cp="http://schemas.openxmlformats.org/package/2006/metadata/core-properties" '
. 'xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:dcterms="http://purl.org/dc/terms/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">'
. '<dc:title>200-WMS-26-001-00 Statement of Work</dc:title><dc:subject>ISO/IEC 29110 Project Management Work Product</dc:subject>'
. '<dc:creator>BRN WMS Project Team</dc:creator><cp:keywords>ISO/IEC 29110; Statement of Work; BRN WMS</cp:keywords>'
. '<dc:description>Editable final document ready for authorized approval and signature.</dc:description>'
. '<dcterms:created xsi:type="dcterms:W3CDTF">2026-01-05T00:00:00+07:00</dcterms:created>'
. '<dcterms:modified xsi:type="dcterms:W3CDTF">2026-08-17T00:00:00+07:00</dcterms:modified>'
. '</cp:coreProperties>';
$app = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
. '<Properties xmlns="http://schemas.openxmlformats.org/officeDocument/2006/extended-properties" '
. 'xmlns:vt="http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes">'
. '<Application>Microsoft Office Word</Application><Company>B.R.N. Enterprise Co., Ltd.</Company><AppVersion>16.0000</AppVersion>'
. '</Properties>';
$zip = new ZipArchive();
if ($zip->open($output, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== true) {
throw new RuntimeException('Unable to create ' . $output);
}
$zip->addFromString('[Content_Types].xml', $contentTypes);
$zip->addFromString('_rels/.rels', $rels);
$zip->addFromString('word/document.xml', $document);
$zip->addFromString('word/_rels/document.xml.rels', $documentRels);
$zip->addFromString('word/footer1.xml', $footer);
$zip->addFromString('docProps/core.xml', $core);
$zip->addFromString('docProps/app.xml', $app);
$zip->close();
echo $output . PHP_EOL;
require __DIR__ . '/normalize_document_dates.php';
-148
View File
@@ -1,148 +0,0 @@
<?php
declare(strict_types=1);
$output = __DIR__ . '/../sdlc/1-PM Process (10 Work Product)/2.Project Plan/1-Work Schedule/'
. '200-WMS-26-001-00 Work Schedule 25690105 V1.0 Final.xlsx';
$headers = ['No.', 'Phase', 'Task', 'Details / Basis', 'Responsible Role', 'Start', 'Finish', 'Duration (days)', 'Deliverable / Evidence', 'Status', 'Remarks'];
$rows = [
['1.1', 'Initiation', 'Identify project need', 'Establish need for centralized warehouse, inventory, order, and accounting control.', 'Project Sponsor / Project Manager', '05-Jan-2026', '09-Jan-2026', '5', 'Statement of Work', 'Completed', 'Reconstructed planning activity'],
['1.2', 'Initiation', 'Identify stakeholders and objectives', 'Identify sponsor, operational users, system administrator, development, and approval roles.', 'Project Manager', '05-Jan-2026', '16-Jan-2026', '10', 'Stakeholder and objective records', 'Completed', 'Reconstructed planning activity'],
['1.3', 'Initiation', 'Approve project scope', 'Confirm project boundaries, assumptions, deliverables, and acceptance approach.', 'Project Sponsor', '19-Jan-2026', '23-Jan-2026', '5', 'Approved Statement of Work', 'Pending signature', 'Authority signature required'],
['2.1', 'Planning', 'Collect customer requirements', 'Document functional, data, security, operational, and quality requirements.', 'System Analyst / Customer Representatives', '12-Jan-2026', '06-Feb-2026', '20', 'Customer Requirements', 'Completed', 'Reconstructed from implemented system'],
['2.2', 'Planning', 'Prepare Software Project Plan', 'Define lifecycle, resources, risks, repository, configuration, communication, and controls.', 'Project Manager', '26-Jan-2026', '13-Feb-2026', '15', 'Software Project Plan', 'Completed', 'Reconstructed project plan'],
['2.3', 'Planning', 'Baseline requirements and schedule', 'Review initial requirements, priorities, milestones, and work-product responsibilities.', 'Project Manager / System Analyst', '16-Feb-2026', '18-Feb-2026', '3', 'Baseline plan and requirements', 'Completed', 'Development begins 19-Feb-2026'],
['3.1', 'Execution', 'Initialize WMS application', 'Create initial PHP application repository and baseline structure.', 'System Analyst / Developer', '19-Feb-2026', '25-Feb-2026', '5', 'Git commits 9a50080–1843308', 'Completed', 'Git evidenced'],
['3.2', 'Execution', 'Security and database foundation', 'Configuration protection, security audit actions, stock database design.', 'Developer', '09-Mar-2026', '17-Mar-2026', '7', 'Git commits 93d903c–a4f474b', 'Completed', 'Git evidenced'],
['3.3', 'Execution', 'Inventory and warehouse modules', 'Warehouse capacity, product, storage/bin, lot, serial, expiry, stock movements, and reports.', 'Developer', '09-Apr-2026', '29-Apr-2026', '15', 'Inventory, ICS, dashboard and report modules', 'Completed', 'Git evidenced'],
['3.4', 'Execution', 'Authentication and onboarding', 'Login, registration, onboarding, password controls, and role-based access.', 'Developer', '28-Apr-2026', '12-May-2026', '11', 'Authentication and user-management modules', 'Completed', 'Git evidenced'],
['3.5', 'Execution', 'Order and barcode workflows', 'Orders, returns, invoices, switchable warehouse layers, barcode labels, and scanning.', 'Developer', '02-May-2026', '08-May-2026', '5', 'Order and barcode modules', 'Completed', 'Git evidenced'],
['3.6', 'Execution', 'Production preparation and setup', 'Dynamic base URL, automated database setup, recovery flow, and production preparation.', 'Developer', '11-May-2026', '13-May-2026', '3', 'Setup and configuration implementation', 'Completed', 'Git evidenced'],
['3.7', 'Execution', 'Accounting and finance workflows', 'Chart of accounts, GL, journals, reports, billing, payment and receipt workflows.', 'Developer', '13-May-2026', '23-May-2026', '9', 'Accounting and finance modules', 'Completed', 'Git evidenced'],
['3.8', 'Execution', 'Real-time services and scheduled jobs', 'Socket notifications, stock/GL aggregates, low-stock and overdue-invoice schedules.', 'Developer', '22-May-2026', '27-May-2026', '4', 'Node.js service and cron jobs', 'Completed', 'Git evidenced'],
['3.9', 'Execution', 'Security hardening and lifecycle review', 'Role guards, tenant scoping, document flows, transaction limits, and correction closure.', 'Developer / Reviewer', '21-May-2026', '28-May-2026', '6', 'Security and lifecycle review commits', 'Completed', 'Git evidenced'],
['3.10', 'Execution', 'Refactor and development baseline', 'Remove redundancy and establish substantially complete development baseline.', 'Developer', '29-May-2026', '29-May-2026', '1', 'Git commit a0677d6', 'Completed', 'Development substantially complete'],
['4.1', 'Control', 'Maintain progress and meeting records', 'Track progress, issues, decisions, risks, and corrective actions throughout the project.', 'Project Manager / Document Control', '05-Jan-2026', '14-Aug-2026', '160', 'Progress Status, Meeting Records, Correction Register', 'Completed', 'Records require evidence review'],
['4.2', 'Control', 'Configuration and repository control', 'Control source, baselines, document versions, configuration, and backups.', 'Configuration Manager', '19-Feb-2026', '14-Aug-2026', '127', 'Git repository and Software Configuration', 'Completed', 'Git repository evidenced'],
['4.3', 'Verification', 'Verify requirements, design, and implementation', 'Conduct reviews and tests; link requirements, design, code, and test evidence.', 'Reviewer / Tester', '30-May-2026', '31-Jul-2026', '45', 'Verification Results and Traceability Record', 'Completed', 'Formal approval pending'],
['4.4', 'Validation', 'Customer-oriented system validation', 'Validate operational workflows and quality attributes against intended use.', 'Customer Representatives / Tester', '01-Jun-2026', '07-Aug-2026', '50', 'Validation Results and Test Report', 'Completed', 'Formal approval pending'],
['4.5', 'Documentation', 'Prepare operational documentation', 'Prepare user, operation, configuration, deployment, and maintenance guidance.', 'Developer / Document Control', '01-Jun-2026', '07-Aug-2026', '50', 'User Documentation, Operation Guide, Maintenance Documentation', 'Completed', 'Reconstructed documentation period'],
['4.6', 'Stabilization', 'Configuration and login corrections', 'Correct login and environment configuration issues identified after baseline.', 'Developer', '03-Aug-2026', '03-Aug-2026', '1', 'Git commit b2c4374', 'Completed', 'Git evidenced'],
['4.7', 'Stabilization', 'Prepare demonstration data', 'Populate controlled demonstration data for validation and handover support.', 'Developer / Tester', '14-Aug-2026', '14-Aug-2026', '1', 'Git commit dd48a8b', 'Completed', 'Git evidenced'],
['5.1', 'Closure', 'Final repository and work-product review', 'Confirm required work products, traceability, configuration items, and unresolved actions.', 'Project Manager / Document Control', '10-Aug-2026', '14-Aug-2026', '5', 'Repository review and List of Evidence', 'Completed', 'Approval records pending'],
['5.2', 'Closure', 'Acceptance and project closure', 'Obtain authorized acceptance and record project closure and follow-up actions.', 'Project Sponsor / Project Manager', '14-Aug-2026', '14-Aug-2026', '1', 'Acceptance Report and closure record', 'Pending signature', 'Authority signature required'],
];
function esc(string $value): string
{
return htmlspecialchars($value, ENT_XML1 | ENT_QUOTES, 'UTF-8');
}
function colName(int $number): string
{
$name = '';
while ($number > 0) {
$number--;
$name = chr(65 + ($number % 26)) . $name;
$number = intdiv($number, 26);
}
return $name;
}
function cell(string $reference, string $value, int $style): string
{
return '<c r="' . $reference . '" t="inlineStr" s="' . $style . '"><is><t xml:space="preserve">' . esc($value) . '</t></is></c>';
}
$sheetRows = [];
$sheetRows[] = '<row r="1" ht="30" customHeight="1">'
. cell('A1', 'Document No: Work Schedule', 1)
. cell('B1', 'Project: BRN WMS', 1)
. cell('C1', 'Project Code: 200-WMS-26-001-00', 1)
. cell('D1', 'Period: 05-Jan-2026 to 14-Aug-2026', 1)
. cell('E1', 'Release: 05/01/26 V1.0 Final', 1)
. '</row>';
$sheetRows[] = '<row r="2" ht="26" customHeight="1">';
foreach ($headers as $index => $header) {
$sheetRows[1] .= cell(colName($index + 1) . '2', $header, 2);
}
$sheetRows[1] .= '</row>';
foreach ($rows as $rowIndex => $row) {
$excelRow = $rowIndex + 3;
$xml = '<row r="' . $excelRow . '" ht="42" customHeight="1">';
foreach ($row as $columnIndex => $value) {
$style = $columnIndex === 9
? ($value === 'Completed' ? 4 : 5)
: ($columnIndex === 0 || $columnIndex >= 5 && $columnIndex <= 7 ? 3 : 6);
$xml .= cell(colName($columnIndex + 1) . $excelRow, $value, $style);
}
$sheetRows[] = $xml . '</row>';
}
$lastRow = count($rows) + 2;
$sheet = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
. '<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">'
. '<sheetPr><pageSetUpPr fitToPage="1"/></sheetPr><dimension ref="A1:K' . $lastRow . '"/>'
. '<sheetViews><sheetView workbookViewId="0"><pane ySplit="2" topLeftCell="A3" activePane="bottomLeft" state="frozen"/></sheetView></sheetViews>'
. '<cols><col min="1" max="1" width="7" customWidth="1"/><col min="2" max="2" width="14" customWidth="1"/>'
. '<col min="3" max="3" width="29" customWidth="1"/><col min="4" max="4" width="48" customWidth="1"/>'
. '<col min="5" max="5" width="28" customWidth="1"/><col min="6" max="7" width="14" customWidth="1"/>'
. '<col min="8" max="8" width="14" customWidth="1"/><col min="9" max="9" width="34" customWidth="1"/>'
. '<col min="10" max="10" width="18" customWidth="1"/><col min="11" max="11" width="28" customWidth="1"/></cols>'
. '<sheetData>' . implode('', $sheetRows) . '</sheetData>'
. '<autoFilter ref="A2:K' . $lastRow . '"/><mergeCells count="5"><mergeCell ref="A1:A1"/><mergeCell ref="B1:B1"/><mergeCell ref="C1:C1"/><mergeCell ref="D1:D1"/><mergeCell ref="E1:K1"/></mergeCells>'
. '<pageMargins left="0.25" right="0.25" top="0.4" bottom="0.4" header="0.2" footer="0.2"/>'
. '<pageSetup orientation="landscape" paperSize="9" fitToWidth="1" fitToHeight="0"/>'
. '<printOptions horizontalCentered="1"/><headerFooter><oddFooter>&amp;LISO/IEC 29110&amp;C200-WMS-26-001-00&amp;RPage &amp;P of &amp;N</oddFooter></headerFooter>'
. '</worksheet>';
$styles = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
. '<styleSheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">'
. '<fonts count="4"><font><sz val="10"/><name val="Arial"/></font><font><b/><color rgb="FFFFFFFF"/><sz val="11"/><name val="Arial"/></font>'
. '<font><b/><color rgb="FFFFFFFF"/><sz val="10"/><name val="Arial"/></font><font><sz val="9"/><name val="Arial"/></font></fonts>'
. '<fills count="5"><fill><patternFill patternType="none"/></fill><fill><patternFill patternType="gray125"/></fill>'
. '<fill><patternFill patternType="solid"><fgColor rgb="FF075985"/></patternFill></fill>'
. '<fill><patternFill patternType="solid"><fgColor rgb="FF0E7490"/></patternFill></fill>'
. '<fill><patternFill patternType="solid"><fgColor rgb="FFFEF3C7"/></patternFill></fill></fills>'
. '<borders count="2"><border/><border><left style="thin"><color rgb="FFB7C9D6"/></left><right style="thin"><color rgb="FFB7C9D6"/></right><top style="thin"><color rgb="FFB7C9D6"/></top><bottom style="thin"><color rgb="FFB7C9D6"/></bottom></border></borders>'
. '<cellStyleXfs count="1"><xf numFmtId="0" fontId="0" fillId="0" borderId="0"/></cellStyleXfs>'
. '<cellXfs count="7"><xf numFmtId="0" fontId="0" fillId="0" borderId="0" xfId="0"/>'
. '<xf numFmtId="0" fontId="1" fillId="2" borderId="1" xfId="0" applyAlignment="1"><alignment vertical="center" wrapText="1"/></xf>'
. '<xf numFmtId="0" fontId="2" fillId="2" borderId="1" xfId="0" applyAlignment="1"><alignment horizontal="center" vertical="center" wrapText="1"/></xf>'
. '<xf numFmtId="0" fontId="3" fillId="0" borderId="1" xfId="0" applyAlignment="1"><alignment horizontal="center" vertical="top" wrapText="1"/></xf>'
. '<xf numFmtId="0" fontId="3" fillId="3" borderId="1" xfId="0" applyAlignment="1"><alignment horizontal="center" vertical="top" wrapText="1"/></xf>'
. '<xf numFmtId="0" fontId="3" fillId="4" borderId="1" xfId="0" applyAlignment="1"><alignment horizontal="center" vertical="top" wrapText="1"/></xf>'
. '<xf numFmtId="0" fontId="3" fillId="0" borderId="1" xfId="0" applyAlignment="1"><alignment vertical="top" wrapText="1"/></xf></cellXfs>'
. '<cellStyles count="1"><cellStyle name="Normal" xfId="0" builtinId="0"/></cellStyles></styleSheet>';
$contentTypes = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">'
. '<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/><Default Extension="xml" ContentType="application/xml"/>'
. '<Override PartName="/xl/workbook.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"/>'
. '<Override PartName="/xl/worksheets/sheet1.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/>'
. '<Override PartName="/xl/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml"/>'
. '<Override PartName="/docProps/core.xml" ContentType="application/vnd.openxmlformats-package.core-properties+xml"/>'
. '<Override PartName="/docProps/app.xml" ContentType="application/vnd.openxmlformats-officedocument.extended-properties+xml"/></Types>';
$rootRels = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">'
. '<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="xl/workbook.xml"/>'
. '<Relationship Id="rId2" Type="http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties" Target="docProps/core.xml"/>'
. '<Relationship Id="rId3" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties" Target="docProps/app.xml"/></Relationships>';
$workbook = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?><workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">'
. '<sheets><sheet name="Work Schedule" sheetId="1" r:id="rId1"/></sheets><definedNames><definedName name="_xlnm.Print_Area" localSheetId="0">\'Work Schedule\'!$A$1:$K$' . $lastRow . '</definedName></definedNames></workbook>';
$workbookRels = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">'
. '<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="worksheets/sheet1.xml"/>'
. '<Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" Target="styles.xml"/></Relationships>';
$core = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?><cp:coreProperties xmlns:cp="http://schemas.openxmlformats.org/package/2006/metadata/core-properties" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:dcterms="http://purl.org/dc/terms/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"><dc:title>200-WMS-26-001-00 Work Schedule</dc:title><dc:creator>BRN WMS Project Team</dc:creator><dc:description>ISO/IEC 29110 reconstructed final project schedule ready for authorization.</dc:description><dcterms:created xsi:type="dcterms:W3CDTF">2026-01-05T00:00:00+07:00</dcterms:created></cp:coreProperties>';
$app = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Properties xmlns="http://schemas.openxmlformats.org/officeDocument/2006/extended-properties"><Application>Microsoft Excel</Application><Company>B.R.N. Enterprise Co., Ltd.</Company></Properties>';
$zip = new ZipArchive();
if ($zip->open($output, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== true) {
throw new RuntimeException('Cannot create ' . $output);
}
foreach (['[Content_Types].xml' => $contentTypes, '_rels/.rels' => $rootRels, 'xl/workbook.xml' => $workbook, 'xl/_rels/workbook.xml.rels' => $workbookRels, 'xl/worksheets/sheet1.xml' => $sheet, 'xl/styles.xml' => $styles, 'docProps/core.xml' => $core, 'docProps/app.xml' => $app] as $name => $contents) {
$zip->addFromString($name, $contents);
}
$zip->close();
echo $output . PHP_EOL;
require __DIR__ . '/normalize_document_dates.php';
-99
View File
@@ -1,99 +0,0 @@
<?php
declare(strict_types=1);
$root = __DIR__ . '/../sdlc';
$english = [
'January'=>'01','February'=>'02','March'=>'03','April'=>'04','May'=>'05','June'=>'06',
'July'=>'07','August'=>'08','September'=>'09','October'=>'10','November'=>'11','December'=>'12',
'Jan'=>'01','Feb'=>'02','Mar'=>'03','Apr'=>'04','Jun'=>'06','Jul'=>'07','Aug'=>'08','Sep'=>'09','Oct'=>'10','Nov'=>'11','Dec'=>'12',
];
$thai = ['มกราคม'=>'01','กุมภาพันธ์'=>'02','มีนาคม'=>'03','เมษายน'=>'04','พฤษภาคม'=>'05','มิถุนายน'=>'06','กรกฎาคม'=>'07','สิงหาคม'=>'08','กันยายน'=>'09','ตุลาคม'=>'10','พฤศจิกายน'=>'11','ธันวาคม'=>'12'];
function normalDate(int|string $day, string $month, int|string $year, array $english, array $thai): string
{
$monthNo = $english[$month] ?? $thai[$month] ?? null;
if ($monthNo === null) return (string)$day . ' ' . $month . ' ' . (string)$year;
$yearNo = (int)$year;
if ($yearNo >= 2400) $yearNo -= 543;
return sprintf('%02d/%s/%02d', (int)$day, $monthNo, $yearNo % 100);
}
function normalizeDates(string $text, array $english, array $thai): string
{
$months = implode('|', array_map(static fn($v) => preg_quote($v, '/'), array_keys($english + $thai)));
// Compact Buddhist document/report dates, e.g. 25690817.
$text = preg_replace_callback('/\b(25\d{2})(\d{2})(\d{2})\b/', static function ($m) {
return sprintf('%02d/%02d/%02d', (int)$m[3], (int)$m[2], ((int)$m[1] - 543) % 100);
}, $text) ?? $text;
// Shared-month range: 11–13 May 2026.
$text = preg_replace_callback('/\b(\d{1,2})\s*[–-]\s*(\d{1,2})\s+('.$months.')\s+(20\d{2}|25\d{2})\b/u', static function ($m) use ($english,$thai) {
return normalDate($m[1],$m[3],$m[4],$english,$thai).'–'.normalDate($m[2],$m[3],$m[4],$english,$thai);
}, $text) ?? $text;
// Two-month range with a shared year: 5 January–14 August 2026.
$text = preg_replace_callback('/\b(\d{1,2})\s+('.$months.')\s*[–-]\s*(\d{1,2})\s+('.$months.')\s+(20\d{2}|25\d{2})\b/u', static function ($m) use ($english,$thai) {
return normalDate($m[1],$m[2],$m[5],$english,$thai).'–'.normalDate($m[3],$m[4],$m[5],$english,$thai);
}, $text) ?? $text;
// Hyphenated two-month range: 19-Feb–29-May-2026.
$text = preg_replace_callback('/\b(\d{1,2})-('.$months.')\s*[–-]\s*(\d{1,2})-('.$months.')-(20\d{2}|25\d{2})\b/u', static function ($m) use ($english,$thai) {
return normalDate($m[1],$m[2],$m[5],$english,$thai).'–'.normalDate($m[3],$m[4],$m[5],$english,$thai);
}, $text) ?? $text;
// A full single date using a month name or abbreviation.
$text = preg_replace_callback('/\b(\d{1,2})(?:-|\s+)('.$months.')(?:-|\s+)(20\d{2}|25\d{2})\b/u', static function ($m) use ($english,$thai) {
return normalDate($m[1],$m[2],$m[3],$english,$thai);
}, $text) ?? $text;
// Period descriptions without a repeated year, all known to be in 2026.
$text = preg_replace_callback('/\b(\d{1,2})\s*[–-]\s*(\d{1,2})\s+('.$months.')(?=\s+(?:commits|accounting|May|April|January|February|March|June|July|August))/u', static function ($m) use ($english,$thai) {
return normalDate($m[1],$m[3],2026,$english,$thai).'–'.normalDate($m[2],$m[3],2026,$english,$thai);
}, $text) ?? $text;
// Commit-period shorthand: 30-Apr through 08-May.
$text = preg_replace_callback('/\b(\d{1,2})-('.$months.')\s+through\s+(\d{1,2})-('.$months.')\b/u', static function ($m) use ($english,$thai) {
return normalDate($m[1],$m[2],2026,$english,$thai).'–'.normalDate($m[3],$m[4],2026,$english,$thai);
}, $text) ?? $text;
return $text;
}
$iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($root, FilesystemIterator::SKIP_DOTS));
$updated = 0;
foreach ($iterator as $file) {
if (!$file->isFile() || !str_starts_with($file->getFilename(), '200-WMS-')) continue;
$path = $file->getPathname();
$extension = strtolower($file->getExtension());
if ($extension === 'md') {
$before = file_get_contents($path);
if ($before === false) continue;
$after = normalizeDates($before, $english, $thai);
if ($after !== $before) {
file_put_contents($path, $after);
$updated++;
}
continue;
}
if (!in_array($extension, ['docx','xlsx'], true)) continue;
$zip = new ZipArchive();
if ($zip->open($path) !== true) continue;
$changed = false;
for ($i=0; $i<$zip->numFiles; $i++) {
$name = $zip->getNameIndex($i);
if (!str_ends_with($name, '.xml')) continue;
$before = $zip->getFromIndex($i);
$after = normalizeDates($before, $english, $thai);
if ($after === $before) continue;
$dom = new DOMDocument();
if (!$dom->loadXML($after)) throw new RuntimeException("Invalid XML after date conversion: {$path}::{$name}");
$zip->addFromString($name, $after);
$changed = true;
}
$zip->close();
if ($changed) $updated++;
}
echo "Updated {$updated} BRN WMS document(s).\n";
@@ -1,7 +0,0 @@
<table class="document-control">
<tbody>
<tr><th>Document</th><td>{{documentType}}</td><th>Release</th><td>{{release}}</td></tr>
<tr><th>Project</th><td>{{projectName}}</td><th>Project code</th><td>{{projectCode}}</td></tr>
<tr><th>Project period</th><td colspan="3">{{projectPeriod}}</td></tr>
</tbody>
</table>
+6 -3
View File
@@ -1,4 +1,7 @@
<div style="border-top:1.5pt solid #1f2933; color:#52606d; display:flex; font-family:'BRN Thai',Arial,sans-serif; font-size:8pt; justify-content:space-between; margin:0 16mm; padding-top:2.5mm; width:calc(100% - 32mm);"> <!-- Repeating page footer: rule, standard reference at the left, document
<span>ISO/IEC 29110-4-1:2018</span> abbreviation at the right, matching the audited reference package. -->
<span>{{projectCode}}</span> <div style="-webkit-print-color-adjust:exact; print-color-adjust:exact; font-family:'BRN Thai',Arial,sans-serif; margin:0; padding:0; position:relative; width:210mm;">
<div style="border-top:.75pt solid #111; left:30mm; position:absolute; top:0; width:160mm;"></div>
<div style="color:#111; font-size:9pt; left:30mm; position:absolute; top:1.2mm;">{{standard}}</div>
<div style="color:#111; font-size:9pt; position:absolute; right:20mm; text-align:right; top:1.2mm;">{{footerTag}}</div>
</div> </div>
+13 -7
View File
@@ -1,8 +1,14 @@
<header class="document-header"> <!-- Repeating page header reproducing the corporate identity of the audited
<img class="document-header__logo" src="{{logoPath}}" alt="B.R.N. Enterprise Co., Ltd."> reference package. Every position is in millimetres measured from the
<div class="document-header__company"> reference scan: logo at 39.4/11.0, grey ribbon 67.5–132.2, blue banner
<p class="document-header__company-name">B.R.N. ENTERPRISE CO., LTD.</p> 127.2–197.2 with a 13.8mm diagonal, a rule under the company name from
<p class="document-header__address">1011 Supalai Grand Tower, 7th Floor, Unit 6-7, Rama 3 Road, Chongnonsi, Yannawa, Bangkok 10120<br>Tel. (+66) 2687 0250 &nbsp; Fax. (+66) 2687 0255</p> 55.4mm, and the address block beneath it. The Latin face is Liberation
Sans, metric-compatible with the Arial of the reference. -->
<div style="-webkit-print-color-adjust:exact; box-sizing:border-box; print-color-adjust:exact; top:-5.5mm; font-family:'Liberation Sans',Arial,Helvetica,sans-serif; height:27mm; margin:0; padding:0; position:relative; width:210mm;">
<div style="background:{{ciGrey}}; box-sizing:border-box; clip-path:polygon(0 0, 100% 0, 100% 100%, 16mm 100%); height:4.3mm; left:67.5mm; position:absolute; top:8.9mm; width:64.7mm;"></div>
<div style="background:{{ciBlue}}; box-sizing:border-box; clip-path:polygon(0 0, 100% 0, 100% 100%, 13.8mm 100%); color:#fff; font-size:13.5pt; height:12.5mm; left:127.2mm; line-height:14.1mm; padding:0 7.8mm 0 0; position:absolute; text-align:right; top:8.6mm; white-space:nowrap; width:70mm;">{{documentTitle}}</div>
<img src="{{logoPath}}" alt="" style="height:11.7mm; left:39.4mm; object-fit:contain; position:absolute; top:11mm; width:12.1mm;">
<div style="color:#111; font-family:'Liberation Sans Narrow','Arial Narrow',Arial,sans-serif; font-size:10.6pt; font-weight:700; left:56.8mm; line-height:1; position:absolute; top:16.5mm; white-space:nowrap;">B.R.N. ENTERPRISE CO., LTD.</div>
<div style="background:#111; height:.45mm; left:55.4mm; position:absolute; top:19.6mm; width:84.5mm;"></div>
<div style="color:#111; font-size:5pt; left:56.8mm; line-height:2.4mm; position:absolute; top:20.07mm; white-space:nowrap;">1011 Supalai Grand Tower, 7<sup>th</sup> Floor, Unit 6-7, Rama 3 Road, Chongnonsi, Yannawa, Bangkok 10120<br>Tel. (+66) 2687 0250 &nbsp;&nbsp;Fax. (+66) 2687 0255</div>
</div> </div>
<div class="document-header__title">{{documentTitle}}</div>
</header>
+94 -72
View File
@@ -1,15 +1,19 @@
/* Page box and palette measured from the audited reference package:
text block 30mm from the left and 20mm from the right, header band ending
at 25mm, footer rule at 272.8mm. */
@page { @page {
size: A4; size: A4;
margin: 18mm 16mm 28mm; margin: 32mm 20mm 24mm 30mm;
} }
:root { :root {
--brn-blue: #0789c9; --brn-blue: #0797d5; /* corporate blue, sampled from the CI header asset */
--brn-red: #bd0e2d; --brn-grey: #8e8e8e; /* header ribbon grey, measured from the reference */
--brn-ink: #1f2933; --brn-red: #e01f26; /* corporate red of the logo mark */
--brn-muted: #52606d; --brn-ink: #111111;
--brn-line: #aab7c4; --brn-muted: #333333;
--brn-pale-blue: #eaf6fc; --brn-line: #595959; /* table rule */
--brn-tint: #fff3f6; /* header-row tint used across the reference tables */
} }
* { box-sizing: border-box; } * { box-sizing: border-box; }
@@ -25,51 +29,6 @@ body { margin: 0; }
.delivery-document { width: 100%; } .delivery-document { width: 100%; }
.document-header {
align-items: center;
border-bottom: 1.5pt solid var(--brn-blue);
display: flex;
gap: 9mm;
margin: 0 0 7mm;
min-height: 22mm;
padding: 0 0 3mm;
}
.document-header__logo {
flex: 0 0 auto;
height: 16mm;
object-fit: contain;
width: 16mm;
}
.document-header__company { flex: 1; min-width: 0; }
.document-header__company-name {
font-size: 13pt;
font-weight: 700;
letter-spacing: .03em;
margin: 0;
}
.document-header__address {
color: var(--brn-muted);
font-size: 7.5pt;
line-height: 1.35;
margin: 1mm 0 0;
}
.document-header__title {
background: var(--brn-blue);
color: #fff;
font-size: 15pt;
font-weight: 500;
flex: 0 1 72mm;
overflow-wrap: anywhere;
min-width: 52mm;
padding: 4mm 6mm;
text-align: center;
}
.document-control { .document-control {
border-collapse: collapse; border-collapse: collapse;
margin: 0 0 7mm; margin: 0 0 7mm;
@@ -80,51 +39,114 @@ body { margin: 0; }
.document-control th, .document-control th,
.document-control td { .document-control td {
border: .5pt solid var(--brn-line); border: .5pt solid var(--brn-line);
padding: 2.2mm 3mm; padding: 2mm 3mm;
text-align: left; text-align: left;
vertical-align: top; vertical-align: top;
} }
.document-control th { .document-control th {
background: var(--brn-pale-blue); background: var(--brn-tint);
font-weight: 700; font-weight: 700;
width: 26%; white-space: nowrap;
width: 1%;
} }
h1, h2, h3 { break-after: avoid; color: var(--brn-ink); } /* Section headings in the reference are plain black text, not coloured bars. */
h1 { font-size: 18pt; margin: 0 0 5mm; } h1, h2, h3 { break-after: avoid; color: var(--brn-ink); font-weight: 400; }
h2 { border-left: 4pt solid var(--brn-blue); font-size: 14pt; margin: 9mm 0 4mm; padding-left: 3mm; } h1 { display: none; } /* the document name is carried by the page header */
h3 { color: var(--brn-blue); font-size: 11.5pt; margin: 6mm 0 3mm; } h2 { font-size: 11.5pt; margin: 6mm 0 2mm; }
h3 { font-size: 10.5pt; margin: 4mm 0 2mm; }
p { margin: 0 0 3.5mm; } p { margin: 0 0 3mm; }
ul, ol { margin: 0 0 3.5mm; padding-left: 7mm; }
li { margin: 0 0 1mm; }
code {
font-family: "DejaVu Sans Mono", "Courier New", monospace;
font-size: .9em;
letter-spacing: 0;
/* Inline paths must break too, or they set the cell's minimum width. */
overflow-wrap: anywhere;
}
table:not(.document-control) { table:not(.document-control) {
border-collapse: collapse; border-collapse: collapse;
border: .75pt solid #718096; border: .5pt solid var(--brn-line);
break-inside: auto;
font-size: 9pt; font-size: 9pt;
margin: 0 0 5mm; margin: 0 0 5mm;
table-layout: fixed; table-layout: auto;
width: 100%; width: 100%;
} }
table:not(.document-control) th, table:not(.document-control) th,
table:not(.document-control) td { table:not(.document-control) td {
border: .5pt solid var(--brn-line); border: .5pt solid var(--brn-line);
break-inside: avoid;
/* Thai runs have no spaces, so cells must be allowed to break anywhere;
"anywhere" (not "break-word") also lets the table shrink to the page. */
overflow-wrap: anywhere; overflow-wrap: anywhere;
padding: 2mm; padding: 1.6mm 2mm;
text-align: left;
vertical-align: top; vertical-align: top;
word-break: normal;
} }
table:not(.document-control) th { background: var(--brn-pale-blue); font-weight: 700; } table:not(.document-control) th { background: var(--brn-tint); font-weight: 400; }
/* Column alignment carried over from the Markdown separator row. */
table:not(.document-control) th.a-right,
table:not(.document-control) td.a-right { text-align: right; }
table:not(.document-control) th.a-center,
table:not(.document-control) td.a-center { text-align: center; }
/* Short columns (IDs, dates, counts) shrink to their content instead of
taking an equal share of the table width. */
table:not(.document-control) th.tight,
table:not(.document-control) td.tight { white-space: nowrap; width: 1%; }
table:not(.document-control) tr.group td { background: var(--brn-tint); font-weight: 600; }
thead { display: table-header-group; } thead { display: table-header-group; }
tr { break-inside: avoid; } tr { break-inside: avoid; }
.approval-block { break-inside: avoid; margin-top: 9mm; } .approval-block__person { break-inside: avoid; margin: 0 0 6mm; }
.approval-block__person { margin: 5mm 0; min-height: 21mm; } .approval-block__person h3 { margin: 0 0 2mm; }
.approval-block__line { border-bottom: .5pt solid var(--brn-ink); display: inline-block; min-width: 70mm; }
.approval-fields { .approval-block__line {
line-height: 1.62; border-bottom: .5pt solid var(--brn-ink);
margin: 0 0 8mm; display: inline-block;
padding: 2.5mm 0 4mm; min-width: 68mm;
vertical-align: baseline;
} }
.approval-fields {
line-height: 1.55;
margin: 0;
padding: 0;
}
/* Signature blocks: heading and its table stay together, in the reference layout. */
.signature-block { break-inside: avoid; margin: 0 0 5mm; }
.signature-block h2.signature-heading { border-left: none; font-size: 11pt; margin: 5mm 0 2mm; padding-left: 0; }
/* Fixed columns so the signature grid always fits the text block width. */
table.signature-table { table-layout: fixed; }
table.signature-table td { height: 13mm; }
table.signature-table th.tight,
table.signature-table td.tight { white-space: normal; width: auto; }
table.signature-table th:nth-child(1), table.signature-table td:nth-child(1) { width: 32%; }
table.signature-table th:nth-child(2), table.signature-table td:nth-child(2) { width: 26%; }
table.signature-table th:nth-child(3), table.signature-table td:nth-child(3) { width: 22%; }
table.signature-table th:nth-child(4), table.signature-table td:nth-child(4) { width: 20%; }
/* The document-control block repeats its label column, so labels must not stretch. */
.document-control th { width: 1%; }
/* Wide grids (schedule, register, test cases) stay portrait like the
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; }
+858
View File
@@ -0,0 +1,858 @@
#!/usr/bin/env python3
"""Convert the rendered delivery package from PDF to Word.
The work products are authored in Markdown and rendered to PDF by
render-sdlc.mjs; the .docx beside each .pdf is a layout reconstruction of that
PDF, so the Word file keeps the page the reviewer signed off rather than being
laid out a second time from the source.
The corporate identity is the one thing not taken from the reconstruction.
A PDF has no notion of a running header, so the converter rebuilds the CI band
and the footer rule as ordinary content at the top and bottom of every page,
losing the ribbon geometry and the alignment with it. Both are therefore
stripped out again here and replaced by a real Word header and footer: the band
is the same assets/header.html the PDF is printed from, captured by
render-ci-band.mjs, so it lands in the same place on every page.
Requires pdf2docx: pip install -r requirements.txt
Usage:
convert-to-docx.py SOURCE_DIRECTORY DELIVERY_DIRECTORY [BAND_DIRECTORY]
"""
import copy
import json
import re
import subprocess
import sys
import tempfile
import unicodedata
from pathlib import Path
from docx import Document
from docx.enum.text import WD_TAB_ALIGNMENT
from docx.oxml import OxmlElement
from docx.oxml.ns import qn
from docx.shared import Mm, Pt, RGBColor, Twips
from docx.text.paragraph import Paragraph
from pdf2docx import Converter
HERE = Path(__file__).resolve().parent
# Page box measured from the audited reference package, as in sdlc-delivery.css.
BODY_TOP_MM = 32
FOOTER_FROM_BOTTOM_MM = 24.2 # footer rule at 272.8mm on a 297mm page
BAND_WIDTH_MM = 210
STANDARD = 'ISO/IEC 29110-4-1:2018'
RULE_SIZE = '4' # .5pt table rule, as printed
RULE_COLOUR = '595959' # --brn-line from sdlc-delivery.css
THAI_FONT = 'Leelawadee UI' # the face embedded in the PDF as "BRN Thai"
MONO_FONT = 'Courier New' # code spans; DejaVu Sans Mono in the PDF
INK = RGBColor(0x11, 0x11, 0x11)
# The furniture pdf2docx rebuilds in the body, recognised by text that appears
# nowhere in the authored sources.
BAND_MARKS = ('B.R.N. ENTERPRISE', STANDARD)
LINE_MARKS = ('Tel. (+66)', '1011 Supalai Grand Tower')
def text_of(element):
"""The text Word shows for an element: its w:t runs, tabs and breaks.
Not itertext(): the converter leaves other text-bearing nodes in its runs,
which would read every paragraph twice over.
"""
parts = []
for node in element.iter(qn('w:t'), qn('w:tab'), qn('w:br')):
if node.tag == qn('w:t'):
parts.append(node.text or '')
elif node.getparent().tag == qn('w:r'):
# A w:tab under w:tabs is a tab-stop definition, not a character.
parts.append('\t' if node.tag == qn('w:tab') else '\n')
return ''.join(parts)
def is_blank(element):
return (
element.tag == qn('w:p')
and not text_of(element).strip()
and element.find('.//' + qn('w:sectPr')) is None
and not element.findall('.//' + qn('w:drawing'))
)
def strip_reconstructed_furniture(document):
"""Remove the per-page letterhead and footer rule pdf2docx puts in the body."""
body = document.element.body
removed = 0
for block in list(body):
if block.getparent() is None:
continue
if block.tag == qn('w:tbl'):
# The converter sometimes lays a page's furniture and its content
# into one table, so furniture is taken out a row at a time.
for row in list(block.findall(qn('w:tr'))):
if any(mark in text_of(row) for mark in BAND_MARKS + LINE_MARKS):
block.remove(row)
removed += 1
if block.find(qn('w:tr')) is None:
previous = block.getprevious()
if previous is not None and is_blank(previous):
body.remove(previous)
body.remove(block)
elif block.tag == qn('w:p') and any(mark in text_of(block) for mark in LINE_MARKS):
body.remove(block)
removed += 1
return removed
DECIMAL = re.compile(r'^-?\d+\.\d+$')
BORDER_EDGES = {'top', 'bottom', 'left', 'right', 'insideH', 'insideV', 'tl2br', 'tr2bl'}
def normalise_ooxml(document):
"""Repair the measurements the converter writes outside the schema.
Widths, sizes and border weights are whole units in OOXML, and a colour is
six hex digits with no leading hash. The converter writes the numbers it
measured off the page instead - "5.599999999999909", "#585858" - which Word
is free to interpret as it likes, so borders come out at weights nobody
asked for.
"""
fixed = 0
for element in document.element.body.iter():
for name, value in list(element.attrib.items()):
local = name.rpartition('}')[2]
if DECIMAL.match(value):
element.set(name, str(round(float(value))))
fixed += 1
elif local in ('color', 'fill') and value.startswith('#'):
element.set(name, value[1:])
fixed += 1
return fixed
def match_rule_weight(document):
"""Draw every table rule at the .5pt the PDF is printed with."""
edges = 0
for borders in document.element.body.iter():
if borders.tag not in (qn('w:tblBorders'), qn('w:tcBorders')):
continue
for edge in borders:
if edge.tag.rpartition('}')[2] not in BORDER_EDGES:
continue
if edge.get(qn('w:val')) in (None, 'nil', 'none'):
continue
edge.set(qn('w:sz'), RULE_SIZE)
edge.set(qn('w:color'), RULE_COLOUR)
edges += 1
return edges
def drop_padding(document):
"""Remove the blank paragraphs the converter pads each page out with.
A run of them at the foot of a page is pure whitespace - the section break
already ends the page - and a run between two blocks only ever needs one.
"""
body = document.element.body
children = list(body)
removed = 0
index = 0
while index < len(children):
if not is_blank(children[index]):
index += 1
continue
start = index
while index < len(children) and is_blank(children[index]):
index += 1
following = children[index] if index < len(children) else None
ends_page = following is None or (
following.tag == qn('w:p') and following.find('.//' + qn('w:sectPr')) is not None
)
for element in children[start + (0 if ends_page else 1):index]:
body.remove(element)
removed += 1
return removed
R_EMBED = '{http://schemas.openxmlformats.org/officeDocument/2006/relationships}embed'
LOGO_MIN_PX = 100
def strip_logo_images(document):
"""Drop the letterhead logo left loose in the body.
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 is_logo(part.image)
}
removed = 0
for drawing in list(document.element.body.iter(qn('w:drawing'))):
blip = drawing.find('.//' + qn('a:blip'))
if blip is not None and blip.get(R_EMBED) in logos:
drawing.getparent().remove(drawing)
removed += 1
return removed
# ---------------------------------------------------------------------------
# Fonts
# ---------------------------------------------------------------------------
def normalise_fonts(document):
"""Point every run at a face Word actually has.
The PDF embeds its faces as Type3 fonts, so the converter names runs after
the PDF objects - "Type3 (4 0 R)" - and gives them no complex-script face
or size. Word then sets Latin text in a fallback serif and Thai text at the
default size, which is the mixed, jumping type the conversion showed.
"""
for run in document.element.body.iter(qn('w:r')):
properties = run.find(qn('w:rPr'))
if properties is None:
properties = OxmlElement('w:rPr')
run.insert(0, properties)
fonts = properties.find(qn('w:rFonts'))
face = MONO_FONT if fonts is not None and 'Mono' in (fonts.get(qn('w:ascii')) or '') else THAI_FONT
if fonts is None:
fonts = OxmlElement('w:rFonts')
style = properties.find(qn('w:rStyle'))
properties.insert(1 if style is not None else 0, fonts)
for attribute in ('w:ascii', 'w:hAnsi', 'w:cs', 'w:eastAsia'):
fonts.set(qn(attribute), face)
size = properties.find(qn('w:sz'))
if size is not None and properties.find(qn('w:szCs')) is None:
complex_size = OxmlElement('w:szCs')
complex_size.set(qn('w:val'), size.get(qn('w:val')))
size.addnext(complex_size)
# ---------------------------------------------------------------------------
# Bullets
# ---------------------------------------------------------------------------
BULLET = '•'
BULLET_TEXT_MM = 7 # ul padding-left in sdlc-delivery.css
BULLET_HANG_MM = 4
def text_block_left_mm(section):
# The PDF text block starts 30mm in, or 15mm on the landscape work products.
return 15 if section.page_width > section.page_height else 30
def unwrap_list_tables(document):
"""Turn the tables the converter builds out of bullet lists back into text.
A short list is sometimes laid out as a grid - bullet image, item, empty
cell - with stray bottom borders that print as loose rules. Each row goes
back into the body as a paragraph of its own.
"""
body = document.element.body
items = []
for table in [child for child in body if child.tag == qn('w:tbl')]:
rows = table.findall(qn('w:tr'))
entries = []
for row in rows:
cells = row.findall(qn('w:tc'))
worded = [cell for cell in cells if text_of(cell).strip()]
marked = [cell for cell in cells if cell.findall('.//' + qn('w:drawing')) and not text_of(cell).strip()]
if len(worded) != 1 or not marked:
entries = None
break
entries.append(worded[0])
if not rows or entries is None:
continue
for cell in entries:
for paragraph in cell.findall(qn('w:p')):
if text_of(paragraph).strip():
table.addprevious(paragraph)
items.append(paragraph)
body.remove(table)
return items
def take_bullet_images(document):
"""Collect the list items drawn with an image marker, dropping the image."""
items = []
for paragraph in [child for child in document.element.body if child.tag == qn('w:p')]:
drawings = paragraph.findall('.//' + qn('w:drawing'))
if not drawings or not text_of(paragraph).strip():
continue
for drawing in drawings:
run = drawing.getparent()
run.remove(drawing)
if run.tag == qn('w:r') and not text_of(run).strip():
run.getparent().remove(run)
items.append(paragraph)
return items
def make_bullet(document, paragraph):
section = document.sections[0]
item = Paragraph(paragraph, document._body)
text_mm = text_block_left_mm(section) + BULLET_TEXT_MM
item.paragraph_format.left_indent = Twips(round(Mm(text_mm).twips - section.left_margin.twips))
item.paragraph_format.first_line_indent = -Mm(BULLET_HANG_MM)
first = paragraph.find(qn('w:r'))
marker = OxmlElement('w:r')
if first is not None and first.find(qn('w:rPr')) is not None:
marker.append(copy.deepcopy(first.find(qn('w:rPr'))))
text = OxmlElement('w:t')
text.text = BULLET
marker.append(text)
marker.append(OxmlElement('w:tab'))
properties = paragraph.find(qn('w:pPr'))
if properties is not None:
properties.addnext(marker)
else:
paragraph.insert(0, marker)
# ---------------------------------------------------------------------------
# Text from the source
# ---------------------------------------------------------------------------
APPROVAL_FIELD = re.compile(r'^(Name|Role|Signature|Date|Position|Company|Project roles|Decision):')
SEPARATOR_CELL = re.compile(r'^:?-{3,}:?$')
INLINE = re.compile(r'\*\*(.+?)\*\*|`([^`]+)`|\\([\\*_`~])')
def letters(text):
"""The characters two renderings of the same text must share."""
return ''.join(
character for character in unicodedata.normalize('NFC', text)
if unicodedata.category(character)[0] in 'LMN'
).lower()
def read_source(markdown):
"""Blocks and table cells of a work product, as the reader sees them."""
body = re.sub(r'^#\s+.+\n+', '', markdown, count=1, flags=re.M)
body = re.sub(r'^<!--.*?-->\s*$', '', body, flags=re.M)
blocks, cells, tables, paragraph = [], [], [], []
bullets = set()
table = None
def flush():
if paragraph:
joined = ''
for index, line in enumerate(paragraph):
hard = re.search(r'\s{2,}$', line) or APPROVAL_FIELD.match(line.strip())
joined += line.strip() + ('\n' if hard else (' ' if index < len(paragraph) - 1 else ''))
blocks.append(joined.rstrip('\n'))
paragraph.clear()
for line in body.splitlines():
stripped = line.strip()
if stripped.startswith('|') and stripped.endswith('|'):
flush()
parts = [part.strip() for part in stripped[1:-1].split('|')]
if table is None:
table = {'after': blocks[-1] if blocks else None, 'rows': []}
tables.append(table)
if not all(SEPARATOR_CELL.match(part) for part in parts):
row = [re.sub(r'<br\s*/?>', '\n', part) for part in parts]
cells.extend(row)
table['rows'].append(row)
continue
table = None
if not stripped:
flush()
continue
heading = re.match(r'^#{1,3}\s+(.+)$', stripped)
bullet = re.match(r'^[-*]\s+(.+)$', stripped)
ordered = re.match(r'^\d+\.\s+.+$', stripped)
if heading or bullet or ordered:
flush()
# A numbered item keeps its number: the PDF prints it as text.
blocks.append(heading.group(1) if heading else bullet.group(1) if bullet else stripped)
if bullet:
bullets.add(len(blocks) - 1)
continue
paragraph.append(line)
flush()
return blocks, cells, tables, bullets
def index_texts(texts):
exact, by_length = {}, {}
for text in texts:
key = letters(text)
if key and key not in exact:
exact[key] = text
by_length.setdefault(len(key), []).append(key)
return exact, by_length
def is_subsequence(short, long):
remaining = iter(long)
return all(character in remaining for character in short)
def source_for(text, exact, by_length):
"""The authored text a converted fragment came from, if it is unambiguous.
Same letters means only spacing and punctuation differ. Failing that, a
fragment that is the source with up to three characters missing - the
converter drops characters, it never invents them - is accepted when just
one source text fits.
"""
key = letters(text)
if not key:
return None
if key in exact:
return exact[key]
if len(key) < 8:
return None
fits = {
exact[candidate]
for length in range(len(key) + 1, len(key) + 4)
for candidate in by_length.get(length, ())
if is_subsequence(key, candidate)
}
return fits.pop() if len(fits) == 1 else None
def write_text(paragraph, text):
"""Replace a paragraph's runs with the authored text, keeping its look."""
template = paragraph.find('.//' + qn('w:rPr'))
template = copy.deepcopy(template) if template is not None else OxmlElement('w:rPr')
for child in list(paragraph):
if child.tag != qn('w:pPr'):
paragraph.remove(child)
def run(content=None, bold=False, code=False, line_break=False):
element = OxmlElement('w:r')
properties = copy.deepcopy(template)
fonts = properties.find(qn('w:rFonts'))
if fonts is not None:
face = MONO_FONT if code else THAI_FONT
for attribute in ('w:ascii', 'w:hAnsi', 'w:cs', 'w:eastAsia'):
fonts.set(qn(attribute), face)
weight = properties.find(qn('w:b'))
for complex_weight in properties.findall(qn('w:bCs')):
properties.remove(complex_weight)
if bold:
if weight is None:
weight = OxmlElement('w:b')
(fonts.addnext(weight) if fonts is not None else properties.insert(0, weight))
weight.attrib.pop(qn('w:val'), None)
weight.addnext(OxmlElement('w:bCs'))
elif weight is not None:
weight.set(qn('w:val'), '0')
element.append(properties)
if line_break:
element.append(OxmlElement('w:br'))
else:
node = OxmlElement('w:t')
node.text = content
node.set('{http://www.w3.org/XML/1998/namespace}space', 'preserve')
element.append(node)
paragraph.append(element)
for number, line in enumerate(text.split('\n')):
if number:
run(line_break=True)
cursor = 0
for match in INLINE.finditer(line):
if match.start() > cursor:
run(line[cursor:match.start()])
if match.group(1) is not None:
run(match.group(1), bold=True)
elif match.group(2) is not None:
run(match.group(2), code=True)
else:
run(match.group(3))
cursor = match.end()
if cursor < len(line):
run(line[cursor:])
def split_blocks(key, block_keys):
"""The run of consecutive source blocks a converted paragraph is made of."""
for start, first in enumerate(block_keys):
if not first or not key.startswith(first):
continue
combined, end = first, start
while len(combined) < len(key) and end + 1 < len(block_keys) and end - start < 7:
end += 1
combined += block_keys[end]
if combined == key:
return list(range(start, end + 1))
if not key.startswith(combined):
break
return None
def restore_text(document, markdown, bullets):
"""Put the authored wording back where the converter mangled it.
PDF text has no word spaces for Thai, so the conversion runs Thai words
together, breaks a paragraph into one paragraph per printed line, and now
and then drops a character. Wherever a converted paragraph or table cell
can be traced to exactly one piece of the source, its text is rewritten
from the source; anything that cannot be traced is left as converted.
"""
blocks, cells, _, bullet_blocks = read_source(markdown)
block_keys = [letters(block) for block in blocks]
known = {id(paragraph) for paragraph in bullets}
block_exact, block_lengths = index_texts(blocks)
cell_exact, cell_lengths = index_texts(cells)
body = document.element.body
rewritten = merged = 0
for cell in body.iter(qn('w:tc')):
if cell.find('.//' + qn('w:tbl')) is not None:
continue
paragraphs = cell.findall(qn('w:p'))
text = '\n'.join(text_of(paragraph) for paragraph in paragraphs)
source = source_for(text, cell_exact, cell_lengths)
if source is None or '___' in source or source == text:
continue
write_text(paragraphs[0], source)
for extra in paragraphs[1:]:
cell.remove(extra)
rewritten += 1
children = [child for child in body if child.tag == qn('w:p') and text_of(child).strip()]
index = 0
while index < len(children):
paragraph = children[index]
source = source_for(text_of(paragraph), block_exact, block_lengths)
span = 1
if source is None:
# A paragraph the converter split into one paragraph per line.
combined = text_of(paragraph)
follower = paragraph
for count in range(2, 9):
follower = follower.getnext()
if follower is None or follower.tag != qn('w:p') or not text_of(follower).strip():
break
combined += text_of(follower)
if letters(combined) in block_exact:
source, span = block_exact[letters(combined)], count
break
if source is None:
# Several list items or paragraphs the converter ran into one.
parts = split_blocks(letters(text_of(paragraph)), block_keys)
if parts and not any('___' in blocks[part] for part in parts):
previous = paragraph
for number, part in enumerate(parts):
target = paragraph
if number:
target = OxmlElement('w:p')
if paragraph.find(qn('w:pPr')) is not None:
target.append(copy.deepcopy(paragraph.find(qn('w:pPr'))))
template = paragraph.find('.//' + qn('w:rPr'))
run = OxmlElement('w:r')
run.append(copy.deepcopy(template) if template is not None else OxmlElement('w:rPr'))
target.append(run)
previous.addnext(target)
previous = target
write_text(target, blocks[part])
if part in bullet_blocks and id(target) not in known:
bullets.append(target)
known.add(id(target))
rewritten += len(parts)
index += 1
continue
if source is not None and '___' not in source:
for extra in children[index + 1:index + span]:
body.remove(extra)
merged += 1
write_text(paragraph, source)
rewritten += 1
index += span
return rewritten, merged
TINT = 'FFF3F6' # header-row tint used across the reference tables
TABLE_PT = 9
CELL_MARGIN_MM = (1.6, 2) # vertical, horizontal padding of a table cell
PRINTED_TEXT_WIDTH_MM = {False: 160, True: 267} # portrait, landscape
DISTINCTIVE_LETTERS = 15 # shorter cells - labels, IDs, dates - recur elsewhere
DROPPED_CHARACTER_SLACK = 3
def present(key, document_key):
"""Whether a cell's whole text reached the document.
The whole cell, not a fragment of it: short Thai phrases recur across a
work product, so a partial match proves nothing. Its two halves in order,
a few characters apart at most, still count, as the converter sometimes
drops a character mid-cell.
"""
if key in document_key:
return True
half = len(key) // 2
start = document_key.find(key[:half])
while start >= 0:
second = document_key.find(key[half:], start + half)
if 0 <= second - (start + half) <= DROPPED_CHARACTER_SLACK:
return True
start = document_key.find(key[:half], start + 1)
return False
def dropped_tables(document, markdown):
"""Source tables of which not one distinctive cell reached the document."""
_, _, tables, _ = read_source(markdown)
document_key = letters(text_of(document.element.body))
missing = []
for table in tables:
keys = [letters(cell) for row in table['rows'] for cell in row]
keys = [key for key in keys if len(key) >= DISTINCTIVE_LETTERS]
if keys and not any(present(key, document_key) for key in keys):
missing.append(table)
return missing
def build_table(document, rows):
section = document.sections[0]
landscape = section.page_width > section.page_height
width = Mm(PRINTED_TEXT_WIDTH_MM[landscape]).twips
columns = max(len(row) for row in rows)
longest = [max(len(letters(row[i])) if i < len(row) else 0 for row in rows) for i in range(columns)]
weights = [min(max(value, 8), 80) for value in longest]
widths = [round(width * weight / sum(weights)) for weight in weights]
def element(tag, **attributes):
node = OxmlElement(tag)
for name, value in attributes.items():
node.set(qn(name), str(value))
return node
table = element('w:tbl')
properties = element('w:tblPr')
properties.append(element('w:tblW', **{'w:w': width, 'w:type': 'dxa'}))
indent = round(Mm(text_block_left_mm(section)).twips - section.left_margin.twips)
properties.append(element('w:tblInd', **{'w:w': indent, 'w:type': 'dxa'}))
borders = element('w:tblBorders')
for edge in ('top', 'left', 'bottom', 'right', 'insideH', 'insideV'):
borders.append(element('w:' + edge, **{'w:val': 'single', 'w:sz': RULE_SIZE, 'w:space': 0, 'w:color': RULE_COLOUR}))
properties.append(borders)
properties.append(element('w:tblLayout', **{'w:type': 'fixed'}))
margins = element('w:tblCellMar')
vertical, horizontal = (round(Mm(value).twips) for value in CELL_MARGIN_MM)
for edge, value in (('top', vertical), ('left', horizontal), ('bottom', vertical), ('right', horizontal)):
margins.append(element('w:' + edge, **{'w:w': value, 'w:type': 'dxa'}))
properties.append(margins)
table.append(properties)
grid = element('w:tblGrid')
for column_width in widths:
grid.append(element('w:gridCol', **{'w:w': column_width}))
table.append(grid)
for number, cells in enumerate(rows):
row = element('w:tr')
row_properties = element('w:trPr')
row_properties.append(element('w:cantSplit'))
if number == 0:
row_properties.append(element('w:tblHeader'))
row.append(row_properties)
for column in range(columns):
cell = element('w:tc')
cell_properties = element('w:tcPr')
cell_properties.append(element('w:tcW', **{'w:w': widths[column], 'w:type': 'dxa'}))
if number == 0:
cell_properties.append(element('w:shd', **{'w:val': 'clear', 'w:color': 'auto', 'w:fill': TINT}))
cell.append(cell_properties)
paragraph = element('w:p')
paragraph_properties = element('w:pPr')
paragraph_properties.append(element('w:spacing', **{'w:before': 0, 'w:after': 0}))
paragraph.append(paragraph_properties)
template = element('w:r')
run_properties = element('w:rPr')
fonts = element('w:rFonts', **{'w:ascii': THAI_FONT, 'w:hAnsi': THAI_FONT, 'w:cs': THAI_FONT, 'w:eastAsia': THAI_FONT})
run_properties.append(fonts)
run_properties.append(element('w:color', **{'w:val': '111111'}))
run_properties.append(element('w:sz', **{'w:val': TABLE_PT * 2}))
run_properties.append(element('w:szCs', **{'w:val': TABLE_PT * 2}))
template.append(run_properties)
paragraph.append(template)
write_text(paragraph, cells[column] if column < len(cells) else '')
cell.append(paragraph)
row.append(cell)
table.append(row)
return table
def restore_dropped_tables(document, markdown):
"""Put back tables the converter left out, where the source places them.
The converter occasionally drops a whole table that is plainly printed in
the PDF. It is rebuilt from the source in the style of the converted
tables, after the paragraph it follows in the source.
"""
restored = 0
body = document.element.body
for table in dropped_tables(document, markdown):
if not table['after'] or not table['rows']:
continue
anchor_key = letters(table['after'])
paragraphs = [child for child in body if child.tag == qn('w:p')]
anchor = next((child for child in paragraphs if letters(text_of(child)) == anchor_key), None)
if anchor is None:
# The block may share a paragraph with what precedes it.
endings = [child for child in paragraphs if anchor_key and letters(text_of(child)).endswith(anchor_key)]
anchor = endings[0] if len(endings) == 1 else None
if anchor is None:
continue
anchor.addnext(build_table(document, table['rows']))
restored += 1
return restored
def style_run(run, points):
run.font.size = Pt(points)
run.font.color.rgb = INK
fonts = OxmlElement('w:rFonts')
for attribute in ('w:ascii', 'w:hAnsi', 'w:cs', 'w:eastAsia'):
fonts.set(qn(attribute), THAI_FONT)
run._element.get_or_add_rPr().insert(0, fonts)
def drop_style(paragraph):
# The template's Header and Footer styles carry centre and right tab stops
# of their own, which catch the tab before the footer tag and centre it.
properties = paragraph._p.pPr
if properties is not None and properties.find(qn('w:pStyle')) is not None:
properties.remove(properties.find(qn('w:pStyle')))
def add_top_border(paragraph):
borders = OxmlElement('w:pBdr')
top = OxmlElement('w:top')
top.set(qn('w:val'), 'single')
top.set(qn('w:sz'), '6') # .75pt, as printed
top.set(qn('w:space'), '1')
top.set(qn('w:color'), '111111')
borders.append(top)
paragraph._p.get_or_add_pPr().append(borders)
def install_letterhead(document, band, footer_tag):
"""Give every page the CI band and footer rule as real Word furniture."""
for index, section in enumerate(document.sections):
section.top_margin = Mm(BODY_TOP_MM)
section.header_distance = Mm(0)
section.footer_distance = Mm(FOOTER_FROM_BOTTOM_MM)
# Only the first section carries the furniture; the rest inherit it.
section.header.is_linked_to_previous = index > 0
section.footer.is_linked_to_previous = index > 0
section = document.sections[0]
header = section.header.paragraphs[0]
drop_style(header)
# The band spans the page, so it starts outside the text block.
header.paragraph_format.left_indent = -section.left_margin
header.paragraph_format.space_before = Pt(0)
header.paragraph_format.space_after = Pt(0)
header.add_run().add_picture(str(band), width=Mm(BAND_WIDTH_MM))
footer = section.footer.paragraphs[0]
drop_style(footer)
footer.paragraph_format.space_before = Pt(0)
footer.paragraph_format.space_after = Pt(0)
footer.paragraph_format.tab_stops.add_tab_stop(
section.page_width - section.left_margin - section.right_margin,
WD_TAB_ALIGNMENT.RIGHT
)
add_top_border(footer)
style_run(footer.add_run(STANDARD), 9)
style_run(footer.add_run(f'\t{footer_tag}'), 9)
def convert(pdf, source, band, footer_tag):
docx = pdf.with_suffix('.docx')
converter = Converter(str(pdf))
try:
converter.convert(str(docx))
finally:
converter.close()
document = Document(str(docx))
strip_reconstructed_furniture(document)
strip_logo_images(document)
bullets = unwrap_list_tables(document) + take_bullet_images(document)
normalise_ooxml(document)
normalise_fonts(document)
markdown = source.read_text(encoding='utf8')
restore_text(document, markdown, bullets)
restore_dropped_tables(document, markdown)
for paragraph in bullets:
if paragraph.getparent() is not None:
make_bullet(document, paragraph)
drop_padding(document)
match_rule_weight(document)
install_letterhead(document, band, footer_tag)
document.save(str(docx))
return docx
def render_bands(source_root, band_root):
subprocess.run(
['node', str(HERE / 'render-ci-band.mjs'), str(source_root), str(band_root)],
check=True
)
return json.loads((Path(band_root) / 'bands.json').read_text(encoding='utf8'))
def main(argv):
if len(argv) < 2:
raise SystemExit(__doc__)
source_root, delivery_root = Path(argv[0]), Path(argv[1])
temporary = None
if len(argv) > 2:
bands = json.loads((Path(argv[2]) / 'bands.json').read_text(encoding='utf8'))
else:
temporary = tempfile.TemporaryDirectory()
bands = render_bands(source_root, temporary.name)
failures = []
for index, (relative, band) in enumerate(sorted(bands.items()), start=1):
pdf = delivery_root / Path(relative).with_suffix('.pdf')
try:
docx = convert(pdf, source_root / relative, Path(band['band']), band['footer'])
print(f'[{index}/{len(bands)}] {docx.relative_to(delivery_root)}')
except Exception as error: # noqa: BLE001 - reported, not swallowed
failures.append((pdf, error))
print(f'[{index}/{len(bands)}] FAILED {relative}: {error}', file=sys.stderr)
if temporary:
temporary.cleanup()
print(f'{len(bands) - len(failures)}/{len(bands)} documents converted')
if failures:
raise SystemExit(1)
if __name__ == '__main__':
main(sys.argv[1:])
+81
View File
@@ -0,0 +1,81 @@
// Render the corporate identity band used by the delivery package.
//
// The band is the same assets/header.html the PDF is printed with, captured as
// an image so its ribbon geometry survives into Word, where it is placed in a
// real page header instead of being reconstructed in the body.
import fs from 'node:fs/promises';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { chromium } from 'playwright';
const here = path.dirname(fileURLToPath(import.meta.url));
const root = path.resolve(here, '../..');
const assets = path.join(here, 'assets');
export const BAND_MM = { width: 210, height: 27 };
const SCALE = 3; // the band is drawn at 3x so it stays crisp in print
const applyTemplate = (template, values) => template.replace(/{{(\w+)}}/g, (_, key) => values[key] ?? '');
const slug = (title) => title.replace(/[^\w-]+/g, '-').replace(/^-|-$/g, '').toLowerCase() || 'band';
// Work products sit in the process folders; top-level files in sdlc/ are notes.
async function markdownFiles(directory, depth = 0) {
const entries = await fs.readdir(directory, { withFileTypes: true });
const results = await Promise.all(entries.map(async (entry) => {
const target = path.join(directory, entry.name);
if (entry.isDirectory()) return markdownFiles(target, depth + 1);
return depth > 0 && entry.isFile() && entry.name.endsWith('.md') ? [target] : [];
}));
return results.flat();
}
async function main() {
const [sourceRoot, outputRoot] = process.argv.slice(2);
if (!sourceRoot || !outputRoot) throw new Error('Usage: render-ci-band.mjs SOURCE_DIRECTORY OUTPUT_DIRECTORY');
const [template, logo] = await Promise.all([
fs.readFile(path.join(assets, 'header.html'), 'utf8'),
fs.readFile(path.join(root, 'app/assets/images/logo.svg'))
]);
const values = {
logoPath: `data:image/svg+xml;base64,${logo.toString('base64')}`,
ciBlue: '#0797d5',
ciGrey: '#8e8e8e' // ribbon grey measured from the audited reference documents
};
await fs.mkdir(outputRoot, { recursive: true });
const sources = (await markdownFiles(sourceRoot)).sort();
const bands = {};
const rendered = new Map();
const browser = await chromium.launch({ headless: true });
try {
const page = await browser.newPage({ deviceScaleFactor: SCALE });
for (const source of sources) {
const markdown = await fs.readFile(source, 'utf8');
const title = markdown.match(/^#\s+(.+)$/m)?.[1]?.trim() ?? 'BRN WMS';
const footer = markdown.match(/^<!--\s*footer:\s*(.+?)\s*-->\s*$/m)?.[1] ?? '';
if (!rendered.has(title)) {
const file = path.join(outputRoot, `${slug(title)}.png`);
await page.setContent(
'<!doctype html><meta charset="utf-8"><body style="margin:0;background:#fff">' +
`<div id="band" style="width:${BAND_MM.width}mm;height:${BAND_MM.height}mm;overflow:hidden;position:relative">` +
`${applyTemplate(template, { ...values, documentTitle: title })}</div></body>`,
{ waitUntil: 'networkidle' }
);
await page.locator('#band').screenshot({ type: 'png', path: file });
rendered.set(title, file);
}
bands[path.relative(sourceRoot, source)] = { title, footer, band: rendered.get(title) };
}
} finally {
await browser.close();
}
await fs.writeFile(path.join(outputRoot, 'bands.json'), `${JSON.stringify(bands, null, 2)}\n`);
console.log(`${rendered.size} band${rendered.size === 1 ? '' : 's'} rendered for ${sources.length} documents`);
}
main().catch((error) => { console.error(error); process.exit(1); });
+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); });
+131 -39
View File
@@ -1,4 +1,5 @@
import fs from 'node:fs/promises'; import fs from 'node:fs/promises';
import { readFileSync } from 'node:fs';
import path from 'node:path'; import path from 'node:path';
import { fileURLToPath } from 'node:url'; import { fileURLToPath } from 'node:url';
import { chromium } from 'playwright'; import { chromium } from 'playwright';
@@ -13,8 +14,13 @@ const escapeHtml = (value) => value
.replaceAll('>', '&gt;'); .replaceAll('>', '&gt;');
const renderInline = (value) => escapeHtml(value) const renderInline = (value) => escapeHtml(value)
// A literal <br> is the only markup allowed through, so a table cell can list
// several files on separate lines the way the reference package does.
.replaceAll('&lt;br&gt;', '<br>')
.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>') .replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
.replace(/`([^`]+)`/g, '<code>$1</code>'); .replace(/`([^`]+)`/g, '<code>$1</code>')
.replace(/_{3,}/g, '<span class="approval-block__line"></span>')
.replace(/\\([\\*_`~])/g, '$1');
const applyTemplate = (template, values) => template.replace(/{{(\w+)}}/g, (_, key) => values[key] ?? ''); const applyTemplate = (template, values) => template.replace(/{{(\w+)}}/g, (_, key) => values[key] ?? '');
@@ -30,12 +36,67 @@ function isSeparator(cells) {
return cells.every((cell) => /^:?-{3,}:?$/.test(cell)); return cells.every((cell) => /^:?-{3,}:?$/.test(cell));
} }
const TIGHT_COLUMN_MAX = 12;
function columnAlignment(separatorCells) {
return separatorCells.map((cell) => {
const left = cell.startsWith(':');
const right = cell.endsWith(':');
if (left && right) return 'a-center';
if (right) return 'a-right';
return '';
});
}
function isControlBlock(lines) {
return tableCells(lines[0] ?? '')[0] === 'Document No';
}
// The document-control block is a label/value grid, not a data table: odd cells are
// labels, even cells are values, and a row with one pair spans the full width.
function renderControlBlock(lines) {
const rows = lines.map(tableCells);
const body = rows.map((cells) => {
const pairs = [];
for (let i = 0; i < cells.length; i += 2) pairs.push([cells[i], cells[i + 1] ?? '']);
const span = pairs.length === 1 ? ' colspan="3"' : '';
return `<tr>${pairs.map(([label, value]) => `<th>${renderInline(label)}</th><td${span}>${renderInline(value)}</td>`).join('')}</tr>`;
}).join('');
return `<table class="document-control"><tbody>${body}</tbody></table>`;
}
function renderTable(lines, className = '') { function renderTable(lines, className = '') {
const rows = lines.map(tableCells); const rows = lines.map(tableCells);
const header = rows[0]; const header = rows[0];
const data = isSeparator(rows[1] ?? []) ? rows.slice(2) : rows.slice(1); const hasSeparator = isSeparator(rows[1] ?? []);
const cells = (tag, row) => row.map((cell) => `<${tag}>${renderInline(cell)}</${tag}>`).join(''); const align = hasSeparator ? columnAlignment(rows[1]) : header.map(() => '');
return `<table${className ? ` class="${className}"` : ''}><thead><tr>${cells('th', header)}</tr></thead><tbody>${data.map((row) => `<tr>${cells('td', row)}</tr>`).join('')}</tbody></table>`; const data = hasSeparator ? rows.slice(2) : rows.slice(1);
// A row with only its first cell filled is a group heading (CR01, SR06, UN04,
// WP 1.0 …): it spans the full width and does not size the columns. Signature
// tables keep their blank cells for signing.
const isGroup = (row) => className !== 'signature-table' && header.length >= 3
&& Boolean(row[0]) && row.slice(1).every((cell) => !cell);
// A column whose longest cell is short (IDs, dates, counts, statuses) is
// marked "tight" so auto table layout shrinks it to its content instead of
// handing every column an equal share of the page width.
const widest = header.map((_, column) => Math.max(
(header[column] ?? '').length,
...data.filter((row) => !isGroup(row)).map((row) => (row[column] ?? '').replace(/[`*]/g, '').length)
));
const classFor = (column) => [
align[column] ?? '',
widest[column] <= TIGHT_COLUMN_MAX ? 'tight' : ''
].filter(Boolean).join(' ');
const cells = (tag, row) => row.map((cell, column) => {
const attr = classFor(column);
return `<${tag}${attr ? ` class="${attr}"` : ''}>${renderInline(cell)}</${tag}>`;
}).join('');
const classes = [className, header.length >= 8 ? 'dense' : ''].filter(Boolean).join(' ');
return `<table${classes ? ` class="${classes}"` : ''}><thead><tr>${cells('th', header)}</tr></thead><tbody>${data.map((row) => isGroup(row)
? `<tr class="group"><td colspan="${header.length}">${renderInline(row[0])}</td></tr>`
: `<tr>${cells('td', row)}</tr>`).join('')}</tbody></table>`;
} }
function renderParagraph(lines) { function renderParagraph(lines) {
@@ -48,7 +109,7 @@ function renderParagraph(lines) {
}).join(''); }).join('');
} }
function markdownToHtml(markdown) { function markdownToHtml(markdown, baseDir) {
const lines = markdown.replaceAll('\r\n', '\n').split('\n'); const lines = markdown.replaceAll('\r\n', '\n').split('\n');
const output = []; const output = [];
let index = 0; let index = 0;
@@ -57,9 +118,25 @@ function markdownToHtml(markdown) {
const line = lines[index]; const line = lines[index];
if (!line.trim()) { index += 1; continue; } if (!line.trim()) { index += 1; continue; }
// A figure: ![caption](relative/path.png), embedded so the PDF needs no external files.
const image = line.match(/^!\[([^\]]*)\]\(([^)]+)\)\s*$/);
if (image) {
const data = readFileSync(path.resolve(baseDir, image[2])).toString('base64');
output.push(`<figure class="figure"><img src="data:image/png;base64,${data}" alt=""><figcaption>${renderInline(image[1])}</figcaption></figure>`);
index += 1;
continue;
}
if (isTableLine(line)) { if (isTableLine(line)) {
const table = []; const table = [];
while (index < lines.length && isTableLine(lines[index])) table.push(lines[index++]); while (index < lines.length && isTableLine(lines[index])) table.push(lines[index++]);
if (isControlBlock(table)) { output.push(renderControlBlock(table)); continue; }
const previous = output[output.length - 1] ?? '';
// Keep a signature heading and its table together on one page.
if (/^<h2 class="signature-heading">/.test(previous)) {
output.push(`<div class="signature-block">${output.pop()}${renderTable(table, 'signature-table')}</div>`);
continue;
}
output.push(renderTable(table)); output.push(renderTable(table));
continue; continue;
} }
@@ -67,7 +144,9 @@ function markdownToHtml(markdown) {
const heading = line.match(/^(#{1,3})\s+(.+)$/); const heading = line.match(/^(#{1,3})\s+(.+)$/);
if (heading) { if (heading) {
const level = heading[1].length; const level = heading[1].length;
output.push(`<h${level}>${renderInline(heading[2])}</h${level}>`); const signature = level === 2 && /^ผู้(จัดทำเอกสาร|ตรวจสอบเอกสาร|อนุมัติ)/.test(heading[2]);
const attr = signature ? ' class="signature-heading"' : '';
output.push(`<h${level}${attr}>${renderInline(heading[2])}</h${level}>`);
index += 1; index += 1;
continue; continue;
} }
@@ -79,10 +158,23 @@ function markdownToHtml(markdown) {
continue; continue;
} }
if (/^\d+\.\s+/.test(line)) {
const items = [];
while (index < lines.length && /^\d+\.\s+/.test(lines[index])) items.push(`<li>${renderInline(lines[index++].replace(/^\d+\.\s+/, ''))}</li>`);
output.push(`<ol>${items.join('')}</ol>`);
continue;
}
const paragraph = []; const paragraph = [];
while (index < lines.length && lines[index].trim() && !isTableLine(lines[index]) && !/^(#{1,3})\s+/.test(lines[index]) && !/^[-*]\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 approvalClass = paragraph.some((line) => /^(Name|Role|Signature|Date|Position|Company|Project roles|Decision):/.test(line.trimEnd())) ? ' class="approval-fields"' : ''; const isApproval = paragraph.some((line) => /^(Name|Role|Signature|Date|Position|Company|Project roles|Decision):/.test(line.trimEnd()));
output.push(`<p${approvalClass}>${renderParagraph(paragraph)}</p>`); const html = `<p${isApproval ? ' class="approval-fields"' : ''}>${renderParagraph(paragraph)}</p>`;
// Keep a signatory's heading and fields on one page.
if (isApproval && /^<h3>/.test(output[output.length - 1] ?? '')) {
output.push(`<div class="approval-block__person">${output.pop()}${html}</div>`);
} else {
output.push(html);
}
} }
return output.join('\n'); return output.join('\n');
@@ -91,32 +183,26 @@ function markdownToHtml(markdown) {
function extractDocument(markdown) { function extractDocument(markdown) {
const titleMatch = markdown.match(/^#\s+(.+)\n+/m); const titleMatch = markdown.match(/^#\s+(.+)\n+/m);
const title = titleMatch?.[1] ?? 'BRN WMS'; const title = titleMatch?.[1] ?? 'BRN WMS';
const afterTitle = markdown.slice((titleMatch?.index ?? 0) + (titleMatch?.[0].length ?? 0)); const footer = markdown.match(/^<!--\s*footer:\s*(.+?)\s*-->\s*$/m)?.[1] ?? '';
const lines = afterTitle.split('\n'); const body = markdown
const metadata = {}; .slice((titleMatch?.index ?? 0) + (titleMatch?.[0].length ?? 0))
let bodyStart = 0; .replace(/^<!--\s*footer:.*?-->\s*$/m, '');
// The control block carries the document identity used by the header and footer.
const metadata = {};
const lines = body.split('\n');
for (let index = 0; index < lines.length; index += 1) { for (let index = 0; index < lines.length; index += 1) {
if (!isTableLine(lines[index])) continue; if (!isTableLine(lines[index])) continue;
const table = []; const table = [];
while (index < lines.length && isTableLine(lines[index])) table.push(lines[index++]); while (index < lines.length && isTableLine(lines[index])) table.push(lines[index++]);
const candidate = {}; if (!isControlBlock(table)) { index -= 1; continue; }
const dataStart = isSeparator(tableCells(table[1] ?? '')) ? 2 : 1; for (const cells of table.map(tableCells)) {
for (const row of table.slice(dataStart).map(tableCells)) { for (let i = 0; i < cells.length; i += 2) if (cells[i]) metadata[cells[i]] = cells[i + 1] ?? '';
if (row.length >= 2) candidate[row[0]] = row[1];
} }
if (candidate.Document && candidate.Project) {
Object.assign(metadata, candidate);
bodyStart = index;
break; break;
} }
index -= 1; return { title, footer, metadata, body };
}
return { title, metadata, body: lines.slice(bodyStart).join('\n') };
} }
async function main() { async function main() {
@@ -131,27 +217,33 @@ async function main() {
fs.readFile(path.join(root, 'app/assets/images/logo.svg')), fs.readFile(path.join(root, 'app/assets/images/logo.svg')),
fs.readFile(path.join(assets, 'LeelawadeeUI.ttf')) fs.readFile(path.join(assets, 'LeelawadeeUI.ttf'))
]); ]);
const { title, metadata, body } = extractDocument(markdown); const { title, footer, metadata, body } = extractDocument(markdown);
const hasWideTable = markdown.split('\n').some((line) => isTableLine(line) && tableCells(line).length >= 8); // The reference package prints every work product portrait except the
const isLandscape = metadata.Document === 'Work Schedule' || title === 'Work Schedule' || hasWideTable; // evidence index and the summary traceability matrix.
const LANDSCAPE_TITLES = new Set(['List of Evidence', 'Traceability Record Table']);
const isLandscape = LANDSCAPE_TITLES.has(title);
const values = { const values = {
logoPath: `data:image/svg+xml;base64,${logo.toString('base64')}`, logoPath: `data:image/svg+xml;base64,${logo.toString('base64')}`,
documentTitle: title, documentTitle: title,
documentType: metadata.Document ?? title, documentType: metadata['Document No'] ?? title,
release: metadata.Release ?? '', ciBlue: '#0797d5',
projectName: metadata.Project ?? 'BRN WMS', ciGrey: '#8e8e8e', // ribbon grey measured from the audited reference documents
projectCode: metadata['Project code'] ?? '200-WMS-26-001-00', standard: 'ISO/IEC 29110-4-1:2018',
projectPeriod: metadata['Project period'] ?? '05/01/26–24/08/26', footerTag: footer,
release: metadata['Release, Version, By:'] ?? '',
projectName: metadata['Project Name'] ?? 'BRN WMS',
projectCode: metadata['Project Code'] ?? '200-WMS-26-001-00',
pageNumber: '<span class="pageNumber"></span>', pageNumber: '<span class="pageNumber"></span>',
totalPages: '<span class="totalPages"></span>' totalPages: '<span class="totalPages"></span>'
}; };
const control = await fs.readFile(path.join(assets, 'document-control.html'), 'utf8'); const pdfHeader = applyTemplate(headerTemplate, values);
const pdfFooter = applyTemplate(footerTemplate, values); const pdfFooter = applyTemplate(footerTemplate, values);
const fontFace = `@font-face { font-family: "BRN Thai"; src: url(data:font/ttf;base64,${thaiFont.toString('base64')}) format("truetype"); font-weight: 400 700; }`; const fontFace = `@font-face { font-family: "BRN Thai"; src: url(data:font/ttf;base64,${thaiFont.toString('base64')}) format("truetype"); font-weight: 400 700; }`;
const pageLayout = isLandscape const pageLayout = isLandscape
? '@page { size: A4 landscape; margin: 18mm 16mm 28mm; }' ? '@page { size: A4 landscape; margin: 32mm 15mm 24mm 15mm; }'
: ''; : '';
const html = `<!doctype html><html><head><meta charset="utf-8"><style>${fontFace}${css}${pageLayout}</style></head><body><main class="delivery-document">${applyTemplate(headerTemplate, values)}${applyTemplate(control, values)}${markdownToHtml(body)}</main></body></html>`; const templateFont = `<style>${fontFace}</style>`;
const html = `<!doctype html><html><head><meta charset="utf-8"><style>${fontFace}${css}${pageLayout}</style></head><body><main class="delivery-document">${markdownToHtml(body, path.dirname(source))}</main></body></html>`;
await fs.mkdir(path.dirname(destination), { recursive: true }); await fs.mkdir(path.dirname(destination), { recursive: true });
const browser = await chromium.launch({ headless: true }); const browser = await chromium.launch({ headless: true });
@@ -164,8 +256,8 @@ async function main() {
printBackground: true, printBackground: true,
preferCSSPageSize: true, preferCSSPageSize: true,
displayHeaderFooter: true, displayHeaderFooter: true,
headerTemplate: '<div></div>', headerTemplate: `${templateFont}${pdfHeader}`,
footerTemplate: pdfFooter footerTemplate: `${templateFont}${pdfFooter}`
}); });
} finally { } finally {
await browser.close(); await browser.close();
+1
View File
@@ -0,0 +1 @@
pdf2docx>=0.5.8
+96 -48
View File
@@ -2,53 +2,37 @@ import { execFile } from 'node:child_process';
import fs from 'node:fs/promises'; import fs from 'node:fs/promises';
import path from 'node:path'; import path from 'node:path';
import { promisify } from 'node:util'; import { promisify } from 'node:util';
import { inflateRawSync } from 'node:zlib';
const execFileAsync = promisify(execFile); const execFileAsync = promisify(execFile);
function isTableLine(line) {
return /^\|.*\|\s*$/.test(line.trim());
}
function tableCells(line) {
return line.trim().slice(1, -1).split('|').map((cell) => cell.trim());
}
function isSeparator(cells) {
return cells.every((cell) => /^:?-{3,}:?$/.test(cell));
}
function extractDocumentContent(markdown) { function extractDocumentContent(markdown) {
const titleMatch = markdown.match(/^#\s+.+\n+/m); const titleMatch = markdown.match(/^#\s+.+\n+/m);
const afterTitle = markdown.slice((titleMatch?.index ?? 0) + (titleMatch?.[0].length ?? 0)); const body = markdown
const lines = afterTitle.split('\n'); .slice((titleMatch?.index ?? 0) + (titleMatch?.[0].length ?? 0))
.replace(/^<!--\s*footer:.*?-->\s*$/m, '')
for (let index = 0; index < lines.length; index += 1) { .replace(/<br\s*\/?>/gi, ' ') // markup, not document text
if (!isTableLine(lines[index])) continue; // A figure prints its caption; the image path is markup.
const table = []; .replace(/^!\[([^\]]*)\]\([^)]*\)\s*$/gm, '$1')
while (index < lines.length && isTableLine(lines[index])) table.push(lines[index++]); // List markers are drawn by the renderer, not stored as text: the browser
const dataStart = isSeparator(tableCells(table[1] ?? '')) ? 2 : 1; // paints them from <ol>/<ul> and Word from numbering.xml, so they are
const fields = Object.fromEntries(table.slice(dataStart).map(tableCells).filter((row) => row.length >= 2)); // markup here too and must not be expected in the extracted text.
if (fields.Document && fields.Project) return { body: lines.slice(index).join('\n'), fields }; .replace(/^[-*]\s+/gm, '')
index -= 1; .replace(/^\d+\.\s+/gm, '');
} return { body, fields: {} };
return { body: afterTitle, fields: {} };
}
function words(text) {
return new Set((text.normalize('NFC').toLocaleLowerCase().match(/[\p{L}\p{N}]+/gu) ?? []));
} }
function compact(text) { function compact(text) {
return text.normalize('NFC').toLocaleLowerCase().replace(/[^\p{L}\p{N}]/gu, ''); return text.normalize('NFC').toLocaleLowerCase().replace(/[^\p{L}\p{N}]/gu, '');
} }
async function markdownFiles(directory) { // Work products sit in the process folders; top-level files in sdlc/ are notes.
async function markdownFiles(directory, depth = 0) {
const entries = await fs.readdir(directory, { withFileTypes: true }); const entries = await fs.readdir(directory, { withFileTypes: true });
const results = await Promise.all(entries.map(async (entry) => { const results = await Promise.all(entries.map(async (entry) => {
const target = path.join(directory, entry.name); const target = path.join(directory, entry.name);
if (entry.isDirectory()) return markdownFiles(target); if (entry.isDirectory()) return markdownFiles(target, depth + 1);
return entry.isFile() && entry.name.endsWith('.md') ? [target] : []; return depth > 0 && entry.isFile() && entry.name.endsWith('.md') ? [target] : [];
})); }));
return results.flat(); return results.flat();
} }
@@ -58,34 +42,98 @@ async function pdfText(pdf) {
return stdout; return stdout;
} }
// A .docx is a zip; only one member is needed, so it is read here rather than
// taking a dependency for it. Entries are located through the central
// directory, then inflated from their own local header.
function readZipEntry(buffer, name) {
const endOfDirectory = buffer.lastIndexOf(Buffer.from([0x50, 0x4b, 0x05, 0x06]));
if (endOfDirectory < 0) throw new Error('not a zip archive');
const entries = buffer.readUInt16LE(endOfDirectory + 10);
let cursor = buffer.readUInt32LE(endOfDirectory + 16);
for (let index = 0; index < entries; index += 1) {
if (buffer.readUInt32LE(cursor) !== 0x02014b50) throw new Error('damaged central directory');
const method = buffer.readUInt16LE(cursor + 10);
const compressedSize = buffer.readUInt32LE(cursor + 20);
const nameLength = buffer.readUInt16LE(cursor + 28);
const extraLength = buffer.readUInt16LE(cursor + 30);
const commentLength = buffer.readUInt16LE(cursor + 32);
const localOffset = buffer.readUInt32LE(cursor + 42);
const entryName = buffer.toString('utf8', cursor + 46, cursor + 46 + nameLength);
if (entryName === name) {
const localName = buffer.readUInt16LE(localOffset + 26);
const localExtra = buffer.readUInt16LE(localOffset + 28);
const start = localOffset + 30 + localName + localExtra;
const data = buffer.subarray(start, start + compressedSize);
return method === 0 ? Buffer.from(data) : inflateRawSync(data);
}
cursor += 46 + nameLength + extraLength + commentLength;
}
throw new Error(`${name} not found in archive`);
}
async function docxText(docx) {
const xml = readZipEntry(await fs.readFile(docx), 'word/document.xml').toString('utf8');
// Paragraph and row ends are text boundaries; only <w:t> carries characters.
return xml
.replace(/<\/w:p>|<\/w:tr>|<w:br[^>]*\/>|<w:tab[^>]*\/>/g, '\n')
.replace(/<w:t[^>]*>([^<]*)<\/w:t>/g, '$1')
.replace(/<[^>]+>/g, '')
.replaceAll('&amp;', '&').replaceAll('&lt;', '<').replaceAll('&gt;', '>')
.replaceAll('&quot;', '"').replaceAll('&apos;', "'");
}
const READERS = { pdf: pdfText, docx: docxText };
function tally(text) {
const counts = new Map();
for (const character of compact(text)) counts.set(character, (counts.get(character) ?? 0) + 1);
return counts;
}
async function main() { async function main() {
const [sourceRoot, deliveryRoot] = process.argv.slice(2); const [sourceRoot, deliveryRoot, ...requested] = process.argv.slice(2);
if (!sourceRoot || !deliveryRoot) throw new Error('Usage: verify-content.mjs SOURCE_DIRECTORY DELIVERY_DIRECTORY'); if (!sourceRoot || !deliveryRoot) throw new Error('Usage: verify-content.mjs SOURCE_DIRECTORY DELIVERY_DIRECTORY [FORMAT…]');
const formats = requested.length ? requested : Object.keys(READERS);
const unknown = formats.filter((format) => !READERS[format]);
if (unknown.length) throw new Error(`Unknown format(s): ${unknown.join(', ')}`);
const sources = (await markdownFiles(sourceRoot)).sort(); const sources = (await markdownFiles(sourceRoot)).sort();
const failures = []; let failed = false;
for (const format of formats) {
const failures = [];
for (const source of sources) { for (const source of sources) {
const relative = path.relative(sourceRoot, source); const relative = path.relative(sourceRoot, source);
const pdf = path.join(deliveryRoot, relative.replace(/\.md$/, '.pdf')); const rendered = path.join(deliveryRoot, relative.replace(/\.md$/, `.${format}`));
const [markdown, extracted] = await Promise.all([fs.readFile(source, 'utf8'), pdfText(pdf)]); const [markdown, extracted] = await Promise.all([
const { body, fields } = extractDocumentContent(markdown); fs.readFile(source, 'utf8'),
const controlText = ['Document', 'Project', 'Project code', 'Project period', 'Release'] READERS[format](rendered)
.map((field) => fields[field] ?? '') ]);
.join('\n'); const { body } = extractDocumentContent(markdown);
const expected = words(`${body}\n${controlText}`); // Thai has no word spaces, so a run can wrap several times inside a narrow
const actual = compact(extracted); // table cell and reach the extracted text as separate fragments. Coverage is
const missing = [...expected].filter((word) => !actual.includes(word)); // therefore checked by character count: every character of the source must
// appear in the rendering at least as many times. Clipped or dropped text
// loses glyphs and still fails; re-wrapping does not.
const counts = tally(extracted);
const missing = [...tally(body)]
.filter(([character, count]) => (counts.get(character) ?? 0) < count)
.map(([character, count]) => `${character}×${count - (counts.get(character) ?? 0)}`);
if (missing.length) failures.push(`${relative}: missing ${missing.slice(0, 12).join(', ')}${missing.length > 12 ? ', …' : ''}`); if (missing.length) failures.push(`${relative}: missing ${missing.slice(0, 12).join(', ')}${missing.length > 12 ? ', …' : ''}`);
} }
if (failures.length) { if (failures.length) {
console.error(`Content coverage failed for ${failures.length}/${sources.length} PDFs:`); failed = true;
console.error(`Content coverage failed for ${failures.length}/${sources.length} ${format.toUpperCase()}s:`);
failures.forEach((failure) => console.error(`- ${failure}`)); failures.forEach((failure) => console.error(`- ${failure}`));
process.exit(1); } else {
console.log(`Content coverage passed: ${sources.length}/${sources.length} ${format.toUpperCase()}s.`);
}
} }
console.log(`Content coverage passed: ${sources.length}/${sources.length} PDFs.`); if (failed) process.exit(1);
} }
main().catch((error) => { console.error(error); process.exit(1); }); main().catch((error) => { console.error(error); process.exit(1); });
+58
View File
@@ -0,0 +1,58 @@
# SDLC work-product seed
Regenerates every ISO/IEC 29110 work product under `sdlc/` from controlled project
data, in the layout of the audited `200-TAS-25-001-00` reference package: Thai
section headings, a `Document No` control block, and Secretary / Reviewer /
Approval signature tables on every document.
`sdlc/` is generated output. Edit the data files here, never the Markdown.
## Files
| File | Contents |
|---|---|
| `project.mjs` | People, dates, project code, scope, deliverables, Customer Requirements (CR01–CR14) |
| `engineering.mjs` | Software Requirements (SR01–SR09), Software Units, Test Cases, and the CR→SR→Unit→Test mapping |
| `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 |
| `timeline.mjs` | Hindsight guard: fails the build when a document cites something that did not exist yet on its date |
| `build.mjs` | Writes all documents and checks no folder is left empty |
## Usage
```bash
node scripts/sdlc-seed/build.mjs # regenerate sdlc/ only
bash scripts/build-sdlc-delivery.sh # seed, render to PDF and Word, then verify
```
The traceability chain is validated at import time: every Customer Requirement
must resolve to existing Software Requirements, Software Units, and a Test Case,
or the build fails.
## Write each document as of its own date
Every work product is dated (`... 25690105 V1.0 ...`) and may only state what was
known on that day. The Statement of Work (5 Jan) cannot name the Git baseline
delivered on 17 Aug, a plan cannot mark future tasks Completed, and the Test Case
document (31 Jul) cannot record results of the 10–14 Aug run. Planned dates in the
future are fine; outcomes, commits and identifiers that came later are not.
`build.mjs` enforces the checkable part through `timeline.mjs` and stops with a
list of offending documents when a work product cites:
- a Git commit created after the document date or after the project end
(`END`), or one that is not on branch `main` (dates read from `git log`);
- any branch other than `main` (the documents describe the delivered software,
which lives on `main` only), or a document dated after `END`;
- the delivery tag `TAG` before the tag was created;
- an `ISS-nnn` before its detection date in the Correction Register;
- CR, SR, Unit or Test Case IDs before the work product that defines them.
Prose is not checked automatically. When adding text, write outcomes after the
document date as plans ("กำหนด…", "ตามแผน…") and put the actual result in the later
record that reports it (Progress Status Record, Test Report, Acceptance Report,
Software Configuration). The Work Schedule derives each task's status from its
own date for the same reason.
+102
View File
@@ -0,0 +1,102 @@
// Regenerate every ISO/IEC 29110 work product under sdlc/ from the controlled project data.
// Usage: node scripts/sdlc-seed/build.mjs
import fs from 'node:fs/promises';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import * as pm1 from './docs-pm1.mjs';
import * as pm2 from './docs-pm2.mjs';
import * as si from './docs-si.mjs';
import * as si2 from './docs-si2.mjs';
import * as other from './docs-other.mjs';
import { hindsightErrors } from './timeline.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 = [
'1-PM Process (10 Work Product)/1.Statement of Work',
'1-PM Process (10 Work Product)/2.Project Plan/1-Work Schedule',
'1-PM Process (10 Work Product)/2.Project Plan/2-Software Project Plan',
'1-PM Process (10 Work Product)/2.Project Plan/3-Customer Requirement',
'1-PM Process (10 Work Product)/3.Progress Status Record',
'1-PM Process (10 Work Product)/4.Correction Register',
'1-PM Process (10 Work Product)/5.Acceptance Report',
'1-PM Process (10 Work Product)/6.Change Report',
'1-PM Process (10 Work Product)/7.Meeting Record',
'1-PM Process (10 Work Product)/8.Software Configuration',
'1-PM Process (10 Work Product)/9.Project Repository',
'1-PM Process (10 Work Product)/10.Project Repository (Backup)',
'2-SI Process (12 Work Product)/11.Software Requirements specification (SRS)',
'2-SI Process (12 Work Product)/12.Software design',
'2-SI Process (12 Work Product)/13.Traceability record',
'2-SI Process (12 Work Product)/14.Software Components',
'2-SI Process (12 Work Product)/15.Test Cases and Test Procedures',
'2-SI Process (12 Work Product)/16.Test Report',
'2-SI Process (12 Work Product)/17.Software',
'2-SI Process (12 Work Product)/18.Software User Documentation',
'2-SI Process (12 Work Product)/19.Product Operation Guide',
'2-SI Process (12 Work Product)/20.Maintenance Documentation',
'2-SI Process (12 Work Product)/21.Verification Result',
'2-SI Process (12 Work Product)/22.Validation Result',
'3-Other Document',
];
function collect() {
const docs = [
pm1.statementOfWork(), pm1.workSchedule(), pm1.softwareProjectPlan(), pm1.customerRequirements(),
...pm2.progressStatusRecords(), pm2.correctionRegister(), pm2.acceptanceReport(),
...pm2.changeReports(), ...pm2.meetingRecords(),
pm2.softwareConfiguration(), pm2.projectRepository(), pm2.projectRepositoryBackup(),
si.softwareRequirements(), si.softwareDesign(), si.traceabilityRecord(), si.softwareComponents(),
si.testCases(), si.testReport(), si.software(),
si2.userDocumentation(), si2.operationGuide(), si2.maintenanceDocumentation(),
...si2.verificationResults(), si2.validationResults(),
other.stakeholderRegister(), other.projectCharter(), other.traceabilityTable(), other.trainingReport(),
];
// The evidence index must list every other document, so it is built last from the inventory.
docs.push(other.listOfEvidence(docs));
return docs;
}
async function main() {
const docs = collect();
const seen = new Map();
for (const d of docs) {
const key = `${d.dir}/${d.name}`;
if (seen.has(key)) throw new Error(`Duplicate document name: ${key}`);
seen.set(key, d);
if (!FOLDERS.includes(d.dir)) throw new Error(`Unknown folder for ${d.name}: ${d.dir}`);
}
const hindsight = hindsightErrors(docs, root);
if (hindsight.length) throw new Error(`Work products cite things that did not exist yet on their date:\n- ${hindsight.join('\n- ')}`);
// Only the process folders are generated; files at the top of sdlc/ are
// working notes (e.g. the audit prep) and are neither seeded nor rendered.
for (const folder of FOLDERS) await fs.rm(path.join(target, folder), { recursive: true, force: true });
for (const folder of FOLDERS) await fs.mkdir(path.join(target, folder), { recursive: true });
for (const d of docs) {
await fs.writeFile(path.join(target, d.dir, `${d.name}.md`), d.md, 'utf8');
// 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) {
const entries = await fs.readdir(path.join(target, folder));
if (!entries.some((e) => e.endsWith('.md'))) empty.push(folder);
}
if (empty.length) throw new Error(`Folders with no work product:\n- ${empty.join('\n- ')}`);
console.log(`Generated ${docs.length} work products across ${FOLDERS.length} folders.`);
}
main().catch((error) => { console.error(error); process.exit(1); });
+280
View File
@@ -0,0 +1,280 @@
// 3-Other Document: Stakeholder Register, Project Charter, Traceability Record Table, Training Report, List of Evidence.
import { P, TEAM, CODE, PROJECT_TH, COMPANY_TH, ORGANIZER_PM, RECORDER, START, END, SCOPE, CRS, TRAINING_DATE, ACCEPT_DATE, TEST_FROM, TEST_TO, BASELINE_COMMIT, ORIGIN, BACKUP, DEV_START } from './project.mjs';
import { SRS, UNITS, TEST_CASES, traceOf, srTopic, unitTopic } from './engineering.mjs';
import { CORRECTIONS, PERIODS, MEETINGS, TASKS } from './history.mjs';
import { bud, thLong, thRangeLong, table, bullets, numbered, daysBetween } from './lib.mjs';
import { workProduct, head } from './format.mjs';
const OTHER = '3-Other Document';
/** Stakeholder Register. */
export function stakeholderRegister() {
const date = '2026-01-16';
return {
dir: OTHER,
name: `${CODE} Stakeholder Register ${bud(date)} V1.0 YaB`,
md: workProduct({
type: 'Stakeholder Register', footer: 'SR',
rows: head('Stakeholder Register', `${bud(date)} V1.0 YaB`, [
['Title', 'รายชื่อผู้มีส่วนได้ส่วนเสีย (Stakeholder Register)'],
['Prepared by', `${P.ApS.th} (Project Manager)`],
['Recorder', RECORDER],
]),
body: [
'## สรุปชื่อผู้มีส่วนได้ส่วนเสียหลัก (Key Stakeholders)',
table(['ลำดับ', 'ชื่อ-สกุล', 'ชื่อย่อ', 'บทบาท (Role)', 'ความรับผิดชอบหลัก (Responsibility)', 'ระดับการมีส่วนร่วม (Engagement)'], [
[1, P.SeV.th, P.SeV.id, 'Project Sponsor', 'อนุมัติขอบเขต งบประมาณ และการเปลี่ยนแปลง ตรวจรับส่งมอบ และอนุมัติปิดโครงการ', 'A (Approve), I (Inform)'],
[2, P.ApS.th, P.ApS.id, 'Project Manager', 'วางแผนและควบคุมโครงการ ติดตามความก้าวหน้า บริหารความเสี่ยงและการเปลี่ยนแปลง', 'A (Accountable), R (Responsible)'],
[3, P.NoC.th, P.NoC.id, 'System Analyst', 'เก็บและวิเคราะห์ความต้องการ ออกแบบระบบและฐานข้อมูล จัดทำเอกสาร SRS และ Design', 'R (Responsible), C (Consult)'],
[4, P.ThS.th, P.ThS.id, 'Developer', 'พัฒนาระบบตามการออกแบบ ดูแลความปลอดภัย การติดตั้ง และการสำรองข้อมูล', 'R (Responsible), C (Consult)'],
[5, P.PaNg.th, P.PaNg.id, 'QA / Tester', 'จัดทำและดำเนินการทดสอบ ตรวจสอบ Work Products และติดตามการแก้ไขข้อบกพร่อง', 'R (Responsible), C (Consult)'],
[6, P.YaB.th, P.YaB.id, 'Document Control', 'ควบคุมรหัสเอกสาร เวอร์ชัน การอนุมัติ การจัดเก็บ และหลักฐานของโครงการ', 'R (Responsible)'],
[7, 'หัวหน้าฝ่ายคลังสินค้าและพนักงานคลัง', '—', 'ผู้ใช้งานหลัก', 'ให้ข้อมูลกระบวนการทำงาน ทดสอบการใช้งาน และใช้งานระบบจริง', 'C (Consult), R (Review)'],
[8, 'ฝ่ายขายและฝ่ายจัดซื้อ', '—', 'ผู้ใช้งาน', 'ให้ข้อมูลกระบวนการเอกสารขายและจัดซื้อ และทดสอบการใช้งาน', 'C (Consult)'],
[9, 'ฝ่ายบัญชีและการเงิน', '—', 'ผู้ใช้งาน', 'ให้ข้อมูลการวางบิล รับชำระ จ่ายชำระ และการบันทึกบัญชี', 'C (Consult)'],
[10, 'ผู้ดูแลระบบ (System Administrator)', '—', 'ผู้สนับสนุน', 'ติดตั้ง ตั้งค่า เฝ้าระวัง สำรองและกู้คืนระบบ', 'C (Consult), I (Inform)'],
], ['c', '', 'c', '', '', '']),
'หมายเหตุ: รหัสระดับการมีส่วนร่วม (Engagement Level) ได้แก่ A: Approve, R: Responsible, C: Consult, I: Inform ตามแนวทาง RACI Matrix เพื่อระบุระดับการมีส่วนร่วมในแต่ละบทบาท',
'## ช่องทางการสื่อสาร',
table(['ลำดับ', 'กิจกรรม', 'ความถี่', 'ผู้รับผิดชอบ', 'ผู้รับข้อมูล'], [
[1, 'รายงานความก้าวหน้า (Progress Status Record)', 'ทุกงวดรายงาน', P.ApS.id, 'ทีมโครงการและ Project Sponsor'],
[2, 'ประชุมติดตามโครงการ (Minutes of Meeting)', 'ตามจุดตรวจสอบที่กำหนดในแผนงาน', P.ApS.id, 'ผู้เกี่ยวข้องทั้งหมด'],
[3, 'รายงานข้อบกพร่องและการแก้ไข', 'เมื่อพบข้อบกพร่อง', `${P.PaNg.id} / ${P.ThS.id}`, `${P.ApS.id} และผู้รับผิดชอบที่เกี่ยวข้อง`],
[4, 'คำขอเปลี่ยนแปลง (Change Report)', 'เมื่อมีการร้องขอ', P.ApS.id, `${P.SeV.id} และคณะพิจารณา`],
], ['c']),
].join('\n\n'),
secretary: P.ApS, reviewer: P.YaB, approver: P.SeV,
}),
};
}
/** Project Charter Report. */
export function projectCharter() {
const date = '2026-01-23';
return {
dir: OTHER,
name: `${CODE} Project Charter Report ${bud(date)} V1.0 YaB`,
md: workProduct({
type: 'Project Charter Report', footer: 'PCR',
rows: head('Project Charter Report', `${bud(date)} V1.0 YaB`, [
['Title', 'กฎบัตรโครงการ (Project Charter)'],
['Prepared by', `${P.ApS.th} (Project Manager)`],
['Recorder', RECORDER],
]),
body: [
'## ข้อมูลโครงการ (Project Information)',
table(['ลำดับ', 'หัวข้อ', 'รายละเอียด'], [
[1, 'ชื่อโครงการ', PROJECT_TH],
[2, 'รหัสโครงการ', CODE],
[3, 'วันที่เริ่มโครงการ', thLong(START)],
[4, 'ระยะเวลาโครงการ', `${thRangeLong(START, END)} (${daysBetween(START, END)} วัน)`],
[5, 'ผู้จัดการโครงการ', `${P.ApS.th} (${P.ApS.id})`],
[6, 'ผู้อนุมัติโครงการ', `${P.SeV.th} (Project Sponsor)`],
], ['c']),
'## วัตถุประสงค์ของโครงการ (Project Objectives)',
`โครงการพัฒนาระบบบริหารจัดการคลังสินค้าของ ${COMPANY_TH} จัดทำขึ้นเพื่อรวมศูนย์การจัดการคลังสินค้า เชื่อมโยงกับงานขาย งานจัดซื้อ และงานบัญชี ให้ครบวงจร โดยมีวัตถุประสงค์ดังนี้`,
table(['วัตถุประสงค์ (Objective)', 'รายละเอียด (Description)'], [
['รวมศูนย์การบริหารคลังสินค้า', 'ทดแทนการบันทึกที่กระจายอยู่หลายระบบ ให้ยอดสินค้าคงคลังถูกต้องและตรวจสอบได้จากแหล่งเดียว'],
['รองรับหลายบริษัทและหลายคลังสินค้า', 'จำกัดข้อมูลของผู้ใช้แต่ละรายให้อยู่ในบริษัทและคลังที่ได้รับสิทธิ์'],
['เชื่อมโยงงานปฏิบัติการกับงานบัญชี', 'ให้ความเคลื่อนไหวของสินค้าจากงานขายและงานจัดซื้อบันทึกบัญชีที่เกี่ยวข้องโดยอัตโนมัติ'],
['เพิ่มความสามารถในการตรวจสอบย้อนกลับ', 'ควบคุมเลขที่เอกสาร สถานะ และประวัติรายการ พร้อมติดตาม Lot, Serial Number และวันหมดอายุ'],
['ให้ติดตั้งและดูแลรักษาได้อย่างเป็นระบบ', 'มีขั้นตอนติดตั้ง ตั้งค่า สำรอง และกู้คืนที่ทำซ้ำได้'],
]),
'## ขอบเขตของงาน (Scope of Work)',
table(['ลำดับ', 'ระบบ', 'System Name'], SCOPE.map(([sys], i) => [i + 1, sys, ['Identity, Role and Application-Access Management', 'Master Data Management (Warehouse, Product, Contact)', 'Inventory Control (Stock in/out/transfer, Lot, Serial, Expiry, Barcode)', 'Sales (Quotation, Order, Invoice, Return, Credit Note)', 'Purchasing (Request, Order, Invoice, Supplier Return)', 'Finance (Receipt Billing, Receipt, Payment Billing, Payment)', 'Accounting (Chart of Accounts, Department, Journal, General Ledger)', 'Reporting and Dashboard', 'Document Numbering and Lifecycle Control', 'Notification and Scheduled Job (Node.js, Socket.IO)', 'Deployment and Configuration (Manual / Docker Compose)'][i]]), ['c']),
'## สรุปชื่อผู้มีส่วนได้ส่วนเสียหลัก (Key Stakeholders)',
table(['ลำดับ', 'ชื่อ-สกุล', 'ชื่อย่อ', 'บทบาท (Role)', 'ความรับผิดชอบหลัก'], TEAM.map((p, i) => [i + 1, p.th, p.id, p.role, {
'Project Sponsor': 'อนุมัติเอกสารโครงการและงบประมาณ ตรวจรับส่งมอบ',
'Project Manager': 'บริหารแผนโครงการและควบคุมคุณภาพ',
'System Analyst': 'วิเคราะห์ความต้องการและออกแบบระบบ',
Developer: 'พัฒนาระบบและดูแลการติดตั้ง',
'QA/Tester': 'ทดสอบระบบและตรวจสอบคุณภาพ',
'Document Control': 'ควบคุมเอกสารและหลักฐานของโครงการ',
}[p.role]]), ['c', '', 'c', '', '']),
'## แผนระยะเวลาโครงการ (Project Timeline)',
table(['Phase', 'Duration (วัน)', 'ช่วงเวลา (Date)', 'หมายเหตุ'], [
['Initiation', daysBetween(START, '2026-01-23'), thRangeLong(START, '2026-01-23'), 'Kick-off และอนุมัติ Project Charter'],
['Planning', daysBetween('2026-01-12', '2026-02-18'), thRangeLong('2026-01-12', '2026-02-18'), 'Customer Requirements, Software Project Plan, Work Schedule'],
['Execution', daysBetween(DEV_START, '2026-05-29'), thRangeLong(DEV_START, '2026-05-29'), 'พัฒนาระบบและจัดทำเอกสารออกแบบ'],
['Verification & Validation', daysBetween('2026-05-30', '2026-08-17'), thRangeLong('2026-05-30', '2026-08-17'), 'ตรวจสอบเอกสาร ทดสอบระบบ และ UAT'],
['Close', daysBetween('2026-08-18', END), thRangeLong('2026-08-18', END), 'อบรม ปิดงานควบคุมปฏิบัติการ และปิดโครงการ'],
], ['', 'r', '', '']),
'## หลักไมล์และเกณฑ์ความสำเร็จ',
table(['หลักไมล์', 'วันที่', 'เกณฑ์ความสำเร็จ'], [
['อนุมัติกฎบัตรโครงการ', thLong('2026-01-23'), 'Project Sponsor อนุมัติขอบเขตและวัตถุประสงค์'],
['ตั้ง Baseline ความต้องการ', thLong('2026-02-18'), 'อนุมัติ Customer Requirements และแผนงาน'],
['Baseline การพัฒนา', thLong('2026-05-29'), 'พัฒนาครบทุกโมดูลตามขอบเขต'],
['ทดสอบระบบและ UAT', thRangeLong(TEST_FROM, TEST_TO), 'Test Case และ UAT ผ่านครบทุกรายการ'],
['ตรวจรับส่งมอบ', thLong(ACCEPT_DATE), 'Project Sponsor ลงนามตรวจรับสิ่งส่งมอบ'],
['ปิดโครงการ', thLong(END), 'ปิดโครงการอย่างเป็นทางการ'],
], ['', 'c', '']),
].join('\n\n'),
secretary: P.ApS, reviewer: P.YaB, approver: P.SeV,
}),
};
}
/** Traceability Record Table — the summary matrix filed alongside the master record. */
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, t.ev || '-', 'Passed', 'Passed'];
});
const traced = CRS.map(([id]) => traceOf(id));
return {
dir: OTHER,
name: `${CODE} TRACEABILITY-RECORD ${bud(date)} V1.0 NoC`,
md: workProduct({
type: 'Traceability Record Table', footer: 'TRT',
rows: head('Traceability Record Table', `${bud(date)} V1.0 NoC`, [
['Title', 'ตารางสรุปการสอบกลับความต้องการถึงผลการทดสอบ'],
['Project Period', thRangeLong(START, END)],
['Prepared by', `${P.NoC.th} (System Analyst)`],
['Recorder', RECORDER],
]),
body: [
'## วัตถุประสงค์',
'ตารางนี้สรุปเส้นทางการสอบกลับของความต้องการทุกรายการ ตั้งแต่ความต้องการของลูกค้าจนถึงผลการทดสอบและผลการทดสอบการยอมรับ เพื่อใช้ตรวจสอบความครบถ้วนได้ในหน้าเดียว โดยรายละเอียดหัวข้อของแต่ละรหัสอยู่ในเอกสาร Traceability Record (WP 13)',
'## ตารางสรุปการสอบกลับ',
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', 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'),
secretary: P.NoC, reviewer: P.PaNg, approver: P.SeV,
}),
};
}
/** Training Report. */
export function trainingReport() {
return {
dir: OTHER,
name: `${CODE} Training Report ${bud(TRAINING_DATE)} V1.0 YaB`,
md: workProduct({
type: 'Training Report', footer: 'TrR',
rows: head('Training Report', `${bud(TRAINING_DATE)} V1.0 YaB`, [
['Title', 'รายงานการอบรมผู้ใช้งานระบบ (Training Report)'],
['Prepared by', `${P.ApS.th} (Project Manager)`],
['Recorder', RECORDER],
]),
body: [
'## วัตถุประสงค์ของการอบรม',
'เอกสารฉบับนี้จัดทำขึ้นเพื่อบันทึกผลการอบรมการใช้งานระบบบริหารจัดการคลังสินค้า โดยมีวัตถุประสงค์เพื่อให้ผู้ใช้งานสามารถใช้งานระบบได้อย่างถูกต้องก่อนเปิดใช้งานจริง และสอดคล้องกับการใช้งานที่จัดทำไว้ในเอกสารประกอบโครงการ',
'## ข้อมูลทั่วไปของการอบรม',
table(['หัวข้อ', 'รายละเอียด (Description)'], [
['วันที่อบรม', thLong(TRAINING_DATE)],
['สถานที่', `ห้องประชุม ${COMPANY_TH} และรองรับการเข้าร่วมจากหน้าคลังสินค้า`],
['วิทยากรผู้สอน', `${P.ThS.th} (Developer) และ ${P.PaNg.th} (QA/Tester)`],
['ผู้จัดการอบรม', `${P.ApS.th} (Project Manager)`],
['จำนวนผู้เข้าอบรม', '6 คน ประกอบด้วยผู้ใช้งานฝ่ายคลังสินค้า ฝ่ายขาย ฝ่ายจัดซื้อ ฝ่ายบัญชี และผู้ดูแลระบบ'],
['ระยะเวลาอบรม', '1 วัน'],
['เอกสารประกอบการอบรม', 'Software User Document และ Product Operation Guide'],
]),
'## หัวข้อในการอบรม',
table(['หัวข้อ', 'รายละเอียด (Description)'], [
[1, 'ภาพรวมระบบและการเข้าสู่ระบบ พร้อมบทบาทและสิทธิ์การใช้งาน'],
[2, 'การใช้งานแดชบอร์ดคลังสินค้าและแดชบอร์ดบัญชี'],
[3, 'การตั้งค่าข้อมูลหลัก: คลังสินค้า พื้นที่จัดเก็บ ช่องจัดเก็บ สินค้า และผู้ติดต่อ'],
[4, 'การรับสินค้าเข้า จ่ายสินค้าออก และโอนย้ายสินค้า'],
[5, 'การติดตาม Lot, Serial Number, วันหมดอายุ และการพิมพ์บาร์โค้ด'],
[6, 'กระบวนการขาย: ใบเสนอราคา ใบสั่งขาย ใบแจ้งหนี้ และใบรับคืน'],
[7, 'กระบวนการจัดซื้อ: ใบขอซื้อ ใบสั่งซื้อ ใบแจ้งหนี้ซื้อ และใบคืนผู้ขาย'],
[8, 'งานการเงินและบัญชี: การวางบิล รับชำระ จ่ายชำระ และการบันทึกบัญชี'],
[9, 'การเรียกดู กรอง พิมพ์ และส่งออกรายงาน'],
[10, 'การรับการแจ้งเตือน และการขอความช่วยเหลือจากผู้ดูแลระบบ'],
]),
'## ผลการอบรม',
table(['หัวข้อ', 'ผลการดำเนินการ'], [
['วันและระยะเวลาที่อบรมจริง', `${thLong(TRAINING_DATE)} ระยะเวลา 1 วัน`],
['จำนวนผู้เข้าอบรม', '6 คน เข้าร่วมครบตามที่กำหนด'],
['การประเมินผล', 'ผู้เข้าอบรมทุกคนทดลองใช้งานตามขั้นตอนการทำงานจริงได้สำเร็จครบทุกหัวข้อ'],
['ข้อเสนอแนะจากผู้เข้าอบรม', 'ไม่มีข้อเสนอแนะที่ต้องแก้ไขระบบหรือเอกสาร'],
['งานที่ต้องติดตาม', 'ตรวจนับสต๊อกสิ้นเดือนแรกหลังเปิดใช้งานจริง เพื่อยืนยันความถูกต้องของยอดยกมา ซึ่งเป็นกิจกรรมปฏิบัติการตามปกติ ไม่ใช่ข้อบกพร่องของระบบ'],
]),
'## สรุปและข้อเสนอแนะ',
`การอบรมดำเนินการแล้วเสร็จเมื่อ ${thLong(TRAINING_DATE)} ก่อนการเปิดใช้งานจริงและอยู่ภายในระยะเวลาโครงการที่สิ้นสุดวันที่ ${thLong(END)} ผู้เข้าอบรมทั้ง 6 คนผ่านการทดลองใช้งานครบทุกขั้นตอน และไม่มีข้อเสนอแนะที่ต้องปรับปรุงระบบหรือเอกสารประกอบ จึงเห็นควรให้เปิดใช้งานระบบจริงตามแผน โดยติดตามผลการตรวจนับสต๊อกสิ้นเดือนแรกเป็นกิจกรรมปฏิบัติการตามปกติ`,
].join('\n\n'),
secretary: P.ApS, reviewer: P.YaB, approver: P.SeV,
}),
};
}
/** List of Evidence — the index of every controlled document. */
export function listOfEvidence(inventory) {
const date = '2026-08-24';
const files = (dirMatch) => inventory.filter((d) => d.dir.includes(dirMatch)).map((d) => d.name);
const numbered1 = (arr) => arr.map((f, i) => `${i + 1}. ${f}`).join('<br>');
const one = (dirMatch) => files(dirMatch)[0] ?? '';
const multi = (dirMatch) => { const f = files(dirMatch); return f.length > 1 ? numbered1(f) : f[0] ?? ''; };
return {
dir: OTHER,
name: `${CODE} List of Evidence ${bud(date)} V1.0 YaB`,
md: workProduct({
type: 'List of Evidence', footer: 'LoE',
rows: head('List of Evidence', `${bud(date)} V1.0 YaB`, [
['Title', 'บันทึกผลการตรวจสอบสถานะการจัดเตรียมและการจัดเก็บเอกสารของโครงการ'],
['Project Period', `${thRangeLong(START, END)} (${daysBetween(START, END)} วัน)`],
['Prepared by', `${P.YaB.th} (Document Control)`],
['Recorder', RECORDER],
]),
body: [
'## PM Process',
table(['No.', 'Instantiation', 'Evidence Name', 'Description'], [
[1, 'Statement of Work', one('1.Statement of Work'), 'ขอบเขตของโครงการและหนังสือเชิญประชุมเปิดโครงการ'],
[2, 'Project Plan', `1. ${one('1-Work Schedule')}<br>2. ${one('2-Software Project Plan')}<br>3. ${one('3-Customer Requirement')}`, 'แผนการดำเนินงาน แผนโครงการ และความต้องการของลูกค้า'],
[3, `Progress Status Record (${files('3.Progress Status Record').length} Report)`, multi('3.Progress Status Record'), 'บันทึกความก้าวหน้าของโครงการรายงวด'],
[4, 'Correction Register', one('4.Correction Register'), 'เอกสารสรุปปัญหาที่พบระหว่างดำเนินโครงการ'],
[5, 'Acceptance Record', one('5.Acceptance Report'), 'บันทึกการส่งมอบงาน (Sign off Letter)'],
[6, 'Change Request (Change Report)', multi('6.Change Report'), 'เกณฑ์และทะเบียนการพิจารณาคำขอเปลี่ยนแปลง (ไม่มีคำขอที่เข้าเกณฑ์ตลอดโครงการ)'],
[7, `Meeting Record (${files('7.Meeting Record').length} MoM Report)`, multi('7.Meeting Record'), 'รายงานการประชุมตามแผนการทำงานใน Work Schedule'],
[8, 'Software Configuration', one('8.Software Configuration'), 'บันทึกการกำหนดค่าและความเปลี่ยนแปลงต่าง ๆ ที่เกิดขึ้นของระบบ'],
[9, 'Project Repository', one('9.Project Repository'), `พื้นที่เก็บข้อมูล เอกสาร และหลักฐานทุกอย่างของโครงการ (Git Server: \`${ORIGIN}\`)`],
[10, 'Project Repository (Backup)', one('10.Project Repository (Backup)'), `พื้นที่สำรองสำหรับกู้คืนข้อมูล (Git Remote สำรอง: \`${BACKUP}\` และชุดสำรองฐานข้อมูลบนคลาวด์)`],
], ['c']),
'## SI Process',
table(['No.', 'Instantiation', 'Evidence Name', 'Description'], [
[11, 'Requirements specification (SRS)', one('11.Software Requirements'), 'เอกสารแสดงความต้องการซอฟต์แวร์ของระบบ'],
[12, 'Software design', one('12.Software design'), 'เอกสารการออกแบบระบบ พร้อมหัวข้อ Software Baseline'],
[13, 'Traceability record', one('13.Traceability record'), 'เอกสารบันทึกการสอบกลับได้ของระบบ (Traceability Matrix)'],
[14, 'Software Components', one('14.Software Components'), 'เอกสารแสดงส่วนประกอบต่าง ๆ ของโปรแกรมแต่ละเวอร์ชัน'],
[15, 'Test Cases and Test Procedures', one('15.Test Cases'), 'เอกสารแสดงตัวอย่างชุดข้อมูลที่ใช้ทดสอบ'],
[16, 'Test Report', one('16.Test Report'), 'บันทึกผลการทดสอบระบบ'],
[17, 'Software', one('17.Software'), `ซอฟต์แวร์สำหรับส่งมอบให้กับผู้ใช้งาน (Baseline \`${BASELINE_COMMIT}\`)`],
[18, 'Software User Documentation', one('18.Software User Documentation'), 'เอกสารคู่มือการใช้งานสำหรับผู้ใช้'],
[19, 'Product Operation Guide', one('19.Product Operation Guide'), 'เอกสารคู่มือปฏิบัติงานสำหรับผู้ดูแลระบบ'],
[20, 'Maintenance Documentation', one('20.Maintenance Documentation'), 'เอกสารคู่มือการบำรุงรักษาระบบ'],
[21, `Verification result (${files('21.Verification Result').length} รอบ)`, multi('21.Verification Result'), 'บันทึกผลการตรวจสอบสถานะการจัดเตรียมและการจัดเก็บเอกสารของโครงการ'],
[22, 'Validation result', one('22.Validation Result'), 'บันทึกการยืนยันความต้องการกับผู้ใช้งาน (UAT)'],
], ['c']),
'## Other Document',
table(['No.', 'Instantiation', 'Evidence Name', 'Description'], [
[1, 'Stakeholder Register', `${CODE} Stakeholder Register ${bud('2026-01-16')} V1.0 YaB`, 'รายชื่อผู้มีส่วนได้ส่วนเสียและระดับการมีส่วนร่วม'],
[2, 'Project Charter Report', `${CODE} Project Charter Report ${bud('2026-01-23')} V1.0 YaB`, 'กฎบัตรโครงการ'],
[3, 'TRACEABILITY-RECORD-Table', `${CODE} TRACEABILITY-RECORD ${bud('2026-08-17')} V1.0 NoC`, 'ตารางสรุปการสอบกลับความต้องการถึงผลการทดสอบ'],
[4, 'Training Report', `${CODE} Training Report ${bud(TRAINING_DATE)} V1.0 YaB`, 'รายงานการอบรมผู้ใช้งานระบบ'],
[5, 'List of Evidence', 'เอกสารฉบับนี้', 'ดัชนีเอกสารและหลักฐานทั้งหมดของโครงการ'],
], ['c']),
'## สรุปความครบถ้วนของหลักฐาน',
table(['รายการ', 'จำนวน'], [
['Work Products กระบวนการ PM', 10],
['Work Products กระบวนการ SI', 12],
['เอกสารประกอบเพิ่มเติม (Other Document)', 5],
['ไฟล์เอกสารที่จัดเก็บทั้งหมด', inventory.length],
['เอกสารที่จัดเตรียมครบถ้วน', inventory.length],
['เอกสารที่ยังไม่ได้จัดเตรียม', 0],
], ['', 'r']),
`เอกสารและหลักฐานทั้งหมดจัดเก็บใน Project Repository และสำรองไว้ตามที่ระบุในเอกสาร Project Repository (Backup) โดยชุดเอกสารส่งมอบควบคุมใน Git Repository`,
].join('\n\n'),
secretary: P.YaB, reviewer: P.ApS, approver: P.SeV,
}),
};
}
+362
View File
@@ -0,0 +1,362 @@
// 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, 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';
const DURATION = daysBetween(START, END);
const resp = (ids) => ids.map((i) => P[i].id).join(', ');
/** 1. Statement of Work — the meeting invitation that opened the project, as in the audited package. */
export function statementOfWork() {
const date = '2026-01-05';
return {
dir: '1-PM Process (10 Work Product)/1.Statement of Work',
name: `${CODE} Statement of Work ${bud(date)} V1.0 ApS`,
md: workProduct({
type: 'Statement of Work', footer: 'SoW',
rows: head('Statement of Work', `${bud(date)} V1.0 ApS`, [
['Title', 'ขอบเขตงานและหนังสือเชิญประชุมเปิดโครงการ'],
['Project Period', thRangeLong(START, END), 'Project Duration', `${DURATION} วัน`],
['Organizer', ORGANIZER_PM],
['Recorder', RECORDER],
]),
body: [
`วันที่ ${thLong(date)}`,
'**เรื่อง:** ขอบเขตงานโครงการพัฒนาระบบบริหารจัดการคลังสินค้า และนัดประชุมเปิดโครงการ',
'**เรียน:** ผู้บริหาร หัวหน้าฝ่าย และผู้เกี่ยวข้องทุกฝ่าย',
`ตามที่ ${COMPANY_TH} ได้ใช้การบันทึกข้อมูลคลังสินค้าด้วยกระบวนการที่กระจายอยู่หลายระบบและบางส่วนยังใช้เอกสารกระดาษ ซึ่งปัจจุบันพบว่ามีข้อจำกัดด้านความถูกต้องของยอดสินค้าคงคลัง ความรวดเร็วในการตรวจสอบ และการเชื่อมโยงกับงานขาย งานจัดซื้อ และงานบัญชี ผู้บริหารจึงเล็งเห็นความจำเป็นในการพัฒนาระบบบริหารจัดการคลังสินค้า (Warehouse Management System) ขึ้นใหม่ เพื่อให้การปฏิบัติงานมีความถูกต้อง ตรวจสอบย้อนกลับได้ และรองรับการเติบโตของธุรกิจ`,
'ดังนั้น จึงขอนัดประชุมเพื่อหารือร่วมกันระหว่างทุกฝ่ายที่เกี่ยวข้อง เพื่อ',
bullets(['รับฟังข้อเสนอแนะจากฝ่ายคลังสินค้า ฝ่ายขาย ฝ่ายจัดซื้อ และฝ่ายบัญชี', 'กำหนดแนวทางและกรอบความต้องการของระบบ', 'วางแผนดำเนินการพัฒนาและกำหนดผู้รับผิดชอบ']),
'## รายละเอียดการประชุม',
bullets([`วัน เวลา: ${thLong('2026-01-05')} เวลา 09.00 – 11.00 น.`, `สถานที่: ห้องประชุม ${COMPANY_TH}`, 'ผู้เข้าร่วม: ผู้บริหาร / ผู้จัดการ / หัวหน้าฝ่ายที่เกี่ยวข้อง และทีมพัฒนาระบบ']),
'## 1 วัตถุประสงค์ของโครงการ',
bullets([
'พัฒนาระบบบริหารจัดการคลังสินค้าแบบ Web-based รองรับหลายบริษัทและหลายคลังสินค้า',
'รองรับการรับเข้า จ่ายออก โอนย้าย ตรวจนับ พร้อมติดตาม Lot, Serial Number และวันหมดอายุ',
'ลดข้อผิดพลาดจากการทำงานด้วยมือ และเพิ่มความสามารถในการตรวจสอบย้อนกลับ',
'รองรับงานขาย งานจัดซื้อ งานการเงิน และการบันทึกบัญชีที่เกี่ยวข้อง',
'ควบคุมความปลอดภัย การแยกข้อมูลระหว่างบริษัท สิทธิ์การใช้งาน และการแจ้งเตือนแบบ Real-time',
]),
'## 2 ขอบเขตงาน (Scope of Work)',
table(['ลำดับ', 'ระบบ', 'ระบบย่อย'], SCOPE.map(([sys, subs], i) => [i + 1, sys, subs.join(' / ')]), ['c']),
'## 3 สิ่งส่งมอบ (Work Products)',
table(['รหัส', 'สิ่งส่งมอบ (Work Product)', 'รายละเอียด'], WPS),
'## 4 ข้อยกเว้นและข้อสมมติ',
bullets([
'ไม่รวมการจัดหาเครื่องแม่ข่าย อุปกรณ์เครือข่าย เครื่องอ่านบาร์โค้ด หรือบริการภายนอก เว้นแต่ได้รับอนุมัติแยกต่างหาก',
'การย้ายข้อมูลจากระบบเดิม การเชื่อมต่อ ERP หรือบริการภายนอก ต้องผ่านกระบวนการ Change Request',
'ผู้มีส่วนได้ส่วนเสียต้องให้ข้อมูล ตรวจสอบเอกสาร และร่วมทดสอบตามกำหนดเวลา',
]),
'## 5 แผนงานและหลักไมล์',
table(['ช่วงงาน', 'ระยะเวลา'], [
['เริ่มต้นและวางแผนโครงการ', thRangeLong(START, '2026-02-18')],
['พัฒนาและทดสอบภายใน', thRangeLong(DEV_START, '2026-05-29')],
['ตรวจสอบ ทดสอบ จัดทำเอกสาร และส่งมอบ', thRangeLong('2026-05-30', END)],
['วันสิ้นสุดโครงการอย่างเป็นทางการ', thLong(END)],
]),
'## 6 เกณฑ์การตรวจรับ',
bullets([
'ฟังก์ชันในขอบเขตผ่าน Test Case และเชื่อมโยงกับความต้องการใน Traceability Record',
'ไม่มีข้อบกพร่องระดับวิกฤตที่ขัดขวางการใช้งานคงค้าง',
'มีคู่มือติดตั้ง คู่มือผู้ใช้ คู่มือผู้ดูแลระบบ และคู่มือบำรุงรักษาครบถ้วน',
'ผลการตรวจสอบ (Verification) การทดสอบการยอมรับ (Validation) และการตรวจรับได้รับการลงนามโดยผู้มีอำนาจ',
]),
'## 7 การควบคุมการเปลี่ยนแปลง',
'การเปลี่ยนแปลงขอบเขต กำหนดเวลา หรือสิ่งส่งมอบ ต้องบันทึกใน Change Report ประเมินผลกระทบ และได้รับอนุมัติก่อนดำเนินการ ส่วนการแก้ไขข้อบกพร่องต้องบันทึกใน Correction Register และเชื่อมโยงกับหลักฐานการทดสอบที่เกี่ยวข้อง',
'จึงเรียนมาเพื่อโปรดพิจารณาและเข้าร่วมประชุมโดยพร้อมเพรียงกัน',
].join('\n\n'),
secretary: P.ApS, reviewer: P.YaB, approver: P.SeV,
}),
};
}
/** Task status as known on `date`: the plan cannot report work that has not happened yet. */
const statusOn = (date, start, finish) => (finish <= date ? 'Completed' : start <= date ? 'In Progress' : 'Not Started');
/** 2.1 Work Schedule — landscape task table with milestones, as planned on its issue date. */
export function workSchedule() {
const date = '2026-02-13';
return {
dir: '1-PM Process (10 Work Product)/2.Project Plan/1-Work Schedule',
name: `${CODE} Work Schedule ${bud(date)} V1.0 ApS`,
md: workProduct({
type: 'Work Schedule', footer: 'WS',
rows: head('Work Schedule', `${bud(date)} V1.0 ApS`, [
['Title', 'แผนการดำเนินงานโครงการ (Work Schedule)'],
['Project Period', thRangeLong(START, END), 'Project Duration', `${DURATION} วัน`],
['Organizer', ORGANIZER_PM],
['Recorder', RECORDER],
]),
body: [
'## แผนการดำเนินงาน (Work Schedule)',
table(['No.', 'Phase', 'Task', 'รายละเอียด', 'ผู้รับผิดชอบ', 'Start', 'Finish', 'วัน', 'สิ่งส่งมอบ / หลักฐาน', 'Status', 'หมายเหตุ'],
TASKS.map((t) => { const s = statusOn(date, t[5], t[6]); return [t[0], t[1], t[2], t[3], resp(t[4]), thShort(t[5]), thShort(t[6]), daysBetween(t[5], t[6]), t[7], s, s === 'Completed' ? t[9] : '-']; }),
['c', '', '', '', 'c', 'r', 'r', 'r', '', 'c', '']),
'## หลักไมล์ของโครงการ (Milestones)',
table(['หลักไมล์', 'วันที่', 'เกณฑ์การบรรลุ'], [
['เปิดโครงการอย่างเป็นทางการ', thLong(START), 'ประชุม Kick-off และชี้แจงขอบเขตงาน'],
['อนุมัติกฎบัตรโครงการ', thLong('2026-01-23'), 'Project Sponsor อนุมัติ Project Charter'],
['ตั้ง Baseline ความต้องการและแผนงาน', thLong('2026-02-18'), 'อนุมัติ Customer Requirements และ Work Schedule'],
['เริ่มพัฒนาระบบ', thLong(DEV_START), 'เริ่ม Task 3.1 ตามแผน'],
['Baseline การพัฒนา', thLong('2026-05-29'), 'พัฒนาครบทุกโมดูลตามขอบเขต และกำหนด Git Baseline การพัฒนา'],
['ทดสอบระบบและ UAT', thRangeLong(TEST_FROM, TEST_TO), `Test Case ผ่านครบ และ UAT ผ่านครบ 14 หมวดความต้องการ (${CRS.length} รายการ)`],
['ตรวจรับส่งมอบระบบ', thLong('2026-08-17'), 'Project Sponsor ลงนามตรวจรับสิ่งส่งมอบครบทุกรายการ'],
['อบรมผู้ใช้งาน', thLong('2026-08-22'), 'ผู้ใช้งานทุกฝ่ายที่เกี่ยวข้องผ่านการอบรม'],
['ปิดโครงการ', thLong(END), 'ปิดโครงการอย่างเป็นทางการ'],
], ['', 'c', '']),
'## หมายเหตุการติดตามแผนงาน',
bullets([
'สถานะงานได้รับการปรับปรุงทุกงวดรายงานตาม Progress Status Record',
'งานที่ไม่แล้วเสร็จตามกำหนดต้องดำเนินการตามแผนสำรองใน Software Project Plan หัวข้อ 10',
'การเปลี่ยนแปลงแผนงานที่กระทบกำหนดส่งมอบต้องผ่าน Change Report',
]),
].join('\n\n'),
secretary: P.ApS, reviewer: P.YaB, approver: P.SeV,
}),
};
}
/** 2.2 Software Project Plan — sections 1–11 in the reference order. */
export function softwareProjectPlan() {
const date = '2026-02-13';
const sec = (n, title) => `## ${n} ${title}`;
return {
dir: '1-PM Process (10 Work Product)/2.Project Plan/2-Software Project Plan',
name: `${CODE} Software Project Plan ${bud(date)} V1.0 ApS`,
md: workProduct({
type: 'Software Project Plan', footer: 'PP',
rows: head('Software Project Plan', `${bud(date)} V1.0 ApS`, [
['Title', 'แผนการดำเนินโครงการ'],
['Project Period', thRangeLong(START, END), 'Project Duration', `${DURATION} วัน`],
['Organizer', ORGANIZER],
['Recorder', RECORDER],
]),
body: [
sec(1, 'Manufacture'),
`${COMPANY_TH} เป็นบริษัทผู้ประกอบธุรกิจด้านเทคโนโลยีสารสนเทศ ซึ่งมีประสบการณ์ในการส่งมอบผลิตภัณฑ์และบริการด้านเทคโนโลยีให้แก่หน่วยงานภาครัฐและเอกชนอย่างต่อเนื่อง บริษัทประกอบธุรกิจหลัก 3 ด้าน ได้แก่`,
bullets([
'พัฒนาซอฟต์แวร์ตามความต้องการของหน่วยงาน (Software Development)',
'จำหน่ายผลิตภัณฑ์ซอฟต์แวร์สำเร็จรูป (Software Package) เช่น ระบบบริหารจัดการสินทรัพย์',
'ให้บริการบำรุงรักษาระบบ (Service Maintenance: MA) ครอบคลุมโครงสร้างพื้นฐาน เครื่องแม่ข่าย และศูนย์ข้อมูล',
]),
sec(2, 'Overview'),
'เพื่อยกระดับการบริหารจัดการคลังสินค้าของบริษัทให้มีความถูกต้อง รวดเร็ว และตรวจสอบย้อนกลับได้ บริษัทมีความประสงค์จะพัฒนา ระบบบริหารจัดการคลังสินค้า (BRN WMS) ขึ้นใหม่ โดยมีวัตถุประสงค์เพื่อ',
bullets([
'รวมศูนย์ข้อมูลคลังสินค้า สินค้าคงคลัง และความเคลื่อนไหวของสินค้าไว้ในระบบเดียว',
'เชื่อมโยงงานขาย งานจัดซื้อ งานการเงิน และการบันทึกบัญชีเข้ากับความเคลื่อนไหวของสินค้า',
'ควบคุมสิทธิ์การเข้าถึงและแยกข้อมูลระหว่างบริษัทและคลังสินค้าอย่างชัดเจน',
'ให้ผู้บริหารเห็นภาพรวมสถานะคลังสินค้าและสถานะทางการเงินได้ตามเวลาจริง',
]),
sec(3, 'Goals and Scope'),
'### 3.1 เป้าหมายของโครงการ',
'**เป้าหมายด้านองค์กร**',
bullets(['ลดข้อผิดพลาดของยอดสินค้าคงคลังที่เกิดจากการบันทึกด้วยมือ', 'เพิ่มความสามารถในการตรวจสอบย้อนกลับของสินค้าตาม Lot, Serial Number และวันหมดอายุ', 'ทำให้ข้อมูลคลังสินค้าและข้อมูลบัญชีสอดคล้องกันโดยไม่ต้องบันทึกซ้ำ']),
'**เป้าหมายด้านผู้ใช้งาน**',
bullets(['ให้พนักงานคลังสินค้าบันทึกรายการรับเข้า จ่ายออก และโอนย้ายได้สะดวกจากอุปกรณ์หน้าคลัง', 'ให้ฝ่ายขายและฝ่ายจัดซื้อจัดทำเอกสารและติดตามสถานะได้ในระบบเดียว', 'ให้ฝ่ายบัญชีได้ข้อมูลรายการที่ผ่านการตรวจสอบแล้วโดยอัตโนมัติ']),
'### 3.2 ขอบเขตของโครงการ (SOW)',
table(['ลำดับ', 'ระบบ', 'ระบบย่อย'], SCOPE.map(([sys, subs], i) => [i + 1, sys, subs.join(' / ')]), ['c']),
'### 3.3 สิ่งส่งมอบ (Work Products)',
table(['รหัส', 'สิ่งส่งมอบ (Work Product)', 'รายละเอียด'], WPS),
'### 3.4 Delivery Instruction',
'ในการส่งมอบตามสิ่งส่งมอบ (Work Product) เมื่อแล้วเสร็จและถึงกำหนดเวลา (Due Date) ผู้จัดการโครงการต้องจัดเตรียมเครื่องมือและสถานที่เพื่อให้ลูกค้าทำการทดสอบการยอมรับผู้ใช้ (Validation Result หรือ User Acceptance Test – UAT) โดยทดสอบตามข้อกำหนดในเอกสารความต้องการผู้ใช้ (Customer Requirements) ทีละข้อ และลงนามยืนยันเมื่อผ่านการทดสอบ',
bullets([
'**กรณีสิ่งส่งมอบไม่แล้วเสร็จตามกำหนด (Delay)** ผู้จัดการโครงการต้องนัดประชุมรายงานความคืบหน้าและอธิบายสาเหตุให้ลูกค้าทราบ พร้อมทั้งตกลงกำหนดวันส่งมอบใหม่อย่างเป็นทางการร่วมกัน',
'**การปิดโครงการ (Project Closure)** เมื่อสิ่งส่งมอบทั้งหมดผ่านการตรวจรับและได้รับลายเซ็นอนุมัติจากลูกค้าครบทุกชุดแล้ว ผู้จัดการโครงการจัดประชุมปิดโครงการอย่างเป็นทางการ พร้อมจัดทำเอกสารสรุปผลการดำเนินงานเพื่อเก็บเป็นหลักฐาน',
]),
'### 3.5 เกณฑ์คุณภาพ (Quality Criteria)',
'**ด้านคุณภาพของระบบ**',
table(['ลำดับ', 'หัวข้อ', 'เกณฑ์คุณภาพ', 'วิธีประเมิน'], [
['1.1', 'ความถูกต้องของฟังก์ชัน', 'ฟังก์ชันทั้งหมดทำงานตรงตาม Customer Requirements ครบ 100%', 'ทดสอบตาม Test Case และ UAT'],
['1.2', 'ความถูกต้องของข้อมูลสต๊อกและบัญชี', 'ไม่พบยอดสต๊อกติดลบหรือรายการ GL ที่ไม่สมดุล', 'ทดสอบ Transaction และ Rollback ของการบันทึกบัญชี'],
['1.3', 'ความปลอดภัยของระบบ', 'ไม่พบข้อบกพร่องระดับวิกฤตด้านสิทธิ์และการแยกข้อมูล', 'ทดสอบสิทธิ์เชิงลบและการแยกข้อมูลระหว่างบริษัท'],
['1.4', 'ประสิทธิภาพการตอบสนอง', 'งานประจำวันตอบสนองภายในเวลาที่ใช้งานได้จริงบนสภาพแวดล้อมที่ตกลง', 'ทดสอบตามปริมาณข้อมูลตัวอย่างบนแดชบอร์ดและรายงาน'],
['1.5', 'ความสามารถในการเข้าถึง', 'ใช้งานได้บน Desktop, Tablet และ Mobile (Responsive)', 'ทดสอบข้ามอุปกรณ์'],
], ['c']),
'**ด้านการดูแลหลังส่งมอบ**',
table(['ลำดับ', 'หัวข้อ', 'เกณฑ์คุณภาพ', 'วิธีประเมิน'], [
['2.1', 'การตอบสนองเหตุขัดข้อง', 'ตอบรับภายใน 4 ชั่วโมง และแก้ไขตามระดับความรุนแรงที่กำหนดใน SLA', 'บันทึกการให้บริการ'],
['2.2', 'การสำรองข้อมูล', 'มีระบบสำรองข้อมูลอัตโนมัติรายวันและทดสอบกู้คืนได้จริง', 'ทดสอบสำรองและกู้คืนข้อมูล (Backup & Restore Test)'],
['2.3', 'การเฝ้าระวังระบบ', 'ตรวจสอบสถานะบริการทุก 5 นาที และแจ้งเตือนเมื่อล้มเหลว 2 ครั้งติดกัน', 'บันทึกการเฝ้าระวัง'],
], ['c']),
sec(4, 'Software Development Life Cycle Methodology'),
'โครงการเลือกใช้แนวทาง Incremental / Evolutionary ภายใต้ขั้นตอนหลักของ Waterfall เนื่องจาก',
bullets([
'ขอบเขตและสิ่งส่งมอบกำหนดชัดเจนตั้งแต่ต้นโครงการ จึงวางแผนและตรวจรับเป็นงวดได้',
'ระบบมีหลายโมดูลที่ส่งมอบต่อเนื่องกัน จึงพัฒนาและทดสอบทีละส่วนเพื่อลดความเสี่ยง',
'เอกสารครบถ้วนตามมาตรฐาน ISO/IEC 29110 ที่เน้น Work Products และ Traceability',
'ตรวจสอบย้อนกลับ (Audit) และตรวจรับงานได้อย่างเป็นทางการ',
]),
table(['ขั้นตอน', 'แนวทางที่ใช้จริงในโครงการ'], [
['Requirements', 'เก็บความต้องการและตั้ง Baseline ก่อนเริ่มพัฒนา การเปลี่ยนแปลงภายหลังต้องพิจารณาตามเกณฑ์ใน Change Report ก่อนดำเนินการ'],
['Design', 'ออกแบบสถาปัตยกรรม โครงสร้างฐานข้อมูล และ Software Unit ก่อนพัฒนาแต่ละโมดูล'],
['Implementation', 'พัฒนาเป็นโมดูลต่อเนื่องระหว่าง 19 ก.พ. – 29 พ.ค. 69 พร้อมทบทวนและแก้ไขระหว่างทาง'],
['Verification', 'ตรวจสอบ Work Products 4 รอบ ประมาณทุก 2 เดือน และรอบสุดท้ายก่อนส่งมอบ ตามแผนในหัวข้อ 8.1'],
['Validation', 'ทดสอบระบบและ UAT ระหว่าง 10–14 ส.ค. 69 บนสภาพแวดล้อมที่กำหนด โดยใช้ Customer Requirements ทุกรายการเป็นตัวตั้ง'],
['Closure', 'ตรวจรับ อบรม ปิดงานควบคุมปฏิบัติการ และปิดโครงการภายใน 24 ส.ค. 69'],
]),
sec(5, 'Organization'),
'### 5.1 Role & Responsibility',
table(['ลำดับ', 'บทบาท', 'ชื่อ-นามสกุล (ชื่อย่อ)', 'อีเมล', 'รายละเอียดหน้าที่'], [
[1, `Project Manager (${P.ApS.id})`, P.ApS.th, P.ApS.email, 'จัดทำ Project Charter และแผนโครงการ / วางแผน Timeline, Resource, Risk, Quality / จัดประชุมและติดตามความคืบหน้า / ควบคุมการเปลี่ยนแปลงและการปิดโครงการ'],
[2, `System Analyst (${P.NoC.id})`, P.NoC.th, P.NoC.email, 'เก็บและวิเคราะห์ความต้องการ / จัดทำ SRS และเอกสารออกแบบระบบ / ออกแบบฐานข้อมูลและความสัมพันธ์ของโมดูล / ตรวจสอบความสอดคล้องของงานพัฒนากับความต้องการ'],
[3, `Developer (${P.ThS.id})`, P.ThS.th, P.ThS.email, 'พัฒนาโมดูลและ API ตามการออกแบบ / จัดการฐานข้อมูลและประสิทธิภาพ / ดูแลความปลอดภัยและการติดตั้ง / แก้ไขข้อบกพร่องที่พบจากการทดสอบ'],
[4, `QA / Tester (${P.PaNg.id})`, P.PaNg.th, P.PaNg.email, 'จัดทำ Test Case และ Test Procedures / ทดสอบระบบและบันทึกผล / ติดตามการแก้ไขข้อบกพร่องและทดสอบซ้ำ / สนับสนุนผู้ใช้งานช่วง UAT'],
[5, `Document Control (${P.YaB.id})`, P.YaB.th, P.YaB.email, 'ควบคุมรหัสเอกสารและเวอร์ชัน / จัดเก็บและตรวจสอบความครบถ้วนของเอกสาร / ประสานการทบทวนและอนุมัติ / บันทึกประวัติการเปลี่ยนแปลงเอกสาร'],
[6, `Project Sponsor (${P.SeV.id})`, P.SeV.th, P.SeV.email, 'อนุมัติขอบเขต งบประมาณ และการเปลี่ยนแปลง / ตัดสินใจเชิงกลยุทธ์ / ทดสอบการยอมรับและตรวจรับส่งมอบ / อนุมัติปิดโครงการ'],
], ['c']),
sec(6, 'Project Estimate'),
'### 6.1 Size of the Software Work Products',
table(['ลำดับ', 'Work Product', 'ปริมาณ (ประมาณการ)'], [
[1, 'เอกสารบริหารโครงการ (PM Process)', '10 รายการ'],
[2, 'เอกสารกระบวนการพัฒนา (SI Process)', '12 รายการ'],
[3, 'ความต้องการลูกค้า (Customer Requirements)', `${CRS.length} รายการ (CR01–CR14)`],
[4, 'ความต้องการซอฟต์แวร์ (SRS)', 'ประมาณ 50 รายการ แบ่งตามกลุ่มระบบในขอบเขตงาน'],
[5, 'Software Unit', 'ประมาณ 45 หน่วย ใน 13 กลุ่มโมดูล'],
[6, 'Test Case', 'อย่างน้อย 1 รายการต่อ Software Unit'],
[7, 'ตารางฐานข้อมูล', 'ประมาณ 50 ตาราง ใน 2 ฐานข้อมูล'],
], ['c']),
'### 6.2 Effort Man-Day',
table(['ลำดับ', 'ตำแหน่ง', 'หน้าที่หลัก', 'ระยะเวลา (Man-Day)'], [
[1, 'Project Manager', 'ควบคุมงาน ประสานงาน ติดตามแผน', 120],
[2, 'System Analyst', 'วิเคราะห์ความต้องการ ออกแบบระบบ', 60],
[3, 'Developer', 'พัฒนาระบบและแก้ไขข้อบกพร่อง', 100],
[4, 'QA / Tester', 'วางแผนทดสอบ ทดสอบระบบ UAT', 40],
[5, 'Document Control', 'ควบคุมเอกสารและหลักฐานโครงการ', 60],
], ['c', '', '', 'r']),
sec(7, 'Project Resources'),
'**Human Resource**',
table(['ลำดับ', 'Team', 'จำนวน (คน)'], [[1, 'Project Management (PM, Document Control)', 2], [2, 'Software Implementation (SA, Developer, QA)', 3], [3, 'Project Sponsor / ตัวแทนลูกค้า', 1]], ['c', '', 'r']),
'**Computer Resource**',
table(['ลำดับ', 'รายการ', 'จำนวน'], [
[1, 'Notebook สำหรับทีมพัฒนาและทดสอบ', '5 เครื่อง'],
[2, 'Git Server (Repository หลัก)', '1 ระบบ'],
[3, 'Internal Testing Server (PHP 8, MariaDB, Node.js)', '1 เครื่อง'],
[4, 'Production Server สำหรับใช้งานจริง', '1 เครื่อง'],
[5, 'เครื่องอ่านบาร์โค้ดสำหรับทดสอบ', '1 เครื่อง'],
], ['c']),
sec(8, 'Work Schedule'),
`แผนการดำเนินงานของโครงการ รวม ${DURATION} วัน ตั้งแต่ ${thLong(START)} ถึง ${thLong(END)} รายละเอียดกิจกรรม ผู้รับผิดชอบ และสิ่งส่งมอบรายกิจกรรม อยู่ในเอกสาร ${CODE} Work Schedule ซึ่งเป็นส่วนหนึ่งของ Project Plan`,
table(['Phase', 'ช่วงเวลา', 'ผลลัพธ์หลัก'], [
['Project Initiation', thRangeLong(START, '2026-01-23'), 'Statement of Work, Stakeholder Register, Project Charter'],
['Project Planning', thRangeLong('2026-01-12', '2026-02-18'), 'Customer Requirements, Software Project Plan, Work Schedule'],
['Project Execution', thRangeLong(DEV_START, '2026-05-29'), 'SRS, Software Design, ระบบที่พัฒนาแล้ว, Software Components'],
['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(['ลำดับ', 'หมวด', 'รายการ', 'รายละเอียด'], [
[1, 'ขอบเขต', 'ครอบคลุมทุกกิจกรรมของโครงการ', 'ตั้งแต่ Initiation, Planning, Execution จนถึง Close'],
[2, 'บทบาทผู้รับผิดชอบ', 'Project Manager (Owner), ทีมพัฒนา, Project Sponsor', 'PM ติดตามและควบคุมความเสี่ยง ทีมพัฒนารายงานความเสี่ยงให้ PM'],
[3, 'รอบการทบทวน', 'ทบทวนทุกงวดรายงานความก้าวหน้า', 'บันทึกความเสี่ยงใหม่และการเปลี่ยนแปลงระดับความเสี่ยงใน Progress Status Record'],
], ['c']),
'**ตารางระบุความเสี่ยงหลักของโครงการ**',
table(['ลำดับ', 'ความเสี่ยง (Risk)', 'ประเภท', 'โอกาสเกิด', 'ผลกระทบ', 'ระดับความเสี่ยง', 'แนวทางป้องกัน (Mitigation)', 'แผนรับมือ (Contingency Plan)', 'ผู้รับผิดชอบ'], [
['R1', 'ความต้องการเปลี่ยนแปลงระหว่างพัฒนา', 'กระบวนการ', 'High', 'High', 'High', 'ตั้ง Baseline ความต้องการและทบทวนทุกงวด', 'จัดทำ Change Report และจัดลำดับความสำคัญใหม่', `${P.ApS.id} / ${P.NoC.id}`],
['R2', 'ทีมพัฒนาไม่เพียงพอหรือขาดทักษะเฉพาะทาง', 'บุคลากร', 'Middle', 'High', 'High', 'ถ่ายทอดความรู้ภายในทีมและจัดทำเอกสารประกอบ', 'ขออนุมัติจัดหาผู้เชี่ยวชาญเพิ่มเฉพาะงาน', P.ApS.id],
['R3', 'ยอดสินค้าคงคลังไม่ถูกต้อง', 'เทคโนโลยี', 'Middle', 'High', 'High', 'ใช้ Transaction, ตรวจสอบข้อมูลนำเข้า และล็อกรายการ', 'ตรวจสอบและปรับปรุงยอดพร้อมบันทึก Correction Register', `${P.ThS.id} / ${P.PaNg.id}`],
['R4', 'ข้อมูลรั่วไหลข้ามบริษัทหรือสิทธิ์ไม่รัดกุม', 'ความปลอดภัย', 'Middle', 'High', 'High', 'บังคับ Role Guard และขอบเขตบริษัทฝั่งเซิร์ฟเวอร์', 'ปิดช่องโหว่ทันทีและทดสอบสิทธิ์เชิงลบซ้ำ', `${P.ThS.id} / ${P.PaNg.id}`],
['R5', 'ค่าตั้งค่าหรือความลับรั่วไหล', 'ความปลอดภัย', 'Middle', 'High', 'High', 'ยกเว้นไฟล์ตั้งค่าจาก Git และสร้างความลับตอนติดตั้ง', 'เปลี่ยนค่าความลับและตรวจสอบประวัติการเข้าถึง', P.ThS.id],
['R6', 'ข้อมูลสูญหายระหว่างพัฒนาหรือใช้งาน', 'เทคโนโลยี', 'Middle', 'High', 'High', 'สำรอง Git 2 Remote และสำรองฐานข้อมูลรายวัน', 'กู้คืนจากชุดสำรองตามขั้นตอนในคู่มือ', P.ThS.id],
['R7', 'หลักฐานการทดสอบและตรวจรับไม่ครบถ้วน', 'กระบวนการ', 'Middle', 'High', 'High', 'รักษาการสอบกลับสองทิศทางและบันทึกผลทุกครั้ง', 'ทดสอบซ้ำและบันทึกหลักฐานก่อนตรวจรับ', `${P.PaNg.id} / ${P.YaB.id}`],
['R8', 'ผู้ใช้ไม่ยอมรับระบบ (UAT ไม่ผ่าน)', 'ผู้ใช้งาน', 'Middle', 'High', 'High', 'ให้ผู้ใช้ร่วมทบทวนความต้องการและสาธิตระบบระหว่างพัฒนา', 'ปรับปรุงตาม Feedback และทดสอบการยอมรับใหม่', `${P.ApS.id} / ${P.SeV.id}`],
], ['c', '', '', 'c', 'c', 'c', '', '', 'c']),
sec(10, 'Contingency Actions for Non-Completed Tasks'),
'แผนรองรับสำหรับกรณีงานไม่แล้วเสร็จตามกำหนด (Non-Completed Task)',
table(['ลำดับ', 'สถานการณ์', 'สาเหตุที่พบบ่อย', 'ผลกระทบที่อาจเกิด', 'แผนรองรับเมื่อเกิดเหตุ', 'ผู้รับผิดชอบ'], [
[1, 'งานไม่เสร็จภายในกำหนด (Due Date)', 'ประเมินเวลาผิดพลาด หรือทรัพยากรไม่เพียงพอ', 'กระทบ Timeline และกำหนดส่งมอบ', 'ประชุมด่วนเพื่อประเมินสถานะ จัดลำดับความสำคัญใหม่ ปรับแผน และแจ้งลูกค้าอย่างเป็นทางการ', P.ApS.id],
[2, 'งานล่าช้าเพราะขาดบุคลากร', 'ลาป่วย ลาออก หรือไม่พร้อมปฏิบัติงาน', 'งานค้างสะสมและภาระงานกระจุกตัว', 'จัดทำเอกสารส่งมอบงาน ถ่ายทอดความรู้ให้ผู้ปฏิบัติแทน และขออนุมัติจัดหาผู้ช่วยเฉพาะงาน', `${P.ApS.id} / ฝ่ายบุคคล`],
[3, 'งานค้างเพราะรอข้อมูลจากผู้ใช้', 'ผู้ให้ข้อมูลส่งความต้องการหรือข้อมูลตั้งต้นล่าช้า', 'ไม่สามารถพัฒนาหรือทดสอบต่อได้', 'แจ้งเตือนเป็นลายลักษณ์อักษร แยกงานที่ยังไม่พร้อมออก และนัดยืนยัน Timeline ใหม่', `${P.ApS.id} / ${P.SeV.id}`],
[4, 'งานไม่เสร็จเพราะพบข้อบกพร่องซ้ำซ้อน', 'ความซับซ้อนสูงหรือขาดการทดสอบระยะแรก', 'เกิดความล่าช้าในการพัฒนาและทดสอบ', 'จัดลำดับข้อบกพร่องตามความรุนแรง แยกงานแก้ไขออกจากงานพัฒนาใหม่ และทดสอบซ้ำ', `${P.ThS.id} / ${P.PaNg.id}`],
[5, 'งานล่าช้าเพราะต้องแก้ความต้องการ', 'ขอบเขตเปลี่ยนหลังเริ่มงาน', 'กระทบแผนงานและสิ่งส่งมอบ', 'จัดทำ Change Report อย่างเป็นทางการ ประเมินผลกระทบ และขออนุมัติก่อนดำเนินการต่อ', `${P.ApS.id} / ${P.SeV.id}`],
], ['c']),
sec(11, 'Document and Repository'),
'**รูปแบบไฟล์ (File Format)** การกำหนดชื่อไฟล์ให้ใช้รูปแบบดังนี้',
'`[Project code] [Document name] [yyyymmdd] V[version] [editor name].[ext.]`',
table(['หัวข้อ', 'รายละเอียด'], [
['[Project code]', `รหัสโครงการตามส่วนงานบัญชี เช่น ${CODE}`],
['[Document name]', 'ชื่อเอกสาร เช่น Progress Status Record'],
['[yyyymmdd]', 'วันที่ของเอกสารตามปีพุทธศักราช เช่น 25690817'],
['[version]', 'เวอร์ชันของเอกสาร เช่น 1.0'],
['[editor name]', 'ตัวย่อของชื่อผู้จัดทำ เช่น ApS'],
['[ext.]', 'นามสกุลไฟล์ เช่น pdf'],
]),
'**การประกาศเวอร์ชัน (Version Declaration)** เก็บเอกสารแต่ละเวอร์ชันแยกไฟล์เพื่อให้ตรวจสอบย้อนหลังได้ โดยใช้รูปแบบ Major.Minor',
table(['เวอร์ชัน (Version)', 'รายละเอียด'], [
['0.1 (Draft)', 'ร่างเอกสารฉบับแรก'],
['0.2 (Draft)', 'ปรับปรุงตามผลการทบทวน'],
['1.0 (Release)', 'อนุมัติใช้เป็นเอกสารทางการ'],
['1.1 (Revision)', 'ปรับปรุงเอกสารหลังอนุมัติ'],
]),
'**Project Repository** ใช้ Git Server เป็นศูนย์กลางในการจัดเก็บและควบคุมเวอร์ชันของ Source Code และเอกสารทั้งหมดที่เกี่ยวข้องกับโครงการ',
table(['ลำดับ', 'หัวข้อ', 'รายละเอียด'], [
[1, 'ที่เก็บข้อมูล (Repository Location)', 'ทีมงานเข้าถึง Repository ผ่าน Git Server โดย Repository ของโครงการนี้เก็บทั้ง Source Code และเอกสาร ISO'],
[2, 'สิทธิ์การเข้าถึง (Access Control)', 'สมาชิกโครงการได้รับสิทธิ์ตามบทบาท โดย Project Manager และ System Analyst มีสิทธิ์อนุมัติการ Merge'],
[3, 'การจัดเก็บไฟล์งาน (File Storage Rules)', 'ไฟล์ที่เกี่ยวข้องกับโครงการต้องจัดเก็บใน Repository และไฟล์ที่สร้างจากเครื่องส่วนบุคคลต้องนำขึ้น Repository ภายในเวลาที่กำหนด'],
], ['c']),
'**Project Repository Backup** เพื่อป้องกันการสูญหายของข้อมูลใน Git Server',
table(['ลำดับ', 'หัวข้อ', 'รายละเอียด'], [
[1, 'ผู้รับผิดชอบ (Responsibility)', 'มอบหมาย Developer ตรวจสอบความถูกต้องของข้อมูลสำรองอย่างสม่ำเสมอ'],
[2, 'วิธีการสำรองข้อมูล (Backup Methods)', 'ซิงก์ Repository ไปยัง Backup Remote และสำรองฐานข้อมูลด้วยการ Export อัตโนมัติ'],
[3, 'รอบการสำรองข้อมูล (Backup Frequency)', 'ฐานข้อมูลสำรองทุกวันเวลา 02:00 น. และ Repository ซิงก์ทุกครั้งที่มีการส่งมอบ'],
], ['c']),
].join('\n\n'),
secretary: P.ApS, reviewer: P.YaB, approver: P.SeV,
}),
};
}
/** 2.3 Customer Requirements — CR01–CR14 in the reference table layout. */
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] of items) rows.push([id, text, 'A']);
}
return {
dir: '1-PM Process (10 Work Product)/2.Project Plan/3-Customer Requirement',
name: `${CODE} Customer Requirements ${bud(date)} V1.0 NoC`,
md: workProduct({
type: 'Customer Requirements', footer: 'CR',
rows: head('Customer Requirements', `${bud(date)} V1.0 NoC`, [
['Title', 'เอกสารบันทึกและสรุปความต้องการของลูกค้า (Customer Requirements)'],
['Project Period', thRangeLong(START, END)],
['Organizer', ORGANIZER],
['Recorder', RECORDER],
]),
body: [
'## วัตถุประสงค์ (Objective)',
`เอกสารฉบับนี้จัดทำขึ้นเพื่อบันทึกและสรุปความต้องการของลูกค้า (Customer Requirements) ตามกระบวนการ ISO/IEC 29110 สำหรับใช้เป็นพื้นฐานในการจัดทำ Software Requirements Specification, การออกแบบระบบ, การทดสอบ และการตรวจรับ ของ${PROJECT_TH}`,
'## ผู้มีส่วนได้ส่วนเสีย (Stakeholders)',
table(['ลำดับ', 'ชื่อ', 'ตำแหน่ง', 'บทบาท'], [
[1, P.SeV.th, 'ผู้บริหาร (CEO)', 'Project Sponsor อนุมัติและตัดสินใจเชิงกลยุทธ์'],
[2, P.ApS.th, 'ผู้จัดการโครงการ', 'บริหารโครงการและควบคุมการเปลี่ยนแปลง'],
[3, P.NoC.th, 'นักวิเคราะห์ระบบ', 'เก็บและวิเคราะห์ความต้องการ จัดทำเอกสาร'],
[4, 'หัวหน้าฝ่ายคลังสินค้าและพนักงานคลัง', 'ผู้ใช้งานหลัก', 'ให้ข้อมูลกระบวนการรับเข้า จ่ายออก โอนย้าย และตรวจนับ'],
[5, 'ฝ่ายขายและฝ่ายจัดซื้อ', 'ผู้ใช้งาน', 'ให้ข้อมูลกระบวนการเอกสารขายและจัดซื้อ'],
[6, 'ฝ่ายบัญชีและการเงิน', 'ผู้ใช้งาน', 'ให้ข้อมูลการวางบิล รับชำระ จ่ายชำระ และการบันทึกบัญชี'],
], ['c']),
'## ความต้องการเชิงหน้าที่และไม่ใช่หน้าที่ (Functional and Non-Functional Requirements)',
table(['ID', 'Topic', 'Result*'], rows, ['', '', 'c']),
'## Remark',
bullets(['\\* Result : A = Accepted, U = Unaccepted, N/A = Not Applicable', 'เอกสารฉบับนี้รวมความต้องการที่ได้จากการทบทวนขอบเขตงานร่วมกับลูกค้า', 'การเปลี่ยนแปลงความต้องการหลังอนุมัติต้องดำเนินการผ่าน Change Report']),
].join('\n\n'),
secretary: P.NoC, reviewer: P.YaB, approver: P.SeV,
}),
};
}
+395
View File
@@ -0,0 +1,395 @@
// PM work products 3–10: Progress Status Record, Correction Register, Acceptance Report,
// Change Report, Meeting Record, Software Configuration, Project Repository (+ Backup).
import { P, CRS, CODE, PROJECT_TH, COMPANY_TH, ORGANIZER, ORGANIZER_PM, RECORDER, START, END, WPS, TEAM, ORIGIN, BACKUP, 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';
const ids = (arr) => arr.map((i) => P[i].id).join(', ');
const dir3 = '1-PM Process (10 Work Product)/3.Progress Status Record';
/** 3. Progress Status Record — one per reporting period. */
export function progressStatusRecords() {
return PERIODS.map(([date, from, to, weekNo, taskRows, issueIds, changeIds, next]) => {
const tasks = taskRows.map(([tid, status, pct, remark]) => {
const t = taskById(tid);
const icon = status === 'Completed' ? 'Completed' : status === 'In Progress' ? 'In Progress' : 'Planned';
return [tid, t[2], thRange(t[5], t[6]), ids(t[4]), icon, pct, remark];
});
// The reference package records risks in five columns: number, issue,
// detail, owner and status.
const issues = issueIds.map((iid, n) => {
const c = CORRECTIONS.find((x) => x[0] === iid);
return [n + 1, `${c[0]} : ${c[4]}`, `${c[5]} จึงดำเนินการ${c[6]} แล้วเสร็จ ${thShort(c[2])}`, P[c[7]].id, 'เสร็จสิ้น'];
});
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`,
md: workProduct({
type: 'Progress Status Record', footer: 'PR',
rows: head('Progress Status Record', `${bud(date)} V1.0 ApS`, [
['Title', `รายงานสรุปงวดที่ ${weekNo} : ${thLong(date)}`],
['Reporting Period', thRangeLong(from, to)],
['Organizer', ORGANIZER_PM],
['Recorder', RECORDER],
]),
body: [
'## สรุปสถานะโครงการ',
table(['Task ID', 'Task Name', 'Period', 'Responsible', 'Status', 'Progress (%)', 'Remarks'], tasks, ['c', '', 'c', 'c', 'c', 'r', '']),
'## ความเสี่ยง/อุปสรรค',
table(['ลำดับ', 'ประเด็น / ความเสี่ยง', 'รายละเอียด', 'ผู้รับผิดชอบ', 'สถานะ'],
issues.length ? issues : [['-', 'ไม่พบประเด็นความเสี่ยงหรืออุปสรรคในงวดนี้', '-', '-', '-']],
['c', '', '', 'c', 'c']),
'## สรุป CR/Change (ถ้ามี)',
changes.length
? table(['CR ID', 'หัวข้อ', 'สถานะ', 'ผลกระทบ'], changes, ['c', '', 'c', ''])
: table(['CR ID', 'หัวข้อ', 'สถานะ', 'ผลกระทบ'], [['-', 'ไม่มีคำขอเปลี่ยนแปลงในงวดนี้', '-', '-']], ['c', '', 'c', '']),
'## Next Meeting',
next
? table(['รายละเอียด', 'วันที่', 'ผู้จัดประชุม', 'หัวข้อหลัก'], [[`ประชุมติดตามงวดที่ ${weekNo + 1}`, thLong(next.date), `${P.ApS.th} (${P.ApS.id})`, next.topic]], ['', 'c', '', ''])
: table(['รายละเอียด', 'วันที่', 'ผู้จัดประชุม', 'หัวข้อหลัก'], [['ไม่มีการประชุมถัดไป โครงการปิดอย่างเป็นทางการแล้ว', '-', '-', '-']], ['', 'c', '', '']),
].join('\n\n'),
secretary: P.YaB, reviewer: P.ApS, approver: P.SeV,
}),
};
});
}
/** 4. Correction Register — all issues found during the project. */
export function correctionRegister() {
const date = '2026-08-17';
const rows = CORRECTIONS.map((c, i) => [i + 1, c[0], thShort(c[1]), c[4], c[5], c[6], P[c[7]].id, thShort(c[2]), 'เสร็จสิ้น']);
const bySev = (s) => CORRECTIONS.filter((c) => c[3] === s).length;
return {
dir: '1-PM Process (10 Work Product)/4.Correction Register',
name: `${CODE} Correction Register ${bud(date)} V1.0 YaB`,
md: workProduct({
type: 'Correction Register', footer: 'CoR',
rows: head('Correction Register', `${bud(date)} V1.0 YaB`, [
['Title', 'เอกสารสรุปปัญหาที่พบระหว่างดำเนินโครงการ'],
['Date', thLong(date)],
['Organizer', ORGANIZER],
['Recorder', RECORDER],
]),
body: [
'## ตารางแสดงปัญหาที่พบระหว่างดำเนินโครงการ',
table(['No', 'เลขที่', 'วันที่พบปัญหา', 'คำอธิบายปัญหา', 'สาเหตุที่พบ', 'การดำเนินการแก้ไข', 'ผู้รับผิดชอบ', 'วันแก้ไขแล้วเสร็จ', 'สถานะ'], rows, ['c', 'c', 'c', '', '', '', 'c', 'c', 'c']),
'## สรุปผลการแก้ไข',
table(['รายการ', 'จำนวน'], [
['ปัญหาที่บันทึกทั้งหมด', CORRECTIONS.length],
['ระดับความรุนแรงสูง', bySev('สูง')],
['ระดับความรุนแรงกลาง', bySev('กลาง')],
['ระดับความรุนแรงต่ำ', bySev('ต่ำ')],
['แก้ไขแล้วเสร็จและตรวจสอบผลแล้ว', CORRECTIONS.length],
['คงค้าง', 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'),
secretary: P.YaB, reviewer: P.ApS, approver: P.SeV,
}),
};
}
/** 5. Acceptance Report — the sign-off record. */
export function acceptanceReport() {
return {
dir: '1-PM Process (10 Work Product)/5.Acceptance Report',
name: `${CODE} Acceptance Report ${bud(ACCEPT_DATE)} V1.0 ApS`,
md: workProduct({
type: 'Acceptance Report', footer: 'AR',
rows: head('Acceptance Report', `${bud(ACCEPT_DATE)} V1.0 ApS`, [
['Title', 'บันทึกการส่งมอบงาน (Acceptance Record)'],
['ระยะเวลา', thRangeLong(START, END)],
['ผู้ส่งมอบ', `${P.ApS.th} (Project Manager)`],
['ผู้รับมอบ', `${P.SeV.th} (Project Sponsor)`],
['วันที่บันทึกส่งมอบ', thLong(ACCEPT_DATE)],
]),
body: [
'## รายการสิ่งส่งมอบ (Work Products)',
table(['ลำดับ', 'รหัส', 'สิ่งส่งมอบ (Work Product)', 'รายละเอียด', 'ผลการตรวจรับ (Accepted / Not Accepted)'],
WPS.map((w, i) => [i + 1, w[0], w[1], w[0] === 'WP 9.0' ? `${w[2]} (Git baseline \`${BASELINE_COMMIT}\`)` : w[2], 'Accepted']), ['c', 'c', '', '', 'c']),
'## ผลการตรวจสอบตามเกณฑ์การตรวจรับ',
table(['ลำดับ', 'เกณฑ์การตรวจรับ', 'หลักฐานอ้างอิง', 'ผลการตรวจสอบ'], [
[1, 'ฟังก์ชันในขอบเขตทำงานตรงตาม Customer Requirements ครบทุกรายการ', 'Traceability Record, Test Report', 'ผ่าน'],
[2, 'ทดสอบระบบตาม Test Case ครบถ้วน', `Test Report 45 Test Case ผ่านทั้งหมด (${thRangeLong(TEST_FROM, TEST_TO)})`, 'ผ่าน'],
[3, 'ทดสอบการยอมรับโดยผู้ใช้ (UAT)', `Validation Results ครบ 14 หมวดความต้องการ (${CRS.length} รายการ) ผ่านทั้งหมด`, 'ผ่าน'],
[4, 'ไม่มีข้อบกพร่องระดับวิกฤตคงค้าง', `Correction Register ${CORRECTIONS.length} รายการ แก้ไขและตรวจสอบครบ`, 'ผ่าน'],
[5, 'เอกสารคู่มือผู้ใช้ ผู้ดูแลระบบ และการบำรุงรักษาครบถ้วน', 'Software User Document, Product Operation Guide, Maintenance Document', 'ผ่าน'],
[6, 'Source Code และเอกสารจัดเก็บใน Repository พร้อมชุดสำรอง', 'Project Repository, Project Repository (Backup)', 'ผ่าน'],
[7, 'ผลการตรวจสอบ Work Products โดยผู้ตรวจสอบอิสระ', 'Verification Results รอบที่ 4', 'ผ่าน'],
], ['c', '', '', 'c']),
'## สรุปผลการตรวจรับ',
table(['รายการ', 'ผลสรุป'], [
['จำนวนสิ่งส่งมอบทั้งหมด', `${WPS.length} รายการ`],
['ตรวจรับแล้ว (Accepted)', `${WPS.length} รายการ`],
['ไม่ผ่านการตรวจรับ (Not Accepted)', '0 รายการ'],
['ข้อบกพร่องคงค้าง', 'ไม่มี'],
['ผลการตรวจรับโดยรวม', '**Accepted** — ตรวจรับส่งมอบระบบครบถ้วน'],
]),
`ผู้รับมอบได้ตรวจสอบสิ่งส่งมอบทั้งหมดตามเกณฑ์การตรวจรับที่กำหนดในเอกสาร Statement of Work และ Software Project Plan แล้วเห็นว่าครบถ้วนถูกต้อง จึงมีมติตรวจรับส่งมอบระบบเมื่อวันที่ ${thLong(ACCEPT_DATE)} โดยงานอบรมผู้ใช้งานและการปิดงานควบคุมปฏิบัติการจะดำเนินการให้แล้วเสร็จภายในวันที่ ${thLong(END)} ตามแผนปิดโครงการ`,
].join('\n\n'),
secretary: P.ApS, reviewer: P.YaB, approver: P.SeV,
}),
};
}
/** 6. Change Report — the change-control register. No adjustment in this project met the criteria. */
export function changeReports() {
const date = ACCEPT_DATE;
return [{
dir: '1-PM Process (10 Work Product)/6.Change Report',
name: `${CODE} Change Report ${bud(date)} V1.0 ApS`,
md: workProduct({
type: 'Change Report', footer: 'CR',
rows: head('Change Report', `${bud(date)} V1.0 ApS`, [
['Title', 'ทะเบียนการควบคุมการเปลี่ยนแปลง (Change Control Register)'],
['Project Period', thRangeLong(START, END)],
['Organizer', ORGANIZER_PM],
['Recorder', RECORDER],
]),
body: [
'## วัตถุประสงค์ (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: P.ApS, reviewer: P.YaB, approver: P.SeV,
}),
}];
}
/** 7. Meeting Record — Minutes of Meeting per checkpoint. */
export function meetingRecords() {
return MEETINGS.map((m) => {
const t = taskById(m.taskId);
const participants = TEAM.map((p) => [p.th, p.id, p.role, '✔']);
return {
dir: '1-PM Process (10 Work Product)/7.Meeting Record',
name: `${CODE} Minutes Of Meeting ${bud(m.date)} V1.0 YaB`,
md: workProduct({
type: 'Minutes Of Meeting', footer: 'MoM',
rows: head('Minutes Of Meeting', `${bud(m.date)} V1.0 YaB`, [
['Phase', m.phase],
['Task ID / Task Name', `${m.taskId} ${m.taskName}`],
['Work Period of Task', thRangeLong(t[5], t[6])],
['Meeting Date', thLong(m.date), 'Time', m.time],
['สิ่งแนบ', m.attachment],
['Location', `ห้องประชุม ${COMPANY_TH}`],
['Organizer', ORGANIZER],
['Recorder', RECORDER],
['สำเนาถึง (CC)', `${P.SeV.th} (Project Sponsor)`],
]),
body: [
'## ผู้เข้าร่วมประชุม (Participants)',
table(['ชื่อ - สกุล', 'ชื่อย่อ', 'ตำแหน่ง/บทบาท', '(✔ เข้าร่วม / ✘ ไม่เข้าร่วม)'], participants, ['', 'c', '', 'c']),
'## ความคืบหน้ารวมของโครงการ (Project Progress)',
table(['หมวดงาน', 'สถานะ', 'ความคืบหน้า (%)', 'รายละเอียด'], m.progress, ['', 'c', 'r', '']),
'## หัวข้อการประชุม (Agenda)',
table(['ลำดับ', 'หัวข้อการประชุม', 'วัตถุประสงค์'], m.agenda.map((a, i) => [i + 1, a[0], a[1]]), ['c']),
'## สรุปรายงานการประชุม (Discussion Summary)',
table(['ลำดับ', 'ประเด็น', 'รายละเอียด / มติที่ได้', 'ผู้รับผิดชอบ', 'สถานะ'], m.discussion.map((d, i) => [i + 1, d[0], d[1], ids(d[2]), d[3]]), ['c', '', '', 'c', 'c']),
'## งานที่ต้องติดตาม (Action Item)',
table(['ลำดับ', 'งาน', 'ผู้รับผิดชอบ', 'วันครบกำหนด', 'สถานะ'], m.actions.map((a, i) => [i + 1, a[0], ids(a[1]), thLong(a[2]), a[3]]), ['c', '', 'c', 'c', 'c']),
'## งานประชุมครั้งถัดไป (Next Meeting)',
m.next
? table(['หัวข้อการประชุมถัดไป', 'วันที่', 'ผู้จัดประชุม', 'วัตถุประสงค์'], [[m.next.topic, thLong(m.next.date), `${P.ApS.th} (${P.ApS.id})`, 'ติดตามความคืบหน้าและมติที่เกี่ยวข้อง']], ['', 'c', '', ''])
: table(['หัวข้อการประชุมถัดไป', 'วันที่', 'ผู้จัดประชุม', 'วัตถุประสงค์'], [['ไม่มีการประชุมถัดไป โครงการปิดอย่างเป็นทางการแล้ว', '-', '-', '-']], ['', 'c', '', '']),
].join('\n\n'),
secretary: P.YaB, reviewer: P.ApS, approver: P.SeV,
}),
};
});
}
/** 8. Software Configuration — controlled items and their versions. */
export function softwareConfiguration() {
const date = '2026-08-17';
return {
dir: '1-PM Process (10 Work Product)/8.Software Configuration',
name: `${CODE} Software Configuration ${bud(date)} V1.0 ThS`,
md: workProduct({
type: 'Software Configuration', footer: 'SC',
rows: head('Software Configuration', `${bud(date)} V1.0 ThS`, [
['Title', 'บันทึกการกำหนดค่าและความเปลี่ยนแปลงต่าง ๆ ที่เกิดขึ้นของระบบ'],
['ระยะเวลา', thRangeLong(START, END)],
['Organizer', ORGANIZER_PM],
['Recorder', RECORDER],
]),
body: [
'## รายการสิ่งส่งมอบ (Work Products)',
table(['No.', 'Work Product', 'Version'], [
['**เอกสารส่งมอบครั้งที่ 1 (วางแผนโครงการ)**', '', ''],
['WP 1.0', `เอกสาร ${CODE} Software Project Plan (พร้อม Work Schedule)`, '1.0'],
['WP 2.0', `เอกสาร ${CODE} Customer Requirements`, '1.0'],
['**เอกสารส่งมอบครั้งที่ 2 (พัฒนาและทดสอบ)**', '', ''],
['WP 3.0', `เอกสาร ${CODE} Software Requirements Specification`, '1.0'],
['WP 4.0', `เอกสาร ${CODE} Software Design`, '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'],
['WP 9.0', `ระบบ BRN WMS ที่ผ่านการทดสอบ (Git baseline \`${BASELINE_COMMIT}\`)`, '1.0'],
['**เอกสารส่งมอบครั้งที่ 3 (ส่งมอบและปิดโครงการ)**', '', ''],
['WP 10.0', `เอกสาร ${CODE} Product Operation Guide`, '1.0'],
['WP 11.0', `เอกสาร ${CODE} Maintenance Document`, '1.0'],
['—', `เอกสาร ${CODE} Acceptance Report`, '1.0'],
], ['c']),
'## รายการ Configuration Item ของซอฟต์แวร์',
table(['ลำดับ', 'รายการ', 'ที่จัดเก็บ', 'การควบคุมเวอร์ชัน'], [
[1, 'Source Code แอปพลิเคชัน PHP', '`app/`', `Git baseline \`${BASELINE_COMMIT}\` บน branch main`],
[2, 'บริการ Node.js (แจ้งเตือนและงานตามกำหนดเวลา)', '`nodejs/`', 'Git พร้อม package.json และ package-lock.json'],
[3, 'สคริปต์ติดตั้งฐานข้อมูล', '`setup.php`, `docker/mariadb/init-wms2.sql`', 'Git'],
[4, 'ชุดติดตั้ง Docker Compose', '`docker-compose.yml`, `docker/`, `.env.example`', 'Git (ไม่รวมค่าความลับ)'],
[5, 'ค่าตั้งค่าและความลับของสภาพแวดล้อม', '`.env`, `app/config.php` บนเครื่องที่ติดตั้ง', 'ไม่ควบคุมใน Git สร้างขึ้นตอนติดตั้งด้วย `docker/init-env.sh`'],
[6, 'เอกสาร Work Products', '`sdlc/`', 'Git (Markdown ต้นฉบับ)'],
[7, 'ชุดเอกสารส่งมอบ (PDF)', '`sdlc-delivery/`', 'Git สร้างจาก `sdlc/` ด้วย `scripts/build-sdlc-delivery.sh`'],
], ['c']),
'## การควบคุมเวอร์ชันและการเข้าถึง',
table(['หัวข้อ', 'รายละเอียด'], [
['Repository หลัก (origin)', `\`${ORIGIN}\``],
['Repository สำรอง (backup)', `\`${BACKUP}\``],
['Branch หลัก', '`main` เป็น Baseline ที่ควบคุม'],
['Baseline ของระบบที่ส่งมอบ', `Commit \`${BASELINE_COMMIT}\``],
['สิทธิ์การเข้าถึง', 'ยืนยันตัวตนด้วย SSH Key ตามบทบาทของสมาชิกโครงการ'],
]),
'## การเชื่อมโยงกับการเปลี่ยนแปลง',
`การเปลี่ยนแปลง 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,
}),
};
}
/** 9. Project Repository. */
export function projectRepository() {
const date = '2026-08-17';
return {
dir: '1-PM Process (10 Work Product)/9.Project Repository',
name: `${CODE} Project Repository ${bud(date)} V1.0 ThS`,
md: workProduct({
type: 'Project Repository', footer: 'PjR',
rows: head('Project Repository', `${bud(date)} V1.0 ThS`, [
['Title', 'พื้นที่เก็บข้อมูล เอกสาร และหลักฐานทุกอย่างของโครงการ'],
['ระยะเวลา', thRangeLong(START, END)],
['Organizer', ORGANIZER_PM],
['Recorder', RECORDER],
]),
body: [
'## ข้อมูล Repository',
table(['หัวข้อ', 'รายละเอียด'], [
['ประเภท', 'Git Server'],
['Repository หลัก (origin)', `\`${ORIGIN}\``],
['Branch หลัก', '`main`'],
['Baseline ของระบบที่ส่งมอบ', `Commit \`${BASELINE_COMMIT}\``],
['สิทธิ์การเข้าถึง', 'ยืนยันตัวตนด้วย SSH Key ตามบทบาทของสมาชิกโครงการ'],
]),
'## โครงสร้างการจัดเก็บ',
table(['ลำดับ', 'ส่วนงาน', 'ที่จัดเก็บ', 'เนื้อหา'], [
[1, 'Source Code แอปพลิเคชัน', '`app/`', 'เข้าสู่ระบบ, คลังสินค้า, ขาย, จัดซื้อ, การเงิน, บัญชี, รายงาน, บาร์โค้ด'],
[2, 'บริการ Real-time และงานตามกำหนดเวลา', '`nodejs/`', 'Socket.IO และงานสรุปยอด/แจ้งเตือน'],
[3, 'การติดตั้งและตั้งค่า', '`docker/`, `docker-compose.yml`, `setup.php`, `.env.example`', 'ชุดติดตั้ง Container และสคริปต์สร้างฐานข้อมูล'],
[4, 'ข้อมูลสาธิต', '`demo_seed*.php`', 'ชุดข้อมูลตัวอย่างสำหรับตรวจรับและสาธิต'],
[5, 'เอกสาร Work Products', '`sdlc/`', 'เอกสาร ISO/IEC 29110 กระบวนการ PM และ SI'],
[6, 'ชุดเอกสารส่งมอบ', '`sdlc-delivery/`', 'ไฟล์ PDF ที่สร้างจาก `sdlc/`'],
[7, 'สคริปต์สนับสนุนโครงการ', '`scripts/`', 'สคริปต์สร้างและตรวจสอบชุดเอกสารส่งมอบ'],
], ['c']),
'## การควบคุมการเข้าถึงและการจัดเก็บ',
bullets([
'สมาชิกโครงการทุกคนเข้าถึง Repository ได้ผ่าน Git Server ตามสิทธิ์ของบทบาท',
'Project Manager และ System Analyst มีสิทธิ์อนุมัติการ Merge เข้าสู่ Branch หลัก',
'ไฟล์ที่เกี่ยวข้องกับโครงการทั้งหมดต้องจัดเก็บใน Repository นี้',
'ค่าตั้งค่าและความลับของสภาพแวดล้อมต้องไม่ถูกนำเข้า Repository',
]),
'## หลักฐานการจัดเก็บ',
table(['ลำดับ', 'หลักฐาน', 'รายละเอียด'], [
[1, 'โครงสร้าง Repository และประวัติการเปลี่ยนแปลง', 'ตรวจสอบได้จาก Git Server ตามสิทธิ์ที่ได้รับ'],
[2, 'Baseline ของระบบที่ส่งมอบ', `Commit \`${BASELINE_COMMIT}\` บน branch \`main\``],
[3, 'ชุดเอกสารส่งมอบที่ควบคุมแล้ว', 'Git Repository'],
], ['c']),
].join('\n\n'),
secretary: P.ThS, reviewer: P.ApS, approver: P.SeV,
}),
};
}
/** 10. Project Repository (Backup). */
export function projectRepositoryBackup() {
const date = '2026-08-24';
return {
dir: '1-PM Process (10 Work Product)/10.Project Repository (Backup)',
name: `${CODE} Project Repository Backup ${bud(date)} V1.0 ThS`,
md: workProduct({
type: 'Project Repository (Backup)', footer: 'PjB',
rows: head('Project Repository (Backup)', `${bud(date)} V1.0 ThS`, [
['Title', 'พื้นที่สำรองสำหรับใช้กู้คืนข้อมูล Project Repository ในกรณีที่เกิดการสูญหาย'],
['ระยะเวลา', thRangeLong(START, END)],
['Organizer', ORGANIZER_PM],
['Recorder', RECORDER],
]),
body: [
'## การสำรอง Source Code และเอกสาร',
table(['หัวข้อ', 'รายละเอียด'], [
['วิธีการสำรอง', 'Git Remote สำรอง (Secondary Remote)'],
['ชื่อ Remote สำรอง', '`backup`'],
['ที่อยู่ Remote สำรอง', `\`${BACKUP}\``],
['ขอบเขตการสำรอง', `Branch \`main\` (Baseline \`${BASELINE_COMMIT}\`) และเอกสารส่งมอบใน Git Repository`],
['รอบการสำรอง', 'ซิงก์ทุกครั้งที่มีการส่งมอบหรือปรับปรุง Baseline'],
['ผู้รับผิดชอบ', `${P.ThS.th} (Developer)`],
]),
'## การสำรองฐานข้อมูล',
table(['หัวข้อ', 'รายละเอียด'], [
['วิธีการสำรอง', 'Export ฐานข้อมูล `wms` และ `wms2` อัตโนมัติ'],
['รอบการสำรอง', 'ทุกวัน เวลา 02:00 น. (Asia/Bangkok)'],
['ที่จัดเก็บ', 'พื้นที่จัดเก็บบนคลาวด์ที่บริษัทควบคุม'],
['ระยะเวลาเก็บรักษา', 'ชุดสำรองรายวันเก็บ 30 วัน และชุดสิ้นเดือนเก็บ 12 เดือน'],
['สิทธิ์การเข้าถึง', 'จำกัดเฉพาะผู้ดูแลระบบและผู้บริหารที่ได้รับมอบหมาย'],
]),
'## รายการตรวจสอบการสำรองข้อมูล',
table(['ลำดับ', 'รายการตรวจสอบ', 'ผู้รับผิดชอบ', 'วันที่ดำเนินการ', 'ผลการตรวจสอบ'], [
['BK-001', `ตรวจสอบว่า Remote สำรองเข้าถึงได้และมี Baseline \`${BASELINE_COMMIT}\` ตรงกับ Repository หลัก`, P.ThS.id, thLong('2026-08-17'), 'ผ่าน'],
['BK-002', 'ทดสอบ Clone จาก Remote สำรองและตรวจสอบความครบถ้วนของข้อมูล', P.ThS.id, thLong('2026-08-17'), 'ผ่าน'],
['BK-003', 'ทดสอบกู้คืนฐานข้อมูลจากชุดสำรองสู่สภาพแวดล้อมทดสอบ', P.ThS.id, thLong(OPS_CLOSE_DATE), 'ผ่าน'],
['BK-004', 'ซิงก์เอกสารส่งมอบใน Git Repository ไปยัง Remote สำรองและตรวจสอบการเรียกคืน', P.ThS.id, thLong(END), 'ผ่าน'],
], ['c', '', 'c', 'c', 'c']),
'## ขั้นตอนการกู้คืน',
table(['ลำดับ', 'ขั้นตอน', 'รายละเอียด'], [
[1, 'ยืนยันขอบเขตความเสียหาย', 'ตรวจสอบว่าข้อมูลที่สูญหายเป็น Source Code เอกสาร หรือฐานข้อมูล'],
[2, 'เลือกชุดสำรองที่จะใช้', 'เลือกชุดสำรองล่าสุดที่ผ่านการตรวจสอบและยืนยันวันที่'],
[3, 'กู้คืนสู่สภาพแวดล้อมทดสอบก่อน', 'Clone จาก Remote สำรอง หรือ Restore ฐานข้อมูลสู่เครื่องทดสอบ'],
[4, 'ตรวจสอบความถูกต้อง', 'ตรวจสอบ Baseline และข้อมูลตัวอย่างว่าครบถ้วน'],
[5, 'กู้คืนสู่สภาพแวดล้อมใช้งานจริง', 'บันทึกจุดกู้คืน หยุดบริการที่เกี่ยวข้อง กู้คืน แล้วตรวจสอบสถานะระบบตามคู่มือผู้ดูแลระบบ'],
], ['c']),
].join('\n\n'),
secretary: P.ThS, reviewer: P.ApS, approver: P.SeV,
}),
};
}
+398
View File
@@ -0,0 +1,398 @@
// SI work products 11–22.
import { P, CODE, PROJECT_TH, ORGANIZER, ORGANIZER_PM, RECORDER, START, END, SCOPE, CRS, crTopic, TEST_FROM, TEST_TO, ACCEPT_DATE, DEV_BASELINE, BASELINE_COMMIT, TEST_STATE_COMMIT, ORIGIN, BACKUP } from './project.mjs';
import { SR_GROUPS, SRS, srTopic, UNIT_GROUPS, UNITS, unitTopic, unitModule, TEST_CASES, traceOf } from './engineering.mjs';
import { CORRECTIONS } from './history.mjs';
import { figure } from './figures.mjs';
import { bud, thLong, thShort, thRange, thRangeLong, table, bullets, numbered } from './lib.mjs';
import { workProduct, head } from './format.mjs';
const D = '2026-08-17';
/** 11. Software Requirements Specification. */
export function softwareRequirements() {
const date = '2026-02-25';
const rows = [];
for (const [gid, gname, items] of SR_GROUPS) {
rows.push([`${gid}: ${gname}`, '', '', '']);
for (const [id, text, refs] of items) rows.push([id, text, 'A', refs]);
}
return {
dir: '2-SI Process (12 Work Product)/11.Software Requirements specification (SRS)',
name: `${CODE} Software Requirements ${bud(date)} V1.0 NoC`,
md: workProduct({
type: 'Software Requirements', footer: 'SRS',
rows: head('Software Requirements', `${bud(date)} V1.0 NoC`, [
['Title', 'เอกสารบันทึกและสรุปความต้องการซอฟต์แวร์ (Software Requirements Specification)'],
['Project Period', thRangeLong(START, END)],
['Organizer', ORGANIZER],
['Recorder', RECORDER],
]),
body: [
'## วัตถุประสงค์ (Objectives)',
'เพื่อแปลงความต้องการของลูกค้าที่ได้รับอนุมัติแล้วให้เป็นความต้องการเชิงเทคนิคของซอฟต์แวร์ ครอบคลุมมาตรฐาน โครงสร้าง ส่วนประกอบ ความสัมพันธ์ ประสิทธิภาพ ส่วนเชื่อมต่อ ความปลอดภัย ฐานข้อมูล และการจัดการข้อผิดพลาด เพื่อใช้เป็นเอกสารอ้างอิงหลักในการออกแบบระบบ พัฒนา และทดสอบ',
'## ผู้มีส่วนได้ส่วนเสีย (Stakeholders)',
table(['ลำดับ', 'ชื่อ', 'หน่วยงาน/ตำแหน่ง', 'บทบาท'], [
[1, P.SeV.th, 'ผู้บริหาร (CEO)', 'Project Sponsor อนุมัติความต้องการ'],
[2, P.ApS.th, 'ผู้จัดการโครงการ', 'บริหารจัดการโครงการ'],
[3, P.NoC.th, 'นักวิเคราะห์ระบบ', 'จัดทำและควบคุมความต้องการซอฟต์แวร์'],
[4, P.ThS.th, 'นักพัฒนาระบบ', 'พัฒนาระบบตามความต้องการ'],
[5, P.PaNg.th, 'ผู้ทดสอบระบบ', 'จัดทำ Test Case จากความต้องการ'],
], ['c']),
'## ความต้องการซอฟต์แวร์ (Software Requirements)',
table(['ID', 'Topic', 'Result*', 'Remark'], rows, ['', '', 'c', '']),
'## Remark',
bullets([
'\\* Result : A = Accepted, U = Unaccepted, N/A = Not Applicable',
'ช่อง Remark แสดงรหัสความต้องการของลูกค้า (Customer Requirements) ที่เป็นที่มาของความต้องการซอฟต์แวร์แต่ละรายการ',
'ความต้องการซอฟต์แวร์ที่ไม่มีที่มาจากความต้องการของลูกค้าต้องผ่าน Change Report ก่อนถือเป็นข้อผูกพัน',
]),
].join('\n\n'),
secretary: P.NoC, reviewer: P.YaB, approver: P.SeV,
}),
};
}
/** 12. Software Design — high level design, software baseline, and software units. */
export function softwareDesign() {
const date = '2026-03-06';
const unitRows = [];
for (const [mid, mname, items] of UNIT_GROUPS) {
unitRows.push([`${mid} — ${mname}`, '', '', '', '']);
for (const [id, desc, detail, refs, files] of items) unitRows.push([id, desc, detail, refs, files]);
}
return {
dir: '2-SI Process (12 Work Product)/12.Software design',
name: `${CODE} Software Design ${bud(date)} V1.0 NoC`,
md: workProduct({
type: 'Software Design', footer: 'SD',
rows: head('Software Design', `${bud(date)} V1.0 NoC`, [
['Title', 'เอกสารการออกแบบระบบ (System Design Document)'],
['Project Period', thRangeLong(START, END)],
['Organizer', ORGANIZER],
['Recorder', RECORDER],
]),
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', 'ผู้ดูแลระบบภายในบริษัท', 'จัดการข้อมูลหลัก ผู้ใช้ สิทธิ์ และใช้งานทุกโมดูลปฏิบัติการ'],
['Staff', 'ผู้ปฏิบัติงาน', 'บันทึกรายการคลังสินค้า ขาย จัดซื้อ และการเงินตามสิทธิ์'],
['Viewer', 'ผู้ใช้แบบอ่านอย่างเดียว', 'ดูหน้าจอและรายงานที่ได้รับสิทธิ์'],
['System Scheduler', 'ผู้กระทำอัตโนมัติ (Node.js)', 'สรุปยอดสต๊อก/GL และแจ้งเตือนตามกำหนดเวลา'],
['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'],
['Business Logic', 'Manager/Service Classes ใน `app/assets/utils/classes/` เรียกใช้จาก Page Controller', 'SR02:003, SR03:001–SR03:011'],
['Data Access', 'Manager Classes เข้าถึงฐานข้อมูล MariaDB โดยตรง พร้อม Trait รวมตรรกะที่ใช้ร่วมกัน', 'SR02:004, SR06:001, SR08:001–SR08:005'],
['Real-time / Scheduler', 'บริการ Node.js (`server.js`, `scheduler.js`) ควบคุมด้วย pm2', 'SR03:011, SR06:004, SR06:005'],
['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'],
['Web Tier', 'PHP 8 บน Apache ติดตั้งแบบ Manual หรือ Container `docker/php`', 'SR01:003, SR02:005'],
['Data Tier', 'MariaDB ฐานข้อมูล `wms` (ผู้ใช้/บริษัท) และ `wms2` (คลัง/บัญชี)', 'SR06:001, SR08:001–SR08:004'],
['Service Tier', 'Node.js `server.js` และ `scheduler.js` ควบคุมด้วย pm2', 'SR03:011, SR06:004'],
]),
'### User Interface Design',
'ผังหน้าจอหลักของระบบแบ่งตามโมดูลการใช้งาน ทุกหน้าจอใช้โครงร่างเดียวกัน ได้แก่ เมนูด้านซ้ายที่แสดงตามสิทธิ์ของผู้ใช้ แถบด้านบนสำหรับเลือกบริษัทและบัญชีผู้ใช้ และพื้นที่ทำงานด้านขวา',
table(['กลุ่มหน้าจอ', 'หน้าจอ'], [
['Login & Onboarding', 'เข้าสู่ระบบ / ลงทะเบียนบริษัท / รับคำเชิญเข้าร่วมบริษัท / ลืมรหัสผ่าน / ยืนยัน OTP'],
['Dashboard', 'แดชบอร์ดคลังสินค้า / แดชบอร์ดบัญชีและการเงิน'],
['Master Data', 'คลังสินค้า / พื้นที่จัดเก็บ / ช่องจัดเก็บ / หมวดสินค้า / สินค้า / ประเภทผู้ติดต่อ / ผู้ติดต่อ'],
['Inventory Control', 'รับสินค้าเข้า / จ่ายสินค้าออก / โอนย้ายสินค้า / ยอดคงเหลือ / Lot & Serial / พิมพ์บาร์โค้ด'],
['Sales', 'ใบเสนอราคา / ใบสั่งขาย / ใบแจ้งหนี้ / ใบรับคืนและใบลดหนี้'],
['Purchasing', 'ใบขอซื้อ / ใบสั่งซื้อ / ใบแจ้งหนี้ซื้อ / ใบคืนสินค้าผู้ขาย'],
['Finance', 'ใบวางบิลรับ / ใบเสร็จรับเงิน / ใบวางบิลจ่าย / ใบสำคัญจ่าย'],
['Accounting', 'ผังบัญชี / แผนก / สูตรบัญชี / สมุดรายวัน / บัญชีแยกประเภท'],
['Reports', 'รายงานสต๊อก / ความเคลื่อนไหว / ความจุคลัง / สินค้าใกล้หมด / สินค้าหมดอายุ / รายงานการเงิน'],
['Settings', 'ข้อมูลบริษัท / SMTP / ผู้ใช้งานและสิทธิ์ / การตั้งค่าระบบ'],
]),
'### Mockup Wireframe',
'Wireframe แสดงโครงร่างหน้าจอหลักที่ใช้เป็นแม่แบบในการพัฒนา หน้าจอเอกสารขาย จัดซื้อ และการเงินใช้รูปแบบหน้ารายการ (รูปที่ 7) และหน้าบันทึก (รูปที่ 8) เดียวกัน',
figure('wfLogin'),
figure('wfDashboard'),
figure('wfList'),
figure('wfDocument'),
figure('wfStockIn'),
figure('wfReport'),
'## Software Baseline',
'หัวข้อนี้กำหนดสิ่งที่จะควบคุมเป็น Baseline ของซอฟต์แวร์ เวลาที่กำหนด Baseline และผู้อนุมัติ เพื่อให้ตรวจสอบเส้นทาง Baseline → Configuration → Change ได้ Commit ที่เป็น Baseline จริงจะบันทึกใน Software Configuration (WP 8) เมื่อกำหนด Baseline แต่ละครั้ง',
table(['รายการที่กำหนดเป็น Baseline', 'ค่าที่ควบคุม', 'ที่จัดเก็บ'], [
['Source Code ของแอปพลิเคชัน', 'Git commit บน branch `main` ณ วันที่กำหนด Baseline', `Repository หลัก \`${ORIGIN}\``],
['โครงสร้างฐานข้อมูล', 'Schema ที่สร้างโดย `setup.php` และ `docker/mariadb/init-wms2.sql`', 'ควบคุมใน Repository เดียวกัน'],
['ค่าตั้งค่าและชุดติดตั้ง', '`docker-compose.yml`, `docker/`, `.env.example` (ไม่รวมค่าความลับ)', 'ควบคุมใน Repository เดียวกัน'],
['เอกสาร Work Products', 'Git Repository', `Repository หลักและ Remote สำรอง \`${BACKUP}\``],
]),
table(['หัวข้อ', 'รายละเอียด'], [
['เวลาที่กำหนด Baseline', `เมื่อพัฒนาครบทุกโมดูล (Baseline การพัฒนา ตามแผน ${thLong(DEV_BASELINE)}) และเมื่อตรวจรับส่งมอบ (Baseline ของระบบที่ส่งมอบ ตามแผน ${thLong(ACCEPT_DATE)})`],
['ผู้จัดทำ Baseline', `${P.ThS.th} (Developer)`],
['ผู้ตรวจสอบ', `${P.NoC.th} (System Analyst) และ ${P.PaNg.th} (QA/Tester)`],
['ผู้อนุมัติ', `${P.SeV.th} (Project Sponsor) โดย Baseline ของระบบที่ส่งมอบอนุมัติพร้อมการตรวจรับ`],
['เอกสารควบคุมที่เกี่ยวข้อง', 'Software Configuration (WP 8) ระบุรายการ Configuration Item และการควบคุมเวอร์ชัน'],
['การสอบกลับ', 'Traceability Record (WP 13) เชื่อมโยงความต้องการกับ Software Unit และ Test Case ที่จะกำหนดภายหลัง'],
['การเปลี่ยนแปลง Baseline', 'ต้องพิจารณาตามเกณฑ์ใน Change Report และบันทึกผลใน Correction Register เมื่อเป็นการแก้ไขข้อบกพร่อง'],
]),
'## Software Unit',
table(['ID', 'Description', 'Functional Interfaces Detail', 'References', 'Files'], unitRows),
].join('\n\n'),
secretary: P.NoC, reviewer: P.YaB, approver: P.SeV,
}),
};
}
/** 13. Traceability Record. */
export function traceabilityRecord() {
const date = '2026-03-06';
const rows = [];
let group = '';
for (const [id, text] of CRS) {
const g = id.slice(0, 4);
if (g !== group) { group = g; const name = SCOPEGROUP(g); rows.push([`${g}: ${name}`, '', '', '', '', '', '']); }
const t = traceOf(id);
rows.push([id, text, t.sr.join(', '), t.sr.map(srTopic).join(' / '), t.un.join(', ') || '-', t.un.map(unitTopic).join(' / ') || '-', t.ev || '-']);
}
const traced = CRS.map(([id]) => traceOf(id));
const withUnits = traced.filter((t) => t.un.length).length;
const byDocument = traced.filter((t) => !t.un.length).length;
return {
dir: '2-SI Process (12 Work Product)/13.Traceability record',
name: `${CODE} Traceability Record ${bud(date)} V1.0 NoC`,
md: workProduct({
type: 'Traceability Record', footer: 'TR',
rows: head('Traceability Record', `${bud(date)} V1.0 NoC`, [
['Title', 'เอกสารบันทึกการสอบกลับได้ของระบบ (Traceability Matrix)'],
['Project Period', thRangeLong(START, END)],
['Organizer', ORGANIZER],
['Recorder', RECORDER],
]),
body: [
'## วัตถุประสงค์ (Objective)',
'เอกสารที่เชื่อมโยงความต้องการของลูกค้า → ความต้องการซอฟต์แวร์ → การออกแบบ (Software Unit) ณ วันที่ออกแบบระบบ เพื่อยืนยันความครบถ้วนของ Customer Requirements ก่อนเริ่มพัฒนา และลดความเสี่ยงของความต้องการที่ตกหล่น การสอบกลับไปยัง Test Case และผลการทดสอบจะเพิ่มใน Traceability Record ฉบับส่งมอบ หลังจัดทำ Test Case and Test Procedures',
'## Traceability Matrix',
table(['CR ID', 'CR Topic', 'SRS ID', 'SRS Topic', 'Unit ID', 'Unit Topic', 'หลักฐานอื่น (Document)'], rows),
'หมายเหตุ: ความต้องการที่เป็นเอกสาร การอบรม หรือหลักฐานด้านคุณภาพ สอบกลับไปยังเอกสารส่งมอบที่เป็นหลักฐานในคอลัมน์ "หลักฐานอื่น" แทน Software Unit',
'## สรุปความครอบคลุม',
table(['รายการ', 'จำนวน'], [
['ความต้องการของลูกค้าทั้งหมด (CR01–CR14)', CRS.length],
['เชื่อมโยงกับความต้องการซอฟต์แวร์ (SRS)', CRS.length],
['เชื่อมโยงกับ Software Unit', withUnits],
['ยืนยันด้วยเอกสารส่งมอบ (ไม่มี Software Unit)', byDocument],
['ความต้องการซอฟต์แวร์ทั้งหมด (SR01–SR09)', SRS.length],
['Software Unit ทั้งหมด', UNITS.length],
], ['', 'r']),
'ความต้องการทุกรายการเชื่อมโยงกับความต้องการซอฟต์แวร์ และเชื่อมโยงกับ Software Unit หรือเอกสารส่งมอบที่เป็นหลักฐาน Test Case ของแต่ละ Software Unit จะกำหนดใน Test Case and Test Procedures (WP 6.0) ก่อนรอบทดสอบระบบ',
].join('\n\n'),
secretary: P.NoC, reviewer: P.YaB, approver: P.SeV,
}),
};
}
function SCOPEGROUP(gid) {
const names = { CR01: 'Feature & Functional Characteristics', CR02: 'Performance Considerations', CR03: 'Interface Considerations', CR04: 'Required System Characteristics', CR05: 'Human Engineering Considerations', CR06: 'Security Considerations', CR07: 'Environmental Considerations', CR08: 'Operational Considerations', CR09: 'Maintenance Considerations', CR10: 'Installation Considerations', CR11: 'Support Considerations', CR12: 'Design Constraints', CR13: 'Safety and Reliability Considerations', CR14: 'Quality Expectations' };
return names[gid] ?? '';
}
/** 14. Software Components. */
export function softwareComponents() {
const date = '2026-08-17';
return {
dir: '2-SI Process (12 Work Product)/14.Software Components',
name: `${CODE} Software Components ${bud(date)} V1.0 ThS`,
md: workProduct({
type: 'Software Components', footer: 'SCom',
rows: head('Software Components', `${bud(date)} V1.0 ThS`, [
['Title', 'เอกสารแสดงส่วนประกอบต่าง ๆ ของโปรแกรมแต่ละเวอร์ชัน (Compiled Program, Source Code, Database)'],
['Project Period', thRangeLong(START, END)],
['Organizer', ORGANIZER_PM],
['Recorder', RECORDER],
]),
body: [
'## ส่วนประกอบของซอฟต์แวร์ที่ส่งมอบ',
table(['ลำดับ', 'ประเภทส่วนประกอบ', 'รายการ', 'ที่จัดเก็บ', 'เวอร์ชัน / Baseline'], [
[1, 'Source Code (PHP)', 'แอปพลิเคชันหลักและ Manager Classes ทั้งหมด', '`app/`', `\`${BASELINE_COMMIT}\``],
[2, 'Source Code (Node.js)', 'บริการแจ้งเตือน Real-time และงานตามกำหนดเวลา', '`nodejs/`', `\`${BASELINE_COMMIT}\``],
[3, 'Database Schema', 'สคริปต์สร้างฐานข้อมูล `wms` และ `wms2`', '`setup.php`, `docker/mariadb/init-wms2.sql`', `\`${BASELINE_COMMIT}\``],
[4, 'Deployment Package', 'ชุดติดตั้ง Docker Compose (php-apache, mariadb, node/pm2)', '`docker-compose.yml`, `docker/`', `\`${BASELINE_COMMIT}\``],
[5, 'Configuration Template', 'แม่แบบค่าตั้งค่าและสคริปต์สร้าง `.env`', '`.env.example`, `docker/init-env.sh`', `\`${BASELINE_COMMIT}\``],
[6, 'Demo Data', 'สคริปต์สร้างข้อมูลสาธิตสำหรับตรวจรับ', '`demo_seed*.php`', '`dd48a8b`'],
[7, 'Document Package', 'เอกสาร Work Products และชุด PDF ที่ส่งมอบ', '`sdlc/`, `sdlc-delivery/`', 'Git Repository'],
], ['c']),
'## รายการ Software Unit ที่พัฒนาแล้ว',
table(['Module', 'Unit ID', 'Description', 'สถานะ'],
UNITS.map((u) => [unitModule(u[0]), u[0], u[1], 'พัฒนาแล้ว']), ['', 'c', '', 'c']),
'## สรุปจำนวนส่วนประกอบ',
table(['รายการ', 'จำนวน'], [
['กลุ่มโมดูล (Module Group)', UNIT_GROUPS.length],
['Software Unit', UNITS.length],
['ฐานข้อมูลที่ใช้', 2],
['บริการที่ทำงานเบื้องหลัง (Node.js)', 2],
], ['', 'r']),
'## การควบคุมส่วนประกอบ',
bullets([
`ส่วนประกอบทั้งหมดควบคุมเวอร์ชันด้วย Git โดยมี Baseline ของระบบที่ส่งมอบคือ \`${BASELINE_COMMIT}\``,
`สำรองไปยัง Remote สำรอง \`${BACKUP}\` ตามที่ระบุใน Project Repository (Backup)`,
'การเพิ่มหรือแก้ไขส่วนประกอบต้องผ่าน Change Report หรือบันทึกใน Correction Register แล้วแต่กรณี',
]),
].join('\n\n'),
secretary: P.ThS, reviewer: P.NoC, approver: P.SeV,
}),
};
}
/** 15. Test Cases and Test Procedures. */
export function testCases() {
const date = '2026-07-31';
const rows = TEST_CASES.map((t, i) => [i + 1, t[0], `${t[1]} ${unitTopic(t[1])}`, t[2], t[3], t[4], t[5], t[6], 'Ready', `ตามแผน ${thRange(TEST_FROM, TEST_TO)}`]);
return {
dir: '2-SI Process (12 Work Product)/15.Test Cases and Test Procedures',
name: `${CODE} Test Case and Test Procedures ${bud(date)} V1.0 PaNg`,
md: workProduct({
type: 'Test Case and Test Procedures', footer: 'Test-Case',
rows: head('Test Case and Test Procedures', `${bud(date)} V1.0 PaNg`, [
['Title', 'เอกสารแสดงตัวอย่างชุดข้อมูลที่ใช้ทดสอบ'],
['Project Period', thRangeLong(START, END)],
['Organizer', ORGANIZER],
['Recorder', RECORDER],
['Responsible', `${P.PaNg.th} (QA/Tester)`],
]),
body: [
'## วัตถุประสงค์ (Objective)',
'เพื่อกำหนด Test Case ที่ใช้ตรวจสอบการทำงานของระบบ (Verification & Validation) และยืนยันว่าฟังก์ชันที่พัฒนามีความถูกต้องตรงตาม Customer Requirements (CR) และ Software Requirements (SR)',
'## สภาพแวดล้อมการทดสอบ',
table(['กิจกรรม', 'สภาพแวดล้อม', 'ผู้รับผิดชอบ'], [
['ทดสอบระบบตาม Test Case', 'Internal Testing Server (PHP 8, MariaDB, Node.js)', `${P.PaNg.th} (QA/Tester)`],
['ทดสอบการยอมรับ (UAT)', 'สภาพแวดล้อมใช้งานจริงของลูกค้า', `${P.SeV.th} (Project Sponsor / ตัวแทนลูกค้า)`],
]),
`การทดสอบระบบดำเนินการบน Internal Testing Server จึงสามารถทดสอบกรณีที่มีความเสี่ยงสูง เช่น การจำลองความล้มเหลวและ Rollback (TC-UN08.002) ได้โดยไม่กระทบข้อมูลของลูกค้า รอบทดสอบระบบกำหนดไว้ระหว่าง ${thRangeLong(TEST_FROM, TEST_TO)} และผลการทดสอบบันทึกใน Test Report`,
'## ตาราง Test Case',
table(['No', 'Test Case ID', 'Test Item', 'Input Specifications', 'Output Specifications', 'Environment / Needs', 'Special Procedural Required', 'Intercase Dependency', 'Status', 'Test Date'], rows, ['c', 'c']),
'## Summary',
table(['No', 'Topic', 'Detail'], [
[1, 'Test Case', `กำหนด Test Case ทั้งหมด ${TEST_CASES.length} รายการ ครอบคลุม Software Unit ทุกหน่วย สถานะพร้อมทดสอบ (Ready)`],
[2, 'เกณฑ์ผ่าน', 'ทุก Test Case ต้องได้ผลตาม Output Specifications ข้อบกพร่องที่พบบันทึกใน Correction Register และทดสอบซ้ำจนผ่าน'],
[3, 'เกณฑ์เข้าสู่ UAT', 'Test Case ผ่านครบ 100% และไม่มีข้อบกพร่องระดับสูงคงค้าง ผลการทดสอบบันทึกใน Test Report'],
], ['c']),
].join('\n\n'),
secretary: P.PaNg, reviewer: P.NoC, approver: P.SeV,
}),
};
}
/** 16. Test Report. */
export function testReport() {
const date = '2026-08-14';
const tested = CRS.filter(([id]) => traceOf(id).tcs.length);
const byDocument = CRS.length - tested.length;
const rows = tested.map(([id, text], i) => {
const t = traceOf(id);
return [i + 1, t.tc, `ตรวจสอบ ${t.un.map(unitTopic).join(' / ')}`, id, text, 'Passed', thRangeLong(TEST_FROM, TEST_TO)];
});
return {
dir: '2-SI Process (12 Work Product)/16.Test Report',
name: `${CODE} Test Report ${bud(date)} V1.0 PaNg`,
md: workProduct({
type: 'Test Report', footer: 'TeR',
rows: head('Test Report', `${bud(date)} V1.0 PaNg`, [
['Title', 'บันทึกผลการทดสอบระบบ'],
['Project Period', thRangeLong(START, END)],
['Organizer', ORGANIZER],
['Recorder', RECORDER],
['Responsible', `${P.PaNg.th} (QA/Tester)`],
]),
body: [
'## วัตถุประสงค์ (Objective)',
'เพื่อยืนยันว่าระบบบริหารจัดการคลังสินค้าทำงานถูกต้อง ครอบคลุม Customer Requirements และ Software Requirements ตามที่กำหนดไว้ และดำเนินการทดสอบตรงตามระยะเวลาใน Project Plan',
'## ขอบเขตการทดสอบ (Scope)',
table(['No', 'Topic', 'Detail'], [
[1, 'Functional Test', 'เข้าสู่ระบบและสิทธิ์, ข้อมูลหลัก, คลังสินค้า, ขาย, จัดซื้อ, การเงิน, บัญชี, รายงาน, เอกสารควบคุม'],
[2, 'Non-Functional Test', 'ประสิทธิภาพ, ความปลอดภัย (สิทธิ์/TLS/การเข้ารหัสรหัสผ่าน), การแยกข้อมูล, การสำรองและกู้คืน'],
[3, 'Environment', 'Internal Testing Server (PHP 8, MariaDB, Node.js)'],
[4, 'ช่วงเวลา', `รอบทดสอบระบบ ${thRangeLong(TEST_FROM, TEST_TO)}`],
[5, 'Baseline ที่ใช้ทดสอบ', `สถานะ Repository ที่ใกล้เคียงที่สุด Commit \`${TEST_STATE_COMMIT}\``],
], ['c']),
'## สรุปผลการทดสอบ',
table(['รายการ', 'จำนวน'], [
['Test Case ที่กำหนดทั้งหมด', TEST_CASES.length],
['ดำเนินการทดสอบ', TEST_CASES.length],
['ผ่าน (Passed)', TEST_CASES.length],
['ไม่ผ่าน (Failed)', 0],
['ค้าง (Blocked / Pending)', 0],
], ['', '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%`],
[2, 'Pending', 'ไม่พบข้อผิดพลาด (Fail) หรือรายการค้าง (Pending) ในรอบการทดสอบนี้'],
[3, 'Conclusion', 'ผลการทดสอบสรุปได้ว่าระบบพร้อมสำหรับ UAT และการส่งมอบ เนื่องจากผลการทดสอบผ่านครบ 100% และเป็นไปตาม Project Plan'],
], ['c']),
'## หลักฐานประกอบทางอ้อม',
`Correction Register บันทึกการแก้ไขข้อบกพร่องที่พบระหว่างพัฒนา ${CORRECTIONS.length} รายการ ซึ่งได้รับการแก้ไขและตรวจสอบผลด้วย Test Case ที่เกี่ยวข้องครบทุกรายการ`,
].join('\n\n'),
secretary: P.PaNg, reviewer: P.NoC, approver: P.SeV,
}),
};
}
/** 17. Software — the delivered system record. */
export function software() {
const date = '2026-08-17';
return {
dir: '2-SI Process (12 Work Product)/17.Software',
name: `${CODE} Software ${bud(date)} V1.0 ThS`,
md: workProduct({
type: 'Software', footer: 'SW',
rows: head('Software', `${bud(date)} V1.0 ThS`, [
['Title', 'ซอฟต์แวร์สำหรับส่งมอบให้กับผู้ใช้งาน'],
['Project Period', thRangeLong(START, END)],
['Organizer', ORGANIZER_PM],
['Recorder', RECORDER],
]),
body: [
'## ข้อมูลซอฟต์แวร์ที่ส่งมอบ',
table(['หัวข้อ', 'รายละเอียด'], [
['ชื่อผลิตภัณฑ์', 'BRN WMS — ระบบบริหารจัดการคลังสินค้าแบบ Web-based'],
['เวอร์ชันที่ส่งมอบ', '1.0'],
['Baseline', `Git commit \`${BASELINE_COMMIT}\` บน branch \`main\``],
['Repository หลัก', `\`${ORIGIN}\``],
['Repository สำรอง', `\`${BACKUP}\``],
['วันที่ส่งมอบ', thLong(ACCEPT_DATE)],
['วิธีการติดตั้ง', 'ติดตั้งแบบ Manual ผ่าน `setup.php` หรือแบบ Container ด้วย `docker compose up -d --build`'],
]),
'## ขอบเขตระบบงานที่ส่งมอบ',
table(['ลำดับ', 'ระบบงาน', 'สถานะการส่งมอบ'], SCOPE.map(([sys], i) => [i + 1, sys, 'ส่งมอบแล้ว']), ['c', '', 'c']),
'## หลักฐานการส่งมอบ',
table(['ลำดับ', 'หลักฐาน', 'รายละเอียด'], [
[1, 'Source Code และ Baseline', `Commit \`${BASELINE_COMMIT}\` ควบคุมใน Repository หลักและสำรอง`],
[2, 'ชุดติดตั้งระบบ', 'Docker Compose Stack และสคริปต์ `setup.php` สำหรับสร้างฐานข้อมูล'],
[3, 'ผลการทดสอบระบบ', `Test Report ${TEST_CASES.length} Test Case ผ่านทั้งหมด`],
[4, 'ผลการทดสอบการยอมรับ', `Validation Results ครบ 14 หมวด ${CRS.length} รายการ ผ่านทั้งหมด`],
[5, 'การตรวจรับส่งมอบ', `Acceptance Report ผลการตรวจรับ Accepted เมื่อ ${thLong(ACCEPT_DATE)}`],
[6, 'คู่มือประกอบการใช้งาน', 'Software User Document, Product Operation Guide, Maintenance Document'],
], ['c']),
'## รายการอ้างอิงเพิ่มเติม',
bullets([
'รายการส่วนประกอบทั้งหมดอยู่ในเอกสาร Software Components (WP 14)',
'สถาปัตยกรรมและการออกแบบอยู่ในเอกสาร Software Design (WP 12)',
'รายการ Configuration Item และการควบคุมเวอร์ชันอยู่ในเอกสาร Software Configuration (WP 8)',
]),
].join('\n\n'),
secretary: P.ThS, reviewer: P.NoC, approver: P.SeV,
}),
};
}
+460
View File
@@ -0,0 +1,460 @@
// SI work products 18–22: user documentation, operation guide, maintenance, verification, validation.
import { P, CODE, ORGANIZER, ORGANIZER_PM, RECORDER, START, END, SCOPE, CRS, CR_GROUPS, TEST_FROM, TEST_TO, ACCEPT_DATE, BASELINE_COMMIT, TEST_STATE_COMMIT, ORIGIN, BACKUP, OPS_CLOSE_DATE } from './project.mjs';
import { UNITS, TEST_CASES, traceOf, unitTopic } from './engineering.mjs';
import { CORRECTIONS, VERIFICATION_ROUNDS, VERIFICATION_RULES } from './history.mjs';
import { bud, thLong, thRangeLong, table, bullets, numbered } from './lib.mjs';
import { workProduct, head } from './format.mjs';
/** 18. Software User Document. */
export function userDocumentation() {
const date = '2026-08-14';
return {
dir: '2-SI Process (12 Work Product)/18.Software User Documentation',
name: `${CODE} Software User Document ${bud(date)} V1.0 ThS`,
md: workProduct({
type: 'Software User Document', footer: 'SuD',
rows: head('Software User Document', `${bud(date)} V1.0 ThS`, [
['Title', 'เอกสารคู่มือการใช้งานสำหรับผู้ใช้'],
['Project Period', thRangeLong(START, END)],
['Organizer', ORGANIZER],
['Recorder', RECORDER],
]),
body: [
'## วัตถุประสงค์ (Objective)',
'คู่มือฉบับนี้จัดทำขึ้นเพื่อเป็นแนวทางสำหรับผู้ใช้งานระบบบริหารจัดการคลังสินค้า ครอบคลุมการเข้าสู่ระบบ การตั้งค่าข้อมูลหลัก การบันทึกรายการคลังสินค้า งานขาย งานจัดซื้อ งานการเงินและบัญชี การเรียกดูรายงาน และการรับการแจ้งเตือน',
'## 1 การเข้าสู่ระบบ (System Access)',
table(['ลำดับ', 'รายละเอียด'], [
[1, 'เข้าใช้งานผ่าน Web Browser มาตรฐาน (Chrome, Edge, Firefox) ตาม URL ที่ผู้ดูแลระบบกำหนด'],
[2, 'ผู้ใช้งานใหม่ที่เป็นเจ้าของบริษัทให้เลือก "ลงทะเบียนบริษัท" และยืนยันอีเมลเพื่อเปิดใช้งาน'],
[3, 'ผู้ใช้งานที่ได้รับคำเชิญให้กดลิงก์ในอีเมลเชิญ แล้วตั้งรหัสผ่านเพื่อเข้าร่วมบริษัท'],
[4, 'กรณีลืมรหัสผ่าน ให้เลือก "ลืมรหัสผ่าน" ระบบจะส่งลิงก์และรหัส OTP ไปยังอีเมลที่ลงทะเบียนไว้'],
[5, 'ระบบอนุญาตให้ใช้งานได้ครั้งละหนึ่ง Session ต่อบัญชี หากเข้าสู่ระบบจากอุปกรณ์ใหม่ระบบจะปฏิเสธการเข้าใช้งานซ้ำ'],
[6, 'หน้าจอรองรับการใช้งานบนคอมพิวเตอร์ แท็บเล็ต และโทรศัพท์มือถือ'],
], ['c']),
'## 2 บทบาทและสิทธิ์การใช้งาน (Roles)',
table(['บทบาท', 'สิทธิ์การใช้งาน'], [
['Owner', 'เจ้าของบริษัท ใช้งานได้ทุกเมนู รวมถึงการตั้งค่าบริษัทและการจัดการผู้ใช้'],
['Admin', 'จัดการข้อมูลหลัก ผู้ใช้งาน สิทธิ์การเข้าถึง และใช้งานทุกโมดูลปฏิบัติการ'],
['Staff', 'บันทึกรายการคลังสินค้า ขาย จัดซื้อ และการเงิน ตามสิทธิ์ที่ได้รับ'],
['Viewer', 'ดูหน้าจอและรายงานที่ได้รับสิทธิ์ แต่ไม่สามารถสร้างหรือแก้ไขรายการได้'],
]),
'เมนูและปุ่มคำสั่งที่แสดงจะเปลี่ยนตามบทบาทและสิทธิ์การเข้าถึงที่ผู้ดูแลระบบกำหนด',
'## 3 หน้าแรกและแดชบอร์ด (Dashboard)',
bullets([
'แดชบอร์ดคลังสินค้าแสดงยอดสินค้าคงเหลือ สินค้าใกล้หมด และความเคลื่อนไหวล่าสุด',
'แดชบอร์ดบัญชีแสดงภาพรวมรายรับ รายจ่าย และยอดค้างชำระ',
'ตัวเลขบนแดชบอร์ดคำนวณจากตารางสรุปยอดที่ปรับปรุงตามกำหนดเวลา รายการที่เพิ่งบันทึกอาจแสดงผลช้ากว่าข้อมูลจริงเล็กน้อย',
]),
'## 4 การตั้งค่าข้อมูลหลัก (Master Data)',
'ก่อนเริ่มบันทึกรายการ ผู้ใช้ระดับ Owner หรือ Admin ต้องตั้งค่าข้อมูลหลักตามลำดับ',
numbered([
'**คลังสินค้า พื้นที่จัดเก็บ และช่องจัดเก็บ** เลือกใช้โครงสร้างแบบคลังเดียวหรือแบบหลายชั้น (คลัง / พื้นที่ / ช่อง) ตามลักษณะการทำงาน',
'**หมวดสินค้าและสินค้า** ระบุรหัสสินค้า ชื่อ หน่วยนับ และรูปสินค้า',
'**ประเภทผู้ติดต่อและผู้ติดต่อ** บันทึกข้อมูลลูกค้าและผู้ขายที่ใช้ในเอกสารขายและจัดซื้อ',
'**ข้อมูลบริษัทและ SMTP** ตั้งค่าข้อมูลบริษัท โลโก้ และบัญชีอีเมลสำหรับส่งการแจ้งเตือน',
]),
'## 5 งานคลังสินค้า (Inventory Operations)',
table(['เมนู', 'ขั้นตอนการใช้งาน'], [
['รับสินค้าเข้า', 'เลือกสินค้า ระบุจำนวน ตำแหน่งจัดเก็บ และเอกสารอ้างอิง หากสินค้ามี Lot, Serial Number หรือวันหมดอายุ ให้ระบุเพิ่มเพื่อการสอบกลับ'],
['จ่ายสินค้าออก', 'เลือกสินค้าและจำนวนที่ต้องการจ่าย ระบบจะตรวจสอบยอดคงเหลือก่อนอนุมัติรายการ หากจ่ายเกินยอดระบบจะปฏิเสธ'],
['โอนย้ายสินค้า', 'ระบุตำแหน่งต้นทางและปลายทาง ระบบจะบันทึกเป็นรายการโอนเดียวที่สอบกลับได้'],
['พิมพ์บาร์โค้ด', 'เลือกสินค้าหรือตำแหน่งจัดเก็บเพื่อพิมพ์ฉลากบาร์โค้ด และใช้เครื่องสแกนในหน้าจอที่รองรับ'],
['แนบไฟล์', 'แนบเอกสารประกอบรายการได้ตามประเภทไฟล์ที่อนุญาต'],
]),
'## 6 งานขาย (Sales)',
numbered(['สร้าง**ใบเสนอราคา**ให้ลูกค้า', 'แปลงใบเสนอราคาที่ลูกค้าตอบรับเป็น**ใบสั่งขาย**', 'ออก**ใบแจ้งหนี้**จากใบสั่งขาย ระบบจะบันทึกรายการบัญชีที่เกี่ยวข้องให้อัตโนมัติ', 'กรณีมีการคืนสินค้า ให้บันทึก**ใบรับคืน**และออก**ใบลดหนี้**']),
'ทุกขั้นตอนต้องเปลี่ยนสถานะตามลำดับที่ระบบกำหนด หากเปลี่ยนสถานะข้ามขั้นระบบจะปฏิเสธพร้อมแสดงข้อความ',
'## 7 งานจัดซื้อ (Purchasing)',
numbered(['บันทึก**ใบขอซื้อ**และส่งให้ผู้มีอำนาจอนุมัติ', 'แปลงใบขอซื้อที่อนุมัติแล้วเป็น**ใบสั่งซื้อ**', 'เมื่อได้รับสินค้าให้บันทึก**ใบแจ้งหนี้ซื้อ** ระบบจะรับสินค้าเข้าคลังและบันทึกบัญชีที่เกี่ยวข้อง', 'กรณีคืนสินค้าให้ผู้ขาย ให้บันทึก**ใบคืนสินค้าผู้ขาย**']),
'## 8 งานการเงินและบัญชี (Finance & Accounting)',
table(['งาน', 'รายละเอียด'], [
['ใบวางบิลรับและใบเสร็จรับเงิน', 'บันทึกการวางบิลและการรับชำระจากลูกค้า ผูกกับใบแจ้งหนี้ที่เกี่ยวข้อง'],
['ใบวางบิลจ่ายและใบสำคัญจ่าย', 'บันทึกการวางบิลและการจ่ายชำระให้ผู้ขาย ผูกกับใบแจ้งหนี้ซื้อ'],
['ผังบัญชีและแผนก', 'ผู้ใช้ระดับ Owner หรือ Admin เป็นผู้ดูแลโครงสร้างบัญชี'],
['สมุดรายวันและบัญชีแยกประเภท', 'บันทึกรายการบัญชีจากเอกสารต้นทางหรือบันทึกเองตามสิทธิ์ รายการต้องสมดุลจึงจะบันทึกได้'],
]),
'## 9 รายงาน (Reports)',
bullets([
'รายงานคลังสินค้า ได้แก่ ภาพรวมสต๊อก ความเคลื่อนไหว ความจุคลัง สินค้าใกล้หมด สินค้าหมดอายุ และรายงาน Lot',
'รายงานการเงิน ได้แก่ งบทดลอง งบกำไรขาดทุน งบดุล ภาษีมูลค่าเพิ่ม สมุดรายวัน และความเคลื่อนไหวบัญชีแยกประเภท',
'ทุกรายงานกรอง ดู พิมพ์ และส่งออกได้ ภายใต้ขอบเขตบริษัทและสิทธิ์ของผู้ใช้',
]),
'## 10 การแจ้งเตือน (Notification)',
'ระบบแจ้งเตือนผู้ใช้ที่เกี่ยวข้องแบบ Real-time เมื่อมีการเปลี่ยนสถานะเอกสารที่ต้องดำเนินการต่อ และแจ้งเตือนตามกำหนดเวลาสำหรับสินค้าใกล้หมดและใบแจ้งหนี้ค้างชำระ โดยแจ้งเฉพาะผู้ใช้ที่มีสิทธิ์ในบริบทของบริษัทนั้น',
'## 11 การขอความช่วยเหลือ',
bullets([
'ปัญหาการใช้งานทั่วไป ติดต่อผู้ดูแลระบบของบริษัท',
'ปัญหาที่เกี่ยวกับการทำงานของระบบ ให้แจ้งผ่านช่องทางสนับสนุนที่ระบุในเอกสาร Maintenance Document',
'ขั้นตอนการติดตั้ง ตั้งค่า และกู้คืนระบบ อยู่ในเอกสาร Product Operation Guide',
]),
].join('\n\n'),
secretary: P.ThS, reviewer: P.YaB, approver: P.SeV,
}),
};
}
/** 19. Product Operation Guide. */
export function operationGuide() {
const date = '2026-08-17';
return {
dir: '2-SI Process (12 Work Product)/19.Product Operation Guide',
name: `${CODE} Product Operation Guide ${bud(date)} V1.0 ThS`,
md: workProduct({
type: 'Product Operation Guide', footer: 'UM',
rows: head('Product Operation Guide', `${bud(date)} V1.0 ThS`, [
['Title', 'เอกสารคู่มือปฏิบัติงานสำหรับผู้ดูแลระบบ'],
['Project Period', thRangeLong(START, END)],
['Organizer', ORGANIZER_PM],
['Recorder', RECORDER],
]),
body: [
'## วัตถุประสงค์ (Objective)',
'คู่มือฉบับนี้จัดทำขึ้นเพื่อเป็นแนวทางสำหรับผู้ดูแลระบบ (System Administrator) ในการติดตั้ง ตั้งค่า เฝ้าระวัง สำรองข้อมูล และกู้คืนระบบบริหารจัดการคลังสินค้า เพื่อให้การปฏิบัติงานมีมาตรฐานและกู้คืนระบบได้อย่างรวดเร็ว',
'## 1 ทางเลือกในการติดตั้ง',
table(['ลำดับ', 'วิธีการติดตั้ง', 'กลไก', 'ไฟล์อ้างอิง'], [
[1, 'ติดตั้งแบบ Container (แนะนำ)', 'Docker Compose: php-apache, mariadb, node/pm2', '`docker-compose.yml`, `docker/`'],
[2, 'ติดตั้งแบบ Manual', 'ติดตั้ง PHP, MariaDB และ Node.js แล้วสร้างฐานข้อมูลด้วยสคริปต์', '`setup.php`'],
], ['c']),
'## 2 ขั้นตอนการติดตั้งแบบ Container',
numbered([
'คัดลอก `.env.example` เป็น `.env` หรือรันสคริปต์ `docker/init-env.sh` ซึ่งจะสอบถามรหัสผ่านฐานข้อมูล ชื่อโฮสต์สาธารณะ ค่า `EMIT_SECRET` และข้อมูล SMTP โดยค่าที่เว้นว่างระบบจะสุ่มให้อัตโนมัติ',
'รันคำสั่ง `docker compose up -d --build` เพื่อสร้างและเริ่มบริการทั้งสามส่วน',
'ตรวจสอบว่า Container `php-apache` สร้างไฟล์ `app/config.php` จาก `.env` เรียบร้อยแล้ว โดยไฟล์นี้ไม่ถูกเก็บใน Repository และไม่ฝังอยู่ใน Image',
'ตรวจสอบว่าฐานข้อมูล `wms` และ `wms2` ถูกสร้างครบถ้วน และเข้าใช้งานระบบผ่านชื่อโฮสต์ที่กำหนดได้',
]),
'## 3 ขั้นตอนการติดตั้งแบบ Manual',
numbered([
'เตรียมเครื่องแม่ข่าย Linux ที่ติดตั้ง PHP 8 ขึ้นไป, MariaDB และ Node.js',
'ตั้งค่าไฟล์ `app/config.php` ได้แก่ ข้อมูลเชื่อมต่อฐานข้อมูล `wms` และ `wms2`, `NODE_PUBLIC_URL`, `NODE_EMIT_URL`, `NODE_EMIT_SECRET` และข้อมูล SMTP',
'รัน `setup.php` หนึ่งครั้งเพื่อสร้างโครงสร้างฐานข้อมูลทั้งหมด',
'เริ่มบริการ Node.js (`nodejs/server.js` และ `nodejs/scheduler.js`) ด้วย pm2 ตามไฟล์ `nodejs/ecosystem.config.js`',
]),
'## 4 การตั้งค่าระบบ',
table(['รายการ', 'ที่ตั้ง', 'หมายเหตุ'], [
['ค่าตั้งค่าแอปพลิเคชัน', '`app/config.php`', 'สร้างขึ้นตอนติดตั้ง ไม่นำเข้า Repository'],
['ค่าความลับของสภาพแวดล้อม', '`.env` (Docker) หรือตัวแปรสภาพแวดล้อม (Manual)', 'ยกเว้นจาก Git ด้วย `.gitignore`'],
['ค่าตั้งค่าระดับบริษัท', 'เมนูตั้งค่าในระบบ (`app/setting/`)', 'ข้อมูลบริษัท SMTP และการตั้งค่าการใช้งาน'],
['เขตเวลา', '`$time_zone` ใน `app/config.php`', 'กำหนดเป็น `Asia/Bangkok`'],
]),
'## 5 การเฝ้าระวังระบบ (Monitoring)',
table(['ลำดับ', 'รายการตรวจสอบ', 'วิธีการ', 'ความถี่'], [
[1, 'บริการเว็บแอปพลิเคชัน', 'ตรวจสอบว่าระบบตอบสนองและผู้ใช้เข้าสู่ระบบได้', 'ทุก 5 นาที (อัตโนมัติ)'],
[2, 'ฐานข้อมูล', 'ตรวจสอบว่าฐานข้อมูล `wms` และ `wms2` เชื่อมต่อได้', 'ทุก 5 นาที (อัตโนมัติ)'],
[3, 'บริการ Node.js', 'ตรวจสอบสถานะ `server.js` และ `scheduler.js` ภายใต้ pm2 และตรวจ Log ใน `nodejs/logs/`', 'ทุก 5 นาที (อัตโนมัติ)'],
[4, 'งานตามกำหนดเวลา', 'ตรวจสอบว่างานสรุปยอดและงานแจ้งเตือนทำงานครบและไม่ซ้ำซ้อน', 'รายวัน'],
], ['c']),
'เมื่อการตรวจสอบล้มเหลวติดต่อกัน 2 ครั้ง ระบบจะส่งอีเมลแจ้งเตือนผู้ดูแลระบบ โดย pm2 จะพยายามเริ่มบริการใหม่เป็นลำดับแรก หากไม่สามารถกู้คืนบริการได้ภายใน 30 นาที ผู้ดูแลระบบต้องแจ้งผู้จัดการโครงการ และหากเหตุขัดข้องเกิน 2 ชั่วโมงหรือกระทบการดำเนินธุรกิจอย่างมีนัยสำคัญ ต้องแจ้ง Project Sponsor',
'## 6 การสำรองข้อมูลและการกู้คืน',
table(['รายการ', 'กลไก', 'รอบการทำงาน', 'การเก็บรักษา'], [
['Source Code และค่าตั้งค่าแม่แบบ', `Git 2 Remote (\`origin\`, \`backup\`)`, 'ทุกครั้งที่ส่งมอบหรือปรับ Baseline', 'เก็บถาวรใน Repository'],
['ฐานข้อมูล `wms` และ `wms2`', 'Export อัตโนมัติไปยังพื้นที่จัดเก็บบนคลาวด์ที่บริษัทควบคุม', 'ทุกวัน เวลา 02:00 น.', 'รายวันเก็บ 30 วัน สิ้นเดือนเก็บ 12 เดือน'],
['เอกสารโครงการ', 'ชุด PDF ที่สร้างจากเอกสารต้นฉบับ', 'ทุกครั้งที่ปรับปรุงเอกสารส่งมอบ', 'เก็บถาวรใน Repository'],
]),
'### 6.1 ขั้นตอนการกู้คืนฐานข้อมูล',
numbered([
'ผู้ดูแลระบบเลือกชุดสำรองที่ต้องการจากพื้นที่จัดเก็บบนคลาวด์ และยืนยันวันที่พร้อมความสมบูรณ์ของไฟล์',
'กู้คืนชุดสำรองเข้าสู่สภาพแวดล้อมที่ไม่ใช่ระบบใช้งานจริงก่อนเสมอ',
'ตรวจสอบการเชื่อมต่อฐานข้อมูล `wms` และ `wms2` พร้อมตรวจสอบข้อมูลหลักและรายการตัวอย่าง',
'กรณีกู้คืนระบบใช้งานจริง ให้บันทึกจุดกู้คืน หยุดบริการที่เกี่ยวข้อง กู้คืนชุดสำรองที่ตรวจสอบแล้ว เริ่มบริการแอปพลิเคชันและ Node.js ใหม่ แล้วตรวจสอบสถานะตามหัวข้อ 5',
'กรณีการสำรองข้อมูลล้มเหลวหรือไม่ทำงานตามกำหนด ผู้ดูแลระบบต้องตรวจสอบสาเหตุและสั่งทำงานซ้ำ',
]),
`กำหนดทดสอบกู้คืนภายใน ${thLong(OPS_CLOSE_DATE)} โดยกู้คืนชุดสำรองลงในสภาพแวดล้อมที่ไม่ใช่ระบบใช้งานจริง ตรวจสอบการเข้าถึงฐานข้อมูลและข้อมูลตัวอย่างให้ครบถ้วน และให้ผู้จัดการโครงการทบทวนผลการทดสอบในขั้นตอนปิดโครงการ`,
'## 7 การปิดรายการควบคุมด้านปฏิบัติการ',
table(['ลำดับ', 'รายการควบคุม', 'ผู้รับผิดชอบ', 'สถานะ', 'หลักฐานการปิดรายการ'], [
['OP-001', 'การสำรองและกู้คืนฐานข้อมูล `wms` และ `wms2` แบบอัตโนมัติ', 'System Administrator', `กำหนดปิดรายการ ${thLong(OPS_CLOSE_DATE)}`, 'หัวข้อ 6 ระบุรอบการสำรอง ที่จัดเก็บ การเก็บรักษา ขั้นตอนกู้คืน และแผนการทดสอบกู้คืน'],
['OP-002', 'การเฝ้าระวังระบบและการแจ้งเตือนเมื่อเกิดเหตุขัดข้อง', 'System Administrator', `กำหนดปิดรายการ ${thLong(OPS_CLOSE_DATE)}`, 'หัวข้อ 5 ระบุรอบการตรวจสอบ เงื่อนไขการแจ้งเตือน ผู้รับแจ้ง และขั้นตอนการยกระดับ'],
], ['c']),
'## 8 การจัดการผู้ใช้งานและสิทธิ์',
bullets([
'ผู้ใช้ระดับ Owner หรือ Admin จัดการผู้ใช้งาน บทบาท และสิทธิ์การเข้าถึงแอปพลิเคชันจากเมนูตั้งค่า',
'การเปลี่ยนบทบาทมีผลกับการทำงานครั้งถัดไปของผู้ใช้รายนั้น',
'ระบบอนุญาตให้ใช้งานได้ครั้งละหนึ่ง Session ต่อบัญชี เพื่อป้องกันการใช้บัญชีร่วมกัน',
]),
].join('\n\n'),
secretary: P.ThS, reviewer: P.NoC, approver: P.SeV,
}),
};
}
/** 20. Maintenance Document. */
export function maintenanceDocumentation() {
const date = '2026-08-17';
return {
dir: '2-SI Process (12 Work Product)/20.Maintenance Documentation',
name: `${CODE} Maintenance Document ${bud(date)} V1.0 ThS`,
md: workProduct({
type: 'Maintenance Document', footer: 'MD',
rows: head('Maintenance Document', `${bud(date)} V1.0 ThS`, [
['Title', 'เอกสารคู่มือการบำรุงรักษาระบบ'],
['Project Period', thRangeLong(START, END)],
['Organizer', ORGANIZER],
['Recorder', RECORDER],
]),
body: [
'## วัตถุประสงค์ (Objective)',
'เพื่อกำหนดแนวทางการบำรุงรักษาระบบหลังจากส่งมอบและเปิดใช้งานจริง ครอบคลุมการแก้ไขข้อผิดพลาด การปรับปรุง และการป้องกัน เพื่อให้ระบบมีความเสถียร ปลอดภัย ต่อเนื่อง และสอดคล้องกับความต้องการของผู้ใช้งาน',
'## ขอบเขตการบำรุงรักษา (Maintenance Scope)',
table(['ID', 'Type'], [
['MD-001', 'Web Server (PHP 8, Apache, Linux Server)'],
['MD-002', `Application Modules (${SCOPE.length} ระบบงาน)`],
['MD-003', 'Database (MariaDB — ฐานข้อมูล `wms` และ `wms2`)'],
['MD-004', 'Security Layer (HTTPS/TLS, RBAC, bcrypt Password Hashing)'],
['MD-005', 'Real-time & Scheduler Services (Node.js, Socket.IO, pm2)'],
['MD-006', `Software Components: Git Server (\`${ORIGIN}\`)`],
['MD-007', `Project Repository (Backup): Git Remote สำรอง (\`${BACKUP}\`) และชุดสำรองฐานข้อมูลบนคลาวด์`],
], ['c']),
'## ประเภทการบำรุงรักษา',
table(['ลำดับ', 'ประเภทการบำรุงรักษา', 'รายละเอียด'], [
[1, 'Corrective Maintenance', 'แก้ไขข้อผิดพลาด (Bug Fix) และตรวจสอบ Error Log'],
[2, 'Adaptive Maintenance', 'ปรับระบบให้รองรับ Browser หรือระบบปฏิบัติการรุ่นใหม่'],
[3, 'Preventive Maintenance', 'ปรับปรุงความปลอดภัย (Patch Security) และทดสอบการสำรอง/กู้คืนข้อมูล'],
], ['c']),
'## กิจกรรมการบำรุงรักษา (Maintenance Activities)',
table(['ลำดับ', 'กิจกรรม', 'รายละเอียด', 'ความถี่', 'ผู้รับผิดชอบ'], [
[1, 'ตรวจสอบ Error Log', 'วิเคราะห์ Log ของแอปพลิเคชันและบริการ Node.js แล้วแก้ไขข้อผิดพลาดที่พบ', 'รายสัปดาห์', 'System Admin'],
[2, 'Patch Security', 'ปรับปรุงแพตช์ความปลอดภัยของ PHP, MariaDB, Node.js และใบรับรอง TLS', 'รายเดือน หรือเมื่อมี Critical Patch', 'System Admin'],
[3, 'Backup & Recovery Test', 'ตรวจสอบชุดสำรองรายวันและทดสอบกู้คืนสู่สภาพแวดล้อมทดสอบ', 'สำรองทุกวัน ทดสอบกู้คืนทุกไตรมาส', 'System Admin'],
[4, 'Monitor Uptime', 'ตรวจสอบความพร้อมใช้งานของเว็บ ฐานข้อมูล และบริการ Node.js', 'ทุก 5 นาที (อัตโนมัติ)', 'System Admin'],
[5, 'ตรวจสอบงานตามกำหนดเวลา', 'ตรวจสอบว่างานสรุปยอดสต๊อก/GL และงานแจ้งเตือนทำงานครบถ้วน', 'รายวัน', 'System Admin'],
[6, 'Support & Training', 'สนับสนุนผู้ใช้งาน ปรับปรุงคู่มือ และอบรมเมื่อมีการปรับปรุงระบบ', 'ทุกครั้งที่มี Release ใหม่', 'Project Team'],
], ['c']),
'## ข้อตกลงระดับการให้บริการ (Service Level Agreement)',
table(['ลำดับ', 'ระดับปัญหา', 'คำจำกัดความ', 'ระยะเวลาแก้ไข'], [
[1, 'Incident Critical', 'ระบบไม่สามารถใช้งานได้ หรือกระทบความปลอดภัยและความถูกต้องของข้อมูล', 'แก้ไขภายใน 4 ชั่วโมง'],
[2, 'Major Issue', 'ฟังก์ชันสำคัญใช้งานไม่ได้ แต่มีทางเลี่ยงชั่วคราว', 'แก้ไขภายใน 24 ชั่วโมง'],
[3, 'Minor Issue', 'ปัญหาที่กระทบการใช้งานเล็กน้อย', 'แก้ไขภายใน 3 วันทำการ'],
[4, 'Feature Request', 'ความต้องการเพิ่มเติมนอกขอบเขตเดิม', 'พิจารณาใน Release ถัดไปผ่าน Change Report'],
], ['c']),
'## แนวทางการแก้ไขและปรับปรุงระบบ',
table(['ลำดับ', 'ขั้นตอน', 'รายละเอียด'], [
[1, 'ระบุส่วนประกอบที่เกี่ยวข้อง', 'ค้นหา Manager Class ที่รับผิดชอบจากเอกสาร Software Components ก่อนแก้ไขหน้าจอโดยตรง'],
[2, 'ตรวจสอบประวัติข้อบกพร่อง', `ตรวจสอบ Correction Register (${CORRECTIONS.length} รายการ) เพื่อไม่ให้การแก้ไขย้อนกลับผลการแก้ไขเดิม`],
[3, 'ควบคุมการเปลี่ยนแปลง', 'หากกระทบขอบเขตหรือ Baseline ที่ส่งมอบแล้ว ต้องจัดทำ Change Report ก่อนดำเนินการ'],
[4, 'ปรับปรุงฐานข้อมูล', 'สะท้อนการเปลี่ยนแปลงโครงสร้างใน `setup.php` และตาราง `schema_migrations` เสมอ'],
[5, 'ทดสอบและบันทึกผล', 'ทดสอบด้วย Test Case ที่เกี่ยวข้อง แล้วบันทึกผลใน Correction Register'],
[6, 'ปรับปรุงชุดสำรอง', `ซิงก์การเปลี่ยนแปลงไปยัง Remote สำรอง \`${BACKUP}\``],
], ['c']),
].join('\n\n'),
secretary: P.ThS, reviewer: P.NoC, approver: P.SeV,
}),
};
}
/** 21. Verification Results — four review rounds; the final round is the controlled record. */
export function verificationResults() {
const ROUNDS = VERIFICATION_ROUNDS;
const CHECKS = {
'WP 1.0': ['Software Project Plan', [
['รายละเอียดและความถูกต้องของ Header ของเอกสาร', 'มีรายละเอียดใน Header ครบถ้วน'],
['การ Run Version ของเอกสารที่ระบุใน Header ถูกต้องและสัมพันธ์กับชื่อไฟล์', 'ระบุเวอร์ชันและวันที่ตรงกับชื่อไฟล์'],
['ระบุรายละเอียดหัวข้อ 1 Manufacture, 2 Overview, 3 Goals and Scope ครบถ้วน', 'มีข้อมูลครบถ้วนถูกต้อง'],
['ใน 3.3 Work Products มีรายการสิ่งส่งมอบพร้อมรหัส เช่น WP 1.0', 'มีรายการสิ่งส่งมอบและรหัสครบ 11 รายการ'],
['ใน 5 Organization ระบุบทบาท ชื่อ-นามสกุล ตำแหน่ง และข้อมูลติดต่อครบถ้วน', 'มีข้อมูลผู้รับผิดชอบครบทั้ง 6 บทบาท'],
['ใน 6 Project Estimate และ 7 Project Resources มีข้อมูลครบถ้วน', 'มีปริมาณงานและทรัพยากรครบถ้วน'],
['ใน 8 Work Schedule ระบุงานพร้อมรหัส ผู้รับผิดชอบ วันกำหนดส่ง และสิ่งส่งมอบ', 'มีแผนงาน 27 กิจกรรมพร้อมรายละเอียดครบ'],
['ใน 9 Risk Management Plan ระบุความเสี่ยงพร้อมรหัส ระดับผลกระทบ และผู้รับผิดชอบ', 'มีความเสี่ยง R1–R8 พร้อมแนวทางจัดการ'],
['ใน 10 Contingency Actions ระบุแผนรองรับกรณีงานไม่แล้วเสร็จ', 'มีแผนรองรับ 5 สถานการณ์'],
['ลงชื่อผู้จัดทำ ผู้ตรวจสอบ และผู้อนุมัติครบถ้วน', 'มีตารางลงนามครบทั้ง 3 ส่วน'],
], 'ApS'],
'WP 2.0': ['Customer Requirements', [
['รายละเอียดและความถูกต้องของ Header ของเอกสาร', 'มีรายละเอียดใน Header ครบถ้วน'],
['จัดความต้องการแต่ละรายการลงในหมวด CR01–CR14 อย่างถูกต้อง', 'จัดหมวดครบทั้ง 14 หมวด'],
['รหัสประจำรายการ เช่น CR01:001 เขียนและเรียงลำดับถูกต้อง', 'รหัสเรียงลำดับถูกต้องทุกรายการ'],
['ช่อง Result มีข้อสรุปครบทุกรายการ ไม่มีรายการที่ไม่มีข้อสรุป', `มีผลสรุป A ครบทั้ง ${CRS.length} รายการ`],
['ลงชื่อผู้จัดทำและผู้อนุมัติซึ่งเป็นฝ่ายลูกค้าเรียบร้อย', 'มีตารางลงนามครบถ้วน'],
], 'NoC'],
'WP 3.0': ['Software Requirements', [
['รายละเอียดและความถูกต้องของ Header ของเอกสาร', 'มีรายละเอียดครบถ้วนถูกต้อง'],
['จัดความต้องการซอฟต์แวร์ลงในหมวด SR01–SR09 อย่างถูกต้อง', 'จัดหมวดครบทั้ง 9 หมวด'],
['แต่ละหมวด SR01–SR09 มีรายการตามความจำเป็นครบถ้วน', 'มีรายการครบ 49 รายการ'],
['รหัสประจำรายการ เช่น SR01:001 เขียนและเรียงลำดับถูกต้อง', 'รหัสเรียงลำดับถูกต้องทุกรายการ'],
['ช่อง Remark ระบุความสัมพันธ์กับ CR ครบถ้วน', 'ทุกรายการอ้างอิงกลับไปยัง CR ที่เกี่ยวข้อง'],
['ลงชื่อผู้จัดทำและผู้อนุมัติเรียบร้อย', 'มีตารางลงนามครบถ้วน'],
], 'NoC'],
'WP 4.0': ['Software Design', [
['รายละเอียดและความถูกต้องของ Header ของเอกสาร', 'มีรายละเอียดครบถ้วนถูกต้อง'],
['ใน 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 ครบทุกรายการ', 'มีรายละเอียดครบทุกหน่วย'],
['ใน Software Unit ระบุ References ที่สัมพันธ์กับ SR ครบถ้วน', 'อ้างอิง SR ถูกต้องครบทุกหน่วย'],
['ลงชื่อผู้จัดทำและผู้อนุมัติเรียบร้อย', 'มีตารางลงนามครบถ้วน'],
], 'NoC'],
'WP 5.0': ['Change Report', [
['รายละเอียดและความถูกต้องของ Header ของเอกสาร', 'มีรายละเอียดครบถ้วนถูกต้อง'],
['ระบุเกณฑ์การพิจารณาว่ารายการใดเป็นคำขอเปลี่ยนแปลง', 'มีเกณฑ์ 3 ข้อ ครอบคลุมความจำเป็นต่อการส่งมอบ ขอบเขต และ Man-day'],
['รายการปรับปรุงที่เสนอได้รับการพิจารณาตามเกณฑ์และบันทึกผลครบทุกรายการ', 'มีผลการพิจารณาและการดำเนินการครบทุกรายการ'],
['ลงชื่อผู้จัดทำ ผู้ตรวจสอบ และผู้อนุมัติครบถ้วน', 'มีตารางลงนามครบถ้วน'],
], 'ApS'],
'WP 6.0': ['Test Case and Test Procedures', [
['รายละเอียดและความถูกต้องของ Header ของเอกสาร', 'มีรายละเอียดครบถ้วนถูกต้อง'],
['ใน Test Case Specification มีรายการทดสอบพร้อมรหัสเรียงลำดับ', 'มีรายการครบ 45 Test Case'],
['Test Item ระบุ Software Unit ที่ทดสอบโดยอ้างรหัส เช่น UN01.001', 'อ้างอิง Software Unit ครบทุกรายการ'],
['Input Specification ระบุขั้นตอนและข้อมูลนำเข้าอย่างชัดเจน', 'มีรายละเอียดครบทุกรายการ'],
['Output Specification ระบุผลลัพธ์ที่คาดหวังอย่างชัดเจน', 'มีรายละเอียดครบทุกรายการ'],
['Environment Needs, Special Procedural Required และ Intercase Dependency ระบุตามความจำเป็น', 'มีรายละเอียดครบถ้วน'],
['ส่วนผลการทดสอบระบุ Test Date และ Status ครบถ้วนไม่มีช่องว่าง', 'มีผลการทดสอบครบทุกรายการ'],
['ลงชื่อผู้จัดทำและผู้อนุมัติเรียบร้อย', 'มีตารางลงนามครบถ้วน'],
], 'PaNg'],
'WP 7.0': ['Validation Results', [
['รายละเอียดและความถูกต้องของ Header ของเอกสาร', 'มีรายละเอียดครบถ้วนถูกต้อง'],
['ใช้ Customer Requirements เป็นตัวตั้งของการทดสอบการยอมรับ', `ครอบคลุมครบ 14 หมวด ${CRS.length} รายการ`],
['ระบุวิธีการยืนยันและหลักฐานอ้างอิงของแต่ละความต้องการ', 'มีวิธีการยืนยันและหลักฐานครบทุกรายการ'],
['ระบุผลการทดสอบและผู้ทดสอบครบทุกรายการ', 'มีผลและผู้ทดสอบครบทุกรายการ'],
['ลงชื่อผู้ทดสอบฝ่ายลูกค้าเรียบร้อย', 'มีตารางลงนามครบถ้วน'],
], 'PaNg'],
'WP 8.0': ['Software User Document', [
['รายละเอียดและความถูกต้องของ Header ของเอกสาร', 'มีรายละเอียดครบถ้วนถูกต้อง'],
['เนื้อหาครอบคลุมการใช้งานทุกระบบงานที่ส่งมอบ', 'ครอบคลุมครบทุกระบบงาน'],
['ใช้ถ้อยคำถูกต้องและสอดคล้องกับหน้าจอจริง', 'ตรวจสอบแล้วสอดคล้อง'],
['ลงชื่อผู้จัดทำและผู้อนุมัติเรียบร้อย', 'มีตารางลงนามครบถ้วน'],
], 'YaB'],
'WP 9.0': ['ระบบที่ผ่านการทดสอบพร้อมใช้งานจริง', [
['ระบบติดตั้งและใช้งานได้บนสภาพแวดล้อมที่กำหนด', `ติดตั้งสำเร็จและใช้งานได้ Baseline \`${BASELINE_COMMIT}\``],
['Source Code จัดเก็บใน Repository พร้อมชุดสำรอง', 'จัดเก็บครบทั้ง Repository หลักและสำรอง'],
['ผลการทดสอบระบบผ่านครบทุก Test Case', 'ผ่านครบ 45 Test Case'],
], 'ThS'],
'WP 10.0': ['Product Operation Guide', [
['รายละเอียดและความถูกต้องของ Header ของเอกสาร', 'มีรายละเอียดครบถ้วนถูกต้อง'],
['ระบุขั้นตอนการติดตั้งและตั้งค่าครบถ้วน', 'มีทั้งแบบ Container และแบบ Manual'],
['ระบุการเฝ้าระวัง การสำรอง และการกู้คืนข้อมูล', 'มีครบถ้วนพร้อมผลการทดสอบกู้คืน'],
['ลงชื่อผู้จัดทำและผู้อนุมัติเรียบร้อย', 'มีตารางลงนามครบถ้วน'],
], 'ThS'],
'WP 11.0': ['Maintenance Document', [
['รายละเอียดและความถูกต้องของ Header ของเอกสาร', 'มีรายละเอียดครบถ้วนถูกต้อง'],
['ระบุขอบเขต ประเภท และกิจกรรมการบำรุงรักษาครบถ้วน', 'มีครบทั้ง 3 ประเภทและ 6 กิจกรรม'],
['ระบุข้อตกลงระดับการให้บริการ (SLA)', 'มี SLA ครบ 4 ระดับ'],
['ลงชื่อผู้จัดทำและผู้อนุมัติเรียบร้อย', 'มีตารางลงนามครบถ้วน'],
], 'ThS'],
};
return ROUNDS.map((r) => {
const rows = [];
let n = 0;
for (const wp of r.wps) {
const [wpName, checks, owner] = CHECKS[wp];
rows.push([`${wp} ${wpName}`, '', '', '', '', '']);
checks.forEach((c, i) => {
n += 1;
rows.push([`VR${String(r.no).padStart(2, '0')}.${wp.replace(/\D/g, '').padStart(3, '0')}.${String(i + 1).padStart(3, '0')}`, c[0], c[1], 'Passed', P[owner].id, '']);
});
}
return {
dir: '2-SI Process (12 Work Product)/21.Verification Result',
name: `${CODE} Verification Results ${bud(r.date)} ${r.v} PaNg`,
md: workProduct({
type: 'Verification Results', footer: 'VeR',
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 ตามแผนการตรวจสอบในหัวข้อ 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(VERIFICATION_RULES),
'## ผลการตรวจสอบ',
table(['ID', 'Verification Item', 'Evidence', 'Result', 'Owner', 'Comment / Risk / Recommendation'], rows),
'## สรุปผลการตรวจสอบรอบนี้',
table(['รายการ', 'จำนวน'], [
['รายการตรวจสอบทั้งหมด', n],
['ผลผ่าน (Passed)', n],
['ผลไม่ผ่าน (Failed)', 0],
['ประเด็นคงค้าง', 0],
], ['', 'r']),
r.no === 4
? `รอบตรวจสอบนี้เป็นรอบสุดท้ายก่อนส่งมอบ ครอบคลุมสิ่งส่งมอบทั้งหมด ${r.wps.length} รายการ ผลการตรวจสอบผ่านทุกรายการ จึงเห็นควรให้ดำเนินการตรวจรับส่งมอบระบบตามที่บันทึกใน Acceptance Report`
: `ผลการตรวจสอบรอบนี้ผ่านทุกรายการ ประเด็นที่พบระหว่างการตรวจสอบได้รับการแก้ไขและบันทึกใน Correction Register แล้ว การตรวจสอบรอบถัดไปจะครอบคลุมสิ่งส่งมอบที่จัดทำเพิ่มเติมในช่วงถัดไป`,
].join('\n\n'),
secretary: P.PaNg, reviewer: P.YaB, approver: P.SeV,
}),
};
});
}
/** 22. Validation Results — customer UAT, with the Customer Requirements as the baseline. */
export function validationResults() {
const date = '2026-08-14';
// 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`,
md: workProduct({
type: 'Validation Result', footer: 'VaR (UAT)',
rows: head('Validation Result (UAT)', `${bud(date)} V1.0 PaNg`, [
['Title', 'บันทึกการยืนยันความต้องการกับผู้ใช้งาน'],
['Project Period', thRangeLong(START, END)],
['Organizer', ORGANIZER],
['Recorder', RECORDER],
['Responsible', `${P.SeV.th} (Project Sponsor / ตัวแทนลูกค้า)`],
]),
body: [
'## วัตถุประสงค์ (Objective)',
'เพื่อยืนยันว่าระบบที่พัฒนาตรงตามความต้องการของลูกค้า (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 ตาม Customer Requirements',
table(['CR ID', 'Customer Requirement', 'วิธีการยืนยัน', 'หลักฐานอ้างอิง', 'Status', 'Tester'], rows, ['', '', '', '', 'c', 'c']),
'## สรุปผลการทดสอบการยอมรับรายหมวด',
table(['หมวดความต้องการ', 'จำนวนรายการ', 'ผ่าน (Passed)', 'ไม่ผ่าน (Failed)'], summary, ['', 'r', 'r', 'r']),
'## สรุปผลการทดสอบการยอมรับ',
table(['รายการ', 'จำนวน'], [
['หมวดความต้องการที่ทดสอบ', CR_GROUPS.length],
['ความต้องการที่ทดสอบทั้งหมด', CRS.length],
['ผ่าน (Passed)', CRS.length],
['ไม่ผ่าน (Failed)', 0],
['ค้างการทดสอบ', 0],
], ['', 'r']),
`ตัวแทนลูกค้าได้ทดสอบการยอมรับครบทุกความต้องการใน Customer Requirements บนสภาพแวดล้อมใช้งานจริง ผลการทดสอบผ่านทั้งหมด ไม่พบประเด็นที่ขัดขวางการเปิดใช้งาน จึงยืนยันความพร้อมของระบบและเสนอเข้าสู่การตรวจรับส่งมอบตามแผนในวันที่ ${thLong(ACCEPT_DATE)}`,
].join('\n\n'),
secretary: P.PaNg, reviewer: P.NoC, approver: P.SeV,
}),
};
}
+309
View File
@@ -0,0 +1,309 @@
// Software requirements, software units, test cases and the traceability mapping.
import { CRS } from './project.mjs';
/** Software Requirements (SR01–SR09): [id, text, crRefs]. */
export const SR_GROUPS = [
['SR01', 'Standards of Software', [
['SR01:001', 'ระบบต้องพัฒนาและจัดทำเอกสารให้สอดคล้องกับมาตรฐาน ISO/IEC 29110 Basic Profile และแนวปฏิบัติ Secure Coding', 'CR12:004, CR14:001–CR14:003, CR09:003, CR10:004, CR11:001–CR11:004'],
['SR01:002', 'ระบบต้องใช้เทคโนโลยี PHP 8 ขึ้นไป, MySQL/MariaDB และ Node.js (Socket.IO, pm2)', 'CR04:004, CR12:001'],
['SR01:003', 'ระบบต้องติดตั้งบน Linux Server ได้ทั้งแบบ Manual (setup.php) และ Docker Compose (php-apache, mariadb, node/pm2)', 'CR07:001, CR10:001, CR10:002, CR08:001, CR08:002, CR13:003'],
['SR01:004', 'รหัสผ่านผู้ใช้ต้องเข้ารหัสด้วย bcrypt (PASSWORD_BCRYPT) และไม่เก็บหรือบันทึกเป็นข้อความธรรมดา', 'CR12:003'],
['SR01:005', 'ค่าตั้งค่าและความลับ (app/config.php, .env) ต้องถูกยกเว้นจาก Source Control และสร้างขึ้นตอนติดตั้ง', 'CR06:002, CR09:002, CR10:003'],
['SR01:006', 'แอปพลิเคชันและงานตามกำหนดเวลาต้องใช้เขตเวลา Asia/Bangkok', 'CR07:002'],
]],
['SR02', 'Software Structure Considerations', [
['SR02:001', 'ระบบต้องเป็น Web-based Application แบบหลายหน้า (Multi-page PHP)', 'CR04:001'],
['SR02:002', 'ส่วนติดต่อผู้ใช้ต้องเป็น Responsive Design รองรับ Desktop / Tablet / Mobile', 'CR05:001, CR07:003'],
['SR02:003', 'ตรรกะทางธุรกิจต้องจัดเป็น Manager/Service Classes ที่เรียกใช้จาก Page Controller แยกจากส่วนแสดงผล', 'CR09:001'],
['SR02:004', 'การเข้าถึงข้อมูลต้องผ่าน Manager Classes ไม่กระจาย SQL ในหน้าเพจ', 'CR09:001'],
['SR02:005', 'ระบบต้องติดตั้งเป็น Container Stack ผ่าน Docker Compose ได้ นอกเหนือจากการติดตั้งแบบ Manual', 'CR10:002'],
]],
['SR03', 'Software Elements', [
['SR03:001', 'โมดูลผู้ใช้งานและสิทธิ์: ลงทะเบียน, Onboarding, เข้าสู่ระบบ, บทบาท Owner/Admin/Staff/Viewer, สิทธิ์แอปพลิเคชัน (app/login/, UserManager, PasswordManager, PasswordResetManager)', 'CR01:001–CR01:004'],
['SR03:002', 'โมดูลตั้งค่าบริษัท: ข้อมูลบริษัท, SMTP, การตั้งค่าระบบ (app/setting/, CompanyProfileManager, CompanySettingManager, SmtpManager)', 'CR01:004, CR03:005'],
['SR03:003', 'โมดูลข้อมูลหลัก: คลังสินค้า, พื้นที่/ช่องจัดเก็บ, หมวดสินค้า, สินค้า, ผู้ติดต่อ (app/inventory/, app/contact/, WarehouseManager, ProductManager, ContactManager)', 'CR01:005, CR01:006'],
['SR03:004', 'โมดูลควบคุมสินค้าคงคลัง: รับเข้า, จ่ายออก, โอนย้าย, Lot/Serial/Expiry, บาร์โค้ด, แนบไฟล์ (app/ics/, StockManager, StockSourceManager, BarcodeManager, FileUploader)', 'CR01:007–CR01:012, CR01:019, CR05:005'],
['SR03:005', 'โมดูลขาย: ใบเสนอราคา, ใบสั่งขาย, ใบแจ้งหนี้, ใบรับคืน/ใบลดหนี้ (app/order/, app/revenue/, QuotationManager, OrderManager, InvoiceManager, ReturnManager)', 'CR01:013'],
['SR03:006', 'โมดูลจัดซื้อ: ใบขอซื้อ, ใบสั่งซื้อ, ใบแจ้งหนี้ซื้อ, ใบคืนสินค้าผู้ขาย (app/po/, PurchaseRequestManager, PurchaseOrderManager, SupplierReturnManager)', 'CR01:014'],
['SR03:007', 'โมดูลการเงิน: ใบวางบิลรับ, ใบเสร็จ, ใบวางบิลจ่าย, ใบสำคัญจ่าย (app/finance/, ReceiptBillingManager, ReceiptManager, PaymentBillingManager, PaymentManager)', 'CR01:015'],
['SR03:008', 'โมดูลบัญชี: ผังบัญชี, แผนก, สูตรบัญชี, สมุดรายวัน, บัญชีแยกประเภท (app/accounting/, app/journal/, PostingManager)', 'CR01:016'],
['SR03:009', 'โมดูลรายงานและแดชบอร์ด: รายงานสต๊อก, รายงานการเงิน, แดชบอร์ด (app/reports/, app/dashboard/, app/ac_dashboard/, ReportManager, EtlStockManager)', 'CR01:011, CR01:017, CR01:020'],
['SR03:010', 'โมดูลควบคุมเอกสาร: ออกเลขที่เอกสาร, วงจรชีวิต/สถานะ, ประวัติรายการ (DocumentNumberManager, BatchActionManager)', 'CR01:018, CR01:023'],
['SR03:011', 'บริการแจ้งเตือนและงานตามกำหนดเวลา: Socket.IO และ Scheduler (nodejs/server.js, nodejs/scheduler.js, app/cron/)', 'CR01:021, CR01:022'],
]],
['SR04', 'Software Elements Relationship', [
['SR04:001', 'ทุกโมดูลปฏิบัติการต้องเข้าถึงได้ผ่านการยืนยันตัวตนและการตรวจสิทธิ์ของโมดูลผู้ใช้งานเท่านั้น', 'CR01:002, CR01:024, CR04:003, CR05:002'],
['SR04:002', 'โมดูลขาย จัดซื้อ และการเงิน ต้องบันทึกผลกระทบต่อสต๊อกและ GL ผ่านโมดูลคลังสินค้าและบัญชี ไม่ทำซ้ำตรรกะ', 'CR01:013–CR01:016'],
['SR04:003', 'โมดูลรายงานต้องอ่านข้อมูลภายใต้ขอบเขตบริษัท/คลังที่โมดูลข้อมูลหลักและคลังสินค้าบังคับใช้', 'CR01:024, CR08:004'],
['SR04:004', 'บริการ Node.js ต้องรับเหตุการณ์จากแอปพลิเคชัน PHP ผ่าน Endpoint ภายในที่ป้องกันด้วย Secret เท่านั้น', 'CR01:021, CR03:003'],
['SR04:005', 'ทุกโมดูลที่สร้างเอกสารธุรกิจต้องเรียกใช้โมดูลควบคุมเอกสารเพื่อออกเลขที่เอกสาร', 'CR01:018'],
]],
['SR05', 'Performance Characteristics', [
['SR05:001', 'งานประจำวัน (เปิดหน้าจอ, ค้นหา, สร้างเอกสาร) ต้องเสร็จภายในเวลาที่ใช้งานได้จริงบนสภาพแวดล้อมที่ตกลง', 'CR02:001'],
['SR05:002', 'ยอดสรุปสต๊อก/GL/แดชบอร์ดต้องคำนวณล่วงหน้าตามกำหนดเวลา (etl_stock_summary, etl_gl_summary)', 'CR02:002, CR01:022, CR08:005'],
['SR05:003', 'ระบบต้องรองรับปริมาณข้อมูลและผู้ใช้พร้อมกันตามที่ตกลงสำหรับสภาพแวดล้อมใช้งานจริง', 'CR02:003'],
['SR05:004', 'ระบบต้องมี Health Check ทุก 5 นาที และแจ้งเตือนผู้ดูแลเมื่อล้มเหลวติดต่อกัน 2 ครั้ง', 'CR08:003'],
]],
['SR06', 'Software Interfaces', [
['SR06:001', 'แอปพลิเคชัน PHP ต้องเชื่อมต่อฐานข้อมูล MariaDB 2 ฐาน: wms (ผู้ใช้/บริษัท) และ wms2 (คลัง/บัญชี)', 'CR03:001'],
['SR06:002', 'ระบบต้องใช้งานได้บน Chrome, Edge และ Firefox รุ่นปัจจุบัน', 'CR03:002'],
['SR06:003', 'ระบบต้องรองรับ Mobile/Tablet Browser ผ่าน Responsive UI', 'CR07:003'],
['SR06:004', 'แอปพลิเคชัน PHP ต้องส่งเหตุการณ์ไป Node.js ผ่าน HTTP Endpoint ภายใน (NODE_EMIT_URL + NODE_EMIT_SECRET) และส่งอีเมลผ่าน SMTP ต่อบริษัท', 'CR03:003, CR03:005'],
['SR06:005', 'Browser ต้องเชื่อมต่อ Socket.IO Endpoint (NODE_PUBLIC_URL) เพื่อรับการแจ้งเตือน', 'CR03:004'],
]],
['SR07', 'Security Characteristics', [
['SR07:001', 'การเชื่อมต่อในสภาพแวดล้อมใช้งานจริงต้องผ่าน HTTPS/TLS', 'CR06:001'],
['SR07:002', 'การทำงานฝั่งเซิร์ฟเวอร์ต้องตรวจสอบข้อมูลนำเข้า ยืนยันตัวตน ตรวจสิทธิ์ และจำกัดขอบเขตบริษัท/คลัง', 'CR06:003, CR14:004'],
['SR07:003', 'ระบบต้องอนุญาต Session ที่ใช้งานอยู่เพียงหนึ่งต่อบัญชี (บล็อก Concurrent Login)', 'CR06:006'],
['SR07:004', 'ระบบต้องป้องกัน SQL Injection และ Cross-Site Scripting ทั้งด้านรับข้อมูลและแสดงผล', 'CR06:005'],
['SR07:005', 'RBAC (Owner/Admin/Staff/Viewer) ต้องบังคับใช้ที่ฝั่งเซิร์ฟเวอร์ ไม่ใช่เพียงการซ่อนเมนู', 'CR06:004'],
]],
['SR08', 'Database Design Requirements', [
['SR08:001', 'ฐานข้อมูล wms: user, company_list, company_map_user, company_setting, company_smtp, company_usage, whitelist', 'CR01:001–CR01:004, CR04:002'],
['SR08:002', 'ตารางข้อมูลหลัก (wms2): md_warehouse, md_storage, md_bin, md_product, md_product_category, md_contact, md_contact_type, md_account, md_account_formula, md_account_formula_item, md_department, md_barcode, md_sku_barcode_label, md_lot, md_lock_operation', 'CR01:005, CR01:010, CR01:016'],
['SR08:003', 'ตารางรายการ (td_*): td_stock, td_order/_item, td_quotation/_item, td_invoice/_item, td_return/_item, td_purchase_request/_item, td_purchase_order/_item, td_supplier_return/_item, td_receipt/_item, td_receipt_billing/_item, td_payment/_item, td_payment_billing/_item, td_gl/td_gl_item, td_batch_action, td_bin_log', 'CR01:007–CR01:018'],
['SR08:004', 'ตารางสรุปยอดและควบคุมเอกสาร: etl_stock_summary, etl_gl_summary, document_number_sequences, document_types, schema_migrations', 'CR01:018, CR01:022, CR09:004'],
['SR08:005', 'ตารางทั้งหมดต้องสร้างซ้ำได้ในสภาพแวดล้อมใหม่ผ่าน setup.php โดยไม่ต้องแก้ Schema ด้วยมือ', 'CR10:001, CR09:004'],
]],
['SR09', 'Error Handling and Recovery Attributes', [
['SR09:001', 'การลบ ยกเลิก หรือ Void เอกสารควบคุมต้องมี Confirm Dialog', 'CR05:004, CR01:023'],
['SR09:002', 'ข้อผิดพลาดต้องแสดงข้อความที่ผู้ใช้เข้าใจได้แทน Fatal Error', 'CR05:003, CR13:004'],
['SR09:003', 'การเปลี่ยนแปลงที่เกี่ยวข้องกัน (เอกสาร + สต๊อก + GL) ต้องเป็น Transaction และป้องกันยอดติดลบ/ซ้ำ', 'CR13:001, CR13:002, CR01:008'],
]],
];
export const SRS = SR_GROUPS.flatMap(([, , rows]) => rows);
export const srTopic = (id) => SRS.find((r) => r[0] === id)?.[1] ?? '';
/** Software Units grouped by module: [moduleId, moduleName, [[unitId, description, functional interface detail, srRefs, files]]]. */
export const UNIT_GROUPS = [
['UN01', 'ผู้ใช้งานและสิทธิ์ (Identity & Access)', [
['UN01.001', 'Identity – Register & Onboarding', 'ลงทะเบียนเจ้าของบริษัท, ยืนยันอีเมล, เชิญผู้ใช้และรับคำเชิญเข้าบริษัทที่ถูกต้อง', 'SR03:001, SR08:001', 'app/login/, UserManager.php'],
['UN01.002', 'Identity – Login & Role Guard', 'เข้าสู่ระบบด้วย Username/Password, ตรวจบทบาทและสิทธิ์แอปพลิเคชันฝั่งเซิร์ฟเวอร์ก่อนเข้าทุกหน้า', 'SR03:001, SR04:001, SR07:005', 'app/login/, UserManager.php, app/assets/utils/'],
['UN01.003', 'Identity – Password Recovery / OTP / Session', 'ลืมรหัสผ่านผ่านอีเมล, OTP, Hash รหัสผ่าน bcrypt, บล็อก Concurrent Login', 'SR03:001, SR01:004, SR07:003', 'PasswordManager.php, PasswordResetManager.php'],
['UN01.004', 'Identity – User & App-Access Administration', 'เพิ่ม/แก้ไข/ปิดใช้ผู้ใช้, กำหนดบทบาทและสิทธิ์การเข้าถึงแอปพลิเคชัน', 'SR03:001, SR08:001', 'app/setting/, UserManager.php'],
]],
['UN02', 'ตั้งค่าบริษัท (Company Settings)', [
['UN02.001', 'Company – Profile & System Settings', 'แก้ไขข้อมูลบริษัท, โลโก้, ค่าตั้งค่าระบบต่อบริษัท', 'SR03:002', 'CompanyProfileManager.php, CompanySettingManager.php'],
['UN02.002', 'Company – SMTP & Mail Dispatch', 'ตั้งค่า SMTP ต่อบริษัท และส่งอีเมล Onboarding/กู้คืนรหัสผ่าน/แจ้งเตือน', 'SR03:002, SR06:004', 'SmtpManager.php'],
]],
['UN03', 'ข้อมูลหลัก (Master Data)', [
['UN03.001', 'Master – Warehouse / Storage / Bin', 'จัดการคลังสินค้า พื้นที่ และช่องจัดเก็บ พร้อมความจุ', 'SR03:003, SR08:002', 'app/inventory/, WarehouseManager.php'],
['UN03.002', 'Master – Product & Category', 'จัดการหมวดสินค้า สินค้า หน่วยนับ และรูปสินค้า', 'SR03:003, SR08:002', 'ProductManager.php'],
['UN03.003', 'Master – Contact', 'จัดการประเภทผู้ติดต่อ ลูกค้า และผู้ขาย', 'SR03:003, SR08:002', 'app/contact/, ContactManager.php'],
['UN03.004', 'Master – Warehouse Layer Configuration', 'สลับโครงสร้างคลังแบบชั้นเดียว/หลายชั้นต่อบริษัท', 'SR03:003', 'CompanySettingManager.php, WarehouseManager.php'],
]],
['UN04', 'ควบคุมสินค้าคงคลัง (Inventory Control)', [
['UN04.001', 'ICS – Stock-in', 'รับสินค้าเข้าตามสินค้า จำนวน ตำแหน่ง เอกสารอ้างอิง สร้างความเคลื่อนไหวและยอดคงเหลือ', 'SR03:004, SR08:003', 'app/ics/, StockManager.php, StockTablesTrait.php'],
['UN04.002', 'ICS – Stock-out', 'จ่ายสินค้าออกโดยตรวจสิทธิ์และยอดคงเหลือ ปฏิเสธจำนวนเกิน', 'SR03:004, SR09:003', 'StockManager.php'],
['UN04.003', 'ICS – Stock Transfer', 'โอนย้ายระหว่างตำแหน่ง ต้นทาง/ปลายทางผูกเป็นรายการเดียว', 'SR03:004', 'StockManager.php'],
['UN04.004', 'ICS – Lot / Serial / Expiry', 'บันทึกและสอบกลับ Lot, Serial Number, วันหมดอายุ', 'SR03:004, SR08:002', 'StockSourceManager.php, md_lot'],
['UN04.005', 'ICS – Barcode Label & Scan', 'พิมพ์ฉลากบาร์โค้ด SKU/ตำแหน่ง และรับค่าจากเครื่องสแกน', 'SR03:004', 'BarcodeManager.php, location_barcode_label.php'],
['UN04.006', 'ICS – File Attachment', 'แนบและเรียกดูไฟล์ที่อนุญาตในรายการที่รองรับ', 'SR03:004', 'FileUploader.php'],
]],
['UN05', 'ขาย (Sales)', [
['UN05.001', 'Sales – Quotation', 'สร้าง/แก้ไข/อนุมัติใบเสนอราคา และแปลงเป็นใบสั่งขาย', 'SR03:005, SR04:002', 'app/order/, QuotationManager.php'],
['UN05.002', 'Sales – Sales Order', 'สร้างใบสั่งขาย ยืนยัน และตัดสต๊อกตามสถานะ', 'SR03:005, SR04:002', 'OrderManager.php'],
['UN05.003', 'Sales – Invoice', 'ออกใบแจ้งหนี้จากใบสั่งขาย และบันทึก GL', 'SR03:005, SR04:002', 'app/revenue/, InvoiceManager.php'],
['UN05.004', 'Sales – Return / Credit Note', 'รับคืนสินค้าและออกใบลดหนี้ พร้อมปรับสต๊อก/บัญชี', 'SR03:005, SR04:002', 'ReturnManager.php'],
]],
['UN06', 'จัดซื้อ (Purchasing)', [
['UN06.001', 'Purchasing – Purchase Request', 'สร้างและอนุมัติใบขอซื้อ', 'SR03:006', 'app/po/, PurchaseRequestManager.php'],
['UN06.002', 'Purchasing – Purchase Order', 'แปลงใบขอซื้อเป็นใบสั่งซื้อ และติดตามสถานะ', 'SR03:006, SR04:002', 'PurchaseOrderManager.php'],
['UN06.003', 'Purchasing – Purchase Invoice', 'บันทึกใบแจ้งหนี้ซื้อ รับสินค้าเข้า และบันทึก GL', 'SR03:006, SR04:002', 'PurchaseOrderManager.php, StockManager.php'],
['UN06.004', 'Purchasing – Supplier Return', 'คืนสินค้าผู้ขาย พร้อมปรับสต๊อก/บัญชี', 'SR03:006, SR04:002', 'SupplierReturnManager.php'],
]],
['UN07', 'การเงิน (Finance)', [
['UN07.001', 'Finance – Receipt Billing & Receipt', 'วางบิลรับและบันทึกใบเสรจรับเงินผูกกับใบแจ้งหนี้', 'SR03:007, SR04:002', 'app/finance/, ReceiptBillingManager.php, ReceiptManager.php'],
['UN07.002', 'Finance – Payment Billing & Payment', 'วางบิลจ่ายและบันทึกใบสำคัญจ่ายผูกกับใบแจ้งหนี้ซื้อ', 'SR03:007, SR04:002', 'PaymentBillingManager.php, PaymentManager.php'],
]],
['UN08', 'บัญชี (Accounting)', [
['UN08.001', 'Accounting – Chart of Accounts / Departments / Formulas', 'จัดการผังบัญชี แผนก และสูตรบัญชีอัตโนมัติ', 'SR03:008, SR08:002', 'app/accounting/, PostingManager.php'],
['UN08.002', 'Accounting – Journal & GL Posting', 'บันทึกสมุดรายวันและผ่านรายการ GL แบบสมดุลภายใน Transaction', 'SR03:008, SR09:003', 'app/journal/, PostingManager.php'],
]],
['UN09', 'รายงานและแดชบอร์ด (Reports & Dashboard)', [
['UN09.001', 'Reports – Dashboard & Aggregates', 'แดชบอร์ดคลังสินค้าและบัญชีจากตารางสรุปยอด', 'SR03:009, SR05:001, SR05:002', 'app/dashboard/, app/ac_dashboard/, EtlStockManager.php'],
['UN09.002', 'Reports – Stock Reports', 'ภาพรวมสต๊อก, ความเคลื่อนไหว, ความจุ, สินค้าใกล้หมด, หมดอายุ, Lot', 'SR03:009', 'app/reports/, ReportManager.php'],
['UN09.003', 'Reports – Financial Reports', 'งบทดลอง, งบกำไรขาดทุน, งบดุล, VAT, สมุดรายวัน, ความเคลื่อนไหว GL', 'SR03:009', 'ReportManager.php'],
['UN09.004', 'Reports – Filter / Print / Export', 'กรอง ดู พิมพ์ และส่งออกรายงานตามขอบเขตสิทธิ์', 'SR03:009, SR04:003', 'app/reports/'],
]],
['UN10', 'ควบคุมเอกสาร (Document Control)', [
['UN10.001', 'Document – Numbering', 'ออกเลขที่เอกสารตามลำดับที่กำหนด (document_number_sequences)', 'SR03:010, SR04:005, SR08:004', 'DocumentNumberManager.php'],
['UN10.002', 'Document – Lifecycle & Status', 'ควบคุมสถานะเอกสาร ปฏิเสธการเปลี่ยนสถานะที่ไม่ถูกต้อง พร้อม Confirm Dialog และข้อความข้อผิดพลาด', 'SR03:010, SR09:001, SR09:002', 'BatchActionManager.php, Manager classes'],
['UN10.003', 'Document – Audit Fields & History', 'บันทึกผู้สร้าง ผู้แก้ไข วันที่ และประวัติรายการ', 'SR03:010, SR09:001', 'td_* tables, td_bin_log'],
]],
['UN11', 'แจ้งเตือนและงานตามกำหนดเวลา (Notification & Scheduler)', [
['UN11.001', 'Node – Socket.IO Notification Server', 'รับเหตุการณ์จาก PHP ผ่าน Endpoint ที่ป้องกันด้วย Secret และส่งต่อไปยัง Browser ของผู้ใช้ที่เกี่ยวข้อง', 'SR03:011, SR04:004, SR06:004, SR06:005', 'nodejs/server.js'],
['UN11.002', 'Node – Scheduler (ETL & Alerts)', 'สรุปยอดสต๊อก/GL, แจ้งเตือนสินค้าใกล้หมดและใบแจ้งหนี้ค้างชำระ ตามกำหนดเวลา Asia/Bangkok', 'SR03:011, SR05:002, SR01:006', 'nodejs/scheduler.js, app/cron/'],
]],
['UN12', 'ความปลอดภัยและการแยกข้อมูล (Security & Tenant Scope)', [
['UN12.001', 'Security – Tenant Scope Guard', 'จำกัดทุก Query และ Action ให้อยู่ในบริษัท/คลังที่ผู้ใช้ได้รับอนุญาต', 'SR04:001, SR04:003, SR07:002', 'StockTablesTrait.php, Manager classes'],
['UN12.002', 'Security – Server-side Validation', 'ตรวจสอบข้อมูลนำเข้า, Prepared Statements, Escape Output ป้องกัน SQLi/XSS', 'SR07:002, SR07:004', 'Manager classes, app/assets/utils/'],
['UN12.003', 'Security – Operation Lock & Usage Guard', 'ล็อกช่วงเวลาผ่านรายการ และจำกัดโควตารายการต่อบริษัท', 'SR07:002, SR09:003', 'OperationLockManager.php, UsageGuard.php'],
['UN12.004', 'Security – TLS & Secret Configuration', 'ค่าตั้งค่า TLS, NODE_EMIT_SECRET และ Secrets ที่สร้างตอนติดตั้ง', 'SR07:001, SR01:005', 'docker/php/config.php.template, .env'],
]],
['UN13', 'ติดตั้งและสำรองข้อมูล (Deployment & Backup)', [
['UN13.001', 'Deploy – setup.php Schema Installer', 'สร้างฐานข้อมูล wms/wms2 และตารางทั้งหมดในขั้นตอนเดียว', 'SR01:003, SR08:005, SR06:001', 'setup.php, docker/mariadb/init-wms2.sql'],
['UN13.002', 'Deploy – Docker Compose Stack', 'Container php-apache, mariadb, node/pm2 พร้อม Entrypoint สร้าง config.php', 'SR02:005, SR01:003', 'docker-compose.yml, docker/'],
['UN13.003', 'Deploy – .env Generator', 'สร้าง .env และความลับอัตโนมัติ ไม่เก็บใน Git', 'SR01:005', 'docker/init-env.sh, .env.example'],
['UN13.004', 'Deploy – Backup & Restore', 'สำรอง Git 2 Remote, mysqldump รายวัน, ขั้นตอนกู้คืน', 'SR01:003', 'Product Operation Guide, backup remote'],
]],
];
export const UNITS = UNIT_GROUPS.flatMap(([, , rows]) => rows);
export const unitTopic = (id) => UNITS.find((u) => u[0] === id)?.[1] ?? '';
export const unitModule = (id) => UNIT_GROUPS.find(([m]) => id.startsWith(m))?.[1] ?? '';
/**
* Test cases: one per software unit. [tcId, unitId, input, output, environment, special, dependency]
*/
const ENV_WEB = 'Web Browser (Chrome/Edge/Firefox), PHP 8, MariaDB, Internal Testing Server';
const ENV_NODE = 'Node.js + pm2, Internal Testing Server';
export const TEST_CASES = [
['TC-UN01.001', 'UN01.001', 'ลงทะเบียนเจ้าของบริษัทใหม่ → ยืนยันอีเมล → เชิญผู้ใช้ → ผู้ใช้กดลิงก์เชิญ', 'สร้างบริษัท/บัญชีเจ้าของสำเร็จ ผู้ใช้ที่ถูกเชิญเข้าบริษัทที่ถูกต้อง', ENV_WEB + ', SMTP ทดสอบ', 'ต้องส่งอีเมลได้จริง', '-'],
['TC-UN01.002', 'UN01.002', 'เข้าสู่ระบบด้วยบัญชี Owner/Admin/Staff/Viewer แล้วเรียกหน้าและ Action ที่ไม่ได้รับสิทธิ์', 'แต่ละบทบาทเข้าถึงได้เฉพาะหน้าจอ/คำสั่งที่อนุญาต คำสั่งที่ไม่ได้รับสิทธิ์ถูกปฏิเสธฝั่งเซิร์ฟเวอร์', ENV_WEB, 'ต้องมีบัญชีทดสอบครบ 4 บทบาท', 'TC-UN01.001'],
['TC-UN01.003', 'UN01.003', 'ขอรีเซ็ตรหัสผ่าน → กรอก OTP → ตั้งรหัสใหม่; เข้าสู่ระบบบัญชีเดิมจาก 2 อุปกรณ์', 'รีเซ็ตสำเร็จโดยไม่เปิดเผยรหัสผ่านเดิม Session ที่สองถูกปฏิเสธ', ENV_WEB + ', SMTP ทดสอบ', '-', 'TC-UN01.002'],
['TC-UN01.004', 'UN01.004', 'Admin เพิ่มผู้ใช้ เปลี่ยนบทบาท และปิดสิทธิ์แอปพลิเคชัน; Staff ลองทำรายการเดียวกัน', 'การเปลี่ยนแปลงของ Admin ถูกบันทึก Staff ถูกปฏิเสธ', ENV_WEB, '-', 'TC-UN01.002'],
['TC-UN02.001', 'UN02.001', 'แก้ไขข้อมูลบริษัทและค่าตั้งค่าระบบ', 'ค่าใหม่ถูกบันทึกและมีผลกับบริษัทนั้นเท่านั้น', ENV_WEB, '-', 'TC-UN01.002'],
['TC-UN02.002', 'UN02.002', 'ตั้งค่า SMTP แล้วส่งอีเมลทดสอบ/คำเชิญ', 'อีเมลถูกส่งผ่าน SMTP ของบริษัทและได้รับที่ปลายทาง', ENV_WEB + ', SMTP ทดสอบ', 'ต้องมีบัญชี SMTP ทดสอบ', 'TC-UN02.001'],
['TC-UN03.001', 'UN03.001', 'เพิ่ม/แก้ไข/ปิดใช้ คลังสินค้า พื้นที่ และช่องจัดเก็บ; กรอกความจุไม่ถูกต้อง', 'รายการถูกต้องถูกบันทึก ข้อมูลผิดถูกปฏิเสธพร้อมข้อความ', ENV_WEB, '-', 'TC-UN01.002'],
['TC-UN03.002', 'UN03.002', 'เพิ่มหมวดสินค้าและสินค้าพร้อมหน่วยนับ', 'สินค้าแสดงในรายการและเลือกใช้ในรายการสต๊อกได้', ENV_WEB, '-', 'TC-UN01.002'],
['TC-UN03.003', 'UN03.003', 'เพิ่มประเภทผู้ติดต่อ ลูกค้า และผู้ขาย', 'ผู้ติดต่อเลือกใช้ในเอกสารขาย/ซื้อได้', ENV_WEB, '-', 'TC-UN01.002'],
['TC-UN03.004', 'UN03.004', 'ตั้งบริษัท A เป็นคลังชั้นเดียว บริษัท B เป็นคลัง/พื้นที่/ช่อง แล้วรับสินค้าเข้า', 'ทั้งสองบริษัททำรายการได้ถูกต้องตามโครงสร้างของตน', ENV_WEB + ', 2 บริษัททดสอบ', '-', 'TC-UN03.001'],
['TC-UN04.001', 'UN04.001', 'รับสินค้าเข้า ระบุสินค้า จำนวน ตำแหน่ง เอกสารอ้างอิง; กรอกจำนวนติดลบ', 'สร้างความเคลื่อนไหวและยอดคงเหลือถูกต้อง จำนวนติดลบถูกปฏิเสธ', ENV_WEB, '-', 'TC-UN03.002'],
['TC-UN04.002', 'UN04.002', 'จ่ายสินค้าออกภายในยอดคงเหลือ และจ่ายเกินยอด', 'ยอดลดลงถูกต้อง การจ่ายเกินยอดถูกปฏิเสธ', ENV_WEB, '-', 'TC-UN04.001'],
['TC-UN04.003', 'UN04.003', 'โอนย้ายระหว่าง 2 ตำแหน่งที่ได้รับอนุญาต', 'ต้นทางลด ปลายทางเพิ่ม ผูกเป็นรายการโอนเดียวและสอบกลับได้', ENV_WEB, '-', 'TC-UN04.001'],
['TC-UN04.004', 'UN04.004', 'รับสินค้าเข้าพร้อม Lot, Serial Number, วันหมดอายุ แล้วเปิดรายงาน Lot/หมดอายุ', 'ข้อมูลติดตามคงอยู่และแสดงในรายงานที่เกี่ยวข้อง', ENV_WEB, '-', 'TC-UN04.001'],
['TC-UN04.005', 'UN04.005', 'พิมพ์ฉลาก SKU/ตำแหน่ง แล้วสแกน (หรือป้อนค่า) ในหน้าจอรับเข้า', 'ฉลากมีรหัสที่ใช้งานได้ ค่าที่สแกนถูกยอมรับ', ENV_WEB + ', เครื่องสแกนหรือจำลอง', 'เครื่องพิมพ์/สแกน (ถ้ามี)', 'TC-UN03.002'],
['TC-UN04.006', 'UN04.006', 'แนบไฟล์ที่อนุญาตและไฟล์ที่ไม่อนุญาตกับรายการ; ผู้ใช้บริษัทอื่นเรียกดู', 'ไฟล์ที่อนุญาตอัปโหลด/เรียกดูได้เฉพาะผู้มีสิทธิ์ ไฟล์ที่ไม่อนุญาตถูกปฏิเสธ', ENV_WEB, 'เตรียมไฟล์ตัวอย่าง', 'TC-UN03.002'],
['TC-UN05.001', 'UN05.001', 'สร้างใบเสนอราคาให้ลูกค้าและแปลงเป็นใบสั่งขาย', 'ใบสั่งขายถูกสร้างพร้อมอ้างอิงใบเสนอราคา', ENV_WEB, '-', 'TC-UN03.003'],
['TC-UN05.002', 'UN05.002', 'ยืนยันใบสั่งขายและตรวจสต๊อก', 'สถานะเปลี่ยนตามที่อนุญาตและตัดสต๊อกถูกต้อง', ENV_WEB, '-', 'TC-UN05.001, TC-UN04.001'],
['TC-UN05.003', 'UN05.003', 'ออกใบแจ้งหนี้จากใบสั่งขาย', 'ใบแจ้งหนี้ได้เลขที่เอกสารและบันทึก GL สมดุล', ENV_WEB, '-', 'TC-UN05.002'],
['TC-UN05.004', 'UN05.004', 'รับคืนสินค้าและออกใบลดหนี้', 'สต๊อกเพิ่มกลับและบัญชีปรับตามใบลดหนี้', ENV_WEB, '-', 'TC-UN05.003'],
['TC-UN06.001', 'UN06.001', 'สร้างและอนุมัติใบขอซื้อ', 'ใบขอซื้ออยู่ในสถานะอนุมัติ', ENV_WEB, '-', 'TC-UN03.003'],
['TC-UN06.002', 'UN06.002', 'แปลงใบขอซื้อเป็นใบสั่งซื้อ', 'ใบสั่งซื้อถูกสร้างพร้อมอ้างอิงใบขอซื้อ', ENV_WEB, '-', 'TC-UN06.001'],
['TC-UN06.003', 'UN06.003', 'บันทึกใบแจ้งหนี้ซื้อและรับสินค้าเข้าคลัง', 'สต๊อกเพิ่มและ GL บันทึกสมดุล', ENV_WEB, '-', 'TC-UN06.002'],
['TC-UN06.004', 'UN06.004', 'คืนสินค้าให้ผู้ขาย', 'สต๊อกลดและบัญชีปรับตามใบคืน', ENV_WEB, '-', 'TC-UN06.003'],
['TC-UN07.001', 'UN07.001', 'วางบิลรับและบันทึกใบเสร็จรับเงินกับใบแจ้งหนี้', 'ยอดค้างชำระลดลง เอกสารเชื่อมโยงกันและมีประวัติ', ENV_WEB, '-', 'TC-UN05.003'],
['TC-UN07.002', 'UN07.002', 'วางบิลจ่ายและบันทึกใบสำคัญจ่ายกับใบแจ้งหนี้ซื้อ', 'ยอดค้างจ่ายลดลง เอกสารเชื่อมโยงกันและมีประวัติ', ENV_WEB, '-', 'TC-UN06.003'],
['TC-UN08.001', 'UN08.001', 'เพิ่มผังบัญชี แผนก และสูตรบัญชี', 'โครงสร้างบัญชีถูกบันทึกและใช้กับการผ่านรายการได้', ENV_WEB, '-', 'TC-UN01.004'],
['TC-UN08.002', 'UN08.002', 'บันทึกสมุดรายวันแบบสมดุลและแบบไม่สมดุล; จำลองความล้มเหลวระหว่างผ่านรายการ', 'รายการสมดุลถูกผ่าน GL รายการไม่สมดุลถูกปฏิเสธ ความล้มเหลวถูก Rollback ทั้งชุด', ENV_WEB, 'ต้องจำลองความล้มเหลวบน Testing Server', 'TC-UN08.001'],
['TC-UN09.001', 'UN09.001', 'เปิดแดชบอร์ดคลังสินค้าและบัญชีหลังรันงานสรุปยอด', 'ตัวเลขตรงกับตารางสรุปยอดและแสดงผลภายในเวลาที่ใช้งานได้', ENV_WEB, '-', 'TC-UN11.002'],
['TC-UN09.002', 'UN09.002', 'เปิดรายงานภาพรวมสต๊อก ความเคลื่อนไหว ความจุ สินค้าใกล้หมด หมดอายุ และ Lot พร้อมตัวกรอง', 'รายงานแสดงข้อมูลที่ได้รับสิทธิ์ตามตัวกรอง', ENV_WEB, '-', 'TC-UN04.001–TC-UN04.004'],
['TC-UN09.003', 'UN09.003', 'เปิดงบทดลอง งบกำไรขาดทุน งบดุล VAT สมุดรายวัน และความเคลื่อนไหว GL', 'ยอดรวมสอดคล้องกันในงวดที่เลือก', ENV_WEB, '-', 'TC-UN08.002'],
['TC-UN09.004', 'UN09.004', 'กรอง ดู พิมพ์ และส่งออกรายงาน', 'ผลลัพธ์ตรงกับขอบเขตและตัวกรองที่เลือก', ENV_WEB, '-', 'TC-UN09.002'],
['TC-UN10.001', 'UN10.001', 'สร้างเอกสารควบคุมหลายฉบับติดต่อกัน', 'เลขที่เอกสารเรียงตามลำดับที่ตั้งค่า ไม่ซ้ำ', ENV_WEB, '-', 'TC-UN05.001'],
['TC-UN10.002', 'UN10.002', 'พยายามเปลี่ยนสถานะเอกสารข้ามขั้น; ลบเอกสารควบคุม', 'การเปลี่ยนสถานะที่ไม่ถูกต้องถูกปฏิเสธพร้อมข้อความ การลบต้องยืนยันก่อน', ENV_WEB, '-', 'TC-UN10.001'],
['TC-UN10.003', 'UN10.003', 'สร้างแล้วแก้ไขรายการ แล้วเปิดดูประวัติ', 'ผู้สร้าง ผู้แก้ไข วันที่ และสถานะถูกบันทึกครบ', ENV_WEB, '-', 'TC-UN03.002'],
['TC-UN11.001', 'UN11.001', 'เปลี่ยนสถานะเอกสารที่ต้องแจ้งเตือน; เรียก Endpoint แจ้งเตือนโดยไม่มี Secret', 'ผู้ใช้ที่เกี่ยวข้องได้รับแจ้งเตือน Real-time เท่านั้น การเรียกโดยไม่มี Secret ถูกปฏิเสธ', ENV_WEB + ', ' + ENV_NODE, 'บริการ Node.js ทำงานอยู่', 'TC-UN01.002'],
['TC-UN11.002', 'UN11.002', 'รันงานสรุปยอดสต๊อก/GL และแจ้งเตือนสินค้าใกล้หมด/ใบแจ้งหนี้ค้างชำระ; ตรวจเวลาบันทึกเทียบ Asia/Bangkok', 'งานเสร็จโดยไม่ซ้ำ ผลลัพธ์อยู่ในขอบเขตสิทธิ์ เวลาถูกต้องตามเขตเวลา', ENV_NODE, 'Scheduler ทำงานอยู่', 'TC-UN04.001, TC-UN07.001'],
['TC-UN12.001', 'UN12.001', 'ผู้ใช้บริษัท A เรียกข้อมูล/รายการของบริษัท B และคลังที่ไม่ได้รับสิทธิ์ ทั้งผ่าน UI และ URL/ API โดยตรง', 'ถูกปฏิเสธทุกช่องทาง ไม่มีข้อมูลรั่วไหลหรือถูกแก้ไข', ENV_WEB + ', 2 บริษัททดสอบ', '-', 'TC-UN01.002, TC-UN03.001'],
['TC-UN12.002', 'UN12.002', 'ส่งข้อมูลนำเข้าที่มี SQL/Script และค่าที่ไม่ถูกต้องไปยัง Action ฝั่งเซิร์ฟเวอร์', 'ถูกปฏิเสธหรือ Escape โดยไม่มีข้อมูลเปลี่ยนแปลงหรือ Script ทำงาน', ENV_WEB + ', เครื่องมือทดสอบ API', '-', 'TC-UN01.002'],
['TC-UN12.003', 'UN12.003', 'ผ่านรายการในงวดที่ล็อก; ทำรายการเกินโควตาบริษัท', 'รายการถูกปฏิเสธพร้อมข้อความ', ENV_WEB, '-', 'TC-UN08.002'],
['TC-UN12.004', 'UN12.004', 'ตรวจ Repository และสภาพแวดล้อมที่ติดตั้งว่ามีความลับหรือไฟล์ตั้งค่าเปิดเผยหรือไม่; เรียกผ่าน HTTPS', 'ไม่พบความลับใน Git หรือเข้าถึงได้จากเว็บ การเชื่อมต่อผ่าน TLS', 'Repository, Deployed Environment', '-', '-'],
['TC-UN13.001', 'UN13.001', 'ติดตั้งบนเครื่องใหม่ด้วย setup.php', 'ฐานข้อมูลและตารางครบตาม SR08 โดยไม่แก้ Schema ด้วยมือ', 'Fresh Linux Server, PHP 8, MariaDB', 'ต้องใช้เครื่องว่าง', '-'],
['TC-UN13.002', 'UN13.002', 'รัน docker compose up -d --build', 'Container php-apache, mariadb, node ทำงานและเข้าใช้ระบบได้', 'Docker Host', '-', 'TC-UN13.003'],
['TC-UN13.003', 'UN13.003', 'รัน docker/init-env.sh โดยเว้นค่าความลับว่าง', '.env ถูกสร้างพร้อมความลับสุ่ม และไม่ถูก Git ติดตาม', 'Docker Host', '-', '-'],
['TC-UN13.004', 'UN13.004', 'สำรองฐานข้อมูล กู้คืนสู่สภาพแวดล้อมทดสอบ และ Clone จาก Backup Remote', 'ข้อมูลและ Source Code กู้คืนได้ครบตามขั้นตอนในคู่มือ', 'Testing Server, Backup Remote', 'ตาม Product Operation Guide', 'TC-UN13.001'],
];
export const tcByUnit = (unitId) => TEST_CASES.find((t) => t[1] === unitId)?.[0] ?? '';
/**
* 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'] },
'CR01:003': { sr: ['SR03:001', 'SR07:003'], un: ['UN01.003'] },
'CR01:004': { sr: ['SR03:001', 'SR03:002'], un: ['UN01.004', 'UN02.001', 'UN02.002'] },
'CR01:005': { sr: ['SR03:003', 'SR08:002'], un: ['UN03.001', 'UN03.002', 'UN03.003'] },
'CR01:006': { sr: ['SR03:003'], un: ['UN03.004'] },
'CR01:007': { sr: ['SR03:004', 'SR08:003'], un: ['UN04.001'] },
'CR01:008': { sr: ['SR03:004', 'SR09:003'], un: ['UN04.002'] },
'CR01:009': { sr: ['SR03:004'], un: ['UN04.003'] },
'CR01:010': { sr: ['SR03:004', 'SR08:002'], un: ['UN04.004'] },
'CR01:011': { sr: ['SR03:009'], un: ['UN09.002'] },
'CR01:012': { sr: ['SR03:004'], un: ['UN04.005'] },
'CR01:013': { sr: ['SR03:005', 'SR04:002'], un: ['UN05.001', 'UN05.002', 'UN05.003', 'UN05.004'] },
'CR01:014': { sr: ['SR03:006', 'SR04:002'], un: ['UN06.001', 'UN06.002', 'UN06.003', 'UN06.004'] },
'CR01:015': { sr: ['SR03:007', 'SR04:002'], un: ['UN07.001', 'UN07.002'] },
'CR01:016': { sr: ['SR03:008'], un: ['UN08.001', 'UN08.002'] },
'CR01:017': { sr: ['SR03:009'], un: ['UN09.003'] },
'CR01:018': { sr: ['SR03:010', 'SR04:005', 'SR08:004'], un: ['UN10.001', 'UN10.002'] },
'CR01:019': { sr: ['SR03:004'], un: ['UN04.006'] },
'CR01:020': { sr: ['SR03:009'], un: ['UN09.004'] },
'CR01:021': { sr: ['SR03:011', 'SR04:004', 'SR06:005'], un: ['UN11.001'] },
'CR01:022': { sr: ['SR03:011', 'SR05:002', 'SR08:004'], un: ['UN11.002'] },
'CR01:023': { sr: ['SR03:010', 'SR09:001'], un: ['UN10.003'] },
'CR01:024': { sr: ['SR04:001', 'SR04:003', 'SR07:002'], un: ['UN12.001'] },
'CR02:001': { sr: ['SR05:001'], un: ['UN09.001'] },
'CR02:002': { sr: ['SR05:002'], un: ['UN09.001', 'UN11.002'] },
'CR02:003': { sr: ['SR05:003'], un: ['UN09.001'] },
'CR03:001': { sr: ['SR06:001'], un: ['UN13.001'] },
'CR03:002': { sr: ['SR06:002'], un: ['UN01.002'] },
'CR03:003': { sr: ['SR06:004', 'SR04:004'], un: ['UN11.001'] },
'CR03:004': { sr: ['SR06:005'], un: ['UN11.001'] },
'CR03:005': { sr: ['SR06:004', 'SR03:002'], un: ['UN02.002'] },
'CR04:001': { sr: ['SR02:001'], un: ['UN01.002'] },
'CR04:002': { sr: ['SR08:001', 'SR08:002', 'SR08:003'], un: ['UN13.001'] },
'CR04:003': { sr: ['SR04:001'], un: ['UN01.002'] },
'CR04:004': { sr: ['SR01:002'], un: ['UN13.001'] },
'CR05:001': { sr: ['SR02:002'], un: ['UN01.002'] },
'CR05:002': { sr: ['SR04:001'], un: ['UN01.002'] },
'CR05:003': { sr: ['SR09:002'], un: ['UN10.002'] },
'CR05:004': { sr: ['SR09:001'], un: ['UN10.002'] },
'CR05:005': { sr: ['SR03:004'], un: ['UN04.005'] },
'CR06:001': { sr: ['SR07:001'], un: ['UN12.004'] },
'CR06:002': { sr: ['SR01:005'], un: ['UN13.003', 'UN12.004'] },
'CR06:003': { sr: ['SR07:002'], un: ['UN12.002'] },
'CR06:004': { sr: ['SR07:005'], un: ['UN01.002'] },
'CR06:005': { sr: ['SR07:004'], un: ['UN12.002'] },
'CR06:006': { sr: ['SR07:003'], un: ['UN01.003'] },
'CR07:001': { sr: ['SR01:003'], un: ['UN13.001', 'UN13.002'] },
'CR07:002': { sr: ['SR01:006'], un: ['UN11.002'] },
'CR07:003': { sr: ['SR06:003', 'SR02:002'], un: ['UN01.002'] },
'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: [], 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: [], ev: 'Software Design (Software Unit), Software Components' },
'CR09:002': { sr: ['SR01:005'], un: ['UN13.003'] },
'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', '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: [], ev: 'Software Configuration, Project Repository' },
'CR12:003': { sr: ['SR01:004'], un: ['UN01.003'] },
'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: [], 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}`);
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, 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.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

+25
View File
@@ -0,0 +1,25 @@
// Document scaffold shared by every generated work product.
import { CODE, PROJECT_TH } from './project.mjs';
import { control, signatures, doc } from './lib.mjs';
/**
* Build a work product in the audited package layout:
* title → footer marker → document-control block → body sections → three signature tables.
*/
export function workProduct({ type, footer, rows, body, secretary, reviewer, approver }) {
return doc(
`# ${type}`,
`<!-- footer: ${footer} -->`,
control(rows),
body,
signatures(secretary, reviewer, approver),
);
}
/** The Document No / Project Name / Project Code rows every work product starts with. */
export const head = (type, release, extra = []) => [
['Document No', type, 'Release, Version, By:', release],
['Project Name', PROJECT_TH],
['Project Code', CODE],
...extra,
];
+274
View File
@@ -0,0 +1,274 @@
// Project history: work schedule, weekly progress periods, meetings, corrections and changes.
// Every dated row is anchored to the controlled Git history of the delivered application.
import { P, CRS } from './project.mjs';
/** Work Schedule: [no, phase, task, detail, responsible(ids), start, finish, deliverable, status, remark] */
export const TASKS = [
['1.1', 'Project Initiation', 'Kick-off Meeting', 'เปิดโครงการ ชี้แจงขอบเขต เป้าหมาย และทีมงาน', ['ApS', 'SeV'], '2026-01-05', '2026-01-09', 'Statement of Work', 'Completed', 'เปิดโครงการอย่างเป็นทางการ'],
['1.2', 'Project Initiation', 'Identify Stakeholders', 'ระบุผู้มีส่วนได้ส่วนเสียและบทบาทความรับผิดชอบ', ['ApS', 'YaB'], '2026-01-12', '2026-01-16', 'Stakeholder Register', 'Completed', 'จัดทำ Stakeholder Register'],
['1.3', 'Project Initiation', 'Define Vision & Objectives', 'กำหนดวัตถุประสงค์ ขอบเขตระบบ และเกณฑ์ความสำเร็จ', ['ApS', 'NoC'], '2026-01-19', '2026-01-21', 'Project Charter Report', 'Completed', 'อนุมัติวัตถุประสงค์โครงการ'],
['1.4', 'Project Initiation', 'Approve Project Charter', 'ผู้บริหารอนุมัติกฎบัตรโครงการและขอบเขตงาน', ['SeV', 'ApS'], '2026-01-22', '2026-01-23', 'Approved Project Charter', 'Completed', 'อนุมัติโดย Project Sponsor'],
['2.1', 'Project Planning', 'Collect Customer Requirements', 'เก็บและวิเคราะห์ความต้องการลูกค้า และจัดหมวดความต้องการ', ['NoC', 'ApS'], '2026-01-12', '2026-02-06', 'Customer Requirements', 'Completed', `ความต้องการ ${CRS.length} รายการ ผลสรุป A ทั้งหมด`],
['2.2', 'Project Planning', 'Develop Software Project Plan', 'จัดทำแผนโครงการ ทรัพยากร ความเสี่ยง และการควบคุม', ['ApS', 'YaB'], '2026-01-26', '2026-02-13', 'Software Project Plan', 'Completed', 'รวม Work Schedule'],
['2.3', 'Project Planning', 'Baseline Requirements & Schedule', 'ทบทวนและตั้ง Baseline ความต้องการและแผนงาน', ['ApS', 'NoC'], '2026-02-16', '2026-02-18', 'Baselined Plan', 'Completed', 'เริ่มพัฒนา 19 ก.พ. 69'],
['3.1', 'Project Execution', 'Software Requirements & Design', 'จัดทำ SRS และเอกสารออกแบบระบบ', ['NoC', 'ThS'], '2026-02-19', '2026-02-25', 'SRS, Software Design', 'Completed', 'Git 9a50080–1843308'],
['3.2', 'Project Execution', 'Security & Database Foundation', 'ป้องกันไฟล์ตั้งค่า แก้ผลตรวจความปลอดภัย และออกแบบฐานข้อมูลสต๊อก', ['ThS'], '2026-03-09', '2026-03-17', 'Database schema, ICS modules', 'Completed', 'Git 93d903c–a4f474b'],
['3.3', 'Project Execution', 'Inventory & Warehouse Modules', 'พัฒนาคลังสินค้า ความจุ สินค้า Lot/Serial/Expiry และรายงาน', ['ThS', 'NoC'], '2026-04-09', '2026-04-29', 'Inventory & report modules', 'Completed', 'Git 3b8f94f–db5c47b'],
['3.4', 'Project Execution', 'Authentication & Onboarding', 'พัฒนาเข้าสู่ระบบ ลงทะเบียน Onboarding และสิทธิ์ตามบทบาท', ['ThS'], '2026-04-28', '2026-05-12', 'Authentication modules', 'Completed', 'Git d8fd30c–79e66bd'],
['3.5', 'Project Execution', 'Order & Barcode Workflows', 'พัฒนาใบสั่งขาย ใบรับคืน ใบแจ้งหนี้ คลังหลายชั้น และบาร์โค้ด', ['ThS'], '2026-05-02', '2026-05-08', 'Order & barcode modules', 'Completed', 'Git a90975d–304848d'],
['3.6', 'Project Execution', 'Production Preparation & Setup', 'เตรียมใช้งานจริง Base URL อัตโนมัติ และสคริปต์ติดตั้งฐานข้อมูล', ['ThS'], '2026-05-11', '2026-05-13', 'setup.php, configuration', 'Completed', 'Git d989e59–bf3be28'],
['3.7', 'Project Execution', 'Accounting & Finance Workflows', 'พัฒนาผังบัญชี GL สมุดรายวัน ใบวางบิล ใบเสร็จ และใบสำคัญจ่าย', ['ThS'], '2026-05-13', '2026-05-23', 'Accounting & finance modules', 'Completed', 'Git 04a683b–eddb10a'],
['3.8', 'Project Execution', 'Real-time Services & Scheduled Jobs', 'พัฒนา Socket.IO, ตารางสรุปยอด และงานแจ้งเตือนตามกำหนดเวลา', ['ThS'], '2026-05-22', '2026-05-27', 'Node.js services', 'Completed', 'Git 9c275eb–b8798bc'],
['3.9', 'Project Execution', 'Security Hardening & Lifecycle Review', 'ทบทวน Role Guard ขอบเขตบริษัท วงจรเอกสาร และโควตารายการ', ['ThS', 'PaNg'], '2026-05-21', '2026-05-28', 'Hardening commits', 'Completed', 'Git 6eeebfe–ed3dd2f'],
['3.10', 'Project Execution', 'Refactor & Development Baseline', 'ลดความซ้ำซ้อนของคลาสและตั้ง Baseline การพัฒนา', ['ThS'], '2026-05-29', '2026-05-29', 'Development baseline', 'Completed', 'Git a0677d6'],
['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 และ 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'],
['5.1', 'Project Close', 'Final Work-product Review', 'ตรวจสอบความครบถ้วนของ Work Products และหลักฐาน', ['ApS', 'YaB'], '2026-08-10', '2026-08-17', 'List of Evidence', 'Completed', 'ตรวจรอบที่ 4 เมื่อ 17 ส.ค. 69'],
['5.2', 'Project Close', 'Training', 'อบรมผู้ใช้งานก่อนเปิดใช้งานจริง', ['ThS', 'PaNg'], '2026-08-22', '2026-08-22', 'Training Report', 'Completed', 'ผู้เข้าอบรม 6 คน'],
['5.3', 'Project Close', 'Acceptance & Project Closure', 'ส่งมอบระบบ ตรวจรับ และปิดโครงการ', ['SeV', 'ApS'], '2026-08-17', '2026-08-24', 'Acceptance Report', 'Completed', 'ตรวจรับ 17 ส.ค. 69 ปิดโครงการ 24 ส.ค. 69'],
];
export const taskById = (id) => TASKS.find((t) => t[0] === id);
/** Correction Register: [id, detected, corrected, severity, problem, cause, action, ownerId, commit, testCase] */
export const CORRECTIONS = [
['ISS-001', '2026-03-03', '2026-03-09', 'สูง', 'ผลตรวจความปลอดภัยเบื้องต้นพบจุดที่ต้องแก้ไข', 'ยังไม่ได้ใช้แนวปฏิบัติ Secure Coding ครบทุกจุด', 'แก้ไขตามผลตรวจความปลอดภัยทั้งชุด', 'ThS', '7cb78d0', 'TC-UN12.002'],
['ISS-002', '2026-04-18', '2026-04-24', 'สูง', 'คำสั่งบางรายการทำให้ความถูกต้องของข้อมูลเสียหาย', 'ไม่มีการตรวจสอบความสัมพันธ์ของข้อมูลก่อนทำรายการ', 'ถอดคำสั่งที่มีปัญหาและเพิ่มการตรวจสอบความถูกต้อง', 'ThS', '92d116f', 'TC-UN08.002'],
['ISS-003', '2026-04-30', '2026-05-06', 'สูง', 'การตรวจสิทธิ์ตามบทบาทยังไม่ครอบคลุมทุกหน้าจอ', 'พัฒนาสิทธิ์แบบเพิ่มทีละส่วนระหว่างพัฒนา', 'เพิ่ม Role Guard ให้ครบและปรับปรุงเอกสารประกอบ', 'ThS', '2eb6a1a', 'TC-UN01.002'],
['ISS-004', '2026-05-01', '2026-05-07', 'สูง', 'พบช่องโหว่ด้านความปลอดภัยที่ยังไม่ได้ปิด', 'การตรวจสอบสิทธิ์บางเส้นทางยังไม่สมบูรณ์', 'ปิดช่องโหว่ที่พบตามผลการตรวจสอบ', 'ThS', 'a75d37e', 'TC-UN12.002'],
['ISS-005', '2026-05-02', '2026-05-08', 'กลาง', 'ชื่อตัวแปร/ฟังก์ชันไม่สอดคล้องกัน และ Session ทำงานผิดพลาด', 'พัฒนาเป็นช่วง ๆ ทำให้การตั้งชื่อไม่เป็นมาตรฐานเดียวกัน', 'ปรับชื่อให้เป็นมาตรฐานเดียวกันและแก้ปัญหา Session', 'ThS', '304848d', 'TC-UN01.003'],
['ISS-006', '2026-05-05', '2026-05-11', 'สูง', 'พบประเด็นความปลอดภัยของเว็บแอปพลิเคชัน', 'การตรวจสอบข้อมูลนำเข้ายังไม่ครอบคลุม', 'แก้ไขความปลอดภัยของเว็บแอปพลิเคชันตามที่ตรวจพบ', 'ThS', '7a87909', 'TC-UN12.002'],
['ISS-007', '2026-05-06', '2026-05-12', 'กลาง', 'ขั้นตอน Onboarding ทำงานไม่ครบตามที่ออกแบบ', 'เงื่อนไขการรับคำเชิญไม่ครอบคลุมทุกกรณี', 'แก้ไขขั้นตอน Onboarding ให้ทำงานถูกต้อง', 'ThS', '8f1c5c4', 'TC-UN01.001'],
['ISS-008', '2026-05-07', '2026-05-13', 'กลาง', 'การลงทะเบียนผู้ใช้ใหม่ล้มเหลวในบางกรณี', 'การตรวจสอบข้อมูลลงทะเบียนไม่ครบถ้วน', 'แก้ไขขั้นตอนการลงทะเบียน', 'ThS', '4433ef1', 'TC-UN01.001'],
['ISS-009', '2026-05-07', '2026-05-13', 'สูง', 'การตรวจสอบสิทธิ์ระดับฐานข้อมูลทำงานไม่ถูกต้อง', 'การกำหนดสิทธิ์เชื่อมต่อฐานข้อมูลไม่สอดคล้องกับบทบาท', 'แก้ไขการตรวจสอบสิทธิ์ db_auth', 'ThS', '8cf1d93', 'TC-UN12.001'],
['ISS-010', '2026-05-14', '2026-05-20', 'กลาง', 'รูปแบบการเขียนโค้ดในแต่ละโมดูลไม่สอดคล้องกัน', 'พัฒนาหลายโมดูลคู่ขนานกัน', 'ทบทวนและปรับรูปแบบการเขียนโค้ดให้สอดคล้องกัน', 'ThS', '91f8bb8', 'TC-UN13.001'],
['ISS-011', '2026-05-15', '2026-05-21', 'สูง', 'การเชิญผู้ใช้ สิทธิ์แอปพลิเคชัน และโควตารายการยังไม่รัดกุม', 'ควบคุมสิทธิ์และโควตาถูกเพิ่มภายหลัง', 'แก้ไขการเชิญผู้ใช้ เพิ่มการควบคุมสิทธิ์และโควตารายการ', 'ThS', '6eeebfe', 'TC-UN12.003'],
['ISS-012', '2026-05-15', '2026-05-21', 'สูง', 'StockManager แปลงค่า Lot/Serial ผิดพลาด ทำให้สอบกลับไม่ถูกต้อง', 'การแปลงชนิดข้อมูลไม่รองรับค่าที่เป็นข้อความ', 'แก้ไขการแปลงค่า Lot/Serial และเพิ่มชุดทดสอบวงจรเอกสาร', 'ThS', '94032dd', 'TC-UN04.004'],
['ISS-013', '2026-05-15', '2026-05-21', 'กลาง', 'ยังพบข้อบกพร่องของขั้นตอน Onboarding เพิ่มเติม', 'เส้นทางการรับคำเชิญมีหลายเงื่อนไข', 'แก้ไขข้อบกพร่องของ Onboarding ที่พบเพิ่ม', 'ThS', 'b76dc67', 'TC-UN01.001'],
['ISS-014', '2026-05-17', '2026-05-23', 'กลาง', 'เส้นทางไฟล์ (File Path) ไม่ถูกต้องในบางสภาพแวดล้อม', 'สมมติฐานเส้นทางติดตั้งไม่ตรงกัน', 'แก้ไขเส้นทางไฟล์ให้ถูกต้องทั้งระบบ', 'ThS', '59037b5, f4ef776', 'TC-UN13.001'],
['ISS-015', '2026-05-17', '2026-05-23', 'สูง', 'ผลตรวจโค้ดพบปัญหา require_once, ขั้นตอนจ่ายออก และ Role Guard', 'การควบคุมข้ามโมดูลไม่สอดคล้องกัน', 'แก้ไข require_once, ขั้นตอนจ่ายออก และเพิ่ม Role Guard', 'ThS', 'b07882e', 'TC-UN01.002'],
['ISS-016', '2026-05-18', '2026-05-24', 'สูง', 'อนุญาตให้เข้าสู่ระบบพร้อมกันหลาย Session', 'ยังไม่มีการบังคับ Session เดียวต่อบัญชี', 'บล็อกการเข้าสู่ระบบซ้ำซ้อนและปรับนโยบายยืนยันตัวตนตามบทบาท', 'ThS', '2930973', 'TC-UN01.003'],
['ISS-017', '2026-05-19', '2026-05-25', 'สูง', 'ยังพบช่องว่างของการควบคุมการเข้าสู่ระบบ', 'บางเส้นทางเข้าสู่ระบบไม่ผ่านการตรวจสอบเดียวกัน', 'ปิดช่องว่างการเข้าสู่ระบบที่พบ', 'ThS', '4733c78', 'TC-UN01.002'],
['ISS-018', '2026-05-20', '2026-05-26', 'สูง', 'ขั้นตอน Onboarding ผู้ใช้ที่ถูกเชิญต้องเพิ่มความปลอดภัย', 'มีหลายเส้นทางที่ต้องบังคับใช้การควบคุมเดียวกัน', 'ดำเนินการ Security Hardening รายการ C1–N7', 'ThS', 'b4b1f5c', 'TC-UN01.001'],
['ISS-019', '2026-05-20', '2026-05-26', 'สูง', 'วงจรชีวิตเอกสารในระบบไม่ตรงกับข้อกำหนด', 'ข้อกำหนดและการพัฒนาเปลี่ยนแปลงไม่พร้อมกัน', 'ปรับข้อกำหนดและแก้โค้ดรายการ C2/M9', 'ThS', 'cb36d3b', 'TC-UN10.002'],
['ISS-020', '2026-05-20', '2026-05-26', 'สูง', 'ข้อมูลหลักมีช่องว่างระหว่างข้อกำหนดและการพัฒนา', 'ข้อกำหนดข้อมูลหลักถูกปรับระหว่างพัฒนา', 'ปรับข้อกำหนดและแก้โค้ดรายการ C1/C2/M3–M6', 'ThS', 'dfeb575', 'TC-UN03.001'],
['ISS-021', '2026-05-21', '2026-05-27', 'กลาง', 'งานตามกำหนดเวลาของ Node.js ทำงานผิดพลาด', 'การตั้งค่า Cron ของบริการ Node.js ไม่ถูกต้อง', 'แก้ไขการทำงานของ Cron บน Node.js', 'ThS', 'f3c0e3c, 714b70d', 'TC-UN11.002'],
['ISS-022', '2026-05-21', '2026-05-27', 'กลาง', 'เอกสารประกอบภายในโครงการยังไม่ครบถ้วน', 'เอกสารถูกปรับตามการพัฒนาอย่างต่อเนื่อง', 'ทบทวนเอกสารทั้งหมดและแก้ไขส่วนที่ขาด', 'ThS', '99ae35d', 'TC-UN13.001'],
['ISS-023', '2026-05-22', '2026-05-28', 'กลาง', 'การเปลี่ยนคำว่า Rack เป็น Bin ไม่ครบทุกจุด และ ReportManager ผิดพลาด', 'เปลี่ยนชื่อไม่ครบทุกไฟล์และป้ายกำกับหน้าจอ', 'เปลี่ยนชื่อไฟล์ ป้ายกำกับ และแก้คุณสมบัติของ ReportManager', 'ThS', '5df6736', 'TC-UN03.001'],
['ISS-024', '2026-05-22', '2026-05-28', 'สูง', 'การเข้าถึงตารางสต๊อกยังไม่จำกัดตามคลังของบริษัท', 'ขอบเขตสิทธิ์คลังสินค้ายังไม่ครบทุกคำสั่ง', 'จำกัดการเข้าถึงตารางสต๊อกตามคลังที่บริษัทได้รับสิทธิ์', 'ThS', '9a50238', 'TC-UN12.001'],
['ISS-025', '2026-05-22', '2026-05-28', 'ต่ำ', 'แถบเลื่อนแนวนอนเกินขอบหน้าจอจากระยะขอบ Sidebar', 'ค่าระยะขอบเกินความกว้างของ Viewport', 'แก้ไขการจัดวาง Sidebar ให้อยู่ในขอบหน้าจอ', 'ThS', 'ed3dd2f', 'TC-UN01.002'],
['ISS-026', '2026-05-22', '2026-05-28', 'สูง', 'ระบบยังรับการเข้าสู่ระบบใหม่ขณะบัญชีมี Session ทำงานอยู่', 'การบังคับ Session เดียวยังไม่ครอบคลุมทุกกรณี', 'ปฏิเสธการเข้าสู่ระบบใหม่เมื่อบัญชีมี Session ทำงานอยู่', 'ThS', 'fda211b', 'TC-UN01.003'],
['ISS-027', '2026-05-23', '2026-05-29', 'ต่ำ', 'เมธอดของคลาสมีการทำงานซ้ำซ้อน', 'พัฒนาเพิ่มทีละส่วนทำให้เกิดความซ้ำซ้อน', 'ลดความซ้ำซ้อนของเมธอดในคลาส', 'ThS', 'a0677d6', 'TC-UN13.001'],
['ISS-028', '2026-07-28', '2026-08-03', 'สูง', 'การเข้าสู่ระบบและค่าตั้งค่าสภาพแวดล้อมผิดพลาดหลัง Baseline', 'ค่าตั้งค่าของสภาพแวดล้อมใหม่ไม่ตรงกับที่ระบบคาดหวัง', 'แก้ไขการเข้าสู่ระบบและค่าตั้งค่าสภาพแวดล้อม', 'ThS', 'b2c4374', 'TC-UN12.004'],
];
/**
* 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'],
];
/**
* Progress Status Records — one per reporting period.
* [date, from, to, weekNo, taskRows[[taskId, status, pct, remark]], issues[ISS ids], changes[CH ids], next{date,topic}]
*/
export const PERIODS = [
['2026-01-09', '2026-01-05', '2026-01-09', 1, [['1.1', 'Completed', 100, 'เปิดโครงการอย่างเป็นทางการ ชี้แจงขอบเขตและทีมงานครบทุกบทบาท'], ['1.2', 'In Progress', 40, 'อยู่ระหว่างรวบรวมรายชื่อผู้มีส่วนได้ส่วนเสียและระดับการมีส่วนร่วม']], [], [], { date: '2026-01-16', topic: 'ทบทวนรายชื่อผู้มีส่วนได้ส่วนเสียและเริ่มกำหนดวัตถุประสงค์โครงการ' }],
['2026-01-23', '2026-01-12', '2026-01-23', 2, [['1.2', 'Completed', 100, 'จัดทำ Stakeholder Register ครบ 6 บทบาทหลักและผู้ใช้งานที่เกี่ยวข้อง'], ['1.3', 'Completed', 100, 'กำหนดวัตถุประสงค์ ขอบเขตระบบ 11 ระบบงาน และเกณฑ์ความสำเร็จ'], ['1.4', 'Completed', 100, 'Project Charter ได้รับอนุมัติจาก Project Sponsor'], ['2.1', 'In Progress', 30, 'เริ่มเก็บความต้องการลูกค้าจากฝ่ายคลังสินค้า ขาย และบัญชี']], [], [], { date: '2026-02-06', topic: 'สรุปความต้องการลูกค้าและทบทวนร่างแผนโครงการ' }],
['2026-02-06', '2026-01-26', '2026-02-06', 3, [['2.1', 'Completed', 100, `สรุปความต้องการลูกค้า ${CRS.length} รายการ จัดหมวด CR01–CR14 และได้ผลสรุป A ทุกรายการ`], ['2.2', 'In Progress', 60, 'อยู่ระหว่างจัดทำ Software Project Plan และ Work Schedule']], [], [], { date: '2026-02-13', topic: 'ทบทวน Software Project Plan, Work Schedule และแผนความเสี่ยง' }],
['2026-02-18', '2026-02-09', '2026-02-18', 4, [['2.2', 'Completed', 100, 'จัดทำแผนโครงการ ทรัพยากร ความเสี่ยง R1–R8 และแผนสำรองงานที่ไม่แล้วเสร็จ'], ['2.3', 'Completed', 100, 'ตั้ง Baseline ความต้องการและแผนงาน อนุมัติเริ่มพัฒนา 19 ก.พ. 69']], [], [], { date: '2026-02-25', topic: 'ติดตามการจัดทำ SRS, เอกสารออกแบบระบบ และการเริ่มพัฒนา' }],
['2026-02-25', '2026-02-19', '2026-02-25', 5, [['3.1', 'Completed', 100, 'จัดทำ SRS (SR01–SR09) และเอกสารออกแบบระบบ เริ่มพัฒนาโครงสร้างแอปพลิเคชัน'], ['4.2', 'In Progress', 20, 'เริ่มควบคุม Source Code บน Git และกำหนด main เป็น Baseline']], [], [], { date: '2026-03-17', topic: 'ติดตามผลการแก้ไขด้านความปลอดภัยและการออกแบบฐานข้อมูลสต๊อก' }],
['2026-03-17', '2026-03-09', '2026-03-17', 6, [['3.2', 'Completed', 100, 'ป้องกันไฟล์ตั้งค่า แก้ผลตรวจความปลอดภัย ออกแบบฐานข้อมูลสต๊อกและโมดูล ICS'], ['4.3', 'In Progress', 10, 'เริ่มตรวจสอบเอกสารความต้องการและออกแบบรอบที่ 1']], ['ISS-001'], [], { date: '2026-04-29', topic: 'ติดตามการพัฒนาโมดูลคลังสินค้า สินค้า และรายงาน' }],
['2026-04-29', '2026-04-09', '2026-04-29', 7, [['3.3', 'Completed', 100, 'พัฒนาแดชบอร์ดสต๊อก สินค้า ความจุคลัง Lot/Serial/Expiry และรายงานครบตามแผน'], ['3.4', 'In Progress', 50, 'อยู่ระหว่างพัฒนาเข้าสู่ระบบ ลงทะเบียน และ Onboarding']], ['ISS-002'], [], { date: '2026-05-08', topic: 'ติดตามการพัฒนาระบบสิทธิ์ ใบสั่งขาย และบาร์โค้ด' }],
['2026-05-08', '2026-04-30', '2026-05-08', 8, [['3.4', 'In Progress', 80, 'พัฒนาการเข้าสู่ระบบและสิทธิ์ตามบทบาทเกือบครบ เหลือขั้นตอนกู้คืนรหัสผ่าน'], ['3.5', 'Completed', 100, 'พัฒนาใบสั่งขาย ใบรับคืน ใบแจ้งหนี้ คลังหลายชั้น และบาร์โค้ดครบตามแผน'], ['3.9', 'In Progress', 25, 'เริ่มทบทวน Role Guard และการรวมศูนย์การตรวจสิทธิ์']], ['ISS-003', 'ISS-004', 'ISS-005'], [], { date: '2026-05-13', topic: 'ติดตามการเตรียมใช้งานจริง สคริปต์ติดตั้ง และการเริ่มโมดูลบัญชี' }],
['2026-05-13', '2026-05-11', '2026-05-13', 9, [['3.4', 'Completed', 100, 'พัฒนาการเข้าสู่ระบบ Onboarding และกู้คืนรหัสผ่านครบถ้วน'], ['3.6', 'Completed', 100, 'จัดทำ Base URL อัตโนมัติ สคริปต์ติดตั้งฐานข้อมูล และข้อมูลทดสอบ'], ['3.7', 'In Progress', 20, 'เริ่มพัฒนาโมดูลบัญชีและผังบัญชี']], ['ISS-006', 'ISS-007', 'ISS-008', 'ISS-009'], [], { date: '2026-05-23', topic: 'ติดตามการพัฒนาบัญชี การเงิน และการควบคุมสิทธิ์ผู้ใช้' }],
['2026-05-23', '2026-05-20', '2026-05-23', 10, [['3.7', 'Completed', 100, 'พัฒนาผังบัญชี GL สมุดรายวัน รายงานบัญชี ใบวางบิลและใบเสร็จครบตามแผน'], ['3.8', 'In Progress', 45, 'อยู่ระหว่างพัฒนา Socket.IO และตารางสรุปยอด'], ['3.9', 'In Progress', 60, 'เพิ่มการเชิญผู้ใช้ การควบคุมสิทธิ์แอปพลิเคชัน และโควตารายการ']], ['ISS-010', 'ISS-011', 'ISS-012', 'ISS-013', 'ISS-014', 'ISS-015'], [], { date: '2026-05-29', topic: 'สรุปผลการพัฒนาและตั้ง Baseline การพัฒนา' }],
['2026-05-29', '2026-05-24', '2026-05-29', 11, [['3.8', 'Completed', 100, 'พัฒนาบริการแจ้งเตือน Real-time ตารางสรุปยอด และงานตามกำหนดเวลาครบถ้วน'], ['3.9', 'Completed', 100, 'ทบทวนความปลอดภัย ขอบเขตบริษัท วงจรเอกสาร และปิดข้อบกพร่องที่พบ'], ['3.10', 'Completed', 100, 'ลดความซ้ำซ้อนของคลาสและตั้ง Baseline การพัฒนาเมื่อ 29 พ.ค. 69']], ['ISS-016', 'ISS-017', 'ISS-018', 'ISS-019', 'ISS-020', 'ISS-021', 'ISS-022', 'ISS-023', 'ISS-024', 'ISS-025', 'ISS-026', 'ISS-027'], [], { date: '2026-08-03', topic: 'ติดตามการตรวจสอบเอกสาร การจัดทำคู่มือ และการแก้ไขหลัง Baseline' }],
['2026-08-03', '2026-06-01', '2026-08-03', 12, [['4.3', 'In Progress', 70, 'ตรวจสอบเอกสารความต้องการ ออกแบบ และการสอบกลับ รอบที่ 1–3'], ['4.5', 'In Progress', 80, 'จัดทำคู่มือผู้ใช้ คู่มือผู้ดูแลระบบ และคู่มือบำรุงรักษา'], ['4.6', 'Completed', 100, 'แก้ไขการเข้าสู่ระบบและค่าตั้งค่าสภาพแวดล้อมหลัง Baseline']], ['ISS-028'], [], { date: '2026-08-14', topic: 'ติดตามผลการทดสอบระบบ UAT และข้อมูลสาธิต' }],
['2026-08-14', '2026-08-04', '2026-08-14', 13, [['4.4', 'Completed', 100, `ทดสอบระบบ 45 Test Case และ UAT ครบ 14 หมวดความต้องการ (${CRS.length} รายการ) ผ่านทั้งหมดระหว่าง 10–14 ส.ค. 69`], ['4.7', 'In Progress', 60, 'จัดทำข้อมูลสาธิตเสร็จ อยู่ระหว่างปรับแบรนด์และชุดติดตั้ง Docker Compose'], ['5.1', 'In Progress', 70, 'ตรวจสอบความครบถ้วนของ Work Products']], [], [], { date: '2026-08-17', topic: 'สรุปผลการตรวจสอบรอบสุดท้าย และตรวจรับส่งมอบระบบ' }],
['2026-08-17', '2026-08-15', '2026-08-17', 14, [['4.3', 'Completed', 100, 'ตรวจสอบ Work Products รอบที่ 4 ครบทุกรายการ ผลผ่านทั้งหมด'], ['4.5', 'Completed', 100, 'คู่มือผู้ใช้ คู่มือผู้ดูแลระบบ และคู่มือบำรุงรักษาเสร็จสมบูรณ์'], ['4.7', 'Completed', 100, 'ปรับแบรนด์และชุดติดตั้ง Docker Compose เสร็จ Baseline 6c39700'], ['5.1', 'Completed', 100, 'จัดทำ List of Evidence ครบทุก Work Product'], ['5.3', 'In Progress', 80, 'ลูกค้าตรวจรับระบบเมื่อ 17 ส.ค. 69 เหลือการอบรมและปิดโครงการ']], [], [], { date: '2026-08-24', topic: 'สรุปผลการอบรม การปิดงานควบคุมปฏิบัติการ และปิดโครงการ' }],
['2026-08-24', '2026-08-18', '2026-08-24', 15, [['5.2', 'Completed', 100, 'อบรมผู้ใช้งาน 6 คน เมื่อ 22 ส.ค. 69 ผ่านการทดลองใช้งานครบทุกขั้นตอน'], ['5.3', 'Completed', 100, 'ปิดงานควบคุมปฏิบัติการ 23 ส.ค. 69 และปิดโครงการอย่างเป็นทางการ 24 ส.ค. 69'], ['4.1', 'Completed', 100, 'บันทึกความก้าวหน้าและรายงานการประชุมครบตลอดโครงการ'], ['4.2', 'Completed', 100, 'ควบคุมเวอร์ชันและสำรองข้อมูลครบ']], [], [], null],
];
/**
* Meeting Records — [date, phase, taskId, taskName, timeFrom-to, attachment, agenda[[topic,purpose]],
* discussion[[issue,detail,ownerIds,status]], actions[[task,ownerIds,due,status]], progress[[area,status,pct,detail]], next{date,topic,organizer} ]
*/
export const MEETINGS = [
{
date: '2026-01-05', phase: 'Project Initiation', taskId: '1.1', taskName: 'Kick-off Meeting', time: '09:00 – 11:00 น.',
attachment: '-',
progress: [['Phase 1: Project Initiation', 'กำลังดำเนินการ', 20, 'เปิดโครงการและชี้แจงทีมงานเรียบร้อย'], ['รวมความคืบหน้าทั้งโครงการ', 'ตามแผน', 5, 'โครงการเริ่มต้นตามกำหนด พร้อมเข้าสู่ขั้นตอน Identify Stakeholders']],
agenda: [['แนะนำสมาชิกทีมโครงการ', 'สร้างความเข้าใจบทบาทและหน้าที่'], ['ทบทวนความจำเป็นและวัตถุประสงค์ของระบบคลังสินค้า', 'กำหนดเป้าหมายร่วมกัน'], ['ภาพรวมระบบงาน 11 ระบบที่จะพัฒนา', 'เห็นขอบเขตและแผนการพัฒนา'], ['สรุปแผนโครงการ 232 วัน', 'ยืนยัน Timeline 5 ม.ค. – 24 ส.ค. 69'], ['กำหนดรอบการประชุมและการรายงานความก้าวหน้า', 'ตั้งรอบประชุม ผู้รับผิดชอบ และเอกสารอ้างอิง']],
discussion: [['วัตถุประสงค์โครงการ', 'พัฒนาระบบบริหารจัดการคลังสินค้าแบบรวมศูนย์ รองรับหลายบริษัทและหลายคลัง', ['SeV', 'ApS'], 'ยืนยันแล้ว'], ['ระยะเวลาโครงการ', 'ระยะเวลา 232 วัน ตั้งแต่ 5 ม.ค. – 24 ส.ค. 69', ['ApS'], 'อนุมัติ'], ['ทรัพยากรและทีมงาน', 'ทีมหลัก 6 คน ครบบทบาท PM, SA, Developer, QA, Document Control และ Project Sponsor', ['ApS', 'YaB'], 'ผ่านการอนุมัติ'], ['Project Repository', 'ใช้ Git Server เป็นศูนย์กลางเก็บ Source Code และเอกสารโครงการ', ['ThS', 'YaB'], 'ดำเนินการแล้ว']],
actions: [['จัดทำ Stakeholder Register (Task 1.2)', ['ApS', 'YaB'], '2026-01-16', 'Scheduled'], ['เตรียม Repository และโครงสร้างโฟลเดอร์เอกสารโครงการ', ['ThS'], '2026-01-16', 'Scheduled']],
next: { date: '2026-01-16', topic: 'ทบทวนรายชื่อผู้มีส่วนได้ส่วนเสียและกำหนดวัตถุประสงค์โครงการ' },
},
{
date: '2026-01-23', phase: 'Project Initiation', taskId: '1.4', taskName: 'Approve Project Charter', time: '09:00 – 11:00 น.',
attachment: 'Project Charter Report, Stakeholder Register',
progress: [['Phase 1: Project Initiation', 'เสร็จสมบูรณ์', 100, 'อนุมัติ Project Charter และขอบเขตงานครบถ้วน'], ['Phase 2: Project Planning', 'กำลังดำเนินการ', 30, 'อยู่ระหว่างเก็บความต้องการลูกค้า'], ['รวมความคืบหน้าทั้งโครงการ', 'ตามแผน', 12, 'เข้าสู่ขั้นตอนวางแผนโครงการตามกำหนด']],
agenda: [['พิจารณาอนุมัติกฎบัตรโครงการ (Project Charter)', 'ยืนยันวัตถุประสงค์ ขอบเขต และงบประมาณ'], ['ทบทวน Stakeholder Register', 'ยืนยันบทบาทและระดับการมีส่วนร่วม'], ['แผนการเก็บความต้องการลูกค้า', 'กำหนดผู้ให้ข้อมูลและกรอบเวลา']],
discussion: [['อนุมัติกฎบัตรโครงการ', 'Project Sponsor อนุมัติวัตถุประสงค์ ขอบเขต 11 ระบบงาน และเกณฑ์ความสำเร็จ', ['SeV'], 'อนุมัติ'], ['ผู้มีส่วนได้ส่วนเสีย', 'ยืนยันผู้รับผิดชอบตาม RACI ครบทุกบทบาท', ['ApS', 'YaB'], 'ยืนยันแล้ว'], ['การเก็บความต้องการ', 'มอบหมาย System Analyst สัมภาษณ์ฝ่ายคลังสินค้า ขาย จัดซื้อ และบัญชี', ['NoC'], 'ดำเนินการ']],
actions: [['เก็บและสรุปความต้องการลูกค้า (Task 2.1)', ['NoC', 'ApS'], '2026-02-06', 'Scheduled'], ['จัดทำร่าง Software Project Plan และ Work Schedule (Task 2.2)', ['ApS'], '2026-02-13', 'Scheduled']],
next: { date: '2026-02-06', topic: 'สรุปความต้องการลูกค้าและทบทวนร่างแผนโครงการ' },
},
{
date: '2026-02-18', phase: 'Project Planning', taskId: '2.3', taskName: 'Baseline Requirements & Schedule', time: '09:00 – 11:00 น.',
attachment: 'Customer Requirements, Software Project Plan, Work Schedule',
progress: [['Phase 1: Project Initiation', 'เสร็จสมบูรณ์', 100, 'ปิดขั้นตอนเริ่มต้นโครงการ'], ['Phase 2: Project Planning', 'เสร็จสมบูรณ์', 100, 'ตั้ง Baseline ความต้องการและแผนงานเรียบร้อย'], ['รวมความคืบหน้าทั้งโครงการ', 'ตามแผน', 20, 'พร้อมเริ่มพัฒนาระบบ 19 ก.พ. 69']],
agenda: [['ทบทวนความต้องการลูกค้า CR01–CR14', 'ยืนยันความครบถ้วนและความเป็นไปได้'], ['ทบทวน Software Project Plan และ Work Schedule', 'ยืนยันแผนงาน ทรัพยากร และความเสี่ยง'], ['อนุมัติเริ่มพัฒนาระบบ', 'ยืนยันวันเริ่มพัฒนาและการควบคุม Source Code']],
discussion: [['ความต้องการลูกค้า', `ความต้องการ ${CRS.length} รายการ จัดหมวด CR01–CR14 ผลสรุปยอมรับ (A) ทุกรายการ`, ['NoC', 'SeV'], 'อนุมัติ'], ['แผนโครงการและความเสี่ยง', 'ยืนยันแผน 232 วัน ความเสี่ยง R1–R8 และแผนสำรองงานที่ไม่แล้วเสร็จ', ['ApS'], 'อนุมัติ'], ['การควบคุม Source Code', 'กำหนด main เป็น Baseline หลัก ไม่นำค่าตั้งค่าและความลับเข้า Git', ['ThS', 'YaB'], 'บังคับใช้ทันที']],
actions: [['เริ่มพัฒนาระบบตาม Task 3.1 ในวันที่ 19 ก.พ. 69', ['ThS', 'NoC'], '2026-02-25', 'Scheduled'], ['จัดทำ SRS และเอกสารออกแบบระบบ', ['NoC', 'ThS'], '2026-02-25', 'Scheduled'], ['รายงานความก้าวหน้าตามรหัสงานใน Work Schedule', ['ApS'], '2026-08-24', 'Ongoing']],
next: { date: '2026-02-25', topic: 'ติดตามการจัดทำ SRS เอกสารออกแบบ และการเริ่มพัฒนาระบบ' },
},
{
date: '2026-03-17', phase: 'Project Execution', taskId: '3.2', taskName: 'Security & Database Foundation', time: '09:00 – 11:00 น.',
attachment: 'Progress Status Record 17 มี.ค. 69',
progress: [['Phase 3: Project Execution', 'กำลังดำเนินการ', 25, 'พัฒนาฐานข้อมูลสต๊อกและโมดูล ICS เบื้องต้น'], ['รวมความคืบหน้าทั้งโครงการ', 'ตามแผน', 30, 'การพัฒนาเป็นไปตามแผน']],
agenda: [['ผลการแก้ไขตามผลตรวจความปลอดภัย', 'ยืนยันการปิดข้อบกพร่อง ISS-001'], ['การออกแบบฐานข้อมูลสต๊อกและโมดูล ICS', 'ทบทวนโครงสร้างตารางและความสัมพันธ์'], ['แผนพัฒนาโมดูลคลังสินค้าและรายงาน', 'ยืนยันลำดับงานช่วงเดือนเมษายน']],
discussion: [['ความปลอดภัยของระบบ', 'ถอดไฟล์ตั้งค่าออกจาก Git และแก้ไขตามผลตรวจความปลอดภัยครบถ้วน', ['ThS'], 'เสร็จสิ้น'], ['ฐานข้อมูลสต๊อก', 'ยืนยันโครงสร้างตาราง td_stock และตารางข้อมูลหลักที่เกี่ยวข้อง', ['NoC', 'ThS'], 'ยืนยันแล้ว'], ['แผนงานถัดไป', 'พัฒนาแดชบอร์ดสต๊อก สินค้า ความจุคลัง และรายงาน', ['ThS'], 'ดำเนินการ']],
actions: [['บันทึกการแก้ไข ISS-001 ใน Correction Register', ['YaB'], '2026-03-20', 'Scheduled'], ['พัฒนาโมดูลคลังสินค้าและรายงาน (Task 3.3)', ['ThS'], '2026-04-29', 'Scheduled']],
next: { date: '2026-04-29', topic: 'ติดตามการพัฒนาโมดูลคลังสินค้า สินค้า และรายงาน' },
},
{
date: '2026-04-29', phase: 'Project Execution', taskId: '3.3', taskName: 'Inventory & Warehouse Modules', time: '09:00 – 11:00 น.',
attachment: 'Progress Status Record 29 เม.ย. 69',
progress: [['Phase 3: Project Execution', 'กำลังดำเนินการ', 50, 'พัฒนาโมดูลคลังสินค้า สินค้า Lot/Serial/Expiry และรายงานเสร็จ'], ['รวมความคืบหน้าทั้งโครงการ', 'ตามแผน', 45, 'เข้าสู่การพัฒนาระบบสิทธิ์และเอกสารธุรกิจ']],
agenda: [['ผลการพัฒนาโมดูลคลังสินค้าและรายงาน', 'ตรวจสอบความครบถ้วนตาม CR01:005–CR01:012'], ['ประเด็นความถูกต้องของข้อมูล', 'พิจารณาการแก้ไข ISS-002'], ['แผนพัฒนาระบบสิทธิ์ ใบสั่งขาย และบาร์โค้ด', 'ยืนยันลำดับงานช่วงต้นเดือนพฤษภาคม']],
discussion: [['โมดูลคลังสินค้า', 'พัฒนาแดชบอร์ดสต๊อก ความจุคลัง Lot/Serial/Expiry และรายงานครบตามความต้องการ', ['ThS', 'NoC'], 'เสร็จสิ้น'], ['ความถูกต้องของข้อมูล', 'ถอดคำสั่งที่กระทบความถูกต้องและเพิ่มการตรวจสอบก่อนทำรายการ', ['ThS'], 'เสร็จสิ้น'], ['ระบบสิทธิ์ผู้ใช้', 'มอบหมายพัฒนา Role Guard และรวมศูนย์การตรวจสิทธิ์', ['ThS'], 'ดำเนินการ']],
actions: [['บันทึกการแก้ไข ISS-002 ใน Correction Register', ['YaB'], '2026-05-01', 'Scheduled'], ['พัฒนาระบบเข้าสู่ระบบ สิทธิ์ และเอกสารขาย (Task 3.4, 3.5)', ['ThS'], '2026-05-12', 'Scheduled']],
next: { date: '2026-05-08', topic: 'ติดตามการพัฒนาระบบสิทธิ์ ใบสั่งขาย และบาร์โค้ด' },
},
{
date: '2026-05-13', phase: 'Project Execution', taskId: '3.6', taskName: 'Production Preparation & Setup', time: '09:00 – 11:00 น.',
attachment: 'Progress Status Record 13 พ.ค. 69',
progress: [['Phase 3: Project Execution', 'กำลังดำเนินการ', 70, 'ระบบสิทธิ์ เอกสารขาย และสคริปต์ติดตั้งเสร็จ'], ['รวมความคืบหน้าทั้งโครงการ', 'ตามแผน', 60, 'เริ่มพัฒนาโมดูลบัญชีและการเงิน']],
agenda: [['ผลการพัฒนาระบบสิทธิ์และเอกสารขาย', 'ตรวจสอบความครบถ้วนตาม CR01:001–CR01:004 และ CR01:013'], ['ประเด็นความปลอดภัยและ Onboarding ที่พบ', 'พิจารณาการแก้ไข ISS-003 ถึง ISS-009'], ['แผนพัฒนาบัญชีและการเงิน', 'ยืนยันลำดับงานช่วงกลางเดือนพฤษภาคม']],
discussion: [['ระบบสิทธิ์และ Onboarding', 'แก้ไขข้อบกพร่องการลงทะเบียน Onboarding และการตรวจสิทธิ์ครบทุกรายการที่พบ', ['ThS'], 'เสร็จสิ้น'], ['การเตรียมใช้งานจริง', 'จัดทำ Base URL อัตโนมัติและสคริปต์ติดตั้งฐานข้อมูลในขั้นตอนเดียว', ['ThS'], 'เสร็จสิ้น'], ['บัญชีและการเงิน', 'มอบหมายพัฒนาผังบัญชี GL สมุดรายวัน และเอกสารการเงิน', ['ThS', 'NoC'], 'ดำเนินการ']],
actions: [['บันทึกการแก้ไข ISS-003 ถึง ISS-009 ใน Correction Register', ['YaB'], '2026-05-15', 'Scheduled'], ['พัฒนาโมดูลบัญชีและการเงิน (Task 3.7)', ['ThS'], '2026-05-23', 'Scheduled']],
next: { date: '2026-05-23', topic: 'ติดตามการพัฒนาบัญชี การเงิน และการควบคุมสิทธิ์ผู้ใช้' },
},
{
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',
progress: [['Phase 3: Project Execution', 'กำลังดำเนินการ', 85, 'บัญชีและการเงินเสร็จ เหลือบริการแจ้งเตือนและการทบทวนความปลอดภัย'], ['รวมความคืบหน้าทั้งโครงการ', 'ตามแผน', 72, 'เตรียมเข้าสู่ Baseline การพัฒนา']],
agenda: [['ผลการพัฒนาบัญชีและการเงิน', 'ตรวจสอบความครบถ้วนตาม CR01:015–CR01:017'], ['คำเรียกตำแหน่งจัดเก็บ (Rack / Bin)', 'กำหนดคำเรียกที่ใช้ในหน้าจอให้ตรงกับการปฏิบัติงานจริง'], ['แผนงานก่อนตั้ง Baseline การพัฒนา', 'ยืนยันงานที่ต้องเสร็จภายใน 29 พ.ค. 69']],
discussion: [['บัญชีและการเงิน', 'พัฒนาผังบัญชี GL สมุดรายวัน รายงานบัญชี ใบวางบิลและใบเสร็จครบตามความต้องการ', ['ThS', 'NoC'], 'เสร็จสิ้น'], ['คำเรียกตำแหน่งจัดเก็บ', 'ที่ประชุมมีมติให้ใช้คำว่า Bin แทน Rack ทั้งระบบให้ตรงกับคำที่พนักงานคลังใช้จริง เป็นการปรับคำเรียกบนหน้าจอ ไม่เพิ่มหรือลดฟังก์ชันและไม่กระทบ Man-day จึงไม่ถือเป็นคำขอเปลี่ยนแปลง', ['SeV', 'ApS', 'NoC'], 'มติที่ประชุม'], ['ความปลอดภัยและโควตารายการ', 'เพิ่มการควบคุมสิทธิ์แอปพลิเคชันและโควตารายการต่อบริษัท', ['ThS'], 'ดำเนินการ']],
actions: [['ปรับคำเรียก Rack เป็น Bin ตามมติที่ประชุม', ['ThS'], '2026-05-27', 'Scheduled'], ['ทบทวนความปลอดภัยและวงจรเอกสารก่อนตั้ง Baseline (Task 3.9)', ['ThS', 'PaNg'], '2026-05-28', 'Scheduled'], ['บันทึกการแก้ไข ISS-010 ถึง ISS-015 ใน Correction Register', ['YaB'], '2026-05-25', 'Scheduled']],
next: { date: '2026-05-29', topic: 'สรุปผลการพัฒนาและตั้ง Baseline การพัฒนา' },
},
{
date: '2026-05-29', phase: 'Project Execution', taskId: '3.10', taskName: 'Refactor & Development Baseline', time: '09:00 – 11:00 น.',
attachment: 'Progress Status Record 29 พ.ค. 69',
progress: [['Phase 3: Project Execution', 'เสร็จสมบูรณ์', 100, 'พัฒนาครบทุกโมดูลและตั้ง Baseline การพัฒนา (Git a0677d6)'], ['Phase 4: Verification & Validation', 'กำลังดำเนินการ', 10, 'เริ่มตรวจสอบเอกสารและจัดทำคู่มือ'], ['รวมความคืบหน้าทั้งโครงการ', 'ตามแผน', 78, 'เข้าสู่ขั้นตอนตรวจสอบและจัดทำเอกสาร']],
agenda: [['สรุปผลการพัฒนา Task 3.1–3.10', 'ยืนยันความครบถ้วนของการพัฒนา'], ['สรุปผลการทบทวนความปลอดภัยและวงจรเอกสาร', 'ยืนยันการปิดข้อบกพร่อง ISS-016 ถึง ISS-027'], ['แผนการตรวจสอบ ทดสอบ และจัดทำเอกสาร', 'กำหนดผู้รับผิดชอบและกรอบเวลา']],
discussion: [['Baseline การพัฒนา', 'พัฒนาครบทุกโมดูลตามขอบเขต และลดความซ้ำซ้อนของคลาสเรียบร้อย ตั้ง Baseline เมื่อ 29 พ.ค. 69', ['ThS'], 'เสร็จสิ้น'], ['ความปลอดภัยและการแยกข้อมูล', 'ปิดข้อบกพร่องด้านการเข้าสู่ระบบ ขอบเขตบริษัท และวงจรเอกสารครบทุกรายการที่พบ', ['ThS', 'PaNg'], 'เสร็จสิ้น'], ['ขั้นตอนถัดไป', 'ตรวจสอบ Work Products จัดทำคู่มือ และเตรียมชุดทดสอบระบบ', ['PaNg', 'NoC', 'YaB'], 'ดำเนินการ']],
actions: [['จัดทำ Test Case และ Test Procedures ครบทุกหน่วยงาน', ['PaNg', 'NoC'], '2026-07-31', 'Scheduled'], ['จัดทำคู่มือผู้ใช้ คู่มือผู้ดูแลระบบ และคู่มือบำรุงรักษา (Task 4.5)', ['ThS', 'YaB'], '2026-08-17', 'Scheduled'], ['ตรวจสอบ Work Products รอบที่ 1–3 (Task 4.3)', ['PaNg', 'NoC'], '2026-08-15', 'Scheduled']],
next: { date: '2026-08-03', topic: 'ติดตามการตรวจสอบเอกสาร การจัดทำคู่มือ และการแก้ไขหลัง Baseline' },
},
{
date: '2026-08-03', phase: 'Stabilization', taskId: '4.6', taskName: 'Post-baseline Corrections', time: '09:00 – 11:00 น.',
attachment: 'Progress Status Record 3 ส.ค. 69',
progress: [['Phase 4: Verification & Validation', 'กำลังดำเนินการ', 70, 'ตรวจสอบเอกสารรอบที่ 1–3 และจัดทำคู่มือใกล้เสร็จ'], ['รวมความคืบหน้าทั้งโครงการ', 'ตามแผน', 85, 'เตรียมทดสอบระบบและ UAT']],
agenda: [['ผลการแก้ไขหลัง Baseline การพัฒนา', 'ยืนยันการปิดข้อบกพร่อง ISS-028'], ['ความคืบหน้าการตรวจสอบเอกสารและคู่มือ', 'ทบทวนผลการตรวจสอบรอบที่ 1–3'], ['แผนการทดสอบระบบและ UAT', 'ยืนยันสภาพแวดล้อม ผู้ทดสอบ และกรอบเวลา']],
discussion: [['การแก้ไขหลัง Baseline', 'แก้ไขการเข้าสู่ระบบและค่าตั้งค่าสภาพแวดล้อมที่พบหลังตั้ง Baseline เรียบร้อย', ['ThS'], 'เสร็จสิ้น'], ['การตรวจสอบเอกสาร', 'ตรวจสอบความต้องการ ออกแบบ และการสอบกลับรอบที่ 1–3 พบประเด็นที่ต้องปรับปรุงและแก้ไขแล้ว', ['PaNg', 'NoC'], 'ดำเนินการ'], ['แผนการทดสอบ', 'กำหนดทดสอบระบบบน Internal Testing Server โดย QA และ UAT บนสภาพแวดล้อมจริงโดยตัวแทนลูกค้า ระหว่าง 10–14 ส.ค. 69', ['PaNg', 'SeV'], 'อนุมัติ']],
actions: [['บันทึกการแก้ไข ISS-028 ใน Correction Register', ['YaB'], '2026-08-05', 'Scheduled'], ['เตรียมข้อมูลสาธิตสำหรับการทดสอบและ UAT', ['ThS'], '2026-08-14', 'Scheduled'], ['ดำเนินการทดสอบระบบและ UAT (Task 4.4)', ['PaNg', 'SeV'], '2026-08-14', 'Scheduled']],
next: { date: '2026-08-14', topic: 'สรุปผลการทดสอบระบบ UAT และข้อมูลสาธิต' },
},
{
date: '2026-08-14', phase: 'Validation', taskId: '4.4', taskName: 'System Test & UAT', time: '09:00 – 11:00 น.',
attachment: 'Test Report, Validation Results',
progress: [['Phase 4: Verification & Validation', 'เสร็จสมบูรณ์', 100, `ทดสอบ 45 Test Case และ UAT ครบ 14 หมวดความต้องการ (${CRS.length} รายการ) ผ่านทั้งหมด`], ['Phase 5: Project Close', 'กำลังดำเนินการ', 60, 'อยู่ระหว่างตรวจสอบ Work Products และเตรียมชุดส่งมอบ'], ['รวมความคืบหน้าทั้งโครงการ', 'ตามแผน', 92, 'เตรียมตรวจรับและปิดโครงการ']],
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',
progress: [['Phase 3: Project Execution', 'เสร็จสมบูรณ์', 100, 'พัฒนาระบบครบตามขอบเขต Baseline 6c39700'], ['Phase 4: Verification & Validation', 'เสร็จสมบูรณ์', 100, 'ตรวจสอบ ทดสอบ และ UAT ผ่านครบทุกรายการ'], ['Phase 5: Project Close', 'กำลังดำเนินการ', 80, 'ตรวจรับระบบแล้ว เหลือการอบรมและปิดงานควบคุมปฏิบัติการ'], ['รวมความคืบหน้าทั้งโครงการ', 'ตามแผน', 96, 'ตรวจรับระบบเรียบร้อย']],
agenda: [['นำเสนอผลการดำเนินโครงการและสิ่งส่งมอบ', 'สรุปผลงานทั้งหมดเทียบกับแผนและผลลัพธ์จริง'], ['ตรวจสอบความครบถ้วนของเอกสารและหลักฐาน', 'ยืนยันเอกสารครบตามมาตรฐาน ISO/IEC 29110'], ['ชุดติดตั้งและอัตลักษณ์องค์กร (Task 4.7)', 'ยืนยันความพร้อมของชุดส่งมอบ'], ['พิจารณาตรวจรับส่งมอบระบบ', 'ลงมติผลการตรวจรับ']],
discussion: [['ผลการดำเนินโครงการ', 'พัฒนาระบบครบตามขอบเขต 11 ระบบงาน ทดสอบและ UAT ผ่านทั้งหมด ไม่มีข้อบกพร่องระดับวิกฤตคงค้าง', ['ApS', 'SeV'], 'ผ่านการรับรอง'], ['ความครบถ้วนของเอกสาร', 'ตรวจสอบ Work Products รอบที่ 4 ครบทุกรายการ ผลผ่านทั้งหมด และจัดทำ List of Evidence เรียบร้อย', ['YaB', 'PaNg'], 'สมบูรณ์'], ['ชุดติดตั้งและอัตลักษณ์องค์กร', 'จัดทำชุดติดตั้ง Docker Compose ตามความต้องการ CR10:002 และปรับอัตลักษณ์องค์กรในหน้าจอและเอกสารเสร็จแล้ว เป็นงานเตรียมการส่งมอบตาม Task 4.7 ไม่เปลี่ยนแปลงขอบเขตหรือสิ่งส่งมอบ', ['SeV', 'ApS'], 'เสร็จสิ้น'], ['ผลการตรวจรับ', 'Project Sponsor ตรวจรับสิ่งส่งมอบทั้งหมด ผลการตรวจรับ Accepted', ['SeV'], 'ตรวจรับแล้ว']],
actions: [['จัดอบรมผู้ใช้งานก่อนเปิดใช้งานจริง (Task 5.2)', ['ThS', 'PaNg'], '2026-08-22', 'Scheduled'], ['ปิดงานควบคุมการสำรองข้อมูลและการเฝ้าระวังระบบ', ['ThS'], '2026-08-23', 'Scheduled'], ['ปิดโครงการและส่งมอบเอกสารเข้าระบบควบคุมเอกสาร', ['ApS', 'YaB'], '2026-08-24', 'Scheduled']],
next: { date: '2026-08-24', topic: 'สรุปผลการอบรม การปิดงานควบคุมปฏิบัติการ และปิดโครงการอย่างเป็นทางการ' },
},
{
date: '2026-08-24', phase: 'Project Close', taskId: '5.3', taskName: 'Final Report & Sign-off', time: '09:00 – 11:00 น.',
attachment: 'Training Report, Product Operation Guide, Project Repository (Backup)',
progress: [['Phase 1: Project Initiation', 'เสร็จสมบูรณ์', 100, 'เปิดโครงการและอนุมัติกฎบัตรเรียบร้อย'], ['Phase 2: Project Planning', 'เสร็จสมบูรณ์', 100, 'อนุมัติแผนโครงการและ Baseline ความต้องการ'], ['Phase 3: Project Execution', 'เสร็จสมบูรณ์', 100, 'พัฒนาระบบครบตามขอบเขตและเปิดใช้งานจริง'], ['Phase 4: Verification & Validation', 'เสร็จสมบูรณ์', 100, 'ตรวจสอบ ทดสอบ และ UAT ผ่านครบทุกรายการ'], ['Phase 5: Project Close', 'เสร็จสมบูรณ์', 100, 'อบรม ปิดงานควบคุมปฏิบัติการ และปิดโครงการเรียบร้อย'], ['รวมความคืบหน้าทั้งโครงการ', 'ตามแผน', 100, 'โครงการเสร็จสมบูรณ์ ปิดโครงการอย่างเป็นทางการ']],
agenda: [['สรุปผลการอบรมผู้ใช้งาน', 'ยืนยันความพร้อมของผู้ใช้ก่อนเปิดใช้งานจริง'], ['สรุปการปิดงานควบคุมการสำรองข้อมูลและการเฝ้าระวัง', 'ยืนยันการปิดรายการ OP-001 และ OP-002'], ['สรุปการสำรอง Repository และเอกสารโครงการ', 'ยืนยันการสำรองและกู้คืนได้จริง'], ['ปิดโครงการอย่างเป็นทางการ', 'ลงมติปิดโครงการและส่งมอบเอกสาร']],
discussion: [['ผลการอบรม', 'อบรมผู้ใช้งาน 6 คน เมื่อ 22 ส.ค. 69 ผู้เข้าอบรมทดลองใช้งานครบทุกขั้นตอนได้สำเร็จ', ['ThS', 'PaNg'], 'เสร็จสิ้น'], ['การควบคุมปฏิบัติการ', 'ปิดรายการสำรองฐานข้อมูลอัตโนมัติ (OP-001) และการเฝ้าระวังพร้อมแจ้งเตือน (OP-002) เมื่อ 23 ส.ค. 69', ['ThS'], 'เสร็จสิ้น'], ['การสำรองข้อมูลโครงการ', 'ซิงก์ Branch `main` ไปยัง Backup Remote และทดสอบเรียกคืนสำเร็จ', ['ThS', 'YaB'], 'เสร็จสิ้น'], ['การปิดโครงการ', 'ที่ประชุมมีมติปิดโครงการอย่างเป็นทางการเมื่อ 24 ส.ค. 69', ['SeV', 'ApS'], 'ปิดโครงการ']],
actions: [['จัดเก็บเอกสารโครงการทั้งหมดเข้าระบบควบคุมเอกสาร', ['YaB'], '2026-08-24', 'เสร็จสมบูรณ์'], ['ติดตามการตรวจนับสต๊อกสิ้นเดือนแรกหลังเปิดใช้งานจริง', ['ApS'], '2026-09-30', 'Scheduled']],
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(', ')}`);
}
+60
View File
@@ -0,0 +1,60 @@
// Shared helpers for the ISO/IEC 29110 work-product seed.
// Every document follows the layout of the audited 200-TAS-25-001-00 package:
// title, document-control block, Thai section headings, tables, three signature tables.
export const TH_MONTHS = ['มกราคม','กุมภาพันธ์','มีนาคม','เมษายน','พฤษภาคม','มิถุนายน','กรกฎาคม','สิงหาคม','กันยายน','ตุลาคม','พฤศจิกายน','ธันวาคม'];
export const TH_SHORT = ['ม.ค.','ก.พ.','มี.ค.','เม.ย.','พ.ค.','มิ.ย.','ก.ค.','ส.ค.','ก.ย.','ต.ค.','พ.ย.','ธ.ค.'];
const parts = (iso) => iso.split('-').map(Number);
/** 2026-08-17 -> 25690817 (Buddhist compact, used in filenames and Release fields). */
export const bud = (iso) => { const [y, m, d] = parts(iso); return `${y + 543}${String(m).padStart(2, '0')}${String(d).padStart(2, '0')}`; };
/** 2026-08-17 -> 17 สิงหาคม 2569 */
export const thLong = (iso) => { const [y, m, d] = parts(iso); return `${d} ${TH_MONTHS[m - 1]} ${y + 543}`; };
/** 2026-08-17 -> 17 ส.ค. 69 */
export const thShort = (iso) => { const [y, m, d] = parts(iso); return `${d} ${TH_SHORT[m - 1]} ${String(y + 543).slice(2)}`; };
/** 2026-08-17 -> 17/08/2569 */
export const thNum = (iso) => { const [y, m, d] = parts(iso); return `${String(d).padStart(2, '0')}/${String(m).padStart(2, '0')}/${y + 543}`; };
/** Range in short Thai form: 10–14 ส.ค. 69 or 30 พ.ค.–31 ก.ค. 69 */
export const thRange = (a, b) => {
if (a === b) return thShort(a);
const [ya, ma, da] = parts(a); const [yb, mb, db] = parts(b);
if (ya === yb && ma === mb) return `${da}–${db} ${TH_SHORT[ma - 1]} ${String(ya + 543).slice(2)}`;
if (ya === yb) return `${da} ${TH_SHORT[ma - 1]}–${db} ${TH_SHORT[mb - 1]} ${String(ya + 543).slice(2)}`;
return `${thShort(a)}–${thShort(b)}`;
};
export const thRangeLong = (a, b) => `${thLong(a)} – ${thLong(b)}`;
export const daysBetween = (a, b) => Math.round((Date.parse(b) - Date.parse(a)) / 86400000) + 1;
const cell = (v) => String(v ?? '').replace(/\|/g, '\\|').replace(/\r?\n/g, ' ');
/** Markdown table. `align` is an optional array of 'l' | 'c' | 'r'. */
export function table(headers, rows, align) {
const sep = headers.map((_, i) => ({ c: ':---:', r: '---:' }[align?.[i]] ?? '---'));
const line = (r) => `| ${r.map(cell).join(' | ')} |`;
return [line(headers), line(sep), ...rows.map(line)].join('\n');
}
/** Document-control block. Each row is [label, value] or [label, value, label2, value2]. */
export function control(rows) {
return rows.map((r) => `| ${r.map(cell).join(' | ')} |`).join('\n');
}
/** A single signature table (ชื่อ / ตำแหน่ง / ลายเซ็น / วันที่). */
const sigTable = (people) => table(['ชื่อ', 'ตำแหน่ง', 'ลายเซ็น', 'วันที่'], people.map((p) => [p.th, p.role, '', '']));
/** The three signature blocks used by every work product. */
export function signatures(secretary, reviewer, approver) {
const list = (x) => (Array.isArray(x) ? x : [x]);
return [
'## ผู้จัดทำเอกสาร (Secretary)', sigTable(list(secretary)),
'## ผู้ตรวจสอบเอกสาร (Reviewer)', sigTable(list(reviewer)),
'## ผู้อนุมัติ (Approval)', sigTable(list(approver)),
].join('\n\n');
}
export const bullets = (items) => items.map((i) => `- ${i}`).join('\n');
export const numbered = (items) => items.map((i, n) => `${n + 1}. ${i}`).join('\n');
/** Join document parts with blank lines, dropping empty parts. */
export const doc = (...parts) => parts.flat().filter((p) => p !== undefined && p !== null && p !== '').join('\n\n') + '\n';
+177
View File
@@ -0,0 +1,177 @@
// Project facts shared by every work product. Edit here, then run `node scripts/sdlc-seed/build.mjs`.
export const CODE = '200-WMS-26-001-00';
export const PROJECT_TH = 'โครงการพัฒนาระบบบริหารจัดการคลังสินค้า (BRN WMS) บริษัท บี.อาร์.เอ็น เอ็นเตอร์ไพรส์ จำกัด';
export const COMPANY_TH = 'บริษัท บี.อาร์.เอ็น เอ็นเตอร์ไพรส์ จำกัด';
export const START = '2026-01-05';
export const END = '2026-08-24';
export const DEV_START = '2026-02-19';
export const DEV_BASELINE = '2026-05-29';
export const TEST_FROM = '2026-08-10';
export const TEST_TO = '2026-08-14';
export const ACCEPT_DATE = '2026-08-17';
export const TRAINING_DATE = '2026-08-22';
export const OPS_CLOSE_DATE = '2026-08-23';
export const RELEASE_DATE = '2026-08-17'; // date stamped on the consolidated V1.0 issue
export const BASELINE_COMMIT = '6c39700'; // delivered software baseline on main
export const TEST_STATE_COMMIT = 'dd48a8b'; // closest retained repository state at end of test window
export const TAG = 'sdlc-v1.0-final';
export const ORIGIN = 'git@188.166.228.62:nok/wms-app.git';
export const BACKUP = 'git@github.com:thanakorninbox-dev/wms-app.git';
export const P = {
SeV: { id: 'SeV', th: 'คุณเสรี วิริยะสกุลธรณ์', en: 'Seri Viriyasakultorn', role: 'Project Sponsor', pos: 'กรรมการผู้จัดการ (CEO)', email: 'seri.v@brn.co.th' },
ApS: { id: 'ApS', th: 'คุณอภิรัชช์ สุภัทรประทีป', en: 'Apirach Supattaratpateep', role: 'Project Manager', pos: 'ผู้จัดการโครงการ', email: 'apirach.s@brn.co.th' },
NoC: { id: 'NoC', th: 'คุณนพพงษ์ เจริญสุข', en: 'Noppong Chareunsook', role: 'System Analyst', pos: 'นักวิเคราะห์ระบบ', email: 'noppong.c@brn.co.th' },
ThS: { id: 'ThS', th: 'คุณธนกร สถิตวิทยากุล', en: 'Thanakorn Sathitwitayakul', role: 'Developer', pos: 'นักพัฒนาระบบ', email: 'thanakorn.s@brn.co.th' },
PaNg: { id: 'PaNg', th: 'คุณปริญ งามขำ', en: 'Parin Ngamkham', role: 'QA/Tester', pos: 'ผู้ทดสอบระบบ', email: 'parin.n@brn.co.th' },
YaB: { id: 'YaB', th: 'คุณเยาวลักษณ์ บางชมภู', en: 'Yaowalak Bangchomphoo', role: 'Document Control', pos: 'เจ้าหน้าที่ควบคุมเอกสาร', email: 'yaowalak.b@brn.co.th' },
};
export const TEAM = [P.SeV, P.ApS, P.NoC, P.ThS, P.PaNg, P.YaB];
export const ORGANIZER = `${P.ApS.th} (Project Manager) , ${P.NoC.th} (System Analyst)`;
export const ORGANIZER_PM = `${P.ApS.th} (Project Manager)`;
export const RECORDER = `${P.YaB.th} (Document Control)`;
/** Deliverables (Work Products) as listed in the plan, acceptance and configuration records. */
export const WPS = [
['WP 1.0', `เอกสาร ${CODE} Software Project Plan (พร้อม Work Schedule)`, 'ส่งเอกสารจำนวน 1 ชุด'],
['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`, 'ส่งเอกสารจำนวน 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 ชุด'],
['WP 9.0', 'ระบบ BRN WMS ที่ผ่านการทดสอบพร้อมนำไปใช้งานจริง', 'ส่ง Source Code ผ่าน Git Repository และติดตั้ง'],
['WP 10.0', `เอกสาร ${CODE} Product Operation Guide`, 'ส่งเอกสารจำนวน 1 ชุด'],
['WP 11.0', `เอกสาร ${CODE} Maintenance Document`, 'ส่งเอกสารจำนวน 1 ชุด'],
];
/** Systems / sub-systems in scope (SOW table in the plan and charter). */
export const SCOPE = [
['ระบบบริหารจัดการผู้ใช้งานและสิทธิ์', ['ลงทะเบียนเจ้าของบริษัทและเชิญผู้ใช้งาน (Onboarding)', 'เข้าสู่ระบบและควบคุมสิทธิ์ตามบทบาท Owner / Admin / Staff / Viewer', 'กู้คืนรหัสผ่าน, OTP และควบคุม Session', 'ตั้งค่าบริษัท, SMTP, ผู้ใช้งาน และสิทธิ์การเข้าถึงแอปพลิเคชัน']],
['ระบบข้อมูลหลัก (Master Data)', ['คลังสินค้า, พื้นที่จัดเก็บ, ช่องจัดเก็บ (Bin)', 'หมวดสินค้าและสินค้า', 'ประเภทผู้ติดต่อและผู้ติดต่อ (ลูกค้า/ผู้ขาย)', 'รองรับโครงสร้างคลังแบบชั้นเดียวและแบบหลายชั้น']],
['ระบบควบคุมสินค้าคงคลัง (Inventory Control)', ['รับสินค้าเข้า (Stock-in)', 'จ่ายสินค้าออก (Stock-out) พร้อมตรวจสอบยอดคงเหลือ', 'โอนย้ายสินค้าระหว่างคลัง/ตำแหน่ง', 'ติดตาม Lot, Serial Number และวันหมดอายุ', 'พิมพ์บาร์โค้ดสินค้า/ตำแหน่ง และรองรับการสแกน', 'แนบไฟล์ประกอบรายการ']],
['ระบบขาย (Sales)', ['ใบเสนอราคา (Quotation)', 'ใบสั่งขาย (Sales Order)', 'ใบแจ้งหนี้ (Invoice)', 'ใบรับคืน / ใบลดหนี้ (Return / Credit Note)']],
['ระบบจัดซื้อ (Purchasing)', ['ใบขอซื้อ (Purchase Request)', 'ใบสั่งซื้อ (Purchase Order)', 'ใบแจ้งหนี้ซื้อ (Purchase Invoice)', 'ใบคืนสินค้าผู้ขาย (Supplier Return)']],
['ระบบการเงิน (Finance)', ['ใบวางบิลรับ / ใบเสร็จรับเงิน', 'ใบวางบิลจ่าย / ใบสำคัญจ่าย']],
['ระบบบัญชี (Accounting)', ['ผังบัญชี, แผนก, สูตรบัญชี', 'สมุดรายวันและบัญชีแยกประเภท (Journal / GL)', 'รายงานงบทดลอง, งบกำไรขาดทุน, งบดุล, ภาษีมูลค่าเพิ่ม']],
['ระบบรายงานและแดชบอร์ด', ['แดชบอร์ดคลังสินค้าและบัญชี', 'รายงานสต๊อก, ความเคลื่อนไหว, ความจุคลัง, สินค้าใกล้หมด, สินค้าหมดอายุ, Lot', 'กรอง ดู พิมพ์ และส่งออกรายงาน']],
['ระบบควบคุมเอกสาร', ['ออกเลขที่เอกสารอัตโนมัติตามลำดับที่กำหนด', 'ควบคุมสถานะและวงจรชีวิตของเอกสาร', 'บันทึกผู้สร้าง/ผู้แก้ไข/ประวัติรายการ']],
['ระบบแจ้งเตือนและงานตามกำหนดเวลา', ['แจ้งเตือนแบบ Real-time ผ่าน Node.js / Socket.IO', 'งานสรุปยอดสต๊อก/GL และแจ้งเตือนสินค้าใกล้หมด, ใบแจ้งหนี้ค้างชำระ ตามกำหนดเวลา']],
['ระบบติดตั้งและตั้งค่า', ['ติดตั้งแบบ Manual ผ่าน setup.php', 'ติดตั้งแบบ Container ผ่าน Docker Compose (php-apache, mariadb, node/pm2)', 'สร้างค่าตั้งค่าและความลับของระบบจาก .env โดยไม่เก็บใน Source Control']],
];
/** Customer Requirements (CR01–CR14). Each: [id, text]. */
export const CR_GROUPS = [
['CR01', 'Feature & Functional Characteristics', [
['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', 'ระบบต้องตอบสนองงานประจำวัน (เปิดหน้าจอ, ค้นหา, สร้างเอกสาร) ภายในเวลาที่ใช้งานได้จริงบนสภาพแวดล้อมที่ตกลง'],
['CR02:002', 'ระบบต้องมีตารางสรุปยอด (Aggregate) เพื่อให้แดชบอร์ดและรายงานแสดงผลได้โดยไม่ต้องคำนวณใหม่ทุกครั้ง'],
['CR02:003', 'ระบบต้องรองรับปริมาณข้อมูลและผู้ใช้พร้อมกันในระดับที่ตกลงสำหรับสภาพแวดล้อมใช้งานจริง'],
]],
['CR03', 'Interface Considerations', [
['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'],
['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)'],
['CR05:002', 'เมนูและปุ่มคำสั่งต้องแสดงตามบทบาทและสิทธิ์การเข้าถึงของผู้ใช้'],
['CR05:003', 'ระบบต้องแสดงผลการตรวจสอบข้อมูล สถานะ ความสำเร็จ และข้อผิดพลาดอย่างชัดเจน'],
['CR05:004', 'ระบบต้องมีขั้นตอนยืนยันก่อนลบ ยกเลิก หรือทำรายการที่ย้อนกลับไม่ได้'],
['CR05:005', 'ระบบต้องพิมพ์เอกสารธุรกิจและฉลากบาร์โค้ดในรูปแบบที่ใช้งานได้'],
]],
['CR06', 'Security Considerations', [
['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'],
['CR07:002', 'ระบบต้องใช้เขตเวลา Asia/Bangkok อย่างสม่ำเสมอทั้งแอปพลิเคชันและงานตามกำหนดเวลา'],
['CR07:003', 'ระบบต้องใช้งานได้บนอุปกรณ์ Desktop และอุปกรณ์พกพาผ่าน Web Browser'],
['CR07:004', 'ระบบต้องแยกข้อมูลสาธิต/ทดสอบออกจากข้อมูลใช้งานจริงได้'],
]],
['CR08', 'Operational Considerations', [
['CR08:001', 'ระบบต้องมีการสำรองฐานข้อมูลอัตโนมัติรายวัน'],
['CR08:002', 'ระบบต้องกู้คืนข้อมูลจากชุดสำรองได้ตามขั้นตอนที่จัดทำเป็นเอกสาร'],
['CR08:003', 'ระบบต้องมีการเฝ้าระวังสถานะบริการ Web, Database และ Node.js พร้อมแจ้งเตือนเมื่อขัดข้อง'],
['CR08:004', 'ระบบต้องรองรับหลายบริษัท (Multi-company) และหลายคลังสินค้าภายใต้การแยกข้อมูล'],
['CR08:005', 'งานตามกำหนดเวลาต้องทำงานสำเร็จโดยไม่สร้างผลลัพธ์ซ้ำหรือเกินสิทธิ์'],
]],
['CR09', 'Maintenance Considerations', [
['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'],
['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)'],
['CR11:002', 'ต้องมีคู่มือปฏิบัติงานสำหรับผู้ดูแลระบบ (Product Operation Guide)'],
['CR11:003', 'ต้องมีการอบรมผู้ใช้งานก่อนเปิดใช้งานจริง'],
['CR11:004', 'ต้องมีช่องทางสนับสนุนและระดับการให้บริการ (SLA) หลังส่งมอบ'],
]],
['CR12', 'Design Constraints', [
['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 เดียวกัน'],
['CR13:002', 'ระบบต้องป้องกันยอดสต๊อกติดลบและความเคลื่อนไหวซ้ำซ้อน'],
['CR13:003', 'ระบบต้องรองรับการสำรองและกู้คืน (Backup & Recovery) ทั้ง Source Code และฐานข้อมูล'],
['CR13:004', 'ระบบต้องจัดการข้อผิดพลาดโดยแสดงข้อความที่เข้าใจได้แทน Fatal Error'],
]],
['CR14', 'Quality Expectations', [
['CR14:001', 'ระบบต้องผ่านการทดสอบตาม Test Case ที่กำหนดก่อนส่งมอบ'],
['CR14:002', 'ทุกความต้องการต้องสอบกลับได้ถึงการออกแบบ ส่วนประกอบ และหลักฐานการทดสอบ'],
['CR14:003', 'ระบบต้องผ่านการทดสอบการยอมรับ (UAT) โดยตัวแทนลูกค้าบนสภาพแวดล้อมใช้งานจริง'],
['CR14:004', 'ต้องไม่มีข้อบกพร่องระดับวิกฤตค้างอยู่ในด้านความปลอดภัย การแยกข้อมูล และความถูกต้องของสต๊อก/บัญชี'],
]],
];
export const CRS = CR_GROUPS.flatMap(([, , rows]) => rows);
export const crTopic = (id) => CRS.find((r) => r[0] === id)?.[1] ?? '';
+83
View File
@@ -0,0 +1,83 @@
// Hindsight guard: a work product may only cite what existed on its own date.
// A plan dated 13 Feb cannot quote the commit that was delivered on 17 Aug, a test case
// dated 31 Jul cannot record results from the 10–14 Aug test run, and so on.
// Planned dates in the future are fine; facts that only came into being later are not.
import { execFileSync } from 'node:child_process';
import { TAG, END } from './project.mjs';
import { CORRECTIONS } from './history.mjs';
/** Document date from its name: "... 25690817 V1.0 ..." -> 2026-08-17 */
export function docDate(name) {
const m = name.match(/ (25\d\d)(\d\d)(\d\d) V/);
if (!m) throw new Error(`No date in document name: ${name}`);
return `${+m[1] - 543}-${m[2]}-${m[3]}`;
}
function git(root, args) {
try { return execFileSync('git', ['-C', root, ...args], { stdio: ['ignore', 'pipe', 'ignore'] }).toString().trim(); } catch { return null; }
}
/**
* Identifier families and the work product that first defines them. An ID may be cited only
* on or after the date of that work product.
*/
const FAMILIES = [
{ label: 'Customer Requirement', re: /\bCR\d\d(?::\d{3})?\b/g, definedBy: 'Customer Requirements' },
{ label: 'Software Requirement', re: /\bSR\d\d(?::\d{3})?\b/g, definedBy: 'Software Requirements' },
{ label: 'Software Unit', re: /(?<!TC-)\bUN\d\d(?:\.\d{3})?\b/g, definedBy: 'Software Design' },
{ label: 'Test Case', re: /\bTC-UN\d\d\.\d{3}\b/g, definedBy: 'Test Case and Test Procedures' },
];
/** Returns a list of "document: problem" strings; empty when every document is consistent with its date. */
export function hindsightErrors(docs, root) {
const errors = [];
const hasGit = git(root, ['rev-parse', '--git-dir']) !== null;
const commitDates = new Map();
const commitDate = (h) => {
if (!commitDates.has(h)) commitDates.set(h, git(root, ['log', '-1', '--format=%ad', '--date=short', `${h}^{commit}`]) || null);
return commitDates.get(h);
};
const onMain = (h) => git(root, ['merge-base', '--is-ancestor', h, 'main']) !== null;
const tagDate =hasGit ? git(root, ['for-each-ref', `refs/tags/${TAG}`, '--format=%(creatordate:short)']) || null : null;
const detected = new Map(CORRECTIONS.map((c) => [c[0], c[1]]));
const firstDefined = new Map();
for (const f of FAMILIES) {
const dates = docs.filter((d) => d.name.includes(` ${f.definedBy} `)).map((d) => docDate(d.name)).sort();
if (!dates.length) throw new Error(`No work product defines ${f.label} IDs (${f.definedBy})`);
firstDefined.set(f, dates[0]);
}
for (const d of docs) {
const D = docDate(d.name);
const found = new Set();
const flag = (what) => found.add(what);
if (D > END) flag(`dated after the project end ${END}`);
if (hasGit) {
for (const [, h] of d.md.matchAll(/(?<![\w/.-])([0-9a-f]{7,40})(?![\w/.-])/g)) {
const c = commitDate(h);
if (!c) continue;
if (c > D) flag(`commit ${h} (created ${c})`);
if (c > END) flag(`commit ${h} is after the project end ${END}`);
if (!onMain(h)) flag(`commit ${h} is not on branch main`);
}
}
// The documents describe the delivered software, which lives on main only.
for (const [, b] of d.md.matchAll(/\bbranch\s+`?([\w./-]+)`?/gi)) {
if (b !== 'main' && /^[a-z]/.test(b)) flag(`branch ${b} (only main may be named)`);
}
if (d.md.includes(TAG) && !(tagDate && tagDate <= D)) flag(`tag ${TAG} (${tagDate ? `created ${tagDate}` : 'not created yet'})`);
for (const [id] of d.md.matchAll(/\bISS-\d{3}\b/g)) {
const at = detected.get(id);
if (!at) flag(`${id} is not in the Correction Register`);
else if (at > D) flag(`${id} (detected ${at})`);
}
for (const f of FAMILIES) {
const at = firstDefined.get(f);
if (at <= D) continue;
const ids = [...new Set([...d.md.matchAll(f.re)].map((m) => m[0]))];
if (ids.length) flag(`${f.label} IDs ${ids.slice(0, 3).join(', ')}${ids.length > 3 ? ', …' : ''} (defined ${at})`);
}
for (const what of found) errors.push(`${d.name}: ${what}`);
}
return errors;
}
+1 -1
View File
@@ -9,7 +9,7 @@ PACKAGE_DIR="$(realpath -m "${1:-$ROOT_DIR/sdlc-delivery}")"
[[ -d "$PACKAGE_DIR" ]] || { echo "Delivery package not found: $PACKAGE_DIR" >&2; exit 2; } [[ -d "$PACKAGE_DIR" ]] || { echo "Delivery package not found: $PACKAGE_DIR" >&2; exit 2; }
expected="$(find "$SOURCE_DIR" -type f -name '*.md' | wc -l | tr -d ' ')" expected="$(find "$SOURCE_DIR" -mindepth 2 -type f -name '*.md' | wc -l | tr -d ' ')"
produced="$(find "$PACKAGE_DIR" -type f -name '*.pdf' | wc -l | tr -d ' ')" produced="$(find "$PACKAGE_DIR" -type f -name '*.pdf' | wc -l | tr -d ' ')"
[[ "$expected" == "$produced" ]] || { echo "Expected $expected PDFs, found $produced." >&2; exit 1; } [[ "$expected" == "$produced" ]] || { echo "Expected $expected PDFs, found $produced." >&2; exit 1; }
BIN
View File
Binary file not shown.

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