Solo Dev · Laravel 11 · DMS Enterprise

Kế hoạch triển khai
Construction PM

Hướng dẫn từng bước dành cho solo dev lần đầu dùng Laravel — từ cài đặt môi trường đến hệ thống DMS hoàn chỉnh với Dual-Storage S3 + SharePoint, PDF/BIM Viewer và Approval Workflow.

Solo Dev — PHP thuần background Laravel 11 + Vue 3 + Inertia.js AWS S3 Primary + SharePoint Backup PDF.js + IFC.js + Fabric.js Target: 1000 users
68
Tổng nhiệm vụ
0
Đã hoàn thành
4
Phase chính
0%
Tiến độ
ModuleCông nghệ / Thư việnLý do chọn
BackendLaravel 11 PHP 8.4Storage facade, Queue, Policy — đủ mạnh cho dual-storage
Frontend SPAVue.js 3 Inertia.jsSPA mượt như Claude UI — không cần REST API riêng
Primary StorageAWS S3Presigned URL, versioning, Intelligent-Tiering
Backup StorageMicrosoft Graph API SharePointTận dụng M365 đang có sẵn — 0 chi phí thêm
Queue / AsyncLaravel Queue Redis HorizonUpload SharePoint async — không block user
PDF ViewerPDF.jsOpen source, render PDF trên browser, hỗ trợ annotation layer
BIM/IFC ViewerThatOpen (IFC.js)Render file .ifc 3D trực tiếp trên browser, WebGL
AnnotationFabric.jsCanvas 2D — vẽ hình, text, stamp; lưu tọa độ vào DB
PDF Flattenpdf-libGhi annotation vào PDF gốc khi approve — tạo file v_approved.pdf
Chunk UploadUppy.jsBắt buộc cho file .rvt/.ifc 30–100MB — tránh PHP timeout
RBACspatie/laravel-permissionRole + Permission per folder/project — cài package, không tự viết
Audit Logspatie/laravel-activitylogGhi log tự động mọi action — không thể xóa
AuthLaravel Breeze hoặc SanctumAuth có sẵn + remember me + password reset
EmailAWS SES hoặc Mailpit (dev)SES $1/10K emails; Mailpit để test local
ServerDocker Nginx VPS $24–40/thángDocker local = production parity; VPS DigitalOcean/Vultr
Tuần 1–3
Phase 0 — Cài đặt & Học Laravel
Docker, Laravel 11, cấu trúc thư mục, Eloquent, Queue — học song song với cài đặt. Laracasts "Laravel From Scratch" (miễn phí). Mục tiêu: hiểu MVC + migration + seeder.
3 tuần
Tuần 4–7
Phase 1 — Nền tảng Backend
DB schema, RBAC, Dual-Storage (S3 + SharePoint), Queue jobs, Audit log, Versioning logic.
4 tuần
Tuần 8–14
Phase 2 — DMS Core (trái tim app)
File explorer, Upload với Uppy.js, Versioning UI, PDF.js viewer, IFC.js BIM viewer, Fabric.js annotation layer.
7 tuần
Tuần 15–19
Phase 3 — Approval Workflow
Approval flow builder, email notification, PDF flatten với pdf-lib, approval log & history.
5 tuần
Tuần 20–24
Phase 4 — Polish, Settings, Security
Settings, Help, Performance tuning, Security audit, Deploy production, UAT với client thực.
5 tuần
💡 Chiến lược solo dev
Không cố code hết mọi thứ cùng lúc. Phase 2 (DMS) là phần nặng nhất — ưu tiên file explorer + upload hoạt động trước, viewer + annotation làm sau. Dùng Claude để giải quyết từng vấn đề cụ thể thay vì hỏi tổng quát.
Phase 0 · Tuần 1–3
🛠 Cài đặt môi trường từ đầu
Mục tiêu: Có môi trường dev chạy được Laravel 11 trên Docker, kết nối được MySQL, Redis, S3. Đây là bước solo dev mất nhiều thời gian nhất nếu lần đầu — hãy kiên nhẫn.
📦 Yêu cầu máy tính (Windows/Mac/Linux)
Cài Docker Desktop
Download tại docker.com/products/docker-desktop — chọn phiên bản theo OS. Sau khi cài xong, mở terminal gõ docker --version để xác nhận.
Cài PHP 8.4 + Composer (để chạy lệnh artisan)
Windows: dùng XAMPP hoặc Laragon (khuyến nghị cho Windows). Mac: brew install php@8.4 composer. Sau đó: composer --version để xác nhận.
Cài Node.js 20 LTS + npm
Tải tại nodejs.org — chọn LTS. Cần để build Vue.js 3 + Inertia. Xác nhận: node --versionnpm --version.
Tạo dự án Laravel 11 mới
Mở terminal tại thư mục làm việc:
composer create-project laravel/laravel construction-pm
Sau đó: cd construction-pm
Cài Laravel Sail (Docker wrapper chính thức của Laravel)
composer require laravel/sail --dev
Sau đó: php artisan sail:install → chọn mysql, redis, mailpit
Khởi động: ./vendor/bin/sail up -d
🐳 docker-compose.yml — Cấu hình đầy đủ
docker-compose.yml Laravel Sail extended
services:
  laravel.test:           # App Laravel chính
    build:
      context: ./vendor/laravel/sail/runtimes/8.4
    ports: ["80:80", "443:443"]
    environment:
      WWWUSER: 1000
    volumes: ['.:${SAIL_APPLICATION_VOLUME}']
    depends_on: [mysql, redis, mailpit]
    networks: [sail]

  mysql:
    image: mysql/mysql-server:8.0
    environment:
      MYSQL_ROOT_PASSWORD: '${DB_PASSWORD}'
      MYSQL_DATABASE: '${DB_DATABASE}'
      MYSQL_USER: '${DB_USERNAME}'
      MYSQL_PASSWORD: '${DB_PASSWORD}'
    volumes: ['sail-mysql:/var/lib/mysql']
    ports: ["3306:3306"]
    networks: [sail]

  redis:
    image: redis:alpine
    ports: ["6379:6379"]
    networks: [sail]

  mailpit:              # Bắt email khi dev — không gửi thật
    image: axllent/mailpit
    ports: ["8025:8025", "1025:1025"]
    networks: [sail]

