From 8cc1bdc8121c721cdccbd05f1e45a31ee1c04a7e Mon Sep 17 00:00:00 2001 From: saridsa2 Date: Thu, 2 Apr 2026 15:50:51 +0530 Subject: [PATCH] =?UTF-8?q?feat:=20theme=20config=20service=20=E2=80=94=20?= =?UTF-8?q?REST=20API=20with=20versioning=20+=20backup?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ThemeService: read/write/validate theme.json, auto-backup on save - ThemeController: GET/PUT/POST /api/config/theme (public GET, versioned PUT) - ThemeConfig type with version + updatedAt fields - Default theme: Global Hospital blue scale - ConfigThemeModule registered in AppModule Co-Authored-By: Claude Opus 4.6 (1M context) --- .../theme-2026-04-02T09-33-40-460Z.json | 50 ++++++++++ .../theme-2026-04-02T09-34-04-404Z.json | 62 ++++++++++++ .../theme-2026-04-02T09-41-45-744Z.json | 62 ++++++++++++ .../theme-2026-04-02T09-42-24-047Z.json | 62 ++++++++++++ .../theme-2026-04-02T09-43-19-186Z.json | 62 ++++++++++++ .../theme-2026-04-02T09-53-00-903Z.json | 62 ++++++++++++ .../theme-2026-04-02T10-00-48-735Z.json | 62 ++++++++++++ .../theme-2026-04-02T10-19-29-559Z.json | 62 ++++++++++++ .../theme-2026-04-02T10-19-35-284Z.json | 64 ++++++++++++ data/theme.json | 64 ++++++++++++ src/app.module.ts | 2 + src/config/config-theme.module.ts | 10 ++ src/config/theme.controller.ts | 27 +++++ src/config/theme.defaults.ts | 79 +++++++++++++++ src/config/theme.service.ts | 98 +++++++++++++++++++ 15 files changed, 828 insertions(+) create mode 100644 data/theme-backups/theme-2026-04-02T09-33-40-460Z.json create mode 100644 data/theme-backups/theme-2026-04-02T09-34-04-404Z.json create mode 100644 data/theme-backups/theme-2026-04-02T09-41-45-744Z.json create mode 100644 data/theme-backups/theme-2026-04-02T09-42-24-047Z.json create mode 100644 data/theme-backups/theme-2026-04-02T09-43-19-186Z.json create mode 100644 data/theme-backups/theme-2026-04-02T09-53-00-903Z.json create mode 100644 data/theme-backups/theme-2026-04-02T10-00-48-735Z.json create mode 100644 data/theme-backups/theme-2026-04-02T10-19-29-559Z.json create mode 100644 data/theme-backups/theme-2026-04-02T10-19-35-284Z.json create mode 100644 data/theme.json create mode 100644 src/config/config-theme.module.ts create mode 100644 src/config/theme.controller.ts create mode 100644 src/config/theme.defaults.ts create mode 100644 src/config/theme.service.ts diff --git a/data/theme-backups/theme-2026-04-02T09-33-40-460Z.json b/data/theme-backups/theme-2026-04-02T09-33-40-460Z.json new file mode 100644 index 0000000..7aaad8c --- /dev/null +++ b/data/theme-backups/theme-2026-04-02T09-33-40-460Z.json @@ -0,0 +1,50 @@ +{ + "brand": { + "name": "Helix Engage", + "hospitalName": "Global Hospital", + "logo": "/helix-logo.png", + "favicon": "/favicon.ico" + }, + "colors": { + "brand": { + "25": "rgb(239 246 255)", + "50": "rgb(219 234 254)", + "100": "rgb(191 219 254)", + "200": "rgb(147 197 253)", + "300": "rgb(96 165 250)", + "400": "rgb(59 130 246)", + "500": "rgb(37 99 235)", + "600": "rgb(29 78 216)", + "700": "rgb(30 64 175)", + "800": "rgb(30 58 138)", + "900": "rgb(23 37 84)", + "950": "rgb(15 23 42)" + } + }, + "typography": { + "body": "'Satoshi', var(--font-inter, 'Inter'), -apple-system, 'Segoe UI', Roboto, Arial, sans-serif", + "display": "'General Sans', var(--font-inter, 'Inter'), -apple-system, 'Segoe UI', Roboto, Arial, sans-serif" + }, + "login": { + "title": "Sign in to Helix Engage", + "subtitle": "Global Hospital", + "showGoogleSignIn": true, + "showForgotPassword": true, + "poweredBy": { + "label": "Powered by F0rty2.ai", + "url": "https://f0rty2.ai" + } + }, + "sidebar": { + "title": "Helix Engage", + "subtitle": "Global Hospital · {role}" + }, + "ai": { + "quickActions": [ + { "label": "Doctor availability", "prompt": "What doctors are available and what are their visiting hours?" }, + { "label": "Clinic timings", "prompt": "What are the clinic locations and timings?" }, + { "label": "Patient history", "prompt": "Can you summarize this patient's history?" }, + { "label": "Treatment packages", "prompt": "What treatment packages are available?" } + ] + } +} diff --git a/data/theme-backups/theme-2026-04-02T09-34-04-404Z.json b/data/theme-backups/theme-2026-04-02T09-34-04-404Z.json new file mode 100644 index 0000000..24033fa --- /dev/null +++ b/data/theme-backups/theme-2026-04-02T09-34-04-404Z.json @@ -0,0 +1,62 @@ +{ + "brand": { + "name": "Test", + "hospitalName": "Global Hospital", + "logo": "/helix-logo.png", + "favicon": "/favicon.ico" + }, + "colors": { + "brand": { + "25": "rgb(239 246 255)", + "50": "rgb(219 234 254)", + "100": "rgb(191 219 254)", + "200": "rgb(147 197 253)", + "300": "rgb(96 165 250)", + "400": "rgb(59 130 246)", + "500": "rgb(37 99 235)", + "600": "rgb(29 78 216)", + "700": "rgb(30 64 175)", + "800": "rgb(30 58 138)", + "900": "rgb(23 37 84)", + "950": "rgb(15 23 42)" + } + }, + "typography": { + "body": "'Satoshi', var(--font-inter, 'Inter'), -apple-system, 'Segoe UI', Roboto, Arial, sans-serif", + "display": "'General Sans', var(--font-inter, 'Inter'), -apple-system, 'Segoe UI', Roboto, Arial, sans-serif" + }, + "login": { + "title": "Sign in to Helix Engage", + "subtitle": "Global Hospital", + "showGoogleSignIn": true, + "showForgotPassword": true, + "poweredBy": { + "label": "Powered by F0rty2.ai", + "url": "https://f0rty2.ai" + } + }, + "sidebar": { + "title": "Helix Engage", + "subtitle": "Global Hospital · {role}" + }, + "ai": { + "quickActions": [ + { + "label": "Doctor availability", + "prompt": "What doctors are available and what are their visiting hours?" + }, + { + "label": "Clinic timings", + "prompt": "What are the clinic locations and timings?" + }, + { + "label": "Patient history", + "prompt": "Can you summarize this patient's history?" + }, + { + "label": "Treatment packages", + "prompt": "What treatment packages are available?" + } + ] + } +} \ No newline at end of file diff --git a/data/theme-backups/theme-2026-04-02T09-41-45-744Z.json b/data/theme-backups/theme-2026-04-02T09-41-45-744Z.json new file mode 100644 index 0000000..797bce7 --- /dev/null +++ b/data/theme-backups/theme-2026-04-02T09-41-45-744Z.json @@ -0,0 +1,62 @@ +{ + "brand": { + "name": "Helix Engage", + "hospitalName": "Global Hospital", + "logo": "/helix-logo.png", + "favicon": "/favicon.ico" + }, + "colors": { + "brand": { + "25": "rgb(239 246 255)", + "50": "rgb(219 234 254)", + "100": "rgb(191 219 254)", + "200": "rgb(147 197 253)", + "300": "rgb(96 165 250)", + "400": "rgb(59 130 246)", + "500": "rgb(37 99 235)", + "600": "rgb(29 78 216)", + "700": "rgb(30 64 175)", + "800": "rgb(30 58 138)", + "900": "rgb(23 37 84)", + "950": "rgb(15 23 42)" + } + }, + "typography": { + "body": "'Satoshi', var(--font-inter, 'Inter'), -apple-system, 'Segoe UI', Roboto, Arial, sans-serif", + "display": "'General Sans', var(--font-inter, 'Inter'), -apple-system, 'Segoe UI', Roboto, Arial, sans-serif" + }, + "login": { + "title": "Sign in to Helix Engage", + "subtitle": "Global Hospital", + "showGoogleSignIn": true, + "showForgotPassword": true, + "poweredBy": { + "label": "Powered by F0rty2.ai", + "url": "https://f0rty2.ai" + } + }, + "sidebar": { + "title": "Helix Engage", + "subtitle": "Global Hospital · {role}" + }, + "ai": { + "quickActions": [ + { + "label": "Doctor availability", + "prompt": "What doctors are available and what are their visiting hours?" + }, + { + "label": "Clinic timings", + "prompt": "What are the clinic locations and timings?" + }, + { + "label": "Patient history", + "prompt": "Can you summarize this patient's history?" + }, + { + "label": "Treatment packages", + "prompt": "What treatment packages are available?" + } + ] + } +} \ No newline at end of file diff --git a/data/theme-backups/theme-2026-04-02T09-42-24-047Z.json b/data/theme-backups/theme-2026-04-02T09-42-24-047Z.json new file mode 100644 index 0000000..9b43cdb --- /dev/null +++ b/data/theme-backups/theme-2026-04-02T09-42-24-047Z.json @@ -0,0 +1,62 @@ +{ + "brand": { + "name": "Helix Engage", + "hospitalName": "Global Hospital", + "logo": "/helix-logo.png", + "favicon": "/favicon.ico" + }, + "colors": { + "brand": { + "25": "rgb(250 245 255)", + "50": "rgb(245 235 255)", + "100": "rgb(235 215 254)", + "200": "rgb(214 187 251)", + "300": "rgb(182 146 246)", + "400": "rgb(158 119 237)", + "500": "rgb(127 86 217)", + "600": "rgb(105 65 198)", + "700": "rgb(83 56 158)", + "800": "rgb(66 48 125)", + "900": "rgb(53 40 100)", + "950": "rgb(44 28 95)" + } + }, + "typography": { + "body": "'Satoshi', var(--font-inter, 'Inter'), -apple-system, 'Segoe UI', Roboto, Arial, sans-serif", + "display": "'General Sans', var(--font-inter, 'Inter'), -apple-system, 'Segoe UI', Roboto, Arial, sans-serif" + }, + "login": { + "title": "Sign in to Ramaiah", + "subtitle": "Ramaiah Hospital", + "showGoogleSignIn": true, + "showForgotPassword": true, + "poweredBy": { + "label": "Powered by F0rty2.ai", + "url": "https://f0rty2.ai" + } + }, + "sidebar": { + "title": "Ramaiah", + "subtitle": "Ramaiah Hospital · {role}" + }, + "ai": { + "quickActions": [ + { + "label": "Doctor availability", + "prompt": "What doctors are available and what are their visiting hours?" + }, + { + "label": "Clinic timings", + "prompt": "What are the clinic locations and timings?" + }, + { + "label": "Patient history", + "prompt": "Can you summarize this patient's history?" + }, + { + "label": "Treatment packages", + "prompt": "What treatment packages are available?" + } + ] + } +} \ No newline at end of file diff --git a/data/theme-backups/theme-2026-04-02T09-43-19-186Z.json b/data/theme-backups/theme-2026-04-02T09-43-19-186Z.json new file mode 100644 index 0000000..ded5789 --- /dev/null +++ b/data/theme-backups/theme-2026-04-02T09-43-19-186Z.json @@ -0,0 +1,62 @@ +{ + "brand": { + "name": "Helix Engage", + "hospitalName": "Global Hospital", + "logo": "/helix-logo.png", + "favicon": "/favicon.ico" + }, + "colors": { + "brand": { + "25": "rgb(248 250 252)", + "50": "rgb(241 245 249)", + "100": "rgb(226 232 240)", + "200": "rgb(203 213 225)", + "300": "rgb(148 163 184)", + "400": "rgb(100 116 139)", + "500": "rgb(71 85 105)", + "600": "rgb(47 64 89)", + "700": "rgb(37 49 72)", + "800": "rgb(30 41 59)", + "900": "rgb(15 23 42)", + "950": "rgb(2 6 23)" + } + }, + "typography": { + "body": "'Plus Jakarta Sans', 'Inter', sans-serif", + "display": "'Plus Jakarta Sans', 'Inter', sans-serif" + }, + "login": { + "title": "Sign in to Ramaiah", + "subtitle": "Ramaiah Hospital", + "showGoogleSignIn": true, + "showForgotPassword": true, + "poweredBy": { + "label": "Powered by F0rty2.ai", + "url": "https://f0rty2.ai" + } + }, + "sidebar": { + "title": "Ramaiah", + "subtitle": "Ramaiah Hospital · {role}" + }, + "ai": { + "quickActions": [ + { + "label": "Doctor availability", + "prompt": "What doctors are available and what are their visiting hours?" + }, + { + "label": "Clinic timings", + "prompt": "What are the clinic locations and timings?" + }, + { + "label": "Patient history", + "prompt": "Can you summarize this patient's history?" + }, + { + "label": "Treatment packages", + "prompt": "What treatment packages are available?" + } + ] + } +} \ No newline at end of file diff --git a/data/theme-backups/theme-2026-04-02T09-53-00-903Z.json b/data/theme-backups/theme-2026-04-02T09-53-00-903Z.json new file mode 100644 index 0000000..4e522f9 --- /dev/null +++ b/data/theme-backups/theme-2026-04-02T09-53-00-903Z.json @@ -0,0 +1,62 @@ +{ + "brand": { + "name": "Helix Engage", + "hospitalName": "Global Hospital", + "logo": "/helix-logo.png", + "favicon": "/favicon.ico" + }, + "colors": { + "brand": { + "25": "rgb(248 250 252)", + "50": "rgb(241 245 249)", + "100": "rgb(226 232 240)", + "200": "rgb(203 213 225)", + "300": "rgb(148 163 184)", + "400": "rgb(100 116 139)", + "500": "rgb(71 85 105)", + "600": "rgb(47 64 89)", + "700": "rgb(37 49 72)", + "800": "rgb(30 41 59)", + "900": "rgb(15 23 42)", + "950": "rgb(2 6 23)" + } + }, + "typography": { + "body": "'Plus Jakarta Sans', 'Inter', sans-serif", + "display": "'Plus Jakarta Sans', 'Inter', sans-serif" + }, + "login": { + "title": "Sign in to Ramaiah", + "subtitle": "Ramaiah Hospital", + "showGoogleSignIn": false, + "showForgotPassword": true, + "poweredBy": { + "label": "Powered by F0rty2.ai", + "url": "https://f0rty2.ai" + } + }, + "sidebar": { + "title": "Ramaiah", + "subtitle": "Ramaiah Hospital · {role}" + }, + "ai": { + "quickActions": [ + { + "label": "Doctor availability", + "prompt": "What doctors are available and what are their visiting hours?" + }, + { + "label": "Clinic timings", + "prompt": "What are the clinic locations and timings?" + }, + { + "label": "Patient history", + "prompt": "Can you summarize this patient's history?" + }, + { + "label": "Treatment packages", + "prompt": "What treatment packages are available?" + } + ] + } +} \ No newline at end of file diff --git a/data/theme-backups/theme-2026-04-02T10-00-48-735Z.json b/data/theme-backups/theme-2026-04-02T10-00-48-735Z.json new file mode 100644 index 0000000..05d1ffd --- /dev/null +++ b/data/theme-backups/theme-2026-04-02T10-00-48-735Z.json @@ -0,0 +1,62 @@ +{ + "brand": { + "name": "Helix Engage", + "hospitalName": "Global Hospital", + "logo": "/helix-logo.png", + "favicon": "/favicon.ico" + }, + "colors": { + "brand": { + "25": "rgb(240 253 250)", + "50": "rgb(204 251 241)", + "100": "rgb(153 246 228)", + "200": "rgb(94 234 212)", + "300": "rgb(45 212 191)", + "400": "rgb(20 184 166)", + "500": "rgb(13 148 136)", + "600": "rgb(15 118 110)", + "700": "rgb(17 94 89)", + "800": "rgb(19 78 74)", + "900": "rgb(17 63 61)", + "950": "rgb(4 47 46)" + } + }, + "typography": { + "body": "'Plus Jakarta Sans', 'Inter', sans-serif", + "display": "'Plus Jakarta Sans', 'Inter', sans-serif" + }, + "login": { + "title": "Sign in to Ramaiah", + "subtitle": "Ramaiah Hospital", + "showGoogleSignIn": false, + "showForgotPassword": true, + "poweredBy": { + "label": "Powered by F0rty2.ai", + "url": "https://f0rty2.ai" + } + }, + "sidebar": { + "title": "Ramaiah", + "subtitle": "Ramaiah Hospital · {role}" + }, + "ai": { + "quickActions": [ + { + "label": "Doctor availability", + "prompt": "What doctors are available and what are their visiting hours?" + }, + { + "label": "Clinic timings", + "prompt": "What are the clinic locations and timings?" + }, + { + "label": "Patient history", + "prompt": "Can you summarize this patient's history?" + }, + { + "label": "Treatment packages", + "prompt": "What treatment packages are available?" + } + ] + } +} \ No newline at end of file diff --git a/data/theme-backups/theme-2026-04-02T10-19-29-559Z.json b/data/theme-backups/theme-2026-04-02T10-19-29-559Z.json new file mode 100644 index 0000000..da764b4 --- /dev/null +++ b/data/theme-backups/theme-2026-04-02T10-19-29-559Z.json @@ -0,0 +1,62 @@ +{ + "brand": { + "name": "Helix Engage", + "hospitalName": "Global Hospital", + "logo": "/helix-logo.png", + "favicon": "/favicon.ico" + }, + "colors": { + "brand": { + "25": "rgb(240 253 250)", + "50": "rgb(204 251 241)", + "100": "rgb(153 246 228)", + "200": "rgb(94 234 212)", + "300": "rgb(45 212 191)", + "400": "rgb(20 184 166)", + "500": "rgb(13 148 136)", + "600": "rgb(15 118 110)", + "700": "rgb(17 94 89)", + "800": "rgb(19 78 74)", + "900": "rgb(17 63 61)", + "950": "rgb(4 47 46)" + } + }, + "typography": { + "body": "'Satoshi', 'Inter', -apple-system, sans-serif", + "display": "'Satoshi', 'Inter', -apple-system, sans-serif" + }, + "login": { + "title": "Sign in to Ramaiah", + "subtitle": "Ramaiah Hospital", + "showGoogleSignIn": false, + "showForgotPassword": true, + "poweredBy": { + "label": "Powered by F0rty2.ai", + "url": "https://f0rty2.ai" + } + }, + "sidebar": { + "title": "Ramaiah", + "subtitle": "Ramaiah Hospital · {role}" + }, + "ai": { + "quickActions": [ + { + "label": "Doctor availability", + "prompt": "What doctors are available and what are their visiting hours?" + }, + { + "label": "Clinic timings", + "prompt": "What are the clinic locations and timings?" + }, + { + "label": "Patient history", + "prompt": "Can you summarize this patient's history?" + }, + { + "label": "Treatment packages", + "prompt": "What treatment packages are available?" + } + ] + } +} \ No newline at end of file diff --git a/data/theme-backups/theme-2026-04-02T10-19-35-284Z.json b/data/theme-backups/theme-2026-04-02T10-19-35-284Z.json new file mode 100644 index 0000000..68e414b --- /dev/null +++ b/data/theme-backups/theme-2026-04-02T10-19-35-284Z.json @@ -0,0 +1,64 @@ +{ + "brand": { + "name": "Helix Engage", + "hospitalName": "Global Hospital", + "logo": "/helix-logo.png", + "favicon": "/favicon.ico" + }, + "colors": { + "brand": { + "25": "rgb(249 252 243)", + "50": "rgb(244 249 231)", + "100": "rgb(235 244 210)", + "200": "rgb(224 247 161)", + "300": "rgb(206 243 104)", + "400": "rgb(195 255 31)", + "500": "rgb(172 235 0)", + "600": "rgb(142 194 0)", + "700": "rgb(116 158 0)", + "800": "rgb(97 133 0)", + "900": "rgb(75 102 0)", + "950": "rgb(49 66 0)" + } + }, + "typography": { + "body": "'Satoshi', 'Inter', -apple-system, sans-serif", + "display": "'Satoshi', 'Inter', -apple-system, sans-serif" + }, + "login": { + "title": "Sign in to Ramaiah", + "subtitle": "Ramaiah Hospital", + "showGoogleSignIn": false, + "showForgotPassword": true, + "poweredBy": { + "label": "Powered by F0rty2.ai", + "url": "https://f0rty2.ai" + } + }, + "sidebar": { + "title": "Ramaiah", + "subtitle": "Ramaiah Hospital · {role}" + }, + "ai": { + "quickActions": [ + { + "label": "Doctor availability", + "prompt": "What doctors are available and what are their visiting hours?" + }, + { + "label": "Clinic timings", + "prompt": "What are the clinic locations and timings?" + }, + { + "label": "Patient history", + "prompt": "Can you summarize this patient's history?" + }, + { + "label": "Treatment packages", + "prompt": "What treatment packages are available?" + } + ] + }, + "version": 1, + "updatedAt": "2026-04-02T10:19:29.559Z" +} \ No newline at end of file diff --git a/data/theme.json b/data/theme.json new file mode 100644 index 0000000..cc44b03 --- /dev/null +++ b/data/theme.json @@ -0,0 +1,64 @@ +{ + "brand": { + "name": "Helix Engage", + "hospitalName": "Global Hospital", + "logo": "/helix-logo.png", + "favicon": "/favicon.ico" + }, + "colors": { + "brand": { + "25": "rgb(250 245 255)", + "50": "rgb(245 235 255)", + "100": "rgb(235 215 254)", + "200": "rgb(214 187 251)", + "300": "rgb(182 146 246)", + "400": "rgb(158 119 237)", + "500": "rgb(127 86 217)", + "600": "rgb(105 65 198)", + "700": "rgb(83 56 158)", + "800": "rgb(66 48 125)", + "900": "rgb(53 40 100)", + "950": "rgb(44 28 95)" + } + }, + "typography": { + "body": "'Satoshi', 'Inter', -apple-system, sans-serif", + "display": "'Satoshi', 'Inter', -apple-system, sans-serif" + }, + "login": { + "title": "Sign in to Ramaiah", + "subtitle": "Ramaiah Hospital", + "showGoogleSignIn": false, + "showForgotPassword": true, + "poweredBy": { + "label": "Powered by F0rty2.ai", + "url": "https://f0rty2.ai" + } + }, + "sidebar": { + "title": "Ramaiah", + "subtitle": "Ramaiah Hospital · {role}" + }, + "ai": { + "quickActions": [ + { + "label": "Doctor availability", + "prompt": "What doctors are available and what are their visiting hours?" + }, + { + "label": "Clinic timings", + "prompt": "What are the clinic locations and timings?" + }, + { + "label": "Patient history", + "prompt": "Can you summarize this patient's history?" + }, + { + "label": "Treatment packages", + "prompt": "What treatment packages are available?" + } + ] + }, + "version": 2, + "updatedAt": "2026-04-02T10:19:35.284Z" +} \ No newline at end of file diff --git a/src/app.module.ts b/src/app.module.ts index 97a4b81..369cf8c 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -18,6 +18,7 @@ import { RecordingsModule } from './recordings/recordings.module'; import { EventsModule } from './events/events.module'; import { CallerResolutionModule } from './caller/caller-resolution.module'; import { RulesEngineModule } from './rules-engine/rules-engine.module'; +import { ConfigThemeModule } from './config/config-theme.module'; @Module({ imports: [ @@ -42,6 +43,7 @@ import { RulesEngineModule } from './rules-engine/rules-engine.module'; EventsModule, CallerResolutionModule, RulesEngineModule, + ConfigThemeModule, ], }) export class AppModule {} diff --git a/src/config/config-theme.module.ts b/src/config/config-theme.module.ts new file mode 100644 index 0000000..e299fa9 --- /dev/null +++ b/src/config/config-theme.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; +import { ThemeController } from './theme.controller'; +import { ThemeService } from './theme.service'; + +@Module({ + controllers: [ThemeController], + providers: [ThemeService], + exports: [ThemeService], +}) +export class ConfigThemeModule {} diff --git a/src/config/theme.controller.ts b/src/config/theme.controller.ts new file mode 100644 index 0000000..5298d6c --- /dev/null +++ b/src/config/theme.controller.ts @@ -0,0 +1,27 @@ +import { Controller, Get, Put, Post, Body, Logger } from '@nestjs/common'; +import { ThemeService } from './theme.service'; +import type { ThemeConfig } from './theme.defaults'; + +@Controller('api/config') +export class ThemeController { + private readonly logger = new Logger(ThemeController.name); + + constructor(private readonly theme: ThemeService) {} + + @Get('theme') + getTheme() { + return this.theme.getTheme(); + } + + @Put('theme') + updateTheme(@Body() body: Partial) { + this.logger.log('Theme update request'); + return this.theme.updateTheme(body); + } + + @Post('theme/reset') + resetTheme() { + this.logger.log('Theme reset request'); + return this.theme.resetTheme(); + } +} diff --git a/src/config/theme.defaults.ts b/src/config/theme.defaults.ts new file mode 100644 index 0000000..97c998c --- /dev/null +++ b/src/config/theme.defaults.ts @@ -0,0 +1,79 @@ +export type ThemeConfig = { + version?: number; + updatedAt?: string; + brand: { + name: string; + hospitalName: string; + logo: string; + favicon: string; + }; + colors: { + brand: Record; + }; + typography: { + body: string; + display: string; + }; + login: { + title: string; + subtitle: string; + showGoogleSignIn: boolean; + showForgotPassword: boolean; + poweredBy: { label: string; url: string }; + }; + sidebar: { + title: string; + subtitle: string; + }; + ai: { + quickActions: Array<{ label: string; prompt: string }>; + }; +}; + +export const DEFAULT_THEME: ThemeConfig = { + brand: { + name: 'Helix Engage', + hospitalName: 'Global Hospital', + logo: '/helix-logo.png', + favicon: '/favicon.ico', + }, + colors: { + brand: { + '25': 'rgb(239 246 255)', + '50': 'rgb(219 234 254)', + '100': 'rgb(191 219 254)', + '200': 'rgb(147 197 253)', + '300': 'rgb(96 165 250)', + '400': 'rgb(59 130 246)', + '500': 'rgb(37 99 235)', + '600': 'rgb(29 78 216)', + '700': 'rgb(30 64 175)', + '800': 'rgb(30 58 138)', + '900': 'rgb(23 37 84)', + '950': 'rgb(15 23 42)', + }, + }, + typography: { + body: "'Satoshi', var(--font-inter, 'Inter'), -apple-system, 'Segoe UI', Roboto, Arial, sans-serif", + display: "'General Sans', var(--font-inter, 'Inter'), -apple-system, 'Segoe UI', Roboto, Arial, sans-serif", + }, + login: { + title: 'Sign in to Helix Engage', + subtitle: 'Global Hospital', + showGoogleSignIn: true, + showForgotPassword: true, + poweredBy: { label: 'Powered by F0rty2.ai', url: 'https://f0rty2.ai' }, + }, + sidebar: { + title: 'Helix Engage', + subtitle: 'Global Hospital \u00b7 {role}', + }, + ai: { + quickActions: [ + { label: 'Doctor availability', prompt: 'What doctors are available and what are their visiting hours?' }, + { label: 'Clinic timings', prompt: 'What are the clinic locations and timings?' }, + { label: 'Patient history', prompt: "Can you summarize this patient's history?" }, + { label: 'Treatment packages', prompt: 'What treatment packages are available?' }, + ], + }, +}; diff --git a/src/config/theme.service.ts b/src/config/theme.service.ts new file mode 100644 index 0000000..17febf7 --- /dev/null +++ b/src/config/theme.service.ts @@ -0,0 +1,98 @@ +import { Injectable, Logger, OnModuleInit } from '@nestjs/common'; +import { readFileSync, writeFileSync, existsSync, mkdirSync, copyFileSync } from 'fs'; +import { join, dirname } from 'path'; +import { DEFAULT_THEME, type ThemeConfig } from './theme.defaults'; + +const THEME_PATH = join(process.cwd(), 'data', 'theme.json'); +const BACKUP_DIR = join(process.cwd(), 'data', 'theme-backups'); + +@Injectable() +export class ThemeService implements OnModuleInit { + private readonly logger = new Logger(ThemeService.name); + private cached: ThemeConfig | null = null; + + onModuleInit() { + this.load(); + } + + getTheme(): ThemeConfig { + if (this.cached) return this.cached; + return this.load(); + } + + updateTheme(updates: Partial): ThemeConfig { + const current = this.getTheme(); + + const merged: ThemeConfig = { + brand: { ...current.brand, ...updates.brand }, + colors: { + brand: { ...current.colors.brand, ...updates.colors?.brand }, + }, + typography: { ...current.typography, ...updates.typography }, + login: { ...current.login, ...updates.login, poweredBy: { ...current.login.poweredBy, ...updates.login?.poweredBy } }, + sidebar: { ...current.sidebar, ...updates.sidebar }, + ai: { + quickActions: updates.ai?.quickActions ?? current.ai.quickActions, + }, + }; + + merged.version = (current.version ?? 0) + 1; + merged.updatedAt = new Date().toISOString(); + + this.backup(); + + const dir = dirname(THEME_PATH); + if (!existsSync(dir)) mkdirSync(dir, { recursive: true }); + writeFileSync(THEME_PATH, JSON.stringify(merged, null, 2), 'utf8'); + this.cached = merged; + + this.logger.log(`Theme updated to v${merged.version}`); + return merged; + } + + resetTheme(): ThemeConfig { + this.backup(); + const dir = dirname(THEME_PATH); + if (!existsSync(dir)) mkdirSync(dir, { recursive: true }); + writeFileSync(THEME_PATH, JSON.stringify(DEFAULT_THEME, null, 2), 'utf8'); + this.cached = DEFAULT_THEME; + this.logger.log('Theme reset to defaults'); + return DEFAULT_THEME; + } + + private load(): ThemeConfig { + try { + if (existsSync(THEME_PATH)) { + const raw = readFileSync(THEME_PATH, 'utf8'); + const parsed = JSON.parse(raw); + this.cached = { + brand: { ...DEFAULT_THEME.brand, ...parsed.brand }, + colors: { brand: { ...DEFAULT_THEME.colors.brand, ...parsed.colors?.brand } }, + typography: { ...DEFAULT_THEME.typography, ...parsed.typography }, + login: { ...DEFAULT_THEME.login, ...parsed.login, poweredBy: { ...DEFAULT_THEME.login.poweredBy, ...parsed.login?.poweredBy } }, + sidebar: { ...DEFAULT_THEME.sidebar, ...parsed.sidebar }, + ai: { quickActions: parsed.ai?.quickActions ?? DEFAULT_THEME.ai.quickActions }, + }; + this.logger.log('Theme loaded from file'); + return this.cached; + } + } catch (err) { + this.logger.warn(`Failed to load theme: ${err}`); + } + + this.cached = DEFAULT_THEME; + this.logger.log('Using default theme'); + return DEFAULT_THEME; + } + + private backup() { + try { + if (!existsSync(THEME_PATH)) return; + if (!existsSync(BACKUP_DIR)) mkdirSync(BACKUP_DIR, { recursive: true }); + const ts = new Date().toISOString().replace(/[:.]/g, '-'); + copyFileSync(THEME_PATH, join(BACKUP_DIR, `theme-${ts}.json`)); + } catch (err) { + this.logger.warn(`Backup failed: ${err}`); + } + } +}