# ระบบรับรองแหล่งผลิต GAP พืช (Web Application) ## สารบัญ (Table of Contents) 1. [Tech Stack & Architecture](#1-tech-stack--architecture) 2. [โครงสร้างโปรเจกต์ (Project Structure)](#2-โครงสร้างโปรเจกต์-project-structure) 3. [Roles & Permissions](#3-roles--permissions) 4. [Authentication — SSO Flow](#4-authentication--sso-flow) 5. [Router Configuration](#5-router-configuration) 6. [Portal Page — หน้าเมนูระบบกลาพร้ง](#6-portal-page--หน้าเมนูระบบกลาง) 7. [Application Step Form (v-stepper)](#7-application-step-form-v-stepper) 8. [Dashboard](#8-dashboard) 9. [Application State Flow](#9-application-state-flow) 10. [Inspection Module](#10-inspection-module) 11. [Certificate Module](#11-certificate-module) 12. [Application Store (Pinia)](#12-application-store-pinia) 13. [Sidebar Navigation](#13-sidebar-navigation) 14. [Vuetify Theme Configuration](#14-vuetify-theme-configuration) 15. [API Endpoints Summary](#15-api-endpoints-summary) 16. [Environment Variables](#16-environment-variables) 17. [Deployment & DevOps Notes](#17-deployment--devops-notes) --- ## 1. Tech Stack & Architecture |Layer |Technology | |------------------|------------------------------------------| |Frontend Framework|Vue 3 (Composition API + ` ``` ----- ## 4. Router Configuration ```typescript // router/index.ts import { createRouter, createWebHistory } from 'vue-router' import { useAuthStore } from '@/stores/auth.store' const routes = [ // ── Auth ── { path: '/login', name: 'Login', component: () => import('@/views/auth/LoginPage.vue'), meta: { layout: 'auth', requiresAuth: false }, }, { path: '/auth/callback', name: 'SsoCallback', component: () => import('@/views/auth/SsoCallbackPage.vue'), meta: { layout: 'auth', requiresAuth: false }, }, // ── Dashboard ── { path: '/', name: 'Dashboard', component: () => import('@/views/dashboard/DashboardPage.vue'), meta: { requiresAuth: true }, }, // ── Application (คำขอ GAP) ── { path: '/applications', name: 'ApplicationList', component: () => import('@/views/application/ApplicationListPage.vue'), meta: { requiresAuth: true }, }, { path: '/applications/new', name: 'ApplicationCreate', component: () => import('@/views/application/ApplicationFormPage.vue'), meta: { requiresAuth: true, roles: ['FARMER', 'GROUP_ADMIN'] }, }, { path: '/applications/group/new', name: 'GroupApplicationCreate', component: () => import('@/views/application/GroupApplicationPage.vue'), meta: { requiresAuth: true, roles: ['GROUP_ADMIN'] }, }, { path: '/applications/:id', name: 'ApplicationDetail', component: () => import('@/views/application/ApplicationDetailPage.vue'), meta: { requiresAuth: true }, }, { path: '/applications/:id/edit', name: 'ApplicationEdit', component: () => import('@/views/application/ApplicationFormPage.vue'), meta: { requiresAuth: true, roles: ['FARMER', 'GROUP_ADMIN'] }, }, // ── Inspection (ตรวจประเมิน) ── { path: '/inspections', name: 'InspectionSchedule', component: () => import('@/views/inspection/InspectionSchedulePage.vue'), meta: { requiresAuth: true, roles: ['staff', 'INSPECTOR', 'ADMIN'] }, }, { path: '/inspections/:id/checklist', name: 'InspectionChecklist', component: () => import('@/views/inspection/InspectionChecklistPage.vue'), meta: { requiresAuth: true, roles: ['INSPECTOR', 'ADMIN'] }, }, { path: '/inspections/:id/result', name: 'InspectionResult', component: () => import('@/views/inspection/InspectionResultPage.vue'), meta: { requiresAuth: true, roles: ['staff', 'INSPECTOR', 'ADMIN'] }, }, // ── Certificate (ใบรับรอง) ── { path: '/certificates', name: 'CertificateList', component: () => import('@/views/certificate/CertificateListPage.vue'), meta: { requiresAuth: true }, }, { path: '/certificates/:id', name: 'CertificateDetail', component: () => import('@/views/certificate/CertificateDetailPage.vue'), meta: { requiresAuth: true }, }, // ── Admin ── { path: '/admin/users', name: 'UserManagement', component: () => import('@/views/admin/UserManagementPage.vue'), meta: { requiresAuth: true, roles: ['ADMIN'] }, }, { path: '/admin/settings', name: 'SystemSettings', component: () => import('@/views/admin/SystemSettingPage.vue'), meta: { requiresAuth: true, roles: ['ADMIN'] }, }, ] const router = createRouter({ history: createWebHistory(), routes, }) // ── Navigation Guard ── router.beforeEach((to, _from, next) => { const auth = useAuthStore() if (to.meta.requiresAuth && !auth.isAuthenticated) { return next({ name: 'Login' }) } if (to.meta.roles && !to.meta.roles.includes(auth.userRole)) { return next({ name: 'Dashboard' }) } next() }) export default router ``` ----- ## 5. Portal Page — หน้าเมนูระบบกลางgi ### 5.1 ภาพรวม Portal หลังจาก Login สำเร็จ ผู้ใช้จะถูก redirect มาที่หน้า Portal (/portal) ซึ่งทำหน้าที่เป็น Single Entry Point สำหรับทุกระบบภายใต้กรมวิชาการเกษตร โดยแสดงเฉพาะระบบที่ผู้ใช้มีสิทธิ์เข้าถึงตาม Role ที่ได้รับ Portal Layout: ┌──────────────────────จัด───────────────────────────────────────────┐ │ 🌿 ระบบรับรองมาตรฐานพืช (Header) [User] [Logout] │ ├─────────────────────────────────────────────────────────────────┤ │ │ │ ยินดีต้อนรับ, [ชื่อผู้ใช้] | บทบาท: [Role] │ │ │ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │ │ GAP │ │ DOA │ │ จดทะเบียน│ │ Health │ │ │ │ Cert. │ │ Factory │ │ ส่งออก │ │ Cert. 1 │ │ │ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │ │ │ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │ │ Health │ │ EL │ │ Admin │ │ │ │ Cert. 2 │ │ System │ │ Backend │ │ │ └──────────┘ └──────────┘ └──────────┘ │ │ │ └─────────────────────────────────────────────────────────────────┘ ### 5.2 System Registry — นิยามระบบทั้งหมด // utils/portal-systems.ts export interface PortalSystem { id: string title: string titleEn: string description: string icon: string color: string routeName: string // ชื่อ route หรือ external URL external?: boolean // true = เปิด tab ใหม่ (microservice อื่น) externalUrl?: string requiredRoles: string[] // [] = ทุก role เข้าได้ badge?: string // ข้อความ badge เช่น "ใหม่", "Beta" } export const PORTAL_SYSTEMS: PortalSystem[] = [ { id: 'gap', title: 'ระบบการรับรองมาตรฐาน GAP', titleEn: 'GAP Certification System', description: 'ยื่นคำขอรับรอง ตรวจประเมินแปลง และออกใบรับรองมาตรฐาน GAP พืช', icon: 'mdi-leaf-circle', color: 'success', routeName: 'Dashboard', requiredRoles: [], // ทุก role }, { id: 'doa-factory', title: 'ระบบการขึ้นทะเบียนโรงงานผลิตสินค้าพืช DOA', titleEn: 'DOA Factory & Certification Body Registration', description: 'ขึ้นทะเบียนโรงงานผลิตสินค้าพืช DOA และหน่วยรับรองโรงงาน (Certification Body: CB)', icon: 'mdi-factory', color: 'primary', routeName: 'DoaFactoryDashboard', requiredRoles: ['FARMER', 'GROUP_ADMIN', 'staff', 'ADMIN'], }, { id: 'export-register', title: 'ระบบจดทะเบียนผู้ส่งออก', titleEn: 'Exporter Registration System', description: 'จดทะเบียนผู้ส่งออกสินค้าเกษตร และต่ออายุใบทะเบียน', icon: 'mdi-truck-delivery', color: 'orange', routeName: 'ExporterDashboard', requiredRoles: ['FARMER', 'GROUP_ADMIN', 'staff', 'ADMIN'], }, { id: 'health-cert-controlled', title: 'ระบบ Health Certificate', titleEn: 'Health Certificate — Controlled Plants', description: 'ออก Health Certificate ตามประกาศพืชควบคุมเฉพาะ', icon: 'mdi-file-certificate', color: 'teal', routeName: 'HealthCertControlledDashboard', requiredRoles: ['FARMER', 'GROUP_ADMIN', 'staff', 'ADMIN'], badge: 'พืชควบคุม', }, { id: 'health-cert-processed', title: 'ระบบ Health Certificate สินค้าเกษตรแปรรูปด้านพืช', titleEn: 'Health Certificate — Processed Agricultural Products', description: 'ออก Health Certificate สำหรับสินค้าเกษตรแปรรูปด้านพืช', icon: 'mdi-file-certificate-outline', color: 'cyan', routeName: 'HealthCertProcessedDashboard', requiredRoles: ['FARMER', 'GROUP_ADMIN', 'staff', 'ADMIN'], badge: 'สินค้าแปรรูป', }, { id: 'establishment-list', title: 'ระบบการควบคุมพิเศษ Establishment List (EL)', titleEn: 'Establishment List Management System', description: 'บริหารจัดการบัญชีรายชื่อโรงคัดบรรจุสินค้าเกษตรเพื่อการส่งออก', icon: 'mdi-format-list-checks', color: 'indigo', routeName: 'EstablishmentListDashboard', requiredRoles: ['staff', 'INSPECTOR', 'ADMIN'], }, { id: 'admin-backend', title: 'ระบบบริหารจัดการผู้ดูแลระบบ (Backend)', titleEn: 'Admin & Open API Management', description: 'บริหารจัดการผู้ใช้งาน สิทธิ์ระบบ และจัดการ Open API', icon: 'mdi-shield-crown', color: 'deep-purple', routeName: 'AdminPortal', requiredRoles: ['ADMIN'], badge: 'Admin', }, ] ### 5.3 Portal Permission Composable // composables/usePortalPermission.ts import { computed } from 'vue' import { useAuthStore } from '@/stores/auth.store' import { PORTAL_SYSTEMS, type PortalSystem } from '@/utils/portal-systems' export function usePortalPermission() { const auth = useAuthStore() const accessibleSystems = computed(() => PORTAL_SYSTEMS.filter(sys => { if (sys.requiredRoles.length === 0) return true return sys.requiredRoles.includes(auth.user?.role ?? '') }) ) const hasAccessTo = (systemId: string) => accessibleSystems.value.some(s => s.id === systemId) return { accessibleSystems, hasAccessTo } } ### 5.4 Portal Page Component mdi-leaf ระบบรับรองมาตรฐานพืช กรมวิชาการเกษตร {{ auth.user?.fullName?.charAt(0) }} {{ auth.user?.fullName }} mdi-chevron-down ยินดีต้อนรับ, {{ auth.user?.fullName }} 👋 บทบาท: {{ roleLabel }} | สิทธิ์เข้าถึง {{ accessibleSystems.length }} ระบบ mdi-leaf-circle mdi-apps ระบบที่คุณสามารถเข้าใช้งาน {{ system.icon }} {{ system.badge }} {{ system.title }} {{ system.description }} mdi-arrow-right-circle เข้าใช้งาน mdi-lock-outline ยังไม่มีสิทธิ์เข้าใช้งานระบบ กรุณาติดต่อผู้ดูแลระบบเพื่อขอสิทธิ์ ``` ----- ## 6. Application Step Form (v-stepper) ### 6.1 หน้า Step Form หลัก ```vue mdi-file-document-edit ยื่นคำขอรับรอง GAP พืช mdi-arrow-left ย้อนกลับ mdi-content-save บันทึกร่าง ถัดไป mdi-arrow-right mdi-send ยื่นคำขอ ``` ### 6.2 Step 1 — ข้อมูลผู้ขอ (ตัวอย่าง Component) ```vue ``` ### 6.3 Step 2 — แปลงปลูก (Dynamic Plot List) ```vue แปลงที่ {{ index + 1 }} mdi-plus เพิ่มแปลง ``` ----- ## 7. Dashboard ### 7.1 Dashboard Layout ```vue Dashboard {{ card.value }} {{ card.title }} mdi-bell การแจ้งเตือนล่าสุด {{ noti.message }} mdi-file-document คำขอล่าสุด ``` ### 7.2 Status Chip Component ```vue {{ statusIcon }} {{ statusLabel }} ``` ----- ## 8. Application State Flow ``` ┌────────┐ ┌─────────│ DRAFT │──────── บันทึกร่าง │ └───┬────┘ │ ยกเลิก │ ยื่นคำขอ ▼ ▼ ┌───────────┐ ┌───────────┐ │ CANCELLED │ │ SUBMITTED │ └───────────┘ └─────┬─────┘ │ เจ้าหน้าที่รับเรื่อง ▼ ┌────────────┐ │ DOC_REVIEW │ ─── ตรวจเอกสาร └─────┬──────┘ เอกสารไม่ผ่าน │ │ เอกสารผ่าน (ส่งกลับแก้ไข)│ ▼ ▲ │ ┌─────────────────────┐ │ │ │ INSPECTION_SCHEDULED │ ─── นัดตรวจแปลง │ │ └──────────┬──────────┘ │ │ ▼ │ │ ┌────────────┐ │ │ │ INSPECTING │ ─── บันทึกผลตรวจ │ │ └─────┬──────┘ │ │ ไม่ผ่าน │ ผ่าน │ │ ┌──────┴──────┐ │ ▼ ▼ ▼ │ ┌──────────┐ ┌──────────┐ │ │ REJECTED │ │ APPROVED │ │ └──────────┘ └────┬─────┘ │ │ ออกใบรับรอง │ ▼ │ ┌─────────────┐ │ │ CERT_ISSUED │ │ └──────┬──────┘ │ │ หมดอายุ │ ▼ │ ┌──────────────┐ └───────────────────────│ CERT_EXPIRED │ ── ยื่นต่ออายุ (loop) └──────────────┘ ``` ----- ## 9. Inspection Module ### 9.1 GAP Checklist Component ```vue mdi-clipboard-check-outline รายการตรวจประเมิน GAP ข้อกำหนด ผลตรวจ หมายเหตุ {{ item.label }} ผ่าน ไม่ผ่าน N/A ``` ### 9.2 GAP Checklist Data (ตัวอย่าง) ```typescript // utils/gap-checklist-template.ts export const GAP_CHECKLIST_TEMPLATE = [ { title: '1. แหล่งน้ำ', items: [ { label: '1.1 แหล่งน้ำไม่มีการปนเปื้อนสารเคมี', result: null, remark: '' }, { label: '1.2 มีระบบการจัดการน้ำอย่างเหมาะสม', result: null, remark: '' }, { label: '1.3 มีการตรวจวิเคราะห์คุณภาพน้ำ', result: null, remark: '' }, ], }, { title: '2. พื้นที่ปลูก', items: [ { label: '2.1 พื้นที่ไม่มีสารปนเปื้อนในดิน', result: null, remark: '' }, { label: '2.2 ไม่อยู่ใกล้แหล่งมลพิษ', result: null, remark: '' }, { label: '2.3 มีการจัดการดินอย่างเหมาะสม', result: null, remark: '' }, ], }, { title: '3. วัตถุอันตรายทางการเกษตร', items: [ { label: '3.1 ใช้สารเคมีตามคำแนะนำ', result: null, remark: '' }, { label: '3.2 มีการเก็บรักษาสารเคมีอย่างปลอดภัย', result: null, remark: '' }, { label: '3.3 ผู้ใช้สารเคมีมีอุปกรณ์ป้องกัน', result: null, remark: '' }, ], }, { title: '4. การจัดการคุณภาพในกระบวนการผลิตก่อนการเก็บเกี่ยว', items: [ { label: '4.1 ใช้พันธุ์พืชที่เหมาะสม', result: null, remark: '' }, { label: '4.2 มีการจดบันทึกการผลิต', result: null, remark: '' }, ], }, { title: '5. การเก็บเกี่ยวและการปฏิบัติหลังเก็บเกี่ยว', items: [ { label: '5.1 เก็บเกี่ยวในระยะเวลาเหมาะสม', result: null, remark: '' }, { label: '5.2 ภาชนะสะอาดและเหมาะสม', result: null, remark: '' }, { label: '5.3 สถานที่เก็บรักษาสะอาดปลอดภัย', result: null, remark: '' }, ], }, { title: '6. การพักผ่อนของสารเคมี', items: [ { label: '6.1 ปฏิบัติตามระยะเวลาหยุดใช้สารเคมีก่อนเก็บเกี่ยว', result: null, remark: '' }, ], }, { title: '7. การบันทึกข้อมูลและการตามสอบ', items: [ { label: '7.1 มีบันทึกการใช้ปัจจัยการผลิต', result: null, remark: '' }, { label: '7.2 สามารถตามสอบได้ตลอดห่วงโซ่', result: null, remark: '' }, ], }, { title: '8. สุขลักษณะส่วนบุคคล', items: [ { label: '8.1 ผู้ปฏิบัติงานมีสุขลักษณะที่ดี', result: null, remark: '' }, { label: '8.2 มีสิ่งอำนวยความสะดวกด้านสุขอนามัย', result: null, remark: '' }, ], }, ] ``` ### 9.3 Photo Upload for Inspection ```vue mdi-camera อัปโหลดรูปภาพการตรวจ ``` ----- ## 10. Certificate Module ### 10.1 Certificate List (v-data-table) ```vue mdi-certificate ใบรับรอง GAP {{ formatDate(value) }} ``` ### 10.2 Certificate PDF Generation ```typescript // utils/pdf-generator.ts import jsPDF from 'jspdf' interface CertData { certNo: string farmerName: string idCard: string cropType: string plotAddress: string area: string issuedDate: string expiryDate: string inspectorName: string approverName: string } export function generateCertificatePdf(data: CertData): jsPDF { const doc = new jsPDF({ orientation: 'landscape', unit: 'mm', format: 'a4' }) // Border doc.setDrawColor(34, 139, 34) doc.setLineWidth(2) doc.rect(10, 10, 277, 190) doc.setLineWidth(0.5) doc.rect(14, 14, 269, 182) // Header doc.setFontSize(24) doc.setTextColor(34, 139, 34) doc.text('ใบรับรองแหล่งผลิต GAP พืช', 148.5, 40, { align: 'center' }) doc.setFontSize(14) doc.text('Certificate of Good Agricultural Practices', 148.5, 50, { align: 'center' }) // Certificate Number doc.setFontSize(12) doc.setTextColor(0, 0, 0) doc.text(`เลขที่ใบรับรอง: ${data.certNo}`, 148.5, 65, { align: 'center' }) // Content doc.setFontSize(11) const startY = 80 const lineHeight = 10 const lines = [ `ขอรับรองว่า ${data.farmerName}`, `เลขบัตรประชาชน: ${data.idCard}`, `ได้ผ่านการตรวจประเมินแปลงผลิตพืช: ${data.cropType}`, `สถานที่ตั้ง: ${data.plotAddress}`, `พื้นที่: ${data.area} ไร่`, `ตามมาตรฐาน GAP (Good Agricultural Practices)`, ] lines.forEach((line, i) => { doc.text(line, 40, startY + i * lineHeight) }) // Dates doc.text(`วันที่ออกใบรับรอง: ${data.issuedDate}`, 40, 155) doc.text(`วันหมดอายุ: ${data.expiryDate}`, 40, 165) // Signatures doc.text('ผู้ตรวจประเมิน', 80, 185, { align: 'center' }) doc.text(data.inspectorName, 80, 192, { align: 'center' }) doc.line(40, 182, 120, 182) doc.text('ผู้อนุมัติ', 220, 185, { align: 'center' }) doc.text(data.approverName, 220, 192, { align: 'center' }) doc.line(180, 182, 260, 182) return doc } ``` ### 10.3 Certificate Store ```typescript // stores/certificate.store.ts import { defineStore } from 'pinia' import { ref } from 'vue' import axios from '@/plugins/axios' import { generateCertificatePdf } from '@/utils/pdf-generator' export const useCertificateStore = defineStore('certificate', () => { const certificates = ref([]) const current = ref(null) async function fetchAll() { const { data } = await axios.get('/certificates') certificates.value = data return data } async function fetchById(id: string) { const { data } = await axios.get(`/certificates/${id}`) current.value = data return data } async function downloadPdf(id: string) { const cert = await fetchById(id) const pdf = generateCertificatePdf(cert) pdf.save(`GAP-Certificate-${cert.certNo}.pdf`) } return { certificates, current, fetchAll, fetchById, downloadPdf } }) ``` ----- ## 11. Application Store (Pinia) ```typescript // stores/application.store.ts import { defineStore } from 'pinia' import { ref, reactive } from 'vue' import axios from '@/plugins/axios' interface DashboardSummary { total: number pending: number approved: number activeCerts: number } export const useApplicationStore = defineStore('application', () => { const summary = reactive({ total: 0, pending: 0, approved: 0, activeCerts: 0 }) const recentList = ref([]) const currentApp = ref(null) async function fetchDashboardSummary() { const { data } = await axios.get('/applications/summary') Object.assign(summary, data) const recent = await axios.get('/applications?limit=10&sort=-submittedAt') recentList.value = recent.data } async function fetchById(id: string) { const { data } = await axios.get(`/applications/${id}`) currentApp.value = data return data } async function saveDraft(form: any) { if (form.id) { await axios.put(`/applications/${form.id}`, { ...form, status: 'DRAFT' }) } else { const { data } = await axios.post('/applications', { ...form, status: 'DRAFT' }) form.id = data.id } } async function submit(form: any) { if (form.id) { await axios.put(`/applications/${form.id}`, { ...form, status: 'SUBMITTED' }) } else { await axios.post('/applications', { ...form, status: 'SUBMITTED' }) } } async function cancel(id: string) { await axios.patch(`/applications/${id}/status`, { status: 'CANCELLED' }) } async function updateStatus(id: string, status: string, payload?: any) { await axios.patch(`/applications/${id}/status`, { status, ...payload }) } return { summary, recentList, currentApp, fetchDashboardSummary, fetchById, saveDraft, submit, cancel, updateStatus } }) ``` ----- ## 12. Sidebar Navigation ```vue {{ auth.user?.fullName?.charAt(0) }} ``` ----- ## 13. Vuetify Theme Configuration ```typescript // plugins/vuetify.ts import { createVuetify } from 'vuetify' import * as components from 'vuetify/components' import * as directives from 'vuetify/directives' import '@mdi/font/css/materialdesignicons.css' import 'vuetify/styles' export default createVuetify({ components, directives, theme: { defaultTheme: 'gapTheme', themes: { gapTheme: { dark: false, colors: { primary: '#2E7D32', // เขียวเกษตร secondary: '#FF8F00', // เหลืองทอง accent: '#00ACC1', success: '#43A047', warning: '#FB8C00', error: '#E53935', info: '#1E88E5', background: '#F5F5F5', surface: '#FFFFFF', }, }, }, }, defaults: { VBtn: { rounded: 'lg' }, VCard: { rounded: 'lg', elevation: 2 }, VTextField: { variant: 'outlined', density: 'comfortable' }, VSelect: { variant: 'outlined', density: 'comfortable' }, }, }) ``` ----- ## 14. API Endpoints Summary |Method |Endpoint |Description |Roles | |-------|--------------------------|------------------------------------|-------------------------| |`POST` |`/auth/token` |Exchange SSO code for token |Public | |`GET` |`/auth/me` |Get current user profile |All | |`GET` |`/applications` |List applications (filtered by role)|All | |`GET` |`/applications/summary` |Dashboard summary counts |All | |`POST` |`/applications` |Create new application |Farmer, GroupAdmin | |`GET` |`/applications/:id` |Get application detail |All | |`PUT` |`/applications/:id` |Update application |Farmer, GroupAdmin | |`PATCH`|`/applications/:id/status`|Update status |staff, Admin | |`GET` |`/inspections` |List inspections |staff, Inspector, Admin| |`POST` |`/inspections` |Schedule inspection |staff, Admin | |`PUT` |`/inspections/:id` |Record inspection result |Inspector, Admin | |`POST` |`/inspections/:id/photos` |Upload inspection photos |Inspector | |`GET` |`/certificates` |List certificates |All | |`GET` |`/certificates/:id` |Get certificate detail |All | |`POST` |`/certificates` |Issue certificate |staff, Admin | |`GET` |`/certificates/:id/pdf` |Download certificate PDF |All | |`GET` |`/users` |List users |Admin | |`POST` |`/users` |Create user |Admin | |`PUT` |`/users/:id` |Update user |Admin | |`GET` |`/notifications` |Get notifications |All | ----- ## 15. Environment Variables ```env # .env VITE_API_BASE_URL=https://api.gap-cert.example.com VITE_SSO_URL=https://sso.example.com VITE_SSO_CLIENT_ID=gap-cert-web VITE_SSO_REDIRECT_URI=http://localhost:3000/auth/callback VITE_APP_TITLE=ระบบรับรองแหล่งผลิต GAP พืช ``` ----- ## 16. Deployment & DevOps Notes |Concern |Recommendation | |------------|------------------------------------| |Build |`vite build` → static SPA in `dist/`| |Hosting |Nginx / CloudFront + S3 | |SPA Fallback|`try_files $uri $uri/ /index.html` | |HTTPS |Required for SSO redirect | |Docker |Multi-stage build (Node → Nginx) | |CI/CD |GitHub Actions / GitLab CI | |Linting |ESLint + Prettier + vue-tsc | |Testing |Vitest (unit) + Cypress (E2E) |
บทบาท: {{ roleLabel }} | สิทธิ์เข้าถึง {{ accessibleSystems.length }} ระบบ
{{ system.description }}
ยังไม่มีสิทธิ์เข้าใช้งานระบบ
กรุณาติดต่อผู้ดูแลระบบเพื่อขอสิทธิ์