Fix file sharing load speed and move error; misc updates
- Remove recursive directory size calculations (single Seafile API call per list) - Remove 'Used in this location' usage display - Fix move using v2 per-type endpoints instead of broken batch endpoint - Send entry type from frontend for correct move routing Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,193 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import Layout from '../../components/Layout';
|
||||
import LoadingButton from '../../components/LoadingButton';
|
||||
import { supabase } from '../../lib/supabase';
|
||||
import { useAuth } from '../../context/AuthContext';
|
||||
|
||||
function emptyForm() {
|
||||
return {
|
||||
title: '',
|
||||
attendees: '',
|
||||
meeting_at: new Date().toISOString().slice(0, 16),
|
||||
notes: '',
|
||||
};
|
||||
}
|
||||
|
||||
function formatMeetingDate(value) {
|
||||
if (!value) return 'Unknown date';
|
||||
return new Date(value).toLocaleString('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
hour: 'numeric',
|
||||
minute: '2-digit',
|
||||
});
|
||||
}
|
||||
|
||||
export default function MeetingNotes() {
|
||||
const { currentUser } = useAuth();
|
||||
const [notes, setNotes] = useState([]);
|
||||
const [form, setForm] = useState(emptyForm());
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [deletingId, setDeletingId] = useState('');
|
||||
const [status, setStatus] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
let isMounted = true;
|
||||
|
||||
async function loadInitialNotes() {
|
||||
const { data, error } = await supabase
|
||||
.from('meeting_notes')
|
||||
.select('*')
|
||||
.order('meeting_at', { ascending: false })
|
||||
.order('created_at', { ascending: false });
|
||||
|
||||
if (!isMounted) return;
|
||||
|
||||
if (error) {
|
||||
setStatus(`Failed to load meeting notes: ${error.message}`);
|
||||
setNotes([]);
|
||||
} else {
|
||||
setNotes(data || []);
|
||||
setStatus('');
|
||||
}
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
loadInitialNotes();
|
||||
|
||||
return () => {
|
||||
isMounted = false;
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
if (!form.title.trim() || !form.notes.trim()) {
|
||||
setStatus('Meeting title and notes are required.');
|
||||
return;
|
||||
}
|
||||
|
||||
setSaving(true);
|
||||
const payload = {
|
||||
title: form.title.trim(),
|
||||
attendees: form.attendees.trim(),
|
||||
meeting_at: form.meeting_at ? new Date(form.meeting_at).toISOString() : new Date().toISOString(),
|
||||
notes: form.notes.trim(),
|
||||
created_by: currentUser?.id || null,
|
||||
updated_at: new Date().toISOString(),
|
||||
};
|
||||
|
||||
const { data, error } = await supabase
|
||||
.from('meeting_notes')
|
||||
.insert(payload)
|
||||
.select('*')
|
||||
.single();
|
||||
|
||||
setSaving(false);
|
||||
if (error) {
|
||||
setStatus(`Failed to save note: ${error.message}`);
|
||||
return;
|
||||
}
|
||||
|
||||
setNotes(prev => [data, ...prev]);
|
||||
setForm(emptyForm());
|
||||
setStatus('Meeting note added.');
|
||||
};
|
||||
|
||||
const handleDelete = async (entry) => {
|
||||
if (!window.confirm(`Delete meeting note "${entry.title}"?`)) return;
|
||||
setDeletingId(entry.id);
|
||||
const { error } = await supabase.from('meeting_notes').delete().eq('id', entry.id);
|
||||
setDeletingId('');
|
||||
if (error) {
|
||||
setStatus(`Failed to delete note: ${error.message}`);
|
||||
return;
|
||||
}
|
||||
setNotes(prev => prev.filter(note => note.id !== entry.id));
|
||||
setStatus('Meeting note deleted.');
|
||||
};
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<div className="page-header">
|
||||
<div>
|
||||
<div className="page-title">Meeting Notes</div>
|
||||
<div className="page-subtitle">Internal team timeline for meeting recaps, decisions, and follow-ups.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'grid', gap: 18 }}>
|
||||
<section className="card">
|
||||
<div className="card-title">Add Note</div>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="grid-2">
|
||||
<div className="form-group">
|
||||
<label>Meeting Title</label>
|
||||
<input type="text" value={form.title} onChange={(e) => setForm(prev => ({ ...prev, title: e.target.value }))} placeholder="Weekly team sync" />
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>Meeting Date</label>
|
||||
<input type="datetime-local" value={form.meeting_at} onChange={(e) => setForm(prev => ({ ...prev, meeting_at: e.target.value }))} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>Attendees</label>
|
||||
<input type="text" value={form.attendees} onChange={(e) => setForm(prev => ({ ...prev, attendees: e.target.value }))} placeholder="Team, client, subcontractor" />
|
||||
</div>
|
||||
<div className="form-group" style={{ marginBottom: 12 }}>
|
||||
<label>Notes</label>
|
||||
<textarea value={form.notes} onChange={(e) => setForm(prev => ({ ...prev, notes: e.target.value }))} placeholder="Key decisions, next steps, blockers, and follow-up items..." style={{ minHeight: 180 }} />
|
||||
</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 12, flexWrap: 'wrap' }}>
|
||||
<div style={{ color: 'var(--text-muted)', fontSize: 12 }}>{status || 'Newest notes appear first in the timeline.'}</div>
|
||||
<LoadingButton className="btn btn-primary" loading={saving} loadingText="Saving...">Save Note</LoadingButton>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<section className="card">
|
||||
<div className="card-title">Timeline</div>
|
||||
{loading ? (
|
||||
<div style={{ color: 'var(--text-muted)' }}>Loading meeting notes...</div>
|
||||
) : notes.length === 0 ? (
|
||||
<div className="empty-state" style={{ padding: '36px 12px' }}>
|
||||
<h3>No meeting notes yet</h3>
|
||||
<p>Add the first entry to start the internal timeline.</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="meeting-timeline">
|
||||
{notes.map((entry) => (
|
||||
<article key={entry.id} className="meeting-note-card">
|
||||
<div className="meeting-note-marker" aria-hidden="true" />
|
||||
<div className="meeting-note-content">
|
||||
<div className="meeting-note-header">
|
||||
<div>
|
||||
<div className="meeting-note-title">{entry.title}</div>
|
||||
<div className="meeting-note-meta">
|
||||
<span>{formatMeetingDate(entry.meeting_at)}</span>
|
||||
{entry.attendees ? <span>Attendees: {entry.attendees}</span> : null}
|
||||
</div>
|
||||
</div>
|
||||
<LoadingButton
|
||||
className="btn btn-danger btn-sm"
|
||||
loading={deletingId === entry.id}
|
||||
disabled={Boolean(deletingId)}
|
||||
loadingText="Deleting..."
|
||||
onClick={() => handleDelete(entry)}
|
||||
>
|
||||
Delete
|
||||
</LoadingButton>
|
||||
</div>
|
||||
<div className="meeting-note-body">{entry.notes}</div>
|
||||
</div>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user