Fix order saving, isMyOrder, blocked waiters, options pricing; add preferences, table groups, product images
Backend:
- OrderItemInput accepts option objects {id,name,price_delta} instead of int IDs
- extra_cost from selected options added to unit_price snapshot
- GET /api/products/?all=true for manager (includes unavailable)
- PUT /api/products/{id} now replaces options, ingredients, preference_sets
- POST /api/products/{id}/image — persistent image upload to /app/data/product_images
- New models: ProductPreferenceSet, ProductPreferenceChoice, TableGroup
- tables: group_id FK, hard delete (?hard=true), batch create POST /api/tables/batch
- GET /api/tables/groups + POST/PUT/DELETE groups endpoints
- POST /api/auth/me endpoint for token rehydration
- Auto-migration on startup for new columns
PWA:
- AuthRehydrator: fetches /auth/me on load so isMyOrder works after page reload
- 401 response force-logs out (covers blocked waiters)
- ItemOptionsModal: uses extra_cost correctly, shows preferences as radio buttons
Manager:
- ProductsPage: shows unavailable products greyed out, category color picker + reorder,
full option/ingredient/preference editing, image upload
- TablesPage: table groups, auto-increment, deactivate vs hard delete, batch add
This commit is contained in:
@@ -8,12 +8,21 @@ export default function ItemOptionsModal({ product, onAdd, onClose }) {
|
||||
|
||||
const options = product.options || []
|
||||
const ingredients = product.ingredients || []
|
||||
const preferenceSets = product.preference_sets || []
|
||||
|
||||
const [selectedPreferences, setSelectedPreferences] = useState(
|
||||
Object.fromEntries(preferenceSets.map(ps => [ps.id, null]))
|
||||
)
|
||||
|
||||
function selectPreference(setId, choice) {
|
||||
setSelectedPreferences(prev => ({ ...prev, [setId]: choice }))
|
||||
}
|
||||
|
||||
function toggleOption(opt) {
|
||||
setSelectedOptions(prev => {
|
||||
const exists = prev.find(o => o.id === opt.id)
|
||||
if (exists) return prev.filter(o => o.id !== opt.id)
|
||||
return [...prev, { id: opt.id, name: opt.name, price_delta: opt.price_delta }]
|
||||
return [...prev, { id: opt.id, name: opt.name, price_delta: opt.extra_cost ?? 0 }]
|
||||
})
|
||||
}
|
||||
|
||||
@@ -23,11 +32,21 @@ export default function ItemOptionsModal({ product, onAdd, onClose }) {
|
||||
)
|
||||
}
|
||||
|
||||
const extraPrice = selectedOptions.reduce((s, o) => s + (o.price_delta || 0), 0)
|
||||
const prefExtra = Object.values(selectedPreferences).reduce((s, ch) => s + (ch?.extra_cost ?? 0), 0)
|
||||
const extraPrice = selectedOptions.reduce((s, o) => s + (o.price_delta ?? o.extra_cost ?? 0), 0) + prefExtra
|
||||
const totalPrice = (product.base_price + extraPrice) * quantity
|
||||
|
||||
function handleAdd() {
|
||||
onAdd({ product_id: product.id, quantity, selected_options: selectedOptions, removed_ingredients: removedIngredients, notes })
|
||||
const prefChoices = Object.values(selectedPreferences)
|
||||
.filter(Boolean)
|
||||
.map(ch => ({ id: ch.id, name: ch.name, price_delta: ch.extra_cost ?? 0 }))
|
||||
onAdd({
|
||||
product_id: product.id,
|
||||
quantity,
|
||||
selected_options: [...selectedOptions, ...prefChoices],
|
||||
removed_ingredients: removedIngredients,
|
||||
notes,
|
||||
})
|
||||
onClose()
|
||||
}
|
||||
|
||||
@@ -49,12 +68,30 @@ export default function ItemOptionsModal({ product, onAdd, onClose }) {
|
||||
onChange={() => toggleOption(opt)}
|
||||
/>
|
||||
<span>{opt.name}</span>
|
||||
{opt.price_delta > 0 && <span className="option-price">+{Number(opt.price_delta).toFixed(2)} €</span>}
|
||||
{(opt.extra_cost ?? 0) !== 0 && <span className="option-price">{(opt.extra_cost ?? 0) > 0 ? '+' : ''}{Number(opt.extra_cost).toFixed(2)} €</span>}
|
||||
</label>
|
||||
))}
|
||||
</section>
|
||||
)}
|
||||
|
||||
{preferenceSets.map(ps => (
|
||||
<section key={ps.id} className="modal-section">
|
||||
<h3>{ps.name}</h3>
|
||||
{ps.choices.map(ch => (
|
||||
<label key={ch.id} className="modal-option">
|
||||
<input
|
||||
type="radio"
|
||||
name={`pref-${ps.id}`}
|
||||
checked={selectedPreferences[ps.id]?.id === ch.id}
|
||||
onChange={() => selectPreference(ps.id, ch)}
|
||||
/>
|
||||
<span>{ch.name}</span>
|
||||
{(ch.extra_cost ?? 0) !== 0 && <span className="option-price">{(ch.extra_cost ?? 0) > 0 ? '+' : ''}{Number(ch.extra_cost).toFixed(2)} €</span>}
|
||||
</label>
|
||||
))}
|
||||
</section>
|
||||
))}
|
||||
|
||||
{ingredients.length > 0 && (
|
||||
<section className="modal-section">
|
||||
<h3>Αφαίρεση υλικών</h3>
|
||||
|
||||
Reference in New Issue
Block a user