Osy#betaRun it on Osyrin: osyrin.com

Reference / UI

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

use Osysharp.SatisfactionSurveys@0; · policy ReadsFeedback => …; policy ManagesFeedback => …; · app.Feedback = new FeedbackSetup { AppName = … }; · [On] void Heard(FeedbackAnswered e) · AskForFeedback(repair, title, person:, address:) · WithdrawFeedback(repair) · FeedbackRatingOf(repair) · TakeFeedbackMail(ask) · FeedbackPrompt(repair.Id) · FeedbackResults()

`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.

preview1 example compiled by CIuikitfeedbackmail

Source on GitHub: Osysharp.Privacy.SatisfactionSurveys — github.com/osysharp/privacy/satisfactionsurveys ↗ · Osysharp.SatisfactionSurveys — github.com/osysharp/satisfactionsurveys ↗

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:

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#

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#

One answer per piece of work and person#

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: 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#

Only the person it was asked of: signed in as that account, or holding the link mailed to them (a grant 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 — 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) and a record's timeline shows it as it is (The RecordTimeline kit — what happened to a record, on its own timeline). 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#

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#

Staff set it on a declared settings page ([Settings] and [Setting]: a settings section declared on its row, each field drawn by its type): 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#

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).

Personal data#

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) — 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#

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#

A cycle workshop asking about repairs#

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#

Related

Records — what any record is called, where it opens, who may read it

`Records.*` answers facts about a row of ANY type from what the app declared — what it is called (`[Title]`), where it…

Entity

`Entity` is a reference to a row of ANY type the app keeps — what a notification, a comment or a question "about some…

[Event] — what happens to a record, declared once

An event is a class marked `[Event]`: something that happens to a record, declared once by the code that owns the…

[On] — hear an event, after the commit that raised it

`[On]` on a function makes it a HANDLER of the event its one parameter is. Every handler of an event — in the app and…

grants (a link that opens one row)

A `grant` is a link the platform signs that lets whoever holds it reach ONE row of ONE entity, for a while, without…

Security.MayRead — may THIS person read this row, under their own rules

`Security.MayRead(person, row)` answers whether a given person may read a given row under THEIR OWN security — the…

[Settings] and [Setting]: a settings section declared on its row, each field drawn by its type

Mark the row staff configure `[Settings("Title", Authorize = Policy)]` and each field on it `[Setting("Label")]`, and…

The Notifications kit — telling a person that something needs them

`Osysharp.Notifications` tells a person that something needs them — in the app, a bell with a live count and a list…

[Email] — mail rendered from data

An `[Email]` component is a mail template. It is rendered on the server — from any function, a workflow step included —…

The Privacy kit — erasure that keeps only what the company must or may

`Osysharp.Privacy` answers a person's GDPR request — access, portability, erasure, restriction — per category of what…