From d30decf723241382a2023d5f02ec9d94603d9e40 Mon Sep 17 00:00:00 2001 From: Nicholai Date: Wed, 4 Feb 2026 16:39:39 -0700 Subject: [PATCH] feat(ui): add mobile support and dashboard improvements (#30) * feat(schema): add auth, people, and financial tables Add users, organizations, teams, groups, and project members tables. Extend customers/vendors with netsuite fields. Add netsuite schema for invoices, bills, payments, and credit memos. Include all migrations, seeds, new UI primitives, and config updates. * feat(auth): add WorkOS authentication system Add login, signup, password reset, email verification, and invitation flows via WorkOS AuthKit. Includes auth middleware, permission helpers, dev mode fallbacks, and auth page components. * feat(people): add people management system Add user, team, group, and organization management with CRUD actions, dashboard pages, invite dialog, user drawer, and role-based filtering. Includes WorkOS invitation integration. * feat(netsuite): add NetSuite integration and financials Add bidirectional NetSuite REST API integration with OAuth 2.0, rate limiting, sync engine, and conflict resolution. Includes invoices, vendor bills, payments, credit memos CRUD, customer/vendor management pages, and financial dashboard with tabbed views. * feat(ui): add mobile support and dashboard improvements Add mobile bottom nav, FAB, filter bar, search, project switcher, pull-to-refresh, and schedule mobile view. Update sidebar with new nav items, settings modal with integrations tab, responsive dialogs, improved schedule and file components, PWA manifest, and service worker. * ci: retrigger build * ci: retrigger build --------- Co-authored-by: Nicholai --- public/manifest.json | 24 + public/sw.js | 34 ++ src/app/dashboard/layout.tsx | 11 +- src/app/dashboard/page.tsx | 102 ++-- src/app/dashboard/projects/[id]/page.tsx | 114 ++-- src/app/layout.tsx | 6 + src/components/account-modal.tsx | 69 ++- src/components/app-sidebar.tsx | 35 +- src/components/command-menu-provider.tsx | 21 +- src/components/dashboard-tabs.tsx | 56 ++ src/components/data-table.tsx | 123 ++-- src/components/feedback-widget.tsx | 39 +- src/components/files/file-browser.tsx | 1 + src/components/files/file-grid.tsx | 8 +- src/components/files/file-item.tsx | 29 +- src/components/files/file-list.tsx | 48 +- src/components/files/file-toolbar.tsx | 128 ++-- src/components/mobile-bottom-nav.tsx | 121 ++++ src/components/mobile-fab.tsx | 123 ++++ src/components/mobile-filter-bar.tsx | 162 +++++ src/components/mobile-project-switcher.tsx | 130 +++++ src/components/mobile-search.tsx | 552 ++++++++++++++++++ src/components/notifications-popover.tsx | 100 +++- src/components/project-list-provider.tsx | 25 + .../schedule/schedule-baseline-view.tsx | 5 +- .../schedule/schedule-calendar-view.tsx | 38 +- .../schedule/schedule-gantt-view.tsx | 537 +++++++++++------ .../schedule/schedule-list-view.tsx | 99 ++-- .../schedule/schedule-mobile-view.tsx | 223 +++++++ src/components/schedule/schedule-toolbar.tsx | 96 +-- src/components/schedule/schedule-view.tsx | 22 +- src/components/schedule/task-form-dialog.tsx | 390 +++++++------ .../workday-exception-form-dialog.tsx | 91 ++- .../schedule/workday-exceptions-view.tsx | 66 ++- src/components/settings-modal.tsx | 221 +++++-- src/components/site-header.tsx | 201 ++++--- src/hooks/use-pull-to-refresh.ts | 59 ++ 37 files changed, 3044 insertions(+), 1065 deletions(-) create mode 100755 public/manifest.json create mode 100755 public/sw.js create mode 100755 src/components/dashboard-tabs.tsx create mode 100755 src/components/mobile-bottom-nav.tsx create mode 100755 src/components/mobile-fab.tsx create mode 100755 src/components/mobile-filter-bar.tsx create mode 100755 src/components/mobile-project-switcher.tsx create mode 100755 src/components/mobile-search.tsx create mode 100755 src/components/project-list-provider.tsx create mode 100755 src/components/schedule/schedule-mobile-view.tsx create mode 100755 src/hooks/use-pull-to-refresh.ts diff --git a/public/manifest.json b/public/manifest.json new file mode 100755 index 0000000..8eb6a36 --- /dev/null +++ b/public/manifest.json @@ -0,0 +1,24 @@ +{ + "name": "Compass Dashboard", + "short_name": "Compass", + "description": "Construction project management by High Performance Structures", + "start_url": "/dashboard", + "display": "standalone", + "background_color": "#ffffff", + "theme_color": "#000000", + "orientation": "portrait-primary", + "icons": [ + { + "src": "/favicon.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "any maskable" + }, + { + "src": "/apple-touch-icon.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "any" + } + ] +} diff --git a/public/sw.js b/public/sw.js new file mode 100755 index 0000000..730c939 --- /dev/null +++ b/public/sw.js @@ -0,0 +1,34 @@ +// basic service worker for PWA support +const CACHE_NAME = 'compass-v1'; +const urlsToCache = [ + '/', + '/dashboard', +]; + +self.addEventListener('install', (event) => { + event.waitUntil( + caches.open(CACHE_NAME) + .then((cache) => cache.addAll(urlsToCache)) + ); +}); + +self.addEventListener('fetch', (event) => { + event.respondWith( + caches.match(event.request) + .then((response) => response || fetch(event.request)) + ); +}); + +self.addEventListener('activate', (event) => { + event.waitUntil( + caches.keys().then((cacheNames) => { + return Promise.all( + cacheNames.map((cacheName) => { + if (cacheName !== CACHE_NAME) { + return caches.delete(cacheName); + } + }) + ); + }) + ); +}); diff --git a/src/app/dashboard/layout.tsx b/src/app/dashboard/layout.tsx index 8395bca..7cf4214 100755 --- a/src/app/dashboard/layout.tsx +++ b/src/app/dashboard/layout.tsx @@ -1,5 +1,6 @@ import { AppSidebar } from "@/components/app-sidebar" import { SiteHeader } from "@/components/site-header" +import { MobileBottomNav } from "@/components/mobile-bottom-nav" import { CommandMenuProvider } from "@/components/command-menu-provider" import { SettingsProvider } from "@/components/settings-provider" import { FeedbackWidget } from "@/components/feedback-widget" @@ -9,6 +10,7 @@ import { SidebarProvider, } from "@/components/ui/sidebar" import { getProjects } from "@/app/actions/projects" +import { ProjectListProvider } from "@/components/project-list-provider" export default async function DashboardLayout({ children, @@ -19,6 +21,7 @@ export default async function DashboardLayout({ return ( + -
-
+
+
{children}
-

+ +

Pre-alpha build

+ ) } diff --git a/src/app/dashboard/page.tsx b/src/app/dashboard/page.tsx index 3fce516..170ef8d 100755 --- a/src/app/dashboard/page.tsx +++ b/src/app/dashboard/page.tsx @@ -84,9 +84,9 @@ export default async function Page() { const data = await getRepoData() return ( -
-
-
+
+
+
-

+

Compass

-

+

Development preview — features may be incomplete or change without notice.

@@ -110,85 +110,85 @@ export default async function Page() {
-
-
+
+
-

+

Working

-
    -
  • Projects — create and manage projects with D1 database
  • -
  • Schedule — Gantt chart with phases, tasks, dependencies, and critical path
  • -
  • File browser — drive-style UI with folder navigation
  • -
  • Settings — app preferences with theme and notifications
  • -
  • Sidebar navigation with contextual project/file views
  • -
  • Command palette search (Cmd+K)
  • +
      +
    • Projects — create and manage projects with D1 database
    • +
    • Schedule — Gantt chart with phases, tasks, dependencies, and critical path
    • +
    • File browser — drive-style UI with folder navigation
    • +
    • Settings — app preferences with theme and notifications
    • +
    • Sidebar navigation with contextual project/file views
    • +
    • Command palette search (Cmd+K)
-

+

In Progress

-
    -
  • Project auto-provisioning (code generation, CSI folder structure)
  • -
  • Budget tracking (CSI divisions, estimated vs actual, change orders)
  • -
  • Document management (S3/R2 storage, metadata, versioning)
  • -
  • Communication logging (manual entries, timeline view)
  • -
  • Dashboard — three-column layout (past due, due today, action items)
  • -
  • User authentication and roles (WorkOS)
  • -
  • Email notifications (Resend)
  • -
  • Basic reports (budget variance, overdue tasks, monthly actuals)
  • +
      +
    • Project auto-provisioning (code generation, CSI folder structure)
    • +
    • Budget tracking (CSI divisions, estimated vs actual, change orders)
    • +
    • Document management (S3/R2 storage, metadata, versioning)
    • +
    • Communication logging (manual entries, timeline view)
    • +
    • Dashboard — three-column layout (past due, due today, action items)
    • +
    • User authentication and roles (WorkOS)
    • +
    • Email notifications (Resend)
    • +
    • Basic reports (budget variance, overdue tasks, monthly actuals)
-

+

Planned

-
    -
  • Client portal with read-only views
  • -
  • BuilderTrend import wizard (CSV-based)
  • -
  • Daily logs
  • -
  • Time tracking
  • -
  • Report builder (custom fields and filters)
  • -
  • Bid package management
  • +
      +
    • Client portal with read-only views
    • +
    • BuilderTrend import wizard (CSV-based)
    • +
    • Daily logs
    • +
    • Time tracking
    • +
    • Report builder (custom fields and filters)
    • +
    • Bid package management
-

+

Future

-
    -
  • Netsuite/QuickBooks API sync
  • -
  • Payment integration
  • -
  • RFI/Submittal tracking
  • -
  • Native mobile apps (iOS/Android)
  • -
  • Advanced scheduling (resource leveling, baseline comparison)
  • +
      +
    • Netsuite/QuickBooks API sync
    • +
    • Payment integration
    • +
    • RFI/Submittal tracking
    • +
    • Native mobile apps (iOS/Android)
    • +
    • Advanced scheduling (resource leveling, baseline comparison)
{data && ( -
+
-
+

View on GitHub

{REPO}

- +
-

+

Recent Commits

@@ -224,20 +224,20 @@ export default async function Page() { href={commit.html_url} target="_blank" rel="noopener noreferrer" - className="hover:bg-muted/50 flex items-start gap-3 px-4 py-3 transition-colors" + className="hover:bg-muted/50 flex items-start gap-2 sm:gap-3 px-3 sm:px-4 py-2 sm:py-3 transition-colors" >
-

+

{commit.commit.message.split("\n")[0]}

-

+

{commit.commit.author.name} · {timeAgo(commit.commit.author.date)}

- + {commit.sha.slice(0, 7)} @@ -262,12 +262,12 @@ function StatCard({ value: number }) { return ( -
+
{icon} {label}
-

+

{value.toLocaleString()}

diff --git a/src/app/dashboard/projects/[id]/page.tsx b/src/app/dashboard/projects/[id]/page.tsx index c188f75..f2a33da 100755 --- a/src/app/dashboard/projects/[id]/page.tsx +++ b/src/app/dashboard/projects/[id]/page.tsx @@ -4,6 +4,7 @@ import { projects, scheduleTasks } from "@/db/schema" import { eq } from "drizzle-orm" import { notFound } from "next/navigation" import Link from "next/link" +import { MobileProjectSwitcher } from "@/components/mobile-project-switcher" import { IconAlertTriangle, IconCalendarStats, @@ -11,9 +12,7 @@ import { IconClock, IconDots, IconFlag, - IconPlus, IconThumbUp, - IconUser, } from "@tabler/icons-react" import type { ScheduleTask } from "@/db/schema" @@ -135,81 +134,44 @@ export default async function ProjectSummaryPage({
{/* header */} -
-
-
-

{projectName}

- - {projectStatus} - -
- {project?.address && ( -

- {project.address} -

- )} -

- {totalCount} tasks · {completedPercent}% complete -

-
-
- {/* client / pm row */} -
-
-

- Client -

-
- {project?.clientName ? ( - <> -
- {project.clientName.split(" ").map(w => w[0]).join("").slice(0, 2)} -
- {project.clientName} - - ) : ( - - )} -
-
-
-

- Project Manager -

-
- {project?.projectManager ? ( - <> -
- -
- {project.projectManager} - - ) : ( - - )} -
-
-
- - - View schedule - -
+ {/* meta line: address + tasks */} +
+ {project?.address &&

{project.address}

} +

+ {totalCount} tasks · {completedPercent}% complete + {project?.clientName && ( + <> · {project.clientName} + )} + {project?.projectManager && ( + <> · {project.projectManager} + )} +

+
+ + {/* schedule link */} +
+ + + View schedule +
{/* progress bar */} -
+

Overall Progress

{completedPercent}%

@@ -230,8 +192,8 @@ export default async function ProjectSummaryPage({
{/* urgency columns */} -
-
+
+

Past Due

@@ -261,7 +223,7 @@ export default async function ProjectSummaryPage({ )}
-
+

Due Today

@@ -279,7 +241,7 @@ export default async function ProjectSummaryPage({ )}
-
+

Upcoming Milestones

@@ -306,7 +268,7 @@ export default async function ProjectSummaryPage({
{/* two-column: phases + active tasks */} -
+
{/* phase breakdown */}

@@ -421,7 +383,7 @@ export default async function ProjectSummaryPage({

{/* right sidebar: week agenda */} -
+

This Week diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 6e7ec7d..59cfc88 100755 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -27,6 +27,12 @@ export const metadata: Metadata = { icon: "/favicon.png", apple: "/apple-touch-icon.png", }, + manifest: "/manifest.json", + viewport: { + width: "device-width", + initialScale: 1, + maximumScale: 5, + }, }; export default function RootLayout({ diff --git a/src/components/account-modal.tsx b/src/components/account-modal.tsx index d34066e..99eaed2 100755 --- a/src/components/account-modal.tsx +++ b/src/components/account-modal.tsx @@ -32,96 +32,101 @@ export function AccountModal({ return ( - - - Account Settings - + + + Account Settings + Manage your profile and security settings. -
-
+
+
- + - MV + MV -
-
-

{name}

-

{email}

+
+

{name}

+

{email}

- + -
-

Profile

-
- +
+

Profile

+
+ setName(e.target.value)} + className="h-9" />
-
- +
+ setEmail(e.target.value)} + className="h-9" />
- + -
-

Change Password

-
- +
+

Change Password

+
+ setCurrentPassword(e.target.value)} placeholder="Enter current password" + className="h-9" />
-
- +
+ setNewPassword(e.target.value)} placeholder="Enter new password" + className="h-9" />
-
- +
+ setConfirmPassword(e.target.value)} placeholder="Confirm new password" + className="h-9" />
- - - diff --git a/src/components/app-sidebar.tsx b/src/components/app-sidebar.tsx index 23cd6dc..ab21542 100755 --- a/src/components/app-sidebar.tsx +++ b/src/components/app-sidebar.tsx @@ -2,13 +2,17 @@ import * as React from "react" import { + IconAddressBook, IconCalendarStats, IconDashboard, IconFiles, IconFolder, IconHelp, + IconReceipt, IconSearch, IconSettings, + IconTruck, + IconUsers, } from "@tabler/icons-react" import { usePathname } from "next/navigation" @@ -47,6 +51,11 @@ const data = { url: "/dashboard/projects", icon: IconFolder, }, + { + title: "People", + url: "/dashboard/people", + icon: IconUsers, + }, { title: "Schedule", url: "/dashboard/projects/demo-project-1/schedule", @@ -57,6 +66,21 @@ const data = { url: "/dashboard/files", icon: IconFiles, }, + { + title: "Customers", + url: "/dashboard/customers", + icon: IconAddressBook, + }, + { + title: "Vendors", + url: "/dashboard/vendors", + icon: IconTruck, + }, + { + title: "Financials", + url: "/dashboard/financials", + icon: IconReceipt, + }, ], navSecondary: [ { @@ -87,11 +111,12 @@ function SidebarNav({ pathname ?? "" ) - React.useEffect(() => { - if ((isFilesMode || isProjectMode) && !isExpanded) { - setOpen(true) - } - }, [isFilesMode, isProjectMode, isExpanded, setOpen]) + // Allow manual collapse/expand in all modes + // React.useEffect(() => { + // if ((isFilesMode || isProjectMode) && !isExpanded) { + // setOpen(true) + // } + // }, [isFilesMode, isProjectMode, isExpanded, setOpen]) const showContext = isExpanded && (isFilesMode || isProjectMode) diff --git a/src/components/command-menu-provider.tsx b/src/components/command-menu-provider.tsx index 46df749..fa1ea79 100755 --- a/src/components/command-menu-provider.tsx +++ b/src/components/command-menu-provider.tsx @@ -2,6 +2,8 @@ import * as React from "react" import { CommandMenu } from "@/components/command-menu" +import { MobileSearch } from "@/components/mobile-search" +import { useIsMobile } from "@/hooks/use-mobile" const CommandMenuContext = React.createContext<{ open: () => void @@ -16,17 +18,32 @@ export function CommandMenuProvider({ }: { children: React.ReactNode }) { + const isMobile = useIsMobile() const [isOpen, setIsOpen] = React.useState(false) + const [mobileSearchOpen, setMobileSearchOpen] = + React.useState(false) const value = React.useMemo( - () => ({ open: () => setIsOpen(true) }), - [] + () => ({ + open: () => { + if (isMobile) { + setMobileSearchOpen(true) + } else { + setIsOpen(true) + } + }, + }), + [isMobile] ) return ( {children} + ) } diff --git a/src/components/dashboard-tabs.tsx b/src/components/dashboard-tabs.tsx new file mode 100755 index 0000000..06f5ad1 --- /dev/null +++ b/src/components/dashboard-tabs.tsx @@ -0,0 +1,56 @@ +"use client" + +import * as React from "react" +import { cn } from "@/lib/utils" + +interface Tab { + id: string + label: string + count?: number +} + +interface DashboardTabsProps { + tabs: Tab[] + activeTab: string + onTabChange: (tabId: string) => void + className?: string +} + +export function DashboardTabs({ + tabs, + activeTab, + onTabChange, + className, +}: DashboardTabsProps) { + return ( +
+
+ {tabs.map((tab) => ( + + ))} +
+
+ ) +} diff --git a/src/components/data-table.tsx b/src/components/data-table.tsx index 1d977f8..cd29dcb 100755 --- a/src/components/data-table.tsx +++ b/src/components/data-table.tsx @@ -141,6 +141,7 @@ const columns: ColumnDef>[] = [ id: "drag", header: () => null, cell: ({ row }) => , + meta: { className: "hidden md:table-cell" }, }, { id: "select", @@ -180,12 +181,13 @@ const columns: ColumnDef>[] = [ accessorKey: "type", header: "Section Type", cell: ({ row }) => ( -
+
{row.original.type}
), + meta: { className: "hidden lg:table-cell" }, }, { accessorKey: "status", @@ -250,6 +252,7 @@ const columns: ColumnDef>[] = [ /> ), + meta: { className: "hidden lg:table-cell" }, }, { accessorKey: "reviewer", @@ -327,11 +330,14 @@ function DraggableRow({ row }: { row: Row> }) { transition: transition, }} > - {row.getVisibleCells().map((cell) => ( - - {flexRender(cell.column.columnDef.cell, cell.getContext())} - - ))} + {row.getVisibleCells().map((cell) => { + const meta = cell.column.columnDef.meta as { className?: string } | undefined + return ( + + {flexRender(cell.column.columnDef.cell, cell.getContext())} + + ) + })} ) } @@ -479,56 +485,63 @@ export function DataTable({ value="outline" className="relative flex flex-col gap-4 overflow-auto px-4 lg:px-6" > -
- - - - {table.getHeaderGroups().map((headerGroup) => ( - - {headerGroup.headers.map((header) => { - return ( - - {header.isPlaceholder - ? null - : flexRender( - header.column.columnDef.header, - header.getContext() - )} - - ) - })} - - ))} - - - {table.getRowModel().rows?.length ? ( - - {table.getRowModel().rows.map((row) => ( - - ))} - - ) : ( - - +
+ +
+ + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + const meta = header.column.columnDef.meta as { className?: string } | undefined + return ( + + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext() + )} + + ) + })} + + ))} + + + {table.getRowModel().rows?.length ? ( + - No results. - - - )} - -
-
+ {table.getRowModel().rows.map((row) => ( + + ))} + + ) : ( + + + No results. + + + )} + + + +
diff --git a/src/components/feedback-widget.tsx b/src/components/feedback-widget.tsx index f68b659..8845f5a 100755 --- a/src/components/feedback-widget.tsx +++ b/src/components/feedback-widget.tsx @@ -101,7 +101,7 @@ export function FeedbackWidget({ children }: { children?: React.ReactNode }) { - - - Send Feedback - + + + Send Feedback + Report a bug, request a feature, or ask a question. -
-
- + +
+
-
- +
+