Phase2.5: Skill SetUp Process
All checks were successful
Build and Push / build (push) Successful in 1m39s

This commit is contained in:
2026-05-24 17:51:09 +02:00
parent 49b085e59e
commit 229bfea263
7 changed files with 126 additions and 66 deletions

View File

@@ -34,6 +34,7 @@ export function PricingEditor({
catalog, catalog,
}: Props) { }: Props) {
const t = useTranslations("adminBilling"); const t = useTranslations("adminBilling");
const tPackages = useTranslations("packages");
const router = useRouter(); const router = useRouter();
// -- Platform pricing form ---------------------------------------------- // -- Platform pricing form ----------------------------------------------
@@ -78,12 +79,19 @@ export function PricingEditor({
} }
}; };
// -- Skill pricing ------------------------------------------------------ // -- Package pricing ----------------------------------------------------
// Server is authoritative — we don't keep an editable local copy of the // Server is authoritative — we don't keep an editable local copy of the
// table; instead each action posts to the API and we router.refresh(). // table; instead each action posts to the API and we router.refresh().
const [newSkillId, setNewSkillId] = useState( //
catalog.find((c) => c.category === "skill")?.id ?? "" // Naming carry-over: the underlying DB table is `skill_pricing` and the
); // column is `skill_id`, dating from when only skills were priced. The
// model now applies to any PackageDef in the catalog regardless of
// category — core, channel, or skill. The state variable names below
// (newSkill*, addingSkill, etc.) retain the legacy "skill" prefix
// because renaming the entire surface for purely cosmetic reasons
// would create churn for no functional gain. Treat "skill" here as
// shorthand for "priced package".
const [newSkillId, setNewSkillId] = useState(catalog[0]?.id ?? "");
const [newSkillPrice, setNewSkillPrice] = useState("0.10"); const [newSkillPrice, setNewSkillPrice] = useState("0.10");
const [newSkillSetupFee, setNewSkillSetupFee] = useState("0"); const [newSkillSetupFee, setNewSkillSetupFee] = useState("0");
const [addingSkill, setAddingSkill] = useState(false); const [addingSkill, setAddingSkill] = useState(false);
@@ -147,9 +155,16 @@ export function PricingEditor({
} }
}; };
// Catalog filtered to skill-kind entries for the picker, but keeping // Pricing applies to any catalog entry regardless of category. Grouped
// existing pricing rows even if they reference non-skill packages. // dropdown sorts options by category for visual scanning — core,
const skillCatalogOptions = catalog.filter((c) => c.category === "skill"); // channel, and skill in a single picker.
const skillCatalogOptions = [...catalog].sort((a, b) => {
const order = { core: 0, channel: 1, skill: 2 } as Record<string, number>;
const ca = order[a.category] ?? 99;
const cb = order[b.category] ?? 99;
if (ca !== cb) return ca - cb;
return a.name.localeCompare(b.name);
});
const catalogIndex = new Map(catalog.map((c) => [c.id, c])); const catalogIndex = new Map(catalog.map((c) => [c.id, c]));
const pricedIds = new Set(initialSkillPricing.map((s) => s.skillId)); const pricedIds = new Set(initialSkillPricing.map((s) => s.skillId));
@@ -260,7 +275,12 @@ export function PricingEditor({
<td className="py-2"> <td className="py-2">
<div className="font-mono text-xs">{sp.skillId}</div> <div className="font-mono text-xs">{sp.skillId}</div>
{entry && ( {entry && (
<div className="text-xs text-text-muted">{entry.name}</div> <div className="text-xs text-text-muted flex items-center gap-2">
<span>{entry.name}</span>
<span className="text-[10px] uppercase tracking-wider bg-surface-3 px-1.5 py-0.5 rounded">
{entry.category}
</span>
</div>
)} )}
</td> </td>
<td className="py-2 text-right"> <td className="py-2 text-right">
@@ -311,13 +331,45 @@ export function PricingEditor({
onChange={(e) => setNewSkillId(e.target.value)} onChange={(e) => setNewSkillId(e.target.value)}
className="mt-1 w-full px-3 py-2 rounded-md border border-border bg-surface-2 text-sm" className="mt-1 w-full px-3 py-2 rounded-md border border-border bg-surface-2 text-sm"
> >
{skillCatalogOptions {(() => {
.filter((c) => !pricedIds.has(c.id)) // Group available options by category for the picker.
.map((c) => ( // Already-priced packages are filtered out (admin
<option key={c.id} value={c.id}> // edits those inline above).
{c.name} ({c.id}) const available = skillCatalogOptions.filter(
</option> (c) => !pricedIds.has(c.id)
))} );
const byCat = new Map<string, typeof available>();
for (const c of available) {
if (!byCat.has(c.category)) byCat.set(c.category, []);
byCat.get(c.category)!.push(c);
}
// Labels for the optgroups. Reuse the existing
// packages.categories.* scope which already has
// translations in all four locales.
const labels: Record<string, string> = {
core: tPackages("categories.core"),
channel: tPackages("categories.channels"),
skill: tPackages("categories.skills"),
};
const order: Array<"core" | "channel" | "skill"> = [
"core",
"channel",
"skill",
];
return order.map((cat) => {
const items = byCat.get(cat);
if (!items || items.length === 0) return null;
return (
<optgroup key={cat} label={labels[cat] ?? cat}>
{items.map((c) => (
<option key={c.id} value={c.id}>
{c.name} ({c.id})
</option>
))}
</optgroup>
);
});
})()}
</select> </select>
</label> </label>
<label className="w-28"> <label className="w-28">

View File

@@ -70,15 +70,22 @@ export function SkillCostDialog({
</div> </div>
)} )}
{showDaily && ( {showDaily && (
/* Display reference monthly cost (daily × 30) plus the
actual daily rate as a sub-note. Billing is always
per UTC day enabled — partial months prorate to that
same daily rate, full months land at roughly the
figure shown (varies ±~3% by month length). */
<div className="flex justify-between items-baseline"> <div className="flex justify-between items-baseline">
<div> <div>
<div className="text-sm">{t("dailyPriceLabel")}</div> <div className="text-sm">{t("monthlyPriceLabel")}</div>
<div className="text-xs text-text-muted"> <div className="text-xs text-text-muted">
{t("dailyPriceNote")} {t("monthlyPriceNote", {
daily: dailyPriceChf.toFixed(2),
})}
</div> </div>
</div> </div>
<div className="text-sm font-mono"> <div className="text-sm font-mono">
CHF {dailyPriceChf.toFixed(2)} / {t("dayUnit")} CHF {(dailyPriceChf * 30).toFixed(2)} / {t("monthUnit")}
</div> </div>
</div> </div>
)} )}

View File

@@ -231,6 +231,7 @@ export const PACKAGE_CATALOG: PackageDef[] = [
}, },
{ {
id: "gog", id: "gog",
requiresManualSetup: true,
name: "Google Workspace (Gog)", name: "Google Workspace (Gog)",
descriptionKey: "packages.gog.description", descriptionKey: "packages.gog.description",
requiresSecrets: true, requiresSecrets: true,

View File

@@ -390,7 +390,7 @@
"resumeRequestTooltip": "Reaktivierungsanfrage für einen bestehenden Tenant. Bei Genehmigung wird der Tenant wieder aktiviert; keine Provisionierung läuft.", "resumeRequestTooltip": "Reaktivierungsanfrage für einen bestehenden Tenant. Bei Genehmigung wird der Tenant wieder aktiviert; keine Provisionierung läuft.",
"openclawTool": "OpenClaw-Versionen", "openclawTool": "OpenClaw-Versionen",
"billingTool": "Abrechnung →", "billingTool": "Abrechnung →",
"skillsQueueTool": "Skill-Warteschlange" "skillsQueueTool": "Aktivierungs-Warteschlange"
}, },
"channelUsers": { "channelUsers": {
"title": "Autorisierte Benutzer", "title": "Autorisierte Benutzer",
@@ -591,17 +591,17 @@
"save": "Speichern", "save": "Speichern",
"saving": "Speichere…", "saving": "Speichere…",
"savedOk": "Gespeichert", "savedOk": "Gespeichert",
"skillPricingTitle": "Skill-Preise", "skillPricingTitle": "Paket-Preise",
"skillPricingDesc": "Tagespreis pro Skill. Ein zu beliebigem Zeitpunkt an einem UTC-Tag aktivierter Skill zählt als ein abrechenbarer Tag.", "skillPricingDesc": "Tagespreis und einmalige Einrichtungsgebühr für jedes Paket — Core, Kanal oder Skill. Die Preisgestaltung gilt für jeden Tenant, der das Paket aktiviert.",
"skillCol": "Skill", "skillCol": "Paket",
"dailyPriceCol": "Tagespreis", "dailyPriceCol": "Tagespreis",
"actionsCol": "", "actionsCol": "",
"remove": "Entfernen", "remove": "Entfernen",
"noSkillsPriced": "Noch keine Skills bepreist.", "noSkillsPriced": "Noch keine Pakete bepreist.",
"addSkillLabel": "Skill hinzufügen", "addSkillLabel": "Paket hinzufügen",
"dailyPriceLabel": "Tagespreis", "dailyPriceLabel": "Tagespreis",
"add": "Hinzufügen", "add": "Hinzufügen",
"confirmDeleteSkillPrice": "Preis für {skill} entfernen?", "confirmDeleteSkillPrice": "Preisgestaltung für {skill} entfernen? Bereits abgerechnete Zeiträume bleiben unberührt.",
"clickToEdit": "Zum Bearbeiten klicken", "clickToEdit": "Zum Bearbeiten klicken",
"generateFormTitle": "Rechnung erstellen", "generateFormTitle": "Rechnung erstellen",
"noOrgsToGenerate": "Keine Organisationen mit Tenants gefunden.", "noOrgsToGenerate": "Keine Organisationen mit Tenants gefunden.",
@@ -667,17 +667,17 @@
"intro": "Die Aktivierung von {skill} verursacht folgende Kosten:", "intro": "Die Aktivierung von {skill} verursacht folgende Kosten:",
"setupFeeLabel": "Einrichtungsgebühr", "setupFeeLabel": "Einrichtungsgebühr",
"setupFeeNote": "Einmalig, nur bei erster Aktivierung", "setupFeeNote": "Einmalig, nur bei erster Aktivierung",
"dailyPriceLabel": "Tagespreis", "monthlyPriceLabel": "Monatspreis",
"dailyPriceNote": "Pro Kalendertag (UTC) berechnet, an dem der Skill aktiviert ist", "monthlyPriceNote": "CHF {daily}/Tag aktiv; Teilmonate werden taggenau berechnet",
"dayUnit": "Tag", "monthUnit": "Monat",
"disclaimer": "Diese Kosten erscheinen auf Ihrer nächsten Monatsrechnung. Mit der Bestätigung stimmen Sie ihnen zu.", "disclaimer": "Diese Kosten erscheinen auf Ihrer nächsten Monatsrechnung. Mit der Bestätigung stimmen Sie ihnen zu.",
"cancel": "Abbrechen", "cancel": "Abbrechen",
"confirm": "Bestätigen & aktivieren", "confirm": "Bestätigen & aktivieren",
"confirming": "Aktiviere…" "confirming": "Aktiviere…"
}, },
"adminSkills": { "adminSkills": {
"title": "Skill-Aktivierungs-Warteschlange", "title": "Aktivierungs-Warteschlange",
"subtitle": "Kundenanfragen für Skills, die manuelle plattformseitige Einrichtung benötigen. Genehmigen, sobald die Konfiguration steht; ablehnen mit Grund, wenn die Aktivierung nicht möglich ist.", "subtitle": "Kundenanfragen für Pakete, die manuelle plattformseitige Einrichtung benötigen. Genehmigen, sobald die Konfiguration steht; ablehnen mit Grund, wenn die Aktivierung nicht möglich ist.",
"backToAdmin": "Zurück zur Verwaltung", "backToAdmin": "Zurück zur Verwaltung",
"emptyQueue": "Keine ausstehenden Skill-Aktivierungsanfragen.", "emptyQueue": "Keine ausstehenden Skill-Aktivierungsanfragen.",
"requestedAtCol": "Angefragt", "requestedAtCol": "Angefragt",

View File

@@ -390,7 +390,7 @@
"resumeRequestTooltip": "Reactivation request for an existing tenant. Approving will un-suspend the tenant; no provisioning runs.", "resumeRequestTooltip": "Reactivation request for an existing tenant. Approving will un-suspend the tenant; no provisioning runs.",
"openclawTool": "OpenClaw versions", "openclawTool": "OpenClaw versions",
"billingTool": "Billing →", "billingTool": "Billing →",
"skillsQueueTool": "Skills Queue" "skillsQueueTool": "Activation Queue"
}, },
"channelUsers": { "channelUsers": {
"title": "Authorized Users", "title": "Authorized Users",
@@ -591,17 +591,17 @@
"save": "Save", "save": "Save",
"saving": "Saving…", "saving": "Saving…",
"savedOk": "Saved", "savedOk": "Saved",
"skillPricingTitle": "Skill pricing", "skillPricingTitle": "Package pricing",
"skillPricingDesc": "Per-skill daily price. A skill enabled at any point during a UTC day counts as one billable day.", "skillPricingDesc": "Set per-day rate and one-time setup fee for any package — core, channel, or skill. Pricing applies to every tenant that enables the package.",
"skillCol": "Skill", "skillCol": "Package",
"dailyPriceCol": "Daily price", "dailyPriceCol": "Daily price",
"actionsCol": "", "actionsCol": "",
"remove": "Remove", "remove": "Remove",
"noSkillsPriced": "No skills are priced yet.", "noSkillsPriced": "No packages priced yet.",
"addSkillLabel": "Add skill", "addSkillLabel": "Add package",
"dailyPriceLabel": "Daily price", "dailyPriceLabel": "Daily price",
"add": "Add", "add": "Add",
"confirmDeleteSkillPrice": "Remove pricing for {skill}?", "confirmDeleteSkillPrice": "Remove pricing for {skill}? Already-billed periods are unaffected.",
"clickToEdit": "Click to edit", "clickToEdit": "Click to edit",
"generateFormTitle": "Generate invoice", "generateFormTitle": "Generate invoice",
"noOrgsToGenerate": "No organizations with tenants found.", "noOrgsToGenerate": "No organizations with tenants found.",
@@ -667,17 +667,17 @@
"intro": "Activating {skill} will incur the following charges:", "intro": "Activating {skill} will incur the following charges:",
"setupFeeLabel": "Setup fee", "setupFeeLabel": "Setup fee",
"setupFeeNote": "One-time, charged on first activation only", "setupFeeNote": "One-time, charged on first activation only",
"dailyPriceLabel": "Daily price", "monthlyPriceLabel": "Monthly price",
"dailyPriceNote": "Charged for each calendar day (UTC) the skill is enabled", "monthlyPriceNote": "CHF {daily}/day enabled; partial months prorated by day",
"dayUnit": "day", "monthUnit": "month",
"disclaimer": "These charges appear on your next monthly invoice. By confirming you agree to incur them.", "disclaimer": "These charges appear on your next monthly invoice. By confirming you agree to incur them.",
"cancel": "Cancel", "cancel": "Cancel",
"confirm": "Confirm & activate", "confirm": "Confirm & activate",
"confirming": "Activating…" "confirming": "Activating…"
}, },
"adminSkills": { "adminSkills": {
"title": "Skill activation queue", "title": "Activation queue",
"subtitle": "Customer requests to activate skills that need manual platform-side setup. Approve once configuration is in place; reject with a reason if the activation can't proceed.", "subtitle": "Customer requests to activate packages that need manual platform-side setup. Approve once configuration is in place; reject with a reason if the activation can't proceed.",
"backToAdmin": "Back to Admin", "backToAdmin": "Back to Admin",
"emptyQueue": "No pending skill activation requests.", "emptyQueue": "No pending skill activation requests.",
"requestedAtCol": "Requested", "requestedAtCol": "Requested",

View File

@@ -390,7 +390,7 @@
"resumeRequestTooltip": "Demande de réactivation d'un locataire existant. L'approbation le réactivera ; aucun provisionnement ne s'exécute.", "resumeRequestTooltip": "Demande de réactivation d'un locataire existant. L'approbation le réactivera ; aucun provisionnement ne s'exécute.",
"openclawTool": "Versions OpenClaw", "openclawTool": "Versions OpenClaw",
"billingTool": "Facturation →", "billingTool": "Facturation →",
"skillsQueueTool": "File des skills" "skillsQueueTool": "File d'activation"
}, },
"channelUsers": { "channelUsers": {
"title": "Utilisateurs autorisés", "title": "Utilisateurs autorisés",
@@ -591,17 +591,17 @@
"save": "Enregistrer", "save": "Enregistrer",
"saving": "Enregistrement…", "saving": "Enregistrement…",
"savedOk": "Enregistré", "savedOk": "Enregistré",
"skillPricingTitle": "Tarifs des skills", "skillPricingTitle": "Tarification des paquets",
"skillPricingDesc": "Prix journalier par skill. Un skill activé à tout moment au cours d'une journée UTC compte comme un jour facturable.", "skillPricingDesc": "Tarif journalier et frais de configuration uniques pour chaque paquet — core, canal ou skill. La tarification s'applique à chaque tenant activant le paquet.",
"skillCol": "Skill", "skillCol": "Paquet",
"dailyPriceCol": "Prix/jour", "dailyPriceCol": "Prix/jour",
"actionsCol": "", "actionsCol": "",
"remove": "Retirer", "remove": "Retirer",
"noSkillsPriced": "Aucun skill n'a encore de prix.", "noSkillsPriced": "Aucun paquet tarifé.",
"addSkillLabel": "Ajouter un skill", "addSkillLabel": "Ajouter un paquet",
"dailyPriceLabel": "Prix/jour", "dailyPriceLabel": "Prix/jour",
"add": "Ajouter", "add": "Ajouter",
"confirmDeleteSkillPrice": "Retirer le prix pour {skill}?", "confirmDeleteSkillPrice": "Supprimer la tarification de {skill} ? Les périodes déjà facturées ne sont pas affectées.",
"clickToEdit": "Cliquer pour modifier", "clickToEdit": "Cliquer pour modifier",
"generateFormTitle": "Générer une facture", "generateFormTitle": "Générer une facture",
"noOrgsToGenerate": "Aucune organisation avec tenants trouvée.", "noOrgsToGenerate": "Aucune organisation avec tenants trouvée.",
@@ -667,17 +667,17 @@
"intro": "L'activation de {skill} entraînera les frais suivants :", "intro": "L'activation de {skill} entraînera les frais suivants :",
"setupFeeLabel": "Frais de configuration", "setupFeeLabel": "Frais de configuration",
"setupFeeNote": "Unique, facturé uniquement à la première activation", "setupFeeNote": "Unique, facturé uniquement à la première activation",
"dailyPriceLabel": "Prix journalier", "monthlyPriceLabel": "Prix mensuel",
"dailyPriceNote": "Facturé pour chaque jour calendaire (UTC) où le skill est activé", "monthlyPriceNote": "CHF {daily}/jour actif ; mois partiels prorata journalier",
"dayUnit": "jour", "monthUnit": "mois",
"disclaimer": "Ces frais figureront sur votre prochaine facture mensuelle. En confirmant, vous acceptez de les engager.", "disclaimer": "Ces frais figureront sur votre prochaine facture mensuelle. En confirmant, vous acceptez de les engager.",
"cancel": "Annuler", "cancel": "Annuler",
"confirm": "Confirmer & activer", "confirm": "Confirmer & activer",
"confirming": "Activation…" "confirming": "Activation…"
}, },
"adminSkills": { "adminSkills": {
"title": "File d'activation des skills", "title": "File d'activation",
"subtitle": "Demandes clients d'activation de skills nécessitant une configuration manuelle côté plateforme. Approuver une fois la configuration en place ; refuser avec un motif si l'activation est impossible.", "subtitle": "Demandes clients d'activation de paquets nécessitant une configuration manuelle côté plateforme. Approuver une fois la configuration en place ; refuser avec un motif si l'activation est impossible.",
"backToAdmin": "Retour à l'administration", "backToAdmin": "Retour à l'administration",
"emptyQueue": "Aucune demande d'activation en attente.", "emptyQueue": "Aucune demande d'activation en attente.",
"requestedAtCol": "Demandée le", "requestedAtCol": "Demandée le",

View File

@@ -390,7 +390,7 @@
"resumeRequestTooltip": "Richiesta di riattivazione di un tenant esistente. L'approvazione lo riattiverà; non viene eseguito alcun provisioning.", "resumeRequestTooltip": "Richiesta di riattivazione di un tenant esistente. L'approvazione lo riattiverà; non viene eseguito alcun provisioning.",
"openclawTool": "Versioni OpenClaw", "openclawTool": "Versioni OpenClaw",
"billingTool": "Fatturazione →", "billingTool": "Fatturazione →",
"skillsQueueTool": "Coda skill" "skillsQueueTool": "Coda di attivazione"
}, },
"channelUsers": { "channelUsers": {
"title": "Utenti autorizzati", "title": "Utenti autorizzati",
@@ -591,17 +591,17 @@
"save": "Salva", "save": "Salva",
"saving": "Salvataggio…", "saving": "Salvataggio…",
"savedOk": "Salvato", "savedOk": "Salvato",
"skillPricingTitle": "Prezzi skill", "skillPricingTitle": "Prezzi dei pacchetti",
"skillPricingDesc": "Prezzo giornaliero per skill. Una skill attiva in qualsiasi momento di un giorno UTC conta come un giorno fatturabile.", "skillPricingDesc": "Tariffa giornaliera e spese di attivazione una tantum per qualsiasi pacchetto — core, canale o skill. La tariffazione si applica a ogni tenant che attiva il pacchetto.",
"skillCol": "Skill", "skillCol": "Pacchetto",
"dailyPriceCol": "Prezzo/giorno", "dailyPriceCol": "Prezzo/giorno",
"actionsCol": "", "actionsCol": "",
"remove": "Rimuovi", "remove": "Rimuovi",
"noSkillsPriced": "Nessuna skill ha ancora un prezzo.", "noSkillsPriced": "Nessun pacchetto con prezzo.",
"addSkillLabel": "Aggiungi skill", "addSkillLabel": "Aggiungi pacchetto",
"dailyPriceLabel": "Prezzo/giorno", "dailyPriceLabel": "Prezzo/giorno",
"add": "Aggiungi", "add": "Aggiungi",
"confirmDeleteSkillPrice": "Rimuovere il prezzo per {skill}?", "confirmDeleteSkillPrice": "Rimuovere la tariffazione per {skill}? I periodi già fatturati non sono influenzati.",
"clickToEdit": "Clicca per modificare", "clickToEdit": "Clicca per modificare",
"generateFormTitle": "Genera fattura", "generateFormTitle": "Genera fattura",
"noOrgsToGenerate": "Nessuna organizzazione con tenant trovata.", "noOrgsToGenerate": "Nessuna organizzazione con tenant trovata.",
@@ -667,17 +667,17 @@
"intro": "L'attivazione di {skill} comporterà i seguenti costi:", "intro": "L'attivazione di {skill} comporterà i seguenti costi:",
"setupFeeLabel": "Spese di attivazione", "setupFeeLabel": "Spese di attivazione",
"setupFeeNote": "Una tantum, addebitate solo alla prima attivazione", "setupFeeNote": "Una tantum, addebitate solo alla prima attivazione",
"dailyPriceLabel": "Prezzo giornaliero", "monthlyPriceLabel": "Prezzo mensile",
"dailyPriceNote": "Addebitato per ogni giorno di calendario (UTC) in cui lo skill è attivato", "monthlyPriceNote": "CHF {daily}/giorno attivo; mesi parziali calcolati al giorno",
"dayUnit": "giorno", "monthUnit": "mese",
"disclaimer": "Questi costi appariranno sulla prossima fattura mensile. Confermando accetti di sostenerli.", "disclaimer": "Questi costi appariranno sulla prossima fattura mensile. Confermando accetti di sostenerli.",
"cancel": "Annulla", "cancel": "Annulla",
"confirm": "Conferma & attiva", "confirm": "Conferma & attiva",
"confirming": "Attivazione…" "confirming": "Attivazione…"
}, },
"adminSkills": { "adminSkills": {
"title": "Coda di attivazione skill", "title": "Coda di attivazione",
"subtitle": "Richieste dei clienti per attivare skill che richiedono configurazione manuale lato piattaforma. Approva quando la configurazione è pronta; rifiuta con motivazione se l'attivazione non è possibile.", "subtitle": "Richieste dei clienti per attivare pacchetti che richiedono configurazione manuale lato piattaforma. Approva quando la configurazione è pronta; rifiuta con motivazione se l'attivazione non è possibile.",
"backToAdmin": "Torna ad amministrazione", "backToAdmin": "Torna ad amministrazione",
"emptyQueue": "Nessuna richiesta di attivazione skill in attesa.", "emptyQueue": "Nessuna richiesta di attivazione skill in attesa.",
"requestedAtCol": "Richiesta", "requestedAtCol": "Richiesta",