Fix order item names, add Select All, pay confirmation with total, close empty order
This commit is contained in:
@@ -8,7 +8,7 @@ from database import get_db
|
|||||||
from models.order import Order, OrderItem, OrderWaiter
|
from models.order import Order, OrderItem, OrderWaiter
|
||||||
from models.user import User, AssistantAssignment
|
from models.user import User, AssistantAssignment
|
||||||
from models.product import Product
|
from models.product import Product
|
||||||
from schemas.order import OrderCreate, OrderOut, OrderItemOut, AddItemsRequest, PayItemsRequest, AssignWaiterRequest
|
from schemas.order import OrderCreate, OrderOut, OrderItemOut, AddItemsRequest, PayItemsRequest, AssignWaiterRequest, OrderWaiterOut
|
||||||
from routers.deps import get_current_user, require_manager
|
from routers.deps import get_current_user, require_manager
|
||||||
from services.printer_service import route_and_print
|
from services.printer_service import route_and_print
|
||||||
|
|
||||||
|
|||||||
@@ -15,10 +15,17 @@ class AddItemsRequest(BaseModel):
|
|||||||
items: List[OrderItemInput]
|
items: List[OrderItemInput]
|
||||||
|
|
||||||
|
|
||||||
|
class ProductNameOut(BaseModel):
|
||||||
|
id: int
|
||||||
|
name: str
|
||||||
|
model_config = {"from_attributes": True}
|
||||||
|
|
||||||
|
|
||||||
class OrderItemOut(BaseModel):
|
class OrderItemOut(BaseModel):
|
||||||
id: int
|
id: int
|
||||||
order_id: int
|
order_id: int
|
||||||
product_id: int
|
product_id: int
|
||||||
|
product: Optional[ProductNameOut] = None
|
||||||
added_by: int
|
added_by: int
|
||||||
quantity: int
|
quantity: int
|
||||||
unit_price: float
|
unit_price: float
|
||||||
@@ -44,6 +51,11 @@ class AssignWaiterRequest(BaseModel):
|
|||||||
waiter_id: int
|
waiter_id: int
|
||||||
|
|
||||||
|
|
||||||
|
class OrderWaiterOut(BaseModel):
|
||||||
|
waiter_id: int
|
||||||
|
model_config = {"from_attributes": True}
|
||||||
|
|
||||||
|
|
||||||
class OrderOut(BaseModel):
|
class OrderOut(BaseModel):
|
||||||
id: int
|
id: int
|
||||||
table_id: int
|
table_id: int
|
||||||
@@ -54,5 +66,6 @@ class OrderOut(BaseModel):
|
|||||||
closed_by: Optional[int] = None
|
closed_by: Optional[int] = None
|
||||||
notes: Optional[str] = None
|
notes: Optional[str] = None
|
||||||
items: List[OrderItemOut] = []
|
items: List[OrderItemOut] = []
|
||||||
|
waiters: List[OrderWaiterOut] = []
|
||||||
|
|
||||||
model_config = {"from_attributes": True}
|
model_config = {"from_attributes": True}
|
||||||
|
|||||||
@@ -4,6 +4,10 @@ import OrderSummary from '../components/OrderSummary'
|
|||||||
import useAuthStore from '../store/authStore'
|
import useAuthStore from '../store/authStore'
|
||||||
import client from '../api/client'
|
import client from '../api/client'
|
||||||
|
|
||||||
|
function fmtPrice(v) {
|
||||||
|
return Number(v).toFixed(2) + ' €'
|
||||||
|
}
|
||||||
|
|
||||||
export default function TableDetailPage() {
|
export default function TableDetailPage() {
|
||||||
const { tableId } = useParams()
|
const { tableId } = useParams()
|
||||||
const { user } = useAuthStore()
|
const { user } = useAuthStore()
|
||||||
@@ -15,6 +19,7 @@ export default function TableDetailPage() {
|
|||||||
const [paying, setPaying] = useState(false)
|
const [paying, setPaying] = useState(false)
|
||||||
const [selectedIds, setSelectedIds] = useState([])
|
const [selectedIds, setSelectedIds] = useState([])
|
||||||
const [confirmClose, setConfirmClose] = useState(false)
|
const [confirmClose, setConfirmClose] = useState(false)
|
||||||
|
const [confirmPay, setConfirmPay] = useState(false)
|
||||||
const [error, setError] = useState('')
|
const [error, setError] = useState('')
|
||||||
|
|
||||||
async function load() {
|
async function load() {
|
||||||
@@ -28,7 +33,7 @@ export default function TableDetailPage() {
|
|||||||
} else {
|
} else {
|
||||||
setOrder(null)
|
setOrder(null)
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch {
|
||||||
setError('Σφάλμα φόρτωσης')
|
setError('Σφάλμα φόρτωσης')
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
@@ -37,6 +42,9 @@ export default function TableDetailPage() {
|
|||||||
|
|
||||||
useEffect(() => { load() }, [tableId])
|
useEffect(() => { load() }, [tableId])
|
||||||
|
|
||||||
|
const activeItems = order?.items?.filter(i => i.status === 'active') || []
|
||||||
|
const allPaid = order && activeItems.length === 0
|
||||||
|
|
||||||
const isMyOrder = order && (
|
const isMyOrder = order && (
|
||||||
order.opened_by === user?.id || order.waiters?.some(w => w.waiter_id === user?.id)
|
order.opened_by === user?.id || order.waiters?.some(w => w.waiter_id === user?.id)
|
||||||
)
|
)
|
||||||
@@ -51,8 +59,8 @@ export default function TableDetailPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function paySelected() {
|
async function paySelected() {
|
||||||
if (selectedIds.length === 0) return
|
|
||||||
setPaying(true)
|
setPaying(true)
|
||||||
|
setConfirmPay(false)
|
||||||
try {
|
try {
|
||||||
await client.post(`/api/orders/${order.id}/pay`, { item_ids: selectedIds })
|
await client.post(`/api/orders/${order.id}/pay`, { item_ids: selectedIds })
|
||||||
setSelectedIds([])
|
setSelectedIds([])
|
||||||
@@ -78,8 +86,17 @@ export default function TableDetailPage() {
|
|||||||
setSelectedIds(prev => prev.includes(id) ? prev.filter(x => x !== id) : [...prev, id])
|
setSelectedIds(prev => prev.includes(id) ? prev.filter(x => x !== id) : [...prev, id])
|
||||||
}
|
}
|
||||||
|
|
||||||
const allPaid = order && order.items?.filter(i => i.status === 'active').length === 0
|
function selectAll() {
|
||||||
&& order.items?.some(i => i.status === 'paid')
|
const allActive = activeItems.map(i => i.id)
|
||||||
|
const allSelected = allActive.every(id => selectedIds.includes(id))
|
||||||
|
setSelectedIds(allSelected ? [] : allActive)
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedTotal = activeItems
|
||||||
|
.filter(i => selectedIds.includes(i.id))
|
||||||
|
.reduce((s, i) => s + i.unit_price * i.quantity, 0)
|
||||||
|
|
||||||
|
const allActiveSelected = activeItems.length > 0 && activeItems.every(i => selectedIds.includes(i.id))
|
||||||
|
|
||||||
if (loading) return <div className="page page--centered"><p style={{ color: '#94a3b8' }}>Φόρτωση…</p></div>
|
if (loading) return <div className="page page--centered"><p style={{ color: '#94a3b8' }}>Φόρτωση…</p></div>
|
||||||
|
|
||||||
@@ -111,6 +128,14 @@ export default function TableDetailPage() {
|
|||||||
onToggle={toggleItem}
|
onToggle={toggleItem}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{isMyOrder && activeItems.length > 0 && (
|
||||||
|
<div style={{ padding: '4px 12px 8px' }}>
|
||||||
|
<button className="link-btn" onClick={selectAll} style={{ fontSize: 15 }}>
|
||||||
|
{allActiveSelected ? '☑ Αποεπιλογή όλων' : '☐ Επιλογή όλων'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{isMyOrder && (
|
{isMyOrder && (
|
||||||
<div className="action-bar">
|
<div className="action-bar">
|
||||||
<button className="btn btn--accent" onClick={() => navigate(`/tables/${tableId}/add`)}>
|
<button className="btn btn--accent" onClick={() => navigate(`/tables/${tableId}/add`)}>
|
||||||
@@ -119,7 +144,7 @@ export default function TableDetailPage() {
|
|||||||
|
|
||||||
<button
|
<button
|
||||||
className="btn btn--success"
|
className="btn btn--success"
|
||||||
onClick={paySelected}
|
onClick={() => setConfirmPay(true)}
|
||||||
disabled={selectedIds.length === 0 || paying}
|
disabled={selectedIds.length === 0 || paying}
|
||||||
>
|
>
|
||||||
{paying ? '…' : `Πληρωμή (${selectedIds.length})`}
|
{paying ? '…' : `Πληρωμή (${selectedIds.length})`}
|
||||||
@@ -143,6 +168,31 @@ export default function TableDetailPage() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Pay confirmation */}
|
||||||
|
{confirmPay && (
|
||||||
|
<div className="modal-overlay" onClick={() => setConfirmPay(false)}>
|
||||||
|
<div className="modal-sheet" onClick={e => e.stopPropagation()}>
|
||||||
|
<div className="modal-handle" />
|
||||||
|
<h2 className="modal-title">Επιβεβαίωση πληρωμής</h2>
|
||||||
|
<p style={{ color: '#94a3b8', textAlign: 'center', marginBottom: 4 }}>
|
||||||
|
{selectedIds.length} αντικείμενο{selectedIds.length !== 1 ? 'α' : ''}
|
||||||
|
</p>
|
||||||
|
<p style={{ color: '#f59e0b', textAlign: 'center', fontSize: 28, fontWeight: 700, marginBottom: 24 }}>
|
||||||
|
{fmtPrice(selectedTotal)}
|
||||||
|
</p>
|
||||||
|
<div style={{ display: 'flex', gap: 12 }}>
|
||||||
|
<button className="btn btn--secondary" style={{ flex: 1 }} onClick={() => setConfirmPay(false)}>
|
||||||
|
Άκυρο
|
||||||
|
</button>
|
||||||
|
<button className="btn btn--success" style={{ flex: 1 }} onClick={paySelected}>
|
||||||
|
Πληρώθηκαν ✓
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Close confirmation */}
|
||||||
{confirmClose && (
|
{confirmClose && (
|
||||||
<div className="modal-overlay" onClick={() => setConfirmClose(false)}>
|
<div className="modal-overlay" onClick={() => setConfirmClose(false)}>
|
||||||
<div className="modal-sheet" onClick={e => e.stopPropagation()}>
|
<div className="modal-sheet" onClick={e => e.stopPropagation()}>
|
||||||
|
|||||||
Reference in New Issue
Block a user