* 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 <nicholaivogelfilms@gmail.com>
154 lines
4.9 KiB
TypeScript
Executable File
154 lines
4.9 KiB
TypeScript
Executable File
"use client"
|
|
|
|
import { useState } from "react"
|
|
import {
|
|
IconLayoutGrid,
|
|
IconList,
|
|
IconPlus,
|
|
IconUpload,
|
|
IconSearch,
|
|
IconSortAscending,
|
|
IconSortDescending,
|
|
IconFolder,
|
|
IconFileText,
|
|
IconTable,
|
|
IconPresentation,
|
|
} from "@tabler/icons-react"
|
|
|
|
import { useFiles, type SortField, type ViewMode } from "@/hooks/use-files"
|
|
import { Button } from "@/components/ui/button"
|
|
import { Input } from "@/components/ui/input"
|
|
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"
|
|
import {
|
|
DropdownMenu,
|
|
DropdownMenuContent,
|
|
DropdownMenuItem,
|
|
DropdownMenuSeparator,
|
|
DropdownMenuTrigger,
|
|
} from "@/components/ui/dropdown-menu"
|
|
|
|
export type NewFileType = "folder" | "document" | "spreadsheet" | "presentation"
|
|
|
|
export function FileToolbar({
|
|
onNew,
|
|
onUpload,
|
|
}: {
|
|
onNew: (type: NewFileType) => void
|
|
onUpload: () => void
|
|
}) {
|
|
const { state, dispatch } = useFiles()
|
|
const [searchFocused, setSearchFocused] = useState(false)
|
|
|
|
const sortLabels: Record<SortField, string> = {
|
|
name: "Name",
|
|
modified: "Modified",
|
|
size: "Size",
|
|
type: "Type",
|
|
}
|
|
|
|
const handleSort = (field: SortField) => {
|
|
const direction =
|
|
state.sortBy === field && state.sortDirection === "asc" ? "desc" : "asc"
|
|
dispatch({ type: "SET_SORT", payload: { field, direction } })
|
|
}
|
|
|
|
return (
|
|
<div className="flex flex-col sm:flex-row gap-2">
|
|
<div className="flex items-center gap-2 flex-1">
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger asChild>
|
|
<Button size="sm" variant="outline" className="h-10 sm:h-9">
|
|
<IconPlus size={18} className="sm:size-4" />
|
|
</Button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent align="start">
|
|
<DropdownMenuItem onClick={() => onNew("folder")}>
|
|
<IconFolder size={16} />
|
|
Folder
|
|
</DropdownMenuItem>
|
|
<DropdownMenuSeparator />
|
|
<DropdownMenuItem onClick={() => onNew("document")}>
|
|
<IconFileText size={16} />
|
|
Document
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem onClick={() => onNew("spreadsheet")}>
|
|
<IconTable size={16} />
|
|
Spreadsheet
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem onClick={() => onNew("presentation")}>
|
|
<IconPresentation size={16} />
|
|
Presentation
|
|
</DropdownMenuItem>
|
|
<DropdownMenuSeparator />
|
|
<DropdownMenuItem onClick={onUpload}>
|
|
<IconUpload size={16} />
|
|
File upload
|
|
</DropdownMenuItem>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
|
|
<div className="relative flex-1">
|
|
<IconSearch
|
|
size={16}
|
|
className="absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground"
|
|
/>
|
|
<Input
|
|
placeholder="Search files..."
|
|
value={state.searchQuery}
|
|
onChange={(e) =>
|
|
dispatch({ type: "SET_SEARCH", payload: e.target.value })
|
|
}
|
|
onFocus={() => setSearchFocused(true)}
|
|
onBlur={() => setSearchFocused(false)}
|
|
className="h-10 sm:h-9 pl-9 text-sm"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-2">
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger asChild>
|
|
<Button size="sm" variant="ghost" className="h-10 sm:h-9">
|
|
{state.sortDirection === "asc" ? (
|
|
<IconSortAscending size={18} className="sm:size-4" />
|
|
) : (
|
|
<IconSortDescending size={18} className="sm:size-4" />
|
|
)}
|
|
</Button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent align="end">
|
|
{(Object.keys(sortLabels) as SortField[]).map((field) => (
|
|
<DropdownMenuItem key={field} onClick={() => handleSort(field)}>
|
|
{sortLabels[field]}
|
|
{state.sortBy === field && (
|
|
<span className="ml-auto text-xs text-muted-foreground">
|
|
{state.sortDirection === "asc" ? "↑" : "↓"}
|
|
</span>
|
|
)}
|
|
</DropdownMenuItem>
|
|
))}
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
|
|
<ToggleGroup
|
|
type="single"
|
|
value={state.viewMode}
|
|
onValueChange={(v) => {
|
|
if (v) dispatch({ type: "SET_VIEW_MODE", payload: v as ViewMode })
|
|
}}
|
|
variant="outline"
|
|
size="sm"
|
|
className="h-10 sm:h-9"
|
|
>
|
|
<ToggleGroupItem value="grid" aria-label="Grid view" className="h-10 w-10 sm:h-9 sm:w-9">
|
|
<IconLayoutGrid size={18} className="sm:size-4" />
|
|
</ToggleGroupItem>
|
|
<ToggleGroupItem value="list" aria-label="List view" className="h-10 w-10 sm:h-9 sm:w-9">
|
|
<IconList size={18} className="sm:size-4" />
|
|
</ToggleGroupItem>
|
|
</ToggleGroup>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|