# The SatisfactionSurveys kit — asking the person a piece of work served how it went

> `Osysharp.SatisfactionSurveys` asks the person a piece of work was for "how did we do?" — one good-or-bad answer and, if they want, a comment. They answer from the mail with no account, or signed in on the app's own page; the database holds one answer per piece of work and person, which they can change for a while. The app decides what is asked and when; the kit applies each answer after the commit, raises `FeedbackAnswered`, and sums the answers up week by week.

<!-- id: ui-feedback-kit · area: ui · stability: preview · html: https://osysharp.com/reference/ui/feedback-kit/ -->

## Summary        {#summary}
`use Osysharp.SatisfactionSurveys@0;` asks people how it went. Ask when the work is done — about the work itself, any record the
app keeps:

```osy syntax
app.Feedback = new FeedbackSetup { AppName = "Spoke & Chain" };

AskForFeedback(repair, "Your city bike, full service", person: repair.Customer, address: repair.Email);
FeedbackPrompt(repair.Id);      // on the repair's own page, for its signed-in customer
```

Who may read what was said about the work, and where it opens for the team, are the work's own facts — its
`security { }` and the page that [[stdlib-records#opens|`[Opens]`]] it — so the app tells the kit nothing about them.

The person gets a mail with **Good** and **Bad** — one click is their answer — and a page to add a comment, with no
account. Signed in, they answer on the work's own page.

## Signature      {#signature}
```osy syntax
use Osysharp.SatisfactionSurveys@0;            // with Osysharp.Identity@0, Osysharp.Mail@0, Osysharp.EmailTemplates@0,
                                    // Osysharp.Workflow and Osysharp.Ui

policy ReadsFeedback => <who reads answers and results>;          // REQUIRED
policy ManagesFeedback => <who sets whether and when to ask>;     // REQUIRED — the settings page

app.Feedback = new FeedbackSetup { AppName = "" };

[On] void Heard(FeedbackAnswered e);   // after the commit, as the app — e.Ask (its About, Rating, Comment), e.Change

// `about` is any record the app keeps (an Entity):
FeedbackAsk? AskForFeedback(Entity about, string title, Principal? person = null, string address = "",
                            string language = "", Principal? by = null, bool mail = true)   // null when switched off
int WithdrawFeedback(Entity about)                  // the work was opened again — unanswered questions are taken back
FeedbackRating? FeedbackRatingOf(Entity about)      // what the person last said — the signal for code that acts on it
List<FeedbackAnswerLine> FeedbackAnswersAbout(Entity about)
FeedbackSummary SummariseFeedback(int weeks = 12)

FeedbackLinks? TakeFeedbackMail(FeedbackAsk? ask)   // put the question in a mail the app sends anyway
FeedbackMailQuestion(FeedbackLinks links)           // …the [Email] part that carries it

FeedbackPrompt(Guid about)                          // the question on the work's page, for its signed-in person
FeedbackResults(int weeks = 12)                     // the team's results: answered, % good, week by week, comments
// pages: /feedback/{link} and /feedback/{link}/{good|bad} (FeedbackByLinkPage, FeedbackChoicePage) — take them over
//        by name to put them in your frame, with FeedbackByLinkPanel(link, choice, showName: false)
// settings: /settings/feedback (FeedbackSettingsPage) — ask or not, ask after, open for, can be changed for, keep for
```

## Description    {#description}

### One answer per piece of work and person   {#one-answer}
A question is one row per piece of work and person — the database holds it (`[Unique(About, PersonKey)]`) — and its
answer is on it. Its `About` is an [`Entity`](https://osysharp.com/reference/types/entity/): a reference to the work, whatever its type, read as its
reader. Asking again never makes a second question: an unanswered one stays open for its full time
again, and an answered one can be changed again. A person is their account when they have one, else the address the
question was mailed to.

### Who answers   {#answer}
Only the person it was asked of: signed in as that account, or holding the link mailed to them (a
[grant](https://osysharp.com/reference/security/grants/) for that one question, which reads it and nothing else in the app). An answer is a reply the
person makes; the kit applies it **after the commit**, checks the windows again as of the moment it was given, and
then raises [`FeedbackAnswered`](https://osysharp.com/reference/function/event/) — heard by any [[function-on|`[On]`]] handler, as the app, so it can write on the
work's own record or tell the team, which a person who is not signed in could not. The event says itself ("Olivia said
it did not go well."), so telling the team is `NotifyAll(team, e)` ([The Notifications kit — telling a person that something needs them](https://osysharp.com/reference/ui/notifications-kit/)) and a record's timeline
shows it as it is ([The RecordTimeline kit — what happened to a record, on its own timeline](https://osysharp.com/reference/ui/activity-kit/)). Nobody else writes an answer, the app's staff included.
SECURITY-INVARIANTs `feedback-one-answer-per-subject-per-person` and `feedback-is-answered-only-by-its-person`.

### What the person sees   {#states}
One card, in five states: **open** (Good and Bad, one click), **answered** (thank you, room for a comment, change your
mind while you may), **settled** (what they said, no longer changeable), **closed** (never answered, too late now) and
**no longer asked** (the work was opened again). A link from one of your mails that has run out, or was never yours,
is the app's `InvalidLinkSurface`.

### When it is asked, and how long it lasts   {#settings}
Staff set it on a declared settings page ([[Settings] and [Setting]: a settings section declared on its row, each field drawn by its type](https://osysharp.com/reference/ui/settings-declared/)): whether to ask at all, how long after the work
the question is mailed ("Ask after"; zero lets a mail the app sends anyway carry it — `TakeFeedbackMail`), how long it
stays open (at most 180 days), how long an answer can be changed (at most 90 days), and how long questions and answers
are kept (30 days to 10 years; an hourly sweep deletes the rest).

### Results   {#results}
`FeedbackResults()` (and `SummariseFeedback(weeks)`) counts what was asked and answered over the last weeks, the share
that said good, good and bad week by week, and the latest comments — each comment only for a reader who may read the work it is about (the work is read as the
reader, all of it together), linked to where the work opens ([`Records.Link`](https://osysharp.com/reference/stdlib/records/)).

### Personal data   {#privacy}
The address a question went to is `Personal.Contact`; what the work was and what a person wrote are `Personal.Content`.
With `Osysharp.Privacy`, `Osysharp.Privacy.SatisfactionSurveys` — which arrives by itself ([Bridges — a package that arrives when an app uses the two it completes](https://osysharp.com/reference/project/bridges/)) — finds a person's questions and answers by their account or
an address their request proved, and counts, copies, keeps restricted and erases them.

### What is not here   {#not-here}
One question, good or bad. A 0–10 recommendation score, an effort score or a survey of several questions are other
shapes of question: build them beside this kit, not into it.

## Examples       {#examples}

### A cycle workshop asking about repairs   {#example-repairs}
```osy title="asking when a repair is done, hearing the answer, the question on the repair's page" test app=ui-feedback-kit
app Workshop {
  use Osysharp.Identity@0;
  use Osysharp.SatisfactionSurveys@0;
  use Osysharp.Mail@0;
  use Osysharp.EmailTemplates@0;
  use Osysharp.Workflow;
  use Osysharp.Ui;
  model "model/**/*.osy";
}

using Osysharp.Identity;
using Osysharp.SatisfactionSurveys;

[Role] enum ShopRole { Authenticator, Customer, Mechanic }
policy IsMechanic => RoleGrant.Any(g => g.Member == user && g.Role == ShopRole.Mechanic);
policy ReadsFeedback => IsMechanic;
policy ManagesFeedback => IsMechanic;

[Title(Bike)]
entity Repair {
  [Required, MaxLength(120)] string Bike;
  Principal? Customer;
  [MaxLength(320)] string Email = "";
  bool Done;
  [MaxLength(20)] string Answer = "";
  security {
    allow read, create, update when IsMechanic;
    allow read when IsAuthenticated where Customer != null && Customer == user;
  }
}

// Mechanics ask from their own requests.
partial entity FeedbackAsk { security { allow create, update when IsMechanic; } }

app.Feedback = new FeedbackSetup { AppName = "Spoke & Chain" };

/// What the workshop does as customers answer: the answer is noted on the repair.
[On]
void RepairAnswered(FeedbackAnswered e) {
  if (e.Ask.About is Repair r) {
    r.Answer = e.Ask.Rating == FeedbackRating.Bad ? "bad" : "good";
  }
}

/// A mechanic finishes a repair, and its customer is asked how it went.
public void FinishRepair(Repair r) {
  r.Done = true;
  AskForFeedback(r, "Your " + r.Bike, person: r.Customer, address: r.Email, by: Session.CurrentUser);
  UnitOfWork.Commit();
}

[Route("/repairs/{id}")] [Opens(Repair)] [Authorize] [Render(CSR)]
component RepairPage(Guid id) {
  live var repairs = Repair.Where(r => r.Id == id).ToList();
  render {
    Stack(gap: 4) {
      foreach (var r in repairs) {
        Text(r.Bike, fontWeight: FontWeight.Semibold);
        if (r.Done) { FeedbackPrompt(r.Id); }
      }
    }
  }
}
```

## See also       {#see-also}
- [Records — what any record is called, where it opens, who may read it](https://osysharp.com/reference/stdlib/records/) — who may read the work, and where it opens, for any record
- [Entity](https://osysharp.com/reference/types/entity/) — `Entity`, the reference a question's `About` is
- [grants (a link that opens one row)](https://osysharp.com/reference/security/grants/) — the link a question is mailed with
- [[Settings] and [Setting]: a settings section declared on its row, each field drawn by its type](https://osysharp.com/reference/ui/settings-declared/) — the settings page the kit declares
- [The Notifications kit — telling a person that something needs them](https://osysharp.com/reference/ui/notifications-kit/) — telling the team when an answer is bad
- [[Email] — mail rendered from data](https://osysharp.com/reference/ui/email/) — the question mail, and putting the question in a mail of your own
- [The Privacy kit — erasure that keeps only what the company must or may](https://osysharp.com/reference/security/privacy-kit/) — a person's answers in their privacy requests
- **`kits/satisfaction-surveys/tests/`** — every behaviour above as a test, the screens under `--pixels`
