management and delivery improvements
This commit is contained in:
@@ -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);
|
||||||
|
|||||||
@@ -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: '',
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
|
||||||
|
}));
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user