# ระบบจัดการ (admin)

::: info สิ่งที่จะได้เรียนรู้
วิธีปรับระบบจัดการให้ทีมใช้ง่าย: กลุ่มเมนู badge และการปักหมุด, command palette (⌘K) และคีย์ลัด, หน้าแก้ไขที่มีแท็บ
ส่วนที่พับได้ แถว และ field ที่แสดงเฉพาะเมื่อเกี่ยวข้อง

**อ่านก่อนหน้านี้:** [การตั้งค่า](./configuration) และ [Fields](./fields)
:::

ทุกอย่างตั้งใน config ระบบจัดการไม่ต้องตั้งค่าแยก แต่ละคนยังพับกลุ่ม ปักหมุด และหุบเมนูได้เอง ค่าเหล่านี้เก็บไว้ในเบราว์เซอร์ของแต่ละคน

## เมนู {#the-menu}

<Screenshot name="dashboard" alt="ระบบจัดการที่เมนูเป็นกลุ่ม: เนื้อหา คลังสื่อ ฟอร์ม และตั้งค่า" />

เมนูเป็นกลุ่มที่พับได้ ลึก 2 ชั้น:

```
แดชบอร์ด
เนื้อหา            บทความ หน้า… (collection ที่ไม่ได้ระบุกลุ่ม)
คลังสื่อ
ร้านค้า             กลุ่มของ plugin เช่น ร้านค้า
  แคตตาล็อก          สินค้า สินค้าย่อย…
  การขาย            คำสั่งซื้อ การชำระเงิน…
ตั้งค่า
  เว็บไซต์           global, redirects
  ผู้ใช้และสิทธิ์      ผู้ใช้ บทบาท API keys SSO
  ระบบ              Backups อีเมล การส่ง งานเบื้องหลัง Audit log
```

collection, global และหน้าระบุกลุ่มของตัวเองด้วย `admin.group` ส่วนกลุ่มของคุณเองประกาศใน `admin.nav`:

```ts
export default defineConfig({
  admin: {
    nav: [
      {
        id: 'library',
        label: { en: 'Library', th: 'ห้องสมุด' },
        icon: 'book-open',
        children: [{ id: 'authors', label: { en: 'Authors', th: 'ผู้เขียน' } }],
      },
    ],
  },
  collections: [
    { slug: 'books', admin: { group: 'library' }, fields: [/* … */] },
    { slug: 'people', admin: { group: 'library.authors' }, fields: [/* … */] },
  ],
  globals: [{ slug: 'footer', admin: { group: 'content' }, fields: [/* … */] }],
})
```

- ถ้าไม่ระบุ `group` collection อยู่ใต้ **เนื้อหา** และ global อยู่ใต้ **ตั้งค่า › เว็บไซต์** ส่วน `group: 'settings'` ก็คือ
  ตั้งค่า › เว็บไซต์
- ใส่ label แทน id (`group: 'Library'`) จะสร้างกลุ่มชื่อนั้นให้ เหมือนเดิม
- `group: false` เอา collection, global หรือหน้าออกจากเมนู แต่ยังเข้าถึงได้ผ่านลิงก์
- `order` กำหนดตำแหน่งของกลุ่ม: เนื้อหาคือ 0, คลังสื่อ 10, กลุ่มของ plugin ราว 100–200, ตั้งค่า 1000 ส่วนภายในกลุ่ม
  `admin.order` ของ collection, global หรือหน้ากำหนดลำดับ (ค่าน้อยมาก่อน) ที่ไม่ได้ตั้งจะตามมาตามลำดับใน config
- ประกาศ id ของกลุ่มที่ plugin หรือ Easy CMS มีอยู่แล้ว (`{ id: 'shop', label: 'Store' }` หรือ `content`) เพื่อเปลี่ยนชื่อ ไอคอน หรือลำดับ
- แต่ละคนเห็นเฉพาะสิ่งที่ตัวเองเปิดได้ และกลุ่มที่ว่างจะไม่แสดง

### ตัวเลขและ badge {#numbers-and-badges}

