diff --git a/docs/USER_SETTINGS_CAPABILITIES.md b/docs/USER_SETTINGS_CAPABILITIES.md index b2bb4b0e..6e0edb85 100644 --- a/docs/USER_SETTINGS_CAPABILITIES.md +++ b/docs/USER_SETTINGS_CAPABILITIES.md @@ -237,11 +237,19 @@ Settings backup and restore: Permission-based access control: - Per-setting edit permissions -- Capability flags for different operations +- Capability flags for different operations (including `canEditPollSettings` for custom poll preferences) - Extensible for future features - Role-based support (future enhancement) -### 5. Migration Support +### 5. Poll Creation Preferences + +The system includes support for user-configurable default preferences for interactive polls: +- `pollCreationEnabled`: Master toggle for creating polls in classes, study groups, or discussions. +- `defaultPollDuration`: Active duration of created polls (1 to 30 days). +- `allowAnonymousVoting`: Default setting for enabling anonymous votes. +- `pollResultsVisibility`: Control who can view the voting results ('always' | 'after_voting' | 'after_ended'). + +### 6. Migration Support Schema version management: - Automatic migration between versions diff --git a/src/app/profile/profile-data.ts b/src/app/profile/profile-data.ts index c31ab241..2b6c3655 100644 --- a/src/app/profile/profile-data.ts +++ b/src/app/profile/profile-data.ts @@ -77,6 +77,12 @@ export const settingsPreferences: PreferenceOption[] = [ description: 'Enable offline learning capabilities', enabled: true, }, + { + id: 'poll-creation', + label: 'Poll Creation', + description: 'Allow creating interactive polls in study groups and courses', + enabled: true, + }, ]; export const achievements: Achievement[] = [ diff --git a/src/lib/settings/__tests__/service.test.ts b/src/lib/settings/__tests__/service.test.ts index 16f4402a..1f9a7429 100644 --- a/src/lib/settings/__tests__/service.test.ts +++ b/src/lib/settings/__tests__/service.test.ts @@ -71,6 +71,30 @@ describe('SettingsService', () => { expect(result.valid).toBe(false); expect(result.errors.some(e => e.includes('notificationsEnabled'))).toBe(true); }); + + it('rejects invalid poll duration (below minimum)', () => { + const invalidSettings = { ...createDefaultSettings(), defaultPollDuration: 0 }; + const result = SettingsService.validateSettings(invalidSettings); + + expect(result.valid).toBe(false); + expect(result.errors.some(e => e.includes('defaultPollDuration'))).toBe(true); + }); + + it('rejects invalid poll duration (above maximum)', () => { + const invalidSettings = { ...createDefaultSettings(), defaultPollDuration: 31 }; + const result = SettingsService.validateSettings(invalidSettings); + + expect(result.valid).toBe(false); + expect(result.errors.some(e => e.includes('defaultPollDuration'))).toBe(true); + }); + + it('rejects invalid poll results visibility values', () => { + const invalidSettings = { ...createDefaultSettings(), pollResultsVisibility: 'none' as any }; + const result = SettingsService.validateSettings(invalidSettings); + + expect(result.valid).toBe(false); + expect(result.errors.some(e => e.includes('pollResultsVisibility'))).toBe(true); + }); }); // ── createStoreState ────────────────────────────────────────────────────── @@ -289,6 +313,41 @@ describe('SettingsService', () => { const result = SettingsService.validateSettingValue('requireSignatureOnCertificates', true); expect(result.valid).toBe(true); }); + + it('validates correct pollCreationEnabled value', () => { + const result = SettingsService.validateSettingValue('pollCreationEnabled', true); + expect(result.valid).toBe(true); + }); + + it('rejects invalid pollCreationEnabled value', () => { + const result = SettingsService.validateSettingValue('pollCreationEnabled', 'yes'); + expect(result.valid).toBe(false); + }); + + it('validates correct defaultPollDuration value', () => { + const result = SettingsService.validateSettingValue('defaultPollDuration', 7); + expect(result.valid).toBe(true); + }); + + it('rejects invalid defaultPollDuration value', () => { + const result = SettingsService.validateSettingValue('defaultPollDuration', 100); + expect(result.valid).toBe(false); + }); + + it('validates correct allowAnonymousVoting value', () => { + const result = SettingsService.validateSettingValue('allowAnonymousVoting', false); + expect(result.valid).toBe(true); + }); + + it('validates correct pollResultsVisibility value', () => { + const result = SettingsService.validateSettingValue('pollResultsVisibility', 'after_voting'); + expect(result.valid).toBe(true); + }); + + it('rejects invalid pollResultsVisibility value', () => { + const result = SettingsService.validateSettingValue('pollResultsVisibility', 'nobody'); + expect(result.valid).toBe(false); + }); }); // ── exportSettings ─────────────────────────────────────────────────────── @@ -406,6 +465,7 @@ describe('SettingsService', () => { expect(capabilities).toHaveProperty('canEditPrefetching'); expect(capabilities).toHaveProperty('canEditReducedMotion'); expect(capabilities).toHaveProperty('canEditElectronicSignature'); + expect(capabilities).toHaveProperty('canEditPollSettings'); expect(capabilities).toHaveProperty('canExportSettings'); expect(capabilities).toHaveProperty('canImportSettings'); expect(capabilities).toHaveProperty('canSyncSettings'); @@ -472,6 +532,26 @@ describe('SettingsService', () => { const result = SettingsService.canEditSetting('requireSignatureOnCertificates'); expect(result).toBe(true); }); + + it('allows editing pollCreationEnabled', () => { + const result = SettingsService.canEditSetting('pollCreationEnabled'); + expect(result).toBe(true); + }); + + it('allows editing defaultPollDuration', () => { + const result = SettingsService.canEditSetting('defaultPollDuration'); + expect(result).toBe(true); + }); + + it('allows editing allowAnonymousVoting', () => { + const result = SettingsService.canEditSetting('allowAnonymousVoting'); + expect(result).toBe(true); + }); + + it('allows editing pollResultsVisibility', () => { + const result = SettingsService.canEditSetting('pollResultsVisibility'); + expect(result).toBe(true); + }); }); // ── migrateSettings ────────────────────────────────────────────────────── diff --git a/src/lib/settings/service.ts b/src/lib/settings/service.ts index 545c0609..a10b0d0c 100644 --- a/src/lib/settings/service.ts +++ b/src/lib/settings/service.ts @@ -35,7 +35,7 @@ export class SettingsService { const parsed = appSettingsSchema.safeParse(data); if (!parsed.success) { - parsed.error.errors.forEach((err) => { + parsed.error.errors.forEach((err: any) => { errors.push(`${err.path.join('.')}: ${err.message}`); }); return { @@ -131,7 +131,7 @@ export class SettingsService { return { valid: false, - error: result.errors.find((e) => e.includes(key)) || 'Invalid value', + error: result.errors.find((e) => e.includes(String(key))) || 'Invalid value', }; } catch { return { @@ -230,6 +230,7 @@ export class SettingsService { canEditPrefetching: boolean; canEditReducedMotion: boolean; canEditElectronicSignature: boolean; + canEditPollSettings: boolean; canExportSettings: boolean; canImportSettings: boolean; canSyncSettings: boolean; @@ -242,6 +243,7 @@ export class SettingsService { canEditPrefetching: true, canEditReducedMotion: true, canEditElectronicSignature: true, + canEditPollSettings: true, canExportSettings: true, canImportSettings: true, canSyncSettings: true, @@ -265,6 +267,10 @@ export class SettingsService { electronicSignatureEnabled: 'canEditElectronicSignature', signatureName: 'canEditElectronicSignature', requireSignatureOnCertificates: 'canEditElectronicSignature', + pollCreationEnabled: 'canEditPollSettings', + defaultPollDuration: 'canEditPollSettings', + allowAnonymousVoting: 'canEditPollSettings', + pollResultsVisibility: 'canEditPollSettings', }; return capabilities[permissionMap[key]] || false; diff --git a/src/lib/settings/types.ts b/src/lib/settings/types.ts index 1ef2c35e..64bc4b83 100644 --- a/src/lib/settings/types.ts +++ b/src/lib/settings/types.ts @@ -19,6 +19,10 @@ export type ThemePreference = z.infer; * - `electronicSignatureEnabled` — Master toggle for electronic signature on authenticated actions. * - `signatureName` — Full name used as the typed electronic signature (max 100 chars). * - `requireSignatureOnCertificates` — Prompt the user to confirm their signature before a certificate is issued. + * - `pollCreationEnabled` — Master toggle for creating interactive polls in classes or study groups. + * - `defaultPollDuration` — Default poll duration in days (1 to 30 days). + * - `allowAnonymousVoting` — Toggle to allow participants to vote anonymously by default. + * - `pollResultsVisibility` — Default visibility of poll results ('always' | 'after_voting' | 'after_ended'). */ export const appSettingsSchema = z.object({ version: z.literal(SETTINGS_SCHEMA_VERSION), @@ -31,6 +35,10 @@ export const appSettingsSchema = z.object({ electronicSignatureEnabled: z.boolean(), signatureName: z.string().max(100), requireSignatureOnCertificates: z.boolean(), + pollCreationEnabled: z.boolean(), + defaultPollDuration: z.number().int().min(1).max(30), + allowAnonymousVoting: z.boolean(), + pollResultsVisibility: z.enum(['always', 'after_voting', 'after_ended']), }); /** Fully typed representation of all user settings. Inferred from `appSettingsSchema`. */ @@ -83,5 +91,9 @@ export function createDefaultSettings(): AppSettings { electronicSignatureEnabled: false, signatureName: '', requireSignatureOnCertificates: false, + pollCreationEnabled: true, + defaultPollDuration: 7, + allowAnonymousVoting: false, + pollResultsVisibility: 'always', }; } diff --git a/src/pages/settings/index.tsx b/src/pages/settings/index.tsx index ee303041..4e1c0d5b 100644 --- a/src/pages/settings/index.tsx +++ b/src/pages/settings/index.tsx @@ -332,6 +332,111 @@ export default function SettingsPage() { +
+

Poll Creation & Voting

+ +
+

+ Configure default preferences for creating interactive polls in your classes, groups, or discussions. +

+ + + +
+
+ + +

+ Default active time limit for your newly created polls. +

+
+ + + +
+ + +

+ Determine who can see the current voting distribution. +

+
+
+
+
+

Export / import file