Реалізація NPS-опитування на сайті
NPS (Net Promoter Score)—стандарт вимірювання лояльності: "Наскільки вірогідно, що ви порекомендуєте нас друзям?" по шкалі 0–10. Промоутери (9–10), Пасивні (7–8), Критики (0–6). NPS = % промоутерів − % критиків.
Коли та кому показувати
Час показу впливає на якість даних:
- Після ключової события: завершення замовлення, конець пробного періоду, перше успішне використання функції
- За часом: 14–30 днів після реєстрації, макс раз на 90 днів на користувача
- Сегментування: не показуйте новим користувачам (< 7 днів), виключіть відкриті тикети підтримки
Backend: модель та API
// База даних
Schema::create('nps_responses', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->nullable()->constrained()->nullOnDelete();
$table->string('session_id')->nullable();
$table->tinyInteger('score')->unsigned(); // 0-10
$table->text('comment')->nullable();
$table->string('trigger_event')->nullable(); // 'order_completed'
$table->string('page_url')->nullable();
$table->ipAddress('ip')->nullable();
$table->timestamps();
});
// Контролер
public function store(Request $request): JsonResponse
{
$validated = $request->validate([
'score' => 'required|integer|min:0|max:10',
'comment' => 'nullable|string|max:1000',
'trigger_event' => 'nullable|string|max:100',
]);
NpsResponse::create([
'user_id' => Auth::id(),
'score' => $validated['score'],
'comment' => $validated['comment'],
'trigger_event' => $validated['trigger_event'],
'page_url' => $request->referrer(),
'ip' => $request->ip(),
]);
return response()->json(['success' => true]);
}
Frontend компонент
function NPSSurvey({ onClose }: { onClose: () => void }) {
const [score, setScore] = useState<number | null>(null);
const [comment, setComment] = useState('');
async function handleSubmit() {
await fetch('/api/nps/submit', {
method: 'POST',
body: JSON.stringify({ score, comment }),
});
onClose();
}
return (
<div className="fixed bottom-6 right-6 bg-white p-6 rounded-lg shadow-lg max-w-sm">
<p className="font-semibold mb-4">Наскільки вірогідно вас рекомендувати?</p>
<div className="flex gap-2 mb-4">
{[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map(n => (
<button
key={n}
onClick={() => setScore(n)}
className={`w-8 h-8 rounded ${
score === n ? 'bg-blue-600 text-white' : 'bg-gray-100'
}`}
>
{n}
</button>
))}
</div>
{score !== null && score < 7 && (
<textarea
placeholder="Як ми можемо поліпшитися?"
value={comment}
onChange={e => setComment(e.target.value)}
className="w-full border rounded p-2 mb-4"
/>
)}
<button
onClick={handleSubmit}
className="bg-blue-600 text-white px-4 py-2 rounded w-full"
>
Надіслати
</button>
</div>
);
}
Аналіз
SELECT
CASE
WHEN score >= 9 THEN 'Промоутер'
WHEN score >= 7 THEN 'Пасивний'
ELSE 'Критик'
END AS category,
COUNT(*) AS count,
ROUND(100.0 * COUNT(*) / SUM(COUNT(*)) OVER (), 1) AS pct
FROM nps_responses
WHERE created_at > NOW() - INTERVAL '30 days'
GROUP BY 1;
Часова шкала
Базовий віджет опитування—2–3 дні. З розумним плануванням та дашбордом—5–7 днів.







