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:
@@ -0,0 +1,118 @@
|
||||
// Import offer membership tags for all 55 services.
|
||||
// Stores in tags table with entity_type = "services" (same as the "Offerta" column).
|
||||
// Idempotent: uses onConflictDoNothing.
|
||||
// Run: same SSH tunnel pattern as other migration scripts.
|
||||
import postgres from "postgres";
|
||||
import { customAlphabet } from "nanoid";
|
||||
|
||||
const nanoid = customAlphabet("0123456789abcdefghijklmnopqrstuvwxyz", 12);
|
||||
|
||||
// name → offer types from the CSV "Offerta" column (multi-value pipe-separated)
|
||||
const OFFER_MEMBERSHIPS: Record<string, string[]> = {
|
||||
"Raccolta e Mappatura Materiali": ["Signature Offer", "Entry Offer"],
|
||||
"Audit iniziale (UX/UI, struttura, conversione)": ["Signature Offer", "Entry Offer"],
|
||||
"Workshop (1° fase)": ["Signature Offer", "Entry Offer"],
|
||||
"Analisi Competitor": ["Signature Offer", "Entry Offer"],
|
||||
"Documento di restituzione (problemi + lista ottimizzazioni)": ["Entry Offer"],
|
||||
"Redesign visivo dell'above-the-fold / hero (il 'prima → dopo')": ["Entry Offer"],
|
||||
"Call di restituzione": ["Entry Offer"],
|
||||
"Call di presentazione Prima/Dopo": ["Entry Offer"],
|
||||
"Roadmap / istruzioni operative + mini kit": ["Entry Offer"],
|
||||
"UX Research": ["Signature Offer"],
|
||||
"Customer Journey": ["Signature Offer"],
|
||||
"Architettura (sitemap)": ["Signature Offer"],
|
||||
"Brand Identity - Visual identity": ["Signature Offer"],
|
||||
"Brand Identity - Voice identity": ["Signature Offer"],
|
||||
"Direzione Creative (moodboard)": ["Signature Offer"],
|
||||
"Workshop (2° fase)": ["Signature Offer"],
|
||||
"Settaggio CMS (Wordpress Webflow ecc)": ["Signature Offer"],
|
||||
"UX - UI": ["Signature Offer"],
|
||||
"Wireframe (Low-Mid Fidelity)": ["Signature Offer"],
|
||||
"Homepage (Figma + Dev)": ["Signature Offer"],
|
||||
"Revisione #1": ["Signature Offer"],
|
||||
"- Chi siamo": ["Signature Offer"],
|
||||
"- Contatti": ["Signature Offer"],
|
||||
"- Blog (Archivio)": ["Signature Offer"],
|
||||
"- - Blog post (Template Singolo)": ["Signature Offer"],
|
||||
"- Case Study (Archivio)": ["Signature Offer"],
|
||||
"- - Case Study (Template Singolo)": ["Signature Offer"],
|
||||
"- Landing Page (Metodo o Differenziante)": ["Signature Offer"],
|
||||
"- - - Thank You Page": ["Signature Offer"],
|
||||
"- - - 404": ["Signature Offer"],
|
||||
"Responsive (inclusa nelle pagine?)": ["Signature Offer"],
|
||||
"UX writing (Revisione testi)": ["Signature Offer"],
|
||||
"Seo Setup (basic on-page)": ["Signature Offer"],
|
||||
"Seo Avanzato (+ keyword + ricerca + blog post dentro retainer)": ["Signature Offer"],
|
||||
"Revisione #2 finale": ["Signature Offer"],
|
||||
"Web Core Vitals (Optimization)": ["Signature Offer"],
|
||||
"QA - Test Cross Browser e responsive": ["Signature Offer"],
|
||||
"Setting GA4 - Tag Manager - Hotjar/Clarify": ["Signature Offer"],
|
||||
"Raccolta Feedback Post Lancio": ["Signature Offer"],
|
||||
"Go-live / messa online & accessi": ["Signature Offer"],
|
||||
"Audit uscita": ["Signature Offer"],
|
||||
"Real User Testing": ["Signature Offer"],
|
||||
"Follow Up Handover": ["Signature Offer"],
|
||||
"Video Tutorial per micro modifiche in autonomia": ["Signature Offer"],
|
||||
"Revisioni Extra illimitate (sicuri illimitati???)": ["Signature Offer"],
|
||||
"Brand Kit (youtube - linkedin - insta)": ["Signature Offer"],
|
||||
"Mantenimento tecnico (sito sempre online, bello, funzionante)": ["Retainer Offer"],
|
||||
"Monitoraggio dati (GA4 / Hotjar-Clarity)": ["Retainer Offer"],
|
||||
"Report mensile": ["Retainer Offer"],
|
||||
"CRO - analisi UX (hotmap, punti di drop)": ["Retainer Offer"],
|
||||
"CRO - implementazione miglioramenti": ["Retainer Offer"],
|
||||
"Call di Mentorship/Consulenza/allineamento/revisione": ["Retainer Offer"],
|
||||
"Art direction su direzione da prendere (consulente strategico interno disponibile 24/7)": ["Retainer Offer"],
|
||||
"Extra landing (prezzo singolo per pompare prezzo)": ["Retainer Offer"],
|
||||
"SEO avanzato (Blog post)": ["Retainer Offer"],
|
||||
};
|
||||
|
||||
const TAG_ENTITY = "services";
|
||||
|
||||
async function run() {
|
||||
const databaseUrl = process.env.DATABASE_URL;
|
||||
if (!databaseUrl) { console.error("DATABASE_URL required"); process.exit(1); }
|
||||
|
||||
const client = postgres(databaseUrl);
|
||||
try {
|
||||
const allServices = await client`SELECT id, name FROM services`;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const serviceByName = new Map((allServices as any[]).map((s) => [s.name as string, s.id as string]));
|
||||
|
||||
let inserted = 0;
|
||||
let skipped = 0;
|
||||
let missing = 0;
|
||||
|
||||
for (const [name, offers] of Object.entries(OFFER_MEMBERSHIPS)) {
|
||||
const serviceId = serviceByName.get(name);
|
||||
if (!serviceId) {
|
||||
console.warn(` ⚠ service not found: ${name}`);
|
||||
missing++;
|
||||
continue;
|
||||
}
|
||||
for (const offer of offers) {
|
||||
const id = nanoid();
|
||||
const result = await client`
|
||||
INSERT INTO tags (id, entity_type, entity_id, name)
|
||||
VALUES (${id}, ${TAG_ENTITY}, ${serviceId}, ${offer})
|
||||
ON CONFLICT (entity_type, entity_id, name) DO NOTHING
|
||||
`;
|
||||
if (result.count === 0) {
|
||||
skipped++;
|
||||
} else {
|
||||
console.log(` ✓ ${name} → ${offer}`);
|
||||
inserted++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`\n✓ Done — ${inserted} tags inserted, ${skipped} skipped, ${missing} services not found`);
|
||||
process.exit(0);
|
||||
} catch (err) {
|
||||
console.error("Error:", err instanceof Error ? err.message : err);
|
||||
process.exit(1);
|
||||
} finally {
|
||||
await client.end();
|
||||
}
|
||||
}
|
||||
|
||||
run();
|
||||
Reference in New Issue
Block a user