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 customerWho 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 forDescription#
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#
- Records — what any record is called, where it opens, who may read it — who may read the work, and where it opens, for any record
- Entity —
Entity, the reference a question'sAboutis - grants (a link that opens one row) — the link a question is mailed with
- [Settings] and [Setting]: a settings section declared on its row, each field drawn by its type — the settings page the kit declares
- The Notifications kit — telling a person that something needs them — telling the team when an answer is bad
- [Email] — mail rendered from data — 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 — a person's answers in their privacy requests
kits/satisfaction-surveys/tests/— every behaviour above as a test, the screens under--pixels