From 653fff6ce285d83e3a9fd5dbdae2150c2ca4fc59 Mon Sep 17 00:00:00 2001 From: Ryan Moon Date: Sun, 29 Mar 2026 15:56:02 -0500 Subject: [PATCH] Add store settings page with location management Company table gains address and logo_file_id columns. New store settings API: GET/PATCH /store for company info, full CRUD for /locations. Settings page shows store name, phone, email, address, timezone with inline edit. Location cards with add/edit/delete. Settings link in admin sidebar. Fixes leftover company_id on location table and seed files. --- packages/admin/src/routeTree.gen.ts | 21 + packages/admin/src/routes/_authenticated.tsx | 3 +- .../src/routes/_authenticated/settings.tsx | 376 ++++++++++++++++++ .../src/db/migrations/0023_store_settings.sql | 6 + .../src/db/migrations/meta/_journal.json | 7 + packages/backend/src/db/schema/stores.ts | 10 +- packages/backend/src/db/seed.ts | 1 - packages/backend/src/main.ts | 2 + packages/backend/src/routes/v1/store.ts | 76 ++++ packages/backend/src/test/helpers.ts | 1 - 10 files changed, 497 insertions(+), 6 deletions(-) create mode 100644 packages/admin/src/routes/_authenticated/settings.tsx create mode 100644 packages/backend/src/db/migrations/0023_store_settings.sql create mode 100644 packages/backend/src/routes/v1/store.ts diff --git a/packages/admin/src/routeTree.gen.ts b/packages/admin/src/routeTree.gen.ts index 8fce3e2..04259ad 100644 --- a/packages/admin/src/routeTree.gen.ts +++ b/packages/admin/src/routeTree.gen.ts @@ -13,6 +13,7 @@ import { Route as LoginRouteImport } from './routes/login' import { Route as AuthenticatedRouteImport } from './routes/_authenticated' import { Route as AuthenticatedIndexRouteImport } from './routes/_authenticated/index' import { Route as AuthenticatedUsersRouteImport } from './routes/_authenticated/users' +import { Route as AuthenticatedSettingsRouteImport } from './routes/_authenticated/settings' import { Route as AuthenticatedProfileRouteImport } from './routes/_authenticated/profile' import { Route as AuthenticatedHelpRouteImport } from './routes/_authenticated/help' import { Route as AuthenticatedRolesIndexRouteImport } from './routes/_authenticated/roles/index' @@ -56,6 +57,11 @@ const AuthenticatedUsersRoute = AuthenticatedUsersRouteImport.update({ path: '/users', getParentRoute: () => AuthenticatedRoute, } as any) +const AuthenticatedSettingsRoute = AuthenticatedSettingsRouteImport.update({ + id: '/settings', + path: '/settings', + getParentRoute: () => AuthenticatedRoute, +} as any) const AuthenticatedProfileRoute = AuthenticatedProfileRouteImport.update({ id: '/profile', path: '/profile', @@ -194,6 +200,7 @@ export interface FileRoutesByFullPath { '/login': typeof LoginRoute '/help': typeof AuthenticatedHelpRoute '/profile': typeof AuthenticatedProfileRoute + '/settings': typeof AuthenticatedSettingsRoute '/users': typeof AuthenticatedUsersRoute '/accounts/$accountId': typeof AuthenticatedAccountsAccountIdRouteWithChildren '/accounts/new': typeof AuthenticatedAccountsNewRoute @@ -221,6 +228,7 @@ export interface FileRoutesByTo { '/login': typeof LoginRoute '/help': typeof AuthenticatedHelpRoute '/profile': typeof AuthenticatedProfileRoute + '/settings': typeof AuthenticatedSettingsRoute '/users': typeof AuthenticatedUsersRoute '/': typeof AuthenticatedIndexRoute '/accounts/new': typeof AuthenticatedAccountsNewRoute @@ -250,6 +258,7 @@ export interface FileRoutesById { '/login': typeof LoginRoute '/_authenticated/help': typeof AuthenticatedHelpRoute '/_authenticated/profile': typeof AuthenticatedProfileRoute + '/_authenticated/settings': typeof AuthenticatedSettingsRoute '/_authenticated/users': typeof AuthenticatedUsersRoute '/_authenticated/': typeof AuthenticatedIndexRoute '/_authenticated/accounts/$accountId': typeof AuthenticatedAccountsAccountIdRouteWithChildren @@ -281,6 +290,7 @@ export interface FileRouteTypes { | '/login' | '/help' | '/profile' + | '/settings' | '/users' | '/accounts/$accountId' | '/accounts/new' @@ -308,6 +318,7 @@ export interface FileRouteTypes { | '/login' | '/help' | '/profile' + | '/settings' | '/users' | '/' | '/accounts/new' @@ -336,6 +347,7 @@ export interface FileRouteTypes { | '/login' | '/_authenticated/help' | '/_authenticated/profile' + | '/_authenticated/settings' | '/_authenticated/users' | '/_authenticated/' | '/_authenticated/accounts/$accountId' @@ -396,6 +408,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof AuthenticatedUsersRouteImport parentRoute: typeof AuthenticatedRoute } + '/_authenticated/settings': { + id: '/_authenticated/settings' + path: '/settings' + fullPath: '/settings' + preLoaderRoute: typeof AuthenticatedSettingsRouteImport + parentRoute: typeof AuthenticatedRoute + } '/_authenticated/profile': { id: '/_authenticated/profile' path: '/profile' @@ -590,6 +609,7 @@ const AuthenticatedAccountsAccountIdRouteWithChildren = interface AuthenticatedRouteChildren { AuthenticatedHelpRoute: typeof AuthenticatedHelpRoute AuthenticatedProfileRoute: typeof AuthenticatedProfileRoute + AuthenticatedSettingsRoute: typeof AuthenticatedSettingsRoute AuthenticatedUsersRoute: typeof AuthenticatedUsersRoute AuthenticatedIndexRoute: typeof AuthenticatedIndexRoute AuthenticatedAccountsAccountIdRoute: typeof AuthenticatedAccountsAccountIdRouteWithChildren @@ -613,6 +633,7 @@ interface AuthenticatedRouteChildren { const AuthenticatedRouteChildren: AuthenticatedRouteChildren = { AuthenticatedHelpRoute: AuthenticatedHelpRoute, AuthenticatedProfileRoute: AuthenticatedProfileRoute, + AuthenticatedSettingsRoute: AuthenticatedSettingsRoute, AuthenticatedUsersRoute: AuthenticatedUsersRoute, AuthenticatedIndexRoute: AuthenticatedIndexRoute, AuthenticatedAccountsAccountIdRoute: diff --git a/packages/admin/src/routes/_authenticated.tsx b/packages/admin/src/routes/_authenticated.tsx index f94f9be..7b5e1b7 100644 --- a/packages/admin/src/routes/_authenticated.tsx +++ b/packages/admin/src/routes/_authenticated.tsx @@ -5,7 +5,7 @@ import { useAuthStore } from '@/stores/auth.store' import { myPermissionsOptions } from '@/api/rbac' import { Avatar } from '@/components/shared/avatar-upload' import { Button } from '@/components/ui/button' -import { Users, UserRound, HelpCircle, Shield, UserCog, LogOut, User, Wrench, Package, ClipboardList, FolderOpen } from 'lucide-react' +import { Users, UserRound, HelpCircle, Shield, UserCog, LogOut, User, Wrench, Package, ClipboardList, FolderOpen, Settings } from 'lucide-react' export const Route = createFileRoute('/_authenticated')({ beforeLoad: () => { @@ -97,6 +97,7 @@ function AuthenticatedLayout() { <> } label="Users" /> } label="Roles" /> + } label="Settings" /> )} diff --git a/packages/admin/src/routes/_authenticated/settings.tsx b/packages/admin/src/routes/_authenticated/settings.tsx new file mode 100644 index 0000000..16ebbe0 --- /dev/null +++ b/packages/admin/src/routes/_authenticated/settings.tsx @@ -0,0 +1,376 @@ +import { useState } from 'react' +import { createFileRoute } from '@tanstack/react-router' +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' +import { queryOptions } from '@tanstack/react-query' +import { api } from '@/lib/api-client' +import { useAuthStore } from '@/stores/auth.store' +import { Button } from '@/components/ui/button' +import { Input } from '@/components/ui/input' +import { Label } from '@/components/ui/label' +import { Textarea } from '@/components/ui/textarea' +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' +import { Skeleton } from '@/components/ui/skeleton' +import { Badge } from '@/components/ui/badge' +import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog' +import { AvatarUpload } from '@/components/shared/avatar-upload' +import { Save, Plus, Trash2, MapPin, Building } from 'lucide-react' +import { toast } from 'sonner' + +interface StoreSettings { + id: string + name: string + phone: string | null + email: string | null + address: { street?: string; city?: string; state?: string; zip?: string } | null + timezone: string + logoFileId: string | null + notes: string | null +} + +interface Location { + id: string + name: string + phone: string | null + email: string | null + address: { street?: string; city?: string; state?: string; zip?: string } | null + timezone: string | null + isActive: boolean +} + +function storeOptions() { + return queryOptions({ + queryKey: ['store'], + queryFn: () => api.get('/v1/store'), + }) +} + +function locationsOptions() { + return queryOptions({ + queryKey: ['locations'], + queryFn: () => api.get<{ data: Location[] }>('/v1/locations'), + }) +} + +export const Route = createFileRoute('/_authenticated/settings')({ + component: SettingsPage, +}) + +function SettingsPage() { + const queryClient = useQueryClient() + const { data: store, isLoading } = useQuery(storeOptions()) + const { data: locationsData } = useQuery(locationsOptions()) + const [addLocationOpen, setAddLocationOpen] = useState(false) + + // Store edit state + const [editing, setEditing] = useState(false) + const [fields, setFields] = useState>({}) + + const updateMutation = useMutation({ + mutationFn: (data: Record) => api.patch('/v1/store', data), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['store'] }) + toast.success('Store settings updated') + setEditing(false) + }, + onError: (err) => toast.error(err.message), + }) + + function startEdit() { + if (!store) return + setFields({ + name: store.name, + phone: store.phone ?? '', + email: store.email ?? '', + street: store.address?.street ?? '', + city: store.address?.city ?? '', + state: store.address?.state ?? '', + zip: store.address?.zip ?? '', + timezone: store.timezone, + notes: store.notes ?? '', + }) + setEditing(true) + } + + function saveEdit() { + updateMutation.mutate({ + name: fields.name, + phone: fields.phone || null, + email: fields.email || null, + address: { street: fields.street, city: fields.city, state: fields.state, zip: fields.zip }, + timezone: fields.timezone, + notes: fields.notes || null, + }) + } + + if (isLoading) { + return
+ } + + if (!store) { + return

No store configured

+ } + + const locations = locationsData?.data ?? [] + + return ( +
+

Settings

+ + {/* Store Info */} + + + + Store Information + + {!editing && } + {editing && ( +
+ + +
+ )} +
+ + {editing ? ( +
+
+
+ + setFields((p) => ({ ...p, name: e.target.value }))} /> +
+
+ + setFields((p) => ({ ...p, timezone: e.target.value }))} placeholder="America/Chicago" /> +
+
+
+
+ + setFields((p) => ({ ...p, phone: e.target.value }))} /> +
+
+ + setFields((p) => ({ ...p, email: e.target.value }))} /> +
+
+
+
+ + setFields((p) => ({ ...p, street: e.target.value }))} /> +
+
+
+ + setFields((p) => ({ ...p, city: e.target.value }))} /> +
+
+ + setFields((p) => ({ ...p, state: e.target.value }))} /> +
+
+ + setFields((p) => ({ ...p, zip: e.target.value }))} /> +
+
+
+
+ +