feat: Add tier change functionality to Players module

WHAT WAS DONE:
- Added tier change dropdown in Players Actions column
- Created POST route /admin/players/:discord_id/tier
- Implemented database tier update with MRR recalculation
- Added audit log entry for tier changes
- htmx reload of table after tier change

WHY:
- Trinity needs ability to manually adjust subscriber tiers
- Customer service: upgrades, downgrades, support cases
- Accountability via audit logging
- Last missing feature in Players module

HOW IT WORKS:
- Dropdown shows all tiers (except Admin 1000)
- On change, htmx POSTs to tier change endpoint
- Route updates subscriptions table (tier_level + mrr_value)
- Audit log records who made the change
- After success, table reloads to show updated tier

FEATURES:
- Real-time tier changes without page refresh
- Automatic MRR recalculation
- Audit trail for compliance
- Skips Admin tier (reserved for Trinity)
- Shows current tier as selected in dropdown

IMPACT:
- Trinity can now manage all subscriber tiers manually
- Critical for customer support scenarios
- Completes Players module functionality
- Ready for soft launch customer service

TODO:
- Discord role sync integration (marked in code)
- This requires bot API endpoint to be built

FILES MODIFIED:
- services/arbiter-3.0/src/views/admin/players/_table_body.ejs
- services/arbiter-3.0/src/routes/admin/players.js

TESTED:
- Not yet deployed - needs testing on Command Center

Signed-off-by: Claude (Chronicler #52) <claude@firefrostgaming.com>
This commit is contained in:
Claude (Chronicler #52)
2026-04-01 15:28:31 +00:00
parent 91a14f8cb8
commit 085e60e748
2 changed files with 51 additions and 1 deletions

View File

@@ -32,4 +32,41 @@ router.get('/table', async (req, res) => {
res.render('admin/players/_table_body', { players, TIER_INFO, page, search });
});
// POST endpoint for tier changes
router.post('/:discord_id/tier', async (req, res) => {
const { discord_id } = req.params;
const { tier_level } = req.body;
try {
// Validate tier exists
if (!TIER_INFO[tier_level]) {
return res.status(400).send('Invalid tier');
}
// Update tier in database
const tierInfo = TIER_INFO[tier_level];
await db.query(`
UPDATE subscriptions
SET tier_level = $1, mrr_value = $2, updated_at = NOW()
WHERE discord_id = $3
`, [tier_level, tierInfo.price, discord_id]);
// TODO: Sync Discord roles via bot
// This will be implemented when Discord bot integration is ready
// Create audit log entry
await db.query(`
INSERT INTO admin_audit_log (admin_discord_id, action, target_discord_id, details)
VALUES ($1, 'tier_change', $2, $3)
`, [req.user.id, discord_id, `Changed tier to ${tierInfo.name}`]);
// Return success (htmx will handle UI update)
res.send('OK');
} catch (error) {
console.error('Tier change error:', error);
res.status(500).send('Error updating tier');
}
});
module.exports = router;

View File

@@ -28,7 +28,20 @@
</span>
</td>
<td class="px-6 py-4 text-right">
<span class="text-gray-400 text-sm">(Coming Soon)</span>
<form hx-post="/admin/players/<%= player.discord_id %>/tier"
hx-trigger="change from:select"
hx-on::after-request="htmx.ajax('GET', '/admin/players/table', {target:'#player-table-body', swap:'innerHTML'})">
<select class="bg-gray-50 dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded px-2 py-1 text-sm"
name="tier_level">
<option value="">Change Tier...</option>
<% Object.keys(TIER_INFO).sort((a, b) => parseInt(b) - parseInt(a)).forEach(tierLevel => {
const tierData = TIER_INFO[tierLevel];
if (tierLevel == 1000) return; // Skip Admin tier
%>
<option value="<%= tierLevel %>" <%= player.tier_level == tierLevel ? 'selected' : '' %>><%= tierData.name %> ($<%= tierData.price %>)</option>
<% }); %>
</select>
</form>
</td>
</tr>
<% }) %>