collection แสดงจำนวนเอกสาร ปิดได้ด้วย `admin.count: false` ส่วน **badge** แสดงสิ่งที่ต้องจัดการแทน นับตามสิ่งที่แต่ละคนอ่านได้:

```ts
{
  slug: 'orders',
  admin: {
    badge: { where: { status: { equals: 'paid' } }, label: { en: 'to send', th: 'รอจัดส่ง' } },
  },
}
```

กลุ่มที่พับอยู่แสดงผลรวมของ badge ในกลุ่ม `tone` คือ `accent` (ค่าเริ่มต้น), `warning` หรือ `danger`

### สิ่งที่แต่ละคนปรับเอง {#what-each-person-changes}

- **พับกลุ่ม**: กลุ่มจะคงสภาพตามที่พับไว้ ส่วนกลุ่มของหน้าที่เปิดอยู่จะกางเสมอ
- **ปักหมุด**: ปุ่มหมุดข้างชื่อ collection เอาไปไว้บนสุดของเมนู
- **หุบเมนู**: บนจอกว้าง เมนูหุบเหลือไอคอนได้ (ปุ่มด้านล่างเมนู หรือ `[`) กดไอคอนกลุ่มแล้วรายการจะแสดงข้างๆ
- **สร้าง**: ชี้ที่รายการ (หรือกด Tab ไปถึง) จะมีปุ่ม **+** สำหรับสร้างเอกสาร

ใช้ลูกศรขึ้นลงเลื่อนในเมนูได้ ซ้ายและขวาพับหรือกางกลุ่ม

## ค้นหาและคำสั่ง (⌘K) {#search-and-commands}

<Screenshot name="command-palette" alt="Command palette: หน้า เอกสาร และคำสั่งที่พบระหว่างพิมพ์" />

`⌘K` (`Ctrl+K`), `/` หรือช่องค้นหาด้านบนของเมนู เปิด command palette:

- **ไปที่** collection, global, หน้า หรือการตั้งค่าใดก็ได้ ตามชื่อหรือกลุ่ม
- **สร้าง** เอกสารของ collection ที่คุณสร้างได้
- **เอกสาร**: ค้นชื่อเรื่องในทุก collection ที่คุณอ่านได้ และค้นด้วย id
- **เปิดล่าสุด**: เอกสารที่เปิดล่าสุด แสดงก่อนเริ่มพิมพ์
- **คำสั่ง**: เปลี่ยนธีม ภาษา หุบเมนู ดูคีย์ลัด ออกจากระบบ และคำสั่งของคุณเอง:

```ts
admin: {
  commands: [{ label: 'ร่างที่รอตรวจ', href: '/collections/posts?status=draft', icon: 'file-text' }],
}
```

| คีย์ลัด | |
|---|---|
| `⌘K`, `/` | ค้นหาและคำสั่ง |
| `⌘S` | บันทึก (ไม่เผยแพร่ร่าง และไม่เอาหน้าออกจากเว็บ) |
| `⌘⇧P` | เผยแพร่ |
| `[` | หุบหรือขยายเมนู |
| `g` แล้ว `d` | แดชบอร์ด |
| `?` | คีย์ลัดทั้งหมด |

## หน้าแก้ไข {#edit-pages}

`admin.layout` จัดหน้าแก้ไขตามชื่อ field เป็นแท็บ ส่วนที่พับได้ และแถว เปลี่ยนแค่หน้าตา ข้อมูลยังเป็นตาม field เหมือนเดิม:

```ts
{
  slug: 'products',
  admin: {
    layout: [
      { tab: 'ข้อมูล', fields: ['title', 'slug', { row: ['brand', 'category'] }, 'description'] },
      {
        tab: 'การจัดส่ง',
        fields: [
          { row: ['weight', 'width', 'height'] },
          { collapsible: 'ศุลกากร', collapsed: true, fields: ['hsCode', 'origin'] },
        ],
      },
    ],
  },
  fields: [
    { name: 'title', type: 'text' },
    { name: 'brand', type: 'text', admin: { width: '1/3' } },
    { name: 'category', type: 'relationship', to: 'categories', admin: { width: '2/3' } },
    { name: 'hsCode', type: 'text', admin: { description: 'รหัสพิกัดศุลกากร' } },
    // …
  ],
}
```

