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>
136 lines
4.2 KiB
TypeScript
136 lines
4.2 KiB
TypeScript
'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>
|
|
</>
|
|
);
|
|
}
|