Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM, PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source files covering clients, berths, interests/pipeline, documents/EOI, expenses/invoices, email, notifications, dashboard, admin, and client portal. CI/CD via Gitea Actions with Docker builds. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
135
src/components/shared/saved-views-dropdown.tsx
Normal file
135
src/components/shared/saved-views-dropdown.tsx
Normal file
@@ -0,0 +1,135 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { Bookmark, Check, Plus, Trash2 } from 'lucide-react';
|
||||
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { useSavedViews } from '@/hooks/use-saved-views';
|
||||
|
||||
interface SavedViewsDropdownProps {
|
||||
entityType: string;
|
||||
currentFilters: Record<string, unknown>;
|
||||
currentSort?: { field: string; direction: 'asc' | 'desc' };
|
||||
onApplyView: (filters: Record<string, unknown>, sort?: { field: string; direction: string }) => void;
|
||||
}
|
||||
|
||||
export function SavedViewsDropdown({
|
||||
entityType,
|
||||
currentFilters,
|
||||
currentSort,
|
||||
onApplyView,
|
||||
}: SavedViewsDropdownProps) {
|
||||
const { views, activeViewId, saveCurrentView, deleteView, applyView } =
|
||||
useSavedViews(entityType);
|
||||
const [saveOpen, setSaveOpen] = useState(false);
|
||||
const [viewName, setViewName] = useState('');
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
|
||||
async function handleSave() {
|
||||
if (!viewName.trim()) return;
|
||||
setIsSaving(true);
|
||||
try {
|
||||
await saveCurrentView(viewName.trim(), currentFilters, currentSort);
|
||||
setSaveOpen(false);
|
||||
setViewName('');
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline" size="sm" className="h-8">
|
||||
<Bookmark className="mr-1.5 h-3.5 w-3.5" />
|
||||
Views
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-56">
|
||||
{views.length === 0 ? (
|
||||
<div className="px-2 py-3 text-sm text-muted-foreground text-center">
|
||||
No saved views yet
|
||||
</div>
|
||||
) : (
|
||||
views.map((view) => (
|
||||
<DropdownMenuItem
|
||||
key={view.id}
|
||||
className="flex items-center justify-between"
|
||||
onClick={() => {
|
||||
applyView(view.id);
|
||||
onApplyView(
|
||||
view.filters as Record<string, unknown>,
|
||||
view.sortConfig as { field: string; direction: string } | undefined,
|
||||
);
|
||||
}}
|
||||
>
|
||||
<span className="truncate">{view.name}</span>
|
||||
<div className="flex items-center gap-1">
|
||||
{activeViewId === view.id && (
|
||||
<Check className="h-3.5 w-3.5 text-primary" />
|
||||
)}
|
||||
<button
|
||||
className="p-0.5 rounded hover:bg-muted"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
deleteView(view.id);
|
||||
}}
|
||||
>
|
||||
<Trash2 className="h-3 w-3 text-muted-foreground" />
|
||||
</button>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
))
|
||||
)}
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem onClick={() => setSaveOpen(true)}>
|
||||
<Plus className="mr-2 h-3.5 w-3.5" />
|
||||
Save current view
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
<Dialog open={saveOpen} onOpenChange={setSaveOpen}>
|
||||
<DialogContent className="sm:max-w-sm">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Save View</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="space-y-2">
|
||||
<Label>View name</Label>
|
||||
<Input
|
||||
value={viewName}
|
||||
onChange={(e) => setViewName(e.target.value)}
|
||||
placeholder="My custom view"
|
||||
onKeyDown={(e) => e.key === 'Enter' && handleSave()}
|
||||
/>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setSaveOpen(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleSave} disabled={!viewName.trim() || isSaving}>
|
||||
Save
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user