feat: Add Admin tier to dropdown and staff tracking
WHAT WAS DONE: - Added Admin tier (1000) back to tier dropdown - Added is_staff toggle checkbox in Actions column - Created POST route /admin/players/:discord_id/staff - Updated query to include is_staff from users table - Both tier and staff status tracked separately WHY: - Trinity needs ability to assign Admin tier to team members - Staff can also be subscribers - need to track both - Example: Moderator who also pays for Elemental tier - Separate tracking prevents conflating employment and subscription HOW IT WORKS: - Tier dropdown shows ALL tiers including Admin - Staff checkbox toggles is_staff on users table - Both changes create separate audit log entries - Staff flag independent of subscription tier DATABASE REQUIREMENT: - Requires migration: ALTER TABLE users ADD COLUMN is_staff BOOLEAN DEFAULT FALSE; - Must be run before deploying this code FEATURES: - Admin tier assignable via dropdown - Staff toggle with visual checkbox - Both tracked in audit log separately - Tier + Staff shown side-by-side in Actions column IMPACT: - Can now hire staff and track their employment - Staff can also be subscribers without conflict - Clear separation of concerns - Ready for team expansion FILES MODIFIED: - services/arbiter-3.0/src/views/admin/players/_table_body.ejs - services/arbiter-3.0/src/routes/admin/players.js DEPLOYMENT STEPS: 1. Run database migration (ADD is_staff column) 2. Deploy code files 3. Restart arbiter-3 service Signed-off-by: Claude (Chronicler #52) <claude@firefrostgaming.com>
This commit is contained in:
@@ -17,7 +17,7 @@ router.get('/table', async (req, res) => {
|
||||
|
||||
// Basic search implementation
|
||||
let query = `
|
||||
SELECT u.discord_id, u.minecraft_username, u.minecraft_uuid,
|
||||
SELECT u.discord_id, u.minecraft_username, u.minecraft_uuid, u.is_staff,
|
||||
s.tier_level, s.status, s.updated_at
|
||||
FROM users u
|
||||
LEFT JOIN subscriptions s ON u.discord_id = s.discord_id
|
||||
@@ -69,4 +69,34 @@ router.post('/:discord_id/tier', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// POST endpoint for staff toggle
|
||||
router.post('/:discord_id/staff', async (req, res) => {
|
||||
const { discord_id } = req.params;
|
||||
|
||||
try {
|
||||
// Toggle staff status
|
||||
await db.query(`
|
||||
UPDATE users
|
||||
SET is_staff = NOT COALESCE(is_staff, FALSE)
|
||||
WHERE discord_id = $1
|
||||
`, [discord_id]);
|
||||
|
||||
// Get new status for audit log
|
||||
const { rows } = await db.query(`SELECT is_staff FROM users WHERE discord_id = $1`, [discord_id]);
|
||||
const newStatus = rows[0]?.is_staff ? 'Staff' : 'Non-Staff';
|
||||
|
||||
// Create audit log entry
|
||||
await db.query(`
|
||||
INSERT INTO admin_audit_log (admin_discord_id, action, target_discord_id, details)
|
||||
VALUES ($1, 'staff_toggle', $2, $3)
|
||||
`, [req.user.id, discord_id, `Changed to ${newStatus}`]);
|
||||
|
||||
res.send('OK');
|
||||
|
||||
} catch (error) {
|
||||
console.error('Staff toggle error:', error);
|
||||
res.status(500).send('Error updating staff status');
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
|
||||
@@ -28,20 +28,33 @@
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-right">
|
||||
<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>
|
||||
<div class="flex items-center justify-end gap-2">
|
||||
<!-- Tier Change Dropdown -->
|
||||
<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="">Tier...</option>
|
||||
<% Object.keys(TIER_INFO).sort((a, b) => parseInt(b) - parseInt(a)).forEach(tierLevel => {
|
||||
const tierData = TIER_INFO[tierLevel];
|
||||
%>
|
||||
<option value="<%= tierLevel %>" <%= player.tier_level == tierLevel ? 'selected' : '' %>><%= tierData.name %></option>
|
||||
<% }); %>
|
||||
</select>
|
||||
</form>
|
||||
|
||||
<!-- Staff Toggle -->
|
||||
<label class="flex items-center gap-1 cursor-pointer" title="Staff Member">
|
||||
<input type="checkbox"
|
||||
class="rounded border-gray-300 dark:border-gray-600"
|
||||
<%= player.is_staff ? 'checked' : '' %>
|
||||
hx-post="/admin/players/<%= player.discord_id %>/staff"
|
||||
hx-trigger="change"
|
||||
hx-on::after-request="htmx.ajax('GET', '/admin/players/table', {target:'#player-table-body', swap:'innerHTML'})">
|
||||
<span class="text-xs text-gray-600 dark:text-gray-400">Staff</span>
|
||||
</label>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<% }) %>
|
||||
|
||||
Reference in New Issue
Block a user