Skip to content

Commit d8a3b8d

Browse files
feat: newm-admin Earnings dashboard
1 parent 2ac2bef commit d8a3b8d

15 files changed

Lines changed: 1487 additions & 45 deletions

File tree

.agent/task/admin/newm-admin.md

Lines changed: 137 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -76,12 +76,144 @@ graph LR
7676

7777
**Priority:** High
7878

79+
#### API Endpoints
80+
81+
The admin app uses JWT-protected admin endpoints from the backend:
82+
83+
| Endpoint | Method | Purpose |
84+
|----------|--------|---------|
85+
| `/v1/earnings/admin` | GET | Fetch all earnings records |
86+
| `/v1/earnings/admin` | POST | Batch create earnings (accepts `List<Earning>`) |
87+
| `/v1/earnings/admin/{songIdOrIsrc}` | GET | Get earnings by song ID or ISRC |
88+
| `/v1/earnings/admin/{songIdOrIsrc}` | POST | Create royalty splits for a song |
89+
90+
**Earning Model Fields:**
91+
- `id` (UUID), `songId` (UUID), `stakeAddress`, `amount` (Long, 6 decimals)
92+
- `memo`, `startDate`, `endDate`, `claimed`, `claimedAt`, `claimOrderId`
93+
- `createdAt`, `nftPolicyId`, `nftAssetName`
94+
95+
**AddSongRoyaltyRequest:** Either `newmAmount` or `usdAmount` (BigInteger, 6 decimals)
96+
97+
---
98+
99+
#### UI Components
100+
101+
##### Summary Boxes
102+
103+
Three summary cards at the top of the Earnings dashboard, calculated from the filtered data:
104+
105+
| Box | Description | Calculation |
106+
|-----|-------------|-------------|
107+
| **Total Earnings** | Sum of all earnings in current filter | `Σ earning.amount` |
108+
| **Claimed Earnings** | Sum of claimed earnings | `Σ earning.amount WHERE claimed == true` |
109+
| **Unclaimed Earnings** | Sum of unclaimed earnings | `Total - Claimed` |
110+
111+
> [!NOTE]
112+
> All amounts display in NEWM tokens with 6 decimal places, prefixed with `Ɲ` symbol (e.g., `Ɲ 1,234.567890`).
113+
114+
##### Earnings Table
115+
116+
A sortable, filterable table displaying all earnings:
117+
118+
| Column | Sortable | Filterable | Notes |
119+
|--------|----------|------------|-------|
120+
| ID ||| UUID |
121+
| Song ID ||| UUID or display song title if available |
122+
| Stake Address ||| Cardano stake address |
123+
| Amount ||| Display as `Ɲ X.XXXXXX` |
124+
| Memo ||| Freeform text |
125+
| Claimed ||| Boolean (checkbox filter) |
126+
| Claimed At ||| DateTime, null if unclaimed |
127+
| Created At ||| DateTime (supports date range filter) |
128+
129+
**Table Features:**
130+
- **Refresh Button:** Fetches latest data from server
131+
- **Date Range Filter:** Filter by `createdAt` date range (critical requirement)
132+
- **Infinite Scroll:** Virtual list handles large datasets efficiently
133+
134+
##### Toast Notification System
135+
136+
A toast notification system to inform users of API operation results:
137+
138+
| Toast Type | Behavior | Auto-Dismiss |
139+
|------------|----------|--------------|
140+
| **Success** | Green background, checkmark icon | 5 seconds |
141+
| **Warning** | Yellow/orange background, warning icon | Manual X close required |
142+
| **Error** | Red background, error icon | Manual X close required |
143+
144+
Toast content should include:
145+
- Clear message describing success or failure
146+
- For failures: Include error message from API response
147+
- Position: Top-right corner of the work area, stacked if multiple
148+
149+
---
150+
151+
#### Workflows
152+
153+
##### Add Earnings (Single Song)
154+
155+
**Button:** "Add Earnings" in the toolbar
156+
157+
**Dialog Fields:**
158+
| Field | Type | Description |
159+
|-------|------|-------------|
160+
| Song ID or ISRC | Text input | Accepts UUID or ISRC format (e.g., `USRC12345678`) |
161+
| Amount (USD) | Decimal input | User enters USD value (e.g., `10.50`) |
162+
163+
**Process:**
164+
1. User enters songId/ISRC and USD amount
165+
2. Convert amount to 6-decimal integer: `10.50``10500000`
166+
3. Call `POST /v1/earnings/admin/{songIdOrIsrc}` with `{ usdAmount: 10500000 }`
167+
4. On success: Show success toast, refresh table
168+
5. On failure: Show error toast with API error message
169+
170+
##### Upload Earnings CSV
171+
172+
**Button:** "Upload CSV" in the toolbar
173+
174+
**CSV Format (Input):**
175+
```csv
176+
songId_or_isrc,amount_usd
177+
550e8400-e29b-41d4-a716-446655440000,10.50
178+
USRC12345678,25.00
179+
```
180+
181+
**Process:**
182+
1. User selects CSV file via native file picker
183+
2. Parse CSV, validate format (2 columns required)
184+
3. For each row:
185+
- Convert USD to 6-decimal integer
186+
- Call `POST /v1/earnings/admin/{songIdOrIsrc}`
187+
- Record result (success or error message)
188+
4. Modify CSV to add result column
189+
5. Save modified CSV with results
190+
191+
**CSV Format (Output):**
192+
```csv
193+
songId_or_isrc,amount_usd,result
194+
550e8400-e29b-41d4-a716-446655440000,10.50,Success
195+
USRC12345678,25.00,Error: No song found with ISRC: USRC12345678
196+
```
197+
198+
6. Show summary toast: "Processed X records: Y succeeded, Z failed"
199+
7. If any failures: Show warning toast prompting user to check output file
200+
201+
---
202+
203+
#### Implementation Status
204+
79205
| Requirement | Status |
80206
|-------------|--------|
81-
| List all earnings with pagination | ⬜ Not started |
82-
| Filter by artist, song, date range | ⬜ Not started |
83-
| View earnings details | ⬜ Not started |
84-
| Export earnings data | ⬜ Not started |
207+
| List all earnings with sortable, scrollable table | ✅ Completed |
208+
| Filter by date range | ✅ Completed |
209+
| Filter by stake address, song, memo | ✅ Completed |
210+
| Summary boxes (Total/Claimed/Unclaimed) | ✅ Completed |
211+
| NEWM token display with Ɲ symbol | ✅ Completed |
212+
| Refresh button | ✅ Completed |
213+
| Toast notification system | ✅ Completed |
214+
| Add Earnings dialog | ✅ Completed |
215+
| Upload CSV workflow | ⬜ Not started |
216+
| CSV result export | ⬜ Not started |
85217