networks:
  sail: {driver: bridge}
volumes:
  sail-mysql: {driver: local}
⚙️ .env — Cấu hình môi trường
.env copy từ .env.example rồi sửa
# App
APP_NAME="Construction PM"
APP_URL=http://localhost

# Database
DB_CONNECTION=mysql
DB_HOST=mysql          # tên service trong docker-compose
DB_PORT=3306
DB_DATABASE=construction_pm
DB_USERNAME=sail
DB_PASSWORD=password

# Redis
REDIS_HOST=redis
REDIS_PORT=6379

# Queue — dùng Redis thay vì sync
QUEUE_CONNECTION=redis

# AWS S3 — Primary Storage
AWS_ACCESS_KEY_ID=your_key
AWS_SECRET_ACCESS_KEY=your_secret
AWS_DEFAULT_REGION=ap-southeast-1   # Singapore
AWS_BUCKET=construction-pm-files
AWS_URL=                             # để trống — dùng Presigned URL

# Microsoft Graph API — SharePoint Backup
MICROSOFT_TENANT_ID=your_tenant_id
MICROSOFT_CLIENT_ID=your_client_id
MICROSOFT_CLIENT_SECRET=your_secret
SHAREPOINT_SITE_ID=your_site_id
SHAREPOINT_DRIVE_ID=your_drive_id

# Mail (dev dùng mailpit, prod dùng SES)
MAIL_MAILER=smtp
MAIL_HOST=mailpit
MAIL_PORT=1025
terminal chạy trong thư mục dự án
# 1. Frontend stack (Vue 3 + Inertia)
./vendor/bin/sail composer require inertiajs/inertia-laravel
npm install @inertiajs/vue3 vue@3

# 2. RBAC — phân quyền
./vendor/bin/sail composer require spatie/laravel-permission

# 3. Audit log
./vendor/bin/sail composer require spatie/laravel-activitylog

# 4. AWS S3
./vendor/bin/sail composer require league/flysystem-aws-s3-v3

# 5. Microsoft Graph API
./vendor/bin/sail composer require microsoft/microsoft-graph

# 6. Excel import/export
./vendor/bin/sail composer require maatwebsite/excel

# 7. PDF generation (approval stamp)
./vendor/bin/sail composer require barryvdh/laravel-dompdf

# 8. Queue dashboard
./vendor/bin/sail composer require laravel/horizon

# 9. Publish configs
./vendor/bin/sail artisan vendor:publish --provider="Spatie\Permission\PermissionServiceProvider"
./vendor/bin/sail artisan vendor:publish --provider="Laravel\Horizon\HorizonServiceProvider"
./vendor/bin/sail artisan migrate    # Tạo bảng permissions, jobs, etc.

