From f02c1e6a64b9fac075a65274ad4023e323e9905d Mon Sep 17 00:00:00 2001 From: Trey t Date: Fri, 28 Nov 2025 08:47:49 -0600 Subject: [PATCH] Add admin settings page with seed data and limitations toggle MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add settings handler with endpoints for: - GET/PUT /api/admin/settings (enable_limitations toggle) - POST /api/admin/settings/seed-lookups (run 001_lookups.sql) - POST /api/admin/settings/seed-test-data (run 002_test_data.sql) - Add settings page to admin panel with: - Toggle switch to enable/disable subscription limitations - Button to seed lookup data (categories, priorities, etc.) - Button to seed test data (with warning for non-production use) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- admin/src/app/(dashboard)/settings/page.tsx | 226 ++++++++++++++++++++ admin/src/lib/api.ts | 32 +++ internal/admin/handlers/settings_handler.go | 128 +++++++++++ internal/admin/routes.go | 10 + 4 files changed, 396 insertions(+) create mode 100644 admin/src/app/(dashboard)/settings/page.tsx create mode 100644 internal/admin/handlers/settings_handler.go diff --git a/admin/src/app/(dashboard)/settings/page.tsx b/admin/src/app/(dashboard)/settings/page.tsx new file mode 100644 index 0000000..11c2065 --- /dev/null +++ b/admin/src/app/(dashboard)/settings/page.tsx @@ -0,0 +1,226 @@ +'use client'; + +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +import { Database, TestTube, Shield } from 'lucide-react'; + +import { settingsApi } from '@/lib/api'; +import { Button } from '@/components/ui/button'; +import { Switch } from '@/components/ui/switch'; +import { Label } from '@/components/ui/label'; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from '@/components/ui/card'; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger, +} from '@/components/ui/alert-dialog'; +import { toast } from 'sonner'; + +export default function SettingsPage() { + const queryClient = useQueryClient(); + + const { data: settings, isLoading } = useQuery({ + queryKey: ['settings'], + queryFn: settingsApi.get, + }); + + const updateMutation = useMutation({ + mutationFn: settingsApi.update, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['settings'] }); + toast.success('Settings updated'); + }, + onError: () => { + toast.error('Failed to update settings'); + }, + }); + + const seedLookupsMutation = useMutation({ + mutationFn: settingsApi.seedLookups, + onSuccess: (data) => { + toast.success(data.message); + }, + onError: () => { + toast.error('Failed to seed lookup data'); + }, + }); + + const seedTestDataMutation = useMutation({ + mutationFn: settingsApi.seedTestData, + onSuccess: (data) => { + toast.success(data.message); + }, + onError: () => { + toast.error('Failed to seed test data'); + }, + }); + + const handleLimitationsToggle = () => { + if (settings) { + updateMutation.mutate({ + enable_limitations: !settings.enable_limitations, + }); + } + }; + + if (isLoading) { + return ( +
+
+
+
+
+
+
+ ); + } + + return ( +
+
+

Settings

+

+ Manage system settings and seed data +

+
+ +
+ {/* Subscription Limitations */} + + + + + Subscription Limitations + + + Control whether tier-based limitations are enforced for users + + + +
+
+ +

+ When enabled, free tier users will have restricted access to features +

+
+ +
+
+
+ + {/* Seed Lookup Data */} + + + + + Seed Lookup Data + + + Populate or refresh static lookup tables (categories, priorities, statuses, etc.) + + + + + + + + + + Seed Lookup Data? + + This will insert or update all lookup tables including: +
    +
  • Residence types
  • +
  • Task categories, priorities, statuses, frequencies
  • +
  • Contractor specialties
  • +
  • Subscription tiers and feature benefits
  • +
+ Existing data will be preserved or updated. +
+
+ + Cancel + seedLookupsMutation.mutate()}> + Seed Data + + +
+
+
+
+ + {/* Seed Test Data */} + + + + + Seed Test Data + + + Populate the database with sample users, residences, and tasks for testing + + + + + + + + + + Seed Test Data? + + This will create sample data for testing including: +
    +
  • Test users
  • +
  • Sample residences
  • +
  • Example tasks and completions
  • +
