diff --git a/src/app/(frontend)/helpdesk/page.tsx b/src/app/(frontend)/helpdesk/page.tsx index 733c8a7..f204db2 100644 --- a/src/app/(frontend)/helpdesk/page.tsx +++ b/src/app/(frontend)/helpdesk/page.tsx @@ -12,6 +12,22 @@ export const metadata = { title: "Helpdesk: Polaris Task Force", }; +function HelpdeskDisabled() { + return ( +
+
+
+ +

Helpdesk

+
+

+ The Helpdesk is currently disabled by a unit admin. +

+
+
+ ); +} + export default async function HelpdeskPage() { const headers = await nextHeaders(); const payload = await getPayload({ config }); @@ -25,6 +41,11 @@ export default async function HelpdeskPage() { redirect(`/login?returnTo=${encodeURIComponent(pathname)}`); } + const { helpdeskEnabled } = await payload.findGlobal({ slug: "game-rules" }); + if (!helpdeskEnabled) { + return ; + } + const ticketsRes = await payload.find({ collection: "tickets", sort: "-updatedAt", diff --git a/src/app/(frontend)/layout.tsx b/src/app/(frontend)/layout.tsx index 1673980..4eff9d9 100644 --- a/src/app/(frontend)/layout.tsx +++ b/src/app/(frontend)/layout.tsx @@ -80,46 +80,57 @@ export default async function RootLayout(props: { children: React.ReactNode }) { const shimEntries = (shimsGlobal.shims ?? []) as NonNullable; let isAdmin = false; + let statisticsEnabled = true; + let helpdeskEnabled = true; if (user) { - const [intelligenceQual, logisticsQual, profileRes, experienceRes, personalAccountRes] = - await Promise.all([ - hasIntelligenceQualification(payload, user).catch(() => false), - hasLogisticsQualification(payload, user).catch(() => false), - payload.find({ - collection: "profiles", - where: { user: { equals: user.id } }, - select: { - rank: true, - progression: { - awards: true, - scoreboard: true, - xp: true, - minigames: true, - }, + const [ + intelligenceQual, + logisticsQual, + profileRes, + experienceRes, + personalAccountRes, + gameRulesRes, + ] = await Promise.all([ + hasIntelligenceQualification(payload, user).catch(() => false), + hasLogisticsQualification(payload, user).catch(() => false), + payload.find({ + collection: "profiles", + where: { user: { equals: user.id } }, + select: { + rank: true, + progression: { + awards: true, + scoreboard: true, + xp: true, + minigames: true, }, - }), - payload.find({ - collection: "experience", - sort: "requiredPoints", - limit: 100, - select: { - name: true, - description: true, - requiredPoints: true, - }, - }), - payload.find({ - collection: "bank-accounts", - where: { - and: [{ accountType: { equals: "personal" } }, { ownerUser: { equals: user.id } }], - }, - limit: 1, - }), - ]); + }, + }), + payload.find({ + collection: "experience", + sort: "requiredPoints", + limit: 100, + select: { + name: true, + description: true, + requiredPoints: true, + }, + }), + payload.find({ + collection: "bank-accounts", + where: { + and: [{ accountType: { equals: "personal" } }, { ownerUser: { equals: user.id } }], + }, + limit: 1, + }), + payload.findGlobal({ slug: "game-rules" }), + ]); isIntelligence = intelligenceQual; isLogistics = logisticsQual; hasPersonalBankAccount = personalAccountRes.docs.length > 0; + statisticsEnabled = gameRulesRes.statisticsEnabled ?? true; + helpdeskEnabled = gameRulesRes.helpdeskEnabled ?? true; const userXp = profileRes.docs[0]?.progression?.xp ?? 0; xpLevel = resolveLevel(userXp, experienceRes.docs); @@ -150,6 +161,8 @@ export default async function RootLayout(props: { children: React.ReactNode }) { adminUrl={isAdmin ? payloadConfig.routes.admin : undefined} hasIntelligence={isIntelligence} hasLogistics={isLogistics} + statisticsEnabled={statisticsEnabled} + helpdeskEnabled={helpdeskEnabled} showCallsign={user.preferences?.display?.showCallsign ?? false} canImpersonate={await isSuperuser(payload, user)} preferences={user.preferences} diff --git a/src/app/(frontend)/personnel/statistics/page.tsx b/src/app/(frontend)/personnel/statistics/page.tsx index daecf13..88a8932 100644 --- a/src/app/(frontend)/personnel/statistics/page.tsx +++ b/src/app/(frontend)/personnel/statistics/page.tsx @@ -32,8 +32,28 @@ function npcIdFromListing(listing: MarketListing): number | null { return null; } +function StatisticsDisabled() { + return ( +
+
+
+ +

Statistics

+
+

+ Personnel statistics are currently disabled by a unit admin. +

+
+
+ ); +} + export default async function StatisticsPage() { const payload = await getPayload({ config }); + const { statisticsEnabled } = await payload.findGlobal({ slug: "game-rules" }); + if (!statisticsEnabled) { + return ; + } const [npcsRes, listingsRes] = await Promise.all([ payload.find({ collection: "game-npcs", diff --git a/src/collections/game/GameRules.ts b/src/collections/game/GameRules.ts index 70dba61..b377f18 100644 --- a/src/collections/game/GameRules.ts +++ b/src/collections/game/GameRules.ts @@ -226,5 +226,31 @@ export const GameRules: GlobalConfig = { }, ], }, + { + name: "zonePressureEnabled", + type: "checkbox", + defaultValue: true, + admin: { + description: + "When off, the Zone Pressure feature is hidden from the Operations feed (the overview card and the AAR section).", + }, + }, + { + name: "statisticsEnabled", + type: "checkbox", + defaultValue: true, + admin: { + description: + "When off, the Personnel > Statistics page and its navigation entry are hidden.", + }, + }, + { + name: "helpdeskEnabled", + type: "checkbox", + defaultValue: true, + admin: { + description: "When off, the Helpdesk page and its navigation entry are hidden.", + }, + }, ], }; diff --git a/src/components/frontend/blocks/AppSidebar.tsx b/src/components/frontend/blocks/AppSidebar.tsx index 88f6399..1010fa6 100644 --- a/src/components/frontend/blocks/AppSidebar.tsx +++ b/src/components/frontend/blocks/AppSidebar.tsx @@ -72,6 +72,8 @@ export function AppSidebar({ adminUrl, hasIntelligence = true, hasLogistics = true, + statisticsEnabled = true, + helpdeskEnabled = true, showCallsign = false, canImpersonate = false, preferences, @@ -86,6 +88,8 @@ export function AppSidebar({ adminUrl?: string; hasIntelligence?: boolean; hasLogistics?: boolean; + statisticsEnabled?: boolean; + helpdeskEnabled?: boolean; showCallsign?: boolean; canImpersonate?: boolean; preferences?: NonNullable; @@ -119,6 +123,19 @@ export function AppSidebar({ } }, [isMiniMode, preferences]); + // Game-flags: hide the Helpdesk group and the Personnel -> Statistics entry + // when their feature is toggled off (data preserved, front-end only). An + // empty filtered list makes NavCore skip the group entirely, label included. + const filteredNavLogistics = data.navLogistics.map((section) => + section.title === "Personnel" + ? { + ...section, + items: section.items.filter((item) => item.title === "Contacts" || statisticsEnabled), + } + : section, + ); + const filteredNavHelpdesk = helpdeskEnabled ? data.navHelpdesk : []; + return ( @@ -155,10 +172,10 @@ export function AppSidebar({ )} {hasLogistics && ( - + )} - + {/**/} diff --git a/src/components/frontend/operations/AarView.tsx b/src/components/frontend/operations/AarView.tsx index 6dab612..e800688 100644 --- a/src/components/frontend/operations/AarView.tsx +++ b/src/components/frontend/operations/AarView.tsx @@ -114,24 +114,28 @@ export function AarView({ aar }: { aar: AarResult }) { )} -
- {aar.pressure.length === 0 ? ( - - ) : ( -
    - {aar.pressure.map((p, i) => ( -
  • - Zone {p.zoneId} - = 0 ? "text-emerald-400" : "text-rose-400"}`} - > - {p.delta >= 0 ? `+${p.delta}` : p.delta} - -
  • - ))} -
- )} -
+ {aar.canSeePressure ? ( +
+ {aar.pressure.length === 0 ? ( + + ) : ( +
    + {aar.pressure.map((p, i) => ( +
  • + Zone {p.zoneId} + = 0 ? "text-emerald-400" : "text-rose-400"}`} + > + {p.delta >= 0 ? `+${p.delta}` : p.delta} + +
  • + ))} +
+ )} +
+ ) : ( + + )}
{aar.reservations.length === 0 ? ( diff --git a/src/lib/operations/surface.ts b/src/lib/operations/surface.ts index 9f0b61a..9faad29 100644 --- a/src/lib/operations/surface.ts +++ b/src/lib/operations/surface.ts @@ -88,12 +88,18 @@ export async function getSurfaceCapabilities( payload: PayloadType, user: SurfaceUser, ): Promise { - const [reservations, readiness, pressure] = await Promise.all([ + const [reservations, readiness, pressure, rules] = await Promise.all([ hasPermission(payload, user, "operation-reservations:read"), hasPermission(payload, user, "operation-readiness:read"), hasPermission(payload, user, "zone-pressure:read"), + payload.findGlobal({ slug: "game-rules" }), ]); - return { canSeeReservations: reservations, canSeeReadiness: readiness, canSeePressure: pressure }; + const zonePressureEnabled = rules.zonePressureEnabled ?? true; + return { + canSeeReservations: reservations, + canSeeReadiness: readiness, + canSeePressure: pressure && zonePressureEnabled, + }; } function relationId(value: unknown): number | null { diff --git a/src/lib/operations/surfaceQuery.ts b/src/lib/operations/surfaceQuery.ts index 65b395a..18a4de0 100644 --- a/src/lib/operations/surfaceQuery.ts +++ b/src/lib/operations/surfaceQuery.ts @@ -41,6 +41,7 @@ export interface AarResult { effects: OperationEffectRow[]; counts: AarCounts; pressure: { zoneId: number; factionId: number | null; delta: number; createdAt: string }[]; + canSeePressure: boolean; reservations: { reservationKey: string; status: string; expiresAt: string | null }[]; } @@ -195,11 +196,16 @@ export async function getOperationAar( else if (status === "reversed") counts.reversed += 1; } - const canSeePressure = await hasPermission(payload, user, "zone-pressure:read"); - const canSeeReservations = await hasPermission(payload, user, "operation-reservations:read"); + const [rules, canSeePressure, canSeeReservations] = await Promise.all([ + payload.findGlobal({ slug: "game-rules" }), + hasPermission(payload, user, "zone-pressure:read"), + hasPermission(payload, user, "operation-reservations:read"), + ]); + const zonePressureEnabled = rules.zonePressureEnabled ?? true; + const canSeePressureFlag = canSeePressure && zonePressureEnabled; const pressure: AarResult["pressure"] = []; - if (canSeePressure) { + if (canSeePressureFlag) { const rows = await payload.find({ collection: "zone-pressure", where: { operationId: { equals: operationId } }, @@ -254,6 +260,7 @@ export async function getOperationAar( effects: effectsFound.docs.map(toEffectRow), counts, pressure, + canSeePressure: canSeePressureFlag, reservations, }; } diff --git a/src/migrations/20260921_150000_add_feature_flags.ts b/src/migrations/20260921_150000_add_feature_flags.ts new file mode 100644 index 0000000..9b07c9d --- /dev/null +++ b/src/migrations/20260921_150000_add_feature_flags.ts @@ -0,0 +1,15 @@ +import { MigrateUpArgs, MigrateDownArgs, sql } from '@payloadcms/db-postgres' + +export async function up({ db, payload, req }: MigrateUpArgs): Promise { + await db.execute(sql` + ALTER TABLE "game_rules" ADD COLUMN "zone_pressure_enabled" boolean DEFAULT true; + ALTER TABLE "game_rules" ADD COLUMN "statistics_enabled" boolean DEFAULT true; + ALTER TABLE "game_rules" ADD COLUMN "helpdesk_enabled" boolean DEFAULT true;`) +} + +export async function down({ db, payload, req }: MigrateDownArgs): Promise { + await db.execute(sql` + ALTER TABLE "game_rules" DROP COLUMN "zone_pressure_enabled"; + ALTER TABLE "game_rules" DROP COLUMN "statistics_enabled"; + ALTER TABLE "game_rules" DROP COLUMN "helpdesk_enabled";`) +} diff --git a/src/migrations/index.ts b/src/migrations/index.ts index b0af3aa..cbd81e5 100644 --- a/src/migrations/index.ts +++ b/src/migrations/index.ts @@ -82,6 +82,7 @@ import * as migration_20260921_123000_add_map_intel_fields from './20260921_1230 import * as migration_20260921_130500_weather_freeform from './20260921_130500_weather_freeform'; import * as migration_20260921_134000_add_node_icons from './20260921_134000_add_node_icons'; import * as migration_20260921_140000_add_currency_symbol_image from './20260921_140000_add_currency_symbol_image'; +import * as migration_20260921_150000_add_feature_flags from './20260921_150000_add_feature_flags'; export const migrations = [ { @@ -504,4 +505,9 @@ export const migrations = [ down: migration_20260921_140000_add_currency_symbol_image.down, name: '20260921_140000_add_currency_symbol_image', }, + { + up: migration_20260921_150000_add_feature_flags.up, + down: migration_20260921_150000_add_feature_flags.down, + name: '20260921_150000_add_feature_flags', + }, ];