# 10. Frontend deps
npm install fabric uppy @uppy/core @uppy/dashboard @uppy/aws-s3-multipart
npm install pdfjs-dist @thatopen/components
npm run dev    # Vite hot reload
💡 Khi gặp lỗi cài đặt
Prompt cho Claude: "Tôi chạy lệnh [paste lệnh] trong Laravel Sail và gặp lỗi [paste error]. Đây là file .env của tôi [paste]. Hãy giải thích lỗi và fix từng bước."
Phase 1 · Tuần 4–7
🏗 Nền tảng & Hạ tầng Backend
Mục tiêu: Khung xương vững chắc — DB schema đầy đủ, RBAC hoạt động, dual-storage kết nối được S3 và SharePoint, Queue worker chạy background.
app/ Service Pattern — tách business logic khỏi Controller
app/
├── Http/Controllers/
│   ├── FileController.php        # Upload, download, delete
│   ├── FolderController.php      # CRUD folder
│   ├── ApprovalController.php    # Submit, approve, reject
│   └── AnnotationController.php  # Save/load annotations
├── Services/
│   ├── StorageManager.php        # Điều phối S3 + SharePoint
│   ├── Drivers/
│   │   ├── S3Driver.php          # Upload, Presigned URL, delete
│   │   └── SharePointDriver.php  # Graph API: upload, create folder
│   ├── VersioningService.php     # Logic v1 → v2 → v3
│   └── ApprovalService.php       # Workflow state machine
├── Jobs/
│   ├── SharePointUploadJob.php   # Async backup lên SharePoint
│   ├── FlattenPdfJob.php         # Flatten annotations → PDF mới
│   └── SendApprovalEmailJob.php  # Gửi email notification
├── Models/
│   ├── Organization.php
│   ├── Project.php
│   ├── Folder.php
│   ├── File.php
│   ├── FileVersion.php
│   ├── Annotation.php
│   ├── Submission.php
│   └── ApprovalStep.php
└── Policies/
    ├── FolderPolicy.php          # Ai được xem/sửa folder nào
    └── FilePolicy.php            # Ai được download/delete file
Migration: organizations — Tổ chức tham gia dự án
id, name, code (ORG-001), logo_s3_key, address, contact_email, is_active, created_at
MigrationPhase 1
2h
Migration: users — Thành viên (thuộc organization)
id, organization_id, name, email, password, avatar_s3_key, job_title, is_active. Quan hệ belongsTo Organization
MigrationPhase 1
2h
Migration: projects — Dự án xây dựng
id, code (PRJ-001), name, description, status (active/completed/archived), thumbnail_s3_key
Migration
1h
Migration: folders — Cây thư mục đệ quy
id, project_id, parent_id (self-reference), name, path (materialized path: /Arc/L7/M3), sort_order, created_by
Migration⚡ Quan trọng
2h
Migration: files — File metadata (không lưu nội dung)
id, folder_id, name, original_name, extension, mime_type, size_bytes, s3_key, s3_bucket, sharepoint_path, sharepoint_synced, current_version, uploaded_by, description
Migration⚡ Core
2h
Migration: file_versions — Lịch sử phiên bản file
id, file_id, version_number (1,2,3...), s3_key, size_bytes, uploaded_by, comment, created_at. KHÔNG BAO GIỜ xóa record này (pháp lý)
Migration⚡ Pháp lý
1h
Migration: annotations — Ghi chú trên file
id, file_id, version_id, page_number, fabric_json (toàn bộ canvas state), created_by, is_flattened (false cho đến khi approve)
MigrationDMS
1h
Migration: submissions + approval_steps — Quy trình phê duyệt
submissions: file_id, title, message, status, submitted_by. approval_steps: submission_id, approver_id, order, status, comment, decided_at
MigrationApproval
2h
Seeder: Dữ liệu mẫu đầy đủ cho dev và demo
3 organizations, 10 users, 2 projects, cây folder mẫu 3 cấp, 5 files (kể cả PDF và IFC), 1 approval flow hoàn chỉnh
Seeder
3h
🔐 Thiết kế Role & Permission
database/seeders/RolePermissionSeeder.php
// Roles hệ thống
$roles = [
    'super_admin',   // Admin tổng — toàn quyền
    'org_admin',     // Admin tổ chức — quản lý member của org mình
    'project_manager', // Quản lý dự án — approve, quản lý folder
    'member',        // Thành viên — upload, download theo quyền folder
    'viewer',        // Chỉ xem — không upload, không download (chỉ preview)
];

