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
+
+
+
+
+
+
Enable Limitations
+
+ 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.)
+
+
+
+
+
+
+ {seedLookupsMutation.isPending ? 'Seeding...' : 'Seed Lookup Data'}
+
+
+
+
+ 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
+
+
+
+
+
+
+ {seedTestDataMutation.isPending ? 'Seeding...' : 'Seed Test Data'}
+
+
+
+
+ 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)
+ }
}
}