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:
Krao Hasanee
2026-05-13 14:20:38 -04:00
parent c9e7816e28
commit eee0885811
117 changed files with 17592 additions and 4057 deletions
+193
View File
@@ -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>
);
}