<!DOCTYPE html>
<html lang="vi">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Phân Quyền Theo Chức Năng Công Việc — The Operator's Journal</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&family=Noto+Serif:ital,wght@0,400;0,600;0,700;1,400;1,600&family=Noto+Sans:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<style>
:root {
--ink: #0a0a0a;
--paper: #fafafa;
--muted: #6b7280;
--rule: #e5e7eb;
--accent: #c2410c;
--accent-soft: #fff7ed;
--code-bg: #0f172a;
}
html { scroll-behavior: smooth; }
body {
font-family: 'Noto Serif', 'Inter', Georgia, serif;
background: var(--paper);
color: var(--ink);
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
}
.font-display { font-family: 'Noto Serif', Georgia, serif; }
.font-sans-ui { font-family: 'Inter', 'Noto Sans', system-ui, sans-serif; }
.font-mono { font-family: 'JetBrains Mono', ui-monospace, monospace; }
.prose-article {
font-size: 1.125rem;
line-height: 1.75;
color: #1f2937;
max-width: 65ch;
margin-inline: auto;
}
.prose-article p { margin-bottom: 1.5rem; }
.prose-article p.lede {
font-size: 1.375rem;
line-height: 1.6;
color: #111827;
font-weight: 400;
}
.prose-article p.lede::first-letter {
font-family: 'Noto Serif', serif;
font-size: 4.5rem;
line-height: 0.85;
float: left;
margin: 0.5rem 0.75rem 0 0;
color: var(--accent);
font-weight: 700;
}
.prose-article h2 {
font-family: 'Noto Serif', serif;
font-size: 2rem;
font-weight: 700;
margin-top: 4rem;
margin-bottom: 1.25rem;
letter-spacing: -0.01em;
color: #0a0a0a;
}
.prose-article h3 {
font-family: 'Inter', sans-serif;
font-size: 0.8125rem;
text-transform: uppercase;
letter-spacing: 0.18em;
color: var(--accent);
font-weight: 600;
margin-top: 2.5rem;
margin-bottom: 0.75rem;
}
.prose-article a { color: var(--accent); text-decoration: underline; text-underline-offset: 3px; text-decoration-thickness: 1px; }
.prose-article strong { color: #0a0a0a; font-weight: 600; }
.prose-article em { font-style: italic; }
.prose-article ul, .prose-article ol { margin-bottom: 1.5rem; padding-left: 1.5rem; }
.prose-article ul li { list-style: disc; margin-bottom: 0.5rem; }
.prose-article ol li { list-style: decimal; margin-bottom: 0.5rem; }
.pull-quote {
font-family: 'Noto Serif', serif;
font-style: italic;
font-size: 1.875rem;
line-height: 1.4;
color: #0a0a0a;
border-left: 4px solid var(--accent);
padding: 0.5rem 0 0.5rem 1.5rem;
margin: 3rem 0;
font-weight: 500;
}
.pull-quote cite {
display: block;
font-style: normal;
font-family: 'Inter', sans-serif;
font-size: 0.8125rem;
text-transform: uppercase;
letter-spacing: 0.15em;
color: var(--muted);
margin-top: 1rem;
font-weight: 500;
}
figure { margin: 3rem 0; }
figcaption {
font-family: 'Inter', sans-serif;
font-style: italic;
font-size: 0.875rem;
color: var(--muted);
margin-top: 0.75rem;
text-align: center;
line-height: 1.5;
}
.code-block {
background: var(--code-bg);
border-radius: 12px;
overflow: hidden;
margin: 2rem 0;
box-shadow: 0 4px 12px rgba(0,0,0,0.08);
}
.code-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.625rem 1.25rem;
background: #1e293b;
color: #94a3b8;
font-family: 'Inter', sans-serif;
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.1em;
border-bottom: 1px solid #334155;
}
.code-header .lang-tag {
background: var(--accent);
color: white;
padding: 0.125rem 0.5rem;
border-radius: 4px;
font-weight: 600;
font-size: 0.6875rem;
letter-spacing: 0.08em;
}
.code-body {
padding: 1.25rem;
font-family: 'JetBrains Mono', monospace;
font-size: 0.9rem;
line-height: 1.7;
color: #e2e8f0;
overflow-x: auto;
}
.code-body .tok-key { color: #fbbf24; }
.code-body .tok-str { color: #86efac; }
.code-body .tok-num { color: #f472b6; }
.code-body .tok-com { color: #64748b; font-style: italic; }
.code-body .tok-op { color: #f97316; }
.inline-code {
font-family: 'JetBrains Mono', monospace;
background: #fef3c7;
color: #78350f;
padding: 0.125rem 0.4rem;
border-radius: 4px;
font-size: 0.9em;
}
.rule-thin { border-color: var(--rule); }
.deco-rule {
height: 1px;
background: linear-gradient(to right, transparent, var(--rule) 20%, var(--rule) 80%, transparent);
margin: 2.5rem auto;
max-width: 65ch;
}
.small-caps {
font-family: 'Inter', sans-serif;
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.2em;
font-weight: 600;
}
.fade-in {
animation: fadeIn 0.6s ease-out;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(8px); }
to { opacity: 1; transform: translateY(0); }
}
/* Field anatomy figure */
.anatomy-token {
display: inline-flex;
flex-direction: column;
align-items: center;
gap: 0.5rem;
position: relative;
}
.anatomy-token .label-top {
font-family: 'Inter', sans-serif;
font-size: 0.65rem;
text-transform: uppercase;
letter-spacing: 0.15em;
color: var(--accent);
font-weight: 600;
}
.anatomy-token .glyph {
font-family: 'JetBrains Mono', monospace;
font-size: 1.5rem;
font-weight: 600;
color: #0a0a0a;
background: #fff;
padding: 0.5rem 0.75rem;
border: 1px solid var(--rule);
border-radius: 8px;
box-shadow: 0 1px 3px rgba(0,0,0,0.04);
}
.anatomy-token .label-bot {
font-family: 'Inter', sans-serif;
font-size: 0.75rem;
color: var(--muted);
text-align: center;
line-height: 1.3;
max-width: 80px;
}
.anatomy-sep {
font-family: 'JetBrains Mono', monospace;
font-size: 1.5rem;
color: #cbd5e1;
align-self: center;
padding-top: 0.5rem;
}
/* Status pill */
.status-pill {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.375rem 0.75rem;
border-radius: 999px;
font-family: 'Inter', sans-serif;
font-size: 0.8125rem;
font-weight: 500;
}
/* Flow diagram */
.flow-step {
background: white;
border: 1px solid var(--rule);
border-radius: 12px;
padding: 1rem 1.25rem;
position: relative;
}
.flow-step .step-num {
position: absolute;
top: -0.75rem;
left: 1.25rem;
background: var(--accent);
color: white;
width: 1.5rem;
height: 1.5rem;
border-radius: 999px;
display: flex;
align-items: center;
justify-content: center;
font-family: 'Inter', sans-serif;
font-size: 0.75rem;
font-weight: 700;
}
/* Table */
.field-table {
width: 100%;
border-collapse: collapse;
font-family: 'Inter', sans-serif;
}
.field-table th {
text-align: left;
font-size: 0.7rem;
text-transform: uppercase;
letter-spacing: 0.15em;
color: var(--muted);
font-weight: 600;
padding: 0.75rem 1rem;
border-bottom: 1.5px solid #0a0a0a;
}
.field-table td {
padding: 0.875rem 1rem;
border-bottom: 1px solid var(--rule);
font-size: 0.9375rem;
vertical-align: top;
}
.field-table tr:hover { background: #fafafa; }
.field-table .col-field { font-family: 'JetBrains Mono', monospace; font-weight: 600; color: var(--accent); }
.field-table .col-rule { color: var(--muted); }
/* Card */
.related-card {
background: white;
border: 1px solid var(--rule);
border-radius: 16px;
padding: 1.5rem;
transition: all 0.2s ease;
cursor: pointer;
}
.related-card:hover {
border-color: #0a0a0a;
transform: translateY(-2px);
box-shadow: 0 8px 24px rgba(0,0,0,0.06);
}
/* Error stripe */
.error-stripe {
background: repeating-linear-gradient(
45deg,
#fef2f2,
#fef2f2 10px,
#fee2e2 10px,
#fee2e2 20px
);
}
/* Margin note */
.margin-note {
font-family: 'Inter', sans-serif;
font-size: 0.8125rem;
line-height: 1.5;
color: var(--muted);
padding-left: 1rem;
border-left: 2px solid var(--rule);
}
.focus\:ring-accent:focus {
outline: 2px solid var(--accent);
outline-offset: 2px;
}
</style>
</head>
<body class="bg-[#fafafa]">
<!-- ============= MASTHEAD ============= -->
<header class="border-b-2 border-[#0a0a0a]">
<div class="max-w-6xl mx-auto px-6 py-4 flex items-center justify-between">
<div class="flex items-center gap-4">
<svg width="36" height="36" viewBox="0 0 36 36" fill="none">
<rect x="2" y="2" width="32" height="32" rx="6" fill="#0a0a0a"/>
<path d="M11 18 L17 24 L25 12" stroke="#fafafa" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<div>
<div class="font-display font-bold text-xl tracking-tight leading-none">The Operator's Journal</div>
<div class="font-sans-ui text-xs text-gray-500 mt-0.5 tracking-wide">Product specs, ops playbooks & internal tooling notes</div>
</div>
</div>
<nav class="hidden md:flex items-center gap-6 font-sans-ui text-sm text-gray-700">
<a href="#" class="hover:text-[#c2410c]">Specs</a>
<a href="#" class="hover:text-[#c2410c]">Playbooks</a>
<a href="#" class="hover:text-[#c2410c]">Postmortems</a>
<a href="#" class="hover:text-[#c2410c]">Archive</a>
</nav>
<div class="font-sans-ui text-xs text-gray-500 tracking-wider uppercase">Vol. 04 · Issue 18</div>
</div>
</header>
<!-- ============= HERO ============= -->
<section class="border-b border-gray-200 fade-in">
<div class="max-w-4xl mx-auto px-6 py-20 md:py-28">
<div class="flex items-center gap-3 mb-8">
<span class="small-caps text-[#c2410c]">Product Spec</span>
<span class="text-gray-300">·</span>
<span class="small-caps text-gray-500">Dashboard quản trị workspace</span>
<span class="text-gray-300">·</span>
<span class="small-caps text-gray-500">Phân quyền · Preset vai trò</span>
</div>
<h1 class="font-display font-bold text-5xl md:text-7xl leading-[1.05] tracking-tight text-[#0a0a0a]">
Phân Quyền Theo Chức Năng Công Việc: Map Vai Trò → Bộ Quyền Có Sẵn
</h1>
<p class="font-display text-xl md:text-2xl text-gray-600 mt-8 italic leading-relaxed max-w-3xl">
Đặc tả lớp "preset vai trò công việc" (Kế toán / Hỗ trợ / Vận hành…) chồng lên màn phân quyền 55 chức năng đã có — để super admin cấp quyền trong một thao tác, vẫn chỉnh tay được từng ô trước khi Xác nhận.
</p>
<div class="mt-12 flex flex-wrap items-center gap-x-8 gap-y-4 pt-6 border-t border-gray-200">
<div class="flex items-center gap-3">
<div class="w-11 h-11 rounded-full bg-[#0a0a0a] text-[#fafafa] flex items-center justify-center font-display font-bold text-lg">D</div>
<div>
<div class="font-sans-ui font-semibold text-sm text-[#0a0a0a]">Doanh Nguyễn</div>
<div class="font-sans-ui text-xs text-gray-500">Editor, The Operator's Journal</div>
</div>
</div>
<div class="font-sans-ui text-sm text-gray-500">
<span class="small-caps text-gray-400">Published</span>
<span class="ml-2">04 Jun 2026</span>
</div>
<div class="font-sans-ui text-sm text-gray-500">
<span class="small-caps text-gray-400">Read time</span>
<span class="ml-2">~ 16 min</span>
</div>
<div class="font-sans-ui text-sm text-gray-500">
<span class="small-caps text-gray-400">Priority</span>
<span class="ml-2">P1 — High</span>
</div>
</div>
</div>
</section>
<!-- ============= TABLE OF CONTENTS ============= -->
<section class="border-b border-gray-200 bg-white">
<div class="max-w-4xl mx-auto px-6 py-10">
<div class="small-caps text-gray-400 mb-4">In this spec</div>
<ol class="grid md:grid-cols-2 gap-x-12 gap-y-2 font-sans-ui text-sm">
<li class="flex gap-3"><span class="text-gray-400 font-mono">01</span><a href="#info" class="hover:text-[#c2410c] text-gray-800">Thông tin chung</a></li>
<li class="flex gap-3"><span class="text-gray-400 font-mono">02</span><a href="#context" class="hover:text-[#c2410c] text-gray-800">Bối cảnh</a></li>
<li class="flex gap-3"><span class="text-gray-400 font-mono">03</span><a href="#objectives" class="hover:text-[#c2410c] text-gray-800">Mục tiêu & Chỉ số</a></li>
<li class="flex gap-3"><span class="text-gray-400 font-mono">04</span><a href="#personas" class="hover:text-[#c2410c] text-gray-800">Personas</a></li>
<li class="flex gap-3"><span class="text-gray-400 font-mono">05</span><a href="#flow" class="hover:text-[#c2410c] text-gray-800">Luồng nghiệp vụ cốt lõi</a></li>
<li class="flex gap-3"><span class="text-gray-400 font-mono">06</span><a href="#edgecases" class="hover:text-[#c2410c] text-gray-800">Edge cases</a></li>
<li class="flex gap-3"><span class="text-gray-400 font-mono">07</span><a href="#stories" class="hover:text-[#c2410c] text-gray-800">User stories</a></li>
<li class="flex gap-3"><span class="text-gray-400 font-mono">08</span><a href="#rules" class="hover:text-[#c2410c] text-gray-800">Business rules</a></li>
<li class="flex gap-3"><span class="text-gray-400 font-mono">09</span><a href="#acceptance" class="hover:text-[#c2410c] text-gray-800">Acceptance & Test cases</a></li>
<li class="flex gap-3"><span class="text-gray-400 font-mono">10</span><a href="#handover" class="hover:text-[#c2410c] text-gray-800">Quy trình bàn giao</a></li>
</ol>
</div>
</section>
<!-- ============= ARTICLE BODY ============= -->
<article class="py-20">
<div class="prose-article">
<!-- INTRO -->
<p class="lede">
Cấp quyền cho một thành viên trong workspace hiện đang là một thao tác <em>thủ công, tốn thời gian, và dễ sai</em>: super admin phải tick tay đúng các ô trong danh mục 55 chức năng, mỗi lần một thành viên mới. Bài này đặc tả một lớp mỏng đặt lên trên — <strong>preset "vai trò công việc"</strong> — cho phép cấp quyền theo Kế toán / Hỗ trợ / Vận hành chỉ trong một thao tác, mà <em>không</em> phải xây lại RBAC.
</p>
<p>
Tài liệu được tổ chức theo trình tự PRD chuẩn: thông tin chung trước, sau đó là bối cảnh và mục tiêu, rồi mới đi vào personas, luồng nghiệp vụ, business rules, và cuối cùng là acceptance criteria + quy trình bàn giao. Phần 6 (Bàn giao) liệt kê những điểm CTO cần chốt <strong>trước khi code</strong> — đọc kỹ phần đó nếu bạn là dev được giao ticket.
</p>
<!-- ============= SECTION 1: INFO ============= -->
<h2 id="info">01 · Thông tin chung</h2>
<h3>Bốn trường meta cần đọc đầu tiên</h3>
<p>
Trước khi đọc bất kỳ phần nào khác, hãy đối chiếu bốn ô dưới đây để chắc rằng bạn đang đọc đúng phiên bản tài liệu và đúng feature. <strong>Feature ID</strong> là khoá nối tới ticket, <strong>Status</strong> cho biết tài liệu đã sẵn sàng để code hay chưa.
</p>
</div>
<!-- FIGURE: Info grid -->
<figure class="max-w-3xl mx-auto px-6">
<div class="border border-gray-200 rounded-2xl bg-white p-8 shadow-sm">
<div class="grid md:grid-cols-2 gap-6">
<div class="flex gap-4">
<div class="flex-shrink-0 w-9 h-9 rounded-lg bg-orange-50 text-[#c2410c] flex items-center justify-center font-mono font-bold">ID</div>
<div>
<div class="font-sans-ui font-semibold text-[#0a0a0a] mb-1">Feature ID</div>
<div class="font-sans-ui text-sm text-gray-600 leading-relaxed"><span class="inline-code">FT-PERM-ROLE-PRESET-01</span> — khoá nối tới ticket & branch.</div>
</div>
</div>
<div class="flex gap-4">
<div class="flex-shrink-0 w-9 h-9 rounded-lg bg-orange-50 text-[#c2410c] flex items-center justify-center font-mono font-bold">PO</div>
<div>
<div class="font-sans-ui font-semibold text-[#0a0a0a] mb-1">Owner / PO</div>
<div class="font-sans-ui text-sm text-gray-600 leading-relaxed"><strong>CTO</strong> — cũng là người Approve cuối cùng trước khi deploy Production.</div>
</div>
</div>
<div class="flex gap-4">
<div class="flex-shrink-0 w-9 h-9 rounded-lg bg-orange-50 text-[#c2410c] flex items-center justify-center font-mono font-bold">P1</div>
<div>
<div class="font-sans-ui font-semibold text-[#0a0a0a] mb-1">Priority</div>
<div class="font-sans-ui text-sm text-gray-600 leading-relaxed"><strong>P1 — High</strong>: cấp quyền tay 55 ô đang mất công, dễ sót/nhầm, hai người cùng vai trò dễ lệch.</div>
</div>
</div>
<div class="flex gap-4">
<div class="flex-shrink-0 w-9 h-9 rounded-lg bg-orange-50 text-[#c2410c] flex items-center justify-center font-mono font-bold">DR</div>
<div>
<div class="font-sans-ui font-semibold text-[#0a0a0a] mb-1">Status</div>
<div class="font-sans-ui text-sm text-gray-600 leading-relaxed"><strong>Draft</strong> — tạo 04/06/2026, cập nhật cuối 04/06/2026.</div>
</div>
</div>
</div>
</div>
<figcaption>Hình 1.1 — Bốn trường meta của feature. ID / PO / Priority / Status — đối chiếu trước khi đọc tiếp.</figcaption>
</figure>
<div class="prose-article">
<p class="margin-note">
<strong>Hệ thống:</strong> Dashboard quản trị workspace — màn <em>phân quyền theo chức năng</em> đã có sẵn (super admin tick trong 55 chức năng, chia nhóm Hệ thống / Audit & Log / Risk…, có "Chọn tất cả" / "Thu hồi tất cả", áp dụng sau khi bấm <strong>Xác nhận</strong>). Module <em>Audit & Log</em> có sẵn — sẽ tái dùng cho việc ghi nhật ký cấp quyền.
</p>
<p class="margin-note">
<strong>Quy mô:</strong> Bổ sung <strong>lớp "chức năng công việc" (preset vai trò)</strong> map sẵn một bộ chức năng cho từng vai trò. Khi cấp quyền, super admin <strong>chọn vai trò → bộ chức năng được tick sẵn</strong>, vẫn <strong>chỉnh tay từng ô</strong> trước khi Xác nhận. <strong>KHÔNG</strong> xây lại RBAC/role lồng nhau; <strong>không</strong> thay thế màn phân quyền hiện có — chỉ thêm tầng "chọn nhanh theo vai trò" lên trên nó.
</p>
<!-- ============= SECTION 2: CONTEXT ============= -->
<h2 id="context">02 · Bối cảnh</h2>
<h3>Vì sao cần lớp preset — và đâu là nguyên nhân gốc</h3>
<p>
Hiện tại, để cấp quyền cho một thành viên, super admin phải <strong>tick thủ công từng chức năng</strong> trong danh mục <strong>55 chức năng</strong> (chia nhóm Hệ thống, Audit & Log, Risk…), rồi bấm <strong>Xác nhận</strong> để áp. Lớp phân quyền chỉ phơi ra <strong>mức chức năng đơn lẻ</strong> — không có khái niệm <strong>vai trò công việc</strong>.
</p>
<p>
Hệ quả khi ánh xạ "vai trò → các ô cần tick" nằm hoàn toàn trong đầu super admin:
</p>
<ul>
<li><strong>Mất công & chậm:</strong> mỗi thành viên là một lần dò tick thủ công, càng nhiều thành viên càng tốn thời gian.</li>
<li><strong>Dễ sai:</strong> dễ <em>sót</em> chức năng cần cấp hoặc <em>tick nhầm</em> chức năng thừa — thành viên thiếu quyền không làm được việc, hoặc dư quyền gây rủi ro.</li>
<li><strong>Không nhất quán:</strong> hai kế toán do cấp ở hai thời điểm / hai người khác nhau có thể nhận <strong>bộ quyền lệch nhau</strong>, không có chuẩn chung.</li>
<li><strong>Phụ thuộc trí nhớ admin:</strong> ánh xạ "vai trò → các ô cần tick" <strong>chưa được chuẩn hóa</strong> ở đâu trong hệ thống.</li>
</ul>
<p>
Tính năng cần bổ sung: định nghĩa sẵn các vai trò (Kế toán, Hỗ trợ, Vận hành…), mỗi vai trò <strong>map tới một bộ chức năng</strong> trong 55 chức năng. Khi cấp quyền, super admin chỉ cần <strong>chọn vai trò → bộ chức năng tương ứng được tick sẵn</strong>, sau đó <strong>chỉnh tay</strong> thêm/bớt nếu cần rồi Xác nhận. Việc cấu hình "vai trò nào gồm những chức năng nào" do Admin/CTO thiết lập một lần, dùng lại cho mọi thành viên.
</p>
<div class="code-block">
<div class="code-header">
<span>role-preset.shape</span>
<span class="lang-tag">SCHEMA</span>
</div>
<div class="code-body">
<span class="tok-com">// Một preset = ánh xạ vai trò → bộ chức năng (subset của 55)</span>
<span class="tok-key">RolePreset</span> <span class="tok-op">=</span> {
<span class="tok-key">name</span><span class="tok-op">:</span> <span class="tok-str">"Kế toán" | "Hỗ trợ" | "Vận hành" | ...</span><span class="tok-op">,</span>
<span class="tok-key">functions</span><span class="tok-op">:</span> <span class="tok-key">Set</span><span class="tok-op"><</span><span class="tok-str">FunctionId</span><span class="tok-op">></span><span class="tok-op">,</span> <span class="tok-com">// subset của 55 chức năng</span>
<span class="tok-key">defined_by</span><span class="tok-op">:</span> <span class="tok-str">"Admin/CTO"</span><span class="tok-op">,</span> <span class="tok-com">// một lần, dùng lại</span>
<span class="tok-key">applied_as</span><span class="tok-op">:</span> <span class="tok-str">"snapshot"</span> <span class="tok-com">// KHÔNG hồi tố sau khi cấp</span>
}
</div>
</div>
<div class="pull-quote">
Nguyên nhân gốc không phải là "admin lười tick" — mà là hệ thống chưa có tầng vai trò nào để gom đúng bộ chức năng theo nghiệp vụ.
<cite>— Vì sao đây là vấn đề thiết kế, không phải vấn đề quy trình</cite>
</div>
<!-- ============= SECTION 3: OBJECTIVES ============= -->
<h2 id="objectives">03 · Mục tiêu & Chỉ số</h2>
<h3>Bốn chỉ số đo lường thành công</h3>
<p>
<strong>Phạm vi & cách làm — chốt rõ để dev không làm quá tay:</strong> <strong>KHÔNG</strong> xây hệ thống RBAC phức tạp (role lồng nhau, kế thừa role, toán tử điều kiện). Đây chỉ là <strong>preset "vai trò → bộ chức năng"</strong> để <strong>điền nhanh</strong> trên màn phân quyền 55 chức năng <em>đã có</em>. Super admin <strong>vẫn tick được từng ô</strong> như hiện tại — chọn vai trò chỉ là bước <em>gợi ý/điền sẵn</em>, không khóa cứng. Giữ tính năng <strong>đơn giản, đúng nhu cầu</strong>.
</p>
<p>
Sau khi làm xong: super admin <strong>cấp quyền theo vai trò</strong> (một thao tác áp đúng bộ chức năng) thay vì tick tay 55 ô; các thành viên <strong>cùng vai trò có bộ quyền nền nhất quán</strong>; cấp nhanh hơn, ít sót/nhầm. Release đầu hỗ trợ một vài vai trò khởi đầu (Kế toán, Hỗ trợ, Vận hành…); Admin/CTO cấu hình map vai trò → chức năng; super admin chọn vai trò khi cấp và <em>vẫn tinh chỉnh tay</em> trước khi Xác nhận.
</p>
</div>
<figure class="max-w-3xl mx-auto px-6">
<div class="grid md:grid-cols-2 gap-4">
<div class="bg-[#fff7ed] border border-orange-200 rounded-xl p-5">
<div class="small-caps text-[#c2410c] mb-2">Metric · 01</div>
<div class="font-sans-ui font-semibold text-[#0a0a0a]">Nhanh hơn</div>
<div class="font-sans-ui text-sm text-gray-600 mt-2 leading-relaxed">Giảm thời gian/số thao tác để cấp quyền cho một thành viên mới — từ dò-tick 55 ô xuống chọn vai trò + chỉnh vài ô.</div>
</div>
<div class="bg-[#fff7ed] border border-orange-200 rounded-xl p-5">
<div class="small-caps text-[#c2410c] mb-2">Metric · 02</div>
<div class="font-sans-ui font-semibold text-[#0a0a0a]">Nhất quán</div>
<div class="font-sans-ui text-sm text-gray-600 mt-2 leading-relaxed">Các thành viên cùng vai trò có <em>bộ quyền nền giống nhau</em> — không còn lệch tùy người/thời điểm cấp.</div>
</div>
<div class="bg-[#fff7ed] border border-orange-200 rounded-xl p-5">
<div class="small-caps text-[#c2410c] mb-2">Metric · 03</div>
<div class="font-sans-ui font-semibold text-[#0a0a0a]">Ít lỗi</div>
<div class="font-sans-ui text-sm text-gray-600 mt-2 leading-relaxed">Giảm trường hợp thiếu/thừa quyền do tick tay sót/nhầm.</div>
</div>
<div class="bg-[#fff7ed] border border-orange-200 rounded-xl p-5">
<div class="small-caps text-[#c2410c] mb-2">Metric · 04</div>
<div class="font-sans-ui font-semibold text-[#0a0a0a]">Chuẩn hóa</div>
<div class="font-sans-ui text-sm text-gray-600 mt-2 leading-relaxed">Ánh xạ "vai trò → quyền" được lưu <em>trong hệ thống</em> thay vì nằm trong trí nhớ admin.</div>
</div>
</div>
<figcaption>Hình 3.1 — Bốn chỉ số đo lường thành công. Không có chỉ số nào cần feature flag hay A/B — đây là cải thiện thuần workflow.</figcaption>
</figure>
<div class="prose-article">
<!-- ============= SECTION 4: PERSONAS ============= -->
<h2 id="personas">04 · Personas</h2>
<h3>Ai làm gì với feature này</h3>
<p>
Feature có ba persona <em>active</em> + một persona <em>passive</em>. Lưu ý kỹ phần phân quyền thao tác: chỉ <strong>Admin/CTO</strong> được cấu hình preset, chỉ <strong>super admin</strong> được cấp quyền cho thành viên — đây là ràng buộc bảo mật, không phải tuỳ chọn UX.
</p>
</div>
<!-- FIGURE: Personas -->
<figure class="max-w-4xl mx-auto px-6">
<div class="grid md:grid-cols-3 gap-4">
<div class="bg-white border border-gray-200 rounded-2xl p-6">
<div class="font-display font-bold text-2xl text-[#c2410c] leading-tight">Super Admin</div>
<div class="font-sans-ui font-semibold text-sm text-[#0a0a0a] mt-3">Người cấp/thu hồi quyền</div>
<div class="font-sans-ui text-sm text-gray-600 mt-2 leading-relaxed">Nay <strong>chọn được vai trò công việc</strong> để áp sẵn bộ chức năng lên màn phân quyền, rồi tinh chỉnh tay và Xác nhận.</div>
</div>
<div class="bg-white border border-gray-200 rounded-2xl p-6">
<div class="font-display font-bold text-2xl text-[#c2410c] leading-tight">Admin / CTO</div>
<div class="font-sans-ui font-semibold text-sm text-[#0a0a0a] mt-3">Người cấu hình preset</div>
<div class="font-sans-ui text-sm text-gray-600 mt-2 leading-relaxed">Cấu hình <strong>danh sách vai trò</strong> và <strong>bộ chức năng mỗi vai trò</strong> map tới. Thiết lập <em>một lần, dùng lại</em>.</div>
</div>
<div class="bg-white border border-gray-200 rounded-2xl p-6">
<div class="font-display font-bold text-2xl text-[#c2410c] leading-tight">Member</div>
<div class="font-sans-ui font-semibold text-sm text-[#0a0a0a] mt-3">Người nhận quyền (thụ động)</div>
<div class="font-sans-ui text-sm text-gray-600 mt-2 leading-relaxed">Hưởng lợi gián tiếp: được cấp <strong>đúng & nhất quán</strong> theo vai trò, nhanh hơn. Không truy cập cấu hình.</div>
</div>
</div>
<figcaption>Hình 4.1 — Ba persona active. Persona thứ tư — <em>lớp phân quyền 55 chức năng đã có</em> — được tái sử dụng nguyên trạng; preset chỉ là tầng điền nhanh phía trên.</figcaption>
</figure>
<div class="prose-article">
<!-- ============= SECTION 5: FLOW ============= -->
<h2 id="flow">05 · Luồng nghiệp vụ cốt lõi</h2>
<h3>Sáu bước, theo thứ tự</h3>
<p>
Luồng nghiệp vụ chia hai pha: Admin/CTO <em>cấu hình một lần</em> (Bước 1), sau đó super admin <em>áp & tinh chỉnh từng lần cấp</em> (Bước 2–6). Lưu ý Bước 4 — <em>tinh chỉnh tay sau khi áp</em> — chính là chỗ super admin quyết định <strong>tập cuối cùng</strong>, không phải bộ gốc của vai trò.
</p>
</div>
<!-- FIGURE: Flow -->
<figure class="max-w-5xl mx-auto px-6">
<div class="grid md:grid-cols-6 gap-4">
<div class="flow-step">
<div class="step-num">1</div>
<div class="mt-2 font-sans-ui font-semibold text-sm text-[#0a0a0a]">Admin cấu hình preset</div>
<div class="font-sans-ui text-xs text-gray-500 mt-1.5 leading-relaxed">Tạo vai trò + chọn chức năng — một lần.</div>
</div>
<div class="flow-step">
<div class="step-num">2</div>
<div class="mt-2 font-sans-ui font-semibold text-sm text-[#0a0a0a]">Mở màn phân quyền</div>
<div class="font-sans-ui text-xs text-gray-500 mt-1.5 leading-relaxed">Super admin mở màn của một thành viên.</div>
</div>
<div class="flow-step">
<div class="step-num">3</div>
<div class="mt-2 font-sans-ui font-semibold text-sm text-[#0a0a0a]">Chọn vai trò</div>
<div class="font-sans-ui text-xs text-gray-500 mt-1.5 leading-relaxed">Hệ thống tick sẵn các chức năng của vai trò.</div>
</div>
<div class="flow-step">
<div class="step-num">4</div>
<div class="mt-2 font-sans-ui font-semibold text-sm text-[#0a0a0a]">Tinh chỉnh tay</div>
<div class="font-sans-ui text-xs text-gray-500 mt-1.5 leading-relaxed">Thêm/bớt từng ô; có thể chọn nhiều vai trò.</div>
</div>
<div class="flow-step">
<div class="step-num">5</div>
<div class="mt-2 font-sans-ui font-semibold text-sm text-[#0a0a0a]">Bấm Xác nhận</div>
<div class="font-sans-ui text-xs text-gray-500 mt-1.5 leading-relaxed">Áp đúng tập chức năng cuối cùng — qua cơ chế cũ.</div>
</div>
<div class="flow-step">
<div class="step-num">6</div>
<div class="mt-2 font-sans-ui font-semibold text-sm text-[#0a0a0a]">Ghi log</div>
<div class="font-sans-ui text-xs text-gray-500 mt-1.5 leading-relaxed">Hành động + vai trò đã áp → module Audit & Log.</div>
</div>
</div>
<figcaption>Hình 5.1 — Sáu bước. Bước 1 chạy một lần (Admin/CTO); Bước 2–6 chạy mỗi lần cấp (super admin).</figcaption>
</figure>
<div class="prose-article">
<p>
Người kiêm nhiệm — vd <em>vừa Kế toán vừa Hỗ trợ</em> — có thể được áp <strong>nhiều vai trò</strong> ở Bước 3; các bộ quyền sẽ hợp nhất (union), ô trùng không nhân đôi. Chi tiết quy tắc hợp nhất ở Mục 08.
</p>
<!-- ============= SECTION 6: EDGE CASES ============= -->
<h2 id="edgecases">06 · Edge cases</h2>
<h3>Năm tình huống dev bắt buộc xử lý</h3>
<p>
Bên cạnh luồng chính ở Mục 05, đây là năm tình huống biên mà <em>cách xử lý phải được code rõ ràng</em>, không để mặc định. Dev tự tư duy & bổ sung thêm nếu cần.
</p>
</div>
<!-- FIGURE: Edge cases -->
<figure class="max-w-4xl mx-auto px-6">
<div class="space-y-3">
<div class="bg-white border border-gray-200 rounded-xl p-5 flex items-start gap-4">
<span class="status-pill bg-amber-50 text-amber-700 flex-shrink-0"><span class="w-2 h-2 rounded-full bg-amber-500"></span>Quan trọng</span>
<div class="flex-1">
<div class="font-mono text-sm text-[#0a0a0a] font-semibold">Áp vai trò lên lựa chọn đang có — cộng dồn hay thay thế?</div>
<div class="font-sans-ui text-sm text-gray-500 mt-1">Đề xuất <strong>cộng dồn (union)</strong>: tick thêm chức năng của vai trò, không tự bỏ ô admin đã chọn. Hiển thị rõ thay đổi. <em>Chốt ở Bước 0.</em></div>
</div>
</div>
<div class="bg-white border border-gray-200 rounded-xl p-5 flex items-start gap-4">
<span class="status-pill bg-blue-50 text-blue-700 flex-shrink-0"><span class="w-2 h-2 rounded-full bg-blue-500"></span>Kiêm nhiệm</span>
<div class="flex-1">
<div class="font-mono text-sm text-[#0a0a0a] font-semibold">Nhiều vai trò cho một thành viên</div>
<div class="font-sans-ui text-sm text-gray-500 mt-1">Vd vừa Kế toán vừa Hỗ trợ → <strong>hợp nhất (union)</strong> hai bộ chức năng; ô trùng <em>không nhân đôi</em>, không xung đột.</div>
</div>
</div>
<div class="bg-white border border-gray-200 rounded-xl p-5 flex items-start gap-4">
<span class="status-pill bg-gray-100 text-gray-700 flex-shrink-0"><span class="w-2 h-2 rounded-full bg-gray-500"></span>Khả dụng</span>
<div class="flex-1">
<div class="font-mono text-sm text-[#0a0a0a] font-semibold">Chức năng không khả dụng cho workspace/gói</div>
<div class="font-sans-ui text-sm text-gray-500 mt-1">Cần gói trả phí (biểu tượng vương miện) hoặc chưa bật → áp vai trò <strong>bỏ qua an toàn</strong> phần không khả dụng, nêu rõ, không crash.</div>
</div>
</div>
<div class="rounded-xl p-5 flex items-start gap-4 error-stripe border border-red-200">
<span class="status-pill bg-red-100 text-red-700 flex-shrink-0"><span class="w-2 h-2 rounded-full bg-red-500"></span>Snapshot</span>
<div class="flex-1">
<div class="font-mono text-sm text-[#0a0a0a] font-semibold">Sửa preset sau khi đã cấp — KHÔNG hồi tố</div>
<div class="font-sans-ui text-sm text-gray-500 mt-1">Áp vai trò là <strong>điền một lần tại thời điểm cấp</strong> (snapshot), KHÔNG ràng buộc sống. Admin sửa preset sau đó <em>không tự đổi</em> quyền thành viên đã cấp trước đó.</div>
</div>
</div>
<div class="bg-white border border-red-200 rounded-xl p-5 flex items-start gap-4">
<span class="status-pill bg-red-50 text-red-700 flex-shrink-0"><span class="w-2 h-2 rounded-full bg-red-500"></span>Bảo mật</span>
<div class="flex-1">
<div class="font-mono text-sm text-[#0a0a0a] font-semibold">Phạm vi quyền cấu hình preset</div>
<div class="font-sans-ui text-sm text-gray-500 mt-1">Chỉ <strong>Admin/CTO</strong> cấu hình; chỉ <strong>super admin</strong> cấp; member <em>không</em> thấy/sửa được. Quyền áp qua vai trò không vượt phạm vi workspace.</div>
</div>
</div>
</div>
<figcaption>Hình 6.1 — Năm tình huống biên + hành vi kỳ vọng. Các edge case phụ (vai trò rỗng, xoá vai trò đang dùng, danh mục 55 chức năng đổi) liệt kê trong văn bản dưới.</figcaption>
</figure>
<div class="prose-article">
<p>
Ba edge case phụ cần xử lý gọn (không cần khung riêng): (1) <strong>Xóa một vai trò đang được dùng</strong> — thành viên đã cấp <em>giữ nguyên</em> quyền, chỉ là không còn chọn vai trò đó cho lần cấp mới; không văng lỗi. (2) <strong>Danh mục 55 chức năng thay đổi</strong> — chức năng không còn tồn tại thì bỏ qua, không lỗi; chức năng mới chưa nằm trong vai trò nào thì admin tự thêm. (3) <strong>Vai trò rỗng / cấu hình lỗi</strong> — chọn vào không tick gì, không lỗi; nên cảnh báo admin khi lưu một vai trò rỗng.
</p>
<div class="pull-quote">
Snapshot là tính năng, không phải bug. Sửa preset hôm nay không được tự đổi quyền của ai đã cấp hôm qua.
<cite>— Quy tắc không hồi tố</cite>
</div>
<!-- ============= SECTION 7: USER STORIES ============= -->
<h2 id="stories">07 · User stories</h2>
<h3>Bảng tham chiếu nhanh</h3>
<p>
Năm user story định nghĩa <em>scope chức năng</em> của release đầu. <strong>P0</strong> là phải có để release; <strong>P1</strong> có thể chậm hơn nhưng vẫn nằm trong cùng feature, không tách ticket.
</p>
</div>
<!-- FIGURE: User stories table -->
<figure class="max-w-5xl mx-auto px-6">
<div class="bg-white border border-gray-200 rounded-2xl overflow-hidden shadow-sm">
<table class="field-table">
<thead>
<tr>
<th>ID</th>
<th>Vai trò</th>
<th>Muốn / Để</th>
<th>Ưu tiên</th>
</tr>
</thead>
<tbody>
<tr>
<td class="col-field">US-01</td>
<td class="col-rule">Super Admin</td>
<td><strong>Chọn một vai trò công việc</strong> (vd Kế toán) để tick sẵn đúng bộ chức năng cho thành viên — <em>không phải dò tick tay từng ô trong 55 chức năng.</em></td>
<td class="col-rule"><span class="inline-code">P0</span></td>
</tr>
<tr>
<td class="col-field">US-02</td>
<td class="col-rule">Super Admin</td>
<td>Sau khi áp vai trò, vẫn <strong>thêm/bớt từng chức năng</strong> — để cấp đúng nhu cầu cụ thể của từng thành viên.</td>
<td class="col-rule"><span class="inline-code">P0</span></td>
</tr>
<tr>
<td class="col-field">US-03</td>
<td class="col-rule">Super Admin</td>
<td>Áp <strong>nhiều vai trò</strong> cho thành viên kiêm nhiệm — để người làm nhiều vai trò vẫn cấp nhanh, đúng.</td>
<td class="col-rule"><span class="inline-code">P1</span></td>
</tr>
<tr>
<td class="col-field">US-04</td>
<td class="col-rule">Admin / CTO</td>
<td><strong>Cấu hình danh sách vai trò</strong> + bộ chức năng mỗi vai trò — để chuẩn hóa "vai trò → quyền", nhất quán, đỡ sai.</td>
<td class="col-rule"><span class="inline-code">P0</span></td>
</tr>
<tr>
<td class="col-field">US-05</td>
<td class="col-rule">Admin / CTO</td>
<td><strong>Sửa/xóa</strong> bộ chức năng của một vai trò — để cập nhật khi nghiệp vụ vai trò thay đổi.</td>
<td class="col-rule"><span class="inline-code">P1</span></td>
</tr>
</tbody>
</table>
</div>
<figcaption>Hình 7.1 — Năm user story. Ba P0 (US-01/02/04) phải có cho release đầu; hai P1 có thể chậm hơn nhưng cùng ticket.</figcaption>
</figure>
<div class="prose-article">
<!-- ============= SECTION 8: RULES ============= -->
<h2 id="rules">08 · Business rules</h2>
<h3>Cấu trúc preset & quy tắc áp</h3>
<p>
Trước khi đọc danh sách rule, nhìn vào sơ đồ dưới: preset không phải là một "role" có quyền của nó — mà chỉ là một <em>tập con</em> của 55 chức năng, dùng làm khuôn để tick sẵn lên màn phân quyền có sẵn. Bảy thành phần trong sơ đồ tương ứng với cách <strong>tập quyền cuối cùng</strong> được tính tại thời điểm Xác nhận.
</p>
</div>
<!-- FIGURE: Apply formula anatomy -->
<figure class="max-w-4xl mx-auto px-6">
<div class="bg-white border border-gray-200 rounded-2xl p-10 shadow-sm">
<div class="flex flex-wrap items-start justify-center gap-x-1 gap-y-6">
<div class="anatomy-token">
<span class="label-top">VAI TRÒ A</span>
<span class="glyph">Kế toán</span>
<span class="label-bot">preset</span>
</div>
<span class="anatomy-sep">∪</span>
<div class="anatomy-token">
<span class="label-top">VAI TRÒ B</span>
<span class="glyph">Hỗ trợ</span>
<span class="label-bot">preset</span>
</div>
<span class="anatomy-sep">∪</span>
<div class="anatomy-token">
<span class="label-top">TICK CŨ</span>
<span class="glyph">✓ k</span>
<span class="label-bot">cộng dồn<br>không tự bỏ</span>
</div>
<span class="anatomy-sep">+</span>
<div class="anatomy-token">
<span class="label-top">CHỈNH TAY</span>
<span class="glyph">±</span>
<span class="label-bot">super admin<br>thêm/bớt</span>
</div>
<span class="anatomy-sep">∩</span>
<div class="anatomy-token">
<span class="label-top">WORKSPACE</span>
<span class="glyph">khả dụng</span>
<span class="label-bot">bỏ qua<br>không cấp được</span>
</div>
<span class="anatomy-sep">=</span>
<div class="anatomy-token">
<span class="label-top">TẬP CUỐI</span>
<span class="glyph">✓ N</span>
<span class="label-bot">áp khi<br>Xác nhận</span>
</div>
<span class="anatomy-sep">→</span>
<div class="anatomy-token">
<span class="label-top">SNAPSHOT</span>
<span class="glyph">freeze</span>
<span class="label-bot">không hồi tố<br>+ ghi log</span>
</div>
</div>
<div class="mt-10 pt-6 border-t border-gray-100 text-center">
<div class="small-caps text-gray-400 mb-2">Công thức áp quyền</div>
<div class="font-mono text-base md:text-lg text-[#0a0a0a] font-semibold">union(preset_A, preset_B, tick_cũ) + chỉnh_tay ∩ workspace = tập_cuối → snapshot</div>
</div>
</div>
<figcaption>Hình 8.1 — Cách tập quyền cuối cùng được tính tại thời điểm Xác nhận. Bốn yếu tố cộng dồn (∪/+), một yếu tố lọc (∩), sau đó freeze làm snapshot.</figcaption>
</figure>
<div class="prose-article">
<p>Tám rule dưới đây là <em>chuẩn mực</em> để dev đối chiếu khi viết logic và QA đối chiếu khi viết test:</p>
<ol>
<li><strong>Preset là ánh xạ vai trò → bộ chức năng</strong>, do <em>Admin/CTO</em> cấu hình; dùng lại cho mọi thành viên.</li>
<li><strong>Chọn vai trò chỉ là điền nhanh:</strong> super admin vẫn tick được từng ô và quyết định tập cuối cùng trước khi Xác nhận. Vai trò <em>không khóa cứng</em> lựa chọn.</li>
<li><strong>Điền một lần (snapshot), không hồi tố:</strong> áp vai trò chốt tập chức năng tại thời điểm cấp; sửa preset về sau <em>không</em> tự đổi quyền thành viên đã cấp.</li>
<li><strong>Nhiều vai trò → hợp nhất (union):</strong> ô trùng không nhân đôi; không phát sinh xung đột.</li>
<li><strong>Tái sử dụng lớp phân quyền 55 chức năng có sẵn:</strong> việc áp quyền cuối cùng đi qua đúng cơ chế tick + Xác nhận hiện có; <em>không</em> đẻ cơ chế cấp quyền song song.</li>
<li><strong>Không vượt phạm vi:</strong> bộ quyền áp qua vai trò không vượt phạm vi workspace cho phép; chức năng không khả dụng (gói/chưa bật) <em>bỏ qua an toàn</em>.</li>
<li><strong>Phân quyền thao tác:</strong> chỉ Admin/CTO cấu hình preset; chỉ super admin cấp quyền; member không truy cập cấu hình.</li>
<li><strong>Ghi log:</strong> mỗi lần cấp quyền (kèm vai trò đã áp) ghi vào nhật ký — tận dụng module <em>Audit & Log</em> có sẵn.</li>
</ol>
<div class="code-block">
<div class="code-header">
<span>apply-preset.pseudo</span>
<span class="lang-tag">LOGIC</span>
</div>
<div class="code-body">
<span class="tok-com">// Áp một hoặc nhiều preset lên màn phân quyền của thành viên</span>
<span class="tok-key">final_set</span> <span class="tok-op">=</span> <span class="tok-key">union</span>(roles<span class="tok-op">.</span>map(r <span class="tok-op">=></span> r<span class="tok-op">.</span>functions))
<span class="tok-op">∪</span> existing_ticks <span class="tok-com">// cộng dồn, không tự bỏ</span>
<span class="tok-op">∩</span> workspace_available <span class="tok-com">// bỏ qua không khả dụng</span>
<span class="tok-com">// Super admin chỉnh tay → tập cuối</span>
<span class="tok-key">final_set</span> <span class="tok-op">+=</span> manual_adjustments
<span class="tok-com">// Khi bấm Xác nhận: snapshot — KHÔNG hồi tố</span>
member<span class="tok-op">.</span><span class="tok-key">permissions</span> <span class="tok-op">=</span> <span class="tok-key">freeze</span>(final_set)
audit<span class="tok-op">.</span><span class="tok-key">write</span>({ <span class="tok-key">action</span><span class="tok-op">:</span> <span class="tok-str">"grant"</span><span class="tok-op">,</span> member<span class="tok-op">,</span> applied_roles<span class="tok-op">,</span> final_set })
</div>
</div>
<h3>Giao diện (gợi ý — dev tự tinh chỉnh UX)</h3>
<p>
Trên màn phân quyền thành viên (màn 55 chức năng hiện có): thêm khu vực <strong>"Cấp theo chức năng công việc"</strong> — danh sách vai trò dạng thẻ/dropdown; chọn vai trò → các ô tương ứng được tick sẵn; super admin vẫn thấy & chỉnh được từng ô; giữ nguyên "Chọn tất cả" / "Thu hồi tất cả"; áp dụng sau khi <strong>Xác nhận</strong>. Nên hiển thị thành viên đang được áp vai trò nào. Màn cấu hình vai trò (Admin/CTO): tạo / sửa / xóa vai trò; với mỗi vai trò, tick chọn các chức năng thuộc về nó trong danh mục 55 chức năng (cùng cách trình bày theo nhóm).
</p>
<!-- ============= SECTION 9: ACCEPTANCE ============= -->
<h2 id="acceptance">09 · Acceptance & Test cases</h2>
<h3>Tám tiêu chí nghiệm thu</h3>
<p>
Để feature được coi là "xong", <em>cả tám</em> tiêu chí dưới phải pass trên staging. Đối chiếu trực tiếp khi viết test:
</p>
<ul>
<li>Super admin chọn một vai trò (vd Kế toán) → <strong>đúng bộ chức năng</strong> của vai trò được tick sẵn trên màn phân quyền của thành viên.</li>
<li>Sau khi áp vai trò, super admin <strong>thêm/bớt từng chức năng</strong> được; chỉ tập cuối cùng (sau <em>Xác nhận</em>) mới thực sự được cấp.</li>
<li>Áp <strong>nhiều vai trò</strong> cho một thành viên → hợp nhất đúng (union), ô trùng không nhân đôi, không lỗi.</li>
<li>Admin/CTO <strong>cấu hình được</strong> danh sách vai trò + bộ chức năng từng vai trò; sửa/xóa được.</li>
<li>Sửa preset về sau <strong>KHÔNG</strong> tự đổi quyền của thành viên đã được cấp trước đó (không hồi tố).</li>
<li>Chức năng cần gói trả phí / chưa bật → <strong>bỏ qua an toàn</strong>, không tick cái không cấp được, không crash.</li>
<li><strong>Chỉ</strong> Admin/CTO cấu hình preset; <strong>chỉ</strong> super admin cấp quyền; member không thấy/không sửa cấu hình.</li>
<li>Mỗi lần cấp quyền (kèm vai trò áp) được <strong>ghi log</strong>.</li>
</ul>
<h3>Ma trận test case</h3>
<p>
Test case chia ba nhóm: <em>luồng chính</em>, <em>edge case</em>, và <em>TC bổ sung do dev tự dựng</em>. Mỗi cột dưới là một nhóm:
</p>
</div>
<figure class="max-w-4xl mx-auto px-6">
<div class="bg-white border border-gray-200 rounded-2xl overflow-hidden shadow-sm">
<div class="grid md:grid-cols-3 divide-y md:divide-y-0 md:divide-x divide-gray-200">
<div class="p-6">
<div class="font-mono text-xs text-gray-400 mb-2">GROUP · 01</div>
<div class="font-display text-xl font-bold text-[#0a0a0a]">Luồng chính</div>
<div class="font-sans-ui text-sm text-gray-600 mt-3 leading-relaxed">TC-01 → TC-04 — phải pass 100%.</div>
<hr class="my-4 border-gray-200">
<ul class="font-sans-ui text-sm text-gray-700 space-y-1.5">
<li>→ <strong>TC-01</strong> áp "Kế toán" → đúng bộ tick sẵn</li>
<li>→ <strong>TC-02</strong> tinh chỉnh sau áp → tập cuối khớp lựa chọn</li>
<li>→ <strong>TC-03</strong> "Kế toán" + "Hỗ trợ" → union, không nhân đôi</li>
<li>→ <strong>TC-04</strong> tạo vai trò mới → chọn được ngay</li>
</ul>
</div>
<div class="p-6">
<div class="font-mono text-xs text-gray-400 mb-2">GROUP · 02</div>
<div class="font-display text-xl font-bold text-[#0a0a0a]">Edge case</div>
<div class="font-sans-ui text-sm text-gray-600 mt-3 leading-relaxed">TC-05 → TC-09 — bảo vệ các invariant.</div>
<hr class="my-4 border-gray-200">
<ul class="font-sans-ui text-sm text-gray-700 space-y-1.5">
<li>→ <strong>TC-05</strong> sửa preset không hồi tố</li>
<li>→ <strong>TC-06</strong> xóa vai trò đang dùng → A giữ quyền</li>
<li>→ <strong>TC-07</strong> chức năng không khả dụng → bỏ qua an toàn</li>
<li>→ <strong>TC-08</strong> member truy cập cấu hình → bị chặn</li>
<li>→ <strong>TC-09</strong> chức năng bị gỡ khỏi hệ thống → bỏ qua</li>
</ul>
</div>
<div class="p-6">
<div class="font-mono text-xs text-gray-400 mb-2">GROUP · 03</div>
<div class="font-display text-xl font-bold text-[#0a0a0a]">Dev tự dựng</div>
<div class="font-sans-ui text-sm text-gray-600 mt-3 leading-relaxed">Bám theo quyết định ở Bước 0.</div>
<hr class="my-4 border-gray-200">
<ul class="font-sans-ui text-sm text-gray-700 space-y-1.5">
<li>→ Hành vi <em>cộng dồn vs thay thế</em> khi áp lên lựa chọn tay đang có</li>
<li>→ Vai trò <em>rỗng</em> (chưa map chức năng nào)</li>
<li>→ Tải lớn: nhiều thành viên × nhiều vai trò</li>
</ul>
</div>
</div>
</div>
<figcaption>Hình 9.1 — Ma trận test case ba nhóm. Cột 3 yêu cầu dev chủ động dựng thêm — không có sẵn ID.</figcaption>
</figure>
<div class="prose-article">
<!-- ============= SECTION 10: HANDOVER ============= -->
<h2 id="handover">10 · Quy trình bàn giao</h2>
<h3>Bước 0 — chốt năm điểm trước khi code</h3>
<p>
Trước khi viết dòng code đầu tiên, năm câu hỏi dưới phải có câu trả lời từ CTO. Đừng đoán — đoán sai ở Bước 0 thì refactor cả module áp quyền.
</p>
</div>
<figure class="max-w-3xl mx-auto px-6">
<ol class="space-y-3">
<li class="bg-white border border-gray-200 rounded-xl p-5 flex items-start gap-4">
<span class="flex-shrink-0 w-8 h-8 rounded-full bg-[#0a0a0a] text-white font-mono font-bold flex items-center justify-center text-sm">1</span>
<div class="flex-1">
<div class="font-sans-ui font-semibold text-[#0a0a0a]">Danh sách vai trò công việc giai đoạn 1</div>
<div class="font-sans-ui text-sm text-gray-500 mt-1">Vd Kế toán, Hỗ trợ, Vận hành… — và bộ chức năng mỗi vai trò map tới.</div>
</div>
</li>
<li class="bg-white border border-gray-200 rounded-xl p-5 flex items-start gap-4">
<span class="flex-shrink-0 w-8 h-8 rounded-full bg-[#0a0a0a] text-white font-mono font-bold flex items-center justify-center text-sm">2</span>
<div class="flex-1">
<div class="font-sans-ui font-semibold text-[#0a0a0a]">Hành vi áp preset: cộng dồn hay thay thế?</div>
<div class="font-sans-ui text-sm text-gray-500 mt-1">Chọn vai trò thì <em>cộng dồn</em> vào lựa chọn hiện có hay <em>thay thế</em> toàn bộ? <strong>Đề xuất: cộng dồn (union)</strong>, hiển thị rõ thay đổi.</div>
</div>
</li>
<li class="bg-white border border-gray-200 rounded-xl p-5 flex items-start gap-4">
<span class="flex-shrink-0 w-8 h-8 rounded-full bg-[#0a0a0a] text-white font-mono font-bold flex items-center justify-center text-sm">3</span>
<div class="flex-1">
<div class="font-sans-ui font-semibold text-[#0a0a0a]">Nhiều vai trò / 1 thành viên — có cho kiêm nhiệm?</div>
<div class="font-sans-ui text-sm text-gray-500 mt-1"><strong>Đề xuất: có</strong>, hợp nhất union. Ô trùng không nhân đôi.</div>
</div>
</li>
<li class="bg-white border border-gray-200 rounded-xl p-5 flex items-start gap-4">
<span class="flex-shrink-0 w-8 h-8 rounded-full bg-[#0a0a0a] text-white font-mono font-bold flex items-center justify-center text-sm">4</span>
<div class="flex-1">
<div class="font-sans-ui font-semibold text-[#0a0a0a]">Hồi tố — xác nhận snapshot, không live binding</div>
<div class="font-sans-ui text-sm text-gray-500 mt-1">Áp preset là <em>điền một lần</em> (mặc định <strong>không</strong> hồi tố). Có cần thêm tính năng "đồng bộ lại thành viên theo preset mới"? <em>Mặc định: không, để sau.</em></div>
</div>
</li>
<li class="bg-white border border-gray-200 rounded-xl p-5 flex items-start gap-4">
<span class="flex-shrink-0 w-8 h-8 rounded-full bg-[#0a0a0a] text-white font-mono font-bold flex items-center justify-center text-sm">5</span>
<div class="flex-1">
<div class="font-sans-ui font-semibold text-[#0a0a0a]">Ai được cấu hình preset?</div>
<div class="font-sans-ui text-sm text-gray-500 mt-1">Chỉ CTO, hay cả super admin? Ảnh hưởng tới phạm vi UI cấu hình.</div>
</div>
</li>
</ol>
<figcaption>Hình 10.1 — Năm câu hỏi Bước 0. Có câu trả lời thì code; chưa có thì hỏi, đừng đoán.</figcaption>
</figure>
<div class="prose-article">
<h3>Bàn giao & deploy gate</h3>
<p>
Sau khi xong + UAT trên staging, dev chuẩn bị <strong>Test Report</strong> kèm bằng chứng thực tế: (1) video cấp quyền vai trò "Kế toán" bằng một thao tác áp đúng bộ chức năng; (2) demo tinh chỉnh tay sau khi áp; (3) áp nhiều vai trò hợp nhất đúng; (4) sửa preset <em>không</em> làm đổi thành viên đã cấp; (5) chặn được member truy cập cấu hình vai trò. Họp demo/review trực tiếp với CTO duyệt logic áp vai trò / union / không-hồi-tố, phạm vi quyền và độ ổn định.
</p>
<div class="my-10 border-l-4 border-red-500 bg-red-50 px-6 py-4 rounded-r-lg">
<div class="small-caps text-red-700 mb-2">Deploy gate — bất di bất dịch</div>
<p class="font-sans-ui text-base text-[#0a0a0a] !mb-0 leading-relaxed">
<strong>CHỈ KHI CTO APPROVE mới deploy Production.</strong> Không có shortcut, không có ngoại lệ. Module phân quyền đụng tới mọi vai trò trong workspace — sai một bước là member dư/thiếu quyền hàng loạt.
</p>
</div>
<hr class="deco-rule">
<p>
Đó là toàn bộ đặc tả của <em>FT-PERM-ROLE-PRESET-01</em>. Tài liệu được sắp xếp theo trình tự PRD chuẩn — nếu sau khi đọc xong bạn vẫn lăn tăn ở một mục nào, hãy <em>quay lại đúng mục đó</em> thay vì dò lại từ đầu. Mười mục, mười câu hỏi: bạn đang vướng câu nào?
</p>
<p class="text-gray-500 italic !mb-0">
— Last reviewed: 04/06/2026. Phản hồi gửi về <a href="mailto:owaf.fakku@gmail.com">owaf.fakku@gmail.com</a>.
</p>
</div>
</article>
<!-- ============= AUTHOR BIO ============= -->
<section class="bg-white border-t border-b border-gray-200 py-16">
<div class="max-w-4xl mx-auto px-6">
<div class="flex flex-col md:flex-row gap-8 items-start">
<div class="flex-shrink-0">
<div class="w-24 h-24 rounded-2xl bg-gradient-to-br from-[#0a0a0a] to-[#374151] text-[#fafafa] flex items-center justify-center font-display font-bold text-4xl shadow-lg">D</div>
</div>
<div class="flex-1">
<div class="small-caps text-gray-400 mb-2">About the author</div>
<h3 class="font-display font-bold text-2xl text-[#0a0a0a]">Doanh Nguyễn</h3>
<p class="font-sans-ui text-gray-600 mt-3 leading-relaxed max-w-2xl">
Editor của <em>The Operator's Journal</em>, viết về product spec cho công cụ nội bộ, phân quyền RBAC, và những lớp "điền nhanh" giúp đội ngũ nhỏ vận hành như đội ngũ lớn. Trước đây xây dựng dashboard quản trị workspace cho các tổ chức ở Đông Nam Á — nơi câu hỏi "ai cấp quyền cho ai, theo chuẩn nào" lặp lại mỗi lần có thành viên mới.
</p>
<div class="mt-4 flex gap-4 font-sans-ui text-sm">
<a href="#" class="text-[#c2410c] hover:underline">More articles →</a>
<a href="#" class="text-gray-600 hover:text-[#0a0a0a]">Subscribe</a>
<a href="#" class="text-gray-600 hover:text-[#0a0a0a]">@doanh</a>
</div>
</div>
</div>
</div>
</section>
<!-- ============= RELATED POSTS ============= -->
<section class="py-20">
<div class="max-w-6xl mx-auto px-6">
<div class="flex items-end justify-between mb-10 border-b border-gray-200 pb-6">
<h3 class="font-display font-bold text-3xl text-[#0a0a0a]">Tiếp tục đọc</h3>
<a href="#" class="font-sans-ui text-sm text-[#c2410c] hover:underline">Xem toàn bộ archive →</a>
</div>
<div class="grid md:grid-cols-3 gap-6">
<article class="related-card">
<div class="small-caps text-[#c2410c] mb-3">Spec</div>
<h4 class="font-display font-bold text-xl text-[#0a0a0a] leading-snug">Khi nào RBAC đầy đủ thắng preset điền nhanh — và ngược lại</h4>
<p class="font-sans-ui text-sm text-gray-600 mt-3 leading-relaxed">Hai cách tiếp cận phân quyền tưởng giống nhau, hai trade-off khác nhau. Bài này chỉ rõ chọn cái nào cho từng giai đoạn của sản phẩm.</p>
<div class="mt-5 flex items-center gap-3 font-sans-ui text-xs text-gray-500">
<span>Doanh Nguyễn</span>
<span class="text-gray-300">·</span>
<span>11 min read</span>
</div>
</article>
<article class="related-card">
<div class="small-caps text-[#c2410c] mb-3">Postmortem</div>
<h4 class="font-display font-bold text-xl text-[#0a0a0a] leading-snug">Khi một thành viên dư quyền 6 tháng: postmortem của một workspace bị leak</h4>
<p class="font-sans-ui text-sm text-gray-600 mt-3 leading-relaxed">Tick nhầm một ô trong 55 chức năng, không ai phát hiện trong nửa năm. Câu chuyện vì sao snapshot + audit log không đủ — và cần thêm gì.</p>
<div class="mt-5 flex items-center gap-3 font-sans-ui text-xs text-gray-500">
<span>Doanh Nguyễn</span>
<span class="text-gray-300">·</span>
<span>13 min read</span>
</div>
</article>
<article class="related-card">
<div class="small-caps text-[#c2410c] mb-3">Pattern</div>
<h4 class="font-display font-bold text-xl text-[#0a0a0a] leading-snug">Bốn cách thiết kế UI "cấp nhanh" cho màn phân quyền nặng</h4>
<p class="font-sans-ui text-sm text-gray-600 mt-3 leading-relaxed">Preset, template, bulk apply, hay copy-from-member: bốn pattern, mỗi cái có chỗ riêng. Đối chiếu với case study trên dashboard nội bộ.</p>
<div class="mt-5 flex items-center gap-3 font-sans-ui text-xs text-gray-500">
<span>Doanh Nguyễn</span>
<span class="text-gray-300">·</span>
<span>9 min read</span>
</div>
</article>
</div>
</div>
</section>
<!-- ============= FOOTER ============= -->
<footer class="bg-[#0a0a0a] text-[#fafafa] py-12 mt-10">
<div class="max-w-6xl mx-auto px-6 flex flex-col md:flex-row justify-between gap-6">
<div>
<div class="font-display font-bold text-xl">The Operator's Journal</div>
<div class="font-sans-ui text-sm text-gray-400 mt-2">Product specs, ops playbooks & internal tooling notes.</div>
</div>
<div class="flex gap-8 font-sans-ui text-sm text-gray-400">
<div>
<div class="small-caps text-gray-500 mb-2">Sections</div>
<div class="space-y-1">
<div>Specs</div>
<div>Playbooks</div>
<div>Postmortems</div>
</div>
</div>
<div>
<div class="small-caps text-gray-500 mb-2">About</div>
<div class="space-y-1">
<div>Editor</div>
<div>Subscribe</div>
<div>RSS</div>
</div>
</div>
</div>
</div>
<div class="max-w-6xl mx-auto px-6 mt-10 pt-6 border-t border-gray-800 font-sans-ui text-xs text-gray-500 flex justify-between">
<span>© 2026 The Operator's Journal · Vol. 04 Issue 18</span>
<span>Published 04 Jun 2026 · Hà Nội / Sài Gòn</span>
</div>
</footer>
</body>
</html>
Số dòng: 1115