feat: tier names + per-tier prices on offer cards, unified admin UI

- offer editor: per-tier name input (mirrors public_name/internal_name)
- offer list cards: show 3-tier services total + manual public price
- shared PageHeader component, full-width layout across all admin pages
- UI-RULES.md design conventions doc

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-26 16:58:18 +02:00
parent 9abe1fe4bb
commit add2176a6b
12 changed files with 281 additions and 91 deletions
@@ -162,6 +162,12 @@ export function OfferEditorClient({
);
}
function updateTierName(tierIdx: number, value: string) {
setTiers((prev) =>
prev.map((tier, idx) => (idx === tierIdx ? { ...tier, public_name: value } : tier))
);
}
function handleSave() {
setSaveError(null);
const payload: SaveOfferEditorPayload = {
@@ -176,15 +182,18 @@ export function OfferEditorClient({
tempo: macro.tempo,
pain: macro.pain,
metodo: macro.metodo,
tiers: tiers.map((t) => ({
id: t.id || undefined,
tier_letter: (t.tier_letter ?? "A") as "A" | "B" | "C",
internal_name: t.internal_name || `Tier ${t.tier_letter}`,
public_name: t.public_name || t.tier_letter || "Tier",
duration_months: t.duration_months || 1,
public_price: t.public_price ? Number(t.public_price) : null,
assignedServiceIds: t.assignedServiceIds,
})),
tiers: tiers.map((t) => {
const tierName = t.public_name?.trim();
return {
id: t.id || undefined,
tier_letter: (t.tier_letter ?? "A") as "A" | "B" | "C",
internal_name: tierName || `Tier ${t.tier_letter}`,
public_name: tierName || t.tier_letter || "Tier",
duration_months: t.duration_months || 1,
public_price: t.public_price ? Number(t.public_price) : null,
assignedServiceIds: t.assignedServiceIds,
};
}),
tipoTags,
obiettivoTags,
};
@@ -474,6 +483,22 @@ export function OfferEditorClient({
)}
</tbody>
<tfoot>
<tr className="border-t border-[#e5e7eb]">
<td className="py-2 px-3 font-semibold text-[#1a1a1a]" colSpan={2}>
Nome Tier
</td>
{tiers.map((tier, tierIdx) => (
<td key={tier.tier_letter} className="py-2 px-3 text-center">
<input
type="text"
value={tier.public_name ?? ""}
onChange={(e) => updateTierName(tierIdx, e.target.value)}
placeholder="Nome…"
className="w-20 text-center text-sm border border-[#e5e7eb] rounded px-1 py-1 focus-visible:ring-1 focus-visible:ring-primary focus:outline-none"
/>
</td>
))}
</tr>
<tr className="border-t border-[#e5e7eb]">
<td className="py-2 px-3 font-semibold text-[#1a1a1a]" colSpan={2}>
Totale Servizi