- ใช้กับ field ชั้นบนสุด แต่ละ field วางได้ครั้งเดียว field ที่ไม่ได้วางจะต่อท้าย (ในแท็บแรก) ส่วน field ที่ `admin: { position: 'sidebar' }`
  ยังอยู่ในแถบข้าง
- ถ้ามีแท็บ ทุกรายการชั้นบนสุดต้องเป็นแท็บ แท็บแสดงจำนวน field ที่ผิดในแท็บนั้น
- `row` วาง field ข้างกัน แบ่งเท่ากันหรือตาม `admin.width` (`'1/4'`…`'full'`) บนจอแคบจะเรียงลงมา
- `collapsible` คือส่วนที่พับได้ `collapsed: true` เริ่มแบบพับไว้
- `admin.description` ของ field เพิ่มคำอธิบายใต้ field
- [plugin SEO](./seo) ย้าย field ของตัวเองไปที่แท็บ **SEO** (ตั้ง `tab: false` ถ้าอยากให้อยู่ใต้ field อื่นเหมือนเดิม)

### เงื่อนไข {#conditions}

`admin.condition` แสดง field เฉพาะเมื่อ field ข้างเคียงตรงเงื่อนไข:

```ts
fields: [
  { name: 'linkType', type: 'select', options: ['internal', 'external'] },
  { name: 'page', type: 'relationship', to: 'pages', admin: { condition: { field: 'linkType', equals: 'internal' } } },
  {
    name: 'url',
    type: 'text',
    required: true,
    admin: { condition: { field: 'linkType', equals: 'external' } },
  },
]
```

มี `equals`, `not_equals`, `in: [...]`, `not_in: [...]` และ `exists: true | false` (ชื่อเดียวกับใน `where`) รวมกันได้ด้วย `and`, `or` และ `not` ส่วน `field` คือ field
ข้างเคียง (ใน group หรือแถวของ array คือ field ที่อยู่ด้วยกัน) หรือ path เข้าไปข้างใน (`link.type`) server ใช้เงื่อนไขเดียวกัน: field ที่ถูกซ่อน
ไม่ถูกบังคับกรอก และค่าเดิมยังอยู่

### การบันทึก {#saving}

- บันทึกไม่ผ่าน: ด้านบนของฟอร์มสรุปทุกช่องที่ต้องแก้ เปิดแท็บหรือส่วนของช่องแรก และย้ายโฟกัสไปที่นั่น
- การแก้ไขที่ยังไม่บันทึกเก็บไว้ในเบราว์เซอร์ระหว่างพิมพ์ เปิดเอกสารนั้นอีกครั้งจะถามว่าจะกู้คืนไหม
- `⌘S` บันทึก `⌘⇧P` เผยแพร่

### แถบข้าง {#the-side-column}

กล่องของ plugin (`admin.sidebar`) อยู่ใต้ field ของแถบข้าง ตั้ง `position: 'top'` เพื่อวางไว้ใต้ส่วนเผยแพร่ เช่น ปุ่มของคำสั่งซื้อ:

```ts
admin: { sidebar: [{ tag: 'ecms-order-actions', position: 'top' }] }
```

## หน้ารายการ {#lists}

- กลับจากหน้าเอกสารแล้วรายการเหมือนเดิม ทั้งคำค้น ตัวกรอง หน้า และตำแหน่ง scroll
- `admin.empty` บอกว่า collection นี้ใช้ทำอะไรเมื่อยังไม่มีเอกสาร:

```ts
admin: {
  empty: {
    description: 'เพิ่มสินค้าแรก: ตั้งชื่อ ใส่ราคา แล้วเผยแพร่',
    link: { label: 'คู่มือร้านค้า', href: 'https://example.com/docs/shop' },
  },
}
```

### ตัวกรอง {#filters}

