← back to Norma
components/drafts/DraftsTab.tsx
248 lines
'use client';
import { useState, useEffect, useCallback } from 'react';
import { Plus, RefreshCw, Inbox } from 'lucide-react';
import SessionCard from './SessionCard';
import AlertsFeed from './AlertsFeed';
import type { DraftData } from './DraftEditor';
import { useToast } from '../ToastProvider';
import { SkeletonList } from '../Skeleton';
import SortDropdown from '../shared/SortDropdown';
import CollapsibleSection from '../shared/CollapsibleSection';
import { useClientSort, SortConfig } from '@/hooks/useClientSort';
const SORT_OPTIONS = [
{ value: 'date_desc', label: 'Newest First' },
{ value: 'date_asc', label: 'Oldest First' },
{ value: 'status', label: 'Status A-Z' },
];
const SORT_CONFIGS: Record<string, SortConfig> = {
date_desc: { key: 'created_at', direction: 'desc', type: 'date' },
date_asc: { key: 'created_at', direction: 'asc', type: 'date' },
status: { key: 'status', direction: 'asc', type: 'string' },
};
/* ─── Types ──────────────────────────────────────────────────────────────── */
interface Session {
id: string;
title?: string;
session_date: string;
status: string;
planning_checklist?: Record<string, boolean>;
planning_notes?: string;
created_at: string;
drafts: DraftData[];
}
/* ─── DraftsTab ──────────────────────────────────────────────────────────── */
interface DraftsTabProps {
onNavigateToSettings?: () => void;
}
export default function DraftsTab({ onNavigateToSettings }: DraftsTabProps) {
const { addToast } = useToast();
const [sessions, setSessions] = useState<Session[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [creating, setCreating] = useState(false);
const [sortBy, setSortBy] = useState('date_desc');
const sortedSessions = useClientSort(sessions, sortBy, SORT_CONFIGS);
const fetchSessions = useCallback(async () => {
setLoading(true);
setError(null);
try {
const res = await fetch('/api/sessions');
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();
const list: Session[] = data.sessions ?? data ?? [];
/* Sorting handled by useClientSort (server returns ORDER BY session_date DESC) */
setSessions(list);
} catch {
setError('Failed to load sessions. Check your connection and try again.');
} finally {
setLoading(false);
}
}, []);
useEffect(() => { fetchSessions(); }, [fetchSessions]);
async function handleNewBatch() {
setCreating(true);
try {
const res = await fetch('/api/sessions', { method: 'POST' });
if (!res.ok) throw new Error(`HTTP ${res.status}`);
await fetchSessions();
addToast('Morning batch created', 'success');
} catch {
addToast('Failed to create a new Morning Batch', 'error');
} finally {
setCreating(false);
}
}
function handleSessionDeleted(id: string) {
setSessions((prev) => prev.filter((s) => s.id !== id));
}
function handleStatusChange(id: string, status: string) {
setSessions((prev) =>
prev.map((s) => (s.id === id ? { ...s, status } : s))
);
}
return (
<div className="p-4 sm:p-6 max-w-4xl mx-auto">
{/* ── Alerts Feed ──────────────────────────────────────────────────── */}
<AlertsFeed onNavigateToSettings={onNavigateToSettings} />
{/* ── Top bar ──────────────────────────────────────────────────────── */}
<div className="flex items-center justify-between mb-5 gap-3">
<div>
<h1
className="text-lg font-semibold"
style={{ color: 'var(--color-text)' }}
>
Email Drafts
</h1>
<p className="text-sm" style={{ color: 'var(--color-text-muted)' }}>
Morning batches with 3 lane drafts each
</p>
</div>
<div className="flex items-center gap-2">
<SortDropdown options={SORT_OPTIONS} value={sortBy} onChange={setSortBy} />
<button
type="button"
onClick={fetchSessions}
disabled={loading}
className="btn btn-ghost btn-sm"
aria-label="Refresh sessions"
title="Refresh"
>
<RefreshCw
size={14}
aria-hidden="true"
style={{
animation: loading ? 'spin 0.7s linear infinite' : 'none',
}}
/>
</button>
<button
type="button"
onClick={handleNewBatch}
disabled={creating}
className="btn btn-primary"
aria-label="Create new morning batch"
>
{creating ? (
<span
className="spinner"
style={{ width: 14, height: 14, borderWidth: 2 }}
/>
) : (
<Plus size={15} aria-hidden="true" />
)}
{creating ? 'Creating...' : 'New Morning Batch'}
</button>
</div>
</div>
{/* ── Loading state ─────────────────────────────────────────────────── */}
{loading && (
<div aria-live="polite" aria-busy="true">
<SkeletonList count={3} />
</div>
)}
{/* ── Error state ───────────────────────────────────────────────────── */}
{!loading && error && (
<div
className="rounded-xl flex flex-col items-center justify-center py-12 gap-3"
style={{
border: '1px solid rgba(239,68,68,0.3)',
backgroundColor: 'rgba(239,68,68,0.05)',
}}
role="alert"
>
<p className="font-medium" style={{ color: 'var(--color-error)' }}>
{error}
</p>
<button
type="button"
onClick={fetchSessions}
className="btn btn-secondary btn-sm"
>
<RefreshCw size={13} aria-hidden="true" />
Retry
</button>
</div>
)}
{/* ── Empty state ───────────────────────────────────────────────────── */}
{!loading && !error && sessions.length === 0 && (
<div
className="rounded-xl flex flex-col items-center justify-center py-16 gap-3"
style={{
backgroundColor: 'var(--color-surface)',
border: '1px dashed var(--color-border)',
}}
>
<Inbox
size={36}
style={{ color: 'var(--color-text-muted)' }}
aria-hidden="true"
/>
<p
className="font-medium text-lg"
style={{ color: 'var(--color-text)' }}
>
No morning batches yet
</p>
<p className="text-sm text-center max-w-xs" style={{ color: 'var(--color-text-muted)' }}>
Click "New Morning Batch" to create your first session with 3 lane
drafts (Action, Policy, Resources).
</p>
<button
type="button"
onClick={handleNewBatch}
disabled={creating}
className="btn btn-primary mt-1"
>
<Plus size={15} aria-hidden="true" />
New Morning Batch
</button>
</div>
)}
{/* ── Session list ──────────────────────────────────────────────────── */}
{!loading && !error && sessions.length > 0 && (
<CollapsibleSection
id="drafts-sessions"
title="Morning Batches"
count={sessions.length}
defaultOpen={true}
noPadding
>
<div
className="flex flex-col gap-4"
role="feed"
aria-label="Morning batch sessions"
style={{ padding: '0 0 1rem' }}
>
{sortedSessions.map((session, i) => (
<SessionCard
key={session.id}
session={session}
defaultExpanded={i === 0}
onDeleted={handleSessionDeleted}
onStatusChange={handleStatusChange}
/>
))}
</div>
</CollapsibleSection>
)}
</div>
);
}