5 - Add buffer multiplier UI with preset and custom options

This commit is contained in:
myrmidex 2026-03-20 00:41:47 +01:00
parent ed6be6249e
commit 8f6de4aace

View file

@ -18,7 +18,8 @@ interface Bucket {
allocation_type: string;
allocation_value: number | null;
allocation_type_label: string;
formatted_allocation_value: string;
buffer_multiplier: number;
effective_capacity: number;
current_balance: number;
has_available_space: boolean;
available_space: number;
@ -64,11 +65,22 @@ const bucketTypeBorderColor = {
overflow: 'border-amber-500',
} as const;
const BUFFER_PRESETS = ['0', '0.5', '1', '1.5', '2'] as const;
const formatAllocationValue = (bucket: Bucket): string => {
if (bucket.allocation_type === 'unlimited') return 'All remaining';
if (bucket.allocation_value === null) return '--';
if (bucket.allocation_type === 'percentage') return `${Number(bucket.allocation_value).toFixed(2)}%`;
return `$${Number(bucket.allocation_value).toFixed(2)}`;
};
const defaultFormData = {
name: '',
type: 'need' as Bucket['type'],
allocation_type: 'fixed_limit',
allocation_value: ''
allocation_value: '',
buffer_multiplier: '0',
buffer_mode: 'preset' as 'preset' | 'custom',
};
export default function Show({ scenario, buckets, streams = { data: [] }, streamStats }: Props) {
@ -79,11 +91,15 @@ export default function Show({ scenario, buckets, streams = { data: [] }, stream
const handleEdit = (bucket: Bucket) => {
setEditingBucket(bucket);
const bufferStr = bucket.buffer_multiplier.toString();
const isPreset = (BUFFER_PRESETS as readonly string[]).includes(bufferStr);
setFormData({
name: bucket.name,
type: bucket.type,
allocation_type: bucket.allocation_type,
allocation_value: bucket.allocation_value ? bucket.allocation_value.toString() : ''
allocation_value: bucket.allocation_value ? bucket.allocation_value.toString() : '',
buffer_multiplier: bufferStr,
buffer_mode: isPreset ? 'preset' : 'custom',
});
setIsModalOpen(true);
};
@ -133,6 +149,7 @@ export default function Show({ scenario, buckets, streams = { data: [] }, stream
type: formData.type,
allocation_type: formData.allocation_type,
allocation_value: formData.allocation_value ? parseFloat(formData.allocation_value) : null,
buffer_multiplier: formData.allocation_type === 'fixed_limit' ? parseFloat(formData.buffer_multiplier) || 0 : 0,
priority: editingBucket ? editingBucket.priority : undefined
}),
});
@ -174,6 +191,7 @@ export default function Show({ scenario, buckets, streams = { data: [] }, stream
type: bucket.type,
allocation_type: bucket.allocation_type,
allocation_value: bucket.allocation_value,
buffer_multiplier: bucket.buffer_multiplier,
priority: newPriority
}),
});
@ -283,8 +301,13 @@ export default function Show({ scenario, buckets, streams = { data: [] }, stream
<div className="mt-2">
<span className="text-sm text-gray-600">
Allocation: {bucket.formatted_allocation_value}
Allocation: {formatAllocationValue(bucket)}
</span>
{bucket.allocation_type === 'fixed_limit' && bucket.buffer_multiplier > 0 && (
<span className="text-sm text-gray-500 ml-2">
({bucket.buffer_multiplier}x buffer = ${bucket.effective_capacity.toFixed(2)} effective)
</span>
)}
</div>
{bucket.allocation_type === 'fixed_limit' && (
@ -292,14 +315,14 @@ export default function Show({ scenario, buckets, streams = { data: [] }, stream
<div className="flex justify-between text-sm">
<span>Progress</span>
<span>
${bucket.current_balance.toFixed(2)} / ${Number(bucket.allocation_value)?.toFixed(2)}
${bucket.current_balance.toFixed(2)} / ${bucket.effective_capacity.toFixed(2)}
</span>
</div>
<div className="mt-1 h-2 bg-gray-200 rounded-full">
<div
className="h-2 bg-blue-600 rounded-full transition-all"
style={{
width: `${bucket.allocation_value ? Math.min((bucket.current_balance / Number(bucket.allocation_value)) * 100, 100) : 0}%`
width: `${bucket.effective_capacity ? Math.min((bucket.current_balance / bucket.effective_capacity) * 100, 100) : 0}%`
}}
/>
</div>
@ -564,6 +587,45 @@ export default function Show({ scenario, buckets, streams = { data: [] }, stream
</div>
)}
{formData.allocation_type === 'fixed_limit' && (
<div>
<label htmlFor="buffer_multiplier" className="block text-sm font-medium text-gray-700">
Buffer
</label>
<select
id="buffer_preset"
value={formData.buffer_mode === 'custom' ? 'custom' : formData.buffer_multiplier}
onChange={(e) => {
if (e.target.value === 'custom') {
setFormData({ ...formData, buffer_mode: 'custom' });
} else {
setFormData({ ...formData, buffer_multiplier: e.target.value, buffer_mode: 'preset' });
}
}}
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 text-gray-900"
>
<option value="0">None</option>
<option value="0.5">0.5x (50% extra)</option>
<option value="1">1x (100% extra)</option>
<option value="1.5">1.5x (150% extra)</option>
<option value="2">2x (200% extra)</option>
<option value="custom">Custom</option>
</select>
{formData.buffer_mode === 'custom' && (
<input
type="number"
id="buffer_multiplier"
value={formData.buffer_multiplier}
onChange={(e) => setFormData({ ...formData, buffer_multiplier: e.target.value })}
className="mt-2 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 text-gray-900"
placeholder="e.g., 0.75"
step="0.01"
min="0"
/>
)}
</div>
)}
<div className="flex gap-3 pt-4">
<button
type="button"