**ตัวกรอง** เปิดแผงเงื่อนไขที่เอกสารต้องตรงทุกข้อ เช่น ชื่อเรื่องมีคำที่ค้น ตัวเลขอยู่ระหว่างสองค่า วันที่ก่อน หลัง
ตรงกับ หรืออยู่ระหว่างวันที่ ตัวเลือกของ select หรือเอกสารที่เชื่อมไว้ที่เป็นอันใดอันหนึ่ง หรือช่องที่ว่าง แต่ละเงื่อนไขแสดงเป็นชิป
เหนือรายการ คลิกเพื่อแก้เงื่อนไข คลิก × เพื่อเอาออก

- วันที่นับเป็นทั้งวันตาม `timeZone` (ดู[การตั้งค่า](../reference/config)) ไม่ว่าจะเปิดหน้าแอดมินจากที่ไหน
  **วันนี้**, **7 วันล่าสุด** และ **30 วันล่าสุด** ใส่วันที่ให้ตอนที่เลือก
- เงื่อนไขอยู่ในที่อยู่ของหน้าในรูปแบบที่ [REST API](./rest-api) อ่าน ส่งลิงก์ไปแล้วคนเปิดเห็นรายการเดียวกัน:
  `/admin/collections/posts?where[and][0][category][in]=3&where[and][1][publishedAt][gte]=…`
  ลิงก์แบบเดิม (`?status=draft`, `?f_form=3`) ยังใช้ได้
- เมนูสถานะ **ของฉัน** และเมนูของช่อง select (ไม่เกินสามช่อง) เพิ่มเงื่อนไขของตัวเองรวมกับเงื่อนไขอื่น
- ช่องใน array หรือ blocks, rich text และ JSON กรองไม่ได้ ถ้าไม่อยากให้กรองช่องอื่นด้วย ตั้ง
  `admin: { filter: false }` ที่ช่องนั้น
- collection ใหญ่ ควรใส่ index (`index: true`) ให้ช่องที่กรองบ่อย

### ปฏิทิน {#calendar}

<Screenshot name="calendar" alt="บทความบนปฏิทินรายเดือน สีตามหมวดหมู่ และบทความของวันนี้พร้อมสถานะในการ์ดด้านข้าง" />

collection ที่มี `admin.calendar` จะมีปุ่ม **รายการ | ปฏิทิน** เหนือหน้ารายการ:


```ts
admin: {
  calendar: { start: 'startsAt', end: 'endsAt', color: 'category' },
}
```

- มีมุมมองเดือน สัปดาห์ วัน และกำหนดการ แสดงตามเขตเวลาของแอดมิน (บอกชื่อเขตไว้เหนือปฏิทิน) มุมมองและวันที่ที่ดูอยู่
  เก็บในที่อยู่ของหน้า ส่งลิงก์ไปแล้วเปิดเห็นปฏิทินเดียวกัน
- ใช้ตัวกรองและคำค้นของหน้ารายการ ถ้าช่วงที่ดูมีเกิน 500 รายการ จะแสดง 500 รายการแรกและบอกให้กรองเพิ่ม
- คลิกเอกสารเพื่อเปิด คลิกวัน (หรือตัวเลขวันด้วยคีย์บอร์ด) เพื่อดูวันนั้นในการ์ดข้างปฏิทิน ซึ่งแสดงเอกสารเรียงตามเวลา
  รายการทั้งวันขึ้นก่อน พร้อมสถานะ ปุ่ม **สร้างในวันนี้** ในการ์ดสร้างเอกสารที่เริ่มวันนั้น เช่นเดียวกับการเลือกช่วงเวลาในมุมมอง
  สัปดาห์หรือวัน (ถ้ามี `drafts` จะสร้างเป็นฉบับร่าง) วันที่เลือกอยู่ในที่อยู่ของหน้าด้วย (`&day=`)
