management and delivery improvements
CI / build (push) Has been skipped
CI / deploy (push) Successful in 44s

This commit is contained in:
2026-03-18 11:42:21 -05:00
parent 716a06ee28
commit fb7b138371
6 changed files with 118 additions and 11 deletions
+2 -1
View File
@@ -4,6 +4,7 @@ import { Portal } from 'solid-js/web';
import { X } from 'lucide-solid'; import { X } from 'lucide-solid';
import { pb, COLLECTIONS } from '../utils/db'; import { pb, COLLECTIONS } from '../utils/db';
import { appStore } from '../store/appStore'; import { appStore } from '../store/appStore';
import { normalizeScopes } from '../utils/scope-utils';
interface CloudLoadModalProps { interface CloudLoadModalProps {
show: boolean; show: boolean;
@@ -39,7 +40,7 @@ const CloudLoadModal: Component<CloudLoadModalProps> = (props) => {
const data = record.items; // items holds the full JSON blob const data = record.items; // items holds the full JSON blob
if (!data) return; if (!data) return;
if (data.scopes) appStore.setScopes(data.scopes); if (data.scopes) appStore.setScopes(normalizeScopes(data.scopes));
if (data.subScopes) appStore.setSubScopes(data.subScopes); if (data.subScopes) appStore.setSubScopes(data.subScopes);
if (data.items) appStore.setEstimateItems(data.items); if (data.items) appStore.setEstimateItems(data.items);
if (data.clientInfo) appStore.setClientInfo(data.clientInfo); if (data.clientInfo) appStore.setClientInfo(data.clientInfo);
+4
View File
@@ -96,8 +96,12 @@ const EstimateBuilder: Component<EstimateBuilderProps> = (props) => {
name: 'New Scope', name: 'New Scope',
managementFee: 0, managementFee: 0,
managementFeeType: 'percent', managementFeeType: 'percent',
managementFeeQuantity: 1,
managementFeeRate: 0,
deliveryFee: 0, deliveryFee: 0,
deliveryFeeType: 'percent', deliveryFeeType: 'percent',
deliveryFeeQuantity: 1,
deliveryFeeRate: 0,
useManagementLogic: 'percent', useManagementLogic: 'percent',
useDeliveryLogic: 'percent', useDeliveryLogic: 'percent',
estimatorNotes: '', estimatorNotes: '',
+92 -8
View File
@@ -312,21 +312,63 @@ const ScopeCard: Component<ScopeCardProps> = (props) => {
<Percent class="w-3 h-3" /> <Percent class="w-3 h-3" />
</button> </button>
<button <button
onClick={() => props.onUpdateScope(props.scope.id, { useManagementLogic: 'simple' })} onClick={() => props.onUpdateScope(props.scope.id, {
useManagementLogic: 'simple',
managementFee: (props.scope.managementFeeQuantity || 0) * (props.scope.managementFeeRate || 0)
})}
class={`p-1 rounded-md transition-colors ${props.scope.useManagementLogic === 'simple' ? 'bg-primary/10 text-primary' : 'hover:bg-muted'}`} class={`p-1 rounded-md transition-colors ${props.scope.useManagementLogic === 'simple' ? 'bg-primary/10 text-primary' : 'hover:bg-muted'}`}
> >
<DollarSign class="w-3 h-3" /> <DollarSign class="w-3 h-3" />
</button> </button>
</div> </div>
</div> </div>
<div class="flex items-center gap-4"> <div class="flex items-center gap-4">
{props.scope.useManagementLogic === 'percent' ? (
<NumericInput <NumericInput
value={props.scope.managementFee} value={props.scope.managementFee}
onUpdate={(val) => props.onUpdateScope(props.scope.id, { managementFee: val })} onUpdate={(val) => props.onUpdateScope(props.scope.id, { managementFee: val })}
class="w-20 text-right text-sm border-b border-dashed border-border focus:border-primary outline-none bg-transparent" class="w-20 text-right text-sm border-b border-dashed border-border focus:border-primary outline-none bg-transparent"
/> />
<span class="w-28 text-right text-foreground font-medium">${formatNumber(totals().mgmtTotal)}</span> ) : (
</div> <div class="flex items-center gap-3">
<div class="flex flex-col items-end gap-0.5 text-[9px] font-black tracking-[0.3em] text-muted-foreground uppercase">
<span>Qty</span>
<NumericInput
value={props.scope.managementFeeQuantity}
onUpdate={(val) => {
const qty = val;
const rate = props.scope.managementFeeRate || 0;
props.onUpdateScope(props.scope.id, {
managementFeeQuantity: qty,
managementFee: qty * rate
});
}}
class="w-16 text-right text-sm border-b border-dashed border-border focus:border-primary outline-none bg-transparent"
/>
</div>
<span class="text-xs font-black text-muted-foreground/70">×</span>
<div class="flex flex-col items-end gap-0.5 text-[9px] font-black tracking-[0.3em] text-muted-foreground uppercase">
<span>Rate</span>
<div class="flex items-center gap-1">
<span class="text-[10px] font-black text-muted-foreground">$</span>
<NumericInput
value={props.scope.managementFeeRate}
onUpdate={(val) => {
const rate = val;
const qty = props.scope.managementFeeQuantity || 0;
props.onUpdateScope(props.scope.id, {
managementFeeRate: rate,
managementFee: qty * rate
});
}}
class="w-20 text-right text-sm border-b border-dashed border-border focus:border-primary outline-none bg-transparent"
/>
</div>
</div>
</div>
)}
<span class="w-28 text-right text-foreground font-medium">${formatNumber(totals().mgmtTotal)}</span>
</div>
</div> </div>
{/* Delivery Fee */} {/* Delivery Fee */}
@@ -341,21 +383,63 @@ const ScopeCard: Component<ScopeCardProps> = (props) => {
<Percent class="w-3 h-3" /> <Percent class="w-3 h-3" />
</button> </button>
<button <button
onClick={() => props.onUpdateScope(props.scope.id, { useDeliveryLogic: 'simple' })} onClick={() => props.onUpdateScope(props.scope.id, {
useDeliveryLogic: 'simple',
deliveryFee: (props.scope.deliveryFeeQuantity || 0) * (props.scope.deliveryFeeRate || 0)
})}
class={`p-1 rounded-md transition-colors ${props.scope.useDeliveryLogic === 'simple' ? 'bg-primary/10 text-primary' : 'hover:bg-muted'}`} class={`p-1 rounded-md transition-colors ${props.scope.useDeliveryLogic === 'simple' ? 'bg-primary/10 text-primary' : 'hover:bg-muted'}`}
> >
<DollarSign class="w-3 h-3" /> <DollarSign class="w-3 h-3" />
</button> </button>
</div> </div>
</div> </div>
<div class="flex items-center gap-4"> <div class="flex items-center gap-4">
{props.scope.useDeliveryLogic === 'percent' ? (
<NumericInput <NumericInput
value={props.scope.deliveryFee} value={props.scope.deliveryFee}
onUpdate={(val) => props.onUpdateScope(props.scope.id, { deliveryFee: val })} onUpdate={(val) => props.onUpdateScope(props.scope.id, { deliveryFee: val })}
class="w-20 text-right text-sm border-b border-dashed border-border focus:border-primary outline-none bg-transparent" class="w-20 text-right text-sm border-b border-dashed border-border focus:border-primary outline-none bg-transparent"
/> />
<span class="w-28 text-right text-foreground font-medium">${formatNumber(totals().deliveryTotal)}</span> ) : (
</div> <div class="flex items-center gap-3">
<div class="flex flex-col items-end gap-0.5 text-[9px] font-black tracking-[0.3em] text-muted-foreground uppercase">
<span>Qty</span>
<NumericInput
value={props.scope.deliveryFeeQuantity}
onUpdate={(val) => {
const qty = val;
const rate = props.scope.deliveryFeeRate || 0;
props.onUpdateScope(props.scope.id, {
deliveryFeeQuantity: qty,
deliveryFee: qty * rate
});
}}
class="w-16 text-right text-sm border-b border-dashed border-border focus:border-primary outline-none bg-transparent"
/>
</div>
<span class="text-xs font-black text-muted-foreground/70">×</span>
<div class="flex flex-col items-end gap-0.5 text-[9px] font-black tracking-[0.3em] text-muted-foreground uppercase">
<span>Rate</span>
<div class="flex items-center gap-1">
<span class="text-[10px] font-black text-muted-foreground">$</span>
<NumericInput
value={props.scope.deliveryFeeRate}
onUpdate={(val) => {
const rate = val;
const qty = props.scope.deliveryFeeQuantity || 0;
props.onUpdateScope(props.scope.id, {
deliveryFeeRate: rate,
deliveryFee: qty * rate
});
}}
class="w-20 text-right text-sm border-b border-dashed border-border focus:border-primary outline-none bg-transparent"
/>
</div>
</div>
</div>
)}
<span class="w-28 text-right text-foreground font-medium">${formatNumber(totals().deliveryTotal)}</span>
</div>
</div> </div>
{/* Scope Notes Section */} {/* Scope Notes Section */}
@@ -1,6 +1,7 @@
import { createSignal } from 'solid-js'; import { createSignal } from 'solid-js';
import { appStore } from '../../store/appStore'; import { appStore } from '../../store/appStore';
import { pb, COLLECTIONS } from '../../utils/db'; import { pb, COLLECTIONS } from '../../utils/db';
import { normalizeScopes } from '../../utils/scope-utils';
export function useEstimateSync( export function useEstimateSync(
setShowSaveModal: (show: boolean) => void setShowSaveModal: (show: boolean) => void
@@ -49,7 +50,7 @@ export function useEstimateSync(
reader.onload = (event) => { reader.onload = (event) => {
try { try {
const data = JSON.parse(event.target?.result as string); const data = JSON.parse(event.target?.result as string);
if (data.scopes) setScopes(data.scopes); if (data.scopes) setScopes(normalizeScopes(data.scopes));
if (data.subScopes) setSubScopes(data.subScopes); if (data.subScopes) setSubScopes(data.subScopes);
if (data.items) setItems(data.items); if (data.items) setItems(data.items);
if (data.clientInfo) setClientInfo(data.clientInfo); if (data.clientInfo) setClientInfo(data.clientInfo);
@@ -206,7 +207,7 @@ export function useEstimateSync(
// snapshot is an entry from the history array // snapshot is an entry from the history array
if (!snapshot) return; if (!snapshot) return;
if (snapshot.scopes) setScopes(snapshot.scopes); if (snapshot.scopes) setScopes(normalizeScopes(snapshot.scopes));
if (snapshot.subScopes) setSubScopes(snapshot.subScopes); if (snapshot.subScopes) setSubScopes(snapshot.subScopes);
if (snapshot.items) setItems(snapshot.items); if (snapshot.items) setItems(snapshot.items);
if (snapshot.clientInfo) setClientInfo(snapshot.clientInfo); if (snapshot.clientInfo) setClientInfo(snapshot.clientInfo);
+4
View File
@@ -3,8 +3,12 @@ export interface Scope {
name: string; name: string;
managementFee: number; managementFee: number;
managementFeeType: 'percent' | 'amount'; managementFeeType: 'percent' | 'amount';
managementFeeQuantity: number;
managementFeeRate: number;
deliveryFee: number; deliveryFee: number;
deliveryFeeType: 'percent' | 'amount'; deliveryFeeType: 'percent' | 'amount';
deliveryFeeQuantity: number;
deliveryFeeRate: number;
useManagementLogic: 'percent' | 'simple'; useManagementLogic: 'percent' | 'simple';
useDeliveryLogic: 'percent' | 'simple'; useDeliveryLogic: 'percent' | 'simple';
estimatorNotes?: string; estimatorNotes?: string;
+13
View File
@@ -0,0 +1,13 @@
import type { Scope } from '../types';
export const normalizeScopes = (scopes: any[]): Scope[] => {
if (!Array.isArray(scopes)) return [];
return scopes.map(scope => ({
...scope,
managementFeeQuantity: scope.managementFeeQuantity ?? 1,
managementFeeRate: scope.managementFeeRate ?? 0,
deliveryFeeQuantity: scope.deliveryFeeQuantity ?? 1,
deliveryFeeRate: scope.deliveryFeeRate ?? 0
}));
};