// Permissions — kết hợp với FolderPolicy để check per-folder
$permissions = [
    // File permissions
    'file.upload',      'file.download',   'file.delete',
    'file.view',        'file.annotate',   'file.version.view',
    // Folder permissions
    'folder.create',    'folder.rename',   'folder.delete',
    // Approval permissions
    'approval.submit',  'approval.review', 'approval.approve',
    // Admin permissions
    'user.manage',      'org.manage',      'project.manage',
    'permission.manage', 'setting.manage',
];
Seeder: Tạo Roles và Permissions với spatie/laravel-permission
RolePermissionSeeder.php — assign permissions mặc định cho mỗi role, chạy sau migrate
RBAC
3h
FolderPolicy — kiểm tra quyền per-folder (quan trọng nhất)
Logic: Super_admin bypass mọi check. Org_admin check org. Member: check bảng folder_permissions xem user/role có quyền view/write folder này không
Policy⚡ Core
4h
Migration: folder_permissions — ACL per folder
folder_id, subject_type (role/user), subject_id, can_view, can_upload, can_delete, can_approve — inheritance từ folder cha
ACL
3h
Permission Matrix UI — trang admin quản lý quyền
Bảng rows=users, cols=folders, checkbox cho mỗi quyền. Dùng Inertia + Vue để update realtime
UIVue
6h
⚡ StorageManager — điều phối S3 (sync) + SharePoint (async)
app/Services/StorageManager.php
class StorageManager
{
    public function store(UploadedFile $file, $folder, $metadata): FileModel
    {
        // 1. Generate UUID key — không dùng tên file gốc
        $uuid   = Str::uuid();
        $s3Key  = "projects/{$folder->project_id}/{$folder->path}/{$uuid}.{$ext}";

        // 2. Upload S3 — SYNCHRONOUS (user chờ kết quả này)
        Storage::disk('s3')->put($s3Key, fopen($file, 'r'), 'private');

        // 3. Lưu metadata vào DB
        $fileRecord = FileModel::create([
            'folder_id'       => $folder->id,
            's3_key'          => $s3Key,
            'original_name'   => $file->getClientOriginalName(),
            'size_bytes'      => $file->getSize(),
            'current_version' => 1,
        ]);

        // 4. Dispatch job SharePoint — ASYNCHRONOUS (user không chờ)
        SharePointUploadJob::dispatch($fileRecord->id, $s3Key)
            ->onQueue('sharepoint')
            ->delay(now()->addSeconds(5));

        return $fileRecord;
    }

    public function temporaryUrl(FileModel $file, $minutes = 15): string
    {
        // LUÔN dùng temporaryUrl — không bao giờ expose S3 URL trực tiếp
        return Storage::disk('s3')
            ->temporaryUrl($file->s3_key, now()->addMinutes($minutes));
    }
}
S3Driver — upload, delete, temporaryUrl, copy (versioning)
Wrapper quanh Storage::disk('s3'). Method: store(), delete(), temporaryUrl(ttl), copyVersion()
S3
4h
SharePointDriver — OAuth2 token cache + upload nhỏ + chunked upload >4MB
getAccessToken() với cache 1h. uploadSmall() PUT. uploadLarge(): createUploadSession() → upload 10MB chunks. Graph API endpoint: /sites/{id}/drives/{id}/root:/{path}:/content
Graph API⚡ Khó
8h
SharePointUploadJob — retry 3 lần, alert admin khi failed
$tries=3, $backoff=[60,300,900]. failed() method gửi email admin. Cập nhật sharepoint_synced=true khi thành công
Queue Job
3h
Laravel Horizon — cài và cấu hình queue dashboard
Tạo queue riêng: 'default', 'sharepoint', 'email'. Horizon config: sharepoint queue chạy 2 worker, email queue chạy 3 worker
Horizon
2h
S3 Presigned URL endpoint — GET /files/{id}/view
Controller kiểm tra quyền → gọi StorageManager::temporaryUrl() → redirect. Không bao giờ trả S3 URL trong JSON response
Security
2h
Cấu hình spatie/activitylog — log tự động mọi Model event
Thêm HasActivity trait vào File, Folder, Submission. Log: created, updated, deleted. Immutable — không xóa được qua app
Audit
2h
VersioningService — logic v1 → v2 khi upload file trùng tên
Khi upload file có cùng tên trong folder: copy s3_key cũ vào file_versions, tăng current_version, upload key mới. Không xóa file cũ trên S3 (pháp lý)
Versioning⚡ Pháp lý
4h
File Version History UI — xem và download các phiên bản cũ
Sidebar panel trong file viewer: danh sách V1, V2, V3... với ngày giờ, người upload, dung lượng. Click xem hoặc download từng version
UI
4h
Phase 2 · Tuần 8–14
📁 DMS Core — Trái tim ứng dụng
Mục tiêu: File explorer hoạt động hoàn chỉnh — upload, versioning, view PDF/IFC, ghi annotation. Đây là phần phức tạp nhất về frontend (Vue 3 + Inertia).
CRUD Organizations — Admin quản lý công ty
Danh sách, thêm/sửa (modal), upload logo lên S3, soft delete, tìm kiếm
VueInertia
6h
CRUD Users / Project Members — Org Admin quản lý team
Danh sách theo org, thêm user (gửi email welcome + temp password), upload avatar, assign role, toggle active
VueEmail
8h
Assign thành viên vào Project với role cụ thể
project_user pivot table với role. Một user có thể là manager ở project A, member ở project B
Vue
4h
Trang Profile cá nhân — đổi info, đổi password, xem activity
Avatar upload S3, change password (validate old), my activity log 30 ngày gần nhất
Vue
4h
🖥 Layout 3 panel — tham khảo Autodesk Docs
resources/js/Pages/DMS/Explorer.vue
<!-- Layout chính DMS -->
<template>
  <div class="dms-layout">

    <!-- Panel trái: Folder Tree -->
    <aside class="folder-tree">
      <FolderTree
        :folders="projectFolders"
        :active-id="activeFolderId"
        @select="onFolderSelect"
        @context-menu="showContextMenu"   <!-- right-click -->
      />
    </aside>

    <!-- Panel giữa: File list -->
    <main class="file-panel">
      <Breadcrumb :path="currentPath" />
      <FileToolbar @upload="openUploader" @view-toggle="toggleView" />
      <FileGrid v-if="viewMode==='grid'" :files="files" @click="openFile" />
      <FileTable v-else :files="files" @click="openFile" />
    </main>

    <!-- Panel phải: File Viewer + Sidebar -->
    <FileViewer
      v-if="selectedFile"
      :file="selectedFile"
      :presigned-url="presignedUrl"
    />
  </div>
