Say which change-log entries are yours
The version this was built against carries 127 adjustments from five accounts, one of them the reader's, so finding your own meant reading the By column down the whole list. Own entries are now marked in place -- a blue rule down the left of the row, the By cell in blue -- and the modal header carries an Everyone / Mine control with a count on each. Marked as well as filterable on purpose: the mark tells you without hiding anyone, the filter is for when you want only yours. The filter resets to Everyone on every open and is not persisted. A filter that hides other people's work is fine as a thing you asked for and a trap as a thing you inherited from last week. `isMine()` is strict authorship, deliberately not `canUndo()` -- an admin can undo everything, so marking every row as theirs would say nothing. The fetch stays unfiltered, since the counts on the control need the whole set. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
7fd07e52d2
commit
8bc7d65fce
16
CLAUDE.md
16
CLAUDE.md
@ -755,6 +755,22 @@ Enforced at:
|
|||||||
Moving a row between territories is reassignment, not forecasting, and it
|
Moving a row between territories is reassignment, not forecasting, and it
|
||||||
would vanish from the view that would have shown what happened.
|
would vanish from the view that would have shown what happened.
|
||||||
|
|
||||||
|
**Whose entries the change log lists.** Everyone's, as before, with an
|
||||||
|
**Everyone / Mine** control in the modal header and each account's own entries
|
||||||
|
marked in place — a blue rule down the left of the row and its `By` cell in
|
||||||
|
blue. On the version this was built against that is 127 adjustments from five
|
||||||
|
accounts, one of them the reader's, so finding your own meant reading the `By`
|
||||||
|
column down the whole list.
|
||||||
|
|
||||||
|
Marked *and* filterable on purpose: the mark is how you tell without hiding
|
||||||
|
anybody, the filter is for when you want only yours. It resets to Everyone on
|
||||||
|
every open and is not persisted — a filter that hides other people's work is
|
||||||
|
fine as a thing you asked for and a trap as a thing you inherited from last
|
||||||
|
week. `isMine()` is strict authorship, deliberately not `canUndo()`: an admin
|
||||||
|
can undo everything, and marking every row as theirs would say nothing.
|
||||||
|
|
||||||
|
The fetch stays unfiltered, since the counts on the control need the whole set.
|
||||||
|
|
||||||
**The change log shows an entry's full impact**, not the reader's share. The
|
**The change log shows an entry's full impact**, not the reader's share. The
|
||||||
totals stamped on `pf.log` are company-wide, so an admin's version-wide scale
|
totals stamped on `pf.log` are company-wide, so an admin's version-wide scale
|
||||||
reads the same in every account — deliberate, and labelled, rather than
|
reads the same in every account — deliberate, and labelled, rather than
|
||||||
|
|||||||
@ -76,6 +76,9 @@ export default function Forecast({ sources = [], sourceId, versions = [], versio
|
|||||||
// account that made it, or an admin. Mirrored here to say so before the click
|
// account that made it, or an admin. Mirrored here to say so before the click
|
||||||
// rather than after the 403.
|
// rather than after the 403.
|
||||||
const canUndo = (entry) => !!user && (user.is_admin || entry.pf_user === user.username)
|
const canUndo = (entry) => !!user && (user.is_admin || entry.pf_user === user.username)
|
||||||
|
// Strictly authorship, which is not the same test as canUndo: an admin can
|
||||||
|
// undo everything, and marking every row as theirs would say nothing.
|
||||||
|
const isMine = (entry) => !!user && entry.pf_user === user.username
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
const [largeDataset, setLargeDataset] = useState(false)
|
const [largeDataset, setLargeDataset] = useState(false)
|
||||||
// The pivot's own filter, refreshed whenever the ledger recomputes. A ref
|
// The pivot's own filter, refreshed whenever the ledger recomputes. A ref
|
||||||
@ -252,6 +255,10 @@ export default function Forecast({ sources = [], sourceId, versions = [], versio
|
|||||||
const [showBridge, setShowBridge] = useState(false)
|
const [showBridge, setShowBridge] = useState(false)
|
||||||
const [logEntries, setLogEntries] = useState([])
|
const [logEntries, setLogEntries] = useState([])
|
||||||
const [logLoading, setLogLoading] = useState(false)
|
const [logLoading, setLogLoading] = useState(false)
|
||||||
|
// Whose entries the log lists. Not persisted, and it resets to everyone on
|
||||||
|
// every open: a filter that hides other people's work is fine as a thing you
|
||||||
|
// asked for and a trap as a thing you inherited from last week.
|
||||||
|
const [logWho, setLogWho] = useState('all') // 'all' | 'mine'
|
||||||
const [editingCell, setEditingCell] = useState(null) // { id, field: 'note'|'tag', text }
|
const [editingCell, setEditingCell] = useState(null) // { id, field: 'note'|'tag', text }
|
||||||
const [undoingId, setUndoingId] = useState(null)
|
const [undoingId, setUndoingId] = useState(null)
|
||||||
|
|
||||||
@ -1725,6 +1732,10 @@ export default function Forecast({ sources = [], sourceId, versions = [], versio
|
|||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// What the history modal lists. The fetch is unfiltered -- the counts on the
|
||||||
|
// Everyone/Mine control need the whole set -- so the narrowing happens here.
|
||||||
|
const visibleLog = logWho === 'mine' ? logEntries.filter(isMine) : logEntries
|
||||||
|
|
||||||
// The scope is read from the ref rather than passed in: the request preview in
|
// The scope is read from the ref rather than passed in: the request preview in
|
||||||
// the panel calls this too, and when it was a parameter the preview defaulted
|
// the panel calls this too, and when it was a parameter the preview defaulted
|
||||||
// it away -- showing a payload with no scope for a write that had one.
|
// it away -- showing a payload with no scope for a write that had one.
|
||||||
@ -1817,6 +1828,7 @@ export default function Forecast({ sources = [], sourceId, versions = [], versio
|
|||||||
async function openLog() {
|
async function openLog() {
|
||||||
setShowLog(true)
|
setShowLog(true)
|
||||||
setExpandedLog(null)
|
setExpandedLog(null)
|
||||||
|
setLogWho('all')
|
||||||
setLogLoading(true)
|
setLogLoading(true)
|
||||||
try {
|
try {
|
||||||
// Baseline and reference loads are segment construction, not forecasting.
|
// Baseline and reference loads are segment construction, not forecasting.
|
||||||
@ -2043,15 +2055,38 @@ export default function Forecast({ sources = [], sourceId, versions = [], versio
|
|||||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40" onClick={() => setShowLog(false)}>
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40" onClick={() => setShowLog(false)}>
|
||||||
<div className="bg-white rounded-lg shadow-xl w-full max-w-6xl mx-4 flex flex-col max-h-[85vh]" onClick={e => e.stopPropagation()}>
|
<div className="bg-white rounded-lg shadow-xl w-full max-w-6xl mx-4 flex flex-col max-h-[85vh]" onClick={e => e.stopPropagation()}>
|
||||||
<div className="flex items-center justify-between px-5 py-3 border-b border-gray-200 shrink-0">
|
<div className="flex items-center justify-between px-5 py-3 border-b border-gray-200 shrink-0">
|
||||||
<span className="font-medium text-gray-700 text-sm">Change History</span>
|
<div className="flex items-baseline gap-4 min-w-0">
|
||||||
|
<span className="font-medium text-gray-700 text-sm">Change History</span>
|
||||||
|
{/* Whose work this is. A version under active use carries
|
||||||
|
everyone's adjustments, and finding your own in it meant
|
||||||
|
reading the By column down a list that is mostly other
|
||||||
|
people. Own entries are marked in place as well, so the
|
||||||
|
filter is a way to narrow rather than the only way to tell. */}
|
||||||
|
{!!user && logEntries.length > 0 && (
|
||||||
|
<div className="inline-flex rounded border border-gray-200 overflow-hidden text-xs">
|
||||||
|
{[['all', `Everyone`, logEntries.length],
|
||||||
|
['mine', `Mine`, logEntries.filter(isMine).length]].map(([val, label, n]) => (
|
||||||
|
<button key={val} onClick={() => setLogWho(val)}
|
||||||
|
className={`px-2.5 py-0.5 whitespace-nowrap transition-colors ${
|
||||||
|
logWho === val ? 'bg-blue-600 text-white' : 'bg-white text-gray-600 hover:bg-gray-50'}`}>
|
||||||
|
{label} <span className={logWho === val ? 'text-blue-100' : 'text-gray-400'}>{n}</span>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
<button onClick={() => setShowLog(false)} className="text-gray-400 hover:text-gray-600 text-lg leading-none">×</button>
|
<button onClick={() => setShowLog(false)} className="text-gray-400 hover:text-gray-600 text-lg leading-none">×</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="overflow-y-auto flex-1">
|
<div className="overflow-y-auto flex-1">
|
||||||
{logLoading ? (
|
{logLoading ? (
|
||||||
<div className="p-8 text-center text-sm text-gray-400">Loading…</div>
|
<div className="p-8 text-center text-sm text-gray-400">Loading…</div>
|
||||||
) : logEntries.length === 0 ? (
|
) : visibleLog.length === 0 ? (
|
||||||
<div className="p-8 text-center text-sm text-gray-400">No log entries yet.</div>
|
<div className="p-8 text-center text-sm text-gray-400">
|
||||||
|
{logEntries.length === 0
|
||||||
|
? 'No log entries yet.'
|
||||||
|
: `None of the ${logEntries.length} entries here are yours.`}
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<table className="w-full text-xs border-collapse table-fixed">
|
<table className="w-full text-xs border-collapse table-fixed">
|
||||||
<thead className="sticky top-0 bg-gray-50 text-gray-400 uppercase tracking-wide" style={{fontSize:'10px'}}>
|
<thead className="sticky top-0 bg-gray-50 text-gray-400 uppercase tracking-wide" style={{fontSize:'10px'}}>
|
||||||
@ -2068,9 +2103,12 @@ export default function Forecast({ sources = [], sourceId, versions = [], versio
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{logEntries.map(entry => (
|
{visibleLog.map(entry => (
|
||||||
<tr key={entry.id} className="border-t border-gray-100 hover:bg-gray-50">
|
<tr key={entry.id} className="border-t border-gray-100 hover:bg-gray-50">
|
||||||
<td className="px-3 py-2 text-gray-400 whitespace-nowrap">{fmtStamp(entry.stamp)}</td>
|
<td className={`px-3 py-2 text-gray-400 whitespace-nowrap ${
|
||||||
|
isMine(entry) ? 'border-l-2 border-blue-500' : 'border-l-2 border-transparent'}`}>
|
||||||
|
{fmtStamp(entry.stamp)}
|
||||||
|
</td>
|
||||||
<td className="px-3 py-2">
|
<td className="px-3 py-2">
|
||||||
<span className={`px-1.5 py-0.5 rounded text-xs font-medium ${opBadge(entry.operation)}`}>
|
<span className={`px-1.5 py-0.5 rounded text-xs font-medium ${opBadge(entry.operation)}`}>
|
||||||
{entry.operation}
|
{entry.operation}
|
||||||
@ -2095,7 +2133,8 @@ export default function Forecast({ sources = [], sourceId, versions = [], versio
|
|||||||
)} />
|
)} />
|
||||||
<LogCell entry={entry} field="note" canEdit={canUndo(entry)} placeholder="add note"
|
<LogCell entry={entry} field="note" canEdit={canUndo(entry)} placeholder="add note"
|
||||||
editing={editingCell} setEditing={setEditingCell} onSave={saveLogField} />
|
editing={editingCell} setEditing={setEditingCell} onSave={saveLogField} />
|
||||||
<td className="px-3 py-2 text-gray-500 truncate" title={entry.pf_user || ''}>
|
<td className={`px-3 py-2 truncate ${isMine(entry) ? 'text-blue-700 font-medium' : 'text-gray-500'}`}
|
||||||
|
title={entry.pf_user || ''}>
|
||||||
{entry.pf_user || '—'}
|
{entry.pf_user || '—'}
|
||||||
</td>
|
</td>
|
||||||
<td className={`px-3 py-2 text-right tabular-nums whitespace-nowrap ${
|
<td className={`px-3 py-2 text-right tabular-nums whitespace-nowrap ${
|
||||||
@ -2121,7 +2160,7 @@ export default function Forecast({ sources = [], sourceId, versions = [], versio
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
)).flatMap((row, i) => {
|
)).flatMap((row, i) => {
|
||||||
const entry = logEntries[i]
|
const entry = visibleLog[i]
|
||||||
if (expandedLog !== entry.id) return [row]
|
if (expandedLog !== entry.id) return [row]
|
||||||
return [row, (
|
return [row, (
|
||||||
<tr key={`${entry.id}-detail`} className="bg-gray-50 border-t border-gray-100">
|
<tr key={`${entry.id}-detail`} className="bg-gray-50 border-t border-gray-100">
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user