86218
### 🔲 Refunds Processing
87219

@@ -178,6 +310,7 @@ The admin app communicates with the NEWM backend:
178310

179311
| Date | Feature | Description |
180312
|------|---------|-------------|
313+
| 2026-01-08 | Earnings Table | Full-featured earnings table with filtering, sorting, date range, summary stats, copy-to-clipboard |
181314
| 2026-01-07 | Dashboard | Added sidebar nav with Earnings/Refunds, work area panels |
182315
| 2026-01-07 | Auth | JWT admin validation, login view with environment selector |
183316
| 2026-01-06 | Init | Project setup with GPUI, basic login UI |

newm-admin/Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

newm-admin/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,3 +28,4 @@ anyhow = "1"
2828

2929
# JWT Parsing
3030
base64 = "0.22"
31+
chrono = "0.4.42"

newm-admin/assets/NEWM_Logo.png

8.93 KB
Loading

newm-admin/assets/NEWM_Logo.svg

Lines changed: 1 addition & 0 deletions
Loading

newm-admin/assets/refresh.svg

Lines changed: 1 addition & 0 deletions
Loading

newm-admin/src/NEWM_Logo.png

-727 KB
Binary file not shown.

newm-admin/src/app.rs

Lines changed: 47 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
use gpui::*;
2+
use gpui_component::Root;
23

4+
use crate::auth::{Environment, LoginResponse};
5+
use crate::session::{Session, SessionExpiredEvent};
36
use crate::views::dashboard::DashboardView;
47
use crate::views::login::LoginView;
58

@@ -15,6 +18,7 @@ pub struct AdminApp {
1518
current_view: AppView,
1619
login_view: Entity<LoginView>,
1720
dashboard_view: Entity<DashboardView>,
21+
session: Option<Session>,
1822
}
1923

2024
impl AdminApp {
@@ -25,31 +29,66 @@ impl AdminApp {
2529
// Subscribe to login success events
2630
cx.subscribe(
2731
&login_view,
28-
|this, _login, _event: &LoginSuccessEvent, cx| {
32+
|this, _login, event: &LoginSuccessEvent, cx| {
33+
// Create session from login response
34+
this.session = Some(Session::new(
35+
event.login_response.clone(),
36+
event.environment,
37+
));
38+
39+
// Update dashboard with session
40+
this.dashboard_view.update(cx, |dashboard, cx| {
41+
dashboard.set_session(this.session.clone(), cx);
42+
});
43+
2944
this.current_view = AppView::Dashboard;
3045
cx.notify();
3146
},
3247
)
3348
.detach();
3449

50+
// Subscribe to session expired events from dashboard
51+
cx.subscribe(
52+
&dashboard_view,
53+
|this, _dashboard, event: &SessionExpiredEvent, cx| {
54+
tracing::warn!("Session expired: {}", event.message);
55+
56+
// Clear session and return to login
57+
this.session = None;
58+
this.current_view = AppView::Login;
59+
60+
cx.notify();
61+
},
62+
)
63+
.detach();
64+
3565
Self {
3666
current_view: AppView::default(),
3767
login_view,
3868
dashboard_view,
69+
session: None,
3970
}
4071
}
4172
}
4273

4374
impl Render for AdminApp {
44-
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
45-
div().size_full().child(match self.current_view {
46-
AppView::Login => self.login_view.clone().into_any_element(),
47-
AppView::Dashboard => self.dashboard_view.clone().into_any_element(),
48-
})
75+
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
76+
div()
77+
.size_full()
78+
.child(match self.current_view {
79+
AppView::Login => self.login_view.clone().into_any_element(),
80+
AppView::Dashboard => self.dashboard_view.clone().into_any_element(),
81+
})
82+
// Render notification layer on top
83+
.children(Root::render_notification_layer(window, cx))
4984
}
5085
}
5186

52-
/// Event emitted when login succeeds
53-
pub struct LoginSuccessEvent;
87+
/// Event emitted when login succeeds, contains tokens and environment
88+
pub struct LoginSuccessEvent {
89+
pub login_response: LoginResponse,
90+
pub environment: Environment,
91+
}
5492

5593
impl EventEmitter<LoginSuccessEvent> for LoginView {}
94+
impl EventEmitter<SessionExpiredEvent> for DashboardView {}

0 commit comments

Comments
 (0)