From 88d9ebbfc2b38fd90b30436b238ee5f464cb36bc Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Date: Sun, 12 Apr 2026 12:17:10 +0200 Subject: [PATCH 001/117] fix(ci): use REGISTRY_HOSTPORT secret for private registry --- .woodpecker.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.woodpecker.yml b/.woodpecker.yml index fa31b4e..0196127 100644 --- a/.woodpecker.yml +++ b/.woodpecker.yml @@ -6,7 +6,7 @@ steps: - name: build-and-push image: woodpeckerci/plugin-kaniko:2.1.1 settings: - registry: docker-registry.registry.svc.cluster.local:5000 + registry: ${REGISTRY_HOSTPORT} repo: nxtgauge-admin-solid dockerfile: Dockerfile.simple tags: From 21ffeda6114f5445274a2c8d4948dfedd41580a8 Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Date: Sun, 12 Apr 2026 12:40:52 +0200 Subject: [PATCH 002/117] chore(ci): trigger pipeline with updated registry secrets From eb235676b4bffc14314d3dcf89eb636e63da216d Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Date: Sun, 12 Apr 2026 13:44:24 +0200 Subject: [PATCH 003/117] fix(ci): use from_secret for REGISTRY_HOSTPORT in kaniko --- .woodpecker.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.woodpecker.yml b/.woodpecker.yml index 0196127..35f0c0a 100644 --- a/.woodpecker.yml +++ b/.woodpecker.yml @@ -6,7 +6,8 @@ steps: - name: build-and-push image: woodpeckerci/plugin-kaniko:2.1.1 settings: - registry: ${REGISTRY_HOSTPORT} + registry: + from_secret: REGISTRY_HOSTPORT repo: nxtgauge-admin-solid dockerfile: Dockerfile.simple tags: From c628c968ae091e47278287dbb194206b121e23c1 Mon Sep 17 00:00:00 2001 From: Tracewebstudio Dev Date: Sun, 12 Apr 2026 18:08:57 +0200 Subject: [PATCH 004/117] chore(ci): trigger rebuild From a8b1b7cd117afde7dae49e4d0a8cad53dc09d786 Mon Sep 17 00:00:00 2001 From: Tracewebstudio Dev Date: Mon, 13 Apr 2026 01:36:15 +0200 Subject: [PATCH 005/117] feat: multi-select roles in pricing management UI --- src/routes/admin/pricing.tsx | 858 ++++++++++++++++++++++++++--------- 1 file changed, 633 insertions(+), 225 deletions(-) diff --git a/src/routes/admin/pricing.tsx b/src/routes/admin/pricing.tsx index b67b7b0..299ff8d 100644 --- a/src/routes/admin/pricing.tsx +++ b/src/routes/admin/pricing.tsx @@ -1,18 +1,18 @@ -import { createSignal, createMemo, onMount, Show, For } from 'solid-js'; +import { createSignal, createMemo, onMount, Show, For } from "solid-js"; -const API = ''; +const API = ""; function getToken(): string { - return typeof sessionStorage !== 'undefined' - ? sessionStorage.getItem('nxtgauge_admin_access_token') || '' - : ''; + return typeof sessionStorage !== "undefined" + ? sessionStorage.getItem("nxtgauge_admin_access_token") || "" + : ""; } function authHeaders(): Record { const token = getToken(); return { - Accept: 'application/json', - 'Content-Type': 'application/json', + Accept: "application/json", + "Content-Type": "application/json", ...(token ? { Authorization: `Bearer ${token}` } : {}), }; } @@ -20,71 +20,124 @@ function authHeaders(): Record { type Package = { id: string; name: string; - role: string; - tracecoin_amount: number; - price_inr: number; - bonus_percentage?: number; + description?: string; + package_type: string; + applicable_roles: string[]; + tracecoins_amount: number; + price: number; + duration_days?: number; + valid_from?: string; + valid_until?: string; + is_promotional: boolean; is_active: boolean; + features?: any; + created_at: string; + updated_at: string; + is_available?: boolean; + is_expired?: boolean; }; -const ROLES = [ - 'company', 'customer', 'job_seeker', 'photographer', 'video_editor', - 'graphic_designer', 'social_media_manager', 'fitness_trainer', - 'catering_services', 'makeup_artist', 'tutor', 'developer', 'ugc_content_creator', +const PACKAGE_TYPES = [ + { value: "TRACECOIN_BUNDLE", label: "Tracecoin Bundle" }, + { value: "CONTACT_VIEWS", label: "Contact Views (Company)" }, + { value: "JOB_POSTING", label: "Job Posting (Company)" }, + { value: "LEAD_REQUEST", label: "Lead Request (Professional)" }, + { value: "REQUIREMENT_SLOTS", label: "Requirement Slots (Customer)" }, ]; -type SortMode = 'name_asc' | 'name_desc' | 'price_asc' | 'price_desc' | 'coins_asc' | 'coins_desc'; +const ALL_ROLES = [ + "COMPANY", + "CUSTOMER", + "JOB_SEEKER", + "PHOTOGRAPHER", + "VIDEO_EDITOR", + "GRAPHIC_DESIGNER", + "SOCIAL_MEDIA_MANAGER", + "FITNESS_TRAINER", + "CATERING_SERVICE", + "MAKEUP_ARTIST", + "TUTOR", + "DEVELOPER", + "UGC_CONTENT_CREATOR", +]; + +const ROLE_LABELS: Record = { + COMPANY: "Company", + CUSTOMER: "Customer", + JOB_SEEKER: "Job Seeker", + PHOTOGRAPHER: "Photographer", + VIDEO_EDITOR: "Video Editor", + GRAPHIC_DESIGNER: "Graphic Designer", + SOCIAL_MEDIA_MANAGER: "Social Media Manager", + FITNESS_TRAINER: "Fitness Trainer", + CATERING_SERVICE: "Catering Service", + MAKEUP_ARTIST: "Makeup Artist", + TUTOR: "Tutor", + DEVELOPER: "Developer", + UGC_CONTENT_CREATOR: "UGC Creator", +}; + +type SortMode = "name_asc" | "name_desc" | "price_asc" | "price_desc" | "coins_asc" | "coins_desc"; const SORT_LABELS: Record = { - name_asc: 'Name A→Z', name_desc: 'Name Z→A', - price_asc: 'Price ↑', price_desc: 'Price ↓', - coins_asc: 'TraceCoins ↑', coins_desc: 'TraceCoins ↓', + name_asc: "Name A→Z", + name_desc: "Name Z→A", + price_asc: "Price ↑", + price_desc: "Price ↓", + coins_asc: "TraceCoins ↑", + coins_desc: "TraceCoins ↓", }; export default function PricingPage() { const [rows, setRows] = createSignal([]); const [loading, setLoading] = createSignal(true); - const [loadError, setLoadError] = createSignal(''); - const [view, setView] = createSignal<'packages' | 'create'>('packages'); + const [loadError, setLoadError] = createSignal(""); + const [view, setView] = createSignal<"packages" | "create">("packages"); // Filters - const [search, setSearch] = createSignal(''); - const [roleFilter, setRoleFilter] = createSignal('all'); - const [statusFilter, setStatusFilter] = createSignal('all'); - const [sortBy, setSortBy] = createSignal('name_asc'); + const [search, setSearch] = createSignal(""); + const [typeFilter, setTypeFilter] = createSignal("all"); + const [statusFilter, setStatusFilter] = createSignal("all"); + const [sortBy, setSortBy] = createSignal("name_asc"); const [sortOpen, setSortOpen] = createSignal(false); // Inline edit - const [editingId, setEditingId] = createSignal(''); - const [editName, setEditName] = createSignal(''); - const [editTracecoins, setEditTracecoins] = createSignal(''); - const [editPrice, setEditPrice] = createSignal(''); + const [editingId, setEditingId] = createSignal(""); + const [editName, setEditName] = createSignal(""); + const [editTracecoins, setEditTracecoins] = createSignal(""); + const [editPrice, setEditPrice] = createSignal(""); const [editSaving, setEditSaving] = createSignal(false); - const [editError, setEditError] = createSignal(''); - const [togglingId, setTogglingId] = createSignal(''); + const [editError, setEditError] = createSignal(""); + const [togglingId, setTogglingId] = createSignal(""); // Create form - const [cName, setCName] = createSignal(''); - const [cRole, setCRole] = createSignal(ROLES[0]); - const [cTracecoins, setCTracecoins] = createSignal(''); - const [cPrice, setCPrice] = createSignal(''); - const [cBonus, setCBonus] = createSignal(''); + const [cName, setCName] = createSignal(""); + const [cDescription, setCDescription] = createSignal(""); + const [cType, setCType] = createSignal("TRACECOIN_BUNDLE"); + const [cRoles, setCRoles] = createSignal([]); + const [cTracecoins, setCTracecoins] = createSignal(""); + const [cPrice, setCPrice] = createSignal(""); + const [cDuration, setCDuration] = createSignal(""); + const [cValidFrom, setCValidFrom] = createSignal(""); + const [cValidUntil, setCValidUntil] = createSignal(""); + const [cPromotional, setCPromotional] = createSignal(false); const [cSaving, setCsaving] = createSignal(false); - const [cError, setCError] = createSignal(''); + const [cError, setCError] = createSignal(""); + const [roleDropdownOpen, setRoleDropdownOpen] = createSignal(false); const load = async () => { setLoading(true); - setLoadError(''); + setLoadError(""); try { - const res = await fetch(`${API}/api/admin/tracecoin-packages`, { + const res = await fetch(`${API}/api/packages`, { headers: authHeaders(), - credentials: 'include', + credentials: "include", }); if (!res.ok) throw new Error(`Request failed (${res.status})`); const data = await res.json(); - setRows(Array.isArray(data) ? data : (data.packages ?? [])); + setRows(Array.isArray(data) ? data : (data.data ?? [])); } catch (err: any) { - setLoadError(err.message || 'Could not load packages.'); + setLoadError(err.message || "Could not load packages."); setRows([]); } finally { setLoading(false); @@ -96,276 +149,631 @@ export default function PricingPage() { const filteredRows = createMemo(() => { let r = rows(); const q = search().toLowerCase(); - if (q) r = r.filter((p) => p.name.toLowerCase().includes(q) || p.role.toLowerCase().includes(q)); - if (roleFilter() !== 'all') r = r.filter((p) => p.role === roleFilter()); - if (statusFilter() === 'active') r = r.filter((p) => p.is_active); - if (statusFilter() === 'inactive') r = r.filter((p) => !p.is_active); + if (q) + r = r.filter( + (p) => + p.name.toLowerCase().includes(q) || + p.package_type.toLowerCase().includes(q) || + p.applicable_roles.some((r) => r.toLowerCase().includes(q)) + ); + if (typeFilter() !== "all") r = r.filter((p) => p.package_type === typeFilter()); + if (statusFilter() === "active") r = r.filter((p) => p.is_active); + if (statusFilter() === "inactive") r = r.filter((p) => !p.is_active); const sorted = [...r]; const mode = sortBy(); sorted.sort((a, b) => { - if (mode === 'name_desc') return b.name.localeCompare(a.name); - if (mode === 'price_asc') return a.price_inr - b.price_inr; - if (mode === 'price_desc') return b.price_inr - a.price_inr; - if (mode === 'coins_asc') return a.tracecoin_amount - b.tracecoin_amount; - if (mode === 'coins_desc') return b.tracecoin_amount - a.tracecoin_amount; + if (mode === "name_desc") return b.name.localeCompare(a.name); + if (mode === "price_asc") return a.price - b.price; + if (mode === "price_desc") return b.price - a.price; + if (mode === "coins_asc") return a.tracecoins_amount - b.tracecoins_amount; + if (mode === "coins_desc") return b.tracecoins_amount - a.tracecoins_amount; return a.name.localeCompare(b.name); }); return sorted; }); const startEdit = (pkg: Package) => { - setEditingId(pkg.id); setEditName(pkg.name); - setEditTracecoins(String(pkg.tracecoin_amount)); setEditPrice(String(pkg.price_inr)); - setEditError(''); + setEditingId(pkg.id); + setEditName(pkg.name); + setEditTracecoins(String(pkg.tracecoins_amount)); + setEditPrice(String(pkg.price)); + setEditError(""); + }; + const cancelEdit = () => { + setEditingId(""); + setEditError(""); }; - const cancelEdit = () => { setEditingId(''); setEditError(''); }; const saveEdit = async (id: string) => { try { - setEditSaving(true); setEditError(''); - const res = await fetch(`${API}/api/admin/tracecoin-packages/${id}`, { - method: 'PATCH', headers: authHeaders(), credentials: 'include', - body: JSON.stringify({ name: editName(), tracecoin_amount: Number(editTracecoins()), price_inr: Number(editPrice()) }), + setEditSaving(true); + setEditError(""); + const res = await fetch(`${API}/api/packages/${id}`, { + method: "PATCH", + headers: authHeaders(), + credentials: "include", + body: JSON.stringify({ + name: editName(), + tracecoins_amount: Number(editTracecoins()), + price: Number(editPrice()), + }), }); - if (!res.ok) throw new Error('Failed to save'); - setEditingId(''); await load(); - } catch (err: any) { setEditError(err.message || 'Failed to save'); } - finally { setEditSaving(false); } + if (!res.ok) throw new Error("Failed to save"); + setEditingId(""); + await load(); + } catch (err: any) { + setEditError(err.message || "Failed to save"); + } finally { + setEditSaving(false); + } }; const toggleActive = async (pkg: Package) => { try { setTogglingId(pkg.id); - await fetch(`${API}/api/admin/tracecoin-packages/${pkg.id}`, { - method: 'PATCH', headers: authHeaders(), credentials: 'include', + await fetch(`${API}/api/packages/${pkg.id}`, { + method: "PATCH", + headers: authHeaders(), + credentials: "include", body: JSON.stringify({ is_active: !pkg.is_active }), }); await load(); - } catch { /* ignore */ } finally { setTogglingId(''); } + } catch { + /* ignore */ + } finally { + setTogglingId(""); + } + }; + + const toggleRole = (role: string) => { + const current = cRoles(); + if (current.includes(role)) { + setCRoles(current.filter((r) => r !== role)); + } else { + setCRoles([...current, role]); + } }; const handleCreate = async (e: Event) => { e.preventDefault(); try { - setCsaving(true); setCError(''); + setCsaving(true); + setCError(""); const body: Record = { - name: cName(), role: cRole(), - tracecoin_amount: Number(cTracecoins()), price_inr: Number(cPrice()), + name: cName(), + description: cDescription() || undefined, + package_type: cType(), + applicable_roles: cRoles(), + tracecoins_amount: Number(cTracecoins()), + price: Number(cPrice()), + is_promotional: cPromotional(), }; - if (cBonus()) body.bonus_percentage = Number(cBonus()); - const res = await fetch(`${API}/api/admin/tracecoin-packages`, { - method: 'POST', headers: authHeaders(), credentials: 'include', + if (cDuration()) body.duration_days = Number(cDuration()); + if (cValidFrom()) body.valid_from = new Date(cValidFrom()).toISOString(); + if (cValidUntil()) body.valid_until = new Date(cValidUntil()).toISOString(); + + const res = await fetch(`${API}/api/packages`, { + method: "POST", + headers: authHeaders(), + credentials: "include", body: JSON.stringify(body), }); - if (!res.ok) throw new Error('Failed to create package'); - setCName(''); setCRole(ROLES[0]); setCTracecoins(''); setCPrice(''); setCBonus(''); - setView('packages'); await load(); - } catch (err: any) { setCError(err.message || 'Failed to create'); } - finally { setCsaving(false); } + if (!res.ok) throw new Error("Failed to create package"); + setCName(""); + setCDescription(""); + setCType("TRACECOIN_BUNDLE"); + setCRoles([]); + setCTracecoins(""); + setCPrice(""); + setCDuration(""); + setCValidFrom(""); + setCValidUntil(""); + setCPromotional(false); + setView("packages"); + await load(); + } catch (err: any) { + setCError(err.message || "Failed to create"); + } finally { + setCsaving(false); + } + }; + + const formatDate = (dateStr?: string) => { + if (!dateStr) return "—"; + return new Date(dateStr).toLocaleDateString("en-IN", { + day: "2-digit", + month: "short", + year: "numeric", + }); + }; + + const getTypeLabel = (type: string) => { + return PACKAGE_TYPES.find((t) => t.value === type)?.label || type; }; return (

Pricing Management

-

Create and manage TraceCoin packages

+

+ Create and manage TraceCoin packages for all roles +

{/* Tabs */}
- {(['packages', 'create'] as const).map((t) => ( + {(["packages", "create"] as const).map((t) => ( ))}
- {/* ── Packages list ── */} - +
-
- setSearch(e.currentTarget.value)} - style="height:34px;flex:1;min-width:220px;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 12px;font-size:13px;color:#111827;outline:none" - /> - - -
+
+ setSearch(e.currentTarget.value)} + style="height:34px;flex:1;min-width:220px;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 12px;font-size:13px;color:#111827;outline:none" + /> + + +
+ + +
+ + {([key, label]) => ( + + )} + +
+
+
- -
- - {([key, label]) => ( - - )} - -
-
- -
- -
{loadError()}
-
+ +
+ {loadError()} +
+
-
-
- - - - - - - - - - - - - - 0}> - - {(pkg) => ( - <> - - - - - - - - + + + + + )} + + + +
NameRoleTraceCoinsPrice (₹)BonusStatusActions
Loading...
No packages found.
{pkg.name}{pkg.role}{pkg.tracecoin_amount}₹{(pkg.price_inr / 100).toFixed(2)}{pkg.bonus_percentage != null ? `${pkg.bonus_percentage}%` : '—'} - - - {pkg.is_active ? 'Active' : 'Inactive'} - - -
- -
+ +
+ {editError()} +
+
+
+
+ + setEditName(e.currentTarget.value)} + style="padding:7px 10px;border:1px solid #e2e8f0;border-radius:6px;font-size:13px;width:180px" + /> +
+
+ + setEditTracecoins(e.currentTarget.value)} + style="padding:7px 10px;border:1px solid #e2e8f0;border-radius:6px;font-size:13px;width:110px" + /> +
+
+ + setEditPrice(e.currentTarget.value)} + style="padding:7px 10px;border:1px solid #e2e8f0;border-radius:6px;font-size:13px;width:110px" + /> +
+
+ + +
+
+
- -
+ +
+ {filteredRows().length} of {rows().length} packages +
+
+
{/* ── Create Package ── */} - -
+ +

New Package

-
{cError()}
+
+ {cError()} +
- - setCName(e.currentTarget.value)} required placeholder="e.g. Starter Pack" style="width:100%;padding:8px 10px;border:1px solid #e2e8f0;border-radius:6px;font-size:14px;box-sizing:border-box" /> + + setCName(e.currentTarget.value)} + required + placeholder="e.g. Christmas Special - 50 Tracecoins" + style="width:100%;padding:8px 10px;border:1px solid #e2e8f0;border-radius:6px;font-size:14px;box-sizing:border-box" + />
- -