Supplier based pricing added
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import type { Component } from 'solid-js';
|
||||
import { createSignal, createMemo, onMount, Show } from 'solid-js';
|
||||
import { createSignal, createMemo, onMount, Show, For } from 'solid-js';
|
||||
import { FileText, ChevronDown, CheckCircle2, X } from 'lucide-solid';
|
||||
import { Portal } from 'solid-js/web';
|
||||
|
||||
@@ -66,6 +66,7 @@ const EstimateBuilder: Component<EstimateBuilderProps> = (props) => {
|
||||
onMount(() => {
|
||||
if (!generalEstimateNotes()) setGeneralEstimateNotes(GENERAL_NOTES_DEFAULT);
|
||||
if (!generalPreNotes()) setGeneralPreNotes(GENERAL_PRE_NOTES_DEFAULT);
|
||||
appStore.loadSuppliers();
|
||||
|
||||
if (items.length === 0 && props.initialItems.length > 0) {
|
||||
const transformed: EstimateItem[] = props.initialItems.map(item => ({
|
||||
@@ -398,6 +399,11 @@ const EstimateBuilder: Component<EstimateBuilderProps> = (props) => {
|
||||
setShowNewEstimateModal(false);
|
||||
}}
|
||||
/>
|
||||
<datalist id="supplier-suggestions">
|
||||
<For each={appStore.suppliers()}>
|
||||
{(sup: string) => <option value={sup} />}
|
||||
</For>
|
||||
</datalist>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -17,6 +17,7 @@ interface ItemRowProps {
|
||||
onToggleSelection: (id: string, isMulti: boolean, isShift: boolean) => void;
|
||||
onDuplicateItem: (id: string) => void;
|
||||
hideColumns?: boolean;
|
||||
activeSupplier?: string;
|
||||
}
|
||||
|
||||
const ItemRow: Component<ItemRowProps> = (props) => {
|
||||
@@ -47,8 +48,15 @@ const ItemRow: Component<ItemRowProps> = (props) => {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const filterParts = [`(name ~ "${searchString}" || category ~ "${searchString}")`];
|
||||
if (props.activeSupplier) {
|
||||
// Assuming exact match or contains? Let's use exact match for supplier
|
||||
filterParts.push(`supplier = "${props.activeSupplier}"`);
|
||||
}
|
||||
const filterQuery = filterParts.join(' && ');
|
||||
|
||||
const records = await pb.collection(COLLECTIONS.STANDARD_PRICES).getList(1, 10, {
|
||||
filter: `name ~ "${searchString}" || category ~ "${searchString}"`,
|
||||
filter: filterQuery,
|
||||
sort: 'category,name'
|
||||
});
|
||||
setSearchResults(records.items as unknown as StandardPrice[]);
|
||||
@@ -162,7 +170,12 @@ const ItemRow: Component<ItemRowProps> = (props) => {
|
||||
>
|
||||
<div class="text-xs font-bold text-gray-900">{result.name}</div>
|
||||
<div class="flex justify-between items-center mt-0.5">
|
||||
<span class="text-[10px] text-gray-500 uppercase font-black tracking-wider">{result.category}</span>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-[10px] text-gray-500 uppercase font-black tracking-wider">{result.category}</span>
|
||||
<Show when={result.supplier}>
|
||||
<span class="text-[9px] px-1.5 py-0.5 rounded-md bg-muted text-muted-foreground uppercase font-black tracking-widest">{result.supplier}</span>
|
||||
</Show>
|
||||
</div>
|
||||
<span class="text-[11px] font-black text-primary">${result.price.toFixed(2)}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -8,6 +8,8 @@ import NoteEditor from './NoteEditor';
|
||||
import LazyItem from './LazyItem';
|
||||
import ScopeScaleWrapper from './ui/ScopeScaleWrapper';
|
||||
import { ModifierRegistry } from '../utils/modifier-registry';
|
||||
import { appStore } from '../store/appStore';
|
||||
import { SupplierDropdown } from './ui/SupplierDropdown';
|
||||
|
||||
interface ScopeCardProps {
|
||||
scope: Scope;
|
||||
@@ -142,12 +144,18 @@ const ScopeCard: Component<ScopeCardProps> = (props) => {
|
||||
onInput={(e) => props.onUpdateScope(props.scope.id, { name: e.currentTarget.value })}
|
||||
class="text-xl font-black text-foreground bg-transparent border-none outline-none focus:ring-0 w-64"
|
||||
/>
|
||||
<div class="flex items-center gap-2 text-xs text-muted-foreground font-bold uppercase tracking-wider">
|
||||
<div class="flex items-center gap-2 text-[10px] text-muted-foreground font-bold uppercase tracking-wider mt-1 h-6">
|
||||
<span>{props.items.length} Items</span>
|
||||
<span>•</span>
|
||||
<span>{formatQuantity(totals().totalQuantity)} Qty</span>
|
||||
<span>•</span>
|
||||
<span class="text-primary">${formatNumber(totals().total)} Total</span>
|
||||
<span>•</span>
|
||||
<SupplierDropdown
|
||||
value={props.scope.supplier || ''}
|
||||
options={appStore.suppliers()}
|
||||
onSelect={(val) => props.onUpdateScope(props.scope.id, { supplier: val })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -255,6 +263,7 @@ const ScopeCard: Component<ScopeCardProps> = (props) => {
|
||||
anySelected={props.selectedIds.length > 0}
|
||||
onToggleSelection={props.onToggleSelection}
|
||||
hideColumns={props.hideColumns}
|
||||
activeSupplier={props.scope.supplier}
|
||||
/>
|
||||
</LazyItem>
|
||||
)}
|
||||
@@ -291,6 +300,7 @@ const ScopeCard: Component<ScopeCardProps> = (props) => {
|
||||
onToggleSelection={props.onToggleSelection}
|
||||
onClearSelection={props.onClearSelection}
|
||||
hideColumns={props.hideColumns}
|
||||
scopeSupplier={props.scope.supplier}
|
||||
/>
|
||||
)}
|
||||
</For>
|
||||
|
||||
@@ -25,6 +25,7 @@ interface SubScopeCardProps {
|
||||
onToggleSelection: (id: string, isMulti: boolean, isShift: boolean) => void;
|
||||
onClearSelection: () => void;
|
||||
hideColumns?: boolean;
|
||||
scopeSupplier?: string;
|
||||
}
|
||||
|
||||
const SubScopeCard: Component<SubScopeCardProps> = (props) => {
|
||||
@@ -218,6 +219,7 @@ const SubScopeCard: Component<SubScopeCardProps> = (props) => {
|
||||
anySelected={props.selectedIds.length > 0}
|
||||
onToggleSelection={props.onToggleSelection}
|
||||
hideColumns={props.hideColumns}
|
||||
activeSupplier={props.scopeSupplier}
|
||||
/>
|
||||
</LazyItem>
|
||||
)}
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
import { createSignal, Show, For, onMount, onCleanup } from 'solid-js';
|
||||
import { Portal } from 'solid-js/web';
|
||||
import { ChevronDown, Check } from 'lucide-solid';
|
||||
|
||||
interface SupplierDropdownProps {
|
||||
value: string;
|
||||
options: string[];
|
||||
onSelect: (value: string) => void;
|
||||
placeholder?: string;
|
||||
}
|
||||
|
||||
export const SupplierDropdown = (props: SupplierDropdownProps) => {
|
||||
const [isOpen, setIsOpen] = createSignal(false);
|
||||
let containerRef: HTMLDivElement | undefined;
|
||||
let buttonRef: HTMLButtonElement | undefined;
|
||||
|
||||
const handleClickOutside = (e: MouseEvent) => {
|
||||
if (containerRef && !containerRef.contains(e.target as Node) &&
|
||||
buttonRef && !buttonRef.contains(e.target as Node)) {
|
||||
setIsOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
onMount(() => {
|
||||
document.addEventListener('click', handleClickOutside);
|
||||
});
|
||||
|
||||
onCleanup(() => {
|
||||
document.removeEventListener('click', handleClickOutside);
|
||||
});
|
||||
|
||||
const getRect = () => buttonRef?.getBoundingClientRect();
|
||||
|
||||
return (
|
||||
<div class="relative inline-block">
|
||||
<button
|
||||
ref={buttonRef}
|
||||
onClick={() => setIsOpen(!isOpen())}
|
||||
class="flex items-center gap-1.5 group/supplier bg-zinc-100/50 hover:bg-zinc-100 border border-zinc-200/50 rounded-full px-3 py-1 transition-all duration-200 focus:outline-none focus:ring-4 focus:ring-primary/10 focus:border-primary/30 active:scale-95"
|
||||
>
|
||||
<span class="text-[9px] font-black uppercase tracking-widest text-muted-foreground/60 select-none">Supplier</span>
|
||||
<span class="text-[9px] font-black uppercase tracking-widest text-foreground min-w-[30px] text-left">
|
||||
{props.value || props.placeholder || 'Any'}
|
||||
</span>
|
||||
<ChevronDown
|
||||
class={`w-2.5 h-2.5 text-muted-foreground transition-transform duration-300 ${isOpen() ? 'rotate-180' : 'group-hover/supplier:translate-y-0.5'}`}
|
||||
/>
|
||||
</button>
|
||||
|
||||
<Show when={isOpen()}>
|
||||
<Portal>
|
||||
<div
|
||||
ref={containerRef}
|
||||
class="fixed z-[1000] min-w-[160px] bg-white/90 backdrop-blur-xl border border-white/20 rounded-2xl shadow-2xl shadow-zinc-900/10 overflow-hidden animate-in fade-in zoom-in-95 duration-200 origin-top"
|
||||
style={{
|
||||
top: `${(getRect()?.bottom || 0) + 8}px`,
|
||||
left: `${getRect()?.left || 0}px`
|
||||
}}
|
||||
>
|
||||
<div class="p-1.5 flex flex-col gap-0.5">
|
||||
<button
|
||||
onClick={() => { props.onSelect(''); setIsOpen(false); }}
|
||||
class={`w-full flex items-center justify-between px-3 py-2 rounded-xl text-[10px] font-black uppercase tracking-widest transition-all ${!props.value ? 'bg-primary text-primary-foreground shadow-lg shadow-primary/20' : 'hover:bg-zinc-100 text-muted-foreground/60'}`}
|
||||
>
|
||||
<span>Any</span>
|
||||
<Show when={!props.value}>
|
||||
<Check class="w-3 h-3" />
|
||||
</Show>
|
||||
</button>
|
||||
<div class="h-px bg-zinc-100 my-0.5 mx-1"></div>
|
||||
<For each={props.options}>
|
||||
{(option) => (
|
||||
<button
|
||||
onClick={() => { props.onSelect(option); setIsOpen(false); }}
|
||||
class={`w-full flex items-center justify-between px-3 py-2 rounded-xl text-[10px] font-black uppercase tracking-widest transition-all ${props.value === option ? 'bg-primary text-primary-foreground shadow-lg shadow-primary/20' : 'hover:bg-zinc-100 text-foreground'}`}
|
||||
>
|
||||
<span>{option}</span>
|
||||
<Show when={props.value === option}>
|
||||
<Check class="w-3 h-3" />
|
||||
</Show>
|
||||
</button>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</div>
|
||||
</Portal>
|
||||
</Show>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -7,6 +7,7 @@ import { ModifierRegistry } from '../utils/modifier-registry';
|
||||
// Raw CSV Data
|
||||
const [activeItems, setActiveItems] = createSignal<ExtractedItem[]>([]);
|
||||
const [activeIgnoredLines, setActiveIgnoredLines] = createSignal<string[][]>([]);
|
||||
const [suppliers, setSuppliers] = createSignal<string[]>([]);
|
||||
|
||||
// Estimate Metadata
|
||||
const [activeEstimateId, setActiveEstimateId] = createSignal<string | null>(null);
|
||||
@@ -44,6 +45,8 @@ const [activeTemplateModifiers, setActiveTemplateModifiers] = createStore<Modifi
|
||||
const [activeTemplateSubScopeName, setActiveTemplateSubScopeName] = createSignal<string | undefined>(undefined);
|
||||
const [activeTemplateId, setActiveTemplateId] = createSignal<string | null>(null);
|
||||
|
||||
import { pb, COLLECTIONS } from '../utils/db';
|
||||
|
||||
export const appStore = {
|
||||
// Raw CSV Extraction
|
||||
items: activeItems,
|
||||
@@ -162,5 +165,19 @@ export const appStore = {
|
||||
setActiveTemplateModifiers([]);
|
||||
setActiveTemplateSubScopeName(undefined);
|
||||
setActiveTemplateId(null);
|
||||
},
|
||||
|
||||
// Suppliers
|
||||
suppliers,
|
||||
async loadSuppliers() {
|
||||
try {
|
||||
const records = await pb.collection(COLLECTIONS.STANDARD_PRICES).getFullList({
|
||||
fields: 'supplier'
|
||||
});
|
||||
const unique = [...new Set(records.map(r => r.supplier).filter(Boolean))].sort() as string[];
|
||||
setSuppliers(unique);
|
||||
} catch (error) {
|
||||
console.error('Error loading suppliers:', error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
+2
-1
@@ -13,6 +13,7 @@ export interface Scope {
|
||||
useDeliveryLogic: 'percent' | 'simple';
|
||||
estimatorNotes?: string;
|
||||
bidDescription?: string;
|
||||
supplier?: string;
|
||||
}
|
||||
|
||||
export type ModifierType = 'tax' | 'man-hours' | 'custom';
|
||||
@@ -81,5 +82,5 @@ export interface StandardPrice {
|
||||
category: string;
|
||||
name: string;
|
||||
price: number;
|
||||
priceRange?: string;
|
||||
supplier?: string;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { createSignal, onMount, For, createMemo } from 'solid-js';
|
||||
import type { Component } from 'solid-js';
|
||||
import { Trash2, Plus, RefreshCw, Edit2, Save, X } from 'lucide-solid';
|
||||
import { Trash2, Plus, RefreshCw, Edit2, Save, X, ChevronDown } from 'lucide-solid';
|
||||
import { pb, COLLECTIONS } from '../utils/db';
|
||||
import type { StandardPrice } from '../types';
|
||||
import { appStore } from '../store/appStore';
|
||||
|
||||
const StandardPricing: Component = () => {
|
||||
const [prices, setPrices] = createSignal<StandardPrice[]>([]);
|
||||
@@ -11,13 +12,13 @@ const StandardPricing: Component = () => {
|
||||
const [newCategory, setNewCategory] = createSignal('');
|
||||
const [newName, setNewName] = createSignal('');
|
||||
const [newPrice, setNewPrice] = createSignal<number>(0);
|
||||
const [newPriceRange, setNewPriceRange] = createSignal('');
|
||||
const [newSupplier, setNewSupplier] = createSignal('');
|
||||
|
||||
const [editingId, setEditingId] = createSignal<string | null>(null);
|
||||
const [editCategory, setEditCategory] = createSignal('');
|
||||
const [editName, setEditName] = createSignal('');
|
||||
const [editPrice, setEditPrice] = createSignal<number>(0);
|
||||
const [editPriceRange, setEditPriceRange] = createSignal('');
|
||||
const [editSupplier, setEditSupplier] = createSignal('');
|
||||
|
||||
const categories = createMemo(() => {
|
||||
return [...new Set(prices().map(p => p.category))].sort();
|
||||
@@ -43,6 +44,7 @@ const StandardPricing: Component = () => {
|
||||
|
||||
onMount(() => {
|
||||
loadPrices();
|
||||
appStore.loadSuppliers();
|
||||
});
|
||||
|
||||
const handleAddPrice = async () => {
|
||||
@@ -53,15 +55,16 @@ const StandardPricing: Component = () => {
|
||||
category: newCategory(),
|
||||
name: newName(),
|
||||
price: newPrice(),
|
||||
priceRange: newPriceRange()
|
||||
supplier: newSupplier()
|
||||
};
|
||||
const added = await pb.collection(COLLECTIONS.STANDARD_PRICES).create(record);
|
||||
setPrices([...prices(), added as unknown as StandardPrice]);
|
||||
appStore.loadSuppliers(); // Refresh global list
|
||||
|
||||
// Reset fields
|
||||
setNewName('');
|
||||
setNewPrice(0);
|
||||
setNewPriceRange('');
|
||||
setNewSupplier('');
|
||||
// Keep category the same to easily add multiple items to same category
|
||||
} catch (error) {
|
||||
console.error('Error adding price:', error);
|
||||
@@ -74,7 +77,7 @@ const StandardPricing: Component = () => {
|
||||
setEditCategory(price.category);
|
||||
setEditName(price.name);
|
||||
setEditPrice(price.price);
|
||||
setEditPriceRange(price.priceRange || '');
|
||||
setEditSupplier(price.supplier || '');
|
||||
};
|
||||
|
||||
const handleEditCancel = () => {
|
||||
@@ -90,11 +93,12 @@ const StandardPricing: Component = () => {
|
||||
category: editCategory(),
|
||||
name: editName(),
|
||||
price: editPrice(),
|
||||
priceRange: editPriceRange()
|
||||
supplier: editSupplier()
|
||||
};
|
||||
await pb.collection(COLLECTIONS.STANDARD_PRICES).update(id, updates);
|
||||
setPrices(prices().map(p => p.id === id ? { ...p, ...updates } : p));
|
||||
setEditingId(null);
|
||||
appStore.loadSuppliers(); // Refresh global list
|
||||
} catch (error) {
|
||||
console.error('Error updating price:', error);
|
||||
alert('Failed to update standard price.');
|
||||
@@ -106,6 +110,7 @@ const StandardPricing: Component = () => {
|
||||
try {
|
||||
await pb.collection(COLLECTIONS.STANDARD_PRICES).delete(id);
|
||||
setPrices(prices().filter(p => p.id !== id));
|
||||
appStore.loadSuppliers(); // Refresh global list
|
||||
} catch (error) {
|
||||
console.error('Error deleting price:', error);
|
||||
alert('Failed to delete standard price.');
|
||||
@@ -135,56 +140,64 @@ const StandardPricing: Component = () => {
|
||||
<h3 class="text-sm font-bold text-blue-800 mb-4 uppercase tracking-wider">Add New Standard Price</h3>
|
||||
<div class="flex items-end gap-4 overflow-x-auto pb-2">
|
||||
<div class="flex-1 min-w-[200px]">
|
||||
<label class="block text-xs font-bold text-gray-600 mb-1">Category</label>
|
||||
<label class="block text-[10px] font-black tracking-widest text-zinc-400 mb-1.5 uppercase pl-1">Category</label>
|
||||
<input
|
||||
type="text"
|
||||
value={newCategory()}
|
||||
onInput={(e) => setNewCategory(e.currentTarget.value)}
|
||||
placeholder="e.g. Drywall"
|
||||
list="category-suggestions"
|
||||
class="w-full bg-white border border-gray-200 rounded-lg px-3 py-2 text-sm focus:border-blue-500 outline-none"
|
||||
class="w-full bg-zinc-50/50 border border-zinc-200/60 rounded-xl px-4 py-2.5 text-sm focus:bg-white focus:ring-4 focus:ring-blue-500/10 focus:border-blue-500 outline-none transition-all duration-200 shadow-sm shadow-zinc-200/50 placeholder:text-zinc-300"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex-1 min-w-[200px]">
|
||||
<label class="block text-xs font-bold text-gray-600 mb-1">Item Name</label>
|
||||
<label class="block text-[10px] font-black tracking-widest text-zinc-400 mb-1.5 uppercase pl-1">Item Name</label>
|
||||
<input
|
||||
type="text"
|
||||
value={newName()}
|
||||
onInput={(e) => setNewName(e.currentTarget.value)}
|
||||
placeholder="e.g. Gyp Type X"
|
||||
list="name-suggestions"
|
||||
class="w-full bg-white border border-gray-200 rounded-lg px-3 py-2 text-sm focus:border-blue-500 outline-none"
|
||||
class="w-full bg-zinc-50/50 border border-zinc-200/60 rounded-xl px-4 py-2.5 text-sm focus:bg-white focus:ring-4 focus:ring-blue-500/10 focus:border-blue-500 outline-none transition-all duration-200 shadow-sm shadow-zinc-200/50 placeholder:text-zinc-300"
|
||||
/>
|
||||
</div>
|
||||
<div class="w-32 shrink-0">
|
||||
<label class="block text-xs font-bold text-gray-600 mb-1">Price ($)</label>
|
||||
<label class="block text-[10px] font-black tracking-widest text-zinc-400 mb-1.5 uppercase pl-1">Price ($)</label>
|
||||
<input
|
||||
type="number"
|
||||
step="0.01"
|
||||
value={newPrice() || ''}
|
||||
onInput={(e) => setNewPrice(parseFloat(e.currentTarget.value) || 0)}
|
||||
placeholder="0.50"
|
||||
class="w-full bg-white border border-gray-200 rounded-lg px-3 py-2 text-sm focus:border-blue-500 outline-none text-right font-bold"
|
||||
placeholder="0.00"
|
||||
class="w-full bg-zinc-50/50 border border-zinc-200/60 rounded-xl px-4 py-2.5 text-sm focus:bg-white focus:ring-4 focus:ring-blue-500/10 focus:border-blue-500 outline-none text-right font-black transition-all duration-200 shadow-sm shadow-zinc-200/50"
|
||||
/>
|
||||
</div>
|
||||
<div class="w-32 shrink-0">
|
||||
<label class="block text-xs font-bold text-gray-600 mb-1">Range (Opt)</label>
|
||||
<input
|
||||
type="text"
|
||||
value={newPriceRange()}
|
||||
onInput={(e) => setNewPriceRange(e.currentTarget.value)}
|
||||
placeholder="0.45-0.80"
|
||||
class="w-full bg-white border border-gray-200 rounded-lg px-3 py-2 text-sm focus:border-blue-500 outline-none"
|
||||
/>
|
||||
<div class="w-40 shrink-0">
|
||||
<label class="block text-[10px] font-black tracking-widest text-zinc-400 mb-1.5 uppercase pl-1">Supplier (Opt)</label>
|
||||
<div class="relative group/sel">
|
||||
<input
|
||||
type="text"
|
||||
value={newSupplier()}
|
||||
onInput={(e) => setNewSupplier(e.currentTarget.value)}
|
||||
placeholder="Any..."
|
||||
list="supplier-suggestions"
|
||||
class="w-full bg-zinc-50/50 border border-zinc-200/60 rounded-xl px-4 py-2.5 text-sm focus:bg-white focus:ring-4 focus:ring-blue-500/10 focus:border-blue-500 outline-none pr-10 cursor-pointer transition-all duration-200 shadow-sm shadow-zinc-200/50 placeholder:text-zinc-300 hover:border-zinc-300"
|
||||
/>
|
||||
<div class="absolute right-3.5 top-1/2 -translate-y-1/2 pointer-events-none text-zinc-300 group-hover/sel:text-blue-500 transition-colors">
|
||||
<ChevronDown class="w-4 h-4" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-end h-[74px]">
|
||||
<button
|
||||
onClick={handleAddPrice}
|
||||
disabled={!newCategory() || !newName() || newPrice() < 0}
|
||||
class="flex items-center gap-2 px-6 py-2.5 bg-blue-600 text-white rounded-xl font-black text-sm hover:bg-blue-700 hover:shadow-xl hover:shadow-blue-500/25 active:scale-95 active:shadow-inner disabled:opacity-30 disabled:pointer-events-none transition-all duration-200 h-[46px] shadow-lg shadow-blue-500/20"
|
||||
>
|
||||
<Plus class="w-4 h-4 transition-transform group-hover:rotate-90" />
|
||||
<span>Add Item</span>
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleAddPrice}
|
||||
disabled={!newCategory() || !newName() || newPrice() < 0}
|
||||
class="flex items-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-lg font-bold hover:bg-blue-700 disabled:opacity-50 transition-colors shrink-0 h-[38px]"
|
||||
>
|
||||
<Plus class="w-4 h-4" />
|
||||
Add
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -206,7 +219,7 @@ const StandardPricing: Component = () => {
|
||||
<th class="py-3 px-4">Category</th>
|
||||
<th class="py-3 px-4">Item Name</th>
|
||||
<th class="py-3 px-4 text-right">Price</th>
|
||||
<th class="py-3 px-4">Range</th>
|
||||
<th class="py-3 px-4">Supplier</th>
|
||||
<th class="py-3 px-4 w-24"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -221,7 +234,7 @@ const StandardPricing: Component = () => {
|
||||
value={editCategory()}
|
||||
onInput={(e) => setEditCategory(e.currentTarget.value)}
|
||||
list="category-suggestions"
|
||||
class="w-full bg-white border border-gray-200 rounded-lg px-2 py-1 text-xs focus:border-blue-500 outline-none"
|
||||
class="w-full bg-white border border-zinc-200/60 rounded-xl px-3 py-1.5 text-xs focus:ring-4 focus:ring-blue-500/10 focus:border-blue-500 outline-none transition-all duration-200 shadow-sm"
|
||||
/>
|
||||
) : price.category}
|
||||
</td>
|
||||
@@ -232,7 +245,7 @@ const StandardPricing: Component = () => {
|
||||
value={editName()}
|
||||
onInput={(e) => setEditName(e.currentTarget.value)}
|
||||
list="name-suggestions"
|
||||
class="w-full bg-white border border-gray-200 rounded-lg px-2 py-1 text-xs focus:border-blue-500 outline-none"
|
||||
class="w-full bg-white border border-zinc-200/60 rounded-xl px-3 py-1.5 text-xs focus:ring-4 focus:ring-blue-500/10 focus:border-blue-500 outline-none transition-all duration-200 shadow-sm"
|
||||
/>
|
||||
) : price.name}
|
||||
</td>
|
||||
@@ -243,19 +256,25 @@ const StandardPricing: Component = () => {
|
||||
step="0.01"
|
||||
value={editPrice()}
|
||||
onInput={(e) => setEditPrice(parseFloat(e.currentTarget.value) || 0)}
|
||||
class="w-24 bg-white border border-gray-200 rounded-lg px-2 py-1 text-xs focus:border-blue-500 outline-none text-right font-bold"
|
||||
class="w-24 bg-white border border-zinc-200/60 rounded-xl px-3 py-1.5 text-xs focus:ring-4 focus:ring-blue-500/10 focus:border-blue-500 outline-none text-right font-black transition-all duration-200 shadow-sm"
|
||||
/>
|
||||
) : `$${price.price.toFixed(2)}`}
|
||||
</td>
|
||||
<td class="py-3 px-4 text-sm text-gray-500">
|
||||
{editingId() === price.id ? (
|
||||
<input
|
||||
type="text"
|
||||
value={editPriceRange()}
|
||||
onInput={(e) => setEditPriceRange(e.currentTarget.value)}
|
||||
class="w-full bg-white border border-gray-200 rounded-lg px-2 py-1 text-xs focus:border-blue-500 outline-none"
|
||||
/>
|
||||
) : (price.priceRange || '-')}
|
||||
<div class="relative group/sel">
|
||||
<input
|
||||
type="text"
|
||||
value={editSupplier()}
|
||||
onInput={(e) => setEditSupplier(e.currentTarget.value)}
|
||||
list="supplier-suggestions"
|
||||
class="w-full bg-white border border-zinc-200/60 rounded-xl px-3 py-1.5 text-xs focus:ring-4 focus:ring-blue-500/10 focus:border-blue-500 outline-none pr-8 cursor-pointer transition-all duration-200 shadow-sm"
|
||||
/>
|
||||
<div class="absolute right-2 top-1/2 -translate-y-1/2 pointer-events-none text-zinc-300 group-hover/sel:text-blue-500 transition-colors">
|
||||
<ChevronDown class="w-3 h-3" />
|
||||
</div>
|
||||
</div>
|
||||
) : (price.supplier || '-')}
|
||||
</td>
|
||||
<td class="py-3 px-4 text-right">
|
||||
<div class="flex items-center justify-end gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
@@ -317,6 +336,11 @@ const StandardPricing: Component = () => {
|
||||
{(name) => <option value={name} />}
|
||||
</For>
|
||||
</datalist>
|
||||
<datalist id="supplier-suggestions">
|
||||
<For each={appStore.suppliers()}>
|
||||
{(sup) => <option value={sup} />}
|
||||
</For>
|
||||
</datalist>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user