+ + Warning: This should only be used in development/testing environments. + +
+
+ + Cancel + seedTestDataMutation.mutate()} + className="bg-destructive text-destructive-foreground hover:bg-destructive/90" + > + Seed Test Data + + +
+
+
+
+
+
+ ); +} diff --git a/admin/src/lib/api.ts b/admin/src/lib/api.ts index cfb6c4c..8103e90 100644 --- a/admin/src/lib/api.ts +++ b/admin/src/lib/api.ts @@ -632,4 +632,36 @@ export const notificationPrefsApi = { }, }; +// Settings types +export interface SystemSettings { + enable_limitations: boolean; +} + +export interface UpdateSettingsRequest { + enable_limitations?: boolean; +} + +// Settings API +export const settingsApi = { + get: async (): Promise => { + const response = await api.get('/settings'); + return response.data; + }, + + update: async (data: UpdateSettingsRequest): Promise => { + const response = await api.put('/settings', data); + return response.data; + }, + + seedLookups: async (): Promise<{ message: string }> => { + const response = await api.post<{ message: string }>('/settings/seed-lookups'); + return response.data; + }, + + seedTestData: async (): Promise<{ message: string }> => { + const response = await api.post<{ message: string }>('/settings/seed-test-data'); + return response.data; + }, +}; + export default api; diff --git a/internal/admin/handlers/settings_handler.go b/internal/admin/handlers/settings_handler.go new file mode 100644 index 0000000..f48d141 --- /dev/null +++ b/internal/admin/handlers/settings_handler.go @@ -0,0 +1,128 @@ +package handlers + +import ( + "net/http" + "os" + "path/filepath" + + "github.com/gin-gonic/gin" + "gorm.io/gorm" + + "github.com/treytartt/mycrib-api/internal/models" +) + +// AdminSettingsHandler handles system settings management +type AdminSettingsHandler struct { + db *gorm.DB +} + +// NewAdminSettingsHandler creates a new handler +func NewAdminSettingsHandler(db *gorm.DB) *AdminSettingsHandler { + return &AdminSettingsHandler{db: db} +} + +// SettingsResponse represents the settings response +type SettingsResponse struct { + EnableLimitations bool `json:"enable_limitations"` +} + +// GetSettings handles GET /api/admin/settings +func (h *AdminSettingsHandler) GetSettings(c *gin.Context) { + var settings models.SubscriptionSettings + if err := h.db.First(&settings, 1).Error; err != nil { + if err == gorm.ErrRecordNotFound { + // Create default settings + settings = models.SubscriptionSettings{ID: 1, EnableLimitations: false} + h.db.Create(&settings) + } else { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch settings"}) + return + } + } + + c.JSON(http.StatusOK, SettingsResponse{ + EnableLimitations: settings.EnableLimitations, + }) +} + +// UpdateSettingsRequest represents the update request +type UpdateSettingsRequest struct { + EnableLimitations *bool `json:"enable_limitations"` +} + +// UpdateSettings handles PUT /api/admin/settings +func (h *AdminSettingsHandler) UpdateSettings(c *gin.Context) { + var req UpdateSettingsRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + var settings models.SubscriptionSettings + if err := h.db.First(&settings, 1).Error; err != nil { + if err == gorm.ErrRecordNotFound { + settings = models.SubscriptionSettings{ID: 1} + } else { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch settings"}) + return + } + } + + if req.EnableLimitations != nil { + settings.EnableLimitations = *req.EnableLimitations + } + + if err := h.db.Save(&settings).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update settings"}) + return + } + + c.JSON(http.StatusOK, SettingsResponse{ + EnableLimitations: settings.EnableLimitations, + }) +} + +// SeedLookups handles POST /api/admin/settings/seed-lookups +func (h *AdminSettingsHandler) SeedLookups(c *gin.Context) { + if err := h.runSeedFile("001_lookups.sql"); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to seed lookups: " + err.Error()}) + return + } + + c.JSON(http.StatusOK, gin.H{"message": "Lookup data seeded successfully"}) +} + +// SeedTestData handles POST /api/admin/settings/seed-test-data +func (h *AdminSettingsHandler) SeedTestData(c *gin.Context) { + if err := h.runSeedFile("002_test_data.sql"); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to seed test data: " + err.Error()}) + return + } + + c.JSON(http.StatusOK, gin.H{"message": "Test data seeded successfully"}) +} + +// runSeedFile executes a seed SQL file +func (h *AdminSettingsHandler) runSeedFile(filename string) error { + // Check multiple possible locations + possiblePaths := []string{ + filepath.Join("seeds", filename), + filepath.Join("./seeds", filename), + filepath.Join("/app/seeds", filename), + } + + var sqlContent []byte + var err error + for _, path := range possiblePaths { + sqlContent, err = os.ReadFile(path) + if err == nil { + break + } + } + + if err != nil { + return err + } + + return h.db.Exec(string(sqlContent)).Error +} diff --git a/internal/admin/routes.go b/internal/admin/routes.go index 879808d..2478797 100644 --- a/internal/admin/routes.go +++ b/internal/admin/routes.go @@ -249,6 +249,16 @@ func SetupRoutes(router *gin.Engine, db *gorm.DB, cfg *config.Config, deps *Depe notifPrefs.DELETE("/:id", notifPrefsHandler.Delete) notifPrefs.GET("/user/:user_id", notifPrefsHandler.GetByUser) } + + // System settings management + settingsHandler := handlers.NewAdminSettingsHandler(db) + settings := protected.Group("/settings") + { + settings.GET("", settingsHandler.GetSettings) + settings.PUT("", settingsHandler.UpdateSettings) + settings.POST("/seed-lookups", settingsHandler.SeedLookups) + settings.POST("/seed-test-data", settingsHandler.SeedTestData) + } } }