feat: service offer tags, catalog cleanup, offer editor filter
- Import 58 offer membership tags for all 55 services (entity_type="services") via scripts/import-service-offer-tags.ts (already run on prod) - ServiceTable: remove Categoria column, rename Tag → Offerta; QuickAddRow no longer has a category field (offer tags set post-creation) - offer-queries: getOfferEditorData fetches offerTags per service (join on tags WHERE entity_type="services") and exposes them in OfferEditorData - OfferEditorClient: filter chips above "Servizi Inclusi" — Tutti / Entry Offer / Signature Offer / Retainer Offer, derived from actual tag data Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -93,8 +93,21 @@ export function OfferEditorClient({
|
||||
|
||||
const [initialArchived] = useState<boolean>(data.macro.is_archived);
|
||||
const [savedDraft, setSavedDraft] = useState(false);
|
||||
const [offerFilter, setOfferFilter] = useState<string | null>(null);
|
||||
|
||||
const filteredServices = data.availableServices;
|
||||
const offerFilterOptions = useMemo(() => {
|
||||
const all = new Set<string>();
|
||||
for (const s of data.availableServices) s.offerTags.forEach((t) => all.add(t));
|
||||
return Array.from(all).sort();
|
||||
}, [data.availableServices]);
|
||||
|
||||
const filteredServices = useMemo(
|
||||
() =>
|
||||
offerFilter
|
||||
? data.availableServices.filter((s) => s.offerTags.includes(offerFilter))
|
||||
: data.availableServices,
|
||||
[data.availableServices, offerFilter]
|
||||
);
|
||||
|
||||
// Totals use the full catalog (not filtered) so changing category doesn't zero them.
|
||||
const serviceById = useMemo(
|
||||
@@ -334,6 +347,35 @@ export function OfferEditorClient({
|
||||
<section className="space-y-3">
|
||||
<h3 className="text-base font-semibold text-[#1a1a1a]">Servizi Inclusi</h3>
|
||||
|
||||
{/* Offer filter chips */}
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setOfferFilter(null)}
|
||||
className={`text-xs px-3 py-1 rounded-full border transition-colors duration-150 ${
|
||||
offerFilter === null
|
||||
? "bg-[#1A463C] text-white border-[#1A463C]"
|
||||
: "border-[#e5e7eb] text-[#71717a] hover:border-[#1A463C] hover:text-[#1A463C]"
|
||||
}`}
|
||||
>
|
||||
Tutti
|
||||
</button>
|
||||
{offerFilterOptions.map((opt) => (
|
||||
<button
|
||||
key={opt}
|
||||
type="button"
|
||||
onClick={() => setOfferFilter(offerFilter === opt ? null : opt)}
|
||||
className={`text-xs px-3 py-1 rounded-full border transition-colors duration-150 ${
|
||||
offerFilter === opt
|
||||
? "bg-[#1A463C] text-white border-[#1A463C]"
|
||||
: "border-[#e5e7eb] text-[#71717a] hover:border-[#1A463C] hover:text-[#1A463C]"
|
||||
}`}
|
||||
>
|
||||
{opt}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-xl border border-[#e5e7eb] overflow-hidden">
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm">
|
||||
|
||||
Reference in New Issue
Block a user