</template>
FolderTree component — recursive, collapsible, drag-and-drop
Dùng Vue treeview recursive. Drag folder để reorder/move. Right-click context menu: Rename, New subfolder, Delete, Set permissions. Breadcrumb cập nhật theo selection
Vue⚡ Phức tạp
10h
FileTable + FileGrid view — toggle, sort, filter
Table: tên, icon loại file, version badge (V1/V2), size, ngày cập nhật, người upload, approval status badge. Toggle grid/list. Sort by: name, date, size. Filter: loại file, status
Vue
8h
Search toàn cục trong project — tìm file/folder
Ctrl+K shortcut mở search modal. Tìm theo: tên file, extension, người upload, khoảng ngày. Kết quả highlight match, click navigate đến folder
VueLaravel
5h
Deep-link URL — bookmark folder cụ thể
URL pattern: /projects/123/dms?folder=456 — reload giữ đúng folder. Dùng Inertia router để không reload full page
Inertia
3h
🚨 Tại sao bắt buộc phải dùng Uppy.js?
File .rvt/.ifc có thể nặng 30–100MB. PHP upload thông thường giới hạn bởi upload_max_filesize và request_timeout. Uppy.js chia file thành chunks 5–10MB, upload từng phần lên S3 Multipart — tránh hoàn toàn vấn đề timeout và có thể resume nếu mất kết nối.
📤 Uppy S3 Multipart Upload Flow
User kéo file vào DMS
Uppy.js (browser)
POST /api/s3/multipart/create
Laravel tạo UploadId trên S3
Uppy chia file thành chunks 10MB
Upload mỗi chunk thẳng lên S3
S3 nhận đủ chunks
Uppy gọi POST /api/s3/multipart/complete
Laravel complete multipart + lưu DB
Dispatch SharePointUploadJob
Backend: S3 Multipart API endpoints (4 endpoints)
POST /multipart/create → createMultipartUpload(). GET /multipart/{id}/sign → signPart(). POST /multipart/{id}/complete → completeMultipartUpload(). DELETE /multipart/{id} → abortMultipartUpload()
S3 API⚡ Core
6h
Frontend: UppyUploader component — drag drop UI với progress
Uppy Dashboard embedded, config AwsS3Multipart plugin. Progress bar per file. Hiển thị upload speed. Cancel/retry từng file. Sau khi complete → refresh file list
VueUppy.js
8h
VersioningService — detect file trùng tên → auto-version
Khi complete upload: kiểm tra file cùng tên trong folder đã tồn tại chưa? Có → lưu version cũ vào file_versions, tăng current_version. Hiện dialog: "File TTG-AR-L7.rvt đã có V1 — đang tạo V2"
Versioning⚡ Logic
5h
SharePoint sync status indicator — badge trên mỗi file
Polling /api/files/{id}/sync-status mỗi 10s sau upload. Badge: "Syncing..." → "SharePoint ✓" hoặc "Sync failed ⚠". Tooltip hiện chi tiết lỗi nếu failed
Vue
3h
PdfViewer component — PDF.js với Presigned URL
Load PDF từ temporaryUrl (không lưu URL). Điều hướng trang, zoom, full screen. Thumbnail sidebar. Search text trong PDF. Overlay Fabric.js canvas lên mỗi trang
PDF.jsVue
10h
IfcViewer component — ThatOpen IFC.js render 3D model
Load .ifc file từ Presigned URL. Orbit, zoom, pan camera. Toggle visibility theo discipline/level. Hiển thị properties khi click element. Chú thích 3D (điểm hotspot)
IFC.js⚡ Phức tạp
14h
FileViewer sidebar — Info, Versions, Annotations, Approval tabs
Right sidebar khi mở file. Tab Info: metadata. Tab Versions: lịch sử V1/V2. Tab Annotations: danh sách notes trên file. Tab Approval: submit/xem status approval
Vue
8h
Metadata viewer cho .rvt và .dwg (không render full model)
Hiển thị: tên file, dung lượng, version Revit, ngày tạo. Preview thumbnail nếu có. Link download. Ghi chú "Xem file trong Revit/AutoCAD để render đầy đủ"
Vue
3h
✏️ Kiến trúc Annotation — Không chỉnh sửa file gốc
Annotation được lưu dưới dạng JSON canvas state của Fabric.js trong DB (cột fabric_json). File gốc trên S3 không bị thay đổi. Chỉ khi người dùng ấn "Flatten & Save" (hoặc khi approval hoàn tất), hệ thống mới gọi pdf-lib ở backend để ghi annotation vào PDF và tạo file mới (tăng version).
Fabric.js canvas overlay — layer vẽ trên PDF pages
Canvas chính xác kích thước trang PDF. Toolbar: bút vẽ, hình chữ nhật, vòng tròn, mũi tên, text, highlight, eraser. Chọn màu sắc, độ dày. Undo/Redo (stack 50 actions)
Fabric.jsVue
12h
Save/Load annotation — API endpoint lưu JSON vào DB
Auto-save mỗi 30s (debounce). POST /api/annotations với fabric_json + page_number + file_id. Load annotation khi mở file: overlay lên đúng trang, đúng tọa độ
API
5h
Comment pin — ghim comment vào tọa độ cụ thể trên file
Click vào điểm bất kỳ trên PDF → tạo pin icon + text comment. Lưu tọa độ (x%, y%, page). Hover pin → hiện comment. Thread reply comment
VueAPI
8h
Annotation permission — chỉ ai có quyền annotate mới được vẽ
Check permission 'file.annotate' trước khi enable canvas. Viewer role: chỉ xem annotation của người khác, không vẽ được. Hiển thị annotation của từng user bằng màu khác nhau
RBAC
3h
Phase 3 · Tuần 15–19
✅ Approval Workflow
Mục tiêu: Tự động hóa quy trình phê duyệt — từ submit, notify qua email, đến flatten PDF và lưu approval log đầy đủ.
ApprovalFlowBuilder component — chọn approver và thứ tự
Kéo thả người duyệt vào các "bước". Sequential (từng bước) hoặc Parallel (tất cả cùng lúc). Thêm observer (xem nhưng không duyệt). Template flow cho từng loại tài liệu (Arc, Str, MEP)
Vue⚡ UX quan trọng
10h
Submit Submission — POST /api/submissions với file + flow
Tạo submission record, tạo approval_steps theo flow đã chọn. Dispatch SendApprovalEmailJob cho approver bước 1 (sequential) hoặc tất cả (parallel)
API
5h
ApprovalService — state machine: pending → in_review → approved/rejected
Sequential: khi step 1 approve → trigger step 2. Parallel: tất cả approve → mark complete. Bất kỳ reject → stop flow, notify submitter. Logic trong ApprovalService::processDecision()
State Machine
8h
Approval Dashboard — pending/approved/rejected submissions
3 tab: "Chờ tôi duyệt", "Tôi đã gửi", "Tất cả". Timeline view từng submission. Xem approver nào đã duyệt, ai chưa, ai từ chối với comment gì
Vue
8h
Approve/Reject UI trong FileViewer sidebar
Approver mở file → tab Approval → xem submission details + annotations của người submit → Nút Approve / Request Changes / Reject + textarea comment bắt buộc khi reject
Vue
6h
Approval templates — lưu flow tái sử dụng
Admin tạo template "Duyệt bản vẽ Kiến trúc" với approver mặc định. Khi submit, chọn template → tự điền flow. Tiết kiệm thời gian cho submission lặp lại
Feature
4h
Email template — Approval Request (HTML email đẹp)
Tên file, người gửi, dự án, preview nội dung, deadline, link "Xem và duyệt" (deeplink đến file + tab Approval). Test với Mailpit local
Email
4h
Email templates — Approved / Rejected / Reminder
Approved: "File đã được duyệt bởi [tên]". Rejected: "File bị từ chối — lý do: [comment]". Reminder: "Bạn có submission chờ duyệt đã 3 ngày"
Email
3h
In-app Notification center — bell icon topbar
Migration: notifications table. Polling mỗi 30s hoặc Laravel Echo + Pusher (realtime). Mark as read. Link đến item liên quan. Badge count trên bell icon
VueLaravel
8h
📄 FlattenPdfJob — pdf-lib ghi annotation vào PDF
app/Jobs/FlattenPdfJob.php + Node.js microservice hoặc dùng pdf-lib trực tiếp
// Khi approval hoàn tất → dispatch job này
class FlattenPdfJob implements ShouldQueue
{
    public function handle(): void
    {
        // 1. Download PDF gốc từ S3 về temp
        $pdfBytes = Storage::disk('s3')->get($this->file->s3_key);

        // 2. Gọi Node.js microservice để flatten với pdf-lib
        //    (pdf-lib là JS lib — gọi qua HTTP hoặc exec node script)
        $response = Http::post('http://pdf-service:3001/flatten', [
            'pdfBase64'   => base64_encode($pdfBytes),
            'annotations' => $this->annotations, // fabric_json array
            'approvalStamp' => [
                'text'     => "APPROVED",
                'approver' => $this->approver->name,
                'date'     => now()->format('d/m/Y H:i'),
            ],
        ]);

        // 3. Upload PDF mới lên S3 → tạo version mới
        $newS3Key = str_replace('.pdf', '_v'.$newVersion.'.pdf', $this->file->s3_key);
        Storage::disk('s3')->put($newS3Key, base64_decode($response['pdfBase64']));

        // 4. Mark annotations as flattened, file version mới
        Annotation::whereFileId($this->file->id)->update(['is_flattened' => true]);
        // dispatch SharePointUploadJob cho bản approved mới
    }
}
📌 Về pdf-lib và Node.js
pdf-lib là thư viện JavaScript — chạy tốt nhất trong Node.js. Bạn cần tạo một microservice nhỏ (Express.js ~50 dòng) trong Docker container riêng để nhận request từ Laravel và flatten PDF. Claude có thể viết toàn bộ microservice này cho bạn trong 1 prompt.
Node.js PDF microservice — Express + pdf-lib (Docker container)
Dockerfile: node:20-alpine. POST /flatten: nhận pdfBase64 + annotations JSON → vẽ lên PDF → trả pdfBase64 mới. POST /stamp: thêm approval watermark. ~80 dòng code
pdf-libNode.js
6h
FlattenPdfJob — kết nối PHP → Node microservice
HTTP::post() đến pdf-service container. Upload kết quả lên S3 với version mới. Dispatch SharePoint backup cho bản approved
Laravel Job
4h
Approval stamp design — watermark "APPROVED" chuyên nghiệp
Stamp góc phải mỗi trang: "APPROVED", tên người duyệt, ngày giờ, mã submission. Font chuyên nghiệp, màu xanh hoặc đỏ. Có thể customize theo tổ chức
pdf-lib
3h
Approval history log — timeline đầy đủ mọi action
Per submission: timeline hiển thị ai submit lúc nào, ai xem lúc nào, ai approve/reject với comment, khi nào flatten hoàn tất. Export PDF report của approval history
Audit
5h
Phase 4 · Tuần 20–24
⚙️ Settings, Security & Production
Mục tiêu: Hoàn thiện app — settings đầy đủ, security audit, deploy production, test với người dùng thực.
Settings — General: tên app, logo, timezone, ngôn ngữ
Lưu vào DB table app_settings (key-value). Logo upload S3. Timezone ảnh hưởng display khắp app
Settings
4h
Settings — Email/SMTP: cấu hình server email
Config SMTP hoặc AWS SES. Test send button. Template editor cho email notification (giới hạn: chỉ sửa nội dung, không sửa HTML structure)
Settings
4h
Settings — File: giới hạn dung lượng, loại file cho phép
max_file_size_mb per role. allowed_extensions whitelist. Blocked types: .exe, .bat, .sh. Virus scan integration (optional)
Settings
3h
Help — trang hướng dẫn nội tuyến
Accordion FAQ theo nhóm: Upload file, Phê duyệt, Phân quyền, Tài khoản. Keyboard shortcuts guide. Video embed (YouTube/Loom). Link feedback form
Help
4h
Database indexing — index các cột query nhiều
Index: folder_id, project_id, uploaded_by, created_at trên bảng files. file_id + version_number trên file_versions. Chạy EXPLAIN trên top 5 query nặng nhất
Performance
4h
Security: Rate limiting, brute force protection
throttle middleware: login 5 req/min, API 60 req/min. CSRF bảo vệ tất cả POST. Validate file MIME type phía server (không tin browser). SQL injection: Eloquent ORM đã cover
Security
4h
S3 lifecycle rule — tự động archive file cũ
S3 Intelligent-Tiering: file_versions/ → archive sau 90 ngày. Giảm ~60% cost storage cho version cũ. Config qua AWS Console hoặc Terraform
Cost
2h
Deploy production — VPS + SSL + CI/CD đơn giản
DigitalOcean Droplet $24/tháng. Laravel Forge (tự động cài Nginx, PHP, SSL). GitHub Action: push main → auto deploy. Supervisor chạy queue workers. Horizon dashboard
DevOps
8h
Laravel Telescope — debug tool cho production
Cài Telescope để monitor: requests, queries, jobs, emails, exceptions. Giới hạn access chỉ admin. Rất hữu ích khi debug production issues
Monitoring
2h
Lưu ý sống còn
⚠️ Không được bỏ qua
🚨 1. Pháp lý — Không bao giờ xóa file_versions
Thêm protected $guarded = ['*'] và override method delete() trong model FileVersion để throw Exception. Chỉ Super Admin mới được "archive" (soft delete với flag), không xóa vật lý. Trên S3: không enable Object Delete trên file_versions/ prefix.
🚨 2. Bảo mật S3 — Không bao giờ expose URL trực tiếp
Tất cả S3 key phải là UUID — không đoán được. Bucket đặt Private hoàn toàn — Block all public access. Mọi truy cập đi qua Storage::temporaryUrl() với TTL 15 phút. Không bao giờ lưu presigned URL vào DB.
⚡ 3. File lớn — Bắt buộc dùng Uppy.js Multipart
PHP upload timeout mặc định 30s — file .rvt 50MB sẽ fail 100%. Uppy.js multipart bypass hoàn toàn PHP cho việc upload — file đi thẳng browser → S3 theo từng chunk 10MB. Laravel chỉ nhận thông báo khi complete.
⚡ 4. SharePoint Rate Limit — Graph API giới hạn 10,000 req/10 phút
Khi nhiều user upload cùng lúc, SharePoint jobs có thể bị throttle. Giải pháp: dùng exponential backoff trong Job retry, cache OAuth token (không request token mới mỗi lần), và monitor qua Horizon để detect bottleneck sớm.
Hướng dẫn
🤖 Cách dùng Claude hiệu quả nhất
✅ Prompt hiệu quả — càng cụ thể càng tốt
Khi gặp lỗi:
"Tôi đang dùng Laravel 11 + Sail. Chạy lệnh [paste lệnh] gặp lỗi [paste toàn bộ error]. File liên quan: [paste code file]. Đây là .env: [paste]. Hãy giải thích nguyên nhân và fix từng dòng."

Khi cần code mới:
"Tôi cần viết class SharePointDriver.php trong Laravel 11 để: (1) lấy OAuth2 token từ Azure với client_credentials, (2) upload file nhỏ <4MB bằng PUT request, (3) upload file lớn bằng createUploadSession + chunk 10MB. Credentials lấy từ env MICROSOFT_*. Trả về sharepoint_file_id khi thành công."

Khi review code:
"Đây là FolderPolicy.php của tôi [paste]. Hãy kiểm tra: (1) có bị SQL injection không, (2) có case nào bị bypass permission không, (3) có thể tối ưu N+1 query không."
📚 Tài nguyên học Laravel cho PHP dev (thứ tự ưu tiên)
1. Laracasts.com — "Laravel From Scratch" series (miễn phí) — xem song song với code
2. laravel.com/docs — Documentation chính thức — search khi cần feature cụ thể
3. spatie.be/docs — Docs của Spatie packages (Permission, ActivityLog)
4. Claude — Debug lỗi, explain concept, review code, generate boilerplate
5. Laravel Daily (YouTube) — Video ngắn về từng tính năng thực tế