@@ -0,0 +1,296 @@
import { useState } from 'react'
import { useQuery , useMutation , useQueryClient } from '@tanstack/react-query'
import toast from 'react-hot-toast'
import client from '../api/client'
// ── Constants ─────────────────────────────────────────────────────────────────
const REASONS = [
{ id : 'kitchen_error' , label : 'Λάθος κουζίνας' , color : '#dc2626' } ,
{ id : 'dropped' , label : 'Έπεσε' , color : '#d97706' } ,
{ id : 'expired' , label : 'Έληξε' , color : '#7c3aed' } ,
{ id : 'other' , label : 'Άλλο' , color : '#6b7280' } ,
]
// ── Helpers ───────────────────────────────────────────────────────────────────
function fmt ( n ) {
if ( n == null ) return '—'
return n . toLocaleString ( 'el-GR' , { minimumFractionDigits : 2 , maximumFractionDigits : 2 } ) + ' €'
}
function fmtDateTime ( iso ) {
if ( ! iso ) return '—'
return new Date ( iso ) . toLocaleString ( 'el-GR' , { day : '2-digit' , month : '2-digit' , hour : '2-digit' , minute : '2-digit' } )
}
// ── Log form ──────────────────────────────────────────────────────────────────
function LogForm ( { products , onLog , isPending } ) {
const [ productId , setProductId ] = useState ( '' )
const [ qty , setQty ] = useState ( '1' )
const [ reason , setReason ] = useState ( 'kitchen_error' )
const [ notes , setNotes ] = useState ( '' )
const canLog = productId && parseFloat ( qty ) > 0
function submit ( ) {
if ( ! canLog ) return
onLog ( {
product _id : Number ( productId ) ,
quantity : parseFloat ( qty ) ,
reason ,
reason _notes : notes . trim ( ) || null ,
} )
setProductId ( '' )
setQty ( '1' )
setReason ( 'kitchen_error' )
setNotes ( '' )
}
const inputStyle = {
width : '100%' , padding : '8px 11px' , border : '1px solid #e5e7eb' ,
borderRadius : 8 , fontSize : 13.5 , outline : 'none' , fontFamily : 'inherit' , background : 'white' ,
boxSizing : 'border-box' ,
}
return (
< div style = { { border : '1.5px dashed #d1d5db' , borderRadius : 14 , padding : '16px 20px' , background : '#fafafa' } } >
< div style = { { fontSize : 12 , fontWeight : 700 , color : '#6b7280' , textTransform : 'uppercase' , letterSpacing : '0.05em' , marginBottom : 14 } } >
Καταγραφή Απόβλητου
< / div >
< div style = { { display : 'grid' , gridTemplateColumns : '2fr 1fr' , gap : 10 , marginBottom : 10 } } >
< div >
< label style = { { fontSize : 11.5 , fontWeight : 600 , color : '#374151' , display : 'block' , marginBottom : 4 } } > Προϊόν * < / label >
< select style = { inputStyle } value = { productId } onChange = { e => setProductId ( e . target . value ) } autoFocus >
< option value = "" > — Επιλέξτε προϊόν — < / option >
{ products . map ( p => (
< option key = { p . id } value = { p . id } > { p . name } { p . category _name ? ` ( ${ p . category _name } ) ` : '' } < / option >
) ) }
< / select >
< / div >
< div >
< label style = { { fontSize : 11.5 , fontWeight : 600 , color : '#374151' , display : 'block' , marginBottom : 4 } } > Ποσότητα * < / label >
< input type = "number" step = "0.5" min = "0.1"
style = { inputStyle } value = { qty } onChange = { e => setQty ( e . target . value ) } / >
< / div >
< / div >
< div style = { { marginBottom : 10 } } >
< label style = { { fontSize : 11.5 , fontWeight : 600 , color : '#374151' , display : 'block' , marginBottom : 6 } } > Αιτία < / label >
< div style = { { display : 'flex' , gap : 6 , flexWrap : 'wrap' } } >
{ REASONS . map ( r => (
< button key = { r . id } type = "button" onClick = { ( ) => setReason ( r . id ) }
style = { {
padding : '5px 12px' , borderRadius : 20 , fontSize : 12.5 , fontWeight : 600 , cursor : 'pointer' ,
border : ` 1.5px solid ${ reason === r . id ? r . color : '#e5e7eb' } ` ,
background : reason === r . id ? r . color : 'white' ,
color : reason === r . id ? 'white' : '#6b7280' ,
} } >
{ r . label }
< / button >
) ) }
< / div >
< / div >
< div style = { { marginBottom : 14 } } >
< label style = { { fontSize : 11.5 , fontWeight : 600 , color : '#374151' , display : 'block' , marginBottom : 4 } } >
Σημείωση < span style = { { fontWeight : 400 , color : '#9ca3af' } } > ( προαιρετική ) < / span >
< / label >
< input style = { inputStyle } value = { notes } onChange = { e => setNotes ( e . target . value ) }
placeholder = "π.χ. Ο πελάτης επέστρεψε ελαττωματικό…" / >
< / div >
< button onClick = { submit } disabled = { ! canLog || isPending }
style = { {
padding : '9px 22px' , borderRadius : 9 , border : 'none' , fontSize : 13.5 , fontWeight : 700 , cursor : canLog ? 'pointer' : 'default' ,
background : canLog ? '#111827' : '#e5e7eb' , color : canLog ? 'white' : '#9ca3af' ,
} } >
{ isPending ? 'Καταγραφή…' : '+ Καταγραφή Απόβλητου' }
< / button >
< / div >
)
}
// ── Waste entry row ───────────────────────────────────────────────────────────
function WasteRow ( { entry , canDelete , onDelete } ) {
const reasonCfg = REASONS . find ( r => r . id === entry . reason ) || { label : entry . reason , color : '#6b7280' }
return (
< div style = { { display : 'flex' , alignItems : 'center' , gap : 12 , padding : '11px 16px' , border : '1px solid #f0f0ef' , borderRadius : 10 , background : 'white' } } >
< div style = { { flex : 1 , minWidth : 0 } } >
< div style = { { fontSize : 14 , fontWeight : 600 , color : '#111315' , display : 'flex' , alignItems : 'center' , gap : 8 } } >
{ entry . product _name }
< span style = { { fontSize : 13 , fontWeight : 700 , color : '#374151' } } > × { entry . quantity } < / span >
< span style = { { fontSize : 11 , fontWeight : 700 , padding : '1px 7px' , borderRadius : 99 , background : ` ${ reasonCfg . color } 18 ` , color : reasonCfg . color } } >
{ reasonCfg . label }
< / span >
< / div >
< div style = { { fontSize : 12 , color : '#9ca3af' , marginTop : 2 } } >
{ fmtDateTime ( entry . logged _at ) } · { entry . logged _by _name }
{ entry . reason _notes && < span style = { { color : '#6b7280' , marginLeft : 8 } } > — { entry . reason _notes } < / span > }
< / div >
< / div >
< div style = { { textAlign : 'right' , flexShrink : 0 } } >
{ entry . total _cost != null ? (
< div style = { { fontSize : 13.5 , fontWeight : 700 , color : '#dc2626' } } > { fmt ( entry . total _cost ) } < / div >
) : (
< div style = { { fontSize : 12 , color : '#d1d5db' } } > χωρίς κόστος < / div >
) }
{ entry . unit _cost _snapshot != null && (
< div style = { { fontSize : 11 , color : '#9ca3af' } } > { fmt ( entry . unit _cost _snapshot ) } / τεμ . < / div >
) }
< / div >
{ canDelete && (
< button onClick = { ( ) => { if ( window . confirm ( 'Διαγραφή καταχώρησης;' ) ) onDelete ( entry . id ) } }
style = { { padding : '4px 8px' , borderRadius : 6 , border : '1px solid #fecaca' , background : 'white' , color : '#ef4444' , fontSize : 12 , cursor : 'pointer' , flexShrink : 0 } } >
✕
< / button >
) }
< / div >
)
}
// ── Main page ─────────────────────────────────────────────────────────────────
export default function WastePage ( ) {
const qc = useQueryClient ( )
const [ showHistory , setShowHistory ] = useState ( false )
const [ histFrom , setHistFrom ] = useState ( ( ) => new Date ( ) . toISOString ( ) . slice ( 0 , 10 ) )
const [ histTo , setHistTo ] = useState ( ( ) => new Date ( ) . toISOString ( ) . slice ( 0 , 10 ) )
const { data : productsData = [ ] } = useQuery ( {
queryKey : [ 'meta-products' ] ,
queryFn : ( ) => client . get ( '/api/reports/meta/products' ) . then ( r => r . data . products ) ,
staleTime : 5 * 60 _000 ,
} )
const { data : bdData } = useQuery ( {
queryKey : [ 'business-day' ] ,
queryFn : ( ) => client . get ( '/api/business-day/current' ) . then ( r => r . data ) ,
staleTime : 30 _000 ,
} )
const activeDayId = bdData ? . id ? ? null
// Today's entries — scoped to active business day (or all if no active day)
const { data : todayData } = useQuery ( {
queryKey : [ 'waste-today' , activeDayId ] ,
queryFn : ( ) => client . get ( '/api/waste/' , { params : activeDayId ? { business _day _id : activeDayId } : { } } ) . then ( r => r . data ) ,
staleTime : 15 _000 ,
} )
// Historical entries
const { data : histData , refetch : refetchHist } = useQuery ( {
queryKey : [ 'waste-history' , histFrom , histTo ] ,
queryFn : ( ) => client . get ( '/api/waste/' , { params : { from : histFrom + 'T00:00:00' , to : histTo + 'T23:59:59' } } ) . then ( r => r . data ) ,
staleTime : 30 _000 ,
enabled : showHistory ,
} )
const todayEntries = todayData ? . entries || [ ]
const histEntries = histData ? . entries || [ ]
const todayCost = todayEntries . reduce ( ( s , e ) => s + ( e . total _cost || 0 ) , 0 )
const todayCount = todayEntries . length
const logWaste = useMutation ( {
mutationFn : ( body ) => client . post ( '/api/waste/' , body ) ,
onSuccess : ( ) => {
qc . invalidateQueries ( { queryKey : [ 'waste-today' ] } )
qc . invalidateQueries ( { queryKey : [ 'waste-history' ] } )
toast . success ( 'Καταγράφηκε' )
} ,
onError : ( e ) => toast . error ( e ? . response ? . data ? . detail || 'Σφάλμα' ) ,
} )
const deleteWaste = useMutation ( {
mutationFn : ( id ) => client . delete ( ` /api/waste/ ${ id } ` ) ,
onSuccess : ( ) => {
qc . invalidateQueries ( { queryKey : [ 'waste-today' ] } )
qc . invalidateQueries ( { queryKey : [ 'waste-history' ] } )
toast . success ( 'Διαγράφηκε' )
} ,
onError : ( e ) => toast . error ( e ? . response ? . data ? . detail || 'Σφάλμα' ) ,
} )
return (
< div style = { { display : 'flex' , flexDirection : 'column' , height : '100%' , overflow : 'hidden' } } >
{ /* Header */ }
< div style = { { padding : '18px 28px 14px' , borderBottom : '1px solid #f0f0ef' , flexShrink : 0 , background : 'white' , display : 'flex' , alignItems : 'center' , justifyContent : 'space-between' } } >
< div >
< h1 style = { { margin : 0 , fontSize : 20 , fontWeight : 800 , color : '#111315' } } > Αποβλήτα / Φθορές < / h1 >
< p style = { { margin : '2px 0 0' , fontSize : 13 , color : '#9ca3af' } } >
Σήμερα : { todayCount } καταχωρήσεις
{ todayCost > 0 && < span style = { { color : '#dc2626' , fontWeight : 600 } } > · εκτιμώμενο κόστος { todayCost . toLocaleString ( 'el-GR' , { minimumFractionDigits : 2 , maximumFractionDigits : 2 } ) } € < / span > }
< / p >
< / div >
< / div >
< div style = { { flex : 1 , overflowY : 'auto' , padding : '16px 28px' , display : 'flex' , flexDirection : 'column' , gap : 16 } } >
{ /* Log form */ }
< LogForm products = { productsData } onLog = { body => logWaste . mutate ( body ) } isPending = { logWaste . isPending } / >
{ /* Today's entries */ }
< div >
< div style = { { fontSize : 11 , fontWeight : 700 , color : '#9ca3af' , textTransform : 'uppercase' , letterSpacing : '0.05em' , marginBottom : 8 } } >
{ activeDayId ? 'Σήμερα' : 'Τελευταίες καταχωρήσεις' }
< / div >
{ todayEntries . length === 0 ? (
< div style = { { textAlign : 'center' , color : '#d1d5db' , fontSize : 13 , padding : '20px 0' } } > Καμία καταχώρηση ακόμα . < / div >
) : (
< div style = { { display : 'flex' , flexDirection : 'column' , gap : 6 } } >
{ todayEntries . map ( e => (
< WasteRow key = { e . id } entry = { e } canDelete = { ! ! activeDayId } onDelete = { id => deleteWaste . mutate ( id ) } / >
) ) }
< / div >
) }
< / div >
{ /* History toggle */ }
< div style = { { borderTop : '1px solid #f0f0ef' , paddingTop : 12 } } >
< button onClick = { ( ) => setShowHistory ( s => ! s ) }
style = { { fontSize : 13 , fontWeight : 600 , color : '#6b7280' , background : 'none' , border : 'none' , cursor : 'pointer' , padding : 0 } } >
{ showHistory ? '▾ Απόκρυψη ιστορικού' : '▸ Εμφάνιση ιστορικού' }
< / button >
{ showHistory && (
< div style = { { marginTop : 12 , display : 'flex' , flexDirection : 'column' , gap : 10 } } >
< div style = { { display : 'flex' , gap : 10 , alignItems : 'center' , flexWrap : 'wrap' } } >
< div style = { { display : 'flex' , alignItems : 'center' , gap : 6 } } >
< label style = { { fontSize : 12 , color : '#6b7280' , fontWeight : 600 } } > Από < / label >
< input type = "date" value = { histFrom } onChange = { e => setHistFrom ( e . target . value ) }
style = { { padding : '5px 8px' , border : '1px solid #e5e7eb' , borderRadius : 7 , fontSize : 13 , outline : 'none' } } / >
< / div >
< div style = { { display : 'flex' , alignItems : 'center' , gap : 6 } } >
< label style = { { fontSize : 12 , color : '#6b7280' , fontWeight : 600 } } > Έως < / label >
< input type = "date" value = { histTo } onChange = { e => setHistTo ( e . target . value ) }
style = { { padding : '5px 8px' , border : '1px solid #e5e7eb' , borderRadius : 7 , fontSize : 13 , outline : 'none' } } / >
< / div >
< button onClick = { ( ) => refetchHist ( ) }
style = { { padding : '5px 14px' , borderRadius : 7 , border : '1px solid #e5e7eb' , background : 'white' , fontSize : 12.5 , cursor : 'pointer' , color : '#374151' , fontWeight : 500 } } >
Αναζήτηση
< / button >
< / div >
{ histEntries . length === 0 ? (
< div style = { { color : '#d1d5db' , fontSize : 13 , padding : '12px 0' } } > Δεν βρέθηκαν καταχωρήσεις . < / div >
) : (
< div style = { { display : 'flex' , flexDirection : 'column' , gap : 6 } } >
{ histEntries . map ( e => (
< WasteRow key = { e . id } entry = { e } canDelete = { false } onDelete = { ( ) => { } } / >
) ) }
< div style = { { fontSize : 12.5 , color : '#6b7280' , fontWeight : 600 , paddingLeft : 4 } } >
{ histEntries . length } καταχωρήσεις · εκτιμώμενο κόστος { ' ' }
< span style = { { color : '#dc2626' } } >
{ histEntries . reduce ( ( s , e ) => s + ( e . total _cost || 0 ) , 0 ) . toLocaleString ( 'el-GR' , { minimumFractionDigits : 2 } ) } €
< / span >
< / div >
< / div >
) }
< / div >
) }
< / div >
< / div >
< / div >
)
}