Require a due date on all reminder creation (v0.1.0:103)
A date-less reminder has no urgency — it lands in the "Later"/"No date" bucket, out of the overdue/today/this-week rollups and the daily digest — so every create flow now pre-fills the due date to +1 week (editable) and blocks an empty save. Shared reminderDefaultDue() helper; edit paths also pre-fill the default for legacy date-less reminders. Surfaces: - Mobile: add-investor sheet (date auto-fills when you start the optional reminder), standalone Reminders "New reminder", Grid-detail "Set a reminder". - Desktop: Reminders page "+ New reminder", grid reminder modal. Server still accepts a null due_date by design (bot/automation callers); this is a human-UI requirement. Frontend-only; no schema/migration/dependency change.
This commit is contained in:
+21
-10
@@ -4953,7 +4953,7 @@
|
||||
const [onlyMine, setOnlyMine] = useState(false);
|
||||
const [users, setUsers] = useState([]);
|
||||
const [showCreate, setShowCreate] = useState(false);
|
||||
const [createForm, setCreateForm] = useState({ title: '', due_date: '', details: '', investor_name: '', assignee_id: '' });
|
||||
const [createForm, setCreateForm] = useState({ title: '', due_date: reminderDefaultDue(), details: '', investor_name: '', assignee_id: '' });
|
||||
const [creating, setCreating] = useState(false);
|
||||
const [editing, setEditing] = useState(null);
|
||||
const [editForm, setEditForm] = useState({ title: '', due_date: '', details: '', status: 'open', assignee_id: '' });
|
||||
@@ -5021,6 +5021,7 @@
|
||||
const submitCreate = async () => {
|
||||
const title = (createForm.title || '').trim();
|
||||
if (!title) { onShowToast('A reminder needs a title', 'error'); return; }
|
||||
if (!createForm.due_date) { onShowToast('A reminder needs a due date', 'error'); return; }
|
||||
setCreating(true);
|
||||
try {
|
||||
await api('/api/reminders', { method: 'POST', body: JSON.stringify({
|
||||
@@ -5029,7 +5030,7 @@
|
||||
}) }, token);
|
||||
onShowToast('Reminder created', 'success');
|
||||
setShowCreate(false);
|
||||
setCreateForm({ title: '', due_date: '', details: '', investor_name: '', assignee_id: '' });
|
||||
setCreateForm({ title: '', due_date: reminderDefaultDue(), details: '', investor_name: '', assignee_id: '' });
|
||||
load();
|
||||
} catch (err) { onShowToast(getErrorMessage(err, 'Create failed'), 'error'); }
|
||||
finally { setCreating(false); }
|
||||
@@ -5063,7 +5064,7 @@
|
||||
<div className="page-container">
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '16px', flexWrap: 'wrap', gap: '10px' }}>
|
||||
<h2 className="section-title" style={{ margin: 0 }}>Reminders</h2>
|
||||
<button type="button" onClick={() => setShowCreate(true)}>+ New reminder</button>
|
||||
<button type="button" onClick={() => { setCreateForm((f) => ({ ...f, due_date: f.due_date || reminderDefaultDue() })); setShowCreate(true); }}>+ New reminder</button>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', gap: '12px', alignItems: 'center', marginBottom: '16px', flexWrap: 'wrap' }}>
|
||||
@@ -5211,6 +5212,11 @@
|
||||
const mk = (days) => { const d = new Date(); d.setHours(0, 0, 0, 0); d.setDate(d.getDate() + days); return d.toISOString().slice(0, 10); };
|
||||
return [['Tomorrow', mk(1)], ['In 3 days', mk(3)], ['1 week', mk(7)], ['2 weeks', mk(14)]];
|
||||
};
|
||||
// Default reminder due date — one week out, local-midnight → YYYY-MM-DD (same convention as
|
||||
// the snooze presets). Every create flow pre-fills this and blocks an empty save: a reminder
|
||||
// needs a date or it has no urgency (it falls to the "Later"/"No date" bucket, out of the
|
||||
// overdue/today/this-week rollups + the daily digest).
|
||||
const reminderDefaultDue = () => { const d = new Date(); d.setHours(0, 0, 0, 0); d.setDate(d.getDate() + 7); return d.toISOString().slice(0, 10); };
|
||||
// Active-reminder bucket (Overdue/Today/This week/Later, dc :318) — split by due-date delta.
|
||||
// Terminal items (done/cancelled) are filtered out before bucketing and rendered separately.
|
||||
const reminderBucket = (r) => {
|
||||
@@ -5384,12 +5390,12 @@
|
||||
}, [investors, investorQuery]);
|
||||
|
||||
const openCreate = () => {
|
||||
setForm({ title: '', due_date: '', details: '', investor_name: '', investor_source_row_id: '', assignee_id: '', status: 'open' });
|
||||
setForm({ title: '', due_date: reminderDefaultDue(), details: '', investor_name: '', investor_source_row_id: '', assignee_id: '', status: 'open' });
|
||||
setEditing({ create: true });
|
||||
};
|
||||
const openEdit = (r) => {
|
||||
setForm({
|
||||
title: r.title || '', due_date: (r.due_date || '').slice(0, 10), details: r.details || '',
|
||||
title: r.title || '', due_date: (r.due_date || '').slice(0, 10) || reminderDefaultDue(), details: r.details || '',
|
||||
investor_name: r.investor_name || '', investor_source_row_id: '', assignee_id: r.assignee_id || '', status: r.status || 'open',
|
||||
});
|
||||
setEditing(r);
|
||||
@@ -5399,6 +5405,7 @@
|
||||
const submit = async () => {
|
||||
const title = (form.title || '').trim();
|
||||
if (!title) { onShowToast('A reminder needs a title', 'error'); return; }
|
||||
if (!form.due_date) { onShowToast('A reminder needs a due date', 'error'); return; }
|
||||
setBusy(true);
|
||||
try {
|
||||
if (editing && editing.create) {
|
||||
@@ -8374,7 +8381,7 @@
|
||||
const openReminderModal = async (row) => {
|
||||
if (!row) return;
|
||||
setReminderContext({ rowId: row.id, investorName: row.investor_name || '' });
|
||||
setReminderForm({ title: '', due_date: '', details: '' });
|
||||
setReminderForm({ title: '', due_date: reminderDefaultDue(), details: '' });
|
||||
setReminderList([]);
|
||||
setShowReminderModal(true);
|
||||
await loadReminders(row.id);
|
||||
@@ -8384,6 +8391,7 @@
|
||||
if (!reminderContext?.rowId) return;
|
||||
const title = (reminderForm.title || '').trim();
|
||||
if (!title) { onShowToast('A reminder needs a title', 'error'); return; }
|
||||
if (!reminderForm.due_date) { onShowToast('A reminder needs a due date', 'error'); return; }
|
||||
setReminderSubmitting(true);
|
||||
try {
|
||||
await api('/api/reminders', {
|
||||
@@ -8397,7 +8405,7 @@
|
||||
}),
|
||||
}, token);
|
||||
onShowToast('Reminder set', 'success');
|
||||
setReminderForm({ title: '', due_date: '', details: '' });
|
||||
setReminderForm({ title: '', due_date: reminderDefaultDue(), details: '' });
|
||||
await loadReminders(reminderContext.rowId);
|
||||
} catch (err) {
|
||||
onShowToast(getErrorMessage(err, 'Failed to set reminder'), 'error');
|
||||
@@ -10560,6 +10568,7 @@
|
||||
const submitReminder = async () => {
|
||||
const row = selectedRow; if (!row) return;
|
||||
if (!String(reminderForm.title || '').trim()) { onShowToast('A reminder needs a title', 'error'); return; }
|
||||
if (!reminderForm.due_date) { onShowToast('A reminder needs a due date', 'error'); return; }
|
||||
setBusy(true);
|
||||
try {
|
||||
if (reminder && reminder.id) {
|
||||
@@ -10595,6 +10604,8 @@
|
||||
const cName = createForm.contactName.trim();
|
||||
if (!name) { onShowToast('Investor name is required', 'error'); return; }
|
||||
if (!cName) { onShowToast('Add at least one contact name', 'error'); return; }
|
||||
// A reminder needs a date (it's pre-filled when you start one; block if it was cleared).
|
||||
if (createForm.reminderTitle.trim() && !createForm.reminderDue) { onShowToast('Set a due date for the reminder', 'error'); return; }
|
||||
setBusy(true);
|
||||
try {
|
||||
// The one-row create path: log-communication finds-or-creates the investor + first
|
||||
@@ -10802,7 +10813,7 @@
|
||||
</div>
|
||||
<div className="sheet-field">
|
||||
<label className="sheet-field-label">Reminder (optional)</label>
|
||||
<input className="sheet-input" value={createForm.reminderTitle} onChange={(e) => setCreateForm((f) => ({ ...f, reminderTitle: e.target.value }))} placeholder="e.g. Send Fund III deck" />
|
||||
<input className="sheet-input" value={createForm.reminderTitle} onChange={(e) => setCreateForm((f) => ({ ...f, reminderTitle: e.target.value, reminderDue: (e.target.value.trim() && !f.reminderDue) ? reminderDefaultDue() : f.reminderDue }))} placeholder="e.g. Send Fund III deck" />
|
||||
</div>
|
||||
{createForm.reminderTitle.trim() && (
|
||||
<div className="sheet-field">
|
||||
@@ -10889,8 +10900,8 @@
|
||||
<div className="fs-section-label">Reminder</div>
|
||||
<button className="detail-tap-card" disabled={reminder === undefined} onClick={() => {
|
||||
setReminderForm(reminder
|
||||
? { title: reminder.title || '', due_date: (reminder.due_date || '').slice(0, 10), details: reminder.details || '' }
|
||||
: { title: '', due_date: '', details: '' });
|
||||
? { title: reminder.title || '', due_date: (reminder.due_date || '').slice(0, 10) || reminderDefaultDue(), details: reminder.details || '' }
|
||||
: { title: '', due_date: reminderDefaultDue(), details: '' });
|
||||
setSheet('reminder');
|
||||
}}>
|
||||
{reminder === undefined ? (
|
||||
|
||||
@@ -67,8 +67,9 @@ export const PACKAGE_TITLE = 'Ten31 Database'
|
||||
// * 0.1.0:99 (Grant device-test round 2, CRM half: intake fuzzy match scores DISTINCTIVE tokens only [no more "Investment Group"/"Capital"/"Family Office" false look-alikes]; mobile grid "Last contact"/staleness sort is reversible; mobile Edit-investor prefills a contact's email [GET /api/fundraising/state heals a blank grid pill from the linked classic contact, fill-only]; mobile quick-log pencil icon renders [CSS sizing on the sole flex-child svg]. The Matrix intake thread-redaction change ships on the Spark, not here. No schema change; no migration)
|
||||
// * 0.1.0:100 (In-app business-card intake [#7]: a mobile camera button [left of the quick-log pencil] takes/picks a card photo, downscales it client-side via <canvas> to JPEG, and POSTs to the new POST /api/intake/card — vision-transcribe + parse + fuzzy-match on the box [local VL via Spark Control, nothing to Claude], reusing the Matrix card flow's nio-free parse/spark core. An editable review sheet [proposal fields + existing-investor picker] writes via log-communication tagged source="app_card"; a human approves every write. No schema change; no migration; no new dependency)
|
||||
// * 0.1.0:101 (Mobile UX batch 1 [Grant device feedback]: [1] inline ✕ clear button on the Grid/Contacts search + reminder/quick-log investor pickers [ClearableInput]; [2] Grid investor-detail contact pills are tappable — name deep-links to the Contacts detail [new Grid→Contacts one-shot action], email opens mailto; [4a] mobile Pipeline is a full-height flex column so the whole area above the now bottom-pinned dots is the swipe target, each stage page scrolling its cards; [4b] expected-amount entry — optional amount when adding to the pipeline from the Grid detail [feeds pipeline/link], editable amount on the Pipeline card detail [PUT /api/opportunities/{id}]; [5] bottom sheets lift above the on-screen keyboard [visualViewport] so the reminder investor-picker results stay visible. Grid contact-name search [#3] already worked. CSS+React only; no schema change; no migration; no new dependency)
|
||||
// * Current: 0.1.0:102 (Mobile email-approval bell [#6]: an admin-only bell in the mobile top bar [left of the camera] with an iPhone-style count badge surfaces the SAME pending email-capture proposals the web "Email Capture" panel + the Matrix review room decide. Tap → card list of proposals → tap one → review screen [investor name + subject + summary + editable proposed note] → Approve & log to grid / Reject. Reuses the existing GET /api/activity/proposals + POST .../{id}/approve|dismiss [require_admin]; bidirectional sync is automatic — an app decision flips the proposal status and the bot's poll redacts the Matrix thread, while a Matrix/web decision drops the proposal from the pending list the bell polls [45s], clearing the badge. No LLM round-trip [edit-then-approve like the web panel]; mobile-gated so the hidden desktop top bar doesn't poll. Frontend-only; no schema change; no migration; no new dependency)
|
||||
export const PACKAGE_VERSION = '0.1.0:102'
|
||||
// * 0.1.0:102 (Mobile email-approval bell [#6]: an admin-only bell in the mobile top bar [left of the camera] with an iPhone-style count badge surfaces the SAME pending email-capture proposals the web "Email Capture" panel + the Matrix review room decide. Tap → card list of proposals → tap one → review screen [investor name + subject + summary + editable proposed note] → Approve & log to grid / Reject. Reuses the existing GET /api/activity/proposals + POST .../{id}/approve|dismiss [require_admin]; bidirectional sync is automatic — an app decision flips the proposal status and the bot's poll redacts the Matrix thread, while a Matrix/web decision drops the proposal from the pending list the bell polls [45s], clearing the badge. No LLM round-trip [edit-then-approve like the web panel]; mobile-gated so the hidden desktop top bar doesn't poll. Frontend-only; no schema change; no migration; no new dependency)
|
||||
// * Current: 0.1.0:103 (Reminders require a due date [Grant feedback]: every reminder-create flow now pre-fills the due date to +1 week [editable] and blocks an empty save — a date-less reminder has no urgency [it falls to the "Later"/"No date" bucket, out of the overdue/today/this-week rollups + daily digest]. Applies to ALL create surfaces via a shared `reminderDefaultDue()` helper — mobile: the add-investor sheet [date auto-fills when you start the optional reminder], the standalone Reminders "New reminder" sheet, the Grid-detail "Set a reminder" card; desktop: the Reminders page "+ New reminder" + the grid reminder modal. Edit paths also pre-fill the default for legacy date-less reminders. Frontend-only; no schema/migration/dependency change)
|
||||
export const PACKAGE_VERSION = '0.1.0:103'
|
||||
|
||||
export const DATA_MOUNT_PATH = '/data'
|
||||
export const WEB_PORT = 8080
|
||||
|
||||
@@ -63,8 +63,9 @@ import { v_0_1_0_99 } from './v0.1.0.99'
|
||||
import { v_0_1_0_100 } from './v0.1.0.100'
|
||||
import { v_0_1_0_101 } from './v0.1.0.101'
|
||||
import { v_0_1_0_102 } from './v0.1.0.102'
|
||||
import { v_0_1_0_103 } from './v0.1.0.103'
|
||||
|
||||
export const versionGraph = VersionGraph.of({
|
||||
current: v_0_1_0_102,
|
||||
other: [v_0_1_0_39, v_0_1_0_40, v_0_1_0_41, v_0_1_0_42, v_0_1_0_43, v_0_1_0_44, v_0_1_0_45, v_0_1_0_46, v_0_1_0_47, v_0_1_0_48, v_0_1_0_49, v_0_1_0_50, v_0_1_0_51, v_0_1_0_52, v_0_1_0_53, v_0_1_0_54, v_0_1_0_55, v_0_1_0_56, v_0_1_0_57, v_0_1_0_58, v_0_1_0_59, v_0_1_0_60, v_0_1_0_61, v_0_1_0_62, v_0_1_0_63, v_0_1_0_64, v_0_1_0_65, v_0_1_0_66, v_0_1_0_67, v_0_1_0_68, v_0_1_0_69, v_0_1_0_70, v_0_1_0_71, v_0_1_0_72, v_0_1_0_73, v_0_1_0_74, v_0_1_0_75, v_0_1_0_76, v_0_1_0_77, v_0_1_0_78, v_0_1_0_79, v_0_1_0_80, v_0_1_0_81, v_0_1_0_82, v_0_1_0_83, v_0_1_0_84, v_0_1_0_85, v_0_1_0_86, v_0_1_0_87, v_0_1_0_88, v_0_1_0_89, v_0_1_0_90, v_0_1_0_91, v_0_1_0_92, v_0_1_0_93, v_0_1_0_94, v_0_1_0_95, v_0_1_0_96, v_0_1_0_97, v_0_1_0_98, v_0_1_0_99, v_0_1_0_100, v_0_1_0_101],
|
||||
current: v_0_1_0_103,
|
||||
other: [v_0_1_0_39, v_0_1_0_40, v_0_1_0_41, v_0_1_0_42, v_0_1_0_43, v_0_1_0_44, v_0_1_0_45, v_0_1_0_46, v_0_1_0_47, v_0_1_0_48, v_0_1_0_49, v_0_1_0_50, v_0_1_0_51, v_0_1_0_52, v_0_1_0_53, v_0_1_0_54, v_0_1_0_55, v_0_1_0_56, v_0_1_0_57, v_0_1_0_58, v_0_1_0_59, v_0_1_0_60, v_0_1_0_61, v_0_1_0_62, v_0_1_0_63, v_0_1_0_64, v_0_1_0_65, v_0_1_0_66, v_0_1_0_67, v_0_1_0_68, v_0_1_0_69, v_0_1_0_70, v_0_1_0_71, v_0_1_0_72, v_0_1_0_73, v_0_1_0_74, v_0_1_0_75, v_0_1_0_76, v_0_1_0_77, v_0_1_0_78, v_0_1_0_79, v_0_1_0_80, v_0_1_0_81, v_0_1_0_82, v_0_1_0_83, v_0_1_0_84, v_0_1_0_85, v_0_1_0_86, v_0_1_0_87, v_0_1_0_88, v_0_1_0_89, v_0_1_0_90, v_0_1_0_91, v_0_1_0_92, v_0_1_0_93, v_0_1_0_94, v_0_1_0_95, v_0_1_0_96, v_0_1_0_97, v_0_1_0_98, v_0_1_0_99, v_0_1_0_100, v_0_1_0_101, v_0_1_0_102],
|
||||
})
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import { VersionInfo } from '@start9labs/start-sdk'
|
||||
|
||||
// Reminders require a due date (Grant feedback). A date-less reminder has no urgency — it falls
|
||||
// to the "Later"/"No date" bucket, out of the overdue/today/this-week rollups and the daily
|
||||
// digest — so every create flow now pre-fills the due date to +1 week (editable) and blocks an
|
||||
// empty save. Applies to ALL create surfaces via a shared reminderDefaultDue() helper — mobile:
|
||||
// the add-investor sheet (the date auto-fills the moment you start the optional reminder), the
|
||||
// standalone Reminders "New reminder" sheet, the Grid-detail "Set a reminder" card; desktop: the
|
||||
// Reminders page "+ New reminder" and the grid reminder modal. Edit paths also pre-fill the
|
||||
// default for legacy date-less reminders. Frontend-only; no schema, migration, or dependency change.
|
||||
export const v_0_1_0_103 = VersionInfo.of({
|
||||
version: '0.1.0:103',
|
||||
releaseNotes: {
|
||||
en_US: [
|
||||
'Reminders now always have a due date: every create flow on mobile pre-fills it to a week',
|
||||
'out (editable) and won’t save without one, so a reminder can’t slip into a no-date state',
|
||||
'that hides it from the Overdue / Today / This week views and the daily digest.',
|
||||
].join(' '),
|
||||
},
|
||||
migrations: { up: async () => {}, down: async () => {} },
|
||||
})
|
||||
Reference in New Issue
Block a user