# Member tags

Staff-only club tags on people and the people filter.

Source: https://docs.champagne.dev-ukab.fr/en/graphql/person-tags

Each club keeps its own catalog of tags. A tag has a `label`, unique per club ignoring case,
and a `color` from a fixed palette. Tags are applied to any person of the club (members,
prospects, guests) and are visible to staff only: member and applicant sessions always read
an empty `tags` list, and the member site never shows them.

## Permissions [#permissions]

| Operation                                               | Permission            |
| ------------------------------------------------------- | --------------------- |
| `personTags`                                            | Any staff of the club |
| `createPersonTag`, `updatePersonTag`, `deletePersonTag` | `tags_manage`         |
| `setPersonTags`                                         | `tags_manage`         |

Club administrators hold this permission. Staff accounts receive it individually from the
team page.

## List tags [#list-tags]

```graphql
query PersonTags($clubId: ID! = "51000000-0000-4000-8000-000000000051") {
  personTags(clubId: $clubId) {
    id
    label
    color
    personCount
  }
}
```

Tags are returned ordered by label, ignoring case. `personCount` is the number of people of
the club carrying the tag. `color` is one of the 14 palette names: `gray`, `red`, `orange`,
`amber`, `yellow`, `lime`, `green`, `emerald`, `teal`, `cyan`, `blue`, `indigo`, `violet` or
`pink`.

## Manage the catalog [#manage-the-catalog]

```graphql
mutation CreatePersonTag($input: CreatePersonTagInput!) {
  createPersonTag(input: $input) {
    tag {
      id
      label
      color
      personCount
    }
    error
  }
}
```

```json
{
  "input": {
    "clubId": "51000000-0000-4000-8000-000000000051",
    "label": "VIP",
    "color": "orange"
  }
}
```

`UpdatePersonTagInput` takes `clubId`, `id`, `label` and `color`; renaming keeps every
assignment. `deletePersonTag(clubId, id)` removes the tag from every person at once. Each
result returns the tag or a user-facing `error` string:

* `Another tag already uses this label.` — labels are compared trimmed and ignoring case.
* `This tag is unavailable.` — the tag does not belong to the club or was deleted.

Labels contain 1–24 characters without line breaks; `color` defaults to `gray`.

## Tag people [#tag-people]

```graphql
mutation SetPersonTags($input: SetPersonTagsInput!) {
  setPersonTags(input: $input) {
    person {
      id
      tags {
        id
        label
        color
      }
    }
    error
  }
}
```

```json
{
  "input": {
    "clubId": "51000000-0000-4000-8000-000000000051",
    "personId": "00000000-0000-4000-8000-000000000001",
    "tagIds": ["7a000000-0000-4000-8000-000000000001"]
  }
}
```

`setPersonTags` replaces the complete tag list of one person and returns the refreshed
`PersonDetail`. An unknown tag or a person outside the club returns an `error` and changes
nothing. Every change is recorded in the staff activity as `person.tags_updated` with the
previous and new tag labels. Tags are only edited from a person's profile; the people list
displays them and filters on them.

## Read tags on people [#read-tags-on-people]

`Person.tags` and `PersonDetail.tags` return `[PersonTagSummary!]!` (`id`, `label`,
`color`) ordered by label. `peoplePage(tagIds: [ID!])` keeps the people carrying **at
least one** of the given tags; it requires `clubId` and accepts up to 50 tags. The back
office stores the filter in the URL as repeated `tag` parameters. `peoplePage(untagged: true)`
keeps only people carrying no tag at all (URL `untagged=1`); combined with `tagIds` it
matches nobody.

```graphql
query TaggedPeople($clubId: ID!, $tagIds: [ID!]) {
  peoplePage(clubId: $clubId, tagIds: $tagIds, page: 1, pageSize: 10) {
    items {
      id
      firstName
      lastName
      tags {
        id
        label
        color
      }
    }
    total
  }
}
```

## Validation errors [#validation-errors]

Invalid IDs, labels, colors and oversized tag or people lists are rejected with
`BAD_USER_INPUT` before any write. Business outcomes such as duplicate labels or unavailable
tags are returned in the `error` field with a null record.