- เอกสารที่เริ่มเที่ยงคืนและไม่มีวันจบ (หรือจบเที่ยงคืน) แสดงเต็มวัน ถ้าไม่ได้ตั้ง `end` เอกสารอื่นใช้เวลาหนึ่งชั่วโมง
- `color` ให้ค่าแต่ละค่าของ field select หรือ relationship มีสีของตัวเอง
- ลากเอกสารไปวันหรือเวลาอื่นได้ หรือลากปลายเพื่อเปลี่ยนเวลาจบในมุมมองสัปดาห์และวัน (ถ้ามี `end`) ปุ่ม **ย้ายไปวันที่…**
  ในการ์ดของวันทำแบบเดียวกันด้วยคีย์บอร์ด โดยคงความยาวเดิม จะมีข้อความพร้อมปุ่ม **เลิกทำ** อยู่ 8 วินาที ถ้า server
  ไม่รับการบันทึก เอกสารจะกลับที่เดิมพร้อมบอกเหตุผล การย้ายบันทึกเหมือนในหน้าแก้ไข ถ้ามี drafts และ versions
  เอกสารที่มีฉบับร่างค้างอยู่จะย้ายในฉบับร่าง และหน้าเว็บเปลี่ยนเมื่อเผยแพร่ การย้ายต้องมีสิทธิ์แก้ไข

ปฏิทินโหลดเมื่อมีคนเปิดใช้เท่านั้น

### หน้าปฏิทิน {#calendar-page}

ถ้ามี collection ที่ตั้ง `admin.calendar` หรือ `schedule` เมนูจะมี **ปฏิทิน** ต่อจากกลุ่มเนื้อหา แสดงเอกสารของ
collection เหล่านั้นแยกสีกัน และสิ่งที่ตั้งเวลาเผยแพร่หรือเลิกเผยแพร่ไว้ (แสดงเป็นกรอบเส้นประ) ทั้งเว็บ การ์ด **แสดง** ข้างปฏิทินใช้ซ่อนบางอย่าง
(จำไว้ในเบราว์เซอร์ของแต่ละคน) ส่วนการ์ดของวันแสดงวันที่เลือก เริ่มต้นเป็นวันนี้
การลากการตั้งเวลาเผยแพร่ (หรือ **ย้ายไปวันที่…**) ตั้งเวลาใหม่ตามที่ย้าย สำหรับผู้มีสิทธิ์เผยแพร่ คลิกเพื่อเปิดเอกสาร ถ้าจะสร้างเอกสารจากวันที่ ให้ใช้โหมดปฏิทินของ collection นั้น

ทุกคนที่ login เห็นหน้านี้ แต่เห็นเฉพาะสิ่งที่อ่านได้ (การตั้งเวลาเผยแพร่: เฉพาะที่แก้ไขได้) ถ้าใช้ `auth.rbac` ให้สิทธิ์บทบาทได้ที่
ตั้งค่า → บทบาท (`calendar`) ส่วน admin เห็นเสมอ

## เขตเวลา {#time-zone}

ถ้าตั้ง `timeZone` ใน config (ดู[การตั้งค่า](../reference/config)) หน้าแอดมินจะแสดงวันเวลาทั้งหมดตามเขตนั้น และช่องกรอกวันที่
การตั้งเวลาเผยแพร่ และตัวกรองวันที่ของหน้ารายการก็อ่านค่าที่กรอกตามเขตนั้นด้วย โพสต์ที่ตั้งไว้ 09:00 จะเป็น 09:00 สำหรับทุกคน
ไม่ว่าเบราว์เซอร์จะอยู่ที่ไหน คนที่อยู่ต่างเขตจะเห็นชื่อเขตเวลาข้างช่องกรอกวันที่ ("เขตเวลา: เวลาอินโดจีน (GMT+7)") ถ้าไม่ได้ตั้ง
`timeZone` หน้าแอดมินใช้เขตเวลาของเบราว์เซอร์แต่ละเครื่อง

## ขั้นต่อไป {#next-steps}

- [Plugins](./plugins): ให้ collection และหน้าของ plugin มีกลุ่มของตัวเอง
- [Fields](./fields): field ที่ layout เหล่านี้จัดวาง
- [บทบาทและสิทธิ์](./roles): สิ่งที่แต่ละคนเห็นในเมนู
